@decocms/tanstack 7.77.5 → 7.77.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/tanstack",
3
- "version": "7.77.5",
3
+ "version": "7.77.6",
4
4
  "type": "module",
5
5
  "description": "Deco framework binding for TanStack Start + Cloudflare Workers",
6
6
  "repository": {
@@ -27,9 +27,9 @@
27
27
  "lint:unused": "knip"
28
28
  },
29
29
  "dependencies": {
30
- "@decocms/blocks": "7.77.5",
31
- "@decocms/blocks-admin": "7.77.5",
32
- "@decocms/blocks-cli": "7.77.5",
30
+ "@decocms/blocks": "7.77.6",
31
+ "@decocms/blocks-admin": "7.77.6",
32
+ "@decocms/blocks-cli": "7.77.6",
33
33
  "@deco-cx/warp-node": "^0.3.16",
34
34
  "fast-json-patch": "^3.1.0",
35
35
  "ws": "^8.18.0"
@@ -15,6 +15,7 @@ import {
15
15
  DEFAULT_SECURITY_HEADERS,
16
16
  detectLocationMatcher,
17
17
  injectGeoCookies,
18
+ setCookieIdentity,
18
19
  } from "./workerEntry";
19
20
 
20
21
  const EMPTY_ENV = {};
@@ -1031,3 +1032,53 @@ describe("fast-deploy hydration ordering", () => {
1031
1032
  }
1032
1033
  });
1033
1034
  });
1035
+
1036
+ describe("Set-Cookie deduplication keys on name + Domain + Path", () => {
1037
+ it("setCookieIdentity: same name, different scope → different identities", () => {
1038
+ expect(setCookieIdentity("a=1; Path=/")).not.toBe(
1039
+ setCookieIdentity("a=1; Path=/; Domain=example.com"),
1040
+ );
1041
+ expect(setCookieIdentity("a=1; Path=/; Domain=www.example.com")).not.toBe(
1042
+ setCookieIdentity("a=1; Path=/; Domain=example.com"),
1043
+ );
1044
+ expect(setCookieIdentity("a=1; Path=/")).not.toBe(setCookieIdentity("a=1; Path=/account"));
1045
+ });
1046
+
1047
+ it("setCookieIdentity: Domain is case-insensitive, a leading dot is ignored, value is not part of the key", () => {
1048
+ expect(setCookieIdentity("a=1; Path=/; Domain=Example.COM")).toBe(
1049
+ setCookieIdentity("a=2; path=/; domain=.example.com; Secure"),
1050
+ );
1051
+ });
1052
+
1053
+ it("keeps every scope of a multi-scope clear (sign-out) and still collapses true duplicates", async () => {
1054
+ setBlocks({});
1055
+ const clears = [
1056
+ "VtexIdclientAutCookie=; Path=/; Max-Age=0",
1057
+ "VtexIdclientAutCookie=; Path=/; Max-Age=0; Domain=www.example.com",
1058
+ "VtexIdclientAutCookie=; Path=/; Max-Age=0; Domain=example.com",
1059
+ ];
1060
+ const entry = {
1061
+ fetch: async () => {
1062
+ const headers = new Headers({ "content-type": "text/plain" });
1063
+ // A true duplicate (same name, same scope): only the last one survives.
1064
+ headers.append("set-cookie", "checkout.vtex.com=__ofid=old; Path=/");
1065
+ for (const sc of clears) headers.append("set-cookie", sc);
1066
+ headers.append("set-cookie", "checkout.vtex.com=__ofid=new; Path=/");
1067
+ return new Response("ok", { status: 200, headers });
1068
+ },
1069
+ };
1070
+ const worker = createDecoWorkerEntry(entry, { observability: false });
1071
+ const res = await worker.fetch(
1072
+ new Request("https://www.example.com/telesales/sign-out"),
1073
+ EMPTY_ENV,
1074
+ MOCK_CTX,
1075
+ );
1076
+ const out = (res.headers as unknown as { getSetCookie: () => string[] }).getSetCookie();
1077
+ // Order is not part of the contract; the set of surviving headers is.
1078
+ expect(out).toHaveLength(4);
1079
+ expect(out).toEqual(
1080
+ expect.arrayContaining([...clears, "checkout.vtex.com=__ofid=new; Path=/"]),
1081
+ );
1082
+ expect(out).not.toContain("checkout.vtex.com=__ofid=old; Path=/");
1083
+ });
1084
+ });
@@ -930,20 +930,45 @@ function stripSafeCookiesForCache(response: Response, safeCookieSet: Set<string>
930
930
  }
931
931
 
932
932
  /**
933
- * Deduplicate Set-Cookie headers — keep only the LAST occurrence of
934
- * each cookie name. Multiple layers (VTEX middleware, invoke handlers,
935
- * etc.) may independently append the same cookie.
933
+ * Identity of a `Set-Cookie` header for deduplication: the browser keys its
934
+ * cookie jar by name + Domain + Path (RFC 6265 §5.3 step 11), so two headers
935
+ * that differ in scope address two different cookies — a clear for the
936
+ * host-only cookie does not touch the `Domain=example.com` one. The domain is
937
+ * case-insensitive and a leading dot is ignored (§5.2.3); the path is
938
+ * compared as sent.
939
+ */
940
+ export function setCookieIdentity(setCookie: string): string {
941
+ const [nameValue = "", ...attrs] = setCookie.split(";");
942
+ const eqIdx = nameValue.indexOf("=");
943
+ const name = eqIdx > 0 ? nameValue.slice(0, eqIdx).trim() : nameValue.trim();
944
+ let domain = "";
945
+ let path = "";
946
+ for (const attr of attrs) {
947
+ const sep = attr.indexOf("=");
948
+ const key = (sep >= 0 ? attr.slice(0, sep) : attr).trim().toLowerCase();
949
+ const value = sep >= 0 ? attr.slice(sep + 1).trim() : "";
950
+ if (key === "domain") domain = value.replace(/^\./, "").toLowerCase();
951
+ else if (key === "path") path = value;
952
+ }
953
+ return `${name}\u0000${domain}\u0000${path}`;
954
+ }
955
+
956
+ /**
957
+ * Deduplicate Set-Cookie headers — keep only the LAST occurrence of each
958
+ * cookie identity (name + Domain + Path, see {@link setCookieIdentity}).
959
+ * Multiple layers (VTEX middleware, invoke handlers, etc.) may independently
960
+ * append the same cookie. Keying by name alone collapsed deliberate clears of
961
+ * the same cookie under different scopes (host-only / `Domain=host` /
962
+ * `Domain=parent`) into whichever came last, leaving the other scopes alive.
936
963
  */
937
964
  function deduplicateSetCookies(response: Response): void {
938
965
  const setCookies = (response.headers as any).getSetCookie?.() as string[] | undefined;
939
966
  if (!setCookies || setCookies.length <= 1) return;
940
967
 
941
- // Build map: cookie name → last Set-Cookie value
968
+ // Build map: cookie identity → last Set-Cookie value
942
969
  const seen = new Map<string, string>();
943
970
  for (const sc of setCookies) {
944
- const eqIdx = sc.indexOf("=");
945
- const name = eqIdx > 0 ? sc.slice(0, eqIdx).trim() : sc;
946
- seen.set(name, sc);
971
+ seen.set(setCookieIdentity(sc), sc);
947
972
  }
948
973
 
949
974
  // If no duplicates, nothing to do