Skip to content

Conversation

@BilalG1
Copy link
Collaborator

@BilalG1 BilalG1 commented Oct 22, 2025

Summary by CodeRabbit

  • New Features

    • Projects now expose a domains field in the client API.
    • Cookie API expanded: domain and secure options added, plus getAll and isSecure helpers.
  • Refactor

    • Domain-aware cookie and token handling for cross-domain refresh flows.
    • Minor signature/formatting tweaks to IP and URL utilities.
  • Tests

    • E2E coverage added: refresh-cookie scenarios and a project scaffolding test.
    • Backend snapshot updated to include domains.
  • Chores

    • Added a new dependency for domain parsing.

@vercel
Copy link

vercel bot commented Oct 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Nov 5, 2025 10:47pm
stack-dashboard Ready Ready Preview Comment Nov 5, 2025 10:47pm
stack-demo Ready Ready Preview Comment Nov 5, 2025 10:47pm
stack-docs Ready Ready Preview Comment Nov 5, 2025 10:47pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 22, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Adds domain-aware refresh-token cookie handling (set/getAll/delete, domain/secure flags, isSecure), expands client project schema with domains, changes isIpAddress to return boolean, adds E2E cookie and scaffold tests, and introduces tldts dependency. (29 words)

Changes

Cohort / File(s) Summary
E2E tests (browser-level)
apps/e2e/tests/js/app.test.ts, apps/e2e/tests/js/cookies.test.ts
Adds a scaffold test and a browser-like cookie E2E suite that simulates document.cookie/location/sessionStorage, validates default/custom/legacy refresh-cookie creation, secure/domain attributes, freshness selection, and behavior when trusted parent domains are present or absent.
E2E tests (backend snapshot)
apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts
Updates snapshot to include domains: [] in the project config response.
Cookie helper & client app impl
packages/template/src/lib/cookie.ts, packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Adds domain?: string and secure?: boolean to cookie options; adds getAll() and isSecure() APIs; implements domain-aware set/get/delete, per-domain refresh-cookie naming/encoding/decoding, queued updates, trusted-parent-domain resolution, and legacy-cookie cleanup; updates client token stores to use new cookie flows.
URL / IP utilities
packages/stack-shared/src/utils/ips.tsx, packages/stack-shared/src/utils/urls.tsx
Changes isIpAddress from a TypeScript type predicate to return plain boolean; minor formatting/signature spacing tweaks in URL helpers.
Project schema
packages/stack-shared/src/interface/crud/projects.ts
Adds domains field to projectsCrudClientReadSchema.config (client-facing project schema).
Dependency
packages/stack-shared/package.json
Adds dependency tldts (^7.0.17).

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Extra attention:
    • packages/template/src/lib/stack-app/.../client-app-impl.ts (dense domain-aware token/cookie logic, caching, queuing)
    • packages/template/src/lib/cookie.ts (cross-platform set/getAll/delete and isSecure semantics)
    • apps/e2e/tests/js/cookies.test.ts (environment shims and timing/flakiness)
    • packages/stack-shared/src/utils/ips.tsx (type-change impact on callers)

Possibly related PRs

Poem

🐇 I hop through cookies, soft and bright,

I tuck fresh tokens where domains take flight,
I sweep old crumbs from legacy days,
I mark secure flags in careful ways,
A little rabbit guards each cookie night.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is empty except for the template comment, providing no context about the changes, rationale, or testing approach despite significant modifications across multiple packages. Add a detailed description explaining the cookie subdomain sharing feature, motivation, implementation approach, testing coverage, and any breaking changes or migration notes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Cookie subdomain sharing' is concise, clear, and directly reflects the main changes in the PR, which involve domain-aware cookie handling and subdomain sharing logic throughout multiple files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cookie-subdomain-sharing

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8eccbaa and b0c74d9.

📒 Files selected for processing (3)
  • apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts (1 hunks)
  • packages/template/src/lib/cookie.ts (10 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (13 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🧬 Code graph analysis (2)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (5)
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/template/src/lib/cookie.ts (4)
  • setCookie (241-244)
  • setOrDeleteCookieClient (212-219)
  • setOrDeleteCookie (221-224)
  • deleteCookieClient (226-229)
packages/template/src/lib/cookie.ts (1)
packages/stack-shared/src/utils/env.tsx (1)
  • isBrowserLike (4-6)
🪛 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)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: restart-dev-and-test
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: all-good
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: docker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@greptile-apps greptile-apps bot left a 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 shareCookiesAcrossSubdomains configuration option to StackClientApp
  • Implemented extractBaseDomainFromHost() to extract the base domain from hostnames
  • Modified cookie operations to accept and use domain parameter 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 like herokuapp.com, github.io, or country-code TLDs like co.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 extractBaseDomainFromHost function 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
Loading

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@patched-codes
Copy link

patched-codes bot commented Oct 22, 2025

Documentation Update Required

Changes are needed in the following file:

[docs/templates/snippets/stack-app-constructor-options-after-ssk.mdx]

  1. Add documentation for the new shareCookiesAcrossSubdomains option in the Stack Client App constructor options. This should be inserted between the urls and noAutomaticPrefetch parameters.

  2. The new documentation should be in the following format:

<ParamField path="shareCookiesAcrossSubdomains" type="boolean">
  When set to `true`, cookies will be shared across all subdomains by using the parent domain. This allows users to remain authenticated when navigating across different subdomains of your application.
</ParamField>

Please ensure these changes are reflected in the documentation to accurately represent the new functionality introduced in the pull request.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 omits shareCookiesAcrossSubdomains. When we hydrate a client app from JSON, the option silently falls back to false, 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 ensure fromClientJson keeps receiving it).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc28c0 and 0e23112.

📒 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.ts
  • packages/stack-shared/src/utils/ips.tsx
  • apps/e2e/tests/js/app.test.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/cookie.ts
  • packages/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.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 Map instead of Record in TS files. Please convert the attribute accumulator to a Map and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0e23112 and ebd14ca.

📒 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between ebd14ca and ca282e7.

📒 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.ts
  • apps/e2e/tests/js/app.test.ts
  • packages/template/src/lib/cookie.ts
  • packages/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.ts
  • packages/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.domains in 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 browsers

Right now, cookies are deleted only when the value is empty. Real deletions also happen via Max-Age=0 or a past Expires. Aligning the shim avoids stale entries lingering in cookieStore.

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 test

Globals 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 in setupBrowserCookieEnv and return a cleanup() 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 fast

10s 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 intervalMs smaller for these checks (e.g., 50ms).

Also applies to: 172-174


181-185: Assert full __Host- invariants

For __Host- cookies, browsers require Secure, no Domain, and Path=/. You’re asserting the first two; add Path=/ 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 dot

Avoid 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 maps

Where 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 needed

Guideline 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 _getTokensFromCookies already accepts a Map, pass it directly.


298-320: Avoid coupling to private API

_getTokensFromCookies is 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

📥 Commits

Reviewing files that changed from the base of the PR and between ca45171 and b2dcb82.

📒 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

@BilalG1 BilalG1 requested a review from N2D4 October 23, 2025 22:13
@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Oct 23, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 setCookieClient automatically set the Secure flag based on window.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 setCookieClient in packages/template/src/lib/cookie.ts to automatically set secure based 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 renaming updated_at to updated_at_millis for 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 _parseStructuredRefreshCookie to use updated_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:

  1. Line 552: Direct call to deleteCookieClient instead of intermediate variable
  2. Lines 561-572: Use await statements instead of Promise.all for cleaner code

The 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:

  1. Line 577: previousDomain tracking may be unnecessary if you delete all potential cookie names instead of tracking the previous domain
  2. Line 604: Use an index counter instead of timestamp for staleness checking (more resilient to same-timestamp writes)
  3. 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

📥 Commits

Reviewing files that changed from the base of the PR and between a22d358 and b88758b.

📒 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 updatedAt timestamp using NEGATIVE_INFINITY as 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: from isSecureCookieContext())
  • Delete legacy cookies
  • Queue custom domain cookie updates
  • Extract tokens using the new _getTokensFromCookies method

The integration properly handles both the default domain-less cookies and the custom domain cookies for cross-subdomain sharing.

Also applies to: 700-773

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between b88758b and 7d852d3.

📒 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.ts
  • 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/cookie.ts
  • packages/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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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_millis instead of updated_at. The current code correctly uses updated_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_millis instead of updated_at. The current code correctly uses updated_at_millis on 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, and sessionStorage are 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 setupBrowserCookieEnv and 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 waitUntil calls. 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 1700000000000 and 1800000000000 are 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 any to access _getTokensFromCookies breaks 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:

  1. Export the method for testing (in the source file):

    export { _getTokensFromCookies } from './internal';
  2. Test through the public API if possible:

    // Instead of directly calling _getTokensFromCookies,
    // set up cookies and verify tokens are retrieved via public methods
  3. Create a test utility that doesn't rely on private implementation details.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d852d3 and af51443.

📒 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between af51443 and 544fac9.

📒 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.

@BilalG1 BilalG1 requested a review from N2D4 October 28, 2025 22:54
@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Oct 28, 2025
@github-actions github-actions bot assigned BilalG1 and unassigned N2D4 Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants