feat: API token management in workspace settings - #3
Open
dnplkndll wants to merge 18 commits into
Open
Conversation
dnplkndll
force-pushed
the
feat/api-token-management
branch
4 times, most recently
from
March 27, 2026 11:30
ebdcb02 to
370a096
Compare
dnplkndll
force-pushed
the
feat/api-token-management
branch
from
April 18, 2026 19:05
370a096 to
cf87e38
Compare
dnplkndll
force-pushed
the
feat/api-token-management
branch
from
May 11, 2026 22:16
e0e0743 to
a3a8019
Compare
Add UI and backend support for creating, listing, and revoking API tokens scoped to workspaces. Includes owner-level workspace token visibility, OpenAPI documentation, Mongo/Postgres persistence, and i18n translations. Signed-off-by: Don Kendall <kendall@donkendall.com>
Embed apiTokenId in JWT extra field and add a per-token revocation cache (60s TTL) in the transactor REST handler. Revoked tokens are now rejected within ~60 seconds instead of remaining valid until JWT expiry. Adds checkApiTokenRevoked account service method for the transactor to query individual token revocation status. Signed-off-by: Don Kendall <kendall@donkendall.com>
Add coarse-grained scope enforcement for API tokens. Tokens can now be created with scopes ['read:*'], ['read:*','write:*'], or ['read:*','write:*','delete:*']. Existing tokens without scopes retain full access (backward compatible). - DB: v26 migration adds scopes TEXT[] column to api_tokens - Types: add scopes field to ApiToken and ApiTokenInfo - Operations: createApiToken accepts/validates/persists scopes, embeds in JWT via extra.scopes - Enforcement: withSession checks scopes against method; tx handler additionally requires delete:* for TxRemoveDoc - Client: createApiToken signature accepts optional scopes param - UI: scope preset dropdown in create popup (default: Read Only), permissions column in token list with i18n labels - Also fixes 3 pre-existing TS2322/TS2345 errors in operations.ts Signed-off-by: Don Kendall <kendall@donkendall.com>
- scopes.test.ts: 8 tests for hasScope() and getRequiredScope() logic - apiTokenScopes.test.ts: 7 tests for createApiToken scope validation (valid scopes, multiple scopes, no scopes backward compat, invalid format rejection, empty array rejection, domain-scope rejection) and listApiTokens scopes inclusion - Export hasScope/getRequiredScope from rpc.ts for testability Signed-off-by: Don Kendall <kendall@donkendall.com>
…tting - Restrict API token creation/revocation to AccountRole.User or higher (guests cannot use API tokens), per reviewer suggestion - Add 5 missing translation keys (ApiTokenPermissions, ApiTokenScopePreset, ApiTokenScopeReadOnly, ApiTokenScopeReadWrite, ApiTokenScopeFullAccess) to all non-en locale files to fix locale parity CI test - Fix prettier formatting in apiTokenScopes.test.ts - Rename local `extra` to `tokenExtra` in createApiToken to avoid shadowing the decoded token's `extra` field Signed-off-by: Don Kendall <kendall@donkendall.com>
- rpc.ts: use system service token for checkApiTokenRevoked so the revocation check is not coupled to the user's potentially-revoked bearer token; systemAccountUuid + service:'server' ensures account service always accepts the call - ApiDocsSection.svelte: derive transactor base URL from login.metadata.LoginEndpoint (set on auth) instead of window.location.origin, which is not necessarily the transactor host - ApiTokenCreatePopup.svelte: replace manual translate() calls and themeStore language watch with DropdownLabelsIntl + DropdownIntlItem[], which handle i18n automatically; error state is now IntlString - General.svelte: remove legacy GenerateApiToken button, handler, and ApiTokenPopup import in favour of the new ApiTokens settings panel Signed-off-by: Don Kendall <kendall@donkendall.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>
Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Signed-off-by: Don Kendall <dkendall@ledoweb.com>
dnplkndll
force-pushed
the
feat/api-token-management
branch
from
May 15, 2026 17:46
a3a8019 to
dfa360b
Compare
Resolve conflicts: - migrations: keep develop's V26 (pending_configuration); api_tokens table (with scopes column) becomes V27 - lang files: union of develop's keys and the API token strings Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Address @aonnikov's review: the bespoke checkApiTokenRevoked RPC and the ad-hoc revocation cache in the transactor are replaced by a reusable verifyToken in server-token that checks signature, expiry, and (for revokable API tokens) revocation via a pluggable checker. - server-token: add verifyToken + isTokenExpired + setApiTokenRevocationChecker (the 'method to verify' metadata the plugin needs, without depending on the account client). Revocation cache (60s TTL) now lives here, reusable by any service (transactor, blob access, etc.). - account: the account is now authoritative — wrap() rejects revoked/expired API tokens, so any account method (selectWorkspace/getWorkspaceInfo/...) naturally 401s. Removed the redundant checkApiTokenRevoked method. - account-client: drop checkApiTokenRevoked. - transactor: withSession uses verifyToken; the registered checker reuses the existing getLoginInfoByToken instead of a dedicated boolean RPC. Scopes are parsed once and threaded through (no re-decode in the tx handler). - tests: verifyToken/isTokenExpired unit coverage. Signed-off-by: Don Kendall <dkendall@ledoweb.com>
…ndpoint Address @aonnikov: the REST API host must come from the transactor endpoint returned by the account service (the login endpoint, a ws(s):// URL), not be constructed from window.location. Convert ws->http and append /api/v1, matching the existing ServerManagerGeneral pattern. Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Address @ArtyomSavchenko: the new API token UI strings were left in English in non-en locales. Provide translations for ru/de/es/fr/it/pt/pt-br/zh/ja/cs/tr. The en/ru locale-parity test passes (the prior failure was an en/ru key mismatch, resolved by the develop merge). Signed-off-by: Don Kendall <dkendall@ledoweb.com>
…DocsSection) Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Conflicts, all from upstream adding strings and error handling next to ours: - plugins/setting-assets/lang/*.json (12 locales): three-way key merge, keeping upstream's new keys and re-applying ours. Locale parity fills requested in review (es/pt/pt-br/cs) are preserved. - pods/server/src/rpc.ts: kept our delete:* scope check ahead of upstream's new try/catch around the tx dispatch. Two fixes the merge surfaced, needed for the tree to build and pass CI: - token.test.ts: Token.account is AccountUuid, so the test constant can no longer be cast to PersonUuid. - server_http.ts: prettier rewrap of a line upstream's changes pushed over the width limit. Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7 Signed-off-by: Don Kendall <dkendall@ledoweb.com>
The scopes were only ever consulted in the transactor's REST handler, so they promised a boundary the platform did not keep: - the WebSocket transport decodes the token and never looks at scopes, so a read-only token could open a socket and issue any transaction; - even on the REST path, delete:* only matched a top-level TxRemoveDoc and was bypassed by nesting the removal in a TxApplyIf; - nothing stopped a scoped token from calling createApiToken and minting an unscoped one. Narrowing a token's rights has to happen in the pipeline, where it covers every transport, and that is a larger design than this feature. Until then a token carries its account's rights and says so, rather than displaying a restriction that does not hold. Scopes are removed end to end. What is kept is made to work: - API tokens can no longer create, list or revoke API tokens. Otherwise a leaked token renews itself and revokes the tokens meant to stop it. - Revocation fails closed. The account is the only authority on it, so an unreachable account now rejects the token instead of admitting a revoked one to whoever can keep the account busy. Verdicts stay trusted for the cache TTL so brief outages do not cut off healthy tokens. - The revocation cache is bounded and re-checks negative verdicts. - Revoking your own token no longer requires a role in its workspace, so leaving a workspace cannot strand a credential you can never revoke. - The per-account limit counts only usable tokens; revoked and expired ones are kept for the audit trail and used to lock out anyone rotating. - Rejected REST tokens are logged with a reason, since expired, revoked and unverifiable are one opaque 401 from outside. The unused listWorkspaceApiTokens/revokeWorkspaceApiToken pair is removed; it had no caller and no test. Tests cover the role restriction, the API-token guard, ownership on revoke, the limit accounting and fail-closed revocation. Removing any of those checks fails them. Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7 Signed-off-by: Don Kendall <dkendall@ledoweb.com>
- Moves the page from workspace settings to account settings. Tokens belong to the account: the page already lists them across every workspace, and creating one only needs the User role the account service checks, not Owner as the category required. - Surfaces failures. A failed revoke was logged to the console and the row re-rendered unchanged; loading workspaces could reject unhandled and leave an empty dropdown with no explanation. - Outside a secure context there is no clipboard API, so the OK button did nothing and the dialog had no way out but Cancel. It now closes, leaving the token selectable. - Replaces hardcoded English labels, adds aria-expanded on the docs disclosure and labels on the copy targets. - Fills in the Korean and Polish translations, which were the only two locales missing these keys, and drops the API access strings orphaned when this PR replaced the old Generate API token button. Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7 Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Unrelated to this feature and wrong for this repo: it documents a /_tokens minting service and a tools/mint-token CLI that do not exist here, uses reverse-proxy prefixes from a self-hosted deployment rather than the transactor's /api/v1 routes, and states that revocation is a future enhancement, which is what this PR implements. Nothing references it. REST API docs belong in their own change, generated against the routes that exist. Claude-Session: https://claude.ai/code/session_01ANdoXbdn5k2hZy734EwKe7 Signed-off-by: Don Kendall <dkendall@ledoweb.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CI validation PR (fork-internal)
Testing rebased API token management branch against latest develop.
Upstream PR: hcengineering#10624
Commits