feat: personal API tokens
Pull request / title (pull_request) Successful in 0s
CI / test (pull_request) Successful in 1m9s
CI / image (apps/api/Dockerfile, t5de-hub-api) (pull_request) Skipped
CI / image (apps/web/Dockerfile, t5de-hub-web) (pull_request) Skipped

Tokens for scripts, made, listed and revoked on the account page. Each grants some of the person's own permissions
at chosen places (a product of theirs, a catalog whose team they're on, or the hub for moderators); every check is
the intersection with what they hold now. Tokens work only on routes that check a permission (session-only routes
answer token_not_allowed), are shown once and stored as hashes, note their last use, and mark the audit entries of
what they do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-11-05 14:58:58 -08:00
1 parent 58a3e70a63
commit 2b88a52bcb
17 files changed
+4358 -98

No files matched your search

+19 -1
View File
@@ -182,6 +182,24 @@ Bearer t5de_pat_<random>`; only the hash is stored; shown once. A token never ha
check is the intersection, so losing a role takes it from tokens too. Listed and revocable in the account page;
every use updates `last_used_at`; actions are audited as the person, marked "by token <name>".
Built (2026-11-05):
- Made, listed and revoked on the account page (a website session only: a token can't make tokens). Each grant is
a permission the person holds now at the hub (moderators and admins), a catalog whose team they're on, or a
product of theirs (400 `not_held` otherwise). At most 20 active tokens, 50 grants, 365 days.
- Usable on the routes that check a permission (product info, publish, locks, history; uploading new revisions of a
product, `revision:upload`, and retracting them; a catalog's settings, key, team, members, invites and hosted
products; blocks; assigning cases). Other routes refuse tokens (403 `token_not_allowed`), including making a new
product, reports and the console. On those routes every check is the intersection, so a moderator's token granted
`product:remove` hub-wide can block files but not suspend catalogs, and losing the role takes it from the token.
- The audit log marks actions by token (`detail.token`: its name); making and revoking one is audited
(`token.create`, `token.revoke`).
| | |
| --- | --- |
| `GET /api/v1/account/tokens` | `{tokens: [{id, name, grants: [{permission, scope, name}], status (active, expired, revoked), createdAt, expiresAt, lastUsedAt, revokedAt}], scopes: [{scope, name, permissions}], maxDays}`: `scopes` is what a token could be given |
| `POST /api/v1/account/tokens` | `{name, days, grants: [{permission, scope: {type: "hub"} or {type: "catalog" or "product", id}}]}` → 201 `{token, info}`; `token` is shown only here |
| `DELETE /api/v1/account/tokens/:id` | revokes it → 204 |
```
api_token(id, person, name, hash, grants jsonb, expires_at, last_used_at, created_at, revoked_at)
```
@@ -224,7 +242,7 @@ for meaning.
| `POST /api/v1/cases/:id/decide` | `{decision: "resolve" or "decline", resolution}`: by those who may decide it (below); resolving carries out a rating review, removal request or deletion request; the opener is told |
| `GET /api/v1/notifications?before=` | `{notifications: [{id, type, tags, data, at, read}], unread, before}`, newest first, 50 a page |
| `POST /api/v1/notifications/read` | `{ids?}` (all if left out) → `{unread}` |
| `GET/POST /api/v1/account/tokens`, `DELETE /api/v1/account/tokens/:id` | API tokens |
| `GET/POST /api/v1/account/tokens`, `DELETE /api/v1/account/tokens/:id` | API tokens (above) |
| `GET/PUT/DELETE /api/v1/roles/:scopeType/:scopeId/:person` | role assignments at a scope |
| `POST /api/v1/console/view-as/:person` | (admins) start viewing as someone, read-only |