feat(relayauth): self-sufficient workspace-path mint#48
Conversation
|
CodeAnt AI is reviewing your PR. |
|
Warning Review limit reached
More reviews will be available in 46 minutes and 7 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a new ChangesWorkspace-path token issuance feature
Sequence DiagramsequenceDiagram
participant Caller as Org with API Key
participant SDK as RelayAuthClient
participant Handler as POST /v1/tokens/workspace-path
participant TokenSvc as Token Service
Caller->>SDK: issueWorkspacePathToken({workspaceId, paths, scopes, ttlSeconds})
SDK->>Handler: POST with x-api-key header
Handler->>Handler: validate org API key grant<br/>validate workspaceId ownership
Handler->>Handler: normalize paths<br/>derive path scopes
Handler->>Handler: enforce requested scopes ⊆ granted scopes
Handler->>Handler: cap TTL at 1h
Handler->>TokenSvc: issueTokenPair(path metadata)
TokenSvc-->>Handler: {accessToken, refreshToken}
Handler-->>SDK: WorkspacePathTokenPair<br/>(tokenClass: "relay_pa")
SDK-->>Caller: return token pair with paths
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new endpoint POST /v1/tokens/workspace-path to directly mint short-lived path-scoped tokens using an organization API key and a workspace ID, along with corresponding SDK client updates, type definitions, and tests. A critical issue was identified in the route handler where auth.claims.sponsorId and auth.claims.sponsorChain can be undefined when using an Org API Key, which could lead to a runtime TypeError (500 error) inside createPathTokenIdentity. A defensive fallback has been suggested to resolve this.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const identity = createPathTokenIdentity({ | ||
| agentId, | ||
| agentName, | ||
| orgId: auth.claims.org, | ||
| workspaceId, | ||
| sponsorId: auth.claims.sponsorId, | ||
| sponsorChain: auth.claims.sponsorChain, | ||
| scopes: auth.claims.scopes, | ||
| }); |
There was a problem hiding this comment.
When minting a token via /workspace-path using an Org API Key, the auth.claims object represents the API key itself rather than a delegated user session. Consequently, auth.claims.sponsorId and auth.claims.sponsorChain will be undefined.
Passing these directly to createPathTokenIdentity will cause a runtime TypeError when it attempts to read options.sponsorChain.length, crashing the request with a 500 error.
We should defensively default sponsorId to agentId and sponsorChain to an empty array [] to ensure safe fallback behavior.
| const identity = createPathTokenIdentity({ | |
| agentId, | |
| agentName, | |
| orgId: auth.claims.org, | |
| workspaceId, | |
| sponsorId: auth.claims.sponsorId, | |
| sponsorChain: auth.claims.sponsorChain, | |
| scopes: auth.claims.scopes, | |
| }); | |
| const identity = createPathTokenIdentity({ | |
| agentId, | |
| agentName, | |
| orgId: auth.claims.org, | |
| workspaceId, | |
| sponsorId: auth.claims.sponsorId ?? agentId, | |
| sponsorChain: auth.claims.sponsorChain ?? [], | |
| scopes: auth.claims.scopes, | |
| }); |
|
CodeAnt AI finished reviewing your PR. |
|
Implemented fixes for PR #48:
Verification run locally:
Note: plain |
|
✅ pr-reviewer applied fixes — committed and pushed Implemented fixes for PR #48:
Verification run locally:
Note: plain |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@packages/server/src/routes/tokens.ts`:
- Around line 424-454: The handler registered in tokens.post("/workspace-path")
currently trusts any auth with relayauth:api-key:manage:* and uses
body.workspaceId (workspaceId) without checking workspace-token scope; call
resolveWorkspaceToken(...) after authenticateAndAuthorizeFromContext to detect
workspace tokens and then either (A) reject workspace tokens by returning a 403
when resolveWorkspaceToken(...) yields a workspace token, or (B) enforce the
workspace boundary by comparing workspaceId ===
resolvedWorkspaceToken.workspaceId and returning 403/400 if they differ; update
the code around authenticateAndAuthorizeFromContext, resolveWorkspaceToken, and
the workspaceId check to implement one of these two behaviors.
- Around line 474-499: The route issues a path token without checking
MAX_SPONSOR_CHAIN_DEPTH even though createPathTokenIdentity appends agentId to
auth.claims.sponsorChain, which can produce an overlong sponsorChain that later
fails refresh; before calling issueTokenPair in this handler, replicate the same
depth guard used in /v1/tokens and /v1/tokens/agent: compute newDepth =
(auth.claims.sponsorChain?.length || 0) + 1 (or equivalent), compare against
MAX_SPONSOR_CHAIN_DEPTH, and return the appropriate error/abort if the limit
would be exceeded so issueTokenPair is only called when the sponsorChain remains
within the allowed depth.
🪄 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 Plus
Run ID: 3b511fc3-a33c-41f7-a12c-77943ec77cf9
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (11)
.trajectories/completed/2026-06/traj_1774612574920_f3fdc1c5.json.trajectories/completed/2026-06/traj_1774612574920_f3fdc1c5.md.trajectories/index.jsonREADME.mdpackages/sdk/typescript/src/__tests__/client-tokens.test.tspackages/sdk/typescript/src/client.tspackages/sdk/typescript/src/index.tspackages/server/src/__tests__/tokens-route.test.tspackages/server/src/routes/tokens.tspackages/types/src/token.tspackages/types/tsconfig.tsbuildinfo
💤 Files with no reviewable changes (1)
- packages/types/tsconfig.tsbuildinfo
| tokens.post("/workspace-path", async (c) => { | ||
| const auth = await authenticateAndAuthorizeFromContext( | ||
| c, | ||
| "relayauth:api-key:manage:*", | ||
| matchScope, | ||
| ); | ||
| if (!auth.ok) { | ||
| return c.json({ error: auth.error, code: auth.code }, auth.status); | ||
| } | ||
|
|
||
| const body = await parseJsonObjectBody<PathTokenRequest>(c.req.raw); | ||
| if (!body) { | ||
| return c.json({ error: "Invalid JSON body" }, 400); | ||
| } | ||
|
|
||
| const workspaceId = normalizeOptionalString(body.workspaceId); | ||
| if (!workspaceId) { | ||
| return c.json({ error: "workspaceId is required", code: "workspaceId_required" }, 400); | ||
| } | ||
|
|
||
| const storage = getSqlStorage(c.get("storage")); | ||
| // Direct workspace-path minting is intentionally equivalent to: | ||
| // POST /v1/tokens/workspace (org API key + caller-supplied workspaceId) | ||
| // followed by /v1/tokens/path. | ||
| // The org API key grant is the authorization boundary here; the workspace | ||
| // row is not an auth source and must not be required on this hot path. If | ||
| // local context does know the workspace, it must still belong to this org. | ||
| const workspaceContext = await storage.contexts.getWorkspace(workspaceId); | ||
| if (workspaceContext && workspaceContext.orgId !== auth.claims.org) { | ||
| return c.json({ error: "workspace_not_found", code: "workspace_not_found" }, 404); | ||
| } |
There was a problem hiding this comment.
Reject workspace tokens here or pin them to their own workspace.
This route only checks for relayauth:api-key:manage:*, but unlike /v1/tokens/path it never calls resolveWorkspaceToken(...). Since /v1/tokens/workspace can mint workspace tokens with caller-granted scopes, a workspace token carrying relayauth:api-key:manage:* can reach this endpoint and mint path tokens for any same-org or unseeded workspaceId, bypassing the workspace boundary.
Please either reject workspace tokens on this route or enforce requestedWorkspaceId === resolvedWorkspaceToken.workspaceId before minting.
🤖 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/server/src/routes/tokens.ts` around lines 424 - 454, The handler
registered in tokens.post("/workspace-path") currently trusts any auth with
relayauth:api-key:manage:* and uses body.workspaceId (workspaceId) without
checking workspace-token scope; call resolveWorkspaceToken(...) after
authenticateAndAuthorizeFromContext to detect workspace tokens and then either
(A) reject workspace tokens by returning a 403 when resolveWorkspaceToken(...)
yields a workspace token, or (B) enforce the workspace boundary by comparing
workspaceId === resolvedWorkspaceToken.workspaceId and returning 403/400 if they
differ; update the code around authenticateAndAuthorizeFromContext,
resolveWorkspaceToken, and the workspaceId check to implement one of these two
behaviors.
| const identity = createPathTokenIdentity({ | ||
| agentId, | ||
| agentName, | ||
| orgId: auth.claims.org, | ||
| workspaceId, | ||
| sponsorId: auth.claims.sponsorId, | ||
| sponsorChain: auth.claims.sponsorChain, | ||
| scopes: auth.claims.scopes, | ||
| }); | ||
|
|
||
| const tokenPair = await issueTokenPair(storage, c.env, identity, { | ||
| accessScopes: accessScopes.scopes, | ||
| accessAudience, | ||
| accessExpiresIn, | ||
| action: "token.issued", | ||
| meta: { | ||
| tokenClass: "path", | ||
| agentName, | ||
| paths: JSON.stringify(paths.paths), | ||
| accessScopes: JSON.stringify(accessScopes.scopes), | ||
| accessAudience: JSON.stringify(accessAudience), | ||
| }, | ||
| wrapAccessToken: true, | ||
| wrapRefreshToken: true, | ||
| tokenIdPrefix: RELAY_PATH_TOKEN_PREFIX, | ||
| }); |
There was a problem hiding this comment.
Enforce the sponsor-chain depth limit before issuing direct path tokens.
createPathTokenIdentity(...) appends the derived agentId to auth.claims.sponsorChain, but this route never re-checks MAX_SPONSOR_CHAIN_DEPTH. If the caller is already at depth 10, the new token is issued with an invalid chain and will later fail refresh in /v1/tokens/refresh, which does enforce that limit.
Add the same depth guard used in /v1/tokens and /v1/tokens/agent before issueTokenPair(...).
🤖 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/server/src/routes/tokens.ts` around lines 474 - 499, The route
issues a path token without checking MAX_SPONSOR_CHAIN_DEPTH even though
createPathTokenIdentity appends agentId to auth.claims.sponsorChain, which can
produce an overlong sponsorChain that later fails refresh; before calling
issueTokenPair in this handler, replicate the same depth guard used in
/v1/tokens and /v1/tokens/agent: compute newDepth =
(auth.claims.sponsorChain?.length || 0) + 1 (or equivalent), compare against
MAX_SPONSOR_CHAIN_DEPTH, and return the appropriate error/abort if the limit
would be exceeded so issueTokenPair is only called when the sponsorChain remains
within the allowed depth.
dfbf601 to
a3d1d42
Compare
There was a problem hiding this comment.
4 issues found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="README.md">
<violation number="1" location="README.md:56">
P3: README omits the supported `expiresIn` field from `WorkspacePathTokenIssueRequest`, so the direct-mint contract is documented too narrowly.</violation>
</file>
<file name="packages/server/src/routes/tokens.ts">
<violation number="1" location="packages/server/src/routes/tokens.ts:434">
P1: `/workspace-path` does not enforce org-API-key callers; bearer/other key kinds can mint direct workspace path tokens for arbitrary `workspaceId`.</violation>
<violation number="2" location="packages/server/src/routes/tokens.ts:473">
P0: Do not trust the requested `workspaceId` for workspace-token callers. Pin this field to the caller’s workspace claim (or reject workspace tokens here), otherwise a workspace token with `relayauth:api-key:manage:*` can mint path tokens outside its workspace boundary.</violation>
<violation number="3" location="packages/server/src/routes/tokens.ts:479">
P2: Add a sponsor-chain depth check before issuing the token pair. Without this guard, this endpoint can mint tokens that already exceed `MAX_SPONSOR_CHAIN_DEPTH` and fail on later refresh.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| agentId, | ||
| agentName, | ||
| orgId: auth.claims.org, | ||
| workspaceId, |
There was a problem hiding this comment.
P0: Do not trust the requested workspaceId for workspace-token callers. Pin this field to the caller’s workspace claim (or reject workspace tokens here), otherwise a workspace token with relayauth:api-key:manage:* can mint path tokens outside its workspace boundary.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/src/routes/tokens.ts, line 473:
<comment>Do not trust the requested `workspaceId` for workspace-token callers. Pin this field to the caller’s workspace claim (or reject workspace tokens here), otherwise a workspace token with `relayauth:api-key:manage:*` can mint path tokens outside its workspace boundary.</comment>
<file context>
@@ -413,6 +421,88 @@ tokens.post("/path", async (c) => {
+ agentId,
+ agentName,
+ orgId: auth.claims.org,
+ workspaceId,
+ sponsorId: auth.claims.sponsorId,
+ sponsorChain: auth.claims.sponsorChain,
</file context>
| return c.json({ error: auth.error, code: auth.code }, auth.status); | ||
| } | ||
|
|
||
| const body = await parseJsonObjectBody<PathTokenRequest>(c.req.raw); |
There was a problem hiding this comment.
P1: /workspace-path does not enforce org-API-key callers; bearer/other key kinds can mint direct workspace path tokens for arbitrary workspaceId.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/src/routes/tokens.ts, line 434:
<comment>`/workspace-path` does not enforce org-API-key callers; bearer/other key kinds can mint direct workspace path tokens for arbitrary `workspaceId`.</comment>
<file context>
@@ -413,6 +421,88 @@ tokens.post("/path", async (c) => {
+ return c.json({ error: auth.error, code: auth.code }, auth.status);
+ }
+
+ const body = await parseJsonObjectBody<PathTokenRequest>(c.req.raw);
+ if (!body) {
+ return c.json({ error: "Invalid JSON body" }, 400);
</file context>
| scopes: auth.claims.scopes, | ||
| }); | ||
|
|
||
| const tokenPair = await issueTokenPair(storage, c.env, identity, { |
There was a problem hiding this comment.
P2: Add a sponsor-chain depth check before issuing the token pair. Without this guard, this endpoint can mint tokens that already exceed MAX_SPONSOR_CHAIN_DEPTH and fail on later refresh.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/src/routes/tokens.ts, line 479:
<comment>Add a sponsor-chain depth check before issuing the token pair. Without this guard, this endpoint can mint tokens that already exceed `MAX_SPONSOR_CHAIN_DEPTH` and fail on later refresh.</comment>
<file context>
@@ -413,6 +421,88 @@ tokens.post("/path", async (c) => {
+ scopes: auth.claims.scopes,
+ });
+
+ const tokenPair = await issueTokenPair(storage, c.env, identity, {
+ accessScopes: accessScopes.scopes,
+ accessAudience,
</file context>
| const tokenPair = await issueTokenPair(storage, c.env, identity, { | |
| if ((auth.claims.sponsorChain?.length ?? 0) >= MAX_SPONSOR_CHAIN_DEPTH) { | |
| return c.json({ error: "sponsor_chain_too_deep", code: "sponsor_chain_too_deep" }, 400); | |
| } | |
| const tokenPair = await issueTokenPair(storage, c.env, identity, { |
| scopes?: string[]; | ||
| audience?: string[]; | ||
| expiresIn?: number; | ||
| ttlSeconds?: number; // capped to 3600s server-side |
There was a problem hiding this comment.
P3: README omits the supported expiresIn field from WorkspacePathTokenIssueRequest, so the direct-mint contract is documented too narrowly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 56:
<comment>README omits the supported `expiresIn` field from `WorkspacePathTokenIssueRequest`, so the direct-mint contract is documented too narrowly.</comment>
<file context>
@@ -48,16 +48,29 @@ type AgentTokenIssueRequest = {
scopes?: string[];
audience?: string[];
expiresIn?: number;
+ ttlSeconds?: number; // capped to 3600s server-side
+};
+
</file context>
a3d1d42 to
88b0fae
Compare
|
Reviewed and fixed PR #48. Changes made:
Verification run:
|
|
Reviewed PR #48 against Local checks run:
Note: I had to run |
|
Reviewed PR #48 against the provided diff and traced the route, tests, SDK/types, and CI workflow impact. I did not find any code changes needed. Verification run locally:
Note: the default parallel Turbo build hit local exit |
|
Reviewed PR #48 against the provided diff and traced the route, tests, SDK/types, and CI workflow impact. I did not find any code changes needed. Verification run locally:
Note: the default parallel Turbo build hit local exit |
User description
Summary
workspaceslookup fromPOST /v1/tokens/workspace-pathand mint directly from the org API-key claim plus caller-suppliedworkspaceId.workspaceId, scope containment, and traversal rejection..github/workflows/publish.ymlowns the version bump during the sanctioned publish flow.Validation
npm run -w @relayauth/server typechecknode --test --import tsx packages/server/src/__tests__/tokens-route.test.tsNotes
workflow_dispatchafter merge, using the workflow-managed version bump and provenance publish.CodeAnt-AI Description
Add direct workspace path token minting
What Changed
relay_pa_*token directly from an org API key andworkspaceId, without needing a pre-existing workspace tokenworkspaceId, direct minting without a seeded workspace row, org stamping, TTL limits, scope checks, and invalid pathsImpact
✅ Shorter path-token setup✅ Fewer workspace-token dependencies✅ Clearer direct-mint API usage💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.