06939551453c49ea51f705a623e84cfa37bf6832
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
58a3e70a63 |
feat: file blocks (#10)
Phase 3, part 2 (`docs/design.md`, "Next"): file blocks. Moderators block a file network-wide by its sha256, so a removed product can't come back under another name or catalog.
**Blocking**
- A file's page (Console → Blocks → a file) shows whether it's blocked and the products using it, so a moderator sees what a block removes before confirming ("Block and remove 2 products").
- Moderators reach it from a product's files (each has a Block link for them) or by pasting a sha256 into the Blocks tab, which also lists blocked files.
- Blocking a file:
- removes every product with a revision using it. Each removal is audited as `product.remove` with the file, so creators are notified and can appeal it as before.
- puts a `{type: "block", sha256}` change in the changes feed of every catalog hosting it, meaning: delete it whatever uses it. (t5de-catalog doesn't read the feed yet.)
- refuses uploads with it from then on (409 `blocked`). Each attempt is audited as `file.blocked_upload` with the person and catalog, so moderators see who tried.
- Unblocking lets the file in again. The products it removed stay removed until a moderator restores them, one by one.
- Who: `product:remove` hub-wide (moderators and admins). The Blocks tab shows only for them.
**API**: `GET /api/v1/blocks`, and `GET`/`PUT`/`DELETE /api/v1/blocks/:sha256` (`docs/products.md`).
**Migration** `0013_file_blocks`: the `file_block` table, and a GIN index on revisions' file lists, so finding the products using a file doesn't scan every revision.
**Checks**
- 53 API tests pass. The new `blocks.test.ts` covers:
- who may block
- the preview of what a block removes
- removal of every product using the file, across catalogs and creators
- the changes feed, notifications and appeals
- refused uploads and their audit entry
- unblocking, and restoring a removed product afterwards
- Type checks pass.
- On a local hub, through the website's own forms:
- the Block link on a product's files, for moderators only
- the Blocks tab, for moderators only (404 for the creator)
- the file's page, then blocking: the product is gone, its creator notified and offered Appeal
- a re-upload through the catalog API: refused, and audited
- unblocking, then Restore
- Fixed along the way: an empty reason gave a generic "Some of that isn't right"; it now says "Give a reason."
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Phase 3, part 2 (`docs/design.md`, "Next"): file blocks. Moderators block a file network-wide by its sha256, so a removed product can't come back under another name or catalog.
**Blocking**
- A file's page (Console → Blocks → a file) shows whether it's blocked and the products using it, so a moderator sees what a block removes before confirming ("Block and remove 2 products").
- Moderators reach it from a product's files (each has a Block link for them) or by pasting a sha256 into the Blocks tab, which also lists blocked files.
- Blocking a file:
- removes every product with a revision using it. Each removal is audited as `product.remove` with the file, so creators are notified and can appeal it as before.
- puts a `{type: "block", sha256}` change in the changes feed of every catalog hosting it, meaning: delete it whatever uses it. (t5de-catalog doesn't read the feed yet.)
- refuses uploads with it from then on (409 `blocked`). Each attempt is audited as `file.blocked_upload` with the person and catalog, so moderators see who tried.
- Unblocking lets the file in again. The products it removed stay removed until a moderator restores them, one by one.
- Who: `product:remove` hub-wide (moderators and admins). The Blocks tab shows only for them.
**API**: `GET /api/v1/blocks`, and `GET`/`PUT`/`DELETE /api/v1/blocks/:sha256` (`docs/products.md`).
**Migration** `0013_file_blocks`: the `file_block` table, and a GIN index on revisions' file lists, so finding the products using a file doesn't scan every revision.
**Checks**
- 53 API tests pass. The new `blocks.test.ts` covers:
- who may block
- the preview of what a block removes
- removal of every product using the file, across catalogs and creators
- the changes feed, notifications and appeals
- refused uploads and their audit entry
- unblocking, and restoring a removed product afterwards
- Type checks pass.
- On a local hub, through the website's own forms:
- the Block link on a product's files, for moderators only
- the Blocks tab, for moderators only (404 for the creator)
- the file's page, then blocking: the product is gone, its creator notified and offered Appeal
- a re-upload through the catalog API: refused, and audited
- unblocking, then Restore
- Fixed along the way: an empty reason gave a generic "Some of that isn't right"; it now says "Give a reason."
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: dhkatz <[email protected]>
|
||
|
|
7299e10b08 |
feat: cases (reports, reviews, requests, appeals) and notifications (#9)
Phase 3, part 1 (`docs/design.md`, "Next"): cases and notifications. File blocks, API tokens and two-factor sign-in come as separate PRs.
**Cases**
- One queue (Console → Cases) for everything someone has to decide. Each case has:
- a thread of notes, where internal notes are visible only to those deciding
- an assignee (moderators assign cases to moderators)
- a decision, Resolve or Decline, with a written resolution that the person who opened it sees
- **Reports:** anyone signed in can report a product or catalog from its page (the API also takes rooms and people).
- Limits: 10 an hour, one open report per person and thing, and you can't report yourself.
- Reports about a product go to the teams of the catalogs hosting it, as well as to moderators.
- Only the reporter and moderators know who reported. The audit entry for a report isn't tagged with what it's about, so a product's creator can't find out from their audit log either.
- **Rating reviews:** a creator lowering a rating from adult to general now opens a review instead of being refused. The rest of the edit still saves. Resolving the review lowers the rating, recorded in the product's history.
- **Removal requests:** a catalog's team can ask moderators to remove a product it hosts, everywhere. Resolving it removes the product.
- **Deletion requests:** a catalog's owner asks for deletion from the catalog's console page, and a moderator decides, as agreed. Resolving it marks the catalog `deleted`: its key stops working, nothing is available from it, and its changes feed says so.
- **Appeals:**
- From the audit log, the person an action affected can appeal it, once: their product removed, delisted or edited by someone else; their catalog suspended or rejected; a ban from a catalog or a declined request to join.
- Another moderator decides. An admin may decide an appeal of their own action, and the audit log flags it.
- Resolving records the decision; the moderator then undoes the action where it's done. For that, there's now a Restore for removed products (Console → Products, for moderators); the design mentioned one, but it was never built.
- The console shows a "Cases" tab, and a to-do with the number of open cases the person could decide.
**Notifications**
- A bell in the header with the unread count, and a notifications page with "Mark all as read".
- Each is also sent to the person's proxies over the realtime connection (`{"type": "notification"}`), so the client can show them later.
- They cover:
- a moderator edited or locked your product, or undid a change (with the reason)
- your product was removed or restored
- your case was decided
- someone replied on your case
- a catalog decided your membership
**Also changed**
- A product's creator and moderators can now see it when it's adult, even without an adult account. Before, a creator who made their own product adult couldn't open its page any more, and neither could moderators. Getting and wearing adult products still need an adult account.
**Not done yet** (in `docs/console.md`): email notifications (the hub sends no email yet), canned reasons, private moderator notes on people and products, and second approval for the most destructive actions.
**Migration** `0012_cases`: the `moderation_case`, `case_note` and `notification` tables. (`case` is a reserved word in SQL, hence the table's name.)
**Checks**
- 51 API tests pass. The 7 in the new `cases.test.ts` cover:
- who sees and decides each kind of case, and that the reporter stays private
- rate limits and duplicate reports
- the thread and internal notes, and assigning
- what deciding each kind of case does
- appeals by the person affected only, decided by another moderator
- restoring a removed product
- the notifications
- Type checks pass.
- On a local hub I went through every flow with the website's own forms: report from a product page; the catalog owner's note and decision; assigning, an internal note and a decision as admin; removal by a moderator, the creator's appeal from the audit log, the admin's decision and Restore; a creator's rating review and the decision lowering the rating; a deletion request and its decline; the notifications, the bell's count, and Mark all as read.
- Fixed along the way:
- The catalog's team could see who reported.
- A missing Resolve/Decline choice was treated as Decline; now it's refused.
- The Unassign button sent the typed name instead of nothing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Phase 3, part 1 (`docs/design.md`, "Next"): cases and notifications. File blocks, API tokens and two-factor sign-in come as separate PRs.
**Cases**
- One queue (Console → Cases) for everything someone has to decide. Each case has:
- a thread of notes, where internal notes are visible only to those deciding
- an assignee (moderators assign cases to moderators)
- a decision, Resolve or Decline, with a written resolution that the person who opened it sees
- **Reports:** anyone signed in can report a product or catalog from its page (the API also takes rooms and people).
- Limits: 10 an hour, one open report per person and thing, and you can't report yourself.
- Reports about a product go to the teams of the catalogs hosting it, as well as to moderators.
- Only the reporter and moderators know who reported. The audit entry for a report isn't tagged with what it's about, so a product's creator can't find out from their audit log either.
- **Rating reviews:** a creator lowering a rating from adult to general now opens a review instead of being refused. The rest of the edit still saves. Resolving the review lowers the rating, recorded in the product's history.
- **Removal requests:** a catalog's team can ask moderators to remove a product it hosts, everywhere. Resolving it removes the product.
- **Deletion requests:** a catalog's owner asks for deletion from the catalog's console page, and a moderator decides, as agreed. Resolving it marks the catalog `deleted`: its key stops working, nothing is available from it, and its changes feed says so.
- **Appeals:**
- From the audit log, the person an action affected can appeal it, once: their product removed, delisted or edited by someone else; their catalog suspended or rejected; a ban from a catalog or a declined request to join.
- Another moderator decides. An admin may decide an appeal of their own action, and the audit log flags it.
- Resolving records the decision; the moderator then undoes the action where it's done. For that, there's now a Restore for removed products (Console → Products, for moderators); the design mentioned one, but it was never built.
- The console shows a "Cases" tab, and a to-do with the number of open cases the person could decide.
**Notifications**
- A bell in the header with the unread count, and a notifications page with "Mark all as read".
- Each is also sent to the person's proxies over the realtime connection (`{"type": "notification"}`), so the client can show them later.
- They cover:
- a moderator edited or locked your product, or undid a change (with the reason)
- your product was removed or restored
- your case was decided
- someone replied on your case
- a catalog decided your membership
**Also changed**
- A product's creator and moderators can now see it when it's adult, even without an adult account. Before, a creator who made their own product adult couldn't open its page any more, and neither could moderators. Getting and wearing adult products still need an adult account.
**Not done yet** (in `docs/console.md`): email notifications (the hub sends no email yet), canned reasons, private moderator notes on people and products, and second approval for the most destructive actions.
**Migration** `0012_cases`: the `moderation_case`, `case_note` and `notification` tables. (`case` is a reserved word in SQL, hence the table's name.)
**Checks**
- 51 API tests pass. The 7 in the new `cases.test.ts` cover:
- who sees and decides each kind of case, and that the reporter stays private
- rate limits and duplicate reports
- the thread and internal notes, and assigning
- what deciding each kind of case does
- appeals by the person affected only, decided by another moderator
- restoring a removed product
- the notifications
- Type checks pass.
- On a local hub I went through every flow with the website's own forms: report from a product page; the catalog owner's note and decision; assigning, an internal note and a decision as admin; removal by a moderator, the creator's appeal from the audit log, the admin's decision and Restore; a creator's rating review and the decision lowering the rating; a deletion request and its decline; the notifications, the bell's count, and Mark all as read.
- Fixed along the way:
- The catalog's team could see who reported.
- A missing Resolve/Decline choice was treated as Decline; now it's refused.
- The Unassign button sent the typed name instead of nothing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: dhkatz <[email protected]>
|
||
|
|
e07a6c6dac |
feat: product drafts, moderator edits, info history, unlisted products, tags and categories (#8)
Phase 2 of the plan in `docs/design.md` ("Next"): product info.
**Drafts**
- A new upload is a draft. Only its creator and moderators can see it, and nobody can get or wear it.
- After uploading, the website goes straight to the product's review page, where the creator checks its info and presses Publish.
- Drafts not published within 30 days are deleted (checked every 10 minutes), and their catalogs get `delete` entries in their changes feed.
- The console's overview shows a "Drafts" to-do.
**Editing is a permission check now** (`product:edit`, from #7)
- Moderators can edit anyone's product. They must give a reason, which the creator sees.
- Moderators can lock fields (`product:lock`). Once a field is locked, only people with `product:edit-locked` can change it, so the creator sees it greyed out with "Locked by a moderator."
- Raising a rating to adult is immediate for anyone who can edit. Lowering it takes `product:rate-lower` (moderators), and is refused while any revision's files are rated adult. With cases (phase 3), a creator's request to lower it will open a rating review.
**Info history**
- Every edit, publish, lock change and undo is recorded with who, when, what changed (before and after) and why.
- It's shown on the edit page to the creator and moderators.
- "Undo" puts back what an entry changed, as a new entry, by the same rules (a creator can't undo a moderator's change to a field that's now locked).
- Moderator actions are also in the creator's audit log.
**Unlisted products and tags**
- An unlisted product is left out of search, lists and creator pages, except for its creator. Its link still works and people can still get it.
- Tags: up to 10 per product, lowercased.
- `/products` filters by `tag` and `catalog`, and a product's tags link to the filtered list.
- People who don't manage a product don't see its state labels (draft, unlisted, locks).
**Console → Products**
- Lists your products with drafts included; moderators can switch to everyone's.
- Filters: draft, published, unlisted, locked, deleted, removed, plus search.
- Shows how many libraries each product is in, and an Edit or Review button.
**Category**
- A product can be filed under a different IMVU category from its parent's.
- The choices are every category path the hub has seen on any product's parent, plus each level of those paths. They're limited to the same top-level category as the parent's, so a product stays the same kind (clothing, furniture or rooms), which the proxy relies on for the shop and inventory tabs.
- The list grows as products are uploaded. IMVU's own category tree, if it has a public one, would replace it.
- Category works like the other info fields: it can be locked, it's in the history, and it can be undone. The product list's category filter, and everything that shows a product's categories (including what the proxy gets), use the chosen path.
**Migration** `0011_product_info`: new product columns and the `product_info_change` table. Existing products count as published (with `published_at` set from their creation date) and listed.
**Compatibility**
- `t5de-catalog` needs no change: the hub's new `draft` field in the registration answer is ignored, and the website doesn't rely on it.
- The proxy only loads products from libraries, and drafts can't be added to one.
**Checks**
- 44 API tests pass, including a new one for moderator edits, locks, history, undo, unlisted, tags, the console list and draft purging, and new checks for category choices.
- Type checks pass.
- On a local hub I loaded the pages as the creator, an admin, another person and a signed-out visitor.
- I submitted the forms through the website, the way a browser does without JavaScript: save info, publish, choosing a category (and undoing it), a moderator's save with and without a reason, save locks, and undo, including a creator's undo refused by a lock. Each saved or refused correctly with the right message.
- Not tried: the upload form's redirect after a real upload to a running catalog.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Phase 2 of the plan in `docs/design.md` ("Next"): product info.
**Drafts**
- A new upload is a draft. Only its creator and moderators can see it, and nobody can get or wear it.
- After uploading, the website goes straight to the product's review page, where the creator checks its info and presses Publish.
- Drafts not published within 30 days are deleted (checked every 10 minutes), and their catalogs get `delete` entries in their changes feed.
- The console's overview shows a "Drafts" to-do.
**Editing is a permission check now** (`product:edit`, from #7)
- Moderators can edit anyone's product. They must give a reason, which the creator sees.
- Moderators can lock fields (`product:lock`). Once a field is locked, only people with `product:edit-locked` can change it, so the creator sees it greyed out with "Locked by a moderator."
- Raising a rating to adult is immediate for anyone who can edit. Lowering it takes `product:rate-lower` (moderators), and is refused while any revision's files are rated adult. With cases (phase 3), a creator's request to lower it will open a rating review.
**Info history**
- Every edit, publish, lock change and undo is recorded with who, when, what changed (before and after) and why.
- It's shown on the edit page to the creator and moderators.
- "Undo" puts back what an entry changed, as a new entry, by the same rules (a creator can't undo a moderator's change to a field that's now locked).
- Moderator actions are also in the creator's audit log.
**Unlisted products and tags**
- An unlisted product is left out of search, lists and creator pages, except for its creator. Its link still works and people can still get it.
- Tags: up to 10 per product, lowercased.
- `/products` filters by `tag` and `catalog`, and a product's tags link to the filtered list.
- People who don't manage a product don't see its state labels (draft, unlisted, locks).
**Console → Products**
- Lists your products with drafts included; moderators can switch to everyone's.
- Filters: draft, published, unlisted, locked, deleted, removed, plus search.
- Shows how many libraries each product is in, and an Edit or Review button.
**Category**
- A product can be filed under a different IMVU category from its parent's.
- The choices are every category path the hub has seen on any product's parent, plus each level of those paths. They're limited to the same top-level category as the parent's, so a product stays the same kind (clothing, furniture or rooms), which the proxy relies on for the shop and inventory tabs.
- The list grows as products are uploaded. IMVU's own category tree, if it has a public one, would replace it.
- Category works like the other info fields: it can be locked, it's in the history, and it can be undone. The product list's category filter, and everything that shows a product's categories (including what the proxy gets), use the chosen path.
**Migration** `0011_product_info`: new product columns and the `product_info_change` table. Existing products count as published (with `published_at` set from their creation date) and listed.
**Compatibility**
- `t5de-catalog` needs no change: the hub's new `draft` field in the registration answer is ignored, and the website doesn't rely on it.
- The proxy only loads products from libraries, and drafts can't be added to one.
**Checks**
- 44 API tests pass, including a new one for moderator edits, locks, history, undo, unlisted, tags, the console list and draft purging, and new checks for category choices.
- Type checks pass.
- On a local hub I loaded the pages as the creator, an admin, another person and a signed-out visitor.
- I submitted the forms through the website, the way a browser does without JavaScript: save info, publish, choosing a category (and undoing it), a moderator's save with and without a reason, save locks, and undo, including a creator's undo refused by a lock. Each saved or refused correctly with the right message.
- Not tried: the upload form's redirect after a real upload to a running catalog.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: dhkatz <[email protected]>
|
||
|
|
2cbedd26fc |
feat: permissions, a scoped console, and catalog management (#7)
Phase 1 of `docs/console.md` (builds on #6, whose docs commit it includes; merge #6 first). **API** - `permissions.ts`: `resource:action` permissions, built-in roles as bundles at a scope (hub, catalog, product), `can`/`require`, "may give" checks. A missing permission answers 403 with `missing: {permission, scope}`. - Catalog management: settings (incl. rating ceiling, upload policy, mirroring), key rotation, operators, membership (open / approval / invite links, bans), delisting, suspension by moderators, and `GET /api/v1/catalog/changes` for catalogs. - Membership is enforced: getting a product (403 `members_only` with the catalogs to join), the library list, and outfits relayed to others. Seeing products still needs no membership. - Console for everyone who controls a catalog, product or hosted room; `/console/audit` (tagged entries, scoped by permission, with filters), `/console/permissions`; `/me` says whether the console is for them. - Migration 0010: `role_assignment`, `catalog_member`, `catalog_invite`, `catalog_change`, catalog settings, audit `tags` (backfilled from old entries). **Website** - Console overview by what you manage; audit log page with filters; catalog list scoped to your teams; a catalog management page (settings, key, team, members, invitations, products, moderation, recent changes). - Public catalog pages with join / ask / accept invitation / leave; product pages say "members only" and link the catalogs. **Checked**: 43 API tests pass (8 new in `catalogs.test.ts`); API, web and db type checks; every new page rendered against a local hub for owner, operator, member, outsider and admin; the Eden DELETE-with-query calls tested against the API. Server-action forms weren't clicked through in a browser. Not in this PR (later phases): catalog deletion requests (with cases), drafts and moderator product edits, notifications, API tokens, two-factor, imports, charts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Phase 1 of `docs/console.md` (builds on #6, whose docs commit it includes; merge #6 first). **API** - `permissions.ts`: `resource:action` permissions, built-in roles as bundles at a scope (hub, catalog, product), `can`/`require`, "may give" checks. A missing permission answers 403 with `missing: {permission, scope}`. - Catalog management: settings (incl. rating ceiling, upload policy, mirroring), key rotation, operators, membership (open / approval / invite links, bans), delisting, suspension by moderators, and `GET /api/v1/catalog/changes` for catalogs. - Membership is enforced: getting a product (403 `members_only` with the catalogs to join), the library list, and outfits relayed to others. Seeing products still needs no membership. - Console for everyone who controls a catalog, product or hosted room; `/console/audit` (tagged entries, scoped by permission, with filters), `/console/permissions`; `/me` says whether the console is for them. - Migration 0010: `role_assignment`, `catalog_member`, `catalog_invite`, `catalog_change`, catalog settings, audit `tags` (backfilled from old entries). **Website** - Console overview by what you manage; audit log page with filters; catalog list scoped to your teams; a catalog management page (settings, key, team, members, invitations, products, moderation, recent changes). - Public catalog pages with join / ask / accept invitation / leave; product pages say "members only" and link the catalogs. **Checked**: 43 API tests pass (8 new in `catalogs.test.ts`); API, web and db type checks; every new page rendered against a local hub for owner, operator, member, outsider and admin; the Eden DELETE-with-query calls tested against the API. Server-action forms weren't clicked through in a browser. Not in this PR (later phases): catalog deletion requests (with cases), drafts and moderator product edits, notifications, API tokens, two-factor, imports, charts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: dhkatz <[email protected]> |
||
|
|
d5cd2be5d0 |
feat: age verification with an ID document (Didit), and closed registration (#4)
## Age verification with Didit A new provider (`apps/api/src/age/didit.ts`) behind the existing vendor-neutral age interface. A person verifies once with an ID document, a passive liveness check and a face match; after that, all their linked IMVU accounts count as adult. The hub keeps only "over 18": never the document, photo or birth date. - Webhooks are checked against Didit's signatures (the raw body or V2, with `-Previous` during secret rotation), and old ones are rejected. If the webhook has no decision, the hub fetches it from Didit. - A later answer replaces an earlier one. An approval that's taken back (say, after a manual review) becomes `revoked`. A check still in review stays pending. - Limits, for every provider: one unfinished check per person (starting again resumes it), at most 3 checks in 30 days, none once verified, and a monthly cap (`DIDIT_MONTHLY_LIMIT`, default 30: since 2026-11-01, Didit's free plan is $10 of credit a month, and a check costs $0.30). - People are only ever sent back to the hub's website. - The Account page gets an Age Verification card. - Configuration: `AGE_PROVIDERS=imvu,didit`, `DIDIT_API_KEY`, `DIDIT_WORKFLOW_ID`, `DIDIT_WEBHOOK_SECRET`. The webhook is `<API_URL>/api/v1/age/webhook/didit`. ## Closed registration Registration is open or closed. It's an admin's setting in the console, with its default from `REGISTRATION`. - When it's closed, new accounts can only sign in until a moderator approves them, or declines them (which deletes the account). - The website shows people who are waiting a waiting page, and the API treats them as signed out everywhere but `/me`. - The console gets a Waiting count, a Waiting for approval filter, Approve and Decline on a person's page, and a Registration setting. - The migration adds `user.approved` (existing accounts stay approved) and `hub_setting`. ## Testing - New tests cover Didit (with a fake Didit server), the limits, revocation, return addresses and registration. - `bun run test`: 35 pass. `bun run check-types` passes. - Checked locally: the account, waiting, sign-up and console pages render. - Not run against Didit itself yet. The sandbox workflow "T5DE Age Check" is published, and the next step is a real webhook once this is on staging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Age verification with Didit A new provider (`apps/api/src/age/didit.ts`) behind the existing vendor-neutral age interface. A person verifies once with an ID document, a passive liveness check and a face match; after that, all their linked IMVU accounts count as adult. The hub keeps only "over 18": never the document, photo or birth date. - Webhooks are checked against Didit's signatures (the raw body or V2, with `-Previous` during secret rotation), and old ones are rejected. If the webhook has no decision, the hub fetches it from Didit. - A later answer replaces an earlier one. An approval that's taken back (say, after a manual review) becomes `revoked`. A check still in review stays pending. - Limits, for every provider: one unfinished check per person (starting again resumes it), at most 3 checks in 30 days, none once verified, and a monthly cap (`DIDIT_MONTHLY_LIMIT`, default 30: since 2026-11-01, Didit's free plan is $10 of credit a month, and a check costs $0.30). - People are only ever sent back to the hub's website. - The Account page gets an Age Verification card. - Configuration: `AGE_PROVIDERS=imvu,didit`, `DIDIT_API_KEY`, `DIDIT_WORKFLOW_ID`, `DIDIT_WEBHOOK_SECRET`. The webhook is `<API_URL>/api/v1/age/webhook/didit`. ## Closed registration Registration is open or closed. It's an admin's setting in the console, with its default from `REGISTRATION`. - When it's closed, new accounts can only sign in until a moderator approves them, or declines them (which deletes the account). - The website shows people who are waiting a waiting page, and the API treats them as signed out everywhere but `/me`. - The console gets a Waiting count, a Waiting for approval filter, Approve and Decline on a person's page, and a Registration setting. - The migration adds `user.approved` (existing accounts stay approved) and `hub_setting`. ## Testing - New tests cover Didit (with a fake Didit server), the limits, revocation, return addresses and registration. - `bun run test`: 35 pass. `bun run check-types` passes. - Checked locally: the account, waiting, sign-up and console pages render. - Not run against Didit itself yet. The sandbox workflow "T5DE Age Check" is published, and the next step is a real webhook once this is on staging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: dhkatz <[email protected]> |
||
|
|
51d5bb5ff3 |
Website: products, library, catalogs and their console page
/products (search), product pages with revisions, getting and staying on a revision, retracting; /products/:id/edit; /create and new revisions upload from the browser straight to the catalog with a token from the hub; /library; /catalogs (directory, registering with a once-shown key); /console/catalogs to approve or reject. Every page now sends a nonce-based CSP (proxy.ts), so product descriptions can't run scripts. buttonVariants now merges classes, so outline links get their border. Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
7a06ba49c3 |
Rebuild the hub on Bun: Elysia API, Better Auth, Drizzle, Next.js website
CI / test (push) Successful in 30s
The hub becomes a Bun monorepo: apps/api (Elysia, Better Auth for accounts, roles, bans, proxy device sign-in and JWT keys; tagline linking, presence and the console as before), packages/db (Drizzle; Postgres in production, PGlite in development and tests), and the Next.js website from apps/web. Self-hostable with docker compose; the website deploys to Vercel-style hosts, the API to any that runs Bun or containers. The first version, in Go, is removed. Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
64c552330b |
Website: Next.js app for accounts, linking, who's online, device sign-in and the console
apps/web: Next.js 16 (App Router), Tailwind 4 and shadcn/ui, light and dark from the system. Pages render on the server and call the API with Eden Treaty (typed by @t5de/api), forwarding the visitor's cookie; forms use server actions; sign-up, sign-in and sign-out use Better Auth's React client. /api is rewritten to the API so its cookies are first-party. Who's online follows the API's event stream (Next's gzip is off: it held the stream back). Standalone output with a Dockerfile; deploys to Vercel as is. Co-Authored-By: Claude Opus 5.5 <[email protected]> |