Files
t5de-hub/README.md
T
dhkatz d5cd2be5d0
Release / release (push) Successful in 4s
CI / test (push) Successful in 1m19s
CI / image (apps/api/Dockerfile, t5de-hub-api) (push) Successful in 7s
CI / image (apps/web/Dockerfile, t5de-hub-web) (push) Successful in 20s
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]>
2026-11-04 11:37:21 +00:00

59 lines
3.5 KiB
Markdown

# t5de-hub
T5DE's hub: accounts linked to IMVU accounts, presence, and (to come) the registry of community products and
catalogs, T5DE-hosted rooms, and the moderation console. Part of T5DE (git.t5de.dev/t5de). Design:
[docs/design.md](docs/design.md); the API: [docs/api.md](docs/api.md); what a room needs from a server:
[docs/imq-rooms.md](docs/imq-rooms.md).
## What works
- **Accounts** (Better Auth): sign-up with email, username and password; sign-in by username. The first account is
an admin; roles user, moderator, admin. Registration open or closed (new accounts wait for a moderator's
approval): an admin's setting in the console, defaulting to the API's `REGISTRATION`.
- **Linking IMVU accounts**: the person puts a code in the account's tagline; the hub reads it from IMVU's public
API. The most recent proof wins (an account linked elsewhere moves, with a new pairwise ID). Link caps per person
(default 10).
- **Age**: an IMVU account counts as adult if IMVU says it's age verified or has Access Pass, checked by the hub, or
if its person verified their age with an ID document and a selfie (Didit, when configured: `AGE_PROVIDERS`, see
`apps/api/src/index.ts` and docs/design.md, "Age").
- **Proxy sign-in**: OAuth 2.0 device authorization (`t5de-proxy signin` shows a code to enter at `/device`); the
proxy then uses its session as a bearer token. JWTs with published keys (`/.well-known/jwks.json`) for catalogs.
- **Presence**: signed-in proxies keep a WebSocket (`/api/v1/realtime`) telling the hub which IMVU account is in
use and the community products it wears. `/online` shows those who opted in, live, with the opt-in T5DE badge.
- **Products** (website): browsing and search, product pages, uploads straight from the browser to a catalog,
editing, revisions (retract, stay on one), the library, and registering catalogs (`docs/products.md`). Pages send
a strict Content Security Policy (`apps/web/proxy.ts`).
- **Console** (moderators): counts, who's online (live), people, link caps, bans, roles, the audit log.
## Layout
A Bun monorepo (workspaces, Turborepo):
| | |
| --- | --- |
| `apps/api` | The API: Elysia on Bun, Better Auth, the realtime WebSocket. Port 4101. |
| `apps/web` | The website: Next.js 16 (React server components), Tailwind CSS 4, shadcn/ui. Port 4100. Calls the API through its own `/api` (rewrites), typed end to end with Eden. |
| `packages/db` | Drizzle schema and migrations; Postgres in production, PGlite (in-process Postgres) for development and tests. |
## Development
Needs Bun (1.4) and Node (for Next.js).
bun install
bun run dev # the API on :4101 (database in apps/api/data) and the website on :4100
`bun run test` tests the API against PGlite; `bun run check-types`, `bun run build`.
## Running it
- **Self-hosted**: `AUTH_SECRET=$(openssl rand -base64 32) docker compose up -d` runs Postgres, the API and the
website (see `docker-compose.yml` for the public addresses).
- **Elsewhere**: the website deploys as is to Vercel and similar (`apps/web`, env `API_URL`); the API to any host
that runs Bun or containers (Railway, Render, Fly.io, Heroku), with `DATABASE_URL`, `AUTH_SECRET`, `API_URL` (its
public address) and `WEB_URL` (the website's).
A staging site shows a slim "Staging" bar: when it's served from a `staging.` address (`hub.staging.t5de.dev`),
or with the website's `HUB_ENVIRONMENT=staging` (`production` hides it).
T5DE's proxies reach the API's address directly (sign-in and the WebSocket): `t5de-proxy signin -hub <API address>`.