Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including You can disable this status message by setting the 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)
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.
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.
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.
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.
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.
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
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
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
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Show resolved
Hide resolved
…uth/stack-auth into cookie-subdomain-sharing
Summary by CodeRabbit
New Features
Refactor
Tests
Chores