Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughBackend and shared JWT utilities now support secret rotation by deriving and accepting keys from both Changes
Sequence Diagram(s)sequenceDiagram
rect rgba(200,230,255,0.5)
participant Client
end
rect rgba(220,255,200,0.5)
participant Server as Backend/IDP
participant JWKS as KeyUtils/JWKS
participant Store as CookieStore
end
Client->>Server: Authenticate / request token
Server->>JWKS: Derive signing key(s) from STACK_SERVER_SECRET (+ optional STACK_SERVER_SECRET_OLD)
JWKS-->>Server: Private JWK(s) (kid_new[, kid_old])
Server-->>Client: Issue JWT (kid_new) + Set cookie (encrypted with key_new)
Client->>Server: Request with cookie / JWT
Server->>Store: Read cookie
alt cookie encrypted with new key
Server->>Server: Decrypt with key_new
else cookie encrypted with old key
Server->>Server: Decrypt with key_old (from STACK_SERVER_SECRET_OLD)
end
Server->>JWKS: Verify JWT using union of public keys (kid_new, kid_old)
JWKS-->>Server: Verification result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 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 |
Updated comment for STACK_SERVER_SECRET_OLD to clarify its purpose.
Greptile SummaryThis PR implements server-secret rotation by deriving a second set of JWKs and OIDC cookie keys from a previous server secret (
Confidence Score: 3/5Not safe to merge — the shipped docker default env and the operator doc both lead to a startup crash for any non-rotating deployment. Two P1 issues create a real production reliability problem: the docker/server/.env ships with the old-secret variable empty, and getOldStackServerSecret throws on empty/missing values, so the server crashes on startup. The misleading .env.example comment compounds the risk for operators following the runbook. docker/server/.env, packages/stack-shared/src/utils/jwt.tsx, and docker/server/.env.example all need fixes before this can be safely deployed. Important Files Changed
Sequence DiagramsequenceDiagram
participant GS as getPrivateJwks()
participant D1 as derivePairForSecret(primary)
participant D2 as derivePairForSecret(previous)
participant S as signJWT()
participant V as verifyJWT()
Note over GS: Deploy 1 rotation window
GS->>D1: primary pair (2 JWKs from new secret)
GS->>D2: old pair (2 JWKs from previous secret)
GS-->>S: returns 4-entry list
S->>S: signs with index 0 only
V->>V: verifies against all 4 keys
Note over GS: Steady state (both secrets equal)
GS->>D1: pair from current secret
GS->>D2: duplicate pair from same secret
GS-->>S: 4 entries, 2 unique kids
Prompt To Fix All With AIThis is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/secret-rotation.test.ts
Line: 1
Comment:
**Leading whitespace on first import**
The first line has 2 extra spaces of indentation before the `import` statement, which will fail lint/formatting checks.
```suggestion
import { isBase64Url } from "@stackframe/stack-shared/dist/utils/bytes";
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: docker/server/.env.example
Line: 7
Comment:
**"Remove after the grace window" instruction will break deployments**
`STACK_SERVER_SECRET_OLD` is required — `getEnvVariable("STACK_SERVER_SECRET_OLD")` (no default) throws when the variable is absent or empty. Instructing operators to remove the variable after the grace window will cause their server to fail to start. The correct steady-state is to keep it set to the current `STACK_SERVER_SECRET` value (not empty, not unset).
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: docker/server/.env
Line: 10
Comment:
**Empty old-secret value causes startup failure**
`getOldStackServerSecret()` calls `getEnvVariable(...)` with no default, so an empty or missing value triggers an immediate `throwErr("Missing environment variable")`. This file ships with the old-secret variable set to an empty string, which means any Docker deployment using this file as-is crashes on startup before serving any requests.
Per the unit-test comment, the variable is required. In steady state it should equal the current server secret; during rotation it holds the previous value.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: packages/stack-shared/src/utils/jwt.tsx
Line: 30-38
Comment:
**`getOldStackServerSecret` throws instead of returning falsy when the var is unset**
`getEnvVariable` (called without a default) throws when the variable is absent or empty. All callers rely on a falsy return to detect the non-rotation case (e.g. `oldSecret ? await derivePairForSecret(oldSecret) : []`). With this code, every non-rotation deployment — including the shipped docker `.env` which leaves the old-secret variable empty — crashes on startup.
If the design requires the variable to always be set, the docker files and operator docs must enforce that explicitly. Alternatively, add a default of `""` and return early to make the non-rotation case safe:
```
const val = getEnvVariable("STACK_SERVER_SECRET_OLD", "");
if (!val) return "";
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "Added tests" | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
Implements a two-phase STACK_SERVER_SECRET rotation mechanism so newly minted JWTs/cookies use the new secret while verification temporarily accepts artifacts signed/encrypted with the previous secret.
Changes:
- Add
STACK_SERVER_SECRET_OLDsupport to JWT key derivation so verification accepts both old/new secrets during the overlap window. - Extend OIDC-provider cookie keyring to include an old-secret-derived key for decrypting/verifying pre-rotation cookies.
- Add vitest coverage pinning rotation invariants and update Docker env templates to document the new variable.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/stack-shared/src/utils/jwt.tsx | Adds optional “old secret” handling to expand the verification key set during rotation. |
| packages/stack-shared/src/utils/jwt.test.ts | Adds tests validating JWT/JWKS behavior across rotation overlap scenarios. |
| docker/server/.env.example | Documents STACK_SERVER_SECRET_OLD in the example server env file. |
| docker/server/.env | Documents STACK_SERVER_SECRET_OLD in the template env file. |
| apps/backend/src/app/api/latest/integrations/idp.ts | Appends old-secret-derived cookie key so OIDC cookies remain readable during the grace window. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/backend/src/app/api/latest/integrations/idp.ts (1)
179-187: Avoid double-invocation ofgetOldStackServerSecret()and switch to explicit null check.
getOldStackServerSecret()is called twice — once for the guard and once inside the template literal. Each call re-readsprocess.envand re-runsjose.base64url.decode, and the two reads are not atomic (an unlikely but real footgun if env changes mid-request, e.g. via a test harness). Cache once.This also pairs with the return-type fix suggested in
jwt.tsx— once that returnsnullfor "unset", the guard here should be an explicit!= nullcheck per coding guidelines.♻️ Proposed fix
cookies: { // oidc-provider passes these to Koa keygrip: index 0 signs new cookies, any entry verifies. // During a STACK_SERVER_SECRET rotation, the old-secret-derived key is appended so cookies // issued before the rotation remain readable until they expire naturally. - keys: [ - toHexString(await sha512(`oidc-idp-cookie-encryption-key:${getEnvVariable("STACK_SERVER_SECRET")}`)), - ...(getOldStackServerSecret() - ? [toHexString(await sha512(`oidc-idp-cookie-encryption-key:${getOldStackServerSecret()}`))] - : []), - ], + keys: await (async () => { + const oldSecret = getOldStackServerSecret(); + return [ + toHexString(await sha512(`oidc-idp-cookie-encryption-key:${getEnvVariable("STACK_SERVER_SECRET")}`)), + ...(oldSecret != null + ? [toHexString(await sha512(`oidc-idp-cookie-encryption-key:${oldSecret}`))] + : []), + ]; + })(), },Or, more cleanly, compute
keysinto a local above thenew Provider(...)call.As per coding guidelines: "Unless very clearly equivalent from types, prefer explicit null/undefinedness checks over boolean checks."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/integrations/idp.ts` around lines 179 - 187, Cache the result of getOldStackServerSecret() into a local variable (e.g., const oldSecret = getOldStackServerSecret()) and compute the keys array in a local variable before calling new Provider(...); call sha512/toHexString once for each needed secret using getEnvVariable("STACK_SERVER_SECRET") and oldSecret, and use an explicit null check (oldSecret != null) instead of a truthy guard so you only invoke getOldStackServerSecret() one time and avoid double-invocation in the keys array.packages/stack-shared/src/utils/jwt.tsx (1)
120-152: Verify downstream JWKS consumer assumptions before merging.The refactor is sound: signing uses
privateJwks[0](legacy derivation of current primary), and old-secret keys are appended when configured. The JWKS endpoint is correctly implemented with 1-hour caching.However, the key count range deserves validation: up to 6 keys published normally (2 per audience × 3 audiences), scaling to 12 during dual-secret overlap. While not large, confirm that no downstream consumer (SDKs, clients with limited key caches, or rate-limited JWKS fetch patterns) assumes bounded key counts. Test
#10covers the dual-secret rotation invariant for a single audience; consider adding coverage for the full 3-audience overlap scenario if not already present.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/stack-shared/src/utils/jwt.tsx` around lines 120 - 152, The change can publish up to 6 keys (or 12 during dual-secret overlap) from getPrivateJwks/derivePairForSecret/getPrivateJwkFromDerivedSecret when three audiences are used; before merging, validate downstream JWKS consumers (SDKs, clients, caches) do not assume a smaller bounded key set and won't break or rate-limit on larger JWKS responses, and add a unit/integration test that simulates all three audiences plus dual-secret overlap to assert the JWKS endpoint returns the expected combined key count and that signing still uses privateJwks[0]; if any consumer cannot handle the larger set, either document the new key-count behavior clearly or limit published keys (e.g., prefer current primary only) and add a note in the codepaths around getPrivateJwks/getStackServerSecret/getOldStackServerSecret.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/stack-shared/src/utils/jwt.tsx`:
- Around line 30-38: getOldStackServerSecret currently always returns a string
("" when unset) which mismatches its declared string | null; change
getOldStackServerSecret to return null when the env value is the empty string
(i.e., after reading STACK_SERVER_SECRET_OLD, if it's "" return null, otherwise
validate with jose.base64url.decode and return the non-empty string); then
update callers to check != null (not truthy) and avoid double calls: update
jwt.tsx usage around derivePairForSecret(oldSecret) to use a stored oldSecret !=
null ? [...], update jwt.test.ts to conditionally include deriveOidcCookieKey
only when old !== null, and in idp.ts read getOldStackServerSecret() once into a
local (e.g., oldSecret) and use oldSecret != null to decide the branch.
---
Nitpick comments:
In `@apps/backend/src/app/api/latest/integrations/idp.ts`:
- Around line 179-187: Cache the result of getOldStackServerSecret() into a
local variable (e.g., const oldSecret = getOldStackServerSecret()) and compute
the keys array in a local variable before calling new Provider(...); call
sha512/toHexString once for each needed secret using
getEnvVariable("STACK_SERVER_SECRET") and oldSecret, and use an explicit null
check (oldSecret != null) instead of a truthy guard so you only invoke
getOldStackServerSecret() one time and avoid double-invocation in the keys
array.
In `@packages/stack-shared/src/utils/jwt.tsx`:
- Around line 120-152: The change can publish up to 6 keys (or 12 during
dual-secret overlap) from
getPrivateJwks/derivePairForSecret/getPrivateJwkFromDerivedSecret when three
audiences are used; before merging, validate downstream JWKS consumers (SDKs,
clients, caches) do not assume a smaller bounded key set and won't break or
rate-limit on larger JWKS responses, and add a unit/integration test that
simulates all three audiences plus dual-secret overlap to assert the JWKS
endpoint returns the expected combined key count and that signing still uses
privateJwks[0]; if any consumer cannot handle the larger set, either document
the new key-count behavior clearly or limit published keys (e.g., prefer current
primary only) and add a note in the codepaths around
getPrivateJwks/getStackServerSecret/getOldStackServerSecret.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8844b9d6-59a0-421f-aa72-3098f0d93749
📒 Files selected for processing (5)
apps/backend/src/app/api/latest/integrations/idp.tsdocker/server/.envdocker/server/.env.examplepackages/stack-shared/src/utils/jwt.test.tspackages/stack-shared/src/utils/jwt.tsx
|
Clean Code review — Major finding
|
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
packages/stack-shared/src/utils/jwt.tsx (1)
30-37:⚠️ Potential issue | 🔴 CriticalReturn
nullwhenSTACK_SERVER_SECRET_OLDis unset.The helper still requires and returns a string, so the “new secret only” phase can fail instead of omitting secondary keys. Normalize the optional env var to
nulland use an explicit nullish check at the call site.Proposed fix
-export function getOldStackServerSecret(): string { - const STACK_SERVER_SECRET_OLD = getEnvVariable("STACK_SERVER_SECRET_OLD"); +export function getOldStackServerSecret(): string | null { + const STACK_SERVER_SECRET_OLD = getEnvVariable("STACK_SERVER_SECRET_OLD", ""); + if (STACK_SERVER_SECRET_OLD === "") return null; try { jose.base64url.decode(STACK_SERVER_SECRET_OLD); } catch (e) { throw new StackAssertionError("STACK_SERVER_SECRET_OLD is set but not a valid base64url string. Remove it, or set it to the previous STACK_SERVER_SECRET value.", { cause: e }); } return STACK_SERVER_SECRET_OLD; }const primaryPair = await derivePairForSecret(getStackServerSecret()); const oldSecret = getOldStackServerSecret(); - const oldPair = oldSecret ? await derivePairForSecret(oldSecret) : []; + const oldPair = oldSecret != null ? await derivePairForSecret(oldSecret) : [];Also applies to: 147-148
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/stack-shared/src/utils/jwt.tsx` around lines 30 - 37, Update getOldStackServerSecret to treat STACK_SERVER_SECRET_OLD as optional: if getEnvVariable("STACK_SERVER_SECRET_OLD") returns undefined/null/empty, return null instead of throwing or returning a string; only call jose.base64url.decode when the env value is present and, on decode failure, throw the existing StackAssertionError with the decode error as cause. Change the function signature/return type to allow null and ensure the callers use an explicit nullish check before using the returned value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/e2e/tests/backend/endpoints/api/v1/secret-rotation.test.ts`:
- Around line 52-68: The test currently fetches the JWKS after sign-up so it
verifies against the refreshed keys; instead, before calling
Auth.Password.signUpWithEmail() use niceBackendFetch(INTERNAL_JWKS_PATH) to
snapshot/cache the pre-rotation JWKS, then perform the sign-up
(Auth.Password.signUpWithEmail) to trigger the key-roll, obtain the minted
access token from backendContext.value.userAuth?.accessToken, and verify that
token cryptographically against the cached set (use jose.createLocalJWKSet on
the cached jwks and jose.jwtVerify) to ensure a verifier built from the old JWKS
still validates tokens minted during rollout; afterwards you can fetch the live
JWKS and assert the new kid is present if needed.
- Around line 29-50: getPrivateJwks returns duplicate JWKS when
STACK_SERVER_SECRET equals STACK_SERVER_SECRET_OLD; change getPrivateJwks to
detect identical secrets (compare STACK_SERVER_SECRET and
STACK_SERVER_SECRET_OLD or derived public values) and skip adding the old-secret
key pair when identical so it yields only the primary pair, and update the test
in secret-rotation.test.ts to assert exactly 2 unique kids when secrets are
identical and exactly 4 unique kids when they differ (use getPrivateJwks or the
env comparison to decide which assertion to run), referencing getPrivateJwks and
the JWKS endpoint check in the test.
In `@packages/stack-shared/src/utils/jwt.test.ts`:
- Around line 73-145: Tests 1, 3, and 5 assert the wrong rotation order: Deploy
1 must continue signing with the old secret while publishing the new JWKS, not
immediately sign with the new secret. Update the specs that reference signJWT,
getPrivateJwks, setRotatingEnv, setSteadyStateEnv, verifyJWT, and
jose.decodeProtectedHeader so that Phase 1 (cached-JWKS) asserts Deploy 1 still
signs with the old-secret kid and that new JWKS are published (i.e.,
getPrivateJwks returns the new kid but signJWT returns old kid), and move the
assertions that new tokens are signed with the new secret into Phase 2 /
post-cache tests; also add an explicit cached-JWKS Phase 1 test to confirm
cached verification behavior during overlap.
- Around line 25-31: The helper setSteadyStateEnv currently sets
STACK_SERVER_SECRET_OLD equal to STACK_SERVER_SECRET which masks the "no
secondary secret" path; change setSteadyStateEnv to set
process.env.STACK_SERVER_SECRET = secret and remove/unset
process.env.STACK_SERVER_SECRET_OLD (delete or set to undefined) so tests model
a steady state with no secondary secret; update any references in jwt.test.ts
that expect duplicate JWKS entries to instead handle the single-secret steady
state via setSteadyStateEnv.
---
Duplicate comments:
In `@packages/stack-shared/src/utils/jwt.tsx`:
- Around line 30-37: Update getOldStackServerSecret to treat
STACK_SERVER_SECRET_OLD as optional: if
getEnvVariable("STACK_SERVER_SECRET_OLD") returns undefined/null/empty, return
null instead of throwing or returning a string; only call jose.base64url.decode
when the env value is present and, on decode failure, throw the existing
StackAssertionError with the decode error as cause. Change the function
signature/return type to allow null and ensure the callers use an explicit
nullish check before using the returned value.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 520240ae-64f7-4187-b531-45cd58631358
📒 Files selected for processing (4)
apps/backend/src/app/api/latest/integrations/idp.tsapps/e2e/tests/backend/endpoints/api/v1/secret-rotation.test.tspackages/stack-shared/src/utils/jwt.test.tspackages/stack-shared/src/utils/jwt.tsx
…T key derivation logic
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/stack-shared/src/utils/jwt.test.ts (1)
73-82:⚠️ Potential issue | 🟠 MajorRe-check Deploy 1 signing against cached JWKS consumers.
These specs still require new tokens to be signed with the new
kidimmediately once both secrets are configured. If any verifier holds a pre-Deploy-1 JWKS cache, those tokens can fail verification until cache refresh; Phase 1 should publish the new key while continuing to sign with the old key, then flip signing after the cache window if cached JWKS consumers are in scope.Also applies to: 99-145
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/stack-shared/src/utils/jwt.test.ts` around lines 73 - 82, The test assumes immediate flip-to-new-kid signing after setRotatingEnv, but Phase 1 requires publishing the new key while continuing to sign with the old key so cached JWKS consumers don't fail; update the test around setRotatingEnv, signJWT, getPrivateJwks, verifyJWT and jose.decodeProtectedHeader to assert that after calling setRotatingEnv the new JWKS entry exists (getPrivateJwks contains the new kid) while the freshly signed JWT still carries the previous (old) kid and verifies successfully; then separately simulate the post-cache-refresh path (or trigger the rotate-to-new-signing step) and assert that subsequent signJWT tokens use the new kid. Ensure you reference setRotatingEnv, signJWT, verifyJWT, getPrivateJwks and jose.decodeProtectedHeader when making these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/stack-shared/src/utils/jwt.test.ts`:
- Around line 113-116: The test is bypassing type safety by casting header.kid
(and another double-cast at variable k); update the tests to remove the "as
string" and double-casts: after decoding the JWT header in the signJWT flow
(variable header from jose.decodeProtectedHeader) add an explicit null/undefined
guard like if (header.kid == null) throw new Error(...) before calling
preRotationKids.has or other assertions, and replace the double-cast assertion
on k with expect(k).not.toHaveProperty("d") so the type system is respected;
keep the existing assertions around verifyJWT unchanged.
---
Duplicate comments:
In `@packages/stack-shared/src/utils/jwt.test.ts`:
- Around line 73-82: The test assumes immediate flip-to-new-kid signing after
setRotatingEnv, but Phase 1 requires publishing the new key while continuing to
sign with the old key so cached JWKS consumers don't fail; update the test
around setRotatingEnv, signJWT, getPrivateJwks, verifyJWT and
jose.decodeProtectedHeader to assert that after calling setRotatingEnv the new
JWKS entry exists (getPrivateJwks contains the new kid) while the freshly signed
JWT still carries the previous (old) kid and verifies successfully; then
separately simulate the post-cache-refresh path (or trigger the
rotate-to-new-signing step) and assert that subsequent signJWT tokens use the
new kid. Ensure you reference setRotatingEnv, signJWT, verifyJWT, getPrivateJwks
and jose.decodeProtectedHeader when making these assertions.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5b8566ee-0841-4178-a679-3b9e84c34cab
📒 Files selected for processing (2)
apps/e2e/tests/backend/endpoints/api/v1/secret-rotation.test.tspackages/stack-shared/src/utils/jwt.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/e2e/tests/backend/endpoints/api/v1/secret-rotation.test.ts
| const mintedJwt = await signJWT({ issuer: "iss", audience: "aud", payload: { sub: "user-3" } }); | ||
| const header = jose.decodeProtectedHeader(mintedJwt); | ||
| expect(preRotationKids.has(header.kid as string)).toBe(false); | ||
| await expect(verifyJWT({ allowedIssuers: ["iss"], jwt: mintedJwt })).resolves.toBeTruthy(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd packages/stack-shared && find . -name "jwt.test.ts" -type fRepository: stack-auth/stack-auth
Length of output: 88
🏁 Script executed:
cat -n packages/stack-shared/src/utils/jwt.test.ts | sed -n '105,250p'Repository: stack-auth/stack-auth
Length of output: 7163
🏁 Script executed:
head -50 packages/stack-shared/src/utils/jwt.test.ts | cat -nRepository: stack-auth/stack-auth
Length of output: 2562
🏁 Script executed:
cat -n packages/stack-shared/src/utils/jwt.ts | head -150Repository: stack-auth/stack-auth
Length of output: 135
🏁 Script executed:
find packages/stack-shared -name "jwt.ts" -type fRepository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
grep -r "export.*signJWT" packages/stack-shared/src --include="*.ts"Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
find packages/stack-shared -type f -name "*.ts" | head -20Repository: stack-auth/stack-auth
Length of output: 1057
🏁 Script executed:
grep -r "function signJWT\|const signJWT\|export.*signJWT" packages --include="*.ts" | grep -v testRepository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
rg "signJWT\s*[:=]" packages --type ts | head -20Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
rg "signJWT" packages/stack-shared/src/utils/jwt.test.ts -A 2 | head -5Repository: stack-auth/stack-auth
Length of output: 333
🏁 Script executed:
ls -la packages/stack-shared/src/utils/Repository: stack-auth/stack-auth
Length of output: 4237
🏁 Script executed:
cat -n packages/stack-shared/src/utils/jwt.tsxRepository: stack-auth/stack-auth
Length of output: 8193
🏁 Script executed:
cat -n packages/stack-shared/src/utils/jwt.tsx | sed -n '56,72p'Repository: stack-auth/stack-auth
Length of output: 727
Remove type casts that bypass safety checks.
The as string casts on header.kid allow undefined values to pass through, which would cause Set.has(undefined) to incorrectly return false and mask missing kid values. Similarly, the double cast on line 236 unnecessarily bypasses the type system. Use explicit null checks and .toHaveProperty() instead:
- Lines 115 & 144: Add
if (header.kid == null) throw new Error(...)before assertions - Line 236: Replace the double cast with
expect(k).not.toHaveProperty("d")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/stack-shared/src/utils/jwt.test.ts` around lines 113 - 116, The test
is bypassing type safety by casting header.kid (and another double-cast at
variable k); update the tests to remove the "as string" and double-casts: after
decoding the JWT header in the signJWT flow (variable header from
jose.decodeProtectedHeader) add an explicit null/undefined guard like if
(header.kid == null) throw new Error(...) before calling preRotationKids.has or
other assertions, and replace the double-cast assertion on k with
expect(k).not.toHaveProperty("d") so the type system is respected; keep the
existing assertions around verifyJWT unchanged.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
docker/server/.env.example (1)
9-10: Clarify the comment to specify what to remove and when.The comment "Remove after the grace window" is ambiguous—it doesn't specify what to remove (just
STACK_SERVER_SECRET_OLD, or both lines 9-10?) or the grace window duration. Given the PR description mentions a ~1 day transition period and the reviewer's finding thatgetOldStackServerSecretreturns an empty string (not null) when unset, it's critical to completely remove the line rather than leaving an empty value.📝 Proposed clarification
-# Remove after the grace window -STACK_SERVER_SECRET_OLD=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo +# TODO next-release: Remove STACK_SERVER_SECRET_OLD after the ~1 day grace period +STACK_SERVER_SECRET_OLD=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyoThis makes it clear that:
- The entire
STACK_SERVER_SECRET_OLDline should be removed (not just emptied)- The approximate grace window duration is ~1 day
- The removal is tracked as a next-release cleanup item
Based on learnings, the codebase uses
// TODO next-releaseto mark temporary backward-compatibility shims for single-release cleanup.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docker/server/.env.example` around lines 9 - 10, Update the ambiguous comment above STACK_SERVER_SECRET_OLD to explicitly state that the entire STACK_SERVER_SECRET_OLD line should be removed (not just emptied), specify the grace window (~1 day), and mark it for follow-up by adding the project’s cleanup marker (e.g., "// TODO next-release"); ensure this matches the runtime behavior of getOldStackServerSecret which returns an empty string when unset so the line must be deleted in the next release rather than left present with no value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@docker/server/.env.example`:
- Around line 9-10: Update the ambiguous comment above STACK_SERVER_SECRET_OLD
to explicitly state that the entire STACK_SERVER_SECRET_OLD line should be
removed (not just emptied), specify the grace window (~1 day), and mark it for
follow-up by adding the project’s cleanup marker (e.g., "// TODO next-release");
ensure this matches the runtime behavior of getOldStackServerSecret which
returns an empty string when unset so the line must be deleted in the next
release rather than left present with no value.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 56d12f13-358b-469d-9593-f02a5a09eee4
📒 Files selected for processing (1)
docker/server/.env.example
This PR is for rotating the STACK_SERVER_SECRET. Right now we are using the secret for creating and verifying jwts. For the rotation, we will essentially need to do two deployments. We first deploy with a new stack server secret and an old stack server secret both. We sign the new jwts with the new stack server secret. The verification takes place with the old stack server secret and the new stack server secret. After a while [a day?], we do another deployment where we just use the new stack server secret and not the old one for signing and verification both.
Summary by CodeRabbit
New Features
Tests
Chores