Skip to content

feat(tokens): long-lived operator session refresh tokens (90-day TTL with auto-propagation)#54

Merged
khaliqgant merged 4 commits into
mainfrom
feat/operator-session-refresh-ttl
Jun 17, 2026
Merged

feat(tokens): long-lived operator session refresh tokens (90-day TTL with auto-propagation)#54
khaliqgant merged 4 commits into
mainfrom
feat/operator-session-refresh-ttl

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds refreshTokenTtlSeconds parameter to POST /v1/tokens, capped at 90 days (MAX_OPERATOR_REFRESH_TOKEN_TTL_SECONDS)
  • TTL is embedded in meta.refreshTokenTtl on both access and refresh claims at issuance
  • POST /v1/tokens/refresh reads meta.refreshTokenTtl and reissues each rotated refresh token with the same TTL — silent indefinite renewal without interactive re-login
  • Adds IdentityTokenIssueRequest type to @relayauth/types
  • 5 new tests covering: TTL issuance, 90d cap enforcement, 24h default fallback, and TTL propagation through rotation

Motivation

Fixes the root cause described in handoff/agent-relay-session-token-ttl.md: operator sessions had a fixed 24h refresh TTL, forcing interactive agent-relay cloud login daily. This is the relayauth layer of a three-repo fix (also touches agent-relay and cloud).

Contract for consumers

Cloud (relayfile delegated token mint): Pass refreshTokenTtlSeconds: 7776000 in the body of POST /v1/tokens (and /v1/tokens/path, /v1/tokens/agent) when minting operator/delegated sessions.

Agent-relay: The refreshTokenExpiresAt field in TokenPair responses is already populated — track it in the canonical auth store and trigger refresh proactively (before access token expires, with a backstop before refresh token expires).

Test plan

  • npm run test -w packages/server → 360/360 pass
  • tsc --noEmit clean on both packages/server and packages/types
  • Default behavior unchanged (no refreshTokenTtlSeconds → 24h refresh TTL as before)
  • Cap enforced at 90 days regardless of caller input
  • TTL propagates through rotation without caller re-supplying it

🤖 Generated with Claude Code

Review in cubic

Proactive Runtime Bot and others added 2 commits June 15, 2026 00:15
…h BASE_URL

When BASE_URL is set, resolveJwksUrl pointed the verifier at <BASE_URL>/.well-known/jwks.json.
On Cloudflare the worker's BASE_URL is its OWN custom domain (api.relayauth.dev), so
verification made the worker fetch itself — a self-subrequest that fails ("Failed to fetch
JWKS"), throwing in verifyRs256Token and rejecting EVERY RS256 bearer as invalid_token. Only
RS256-bearer auth (e.g. the admin-bearer api-key mint) was affected; x-api-key auth never
fetches JWKS. The worker already holds RELAYAUTH_SIGNING_KEY_PEM_PUBLIC, so build the single-key
JWKS inline (a data: URL, no network) and only fall back to a network fetch when no public key
is bound.

Diagnosed live via worker logs: verifyToken threw "RelayAuthError: Failed to fetch JWKS".
…ions

Allows callers to request a custom refresh token TTL (up to 90 days) on
POST /v1/tokens. The TTL is stored in token meta (refreshTokenTtl) and
propagated through each rotation on POST /v1/tokens/refresh, so an
unattended host can hold a 30–90 day session that silently renews without
interactive re-login.

- POST /v1/tokens accepts refreshTokenTtlSeconds (capped at 90d)
- issueTokenPair embeds the TTL in meta on both access and refresh claims
- POST /v1/tokens/refresh reads meta.refreshTokenTtl and reissues with
  the same TTL on each rotation
- IdentityTokenIssueRequest type added to @relayauth/types
- 5 new tests covering TTL issuance, capping, defaulting, and propagation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@khaliqgant, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 43 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 16cc27ef-878a-4c5f-b625-11ac23545348

📥 Commits

Reviewing files that changed from the base of the PR and between bff4f62 and 8d38952.

📒 Files selected for processing (2)
  • packages/server/src/__tests__/tokens-route.test.ts
  • packages/server/src/routes/tokens.ts
📝 Walkthrough

Walkthrough

Adds an optional refreshTokenTtlSeconds parameter to POST /v1/tokens, normalizes and caps it at 90 days, embeds the effective TTL in JWT meta.refreshTokenTtl, and propagates it through POST /v1/tokens/refresh rotation. A new IdentityTokenIssueRequest type is exported. resolveJwksUrl is updated to prefer an inline JWKS built from the local public key PEM over a network fetch.

Changes

Configurable Refresh Token TTL

Layer / File(s) Summary
Type contracts and constants
packages/types/src/token.ts, packages/server/src/routes/tokens.ts
Adds IdentityTokenIssueRequest interface with refreshTokenTtlSeconds, extends IssueTokenRequest and issueTokenPair options type with the same field, and introduces MAX_OPERATOR_REFRESH_TOKEN_TTL_SECONDS and REFRESH_TOKEN_TTL_META_KEY constants.
TTL helpers and issueTokenPair core logic
packages/server/src/routes/tokens.ts
Introduces normalizeRefreshTokenTtl (parse/cap) and parseMetaRefreshTokenTtl (extract from meta), then uses them in issueTokenPair to compute refreshExpiresAt and build mergedMeta that injects refreshTokenTtl into both access and refresh claims.
Route handler wiring
packages/server/src/routes/tokens.ts
POST /v1/tokens normalizes refreshTokenTtlSeconds before calling issueTokenPair; POST /v1/tokens/refresh extracts the TTL from the presented token's meta and forwards it to preserve it across rotation.
Refresh TTL test coverage
packages/server/src/__tests__/tokens-route.test.ts
Tests cover: explicit TTL with ~30-day expiry, 90-day cap enforcement, 24h default with absent meta.refreshTokenTtl, and rotation preserving original TTL.

JWKS URL Resolution Fix

Layer / File(s) Summary
resolveJwksUrl fallback order
packages/server/src/lib/token-verifier.ts
Inverts resolution priority: inline data: JWKS from RELAYAUTH_SIGNING_KEY_PEM_PUBLIC first, then BASE_URL/.well-known/jwks.json, then http://127.0.0.1:8787/.well-known/jwks.json.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • AgentWorkforce/relayauth#51: Modifies the same resolveJwksUrl function in token-verifier.ts to build an inline data: JWKS from RELAYAUTH_SIGNING_KEY_PEM_PUBLIC with the same fallback pattern.

Poem

🐇 Hop hop, the tokens now remember their age,
A TTL baked right onto the page!
Ninety days max, or twenty-four low,
Rotation keeps the original in tow.
And JWKS fetched from local PEM first—
No more self-fetch to quench our thirst! 🔑

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Title check ✅ Passed The title clearly summarizes the main feature: adding long-lived operator session refresh tokens with a 90-day TTL cap and automatic propagation through token rotation.
Description check ✅ Passed The description is directly related to the changeset, providing clear context about the feature, motivation, consumer contract, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/operator-session-refresh-ttl

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.

❤️ Share

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

… /agent endpoints

Cloud's delegated-token mint calls POST /v1/tokens/path, /v1/tokens/workspace-path,
and /v1/tokens/agent — not just POST /v1/tokens. Without this fix those endpoints
would silently ignore refreshTokenTtlSeconds and fall back to the 24h default,
breaking the relayfile unattended-refresh contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member Author

Cross-repo contract blocker from Cloud review:

Cloud PR https://github.com/AgentWorkforce/cloud/pull/2260 sends refreshTokenTtlSeconds: 7776000 on delegated Relayfile token-pair mint bodies for /v1/tokens/path, /v1/tokens/workspace-path, and /v1/tokens/agent. Those are the endpoints used by Cloud delegated Relayfile credentials.

This PR currently appears to wire refreshTokenTtlSeconds only into POST /v1/tokens and refresh propagation. I do not see the path/workspace-path/agent handlers parsing the field or passing it to issueTokenPair, so delegated Relayfile credentials would still receive the default refresh TTL even though Cloud sends the 90d request.

Please accept/cap refreshTokenTtlSeconds on the delegated token-pair endpoints Cloud uses, pass it into the common token-pair issuance path, and add route coverage showing a path/workspace-path or agent token refresh TTL propagates through /v1/tokens/refresh. Until then the unattended relayfile delegated credential portion of the handoff is not complete.

Note: I tried to submit this as a request-changes review, but GitHub disallowed it because the PR author account matches this reviewer account.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bff4f62dfb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

accessScopes,
accessAudience,
accessExpiresIn,
refreshTokenTtlSeconds,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Thread refresh TTL through delegated token endpoints

This only passes the requested refresh TTL for POST /v1/tokens; the delegated/operator endpoints still use AgentTokenRequest/PathTokenRequest and call issueTokenPair without refreshTokenTtlSeconds, so a caller following the commit contract and sending refreshTokenTtlSeconds to /v1/tokens/agent, /v1/tokens/path, or /v1/tokens/workspace-path gets a 24h refresh token with no meta.refreshTokenTtl. Those sessions then rotate with the default 24h TTL, leaving the daily re-login problem unfixed for the relayfile delegated token mint flow.

Useful? React with 👍 / 👎.

@khaliqgant

Copy link
Copy Markdown
Member Author

Re-review after commit 00f1da7: the code contract now matches Cloud. I can see refreshTokenTtlSeconds parsed and forwarded for /v1/tokens/agent, /v1/tokens/path, and /v1/tokens/workspace-path.

Remaining proof gap: the new tests still appear to cover only plain /v1/tokens; the endpoint-fix commit seems to change only packages/server/src/routes/tokens.ts. Please add at least one route test for a Cloud-used delegated endpoint, preferably /v1/tokens/workspace-path, proving that refreshTokenTtlSeconds: 7776000 is embedded in token meta and survives /v1/tokens/refresh.

Once that test is in, Cloud considers the relayauth contract aligned and proven.

… rotation

Adds the delegated-endpoint regression test requested during cross-review:
mints a relay_pa via POST /v1/tokens/workspace-path with refreshTokenTtlSeconds
equal to the Cloud-used 90d value, asserts meta.refreshTokenTtl is present on
both minted tokens, then calls /v1/tokens/refresh and asserts the rotated pair
retains the same TTL and meta.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@khaliqgant khaliqgant left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Relay cross-review against AgentWorkforce/relay#1148 and AgentWorkforce/cloud#2260:

The current code path appears contract-aligned: refreshTokenTtlSeconds is now parsed/capped and forwarded for /v1/tokens, /v1/tokens/agent, /v1/tokens/path, and /v1/tokens/workspace-path, and /v1/tokens/refresh preserves the TTL from meta.refreshTokenTtl.

Remaining proof gap before full signoff: please add one delegated endpoint regression test, preferably /v1/tokens/workspace-path, that sends refreshTokenTtlSeconds: 7776000, asserts token meta includes refreshTokenTtl: "7776000", refreshes through /v1/tokens/refresh, and asserts the rotated refresh token retains the same TTL/meta. That directly covers the Cloud-used Relayfile delegated credential path.

@khaliqgant

Copy link
Copy Markdown
Member Author

Re-review after commit 8d38952: proof gap is closed. The new /v1/tokens/workspace-path test sends refreshTokenTtlSeconds: 7776000, checks minted token meta, refreshes via /v1/tokens/refresh, and checks the rotated pair keeps the TTL/meta.

Cloud contract review is satisfied for relayauth PR #54. Recommended order remains relayauth -> cloud -> relay.

@khaliqgant

Copy link
Copy Markdown
Member Author

Relay cross-review final signoff: the delegated endpoint proof gap is closed. Current diff includes a /v1/tokens/workspace-path test with refreshTokenTtlSeconds: 7776000, verifies meta.refreshTokenTtl, refreshes through /v1/tokens/refresh, and verifies the rotated pair retains TTL/meta. Contract now matches Cloud PR #2260 and Relay PR #1148.

Note: GitHub would not accept a formal approval from this shared automation identity because it treats it as approving its own PR, so recording signoff as a comment.

@khaliqgant

Copy link
Copy Markdown
Member Author

Addressing Codex bot review (discussion_r3430966670):

The P1 finding — refreshTokenTtlSeconds not threaded through the delegated endpoints — was already caught and fixed in the second commit (00f1da7b) before the bot comment was posted. All three delegated endpoints now parse, cap, and forward the field:

  • /v1/tokens/agent via AgentTokenRequest.refreshTokenTtlSeconds
  • /v1/tokens/path via PathTokenRequest.refreshTokenTtlSeconds
  • /v1/tokens/workspace-path via PathTokenRequest.refreshTokenTtlSeconds

The third commit (8d38952d) adds the regression test proving the Cloud-used delegated path works end-to-end: POST /v1/tokens/workspace-path with refreshTokenTtlSeconds: 7776000 → asserts meta.refreshTokenTtl === "7776000" on minted tokens → POST /v1/tokens/refresh → asserts rotated pair keeps same TTL/meta. 361/361 passing.

@khaliqgant khaliqgant merged commit d4c3d03 into main Jun 17, 2026
4 checks passed
@khaliqgant khaliqgant deleted the feat/operator-session-refresh-ttl branch June 17, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant