Skip to main content
← Back to list
01Issue
FeatureShippedSwamp Club
Assigneesstack72

Relationships

#1299 Token creation API and UI scope selector for collective tokens (phase 4 of #960)

Opened by stack72 · 7/20/2026· Shipped 7/21/2026

Context

Phase 4 of #960. The foundation is complete:

  • Phase 1 (#1228) — domain, persistence, auth threading
  • Phase 1 addendum (#1232) — full scope taxonomy (22 scopes)
  • Phase 2 (#1261) — enforcement at routes + whoami introspection
  • Phase 3 (#1264) — CLI collective token support

This phase makes scoped tokens user-creatable for the first time. Web-only — no CLI changes needed.

What this covers

1. Token creation API (routes/api/v1/collectives/[slug]/api-tokens/index.ts)

Update POST handler to accept an optional scopes array in the request body:

  • When scopes is provided: validate against the canonical scope registry (TokenScopes.create()), pass through to createCollectiveApiToken()
  • When scopes is omitted: default to ["extensions:*"] (existing behavior, no breaking change)
  • Invalid scopes return 422 with a message naming the invalid scope(s)
  • Return scopes in the response alongside the existing fields

2. Token creation command (lib/app/collective-api-token-commands.ts)

Update createCollectiveApiToken() to accept an optional scopes parameter instead of hardcoding ["extensions:*"]. Default to ["extensions:*"] when not provided.

3. Token list API (routes/api/v1/collectives/[slug]/api-tokens/index.ts)

Update GET handler response to include scopes and legacyToken for each token in the list. The view (collective-token-view.ts) already has these fields from phase 1.

4. UI scope selector (islands/CollectiveApiKeyManager.tsx)

Update the create form:

  • Add a checkbox group for scope selection below the name field
  • Show only the extension scopes that are currently enforced: extensions:push, extensions:promote, extensions:read, and extensions:* (full access)
  • Default to extensions:* (full access) selected
  • When extensions:* is selected, disable/uncheck individual extension scopes (they're redundant)
  • Send the selected scopes in the POST request body
  • Use semantic HUD tokens (text-hud-label, text-hud-muted), accessible form patterns (label + htmlFor, focus:ring-2), mobile-first layout at 375px

5. Display scopes on listed tokens (islands/CollectiveApiKeyManager.tsx)

  • Show scopes as badges/chips on each listed token
  • Show a warning badge on legacy tokens: "Created before scoped tokens — rotate to a scoped token"
  • legacyToken is used for the warning badge display only — it is NOT exposed via the API to end users

6. Tests

  • Route tests: POST with valid scopes returns them in response, POST with invalid scopes returns 422, POST without scopes defaults to extensions:*, GET includes scopes in token list
  • App tests: createCollectiveApiToken with explicit scopes, without scopes (defaults)

What this does NOT change

  • Personal API keys — completely untouched
  • CLI — no changes needed (already handles collective tokens from phase 3)
  • Login — untouched
  • Enforcement — already live from phase 2
  • Domain entity / MongoDB / middleware / cache — already have scopes from phase 1

User impact

This is the first user-visible feature moment. Users can now:

  1. Go to /o/[slug]/settings → API Tokens
  2. Create a token with specific scopes (e.g. only extensions:push)
  3. Copy the token and use it in CI with SWAMP_API_KEY=[REDACTED-SECRET-1]
  4. That token can push extensions but gets a 403 if it tries to promote or search private extensions
  5. See which scopes each existing token has
  6. See a warning on legacy tokens suggesting rotation

Verification

  • deno task check passes
  • deno task test passes
  • deno task build passes
  • Create a token with extensions:push scope in the UI — verify it shows in the token list
  • Use that token to push — succeeds
  • Use that token to promote — 403 with scope message
  • Create a token without selecting scopes — defaults to extensions:*
  • Legacy tokens show warning badge
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 3 MOREPR_MERGED+ 1 MORENOTIFICATION_SKIPPED

Shipped

7/21/2026, 4:17:22 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack727/20/2026, 10:04:55 PM

Sign in to post a ripple.