Files
t5de-hub/docs/api.md
T
dhkatzandClaude Opus 5.5 7db9fc0370
CI / test (push) Successful in 35s
Vendor-neutral age verification, with IMVU as the first provider
Age checks are recorded in one table (age_check) by provider, method and result, never with documents or
birth dates; one rule decides whether an IMVU account counts as adult (its person's check, or its own).
Providers implement a small interface (start, webhook or browser completion, re-check) and are chosen by
configuration, so phone wallets and Didit can be added, or swapped, as adapters. IMVU's answer per account
is re-checked when it expires (about daily). Existing links' results move into the new table.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:44:36 -07:00

4.1 KiB

The hub's API

apps/api (Elysia on Bun). The route definitions in apps/api/src/app.ts are the reference: their schemas validate requests and type the website's client (Eden). This is the overview.

  • /api/auth/*: Better Auth (accounts, sessions, device sign-in, JWT keys).
  • /api/v1/*: the hub's own routes.

The website reaches the API through its own origin (Next.js rewrites /api to it), so sessions are first-party cookies. T5DE's proxy calls it directly with Authorization: Bearer <session token>.

Errors are {"error": "<code>", "message": "<text for people>"} with a 4xx/5xx status; message can be shown as is. Codes: invalid, signed_out, forbidden, not_found, no_such_imvu_account, already_linked, code_missing, expired, link_cap, rate_limited, imvu_unavailable.

Accounts (Better Auth)

POST /api/auth/sign-up/email {email, password, name, username}. Usernames 3-32 of A-Za-z0-9_; passwords 10-256 characters. The first account is an admin.
POST /api/auth/sign-in/username {username, password}
POST /api/auth/sign-out
GET /api/v1/me {person}

Proxies

POST /api/auth/device/code {client_id: "t5de-proxy"} → {device_code, user_code, verification_uri, verification_uri_complete, interval, expires_in} (RFC 8628). verification_uri is the website's /device.
POST /api/auth/device/token {grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code, client_id} → {access_token} (a session token, used as a bearer token) or {error: authorization_pending | slow_down | expired_token | access_denied}
POST /api/v1/device/approve (website, signed in) {code}: signs the waiting proxy in as this person
GET /api/v1/realtime WebSocket, bearer token. Protocol in apps/api/src/realtime.ts.
GET /.well-known/jwks.json the Ed25519 keys JWTs are signed with

Website routes (signed in unless noted)

GET /api/v1/home (anyone) {onlineCount, links?}
GET /api/v1/online (anyone) {online}: accounts showing they're online (moderators see all, with hidden)
GET /api/v1/online/events (anyone) server-sent events: online with {online} now and on each change
GET /api/v1/account {person, links, challenges, computers, canLink}
POST /api/v1/account/links {username} → 201 {challenge} (the tagline code)
POST /api/v1/account/links/:imvu/check {link, moved}
DELETE /api/v1/account/challenges/:imvu cancel linking
PATCH /api/v1/account/links/:imvu {showBadge?, showPresence?} → {link}
POST /api/v1/account/links/:imvu/recheck {link} (age checked with IMVU again)
DELETE /api/v1/account/links/:imvu unlink
POST /api/v1/account/signout-computers signs T5DE out on all their computers

Age verification

Vendor-neutral (docs/design.md, "Age"): IMVU's answer is recorded when an account is linked or checked again; person-level providers (phone wallets, document checks) are started here.

GET /api/v1/age {providers: [{id, label, scope}], checks}: the providers in use and the person's own checks
POST /api/v1/age/:provider/start {returnURL} → {kind: "done"}, {kind: "redirect", url, ref} (send the person there) or {kind: "browser", request, ref} (hand request to the browser's Digital Credentials API)
POST /api/v1/age/:provider/complete {ref, response}: the browser's answer, for wallet providers
POST /api/v1/age/webhook/:provider the provider's webhook; the provider checks its signature

Console (moderators; roles: admins)

GET /api/v1/console {counts, connections, online, audit}
GET /api/v1/console/people?q= {people}
GET /api/v1/console/people/:id {person, links, computers}
POST /api/v1/console/people/:id/link-cap {linkCap} (0-1000)
POST /api/v1/console/people/:id/ban {reason}; only admins ban moderators
POST /api/v1/console/people/:id/unban
POST /api/v1/console/people/:id/role (admins) {role}