From 0f12171bded2d50904e91e71e589e0ac7a2077c6 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 8 Dec 2025 12:53:29 -0800 Subject: [PATCH 1/4] Partitioned cookie support See comment in cookie.ts for more information --- apps/dashboard/next.config.mjs | 4 - apps/dashboard/src/app/global-error.tsx | 2 +- packages/stack-shared/src/apps/apps-config.ts | 2 +- packages/template/src/lib/cookie.ts | 163 +++++++++++++++--- .../apps/implementations/client-app-impl.ts | 15 +- 5 files changed, 147 insertions(+), 39 deletions(-) diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index ca3959bb07..50f74248bb 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -106,10 +106,6 @@ const nextConfig = { key: "X-Content-Type-Options", value: "nosniff", }, - { - key: "X-Frame-Options", - value: "SAMEORIGIN", - }, { key: "Content-Security-Policy", value: "", diff --git a/apps/dashboard/src/app/global-error.tsx b/apps/dashboard/src/app/global-error.tsx index e43375bf60..dd9e828b92 100644 --- a/apps/dashboard/src/app/global-error.tsx +++ b/apps/dashboard/src/app/global-error.tsx @@ -26,7 +26,7 @@ export default function GlobalError({ error }: any) { return ( - + {isProdLike ? ( ) : ( diff --git a/packages/stack-shared/src/apps/apps-config.ts b/packages/stack-shared/src/apps/apps-config.ts index 9f0846daf9..607c4bf62c 100644 --- a/packages/stack-shared/src/apps/apps-config.ts +++ b/packages/stack-shared/src/apps/apps-config.ts @@ -76,7 +76,7 @@ export const ALL_APPS = { displayName: "Payments", subtitle: "Payment processing and subscription management", tags: ["operations", "gtm"], - stage: "stable", + stage: "beta", }, "emails": { displayName: "Emails", diff --git a/packages/template/src/lib/cookie.ts b/packages/template/src/lib/cookie.ts index c9fc0469a1..e28a138827 100644 --- a/packages/template/src/lib/cookie.ts +++ b/packages/template/src/lib/cookie.ts @@ -4,7 +4,67 @@ import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors' import Cookies from "js-cookie"; import { calculatePKCECodeChallenge, generateRandomCodeVerifier, generateRandomState } from "oauth4webapi"; -type SetCookieOptions = { maxAge?: number, noOpIfServerComponent?: boolean, domain?: string, secure?: boolean }; + +// INFO: This file is used to manage cookies. It also sets some cookie flags automatically, see this description. +// +// It provides asynchronous setCookie, getCookie, deleteCookie, etc. functions that can be used in various environments +// (browser + Next.js for now). Under the hood, they just get a CookieHelper object and then set the cookies there. +// +// The CookieHelper object is a simple object that lets you set, get and delete cookies synchronously. Acquiring one +// is asynchronous (except for browser environments, where they can be acquired synchronously), but once you have it, +// you can use it synchronously. This function is useful if you cannot await in the calling code, but otherwise you +// should prefer to await the functions directly. +// +// Some cookie flags are set automatically by the CookieHelper (and hence also the Cookie functions). +// In particular: +// - SameSite is set to `Lax` by default, which is already true in Chromium-based browsers, so this creates +// compatibility with other browsers that use either Strict or None (particularly Safari and Firefox, and older +// versions of Chrome). If Partitioned is automatically set (as described below), then this value is set to `None` +// instead. +// - Secure is set depending on whether we could successfully determine that the client is on HTTPS. For this, we use a +// set of heuristics: +// - In a browser environment, we check window.location.protocol which is always accurate +// - In a Next.js server environment: +// - First we check the `stack-is-https` cookie, which is set in various places on the +// client with a Secure attribute. If that one is passed on to the server, we know that the client is on HTTPS +// and we can set the Secure flag on the cookie. TODO: Should we also do this with a second cookie with a +// __Host- prefix, so a malicious subdomain of the current domain cannot forcibly enable HTTPS mode and +// therefore prevent new cookies from being set? +// - Otherwise, we check the X-Forwarded-Proto header. If that one is `https`, we know that the client is +// (pretending to be) on HTTPS and we can set the Secure flag on the cookie. Note that this header is +// spoofable by malicious clients (so is the cookie actually), but since setting this value can only *increase* +// security (and therefore prevent setting of a cookie), and requires a malicious client, this is still safe. +// - If neither of the above is true, we don't set the Secure flag on the cookie. +// - Partitioned is set depending on whether it is needed & supported. Unfortunately, the fact that Partitioned +// cookies require SameSite=None, browsers that don't support it will still set them as normal third-party cookies, +// which are fundamentally unsafe. Therefore, we need to take extra care that we only ever set Partitioned cookies +// if we know for sure that the browser supports it. +// - In a browser environment, we check: +// - Whether `Secure` is set. If it's not, we don't set Partitioned. +// - Whether we can set & retrieve cookies without Partitioned being set. If this is the case, we are likely in a +// top-level context or a browser that partitions cookies by default (eg. Firefox). In this case, we don't need +// Partitioned and can just proceed as normal. +// - Whether CHIPS is supported. To prevent the case where CHIPS is not supported but third-party cookies are (in +// which we would accidentally set SameSite=None without Partitioned as the latter requires the former), we +// check this by running a simple test with document.cookie. +// - Whether the browser supports Partitioned cookies. If yes, set Partitioned. Otherwise, don't set Partitioned. +// Since there's no easy cross-compat way to do this (CookieStore and document.cookie do not return whether a +// cookie is partitioned on some/all versions of Safari and Firefox), we use a heuristic; we run this test by +// creating two cookies with the same name: One with Partitioned and one without. If there are two resulting +// cookies, that means they were put into different jars, implying that the browser supports Partitioned cookies +// (but doesn't partition cookies by default). If they result in just one cookie, that could mean that the +// browser doesn't support Partitioned cookies, or that the browser doesn't put partitioned cookies into +// different jars by default, in which case we still don't know. This heuristic works on Chrome, but may +// incorrectly conclude that some other browsers don't support Partitioned. But from a security perspective, +// that is better than accidentally setting SameSite=None without Partitioned. TODO: Find a better heuristic to +// to determine whether the browser supports Partitioned cookies or not. +// - In a Next.js server environment, right now we do nothing because of the complexity involved :( TODO: In the +// future, we could improve this for example by setting hint cookies from the client, but we need to make sure that +// no malicious actor (eg. on a malicious subdomain) can forcefully enable Partitioned cookies on a browser that +// does not support it. + + +type SetCookieOptions = { maxAge: number | "session", noOpIfServerComponent?: boolean, domain?: string, secure?: boolean }; type DeleteCookieOptions = { noOpIfServerComponent?: boolean, domain?: string }; function ensureClient() { @@ -85,7 +145,7 @@ function createNextCookieHelper( getAll: () => { // set a helper cookie, see comment in `NextCookieHelper.set` below try { - rscCookiesAwaited.set("stack-is-https", "true", { secure: true }); + rscCookiesAwaited.set("stack-is-https", "true", { secure: true, expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365) }); } catch (e) { if ( typeof e === 'object' @@ -108,13 +168,7 @@ function createNextCookieHelper( set: (name: string, value: string, options: SetCookieOptions) => { // Whenever the client is on HTTPS, we want to set the Secure flag on the cookie. // - // This is not easy to find out on a Next.js server, so we use the following steps: - // - // 1. If we're on the client, we can check window.location.protocol which is the ground-truth - // 2. Check whether the stack-is-https cookie exists. This cookie is set in various places on - // the client if the protocol is known to be HTTPS - // 3. Check the X-Forwarded-Proto header - // 4. Otherwise, assume HTTP without the S + // This is not easy to find out on a Next.js server, so see the algorithm at the top of this file. // // Note that malicious clients could theoretically manipulate the `stack-is-https` cookie or // the `X-Forwarded-Proto` header; that wouldn't cause any trouble except for themselves, @@ -124,21 +178,21 @@ function createNextCookieHelper( try { rscCookiesAwaited.set(name, value, { secure: isSecureCookie, - maxAge: options.maxAge, + maxAge: options.maxAge === "session" ? undefined : options.maxAge, domain: options.domain, }); } catch (e) { handleCookieError(e, options); } }, - setOrDelete(name: string, value: string | null, options: SetCookieOptions & DeleteCookieOptions = {}) { + setOrDelete(name: string, value: string | null, options: SetCookieOptions & DeleteCookieOptions) { if (value === null) { this.delete(name, options); } else { this.set(name, value, options); } }, - delete(name: string, options: DeleteCookieOptions = {}) { + delete(name: string, options: DeleteCookieOptions) { try { if (options.domain !== undefined) { rscCookiesAwaited.delete({ name, domain: options.domain }); @@ -162,7 +216,7 @@ export function getCookieClient(name: string): string | null { export function getAllCookiesClient(): Record { ensureClient(); // set a helper cookie, see comment in `NextCookieHelper.set` above - Cookies.set("stack-is-https", "true", { secure: true }); + Cookies.set("stack-is-https", "true", { secure: true, expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365) }); return Cookies.get(); } @@ -189,27 +243,84 @@ function determineSecureFromServerContext( cookies: Awaited>, headers: Awaited>, ): boolean { + // see the algorithm at the top of this file + // TODO: We should probably also check that the stack-is-https cookie has a Secure attribute itself, + // TODO: We should consider adding another cookie __Host-stack-is-https, see the comment in the algorithm at the top of this file return cookies.has("stack-is-https") || headers.get("x-forwarded-proto") === "https"; } // END_PLATFORM -function setCookieClientInternal(name: string, value: string, options: SetCookieOptions = {}) { + +function shouldSetPartitionedClient() { + ensureClient(); + + if (!(determineSecureFromClientContext())) { + return false; + } + + // check whether we can set & retrieve normal cookies (either because we're on a top-level/same-origin context or the browser partitions cookies by default) + const cookie1Name = "__Host-stack-temporary-chips-test-" + Math.random().toString(36).substring(2, 15); + document.cookie = `${cookie1Name}=value1; Secure; path=/`; + const cookies1 = document.cookie.split("; "); + document.cookie = `${cookie1Name}=delete1; Secure; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;`; + if (cookies1.some((c) => c.startsWith(cookie1Name + "="))) { + console.log("not using partitioned cookies because we can set & retrieve normal cookies"); + return false; + } + + + // check whether Partitioned cookies are supported by the browser + // TODO: See comment at the top. Feels like we should ifnd a better way to do this + const cookie2Name = "__Host-stack-temporary-chips-test-" + Math.random().toString(36).substring(2, 15); + + // just to be safe, delete the cookie first to avoid weird RNG-prediction attacks + // I don't know what they look like (since this is a host cookie) but better safe than sorry + // (this function should be 100% bulletproof so we don't accidentally fall back to non-partitioned third party cookies on unsupported browsers) + document.cookie = `${cookie2Name}=delete1; Secure; SameSite=None; Partitioned; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + document.cookie = `${cookie2Name}=delete2; Secure; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + + // set the cookie, once partitioned and once not partitioned + document.cookie = `${cookie2Name}=set1; Secure; SameSite=None; Partitioned; path=/`; + document.cookie = `${cookie2Name}=set2; Secure; SameSite=None; path=/`; + + // check if there are two cookies + const cookies2 = document.cookie.split("; "); + const numberOfCookiesWithThisName = cookies2.filter((c) => c.startsWith(cookie2Name + "=")).length; + console.log("numberOfCookiesWithThisName", numberOfCookiesWithThisName); + + // clean up + document.cookie = `${cookie2Name}=delete3; Secure; SameSite=None; Partitioned; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + document.cookie = `${cookie2Name}=delete4; Secure; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + + return numberOfCookiesWithThisName === 2; +} + +function setCookieClientInternal(name: string, value: string, options: SetCookieOptions) { const secure = options.secure ?? determineSecureFromClientContext(); + const partitioned = shouldSetPartitionedClient(); + console.log("setting cookie on client", name, value, options, { secure, partitioned }); Cookies.set(name, value, { - expires: options.maxAge === undefined ? undefined : new Date(Date.now() + (options.maxAge) * 1000), + expires: options.maxAge === "session" ? undefined : new Date(Date.now() + (options.maxAge) * 1000), domain: options.domain, secure, + sameSite: "Lax", + ...(partitioned ? { + partitioned, + sameSite: "None", + } : {}), }); } -function deleteCookieClientInternal(name: string, options: DeleteCookieOptions = {}) { - if (options.domain !== undefined) { - Cookies.remove(name, { domain: options.domain, secure: determineSecureFromClientContext() }); +function deleteCookieClientInternal(name: string, options: DeleteCookieOptions) { + for (const partitioned of [true, false]) { + if (options.domain !== undefined) { + Cookies.remove(name, { domain: options.domain, secure: determineSecureFromClientContext(), partitioned }); + } + Cookies.remove(name, { secure: determineSecureFromClientContext(), partitioned }); } - Cookies.remove(name, { secure: determineSecureFromClientContext() }); } -export function setOrDeleteCookieClient(name: string, value: string | null, options: SetCookieOptions & DeleteCookieOptions = {}) { +export function setOrDeleteCookieClient(name: string, value: string | null, options: SetCookieOptions & DeleteCookieOptions) { ensureClient(); if (value === null) { deleteCookieClientInternal(name, options); @@ -218,27 +329,27 @@ export function setOrDeleteCookieClient(name: string, value: string | null, opti } } -export async function setOrDeleteCookie(name: string, value: string | null, options: SetCookieOptions & DeleteCookieOptions = {}) { +export async function setOrDeleteCookie(name: string, value: string | null, options: SetCookieOptions & DeleteCookieOptions) { const cookieHelper = await createCookieHelper(); cookieHelper.setOrDelete(name, value, options); } -export function deleteCookieClient(name: string, options: DeleteCookieOptions = {}) { +export function deleteCookieClient(name: string, options: DeleteCookieOptions) { ensureClient(); deleteCookieClientInternal(name, options); } -export async function deleteCookie(name: string, options: DeleteCookieOptions = {}) { +export async function deleteCookie(name: string, options: DeleteCookieOptions) { const cookieHelper = await createCookieHelper(); cookieHelper.delete(name, options); } -export function setCookieClient(name: string, value: string, options: SetCookieOptions = {}) { +export function setCookieClient(name: string, value: string, options: SetCookieOptions) { ensureClient(); setCookieClientInternal(name, value, options); } -export async function setCookie(name: string, value: string, options: SetCookieOptions = {}) { +export async function setCookie(name: string, value: string, options: SetCookieOptions) { const cookieHelper = await createCookieHelper(); cookieHelper.set(name, value, options); } @@ -263,7 +374,7 @@ export function consumeVerifierAndStateCookie(state: string) { if (!codeVerifier) { return null; } - deleteCookieClient(cookieName); + deleteCookieClient(cookieName, {}); return { codeVerifier, }; diff --git a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts index d8b3801c5e..bdc678be53 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts @@ -20,6 +20,7 @@ import { encodeBase32 } from "@stackframe/stack-shared/dist/utils/bytes"; import { scrambleDuringCompileTime } from "@stackframe/stack-shared/dist/utils/compile-time"; import { isBrowserLike } from "@stackframe/stack-shared/dist/utils/env"; import { StackAssertionError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { parseJson } from "@stackframe/stack-shared/dist/utils/json"; import { DependenciesMap } from "@stackframe/stack-shared/dist/utils/maps"; import { ProviderType } from "@stackframe/stack-shared/dist/utils/oauth"; import { deepPlainEquals, omit } from "@stackframe/stack-shared/dist/utils/objects"; @@ -27,7 +28,7 @@ import { neverResolve, runAsynchronously, wait } from "@stackframe/stack-shared/ import { suspend, suspendIfSsr } from "@stackframe/stack-shared/dist/utils/react"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { Store, storeLock } from "@stackframe/stack-shared/dist/utils/stores"; -import { deindent, mergeScopeStrings, stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; +import { deindent, mergeScopeStrings } from "@stackframe/stack-shared/dist/utils/strings"; import { getRelativePart, isRelative } from "@stackframe/stack-shared/dist/utils/urls"; import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; import * as cookie from "cookie"; @@ -36,7 +37,7 @@ import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react import type * as yup from "yup"; import { constructRedirectUrl } from "../../../../utils/url"; import { addNewOAuthProviderOrScope, callOAuthCallback, signInWithOAuth } from "../../../auth"; -import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, isSecure as isSecureCookieContext, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie"; +import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, isSecure as isSecureCookieContext, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie"; import { ApiKey, ApiKeyCreationOptions, ApiKeyUpdateOptions, apiKeyCreationOptionsToCrud } from "../../api-keys"; import { ConvexCtx, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, RequestLike, TokenStoreInit, stackAppInternalsSymbol } from "../../common"; import { OAuthConnection } from "../../connected-accounts"; @@ -50,7 +51,6 @@ import { ActiveSession, Auth, BaseUser, CurrentUser, InternalUserExtra, OAuthPro import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson } from "../interfaces/client-app"; import { _StackAdminAppImplIncomplete } from "./admin-app-impl"; import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveConstructorOptions } from "./common"; -import { parseJson } from "@stackframe/stack-shared/dist/utils/json"; // IF_PLATFORM react-like import { useAsyncCache } from "./common"; @@ -605,9 +605,10 @@ export class _StackClientAppImplIncomplete { const name = this._getCustomRefreshCookieName(targetDomain); - const options = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true }; + const options = { ...cookieOptions, domain: targetDomain }; if (context === "browser") { setOrDeleteCookieClient(name, value, options); } else { @@ -621,7 +622,7 @@ export class _StackClientAppImplIncomplete { @@ -677,7 +678,7 @@ export class _StackClientAppImplIncomplete deleteCookieClient(name)); + cookieNamesToDelete.forEach((name) => deleteCookieClient(name, {})); this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "browser"); hasSucceededInWriting = true; } catch (e) { @@ -735,7 +736,7 @@ export class _StackClientAppImplIncomplete 0) { await Promise.all( cookieNamesToDelete.map((name) => - setOrDeleteCookie(name, null, { noOpIfServerComponent: true }), + deleteCookie(name, { noOpIfServerComponent: true }), ), ); } From 95107b04ad18e4701bbc8ab655c3a3e3df4fc520 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 8 Dec 2025 12:59:14 -0800 Subject: [PATCH 2/4] remove logs --- packages/template/src/lib/cookie.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/template/src/lib/cookie.ts b/packages/template/src/lib/cookie.ts index e28a138827..916a6d1f81 100644 --- a/packages/template/src/lib/cookie.ts +++ b/packages/template/src/lib/cookie.ts @@ -264,7 +264,6 @@ function shouldSetPartitionedClient() { const cookies1 = document.cookie.split("; "); document.cookie = `${cookie1Name}=delete1; Secure; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;`; if (cookies1.some((c) => c.startsWith(cookie1Name + "="))) { - console.log("not using partitioned cookies because we can set & retrieve normal cookies"); return false; } @@ -286,7 +285,6 @@ function shouldSetPartitionedClient() { // check if there are two cookies const cookies2 = document.cookie.split("; "); const numberOfCookiesWithThisName = cookies2.filter((c) => c.startsWith(cookie2Name + "=")).length; - console.log("numberOfCookiesWithThisName", numberOfCookiesWithThisName); // clean up document.cookie = `${cookie2Name}=delete3; Secure; SameSite=None; Partitioned; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; @@ -298,7 +296,6 @@ function shouldSetPartitionedClient() { function setCookieClientInternal(name: string, value: string, options: SetCookieOptions) { const secure = options.secure ?? determineSecureFromClientContext(); const partitioned = shouldSetPartitionedClient(); - console.log("setting cookie on client", name, value, options, { secure, partitioned }); Cookies.set(name, value, { expires: options.maxAge === "session" ? undefined : new Date(Date.now() + (options.maxAge) * 1000), domain: options.domain, From 3e505690bb22f7dad5aefcfa78f1229c21a32611 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 8 Dec 2025 13:05:05 -0800 Subject: [PATCH 3/4] comments --- apps/dashboard/next.config.mjs | 4 ++++ packages/template/src/lib/cookie.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index 50f74248bb..ca3959bb07 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -106,6 +106,10 @@ const nextConfig = { key: "X-Content-Type-Options", value: "nosniff", }, + { + key: "X-Frame-Options", + value: "SAMEORIGIN", + }, { key: "Content-Security-Policy", value: "", diff --git a/packages/template/src/lib/cookie.ts b/packages/template/src/lib/cookie.ts index 916a6d1f81..16ee80aa42 100644 --- a/packages/template/src/lib/cookie.ts +++ b/packages/template/src/lib/cookie.ts @@ -180,6 +180,7 @@ function createNextCookieHelper( secure: isSecureCookie, maxAge: options.maxAge === "session" ? undefined : options.maxAge, domain: options.domain, + sameSite: "lax", }); } catch (e) { handleCookieError(e, options); @@ -251,7 +252,11 @@ function determineSecureFromServerContext( // END_PLATFORM +let _shouldSetPartitionedClientCache: boolean | undefined = undefined; function shouldSetPartitionedClient() { + return _shouldSetPartitionedClientCache ??= _internalShouldSetPartitionedClient(); +} +function _internalShouldSetPartitionedClient() { ensureClient(); if (!(determineSecureFromClientContext())) { From d8b808213e612251b8bc26546a3ac83b24f51a00 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 8 Dec 2025 13:14:43 -0800 Subject: [PATCH 4/4] find --- packages/template/src/lib/cookie.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template/src/lib/cookie.ts b/packages/template/src/lib/cookie.ts index 16ee80aa42..5d7b7a779c 100644 --- a/packages/template/src/lib/cookie.ts +++ b/packages/template/src/lib/cookie.ts @@ -274,7 +274,7 @@ function _internalShouldSetPartitionedClient() { // check whether Partitioned cookies are supported by the browser - // TODO: See comment at the top. Feels like we should ifnd a better way to do this + // TODO: See comment at the top. Feels like we should find a better way to do this const cookie2Name = "__Host-stack-temporary-chips-test-" + Math.random().toString(36).substring(2, 15); // just to be safe, delete the cookie first to avoid weird RNG-prediction attacks