Skip to content

Cookie subdomain sharing#971

Merged
BilalG1 merged 28 commits intodevfrom
cookie-subdomain-sharing
Nov 6, 2025
Merged

Cookie subdomain sharing#971
BilalG1 merged 28 commits intodevfrom
cookie-subdomain-sharing

Conversation

@BilalG1
Copy link
Contributor

@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 6, 2025 1:54am
stack-dashboard Ready Ready Preview Comment Nov 6, 2025 1:54am
stack-demo Ready Ready Preview Comment Nov 6, 2025 1:54am
stack-docs Ready Ready Preview Comment Nov 6, 2025 1:54am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 22, 2025

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

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. (28 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
packages/template/src/lib/cookie.ts
Adds domain?: string and secure?: boolean to options; introduces getAll() and isSecure() APIs; implements domain-aware set/getAll/delete behavior and internal client/server helpers handling domain, secure, expires.
Client app implementation (token/cookie)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Implements per-domain refresh/access cookie naming, encoding/decoding, cached trusted-parent-domain resolution, queued per-domain cookie updates, legacy-cookie cleanup, and integrates domain-aware reads/writes into token stores.
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 spacing/formatting tweaks in URL helper signatures and tests.
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/apps/implementations/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, timing and flakiness concerns)
    • Call sites of isIpAddress in packages/stack-shared/src/utils/ips.tsx consumers (type-narrowing removed)

Possibly related PRs

  • project owner team #835 — related changes to project creation/schema surfaces and project ownership fields; likely overlaps with projects schema edits.
  • Convex implementation #913 — modifies client-app token/cookie handling and touches the same client-app implementation file.
  • Project logo upload #817 — touches projects CRUD schema and may overlap with adding domains to project read shapes.

Poem

🐇 I hop through cookies with a careful paw,

I hide fresh tokens behind each trusted maw,
I tidy legacy crumbs and set secure flags right,
I whisper domains to cookies in the night,
A small brown rabbit guards your session tight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is empty except for a template comment, providing no actual context, rationale, testing details, or explanation of the changes. Add a detailed description explaining the feature purpose, implementation approach, domains affected, testing performed, and any breaking changes or migration notes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Cookie subdomain sharing' clearly and specifically summarizes the main feature being implemented: enabling cookies to be shared across subdomains.

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
@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Oct 28, 2025
@github-actions github-actions bot assigned BilalG1 and unassigned N2D4 Nov 5, 2025
@BilalG1 BilalG1 merged commit 7f2de7e into dev Nov 6, 2025
22 checks passed
@BilalG1 BilalG1 deleted the cookie-subdomain-sharing branch November 6, 2025 02:12
@greptile-apps greptile-apps bot mentioned this pull request Nov 6, 2025
@coderabbitai coderabbitai bot mentioned this pull request Nov 11, 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.

2 participants