-
Notifications
You must be signed in to change notification settings - Fork 474
Cookie subdomain sharing #971
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds domain-aware refresh-token cookie handling (set/getAll/delete, domain/secure flags, isSecure), expands client project schema with Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Browser
participant App as Client App
participant CookieLib as Cookie Helper
participant Server as Server
Note over Browser,Server: signup / signin flow
Browser->>App: initiate sign-in/signup
App->>CookieLib: isSecure() -> determine secure context
App->>Server: request trusted parent domain
Server-->>App: trustedParentDomain
rect rgb(200,240,220)
Note over App,CookieLib: Write structured refresh cookies (default + optional custom)
App->>CookieLib: set(defaultName, value, { secure?, domain? })
App->>CookieLib: set(customName, value, { secure?, domain: trustedParentDomain })
CookieLib-->>Browser: cookies written with attributes
end
rect rgb(220,230,255)
Note over Browser,App: Read & select freshest token across cookie map
Browser->>App: request tokens
App->>CookieLib: getAll()
CookieLib-->>App: Record<string,string>
App->>App: parse structured values -> pick newest
App-->>Browser: tokens returned
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2025-10-11T04:13:19.308ZApplied to files:
🧬 Code graph analysis (2)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (5)
packages/template/src/lib/cookie.ts (1)
🪛 Biome (2.1.2)packages/template/src/lib/cookie.ts[error] 181-181: This code will never be reached ... ... because this statement will return from the function beforehand (lint/correctness/noUnreachable) ⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Greptile Overview
Summary
This PR implements optional cookie subdomain sharing to allow authentication cookies to be shared across subdomains (e.g., app.example.com and api.example.com).
Key changes:
- Added
shareCookiesAcrossSubdomainsconfiguration option toStackClientApp - Implemented
extractBaseDomainFromHost()to extract the base domain from hostnames - Modified cookie operations to accept and use
domainparameter when subdomain sharing is enabled - Added comprehensive E2E test to verify subdomain cookie sharing behavior
- Updated both client-side (browser) and server-side (Next.js) cookie handling
Critical issues found:
- The
extractBaseDomainFromHost()function doesn't handle Public Suffix List (PSL) domains correctly, which creates a security vulnerability for sites hosted on shared domains likeherokuapp.com,github.io, or country-code TLDs likeco.uk - Cookie deletion order in
deleteCookieClient()may fail to remove cookies that were set with a domain attribute
Confidence Score: 1/5
- This PR has critical security vulnerabilities that must be addressed before merging
- The
extractBaseDomainFromHostfunction creates a significant security issue by not handling Public Suffix List domains. On shared hosting platforms (Heroku, GitHub Pages, Vercel, etc.) or country-code TLDs, this will extract public suffixes as the base domain, allowing authentication cookies to leak across completely unrelated applications. This is a critical security vulnerability that could expose user sessions. - Critical attention needed:
packages/stack-shared/src/utils/urls.tsx(PSL vulnerability). Important:packages/template/src/lib/cookie.ts(cookie deletion bug)
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| packages/stack-shared/src/utils/urls.tsx | 1/5 | Added extractBaseDomainFromHost function with critical PSL vulnerability that could share cookies across unrelated domains |
| packages/template/src/lib/cookie.ts | 2/5 | Added domain parameter to cookie operations; deletion order issue may leave cookies undeleted |
| packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts | 3/5 | Implemented subdomain cookie sharing with domain extraction from host headers and window.location |
Sequence Diagram
sequenceDiagram
participant Browser
participant ClientApp
participant CookieHelper
participant ExtractDomain
participant JSCookie
Note over Browser,ClientApp: User signs in with shareCookiesAcrossSubdomains enabled
Browser->>ClientApp: signInWithCredential()
ClientApp->>ClientApp: _getBrowserCookieDomain()
ClientApp->>ExtractDomain: extractBaseDomainFromHost(window.location.host)
ExtractDomain-->>ClientApp: example.com
ClientApp->>CookieHelper: setOrDeleteCookieClient(refresh-token)
CookieHelper->>JSCookie: set with domain=example.com
JSCookie->>Browser: Set refresh cookie
ClientApp->>CookieHelper: setOrDeleteCookieClient(access-token)
CookieHelper->>JSCookie: set with domain=example.com
JSCookie->>Browser: Set access cookie
ClientApp->>CookieHelper: deleteCookieClient(legacy stack-refresh)
CookieHelper->>JSCookie: remove with domain
CookieHelper->>JSCookie: remove without domain
Note over Browser: Cookies accessible on all example.com subdomains
Browser->>ClientApp: Navigate to subdomain.example.com
Browser->>ClientApp: Cookies sent automatically
ClientApp->>ClientApp: User authenticated
6 files reviewed, 2 comments
Documentation Update RequiredChanges are needed in the following file: [
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
2286-2296: Preserve shareCookiesAcrossSubdomains in serialized options
toClientJson()still omitsshareCookiesAcrossSubdomains. When we hydrate a client app from JSON, the option silently falls back tofalse, so cross-subdomain cookies break on the client even if they were enabled on the server. Please include the new flag in the serialized object (and ensurefromClientJsonkeeps receiving it).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/e2e/tests/js/app.test.ts(2 hunks)packages/stack-shared/src/utils/ips.tsx(1 hunks)packages/stack-shared/src/utils/urls.tsx(4 hunks)packages/template/src/lib/cookie.ts(5 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts(8 hunks)packages/template/src/lib/stack-app/apps/interfaces/client-app.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
packages/template/src/lib/stack-app/apps/interfaces/client-app.tspackages/stack-shared/src/utils/ips.tsxapps/e2e/tests/js/app.test.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.tspackages/template/src/lib/cookie.tspackages/stack-shared/src/utils/urls.tsx
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When changes are needed for stack or js packages, make them in packages/template instead
Files:
packages/template/src/lib/stack-app/apps/interfaces/client-app.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.tspackages/template/src/lib/cookie.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
When writing tests, prefer .toMatchInlineSnapshot over other selectors where possible
Files:
apps/e2e/tests/js/app.test.ts
🧬 Code graph analysis (3)
apps/e2e/tests/js/app.test.ts (2)
apps/e2e/tests/helpers.ts (1)
it(12-12)apps/e2e/tests/js/js-helpers.ts (1)
createApp(41-86)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)
packages/stack-shared/src/utils/env.tsx (1)
isBrowserLike(4-6)packages/stack-shared/src/utils/urls.tsx (1)
extractBaseDomainFromHost(229-239)packages/template/src/lib/cookie.ts (3)
setOrDeleteCookieClient(161-168)deleteCookieClient(175-181)setOrDeleteCookie(170-173)
packages/stack-shared/src/utils/urls.tsx (1)
packages/stack-shared/src/utils/ips.tsx (1)
isIpAddress(6-8)
🪛 Biome (2.1.2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
[error] 444-444: This code will never be reached ...
... because either this statement, ...
... this statement, ...
... or this statement will return from the function beforehand
(lint/correctness/noUnreachable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: lint_and_build (latest)
- GitHub Check: restart-dev-and-test
- GitHub Check: build (22.x)
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: all-good
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: docker
- GitHub Check: Security Check
There was a problem hiding this comment.
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)
apps/e2e/tests/js/app.test.ts (1)
158-177: Follow the Map-over-Record guideline.Coding guidelines ask us to use ES6
Mapinstead ofRecordin TS files. Please convert the attribute accumulator to aMapand update the callers to read via.get().Apply this diff:
- const parseCookieAttributes = (name: string) => { + const parseCookieAttributes = (name: string): Map<string, string> | null => { const raw = [...cookieWrites].reverse().find((entry) => entry.trim().toLowerCase().startsWith(`${name.toLowerCase()}=`)); if (!raw) { return null; } const [, ...attributeParts] = raw.split(";").map((part) => part.trim()).filter(Boolean); - const attrs: Record<string, string> = {}; + const attrs = new Map<string, string>(); for (const attribute of attributeParts) { const [attrName, ...attrValueParts] = attribute.split("="); - attrs[attrName.toLowerCase()] = attrValueParts.join("=") || ""; + attrs.set(attrName.toLowerCase(), attrValueParts.join("=") || ""); } return attrs; }; const refreshAttrs = parseCookieAttributes(`stack-refresh-${clientApp.projectId}`); - expect(refreshAttrs?.domain).toBe("example.com"); + expect(refreshAttrs?.get("domain")).toBe("example.com"); const accessAttrs = parseCookieAttributes("stack-access"); - expect(accessAttrs?.domain).toBe("example.com"); + expect(accessAttrs?.get("domain")).toBe("example.com");
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/e2e/tests/js/app.test.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
apps/e2e/tests/js/app.test.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
When writing tests, prefer .toMatchInlineSnapshot over other selectors where possible
Files:
apps/e2e/tests/js/app.test.ts
🧬 Code graph analysis (1)
apps/e2e/tests/js/app.test.ts (2)
apps/e2e/tests/helpers.ts (1)
it(12-12)apps/e2e/tests/js/js-helpers.ts (1)
createApp(41-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: docker
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: restart-dev-and-test
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: all-good
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: Security Check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
apps/e2e/tests/js/app.test.ts (1)
147-166: Restore stubbed globals with try/finally to keep tests isolated.Globals are stubbed but never restored; wrap body and call vi.unstubAllGlobals() in finally.
- vi.stubGlobal("window", fakeWindow); - vi.stubGlobal("document", fakeDocument); - vi.stubGlobal("sessionStorage", fakeSessionStorage); - - const { clientApp } = await createApp( + vi.stubGlobal("window", fakeWindow); + vi.stubGlobal("document", fakeDocument); + vi.stubGlobal("sessionStorage", fakeSessionStorage); + try { + const { clientApp } = await createApp( { config: { domains: [ { domain: "https://example.com", handlerPath: "/handler" }, { domain: "https://*.example.com", handlerPath: "/handler" }, ], } }, { client: { tokenStore: "cookie", noAutomaticPrefetch: true, }, } - ); + ); ... - expect(cookieWrites.some((entry) => entry.toLowerCase().startsWith("stack-refresh=") && entry.toLowerCase().includes("expires="))).toBe(true); -}); + expect(cookieWrites.some((entry) => entry.toLowerCase().startsWith("stack-refresh=") && entry.toLowerCase().includes("expires="))).toBe(true); + } finally { + vi.unstubAllGlobals(); + } +});Also applies to: 168-242
packages/template/src/lib/cookie.ts (1)
204-210: Avoid duplicate cookie deletes; only delete with domain OR without.Still calls Cookies.remove(name) unconditionally after domain-specific remove. Do one or the other to prevent redundant ops and accidental host-only deletion.
export function deleteCookieClient(name: string, options: DeleteCookieOptions = {}) { ensureClient(); - if (options.domain !== undefined) { - Cookies.remove(name, { domain: options.domain }); - } - Cookies.remove(name); + if (options.domain !== undefined) { + Cookies.remove(name, { domain: options.domain }); + } else { + Cookies.remove(name); + } }
🧹 Nitpick comments (6)
packages/template/src/lib/cookie.ts (2)
217-224: Set path=/ explicitly to satisfy __Host- requirements and be explicit.*Relying on library defaults is brittle; __Host-* cookies must have Path=/ and no Domain. Add path: "/" on all writes.
export function setCookieClient(name: string, value: string, options: SetCookieOptions = {}) { ensureClient(); Cookies.set(name, value, { expires: options.maxAge === undefined ? undefined : new Date(Date.now() + (options.maxAge) * 1000), domain: options.domain, secure: options.secure, + path: "/", }); }
173-188: isSecure(): LGTM with a small nit.Looks good. Optionally check x-forwarded-proto before reading cookies to skip an extra lookup.
apps/e2e/tests/js/app.test.ts (2)
217-229: Prefer Map over Record for parsed cookie attributes.Aligns with repo guideline to use ES6 Maps in TS. Also avoids accidental proto collisions.
- const parseCookieAttributes = (name: string) => { + const parseCookieAttributes = (name: string): Map<string, string> | null => { const raw = [...cookieWrites].reverse().find((entry) => entry.trim().toLowerCase().startsWith(`${name.toLowerCase()}=`)); if (!raw) { return null; } const [, ...attributeParts] = raw.split(";").map((part) => part.trim()).filter(Boolean); - const attrs: Record<string, string> = {}; - for (const attribute of attributeParts) { - const [attrName, ...attrValueParts] = attribute.split("="); - attrs[attrName.toLowerCase()] = attrValueParts.join("=") || ""; - } - return attrs; + const attrs = new Map<string, string>(); + for (const attribute of attributeParts) { + const [attrName, ...attrValueParts] = attribute.split("="); + attrs.set(attrName.toLowerCase(), attrValueParts.join("=") || ""); + } + return attrs; };Update callsites accordingly, e.g.:
-const defaultAttrs = parseCookieAttributes(defaultCookieName); -expect(defaultAttrs?.domain).toBeUndefined(); -expect(defaultAttrs).not.toBeNull(); -expect(Object.prototype.hasOwnProperty.call(defaultAttrs!, "secure")).toBe(true); +const defaultAttrs = parseCookieAttributes(defaultCookieName); +expect(defaultAttrs).not.toBeNull(); +expect(defaultAttrs!.has("domain")).toBe(false); +expect(defaultAttrs!.has("secure")).toBe(true); -const customAttrs = parseCookieAttributes(customCookieName); -expect(customAttrs?.domain).toBe("example.com"); +const customAttrs = parseCookieAttributes(customCookieName); +expect(customAttrs!.get("domain")).toBe("example.com");As per coding guidelines.
231-238: Strengthen assertions: enforce Path=/ and Secure on both cookies.Assert __Host-* invariants and that the domain cookie is also Secure.
- expect(defaultAttrs?.domain).toBeUndefined(); - expect(defaultAttrs).not.toBeNull(); - expect(Object.prototype.hasOwnProperty.call(defaultAttrs!, "secure")).toBe(true); + expect(defaultAttrs).not.toBeNull(); + expect(defaultAttrs!.has("domain")).toBe(false); + expect(defaultAttrs!.get("path")).toBe("/"); + expect(defaultAttrs!.has("secure")).toBe(true); - const customAttrs = parseCookieAttributes(customCookieName); - expect(customAttrs?.domain).toBe("example.com"); + const customAttrs = parseCookieAttributes(customCookieName); + expect(customAttrs!.get("domain")).toBe("example.com"); + expect(customAttrs!.has("secure")).toBe(true); + expect(customAttrs!.get("path")).toBe("/");packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (2)
619-647: Choose the trusted parent domain that matches the current base domain._current implementation may pick the wrong candidate when multiple parent domains exist (e.g., example.com vs example.co.uk). Prefer the one matching the current base domain; fall back to the first.
- private async _fetchTrustedParentDomain(): Promise<string | null> { + private async _fetchTrustedParentDomain(): Promise<string | null> { const project = Result.orThrow(await this._interface.getClientProject()); const domains = project.config.domains; @@ - const selected = candidates[0] ?? null; - return selected; + // Prefer the candidate that matches the current base domain (browser or server) + let currentBase: string | undefined; + if (isBrowserLike()) { + currentBase = this._getBrowserBaseDomain(); + } else { + currentBase = await this._getServerBaseDomain(); + } + const selected = + (currentBase && candidates.find((c) => c === currentBase)) ?? + candidates[0] ?? + null; + return selected;
539-550: Minor: remove unreachable return.After the try/catch both branches return; the final return is unreachable in Next builds. Safe to drop.
private async _getServerBaseDomain(): Promise<string | undefined> { // IF_PLATFORM next try { const resolvedHeaders = typeof nextHeaders === "function" ? await nextHeaders() : nextHeaders; const hostHeader = resolvedHeaders?.get("x-forwarded-host") ?? resolvedHeaders?.get("host"); return hostHeader ? extractBaseDomainFromHost(hostHeader) : undefined; } catch { return undefined; } // END_PLATFORM - return undefined; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/e2e/tests/js/app.test.ts(2 hunks)packages/stack-shared/src/interface/crud/projects.ts(1 hunks)packages/template/src/lib/cookie.ts(10 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts(8 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
packages/stack-shared/src/interface/crud/projects.tsapps/e2e/tests/js/app.test.tspackages/template/src/lib/cookie.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
When writing tests, prefer .toMatchInlineSnapshot over other selectors where possible
Files:
apps/e2e/tests/js/app.test.ts
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When changes are needed for stack or js packages, make them in packages/template instead
Files:
packages/template/src/lib/cookie.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🧬 Code graph analysis (4)
packages/stack-shared/src/interface/crud/projects.ts (1)
packages/stack-shared/src/schema-fields.ts (1)
yupArray(213-216)
apps/e2e/tests/js/app.test.ts (3)
apps/e2e/tests/helpers.ts (2)
it(12-12)value(82-86)apps/e2e/tests/js/js-helpers.ts (1)
createApp(41-86)packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase32(50-72)
packages/template/src/lib/cookie.ts (1)
packages/stack-shared/src/utils/env.tsx (1)
isBrowserLike(4-6)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (8)
packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase32(50-72)packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
TokenObject(147-150)packages/stack-shared/src/utils/env.tsx (1)
isBrowserLike(4-6)packages/stack-shared/src/utils/urls.tsx (1)
extractBaseDomainFromHost(229-239)packages/template/src/lib/cookie.ts (5)
deleteCookieClient(204-210)setOrDeleteCookie(199-202)setOrDeleteCookieClient(190-197)deleteCookie(212-215)setCookie(226-229)packages/stack-shared/src/utils/promises.tsx (1)
runAsynchronously(343-366)packages/stack-shared/src/utils/strings.tsx (1)
stringCompare(61-65)packages/stack-shared/src/utils/stores.tsx (1)
Store(24-64)
🪛 Biome (2.1.2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
[error] 549-549: This code will never be reached ...
... because either this statement ...
... or this statement will return from the function beforehand
(lint/correctness/noUnreachable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: all-good
- GitHub Check: lint_and_build (latest)
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: Security Check
🔇 Additional comments (3)
packages/stack-shared/src/interface/crud/projects.ts (1)
111-111: The domains field addition is appropriate and aligns with existing client-side usage.Client code is already accessing
project.config.domainsin multiple places—including iframe parent domain detection, domain management UI, and URL validation—confirming this field is needed in the client schema. The change properly formalizes what is already being used in production code.packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (2)
676-686: Cookie writes: default cookie path and legacy cleanup are solid. One nit.The default cookie uses __Host- when secure. With the cookie.ts change to path "/", __Host-* will be standards-compliant. Legacy deletions look correct.
Also applies to: 688-695
710-726: Server cookie writes: LGTM; uses isSecure() to decide __Host- vs host-only.*No action needed.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (10)
apps/e2e/tests/js/cookies.test.ts (10)
1-2: Avoid dist import; drop TextEncoder import
- Importing from a package’s dist path is brittle. Prefer the public entrypoint.
- TextEncoder is global in Node ≥18; the explicit import is unnecessary.
Apply one of these (verify export availability):
-import { encodeBase32 } from "@stackframe/stack-shared/dist/utils/bytes"; -import { TextEncoder } from "util"; +import { encodeBase32 } from "@stackframe/stack-shared"; // If not re‑exported: // import { encodeBase32 } from "@stackframe/stack-shared/utils/bytes";If you keep encodeBase32 usage, rely on the global TextEncoder (no import needed).
56-74: Make cookie deletion semantics closer to browsersRight now, cookies are deleted only when the value is empty. Real deletions also happen via
Max-Age=0or a pastExpires. Aligning the shim avoids stale entries lingering incookieStore.Apply:
set: (value: string) => { cookieWrites.push(value); const [pair] = value.split(";").map((part) => part.trim()).filter(Boolean); if (!pair) { return; } const [rawName, ...rawValueParts] = pair.split("="); const name = rawName.trim(); const storedValue = rawValueParts.join("="); - if (storedValue === "") { - cookieStore.delete(name); - } else { - cookieStore.set(name, storedValue); - } + // Parse attributes for deletion semantics + const attrs = new Map<string, string>(); + for (const part of value.split(";").slice(1)) { + const [k, ...v] = part.trim().split("="); + attrs.set(k.toLowerCase(), v.join("=") || ""); + } + const maxAge = attrs.get("max-age"); + const expires = attrs.get("expires"); + const isExpired = + maxAge === "0" || + (expires ? !Number.isNaN(Date.parse(expires)) && Date.parse(expires) <= Date.now() : false); + if (storedValue === "" || isExpired) cookieStore.delete(name); + else cookieStore.set(name, storedValue); },
76-79: Restore stubbed globals after each testGlobals are stubbed but never un-stubbed, risking cross-test leakage. Add a teardown.
-import { vi } from "vitest"; +import { vi, afterEach } from "vitest"; ... vi.stubGlobal("window", fakeWindow); vi.stubGlobal("document", fakeDocument); vi.stubGlobal("sessionStorage", fakeSessionStorage); return { cookieStore, cookieWrites, location, }; } + +// Ensure no pollution across tests +afterEach(() => { + // Vitest >=1.2 + (vi as any).unstubAllGlobals?.(); +});If your Vitest version lacks
unstubAllGlobals, store previous values insetupBrowserCookieEnvand return acleanup()you call in tests.
98-110: Normalize Domain attribute (handle leading dot)Some setters write
Domain=.example.com. Normalizing avoids brittle assertions.- for (const attribute of attributeParts) { + for (const attribute of attributeParts) { const [attrName, ...attrValueParts] = attribute.split("="); - attrs.set(attrName.toLowerCase(), attrValueParts.join("=") || ""); + const key = attrName.toLowerCase(); + let val = attrValueParts.join("=") || ""; + if (key === "domain") val = val.replace(/^\./, ""); + attrs.set(key, val); }
163-168: Trim long waits to keep e2e fast10s waits will slow CI. Unless truly necessary, reduce to 2–3s and tighten polling.
-const customReady = await waitUntil(() => cookieStore.has(customCookieName), 10_000); +const customReady = await waitUntil(() => cookieStore.has(customCookieName), 3_000); ... -const valuesEqual = await waitUntil(() => cookieStore.get(customCookieName) === cookieStore.get(defaultCookieName), 10_000); +const valuesEqual = await waitUntil(() => cookieStore.get(customCookieName) === cookieStore.get(defaultCookieName), 3_000);Optionally make
intervalMssmaller for these checks (e.g., 50ms).Also applies to: 172-174
181-185: Assert full __Host- invariantsFor
__Host-cookies, browsers requireSecure, noDomain, andPath=/. You’re asserting the first two; addPath=/to catch regressions.expect(defaultAttrs).not.toBeNull(); expect(defaultAttrs?.has("secure")).toBe(true); expect(defaultAttrs?.get("domain")).toBeUndefined(); +expect(defaultAttrs?.get("path")).toBe("/");Confirm the writer sets
Path=/for__Host-cookies.
186-188: Relax exact match on Domain to tolerate leading dotAvoid false negatives if the implementation writes
.example.com.- expect(customAttrs?.get("domain")).toBe("example.com"); + expect(customAttrs?.get("domain")).toBe("example.com"); // after helper normalization + // If you don't normalize in the helper, use: + // expect(customAttrs?.get("domain")?.replace(/^\./, "")).toBe("example.com");
243-296: Consider snapshotting cookie attribute mapsWhere attributes are stable (e.g., default insecure cookie), inline snapshots can surface unintended changes (e.g., SameSite flips) per test guidelines.
// Example: expect(Object.fromEntries(insecureAttrs!)).toMatchInlineSnapshot(` { "path": "/", // "samesite": "lax", // include if applicable/stable } `);As per coding guidelines
313-320: Prefer Map over Record; convert at call site if neededGuideline favors ES6 Map. You can still feed an object to the existing API via
Object.fromEntries.-const cookieMap: Record<string, string> = { - [defaultCookieName]: staleCookieValue, - [customCookieName]: freshCookieValue, - "stack-access": JSON.stringify(["fresh-token", "fresh-access-token"]), -}; - -const tokens = (clientApp as any)._getTokensFromCookies(cookieMap); +const cookieMap = new Map<string, string>([ + [defaultCookieName, staleCookieValue], + [customCookieName, freshCookieValue], + ["stack-access", JSON.stringify(["fresh-token", "fresh-access-token"])], +]); +const tokens = (clientApp as any)._getTokensFromCookies(Object.fromEntries(cookieMap));If
_getTokensFromCookiesalready accepts aMap, pass it directly.
298-320: Avoid coupling to private API
_getTokensFromCookiesis an internal method; tests may break on refactors. Prefer a public surface or a small exported helper.Expose a stable helper (e.g.,
extractTokensFromCookies(cookies: Map<string,string>|Record<string,string>)) and test that instead.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/e2e/tests/js/app.test.ts(1 hunks)apps/e2e/tests/js/cookies.test.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/e2e/tests/js/app.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
apps/e2e/tests/js/cookies.test.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
When writing tests, prefer .toMatchInlineSnapshot over other selectors where possible
Files:
apps/e2e/tests/js/cookies.test.ts
🧬 Code graph analysis (1)
apps/e2e/tests/js/cookies.test.ts (3)
apps/e2e/tests/helpers.ts (2)
value(82-86)it(12-12)packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase32(50-72)apps/e2e/tests/js/js-helpers.ts (1)
createApp(41-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: all-good
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: setup-tests
- GitHub Check: restart-dev-and-test
- GitHub Check: lint_and_build (latest)
- GitHub Check: Security Check
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Outdated
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Outdated
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
574-589: CRITICAL: Refresh token cookie lacks Secure flag in browser context.As flagged in existing comments, the browser path writes domain cookies without the Secure flag, exposing refresh tokens to potential downgrade attacks. The Secure flag must be set based on the current protocol.
Per the existing discussion with N2D4, the ideal solution is to make
setCookieClientautomatically set the Secure flag based onwindow.location.protocol === "https:". This would fix this issue and prevent similar bugs in the future.As a temporary fix, apply this diff:
const setCookie = async (targetDomain: string, value: string) => { const name = this._getCustomRefreshCookieName(targetDomain); - await setOrDeleteCookieWrapper(name, value, { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true }); + const baseOptions = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true } as const; + if (context === "browser") { + await setOrDeleteCookieWrapper( + name, + value, + { ...baseOptions, secure: typeof window !== "undefined" && window.location.protocol === "https:" } + ); + } else { + await setOrDeleteCookieWrapper(name, value, baseOptions); + } };However, the better long-term solution is to update
setCookieClientinpackages/template/src/lib/cookie.tsto automatically setsecurebased on the protocol, as discussed in the existing comments.
🧹 Nitpick comments (3)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)
437-442: Consider renamingupdated_attoupdated_at_millisfor consistency.As noted in the past review comment, the field name could be more explicit about the unit. This would align with other timestamp fields in the codebase.
Based on past review comments.
Apply this diff to rename the field:
private _formatRefreshCookieValue(refreshToken: string, updatedAt: number): string { return JSON.stringify({ refresh_token: refreshToken, - updated_at: updatedAt, + updated_at_millis: updatedAt, }); }Also update the corresponding parsing logic in
_parseStructuredRefreshCookieto useupdated_at_millis.
551-573: Consider simplifying legacy cookie deletion per past review comments.The implementation is correct, but as noted in past review comments, this could be simplified:
- Line 552: Direct call to
deleteCookieClientinstead of intermediate variable- Lines 561-572: Use
awaitstatements instead ofPromise.allfor cleaner codeThe domain-based deletion (lines 554-558, 565-571) is necessary because cookies set with a domain attribute are distinct from those without, so both must be cleaned up.
Based on past review comments.
Apply this diff:
private _deleteLegacyBrowserRefreshCookies() { - deleteCookieClient(this._refreshTokenCookieName); + deleteCookieClient(this._refreshTokenCookieName); deleteCookieClient("stack-refresh"); const domain = this._getBrowserBaseDomain(); if (domain) { deleteCookieClient(this._refreshTokenCookieName, { domain }); deleteCookieClient("stack-refresh", { domain }); } } private async _deleteLegacyServerRefreshCookies() { - const operations: Promise<unknown>[] = [ - setOrDeleteCookie(this._refreshTokenCookieName, null, { noOpIfServerComponent: true }), - setOrDeleteCookie("stack-refresh", null, { noOpIfServerComponent: true }), - ]; + await setOrDeleteCookie(this._refreshTokenCookieName, null, { noOpIfServerComponent: true }); + await setOrDeleteCookie("stack-refresh", null, { noOpIfServerComponent: true }); + const domain = await this._getServerBaseDomain(); if (domain) { - operations.push( - setOrDeleteCookie(this._refreshTokenCookieName, null, { domain, noOpIfServerComponent: true }), - setOrDeleteCookie("stack-refresh", null, { domain, noOpIfServerComponent: true }), - ); + await setOrDeleteCookie(this._refreshTokenCookieName, null, { domain, noOpIfServerComponent: true }); + await setOrDeleteCookie("stack-refresh", null, { domain, noOpIfServerComponent: true }); } - await Promise.all(operations); }
574-618: Consider simplifying the queue update logic per past review comments.Several simplifications were suggested in past review comments:
- Line 577:
previousDomaintracking may be unnecessary if you delete all potential cookie names instead of tracking the previous domain- Line 604: Use an index counter instead of timestamp for staleness checking (more resilient to same-timestamp writes)
- Lines 590-601, 603-616: Simplify by always deleting all potential cookies instead of tracking previous domain
The current implementation is functional but could be more maintainable with these changes.
Based on past review comments.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts(1 hunks)apps/e2e/tests/js/app.test.ts(1 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts(8 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts
- apps/e2e/tests/js/app.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When changes are needed for stack or js packages, make them in packages/template instead
Files:
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🧬 Code graph analysis (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (7)
packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase32(50-72)packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
TokenObject(147-150)packages/stack-shared/src/utils/env.tsx (1)
isBrowserLike(4-6)packages/stack-shared/src/utils/urls.tsx (1)
extractBaseDomainFromHost(229-243)packages/template/src/lib/cookie.ts (5)
deleteCookieClient(204-210)setOrDeleteCookie(199-202)setOrDeleteCookieClient(190-197)deleteCookie(212-215)setCookie(226-229)packages/stack-shared/src/utils/promises.tsx (1)
runAsynchronously(343-366)packages/stack-shared/src/utils/strings.tsx (1)
stringCompare(61-65)
🪛 Biome (2.1.2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
[error] 549-549: This code will never be reached ...
... because either this statement ...
... or this statement will return from the function beforehand
(lint/correctness/noUnreachable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: all-good
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: build (22.x)
- GitHub Check: setup-tests
- GitHub Check: Security Check
🔇 Additional comments (4)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (4)
19-19: LGTM: Clean imports and state initialization.The new imports (
encodeBase32,stringCompare,extractBaseDomainFromHost,nextHeaders,isSecureCookieContext) and state tracking fields (_lastCustomRefreshCookieDomain,_lastCustomRefreshCookieUpdatedAt) properly support the domain-aware cookie handling feature.Also applies to: 30-31, 39-39, 422-423
427-436: LGTM: Proper cookie naming with security considerations.The
__Host-prefix correctly enforces secure-only cookies and prevents domain attribute usage per the cookie spec. Base32 encoding of domain names ensures consistent, safe cookie name generation.
465-493: LGTM: Robust cookie extraction with proper fallback logic.The extraction correctly:
- Handles legacy cookies first
- Iterates through all cookies with matching prefixes
- Selects the most recent based on
updatedAttimestamp usingNEGATIVE_INFINITYas a safe default- Properly handles both
__Host-prefixed and non-prefixed cookies
649-699: LGTM: Token store integration properly implements domain-aware cookies.Both browser and server paths correctly:
- Use the new structured cookie format with timestamps
- Set the Secure flag appropriately (browser: from
window.location.protocol, server: fromisSecureCookieContext())- Delete legacy cookies
- Queue custom domain cookie updates
- Extract tokens using the new
_getTokensFromCookiesmethodThe integration properly handles both the default domain-less cookies and the custom domain cookies for cross-subdomain sharing.
Also applies to: 700-773
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Outdated
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Outdated
Show resolved
Hide resolved
…uth/stack-auth into cookie-subdomain-sharing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (3)
packages/template/src/lib/cookie.ts (2)
209-214: Avoid duplicate cookie deletions when domain is provided.Currently removes both domain-scoped and host-scoped cookies even when a domain is passed. Gate the second call to avoid redundant ops.
[ suggest_essential_refactor ]
function deleteCookieClientInternal(name: string, options: DeleteCookieOptions = {}) { if (options.domain !== undefined) { Cookies.remove(name, { domain: options.domain }); - } - Cookies.remove(name); + } else { + Cookies.remove(name); + } }
157-160: Good dedupe: get() now uses getAll() and client getAll uses js-cookie.This removes duplication and aligns with prior suggestion to route get through getAll and set the helper cookie on access. Nice.
Also applies to: 162-167
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
444-449: Nice: structured cookie fields and safe JSON parsing.
- updated_at_millis aligns with API naming.
- parseJson replaces broad try/catch and keeps type checks tight.
Also applies to: 453-472, 507-521
🧹 Nitpick comments (3)
packages/template/src/lib/cookie.ts (1)
16-22: Optional: prefer Map internally for cookie collections.Guideline prefers ES6 Map over Record. Public API can stay Record<string,string>, but you could convert at boundaries and operate on Map internally for better key handling and iteration.
Happy to provide a small helper: toCookieMap()/fromCookieMap() if you want to try this without changing the external types. As per coding guidelines.
Also applies to: 85-107, 162-167
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (2)
646-659: Also delete domain-scoped refresh cookies (browser).You delete only host-scoped names. Domain-scoped (parent-domain) cookies linger. Clean both to avoid stale tokens.
cookieNamesToDelete.forEach((name) => deleteCookieClient(name)); + runAsynchronously(async () => { + const res = await this._trustedParentDomainCache.getOrWait([window.location.hostname], "read-write"); + if (res.status === "ok" && res.data) { + await Promise.all( + cookieNamesToDelete.map((name) => + setOrDeleteCookieClient(name, null, { domain: res.data, noOpIfServerComponent: true }) + ), + ); + } + });
709-721: Also delete domain-scoped refresh cookies (server).Mirror the browser cleanup on server to remove parent-domain copies too.
if (cookieNamesToDelete.length > 0) { await Promise.all( cookieNamesToDelete.map((name) => setOrDeleteCookie(name, null, { noOpIfServerComponent: true }), ), ); + const baseDomain = await this._getServerBaseDomain(); + if (baseDomain) { + await Promise.all( + cookieNamesToDelete.map((name) => + setOrDeleteCookie(name, null, { domain: baseDomain, noOpIfServerComponent: true }), + ), + ); + } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
packages/stack-shared/src/utils/urls.tsx(2 hunks)packages/template/src/lib/cookie.ts(10 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts(12 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/stack-shared/src/utils/urls.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
packages/template/src/lib/cookie.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When changes are needed for stack or js packages, make them in packages/template instead
Files:
packages/template/src/lib/cookie.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🧬 Code graph analysis (2)
packages/template/src/lib/cookie.ts (1)
packages/stack-shared/src/utils/env.tsx (1)
isBrowserLike(4-6)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (6)
packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase32(50-72)packages/stack-shared/src/utils/json.tsx (1)
parseJson(72-74)packages/stack-shared/src/utils/env.tsx (1)
isBrowserLike(4-6)packages/stack-shared/src/utils/promises.tsx (1)
runAsynchronously(343-366)packages/template/src/lib/cookie.ts (4)
setCookie(245-248)setOrDeleteCookieClient(216-223)setOrDeleteCookie(225-228)deleteCookieClient(230-233)packages/stack-shared/src/utils/stores.tsx (1)
Store(24-64)
🪛 Biome (2.1.2)
packages/template/src/lib/cookie.ts
[error] 181-181: This code will never be reached ...
... because this statement will return from the function beforehand
(lint/correctness/noUnreachable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: build (22.x)
- GitHub Check: docker
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: all-good
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: Security Check
🔇 Additional comments (1)
packages/template/src/lib/cookie.ts (1)
85-107: Side‑effect write in getAll(): acceptable here; consider minimal scope.Writing stack-is-https inside getAll() is intentional and wrapped in a narrow try/catch. Looks good, just keep the write minimal and idempotent.
If you see unexpected cookie churn in logs, we can move this sentinel write to first set()/delete() call instead.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
apps/e2e/tests/js/cookies.test.ts (2)
179-179: Past review comment appears resolved.The previous review flagged that this line should check
updated_at_millisinstead ofupdated_at. The current code correctly usesupdated_at_millis, so this concern has been addressed.
303-303: Past review comment appears resolved.The previous review flagged that test data should use
updated_at_millisinstead ofupdated_at. The current code correctly usesupdated_at_millison lines 303 and 307, so this concern has been addressed.
🧹 Nitpick comments (5)
apps/e2e/tests/js/cookies.test.ts (5)
59-73: Consider edge cases in cookie parsing logic.The current cookie parsing splits on
;and=characters, which could fail if cookie values legitimately contain these characters. While this may be sufficient for controlled test scenarios, real cookies can have URL-encoded values or quoted strings with these characters.If tests will only use simple cookie values, this is acceptable. Otherwise, consider using a more robust cookie parsing library or handling edge cases like quoted values.
76-78: Add cleanup to prevent test pollution.The global stubs for
window,document, andsessionStorageare not cleaned up after tests complete. This could cause test pollution if multiple tests run in the same environment or if a test fails partway through.Consider returning a cleanup function from
setupBrowserCookieEnvand calling it in test cleanup:function setupBrowserCookieEnv(options: BrowserEnvOptions = {}): BrowserEnv { // ... existing setup code ... vi.stubGlobal("window", fakeWindow); vi.stubGlobal("document", fakeDocument); vi.stubGlobal("sessionStorage", fakeSessionStorage); return { cookieStore, cookieWrites, location, + cleanup: () => { + vi.unstubAllGlobals(); + }, }; }Then in each test:
-const { cookieStore, cookieWrites } = setupBrowserCookieEnv({ protocol: "https:" }); +const env = setupBrowserCookieEnv({ protocol: "https:" }); +try { + // test code +} finally { + env.cleanup(); +}
163-172: Consider extracting timeout constants.The test uses hardcoded timeout values (2,000 ms and 10,000 ms) directly in
waitUntilcalls. Different CI environments may have different performance characteristics, making these timeouts potentially flaky.Consider extracting these as configurable constants:
const COOKIE_READY_TIMEOUT = 2_000; const COOKIE_SYNC_TIMEOUT = 10_000;Or make them environment-aware:
const DEFAULT_TIMEOUT = process.env.CI ? 20_000 : 2_000;
301-308: Add explanatory constants for timestamp values.The timestamp values
1700000000000and1800000000000are magic numbers without context. Using named constants would make the test's intent clearer.Apply this diff:
+ const STALE_TIMESTAMP_MILLIS = 1700000000000; // Nov 15, 2023 + const FRESH_TIMESTAMP_MILLIS = 1800000000000; // Jan 21, 2027 + const staleCookieValue = JSON.stringify({ refresh_token: "stale-token", - updated_at_millis: 1700000000000, + updated_at_millis: STALE_TIMESTAMP_MILLIS, }); const freshCookieValue = JSON.stringify({ refresh_token: "fresh-token", - updated_at_millis: 1800000000000, + updated_at_millis: FRESH_TIMESTAMP_MILLIS, });
316-316: Avoid accessing private methods in tests.Casting to
anyto access_getTokensFromCookiesbreaks encapsulation and makes the test fragile to implementation changes. If this method needs to be tested, consider making it part of the public API or creating a test-specific export.Consider one of these approaches:
Export the method for testing (in the source file):
export { _getTokensFromCookies } from './internal';Test through the public API if possible:
// Instead of directly calling _getTokensFromCookies, // set up cookies and verify tokens are retrieved via public methodsCreate a test utility that doesn't rely on private implementation details.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/e2e/tests/js/cookies.test.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
apps/e2e/tests/js/cookies.test.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
When writing tests, prefer .toMatchInlineSnapshot over other selectors where possible
Files:
apps/e2e/tests/js/cookies.test.ts
🧬 Code graph analysis (1)
apps/e2e/tests/js/cookies.test.ts (3)
apps/e2e/tests/helpers.ts (2)
value(82-86)it(12-12)packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase32(50-72)apps/e2e/tests/js/js-helpers.ts (1)
createApp(41-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: setup-tests
- GitHub Check: lint_and_build (latest)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: docker
- GitHub Check: build (22.x)
- GitHub Check: all-good
- GitHub Check: Security Check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
582-614: Fix host resolution and add Secure for browser-set domain cookie.
- Hostname is overwritten by the Next headers path and can become undefined in browsers; also port is not stripped.
- Browser path sets domain cookie without Secure.
Apply:
@@ - let hostname; - if (isBrowserLike()) { - hostname = window.location.hostname; - } - // IF_PLATFORM next - hostname = (await sc.headers?.())?.get("host"); - // END_PLATFORM - if (!hostname) { + let hostname: string | null = null; + if (context === "browser") { + hostname = typeof window !== "undefined" ? window.location.hostname : null; + } else { + // IF_PLATFORM next + const hdrs = await sc.headers?.(); + hostname = (hdrs?.get("x-forwarded-host") ?? hdrs?.get("host")) ?? null; + // END_PLATFORM + } + // strip port if present + hostname = hostname?.split(":")[0] ?? null; + if (!hostname) { return; } @@ - const setCookie = async (targetDomain: string, value: string | null) => { + const setCookie = async (targetDomain: string, value: string | null) => { const name = this._getCustomRefreshCookieName(targetDomain); - const options = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true }; - if (context === "browser") { - setOrDeleteCookieClient(name, value, options); - } else { - await setOrDeleteCookie(name, value, options); - } + const baseOptions = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true } as const; + if (context === "browser") { + setOrDeleteCookieClient( + name, + value, + { ...baseOptions, secure: typeof window !== "undefined" && window.location.protocol === "https:" }, + ); + } else { + await setOrDeleteCookie(name, value, baseOptions); + } };
🧹 Nitpick comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
473-502: Consider ES6 Map for cookie collections (optional).New helpers pass around Record<string,string>. Per guidelines, prefer Map for lookups/iteration clarity and to avoid prototype pitfalls. You can convert once at boundaries (e.g., from cookie.parse) and refactor _collectRefreshTokenCookieNames/_extractRefreshTokenFromCookieMap accordingly. Low priority.
Also applies to: 534-563
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts(13 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When changes are needed for stack or js packages, make them in packages/template instead
Files:
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🧬 Code graph analysis (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (8)
packages/template/src/lib/stack-app/apps/implementations/common.ts (2)
createCache(29-34)TokenObject(147-150)packages/stack-shared/src/utils/bytes.tsx (1)
encodeBase32(50-72)packages/stack-shared/src/utils/json.tsx (1)
parseJson(72-74)packages/stack-shared/src/utils/env.tsx (1)
isBrowserLike(4-6)packages/stack-shared/src/utils/promises.tsx (1)
runAsynchronously(343-366)packages/template/src/lib/cookie.ts (4)
setCookie(245-248)setOrDeleteCookieClient(216-223)setOrDeleteCookie(225-228)deleteCookieClient(230-233)packages/stack-shared/src/utils/stores.tsx (1)
Store(24-64)packages/stack-shared/src/known-errors.tsx (2)
KnownErrors(1589-1591)KnownErrors(1593-1716)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: restart-dev-and-test
- GitHub Check: setup-tests
- GitHub Check: all-good
- GitHub Check: Vercel Agent Review
- GitHub Check: docker
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: lint_and_build (latest)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: Security Check
🔇 Additional comments (5)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (5)
453-472: LGTM: structured refresh cookie parsing with parseJson.Safer parsing and null-guarding look correct.
564-581: LGTM: cookie update preparation.Good separation: compute updatedAt, payloads, and deletion set.
276-279: Caching and parent-domain resolution look correct; minor robustness note.
- Good: domain-aware cache with dependency key and parametric _getTrustedParentDomain(currentDomain).
- Robustness: ensure callers always pass a port-stripped host (fix in earlier comment handles this). No further changes needed here.
Also applies to: 615-628
438-452: LGTM: naming and payload shape.
- __Host- default-name helper matches updated_at_millis.
- Access-cookie pairing format consistent with readers.
503-521: LGTM: access-token extraction via parseJson with token pairing check.Efficient short-circuit on prefix; correct token match check.
Summary by CodeRabbit
New Features
Refactor
Tests
Chores