feat: age verification with an ID document (Didit), and closed registration
Age: a Didit provider behind the vendor-neutral age interface. A person verifies once (ID document, passive liveness, face match) and all their linked IMVU accounts count as adult. Webhooks are checked against Didit's signatures; only "over 18" is kept. A later answer replaces an earlier one (an approval taken back is revoked). Checks are limited: one unfinished check per person, 3 in 30 days, none once verified, and a monthly cap (DIDIT_MONTHLY_LIMIT, default 30: Didit's free plan is $10 of credit a month and a check costs $0.30). The Account page gets an Age Verification card. Registration: open or closed, an admin's setting in the console (default from REGISTRATION). Closed: new accounts can only sign in until a moderator approves them (or declines, deleting the account); the website shows them a waiting page and the API treats them as signed out. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
86ebe976ba
commit
c75a4c9afc
28 files changed
+3260
-89
No files matched your search
+10
-6
@@ -17,10 +17,11 @@ is. Codes: `invalid`, `signed_out`, `forbidden`, `not_found`, `no_such_imvu_acco
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `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-up/email` | `{email, password, name, username}`. Usernames 3-32 of `A-Za-z0-9_`; passwords 10-256 characters. The first account is an admin. With registration closed, the others wait for approval (`approved: false`): every route but `/me` answers 403 `awaiting_approval`, and the rest of the hub treats them as signed out. |
|
||||
| `POST /api/auth/sign-in/username` | `{username, password}` |
|
||||
| `POST /api/auth/sign-out` | |
|
||||
| `GET /api/v1/me` | `{person}`, with `picture`: the avatar picture of their first linked IMVU account (from IMVU, kept an hour), or `null` |
|
||||
| `GET /api/v1/me` | `{person}` (`approved` false while waiting), with `picture`: the avatar picture of their first linked IMVU account (from IMVU, kept an hour), or `null` |
|
||||
| `GET /api/v1/hub` | (anyone) `{registration}`: `open` or `closed` |
|
||||
|
||||
## Proxies
|
||||
|
||||
@@ -97,17 +98,20 @@ 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) |
|
||||
| `GET /api/v1/age` | `{providers: [{id, label, vendor, scope}], checks}`: the providers in use and the person's own checks (newest first; status `pending`, `complete`, `failed` or `revoked`) |
|
||||
| `POST /api/v1/age/:provider/start` | `{returnURL}` (a path on the website, or an address on it) → `{kind: "done"}`, `{kind: "redirect", url, ref}` (send the person there) or `{kind: "browser", request, ref}` (hand `request` to the browser's Digital Credentials API). An unfinished check is given again. 409 `refused`: already verified, or a limit reached (`message` says which); 502 `provider_unavailable` |
|
||||
| `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 |
|
||||
| `POST /api/v1/age/webhook/:provider` | the provider's webhook; the provider checks its signature (401 if it isn't the provider's). A later answer replaces an earlier one (an adult answer taken back is `revoked`) |
|
||||
|
||||
## Console (moderators; roles: admins)
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `GET /api/v1/console` | `{counts, connections, online, audit}` |
|
||||
| `GET /api/v1/console/people?q=` | `{people}` |
|
||||
| `GET /api/v1/console/people?q=&waiting=true` | `{people}` (`waiting`: only accounts waiting for approval) |
|
||||
| `POST /api/v1/console/people/:id/approve` | `{person}` |
|
||||
| `POST /api/v1/console/people/:id/decline` | an account waiting for approval: deletes it |
|
||||
| `GET /api/v1/console/settings`, `PUT` (admins) | `{registration}`: `open` or `closed` (default: the API's `REGISTRATION`, else open) |
|
||||
| `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 |
|
||||
|
||||
+22
-5
@@ -100,6 +100,9 @@ age, not verified) and `tagline`.
|
||||
- **Switching accounts in the client**: the proxy sees which IMVU account logs in and tells the hub. Not linked:
|
||||
offer to link. Linked to another hub account: no community features that session.
|
||||
- **Bans** apply to the person, so all linked accounts.
|
||||
- **Registration** is open or closed, an admin's setting in the console (default from `REGISTRATION`; staging is
|
||||
meant to be closed, for testers only). Closed: anyone can sign up, but the account can only sign in until a
|
||||
moderator approves it (or declines it, deleting it); the hub treats it as signed out everywhere else.
|
||||
|
||||
## Age (**Decided**: free, several providers, vendor-neutral)
|
||||
|
||||
@@ -112,8 +115,11 @@ No payment for verification (adult content rules out mainstream merchants of rec
|
||||
2. **Phone wallets** (planned): a government ID in Google Wallet, Apple Wallet or an EU age-verification wallet,
|
||||
asked for "age over 18" only, through the browser's Digital Credentials API. Free for the hub; no documents or
|
||||
birth date reach it. Scope: the person.
|
||||
3. **ID document and selfie** (planned): Didit's hosted check, within its free monthly allowance. Scope: the person
|
||||
(the liveness step stops someone using another person's ID).
|
||||
3. **ID document and selfie** (live, `didit`): Didit's hosted check: ID document $0.15, passive liveness $0.10 and
|
||||
face match $0.05, so $0.30 a check, paid from the free plan's $10 of credit a month (since 2026-11-01; the 500
|
||||
free checks a month are gone), so about 30 a month unless topped up. Scope: the person (liveness and face match
|
||||
stop someone using another person's ID). Over 18 is worked out from the age Didit reads off the document;
|
||||
nothing else is kept. With so few checks, phone wallets (free) should become the main way.
|
||||
|
||||
### How it's built
|
||||
|
||||
@@ -128,14 +134,25 @@ No payment for verification (adult content rules out mainstream merchants of rec
|
||||
webhook), and optionally `recheck` (for providers whose answer can change, like IMVU). Which ones run is
|
||||
configuration (`AGE_PROVIDERS`), with each provider's keys in its own settings. Adding or replacing a vendor is
|
||||
one new adapter; nothing else changes.
|
||||
- Webhooks arrive at `/api/v1/age/webhook/<provider>` and each provider checks its own signature.
|
||||
- Webhooks arrive at `/api/v1/age/webhook/<provider>` and each provider checks its own signature. The provider's
|
||||
latest answer stands: one taken back after approval (say, by a manual review) becomes `revoked`.
|
||||
- **Limits** on checks of a person, for every provider: one unfinished check at a time (starting again gives the
|
||||
same one), at most 3 started in 30 days, none once verified, and a provider's monthly cap (`monthlyLimit`; Didit's
|
||||
`DIDIT_MONTHLY_LIMIT`, default 30), counted from the checks started this month (UTC).
|
||||
- People are sent back only to the hub's website (`returnURL` is a path on it, or an address on `WEB_URL`).
|
||||
- Enforced at three points: catalogs (adult files need an adult token), the hub (adult items left out of room
|
||||
channels for viewers who aren't adult and opted in), and the viewer's proxy.
|
||||
|
||||
### Setting up Didit
|
||||
|
||||
In Didit's console (business.didit.me): an application; a KYC workflow with ID verification, passive liveness and
|
||||
face match only (no IP analysis, AML or proof of address, which cost extra; no white label, $0.10 a session more);
|
||||
an API key; and a webhook destination
|
||||
`<API_URL>/api/v1/age/webhook/didit` with its secret. Then the API's environment: `AGE_PROVIDERS=imvu,didit`,
|
||||
`DIDIT_API_KEY`, `DIDIT_WORKFLOW_ID`, `DIDIT_WEBHOOK_SECRET`. Use the sandbox application on staging.
|
||||
|
||||
### To add the planned providers
|
||||
|
||||
- Didit: an account, an API key and webhook secret, and a workflow set to ID document plus liveness, returning
|
||||
only "over 18". Confirm its free monthly allowance covers age checks.
|
||||
- Wallets: Google's Digital Credentials verifier registration (free), and an `age_over_18` request (OpenID4VP,
|
||||
mdoc); Apple and the EU wallet through the same browser API where supported.
|
||||
|
||||
|
||||
Reference in new issue
Block a user