Skip to content

feat(api): scoped admin/operator token minting for CLI admin operations (#257)#261

Merged
Zach Dunn (zachdunn) merged 8 commits into
mainfrom
claude/agent-orchestration-e35e18
Jul 18, 2026
Merged

feat(api): scoped admin/operator token minting for CLI admin operations (#257)#261
Zach Dunn (zachdunn) merged 8 commits into
mainfrom
claude/agent-orchestration-e35e18

Conversation

@zachdunn

@zachdunn Zach Dunn (zachdunn) commented Jul 18, 2026

Copy link
Copy Markdown
Member

Implements #257: admins can mint workspace tokens carrying opt-in operator scopes (operator:read, operator:write) via POST /v1/tokens, and /admin/* accepts those tokens alongside the static ADMIN_TOKEN — giving per-token revocation, labels, and attribution (minting_user_id) instead of only the shared break-glass secret.

What changed

  • apps/api/src/auth-db.ts — new OPERATOR_SCOPES (operator:read, operator:write), isOperatorScope, and a validateScopes(..., { allowOperator }) overload. All existing callers (/admin/tokens, enrollments, admin-ui invite links) stay file-scopes-only.
  • apps/api/src/routes/tokens.ts — mint requests that include admin scopes require the session user to hold the better-auth admin role; non-admins get 400 invalid_scopes. Default mint scopes are unchanged (files:read, files:write) — admin scopes are never implicit.
  • apps/api/src/session-auth.ts — new userHasAdminRole using better-auth comma-split multi-role semantics; requireAdminUser now uses it (fixes a pre-existing bug where a "admin,support" role was rejected).
  • apps/api/src/admin.tsadminAuth now accepts D1-backed operator tokens after the timing-safe ADMIN_TOKEN check: operator:read grants GET/HEAD, operator:write grants everything (superset). Revoked/expired/files-only/legacy-null-scope tokens are rejected fail-closed. ADMIN_TOKEN behavior is unchanged and remains break-glass; scoped tokens also work when ADMIN_TOKEN is unset.
  • packages/uploads — client scope unions widened to allow the new scopes (no new commands/flags); patch changeset included.
  • docs/admin-tokens.md — operator-scopes section, including two sharp edges: operator authority is global across /admin/* (the minted workspace only anchors storage/revocation), and a token carrying an admin scope gets no file-route access (file-route scope parsing fails closed on any non-file scope) — mint separate tokens.

Guardrails (per issue)

  • apps/auth is completely untouched — nothing here lands in OAUTH_SCOPES, so DCR-registered MCP clients cannot request admin scopes.
  • Note: this deliberately relaxes the prior "/admin/* is bearer-token-only and deliberately untouched" boundary noted in apps/api/src/index.ts; the comment there is updated.

Testing

  • pnpm test at repo root: 131 files / 1477 tests passing (includes new coverage: role gating incl. multi-role, default-mint safety, read/write scope mapping on /admin/*, revoked/expired/files-only rejection, no-ADMIN_TOKEN operation, and a regression pinning mixed-scope tokens to zero file-route access).

Closes #257

Naming note: scopes were renamed from admin:* to operator:* mid-review — they express platform-wide operator authority, distinct from the (weaker) workspace-admin org role. The better-auth admin user role, /admin/* paths, and ADMIN_TOKEN keep their names.

…ting (#257)

Adds ADMIN_SCOPES/isAdminScope and an allowAdmin option to validateScopes,
fixes requireAdminUser's multi-role rejection bug via a shared
userHasAdminRole helper, and wires POST /v1/tokens to accept admin:* scopes
only when the minting session user holds the admin role (400 invalid_scopes
otherwise). Default mint scopes are unchanged.
adminAuth now falls through from the static ADMIN_TOKEN check (unchanged,
still fail-closed when unset) to a D1 auth_tokens lookup for bearer tokens
shaped like up_<workspace>_<random>. admin:write is a read+write superset;
admin:read only covers GET/HEAD. Scoped-token auth works even without
ADMIN_TOKEN configured, since the fail-closed rule is specific to the
static secret.
Allow packages/uploads client.ts scope unions to accept admin:read /
admin:write alongside file scopes so CLI/SDK callers can request
operator scopes minted server-side; no new commands or flags. Adds
patch changeset for the published package.
… scopes (#257)

Operator admin scopes grant global /admin/* authority regardless of the
workspace a token is minted against; docs now say so explicitly. Adds a
routes-files.test.ts regression proving a token stored with
["files:read","admin:write"] gets no file-route access at all (parseScopes
fails the whole array when it contains a non-file scope, so the admin
scope doesn't silently downgrade into extra file permissions).
@zachdunn Zach Dunn (zachdunn) added the coderabbit:review Trigger CodeRabbit review for the PR. label Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d1fceca-40dd-43b5-a04e-690d117d62c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Admin-scoped workspace tokens can be minted by admin-role users, stored with existing token records, and used on /admin/* with read/write authorization. Static ADMIN_TOKEN access remains supported, while file-route scope behavior stays isolated.

Changes

Admin operator token flow

Layer / File(s) Summary
Scope contracts and admin-role detection
apps/api/src/auth-db.ts, apps/api/src/session-auth.ts, apps/api/src/session-auth.test.ts
Adds admin:read and admin:write scope types, optional admin scope validation, widened token input typing, and support for comma-separated admin roles.
Admin-scoped token minting
apps/api/src/routes/tokens.ts, apps/api/src/routes/tokens.test.ts
Allows admin-role sessions to request admin scopes while rejecting them for other users and keeping them opt-in.
Admin route authorization
apps/api/src/admin.ts, apps/api/src/index.ts, apps/api/src/routes/admin-scoped-token.test.ts
Adds D1-backed operator-token authorization with method-based scope checks, while retaining static ADMIN_TOKEN support and recording the authorized token ID.
Scope boundaries and SDK support
apps/api/test/routes-files.test.ts, docs/admin-tokens.md, packages/uploads/src/client.ts, .changeset/admin-scope-widening.md
Documents operator scopes, preserves file-route isolation, widens SDK scope literals, and records a patch release.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant TokenAPI
  participant AuthDB
  participant AdminAPI
  participant D1
  Admin->>TokenAPI: POST /v1/tokens with admin scope
  TokenAPI->>AuthDB: validate scopes using admin role
  AuthDB-->>TokenAPI: validated admin-scoped token
  TokenAPI-->>Admin: workspace token
  Admin->>AdminAPI: /admin/* with bearer token
  AdminAPI->>D1: find active token and scopes
  D1-->>AdminAPI: active token record
  AdminAPI-->>Admin: authorize read/write request
Loading

Possibly related PRs

Poem

A bunny mints a token bright,
With admin read or admin write.
D1 checks its leafy trail,
While file scopes stay in their rail.
Break-glass secrets still remain—
Hop, hop, secure again!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies #257 by gating admin scopes on admin role, keeping defaults file-only, and allowing scoped tokens plus ADMIN_TOKEN for /admin/*.
Out of Scope Changes check ✅ Passed The changes stay focused on admin/operator token minting, related auth logic, docs, tests, and client typings.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: scoped admin/operator token minting for CLI admin operations.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/agent-orchestration-e35e18

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-api 4b6336f Commit Preview URL

Branch Preview URL
Jul 18 2026, 08:51 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-auth 4b6336f Commit Preview URL

Branch Preview URL
Jul 18 2026, 08:51 PM

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/uploads/src/client.ts (1)

247-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared scope type alias to avoid repeating the 5-element union.

The literal union "files:read" | "files:write" | "files:delete" | "admin:read" | "admin:write" is duplicated across four locations. A single type alias would prevent drift if scopes change again.

♻️ Proposed refactor
+export type TokenScope = "files:read" | "files:write" | "files:delete" | "admin:read" | "admin:write";
+
 export interface EnrollmentExchangeResult {
   apiUrl?: string;
   workspace: string;
   token: string;
-  scopes?: Array<"files:read" | "files:write" | "files:delete" | "admin:read" | "admin:write">;
+  scopes?: TokenScope[];
   expiresAt?: string;
 }

Then apply the same substitution at lines 294, 489, and 533.

Also applies to: 294-294, 489-489, 533-533

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/uploads/src/client.ts` at line 247, Extract the repeated five-value
scope union in client.ts into a shared type alias, then replace the inline
unions at the visible scopes declaration and the corresponding declarations near
lines 294, 489, and 533 with that alias. Use the alias consistently so future
scope changes require updating only one definition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/routes/admin-scoped-token.test.ts`:
- Around line 38-44: Update appWith so an explicitly provided undefined
adminToken remains undefined instead of being replaced by ADMIN_TOKEN; use a
presence-aware defaulting approach while retaining ADMIN_TOKEN only when the
option is omitted. Ensure the unset-secret test exercises an absent ADMIN_TOKEN
binding.

---

Nitpick comments:
In `@packages/uploads/src/client.ts`:
- Line 247: Extract the repeated five-value scope union in client.ts into a
shared type alias, then replace the inline unions at the visible scopes
declaration and the corresponding declarations near lines 294, 489, and 533 with
that alias. Use the alias consistently so future scope changes require updating
only one definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f4e7537-c68f-4739-9279-3cd0937996ee

📥 Commits

Reviewing files that changed from the base of the PR and between 4adb66d and fe05adb.

📒 Files selected for processing (12)
  • .changeset/admin-scope-widening.md
  • apps/api/src/admin.ts
  • apps/api/src/auth-db.ts
  • apps/api/src/index.ts
  • apps/api/src/routes/admin-scoped-token.test.ts
  • apps/api/src/routes/tokens.test.ts
  • apps/api/src/routes/tokens.ts
  • apps/api/src/session-auth.test.ts
  • apps/api/src/session-auth.ts
  • apps/api/test/routes-files.test.ts
  • docs/admin-tokens.md
  • packages/uploads/src/client.ts

Comment on lines +38 to +44
function appWith(opts: { adminToken?: string | undefined; db: SqliteD1 }) {
const { adminToken = ADMIN_TOKEN, db } = opts;
const app = new Hono<{ Bindings: Env }>()
.route("/admin", admin)
.onError((err, c) => respondError(c, err));
const env = {
ADMIN_TOKEN: adminToken,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Actually remove ADMIN_TOKEN in the unset-secret test.

Line 39’s destructuring default converts the explicit undefined at Line 156 back to ADMIN_TOKEN, so this test never exercises the unset binding.

Proposed fix
 function appWith(opts: { adminToken?: string | undefined; db: SqliteD1 }) {
-  const { adminToken = ADMIN_TOKEN, db } = opts;
+  const { db } = opts;
+  const adminToken = Object.hasOwn(opts, "adminToken") ? opts.adminToken : ADMIN_TOKEN;
   const app = new Hono<{ Bindings: Env }>()
     .route("/admin", admin)
     .onError((err, c) => respondError(c, err));
   const env = {
-    ADMIN_TOKEN: adminToken,
+    ...(adminToken === undefined ? {} : { ADMIN_TOKEN: adminToken }),
     REGISTRY: fakeKv({ "ws:acme": RECORD }),
     DB: database(db),
   } as unknown as Env;

Also applies to: 150-158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/admin-scoped-token.test.ts` around lines 38 - 44, Update
appWith so an explicitly provided undefined adminToken remains undefined instead
of being replaced by ADMIN_TOKEN; use a presence-aware defaulting approach while
retaining ADMIN_TOKEN only when the option is omitted. Ensure the unset-secret
test exercises an absent ADMIN_TOKEN binding.

Avoid collision with the weaker workspace-admin org role; these scopes
express platform-wide operator authority. Renames ADMIN_SCOPES/AdminScope/
isAdminScope/allowAdmin to their Operator equivalents across apps/api,
packages/uploads, docs, and the changeset.
… TokenScope type (#257)

- appWith() now distinguishes an explicitly-passed undefined adminToken
  from an omitted one, so the "no ADMIN_TOKEN set" test truly exercises
  an absent binding instead of falling back to the default token.
- Extract the repeated five-value scope union in client.ts into a single
  exported TokenScope type used at all four call sites.
@zachdunn
Zach Dunn (zachdunn) merged commit 0d1db80 into main Jul 18, 2026
5 checks passed
@zachdunn
Zach Dunn (zachdunn) deleted the claude/agent-orchestration-e35e18 branch July 18, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coderabbit:review Trigger CodeRabbit review for the PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scoped admin/operator token minting for CLI admin operations

1 participant