@shware/analytics 9.1.0 → 10.0.1
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/README.md +34 -0
- package/dist/click-id/index.cjs +32 -2
- package/dist/click-id/index.cjs.map +1 -1
- package/dist/click-id/index.d.cts +22 -9
- package/dist/click-id/index.d.cts.map +1 -1
- package/dist/click-id/index.d.mts +22 -9
- package/dist/click-id/index.d.mts.map +1 -1
- package/dist/click-id/index.mjs +28 -3
- package/dist/click-id/index.mjs.map +1 -1
- package/dist/tanstack/index.cjs +0 -1
- package/dist/tanstack/index.d.cts +2 -2
- package/dist/tanstack/index.d.mts +2 -2
- package/dist/tanstack/index.mjs +2 -2
- package/dist/tanstack/middleware.cjs +22 -9
- package/dist/tanstack/middleware.cjs.map +1 -1
- package/dist/tanstack/middleware.d.cts +26 -17
- package/dist/tanstack/middleware.d.cts.map +1 -1
- package/dist/tanstack/middleware.d.mts +26 -17
- package/dist/tanstack/middleware.d.mts.map +1 -1
- package/dist/tanstack/middleware.mjs +23 -9
- package/dist/tanstack/middleware.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,6 +170,40 @@ source.
|
|
|
170
170
|
- Clients older than 9.0.0 send `fbc`, `fbp` and `rdt_uuid` for `_fbc`, `_fbp` and `_rdt_uuid`;
|
|
171
171
|
the senders still read them as a fallback (deprecated).
|
|
172
172
|
|
|
173
|
+
### Keeping the cookies past Safari's limits (TanStack Start)
|
|
174
|
+
|
|
175
|
+
The pixels write their cookies through `document.cookie`, which Safari caps at 7 days (24 hours on
|
|
176
|
+
a landing page decorated by an ad click), so a visitor returning a week later has lost the click
|
|
177
|
+
and, for the browser ids, gets a new identity. `createClickIdMiddleware` re-issues them over HTTP
|
|
178
|
+
on every document response — the server-set copy is not capped — and captures the click ids from
|
|
179
|
+
the landing URL:
|
|
180
|
+
|
|
181
|
+
| Cookie | Written by | Re-issued for |
|
|
182
|
+
| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------ |
|
|
183
|
+
| `_fbc` | this middleware, from `fbclid` | its remaining 90 days (never slides: Meta warns on a moved fbclid) |
|
|
184
|
+
| `_gcl_aw` / `_gcl_gb` | gtag | their remaining 90 days |
|
|
185
|
+
| `_fbp`, `_rdt_uuid` | the Meta / Reddit pixel | 90 days, as the pixel itself rewrites them on every page |
|
|
186
|
+
| `_rdt_cid`, `_uetmsclkid` | the Reddit pixel / UET tag, and this middleware from the URL | 90 days, as their pixel does |
|
|
187
|
+
| `__oppref` | oaiq, and this middleware from `oppref` | 30 days |
|
|
188
|
+
| `__obref` | oaiq | 365 days |
|
|
189
|
+
|
|
190
|
+
A browser id is only ever re-issued, never created: the pixel creates it. Nothing is written for
|
|
191
|
+
OpenAI once the visitor has opted out of oaiq (`__oaiq_consent=false`).
|
|
192
|
+
|
|
193
|
+
`domain` is required: in production the site's registrable domain (e.g. `.example.com`), where
|
|
194
|
+
the pixels write theirs — a host-only cookie would be a second cookie of the same name, which
|
|
195
|
+
the pixels read in its place or delete. `null` (host-only) is only for `localhost`:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// src/start.ts
|
|
199
|
+
import { createClickIdMiddleware } from '@shware/analytics/tanstack';
|
|
200
|
+
|
|
201
|
+
const clickIdMiddleware = createClickIdMiddleware(
|
|
202
|
+
import.meta.env.DEV ? { domain: null, secure: false } : { domain: '.example.com' }
|
|
203
|
+
);
|
|
204
|
+
export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }));
|
|
205
|
+
```
|
|
206
|
+
|
|
173
207
|
## Third Parties Advices
|
|
174
208
|
|
|
175
209
|
### Reddit
|
package/dist/click-id/index.cjs
CHANGED
|
@@ -31,6 +31,10 @@ let cookie = require("cookie");
|
|
|
31
31
|
*/
|
|
32
32
|
const FBC_TTL_MS = 7776e6;
|
|
33
33
|
const RDT_CID_TTL_MS = 7776e6;
|
|
34
|
+
const RDT_UUID_TTL_MS = 7776e6;
|
|
35
|
+
const FBP_TTL_MS = 7776e6;
|
|
36
|
+
const OPPREF_TTL_MS = 2592e6;
|
|
37
|
+
const OBREF_TTL_MS = 31536e6;
|
|
34
38
|
const MSCLKID_TTL_MS = 7776e6;
|
|
35
39
|
const GCL_TTL_MS = 7776e6;
|
|
36
40
|
const CLOCK_SKEW_MS = 864e5;
|
|
@@ -39,6 +43,12 @@ const RDT_CID_COOKIE = "_rdt_cid";
|
|
|
39
43
|
const GCL_AW_COOKIE = "_gcl_aw";
|
|
40
44
|
const GCL_GB_COOKIE = "_gcl_gb";
|
|
41
45
|
const UET_MSCLKID_COOKIE = "_uetmsclkid";
|
|
46
|
+
const RDT_UUID_COOKIE = "_rdt_uuid";
|
|
47
|
+
const FBP_COOKIE = "_fbp";
|
|
48
|
+
const OPPREF_COOKIE = "__oppref";
|
|
49
|
+
const OBREF_COOKIE = "__obref";
|
|
50
|
+
/** OpenAI's consent cookie; `false` is the visitor's opt-out, on which oaiq deletes its cookies. */
|
|
51
|
+
const OAIQ_CONSENT_COOKIE = "__oaiq_consent";
|
|
42
52
|
/**
|
|
43
53
|
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's
|
|
44
54
|
* Parameter Builder adds to the values it writes ("an appendix field at the end of each
|
|
@@ -163,7 +173,7 @@ function resolveClickIdCookies(input) {
|
|
|
163
173
|
path: "/",
|
|
164
174
|
secure,
|
|
165
175
|
sameSite: "lax",
|
|
166
|
-
domain
|
|
176
|
+
domain: domain ?? void 0
|
|
167
177
|
};
|
|
168
178
|
const set = (name, value, ttlMs) => cookies.push({
|
|
169
179
|
name,
|
|
@@ -219,7 +229,22 @@ function resolveClickIdCookies(input) {
|
|
|
219
229
|
if (urlRdtCid && urlRdtCid !== existingRdtCid) {
|
|
220
230
|
set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);
|
|
221
231
|
result.rdt_cid = urlRdtCid;
|
|
222
|
-
} else if (existingRdtCid)
|
|
232
|
+
} else if (existingRdtCid) {
|
|
233
|
+
result.rdt_cid = existingRdtCid;
|
|
234
|
+
if (refresh) set(RDT_CID_COOKIE, existingRdtCid, RDT_CID_TTL_MS);
|
|
235
|
+
}
|
|
236
|
+
const existingRdtUuid = jar["_rdt_uuid"] || void 0;
|
|
237
|
+
if (existingRdtUuid && refresh) set(RDT_UUID_COOKIE, existingRdtUuid, RDT_UUID_TTL_MS);
|
|
238
|
+
const existingFbp = jar["_fbp"] || void 0;
|
|
239
|
+
if (existingFbp && refresh) set(FBP_COOKIE, existingFbp, FBP_TTL_MS);
|
|
240
|
+
if (jar["__oaiq_consent"] !== "false") {
|
|
241
|
+
const urlOppref = params.get("oppref") || void 0;
|
|
242
|
+
const existingOppref = jar["__oppref"] || void 0;
|
|
243
|
+
if (urlOppref) set(OPPREF_COOKIE, urlOppref, OPPREF_TTL_MS);
|
|
244
|
+
else if (existingOppref && refresh) set(OPPREF_COOKIE, existingOppref, OPPREF_TTL_MS);
|
|
245
|
+
const existingObref = jar["__obref"] || void 0;
|
|
246
|
+
if (existingObref && refresh) set(OBREF_COOKIE, existingObref, OBREF_TTL_MS);
|
|
247
|
+
}
|
|
223
248
|
const urlMsclkid = params.get("msclkid")?.trim();
|
|
224
249
|
const existingMsclkid = parseUetMsclkid(jar[UET_MSCLKID_COOKIE]);
|
|
225
250
|
if (urlMsclkid && MSCLKID.test(urlMsclkid) && urlMsclkid.toLowerCase() !== existingMsclkid) {
|
|
@@ -237,9 +262,14 @@ function toSetCookieHeaders(cookies) {
|
|
|
237
262
|
}
|
|
238
263
|
//#endregion
|
|
239
264
|
exports.FBC_COOKIE = FBC_COOKIE;
|
|
265
|
+
exports.FBP_COOKIE = FBP_COOKIE;
|
|
240
266
|
exports.GCL_AW_COOKIE = GCL_AW_COOKIE;
|
|
241
267
|
exports.GCL_GB_COOKIE = GCL_GB_COOKIE;
|
|
268
|
+
exports.OAIQ_CONSENT_COOKIE = OAIQ_CONSENT_COOKIE;
|
|
269
|
+
exports.OBREF_COOKIE = OBREF_COOKIE;
|
|
270
|
+
exports.OPPREF_COOKIE = OPPREF_COOKIE;
|
|
242
271
|
exports.RDT_CID_COOKIE = RDT_CID_COOKIE;
|
|
272
|
+
exports.RDT_UUID_COOKIE = RDT_UUID_COOKIE;
|
|
243
273
|
exports.UET_MSCLKID_COOKIE = UET_MSCLKID_COOKIE;
|
|
244
274
|
exports.formatFbc = formatFbc;
|
|
245
275
|
exports.formatGcl = formatGcl;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["parseCookie","cookie","stringifySetCookie"],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`, `_rdt_cid`,\n * `_uetmsclkid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * The Google pair follows the same architecture Google itself ships for server containers —\n * sGTM's Conversion Linker sets FPGCLAW via Set-Cookie for 90 days — and the sGTM ecosystem\n * productized for gtag's own cookies (stape Cookie Keeper re-issues `_gcl_*` over HTTP; vendor\n * case studies report 5–20% of Google Ads conversions recovered, up to +44% on the Safari\n * slice). One deployment caveat from WebKit's rules: the HTTP-cookie exemption holds only for\n * responses from the site's own infrastructure — a CNAME or third-party-IP host (a tagging\n * subdomain on someone else's cloud) gets its Set-Cookie silently capped to 7 days. This module\n * runs on the host's own document response, which is exactly the exempt case.\n *\n * references:\n * - https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n * - https://webkit.org/tracking-prevention/ (7d JS cookies, 24h on ad-decorated landings,\n * CNAME/third-party-IP HTTP cookies capped to 7d, same-origin HTTP cookies exempt)\n * - https://www.simoahava.com/analytics/google-ads-server-side-tagging-google-tag-manager/\n * (Conversion Linker → FPGCLAW via Set-Cookie, Google's own server-side equivalent)\n * - https://stape.io/helpdesk/documentation/cookie-keeper-power-up (the productized re-issue)\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// bat.js's own expiry for _uetmsclkid (`msClkIdExpirationTime`), the Microsoft Ads click window.\nconst MSCLKID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// gtag's own expiry for _gcl_* — 90 days, the length of a Google Ads click's upload window.\nconst GCL_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\nexport const GCL_AW_COOKIE = '_gcl_aw';\nexport const GCL_GB_COOKIE = '_gcl_gb';\nexport const UET_MSCLKID_COOKIE = '_uetmsclkid';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's\n * Parameter Builder adds to the values it writes (\"an appendix field at the end of each\n * parameter\"); it is not part of the click id, so `fbclid` is the fourth segment alone and the\n * value is passed on as `raw`, unchanged. Returns undefined for anything malformed.\n * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts.length > 5 || parts[0] !== 'fb') return undefined;\n if (parts.length === 5 && !parts[4]) return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts[3];\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ParsedGcl = { raw: string; creationTime: number; clickId: string };\n\n/**\n * Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or\n * `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on\n * Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.\n * ref: https://support.google.com/sa360/answer/7342044\n */\nexport function isGoogleAdsGclid(gclsrc: string | null | undefined): boolean {\n return !gclsrc || gclsrc === 'aw.ds';\n}\n\n// gtag's own value validator, extracted from the live gtag.js bundle (2026-09): its parser\n// accepts version \"GCL\" or \"1\", a /^\\d+$/ seconds timestamp, and a click id matching this.\n// Re-verify against gtag.js before changing anything here — the format is gtag's, not ours.\nconst GCL_CLICK_ID = /^[\\w-]+$/;\n\n/**\n * Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp\n * is in SECONDS, unlike `_fbc`'s milliseconds. gtag's parser accepts version `GCL` or `1` and\n * validates the segments exactly this way (`/^\\d+$/` timestamp, `/^[\\w-]+$/` click id); any\n * labels tail is preserved through `raw` so a re-issue stays byte-identical.\n */\nexport function parseGcl(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedGcl | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 3) return undefined;\n if (parts[0] !== 'GCL' && parts[0] !== '1') return undefined;\n const seconds = parts[1];\n const clickId = parts[2];\n if (seconds === undefined || !/^\\d+$/.test(seconds)) return undefined;\n if (clickId === undefined || !GCL_CLICK_ID.test(clickId)) return undefined;\n const creationTime = Number(seconds) * 1000;\n // Reject millisecond-precision or future-dated values as malformed.\n if (creationTime >= 1e15 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n return { raw, creationTime, clickId };\n}\n\n/** Build a fresh `_gcl_aw`/`_gcl_gb` value in gtag's own format. */\nexport function formatGcl(clickId: string, now: number): string {\n return `GCL.${Math.floor(now / 1000)}.${clickId}`;\n}\n\n// bat.js writes the click id as `_uet<msclkid>` and reads it back only when what follows the\n// prefix is at most 32 characters (`msClkIdCookieValuePrefix`, `lengthMsClkId`); a msclkid is a\n// 32-hex-digit UUID without dashes. Re-verify against bat.js before changing anything here —\n// the format is UET's, not ours.\nconst UET_MSCLKID_PREFIX = '_uet';\nconst MSCLKID = /^[0-9a-f]{32}$/i;\n\n/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */\nexport function isMsclkid(value: string | null | undefined): value is string {\n return !!value && MSCLKID.test(value);\n}\n\n/**\n * Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined\n * for anything bat.js itself would not read back.\n */\nexport function parseUetMsclkid(raw: string | undefined | null): string | undefined {\n if (!raw || !raw.startsWith(UET_MSCLKID_PREFIX)) return undefined;\n const clickId = raw.slice(UET_MSCLKID_PREFIX.length);\n return MSCLKID.test(clickId) ? clickId.toLowerCase() : undefined;\n}\n\n/** Build a `_uetmsclkid` value in bat.js's own format. */\nexport function formatUetMsclkid(clickId: string): string {\n return `${UET_MSCLKID_PREFIX}${clickId.toLowerCase()}`;\n}\n\n/**\n * The click id as the Conversions API wants it: a dashed, lowercase UUID. The URL and the cookie\n * carry the 32-hex form; a value in any other shape is returned untouched for the API to judge.\n */\nexport function formatMsclkid(clickId: string): string {\n const id = clickId.trim().toLowerCase();\n if (!MSCLKID.test(id)) return id;\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /** `Domain` attribute for the emitted cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` at its *remaining* lifetime on every call. On by default, as an\n * ITP self-heal: cookies follow last-writer-wins, so if the Meta Pixel overwrote `_fbc` via\n * `document.cookie` (Safari caps JS-written cookies on a fbclid-decorated landing to 24h),\n * re-issuing the long-lived HTTP cookie on the next navigation restores it — the same\n * continuous-re-issue mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster).\n * The value (and its creationTime) is byte-for-byte identical and the window never slides, so it\n * cannot trigger Meta's expired/modified-fbclid warnings.\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n /** The resolved Google Ads click id (from the URL or a still-valid `_gcl_aw`). */\n gclid?: string;\n /** The resolved iOS web-to-app click id (from the URL or a still-valid `_gcl_gb`). */\n wbraid?: string;\n /** The resolved Microsoft Ads click id (from the URL or `_uetmsclkid`), 32-hex form. */\n msclkid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Google Ads _gcl_aw / _gcl_gb ---\n // The same first-party HTTP persistence Google's own server-side Conversion Linker performs\n // (it sets FPGCLAW via Set-Cookie for 90 days), applied to gtag's cookies the way the sGTM\n // ecosystem does (stape Cookie Keeper re-issues _gcl_* over HTTP). Ownership is shared with\n // gtag, so the rules are stricter than for _fbc: a value this module cannot parse is left\n // untouched — never rewritten, never deleted — and a re-issue is byte-identical (labels tail\n // included) at the window's REMAINING lifetime, so the 90-day click window never slides.\n const gclsrc = params.get('gclsrc') || undefined;\n const google = [\n {\n cookie: GCL_AW_COOKIE,\n // gtag's own gating (`isGoogleAdsGclid`): a gclid enters _gcl_aw only when gclsrc is\n // absent or 'aw.ds' — 'ds'/'3p.ds' clicks are Search Ads 360's and belong to _gcl_dc.\n urlClickId: isGoogleAdsGclid(gclsrc) ? params.get('gclid') || undefined : undefined,\n assign: (value: string) => (result.gclid = value),\n },\n {\n cookie: GCL_GB_COOKIE,\n urlClickId: params.get('wbraid') || undefined,\n assign: (value: string) => (result.wbraid = value),\n },\n ];\n for (const { cookie, urlClickId, assign } of google) {\n const existing = parseGcl(jar[cookie], now);\n if (urlClickId && GCL_CLICK_ID.test(urlClickId) && urlClickId !== existing?.clickId) {\n // A new click always wins and opens a fresh 90-day window, in gtag's exact format.\n set(cookie, formatGcl(urlClickId, now), GCL_TTL_MS);\n assign(urlClickId);\n } else if (existing) {\n const remainingMs = existing.creationTime + GCL_TTL_MS - now;\n if (remainingMs > 0) {\n assign(existing.clickId);\n if (refresh) set(cookie, existing.raw, remainingMs);\n }\n // Expired: not returned and not re-issued, but gtag owns the deletion.\n }\n }\n\n // --- Reddit _rdt_cid ---\n // No embedded timestamp, so it can only be anchored at first capture; set it once from the URL and\n // otherwise leave the existing cookie untouched (re-issuing would slide its window).\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n }\n\n // --- Microsoft Ads _uetmsclkid ---\n // The UET tag's own cookie, in its own format, so the tag and this module read each other's\n // writes. Microsoft's ITP answer is this first-party cookie (plus a localStorage backup), but\n // bat.js writes it through `document.cookie`, which Safari caps at 7 days — 24 hours on a\n // landing page decorated by a classified domain, and a bing.com ad click is exactly that.\n // Persisting it here over HTTP on the document response restores the 90-day window.\n //\n // The re-issue (default on, see `refresh`) is the same self-heal as for `_fbc`, and it matters\n // more here: bat.js rewrites this cookie on EVERY page load, so without the re-issue the HTTP\n // copy is downgraded to a JS cookie on the very next page. There is no embedded timestamp to\n // compute a remaining lifetime from, so it is re-issued at a fresh 90 days — which is exactly\n // what bat.js itself does on each page (`msClkIdExpirationTime` from now), so the window\n // slides no more than the tag already slides it. A value this module cannot parse belongs to\n // the tag: never rewritten, never deleted.\n const urlMsclkid = params.get('msclkid')?.trim();\n const existingMsclkid = parseUetMsclkid(jar[UET_MSCLKID_COOKIE]);\n if (urlMsclkid && MSCLKID.test(urlMsclkid) && urlMsclkid.toLowerCase() !== existingMsclkid) {\n set(UET_MSCLKID_COOKIE, formatUetMsclkid(urlMsclkid), MSCLKID_TTL_MS);\n result.msclkid = urlMsclkid.toLowerCase();\n } else if (existingMsclkid) {\n result.msclkid = existingMsclkid;\n if (refresh) set(UET_MSCLKID_COOKIE, formatUetMsclkid(existingMsclkid), MSCLKID_TTL_MS);\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["parseCookie","cookie","stringifySetCookie"],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`, `_rdt_cid`,\n * `_uetmsclkid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * The Google pair follows the same architecture Google itself ships for server containers —\n * sGTM's Conversion Linker sets FPGCLAW via Set-Cookie for 90 days — and the sGTM ecosystem\n * productized for gtag's own cookies (stape Cookie Keeper re-issues `_gcl_*` over HTTP; vendor\n * case studies report 5–20% of Google Ads conversions recovered, up to +44% on the Safari\n * slice). One deployment caveat from WebKit's rules: the HTTP-cookie exemption holds only for\n * responses from the site's own infrastructure — a CNAME or third-party-IP host (a tagging\n * subdomain on someone else's cloud) gets its Set-Cookie silently capped to 7 days. This module\n * runs on the host's own document response, which is exactly the exempt case.\n *\n * references:\n * - https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n * - https://webkit.org/tracking-prevention/ (7d JS cookies, 24h on ad-decorated landings,\n * CNAME/third-party-IP HTTP cookies capped to 7d, same-origin HTTP cookies exempt)\n * - https://www.simoahava.com/analytics/google-ads-server-side-tagging-google-tag-manager/\n * (Conversion Linker → FPGCLAW via Set-Cookie, Google's own server-side equivalent)\n * - https://stape.io/helpdesk/documentation/cookie-keeper-power-up (the productized re-issue)\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Reddit's pixel.js rewrites `_rdt_cid` and `_rdt_uuid` for 90 days on every page load.\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_UUID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Meta's pixel rewrites `_fbp` for 90 days on every page load.\nconst FBP_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// OpenAI's oaiq: `__oppref` for 30 days (`720*60*60` s), `__obref` for 365 days.\nconst OPPREF_TTL_MS = 30 * 24 * 60 * 60 * 1000;\nconst OBREF_TTL_MS = 365 * 24 * 60 * 60 * 1000;\n// bat.js's own expiry for _uetmsclkid (`msClkIdExpirationTime`), the Microsoft Ads click window.\nconst MSCLKID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// gtag's own expiry for _gcl_* — 90 days, the length of a Google Ads click's upload window.\nconst GCL_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\nexport const GCL_AW_COOKIE = '_gcl_aw';\nexport const GCL_GB_COOKIE = '_gcl_gb';\nexport const UET_MSCLKID_COOKIE = '_uetmsclkid';\nexport const RDT_UUID_COOKIE = '_rdt_uuid';\nexport const FBP_COOKIE = '_fbp';\nexport const OPPREF_COOKIE = '__oppref';\nexport const OBREF_COOKIE = '__obref';\n/** OpenAI's consent cookie; `false` is the visitor's opt-out, on which oaiq deletes its cookies. */\nexport const OAIQ_CONSENT_COOKIE = '__oaiq_consent';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's\n * Parameter Builder adds to the values it writes (\"an appendix field at the end of each\n * parameter\"); it is not part of the click id, so `fbclid` is the fourth segment alone and the\n * value is passed on as `raw`, unchanged. Returns undefined for anything malformed.\n * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts.length > 5 || parts[0] !== 'fb') return undefined;\n if (parts.length === 5 && !parts[4]) return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts[3];\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ParsedGcl = { raw: string; creationTime: number; clickId: string };\n\n/**\n * Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or\n * `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on\n * Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.\n * ref: https://support.google.com/sa360/answer/7342044\n */\nexport function isGoogleAdsGclid(gclsrc: string | null | undefined): boolean {\n return !gclsrc || gclsrc === 'aw.ds';\n}\n\n// gtag's own value validator, extracted from the live gtag.js bundle (2026-09): its parser\n// accepts version \"GCL\" or \"1\", a /^\\d+$/ seconds timestamp, and a click id matching this.\n// Re-verify against gtag.js before changing anything here — the format is gtag's, not ours.\nconst GCL_CLICK_ID = /^[\\w-]+$/;\n\n/**\n * Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp\n * is in SECONDS, unlike `_fbc`'s milliseconds. gtag's parser accepts version `GCL` or `1` and\n * validates the segments exactly this way (`/^\\d+$/` timestamp, `/^[\\w-]+$/` click id); any\n * labels tail is preserved through `raw` so a re-issue stays byte-identical.\n */\nexport function parseGcl(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedGcl | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 3) return undefined;\n if (parts[0] !== 'GCL' && parts[0] !== '1') return undefined;\n const seconds = parts[1];\n const clickId = parts[2];\n if (seconds === undefined || !/^\\d+$/.test(seconds)) return undefined;\n if (clickId === undefined || !GCL_CLICK_ID.test(clickId)) return undefined;\n const creationTime = Number(seconds) * 1000;\n // Reject millisecond-precision or future-dated values as malformed.\n if (creationTime >= 1e15 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n return { raw, creationTime, clickId };\n}\n\n/** Build a fresh `_gcl_aw`/`_gcl_gb` value in gtag's own format. */\nexport function formatGcl(clickId: string, now: number): string {\n return `GCL.${Math.floor(now / 1000)}.${clickId}`;\n}\n\n// bat.js writes the click id as `_uet<msclkid>` and reads it back only when what follows the\n// prefix is at most 32 characters (`msClkIdCookieValuePrefix`, `lengthMsClkId`); a msclkid is a\n// 32-hex-digit UUID without dashes. Re-verify against bat.js before changing anything here —\n// the format is UET's, not ours.\nconst UET_MSCLKID_PREFIX = '_uet';\nconst MSCLKID = /^[0-9a-f]{32}$/i;\n\n/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */\nexport function isMsclkid(value: string | null | undefined): value is string {\n return !!value && MSCLKID.test(value);\n}\n\n/**\n * Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined\n * for anything bat.js itself would not read back.\n */\nexport function parseUetMsclkid(raw: string | undefined | null): string | undefined {\n if (!raw || !raw.startsWith(UET_MSCLKID_PREFIX)) return undefined;\n const clickId = raw.slice(UET_MSCLKID_PREFIX.length);\n return MSCLKID.test(clickId) ? clickId.toLowerCase() : undefined;\n}\n\n/** Build a `_uetmsclkid` value in bat.js's own format. */\nexport function formatUetMsclkid(clickId: string): string {\n return `${UET_MSCLKID_PREFIX}${clickId.toLowerCase()}`;\n}\n\n/**\n * The click id as the Conversions API wants it: a dashed, lowercase UUID. The URL and the cookie\n * carry the 32-hex form; a value in any other shape is returned untouched for the API to judge.\n */\nexport function formatMsclkid(clickId: string): string {\n const id = clickId.trim().toLowerCase();\n if (!MSCLKID.test(id)) return id;\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /**\n * `Domain` attribute for the emitted cookies: the site's registrable domain, e.g.\n * `.shware.io`, where the pixels write theirs. Required in production: a host-only cookie is a\n * second cookie of the same name next to the pixel's, which the pixel then reads — or deletes,\n * as Reddit's does at every subdomain level before writing its own. `null` for a host-only\n * cookie, which only a local `localhost` setup should ask for.\n */\n domain: string | null;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue the stored click and browser ids on every call. On by default, as an ITP self-heal:\n * cookies follow last-writer-wins, so when a pixel has rewritten one through `document.cookie`\n * (Safari caps JS-written cookies at 7 days, 24h on an ad-decorated landing), re-issuing the\n * long-lived HTTP cookie on the next navigation restores it — the same continuous-re-issue\n * mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster). `_fbc` and\n * `_gcl_*` are re-issued byte-for-byte at their *remaining* lifetime, so their window never\n * slides (Meta warns on a moved fbclid); the others at the lifetime their pixel itself gives\n * them on every page load (see each below).\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n /** The resolved Google Ads click id (from the URL or a still-valid `_gcl_aw`). */\n gclid?: string;\n /** The resolved iOS web-to-app click id (from the URL or a still-valid `_gcl_gb`). */\n wbraid?: string;\n /** The resolved Microsoft Ads click id (from the URL or `_uetmsclkid`), 32-hex form. */\n msclkid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain: domain ?? undefined } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Google Ads _gcl_aw / _gcl_gb ---\n // The same first-party HTTP persistence Google's own server-side Conversion Linker performs\n // (it sets FPGCLAW via Set-Cookie for 90 days), applied to gtag's cookies the way the sGTM\n // ecosystem does (stape Cookie Keeper re-issues _gcl_* over HTTP). Ownership is shared with\n // gtag, so the rules are stricter than for _fbc: a value this module cannot parse is left\n // untouched — never rewritten, never deleted — and a re-issue is byte-identical (labels tail\n // included) at the window's REMAINING lifetime, so the 90-day click window never slides.\n const gclsrc = params.get('gclsrc') || undefined;\n const google = [\n {\n cookie: GCL_AW_COOKIE,\n // gtag's own gating (`isGoogleAdsGclid`): a gclid enters _gcl_aw only when gclsrc is\n // absent or 'aw.ds' — 'ds'/'3p.ds' clicks are Search Ads 360's and belong to _gcl_dc.\n urlClickId: isGoogleAdsGclid(gclsrc) ? params.get('gclid') || undefined : undefined,\n assign: (value: string) => (result.gclid = value),\n },\n {\n cookie: GCL_GB_COOKIE,\n urlClickId: params.get('wbraid') || undefined,\n assign: (value: string) => (result.wbraid = value),\n },\n ];\n for (const { cookie, urlClickId, assign } of google) {\n const existing = parseGcl(jar[cookie], now);\n if (urlClickId && GCL_CLICK_ID.test(urlClickId) && urlClickId !== existing?.clickId) {\n // A new click always wins and opens a fresh 90-day window, in gtag's exact format.\n set(cookie, formatGcl(urlClickId, now), GCL_TTL_MS);\n assign(urlClickId);\n } else if (existing) {\n const remainingMs = existing.creationTime + GCL_TTL_MS - now;\n if (remainingMs > 0) {\n assign(existing.clickId);\n if (refresh) set(cookie, existing.raw, remainingMs);\n }\n // Expired: not returned and not re-issued, but gtag owns the deletion.\n }\n }\n\n // --- Reddit _rdt_cid / _rdt_uuid ---\n // Reddit's pixel.js rewrites both on every page load through `document.cookie` — the click id\n // from the URL, else the stored one, and the browser id — each for a fresh 90 days. So Reddit\n // slides the window itself, and the re-issue (default on, see `refresh`) does the same over\n // HTTP: without it the pixel's JS copy, capped at 7 days in Safari, is the only one left.\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n if (refresh) set(RDT_CID_COOKIE, existingRdtCid, RDT_CID_TTL_MS);\n }\n const existingRdtUuid = jar[RDT_UUID_COOKIE] || undefined;\n if (existingRdtUuid && refresh) set(RDT_UUID_COOKIE, existingRdtUuid, RDT_UUID_TTL_MS);\n\n // --- Meta _fbp ---\n // The pixel's browser id: created by the pixel, which rewrites it for 90 days on every page\n // load, as Reddit's does its own. Re-issued as it is, never created here.\n const existingFbp = jar[FBP_COOKIE] || undefined;\n if (existingFbp && refresh) set(FBP_COOKIE, existingFbp, FBP_TTL_MS);\n\n // --- OpenAI __oppref / __obref ---\n // oaiq writes `__oppref` only when the URL carries `oppref` (30 days), and `__obref` — its\n // browser reference — only when there is none (365 days); otherwise it reads them, through\n // `document.cookie`, so in Safari each is gone after 7 days and the browser starts over with a\n // new reference. The first is captured here from the URL as well, and both are re-issued as\n // they are, `__obref` never created. Unlike oaiq, the re-issue restarts their windows: their\n // values carry no creation time to count a remaining lifetime from, and OpenAI applies its own\n // attribution window to the click whatever the cookie's age. Nothing is written once the\n // visitor has opted out of oaiq, which deletes both cookies then.\n if (jar[OAIQ_CONSENT_COOKIE] !== 'false') {\n const urlOppref = params.get('oppref') || undefined;\n const existingOppref = jar[OPPREF_COOKIE] || undefined;\n if (urlOppref) set(OPPREF_COOKIE, urlOppref, OPPREF_TTL_MS);\n else if (existingOppref && refresh) set(OPPREF_COOKIE, existingOppref, OPPREF_TTL_MS);\n const existingObref = jar[OBREF_COOKIE] || undefined;\n if (existingObref && refresh) set(OBREF_COOKIE, existingObref, OBREF_TTL_MS);\n }\n\n // --- Microsoft Ads _uetmsclkid ---\n // The UET tag's own cookie, in its own format, so the tag and this module read each other's\n // writes. Microsoft's ITP answer is this first-party cookie (plus a localStorage backup), but\n // bat.js writes it through `document.cookie`, which Safari caps at 7 days — 24 hours on a\n // landing page decorated by a classified domain, and a bing.com ad click is exactly that.\n // Persisting it here over HTTP on the document response restores the 90-day window.\n //\n // The re-issue (default on, see `refresh`) is the same self-heal as for `_fbc`, and it matters\n // more here: bat.js rewrites this cookie on EVERY page load, so without the re-issue the HTTP\n // copy is downgraded to a JS cookie on the very next page. There is no embedded timestamp to\n // compute a remaining lifetime from, so it is re-issued at a fresh 90 days — which is exactly\n // what bat.js itself does on each page (`msClkIdExpirationTime` from now), so the window\n // slides no more than the tag already slides it. A value this module cannot parse belongs to\n // the tag: never rewritten, never deleted.\n const urlMsclkid = params.get('msclkid')?.trim();\n const existingMsclkid = parseUetMsclkid(jar[UET_MSCLKID_COOKIE]);\n if (urlMsclkid && MSCLKID.test(urlMsclkid) && urlMsclkid.toLowerCase() !== existingMsclkid) {\n set(UET_MSCLKID_COOKIE, formatUetMsclkid(urlMsclkid), MSCLKID_TTL_MS);\n result.msclkid = urlMsclkid.toLowerCase();\n } else if (existingMsclkid) {\n result.msclkid = existingMsclkid;\n if (refresh) set(UET_MSCLKID_COOKIE, formatUetMsclkid(existingMsclkid), MSCLKID_TTL_MS);\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":"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"}
|
|
@@ -5,6 +5,12 @@ export declare const RDT_CID_COOKIE = "_rdt_cid";
|
|
|
5
5
|
export declare const GCL_AW_COOKIE = "_gcl_aw";
|
|
6
6
|
export declare const GCL_GB_COOKIE = "_gcl_gb";
|
|
7
7
|
export declare const UET_MSCLKID_COOKIE = "_uetmsclkid";
|
|
8
|
+
export declare const RDT_UUID_COOKIE = "_rdt_uuid";
|
|
9
|
+
export declare const FBP_COOKIE = "_fbp";
|
|
10
|
+
export declare const OPPREF_COOKIE = "__oppref";
|
|
11
|
+
export declare const OBREF_COOKIE = "__obref";
|
|
12
|
+
/** OpenAI's consent cookie; `false` is the visitor's opt-out, on which oaiq deletes its cookies. */
|
|
13
|
+
export declare const OAIQ_CONSENT_COOKIE = "__oaiq_consent";
|
|
8
14
|
export type ParsedFbc = {
|
|
9
15
|
raw: string;
|
|
10
16
|
creationTime: number;
|
|
@@ -65,20 +71,27 @@ export type ResolveClickIdCookiesInput = {
|
|
|
65
71
|
cookieHeader?: string | null;
|
|
66
72
|
/** Overridable clock, primarily for tests. Defaults to `Date.now()`. */
|
|
67
73
|
now?: number;
|
|
68
|
-
/**
|
|
69
|
-
|
|
74
|
+
/**
|
|
75
|
+
* `Domain` attribute for the emitted cookies: the site's registrable domain, e.g.
|
|
76
|
+
* `.shware.io`, where the pixels write theirs. Required in production: a host-only cookie is a
|
|
77
|
+
* second cookie of the same name next to the pixel's, which the pixel then reads — or deletes,
|
|
78
|
+
* as Reddit's does at every subdomain level before writing its own. `null` for a host-only
|
|
79
|
+
* cookie, which only a local `localhost` setup should ask for.
|
|
80
|
+
*/
|
|
81
|
+
domain: string | null;
|
|
70
82
|
/** `Secure` attribute, default true. Set false only for local http testing. */
|
|
71
83
|
secure?: boolean;
|
|
72
84
|
/** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */
|
|
73
85
|
subdomainIndex?: number;
|
|
74
86
|
/**
|
|
75
|
-
* Re-issue
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
87
|
+
* Re-issue the stored click and browser ids on every call. On by default, as an ITP self-heal:
|
|
88
|
+
* cookies follow last-writer-wins, so when a pixel has rewritten one through `document.cookie`
|
|
89
|
+
* (Safari caps JS-written cookies at 7 days, 24h on an ad-decorated landing), re-issuing the
|
|
90
|
+
* long-lived HTTP cookie on the next navigation restores it — the same continuous-re-issue
|
|
91
|
+
* mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster). `_fbc` and
|
|
92
|
+
* `_gcl_*` are re-issued byte-for-byte at their *remaining* lifetime, so their window never
|
|
93
|
+
* slides (Meta warns on a moved fbclid); the others at the lifetime their pixel itself gives
|
|
94
|
+
* them on every page load (see each below).
|
|
82
95
|
*
|
|
83
96
|
* Set false to strictly follow Meta's documented conditional-write rule ("only set the cookie if
|
|
84
97
|
* it doesn't exist or the fbclid changed") — e.g. to keep pages CDN-cacheable, since the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/click-id/index.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/click-id/index.ts"],"mappings":";;qBAkDa;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;;qBAEA;YAED;EAAc;EAAa;EAAsB;;;;;;;;;wBAS7C,SACd,gCACA,eACC;;;;;wBAoBa,UAAU,gBAAgB,aAAa;YAI3C;EAAc;EAAa;EAAsB;;;;;;;;wBAQ7C,iBAAiB;;;;;;;wBAejB,SACd,gCACA,eACC;;wBAgBa,UAAU,iBAAiB;;wBAY3B,UAAU,mCAAmC;;;;;wBAQ7C,gBAAgB;;wBAOhB,iBAAiB;;;;;wBAQjB,cAAc;YAMlB;;EAEV;;EAEA;;EAEA;;;;;;;;EAQA;;EAEA;;EAEA;;;;;;;;;;;;;;;;EAgBA;;YAGU;;;;;EAKV,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;wBAuBc,sBACd,OAAO,6BACN;;wBAiJa,mBAAmB,SAAS"}
|
|
@@ -5,6 +5,12 @@ export declare const RDT_CID_COOKIE = "_rdt_cid";
|
|
|
5
5
|
export declare const GCL_AW_COOKIE = "_gcl_aw";
|
|
6
6
|
export declare const GCL_GB_COOKIE = "_gcl_gb";
|
|
7
7
|
export declare const UET_MSCLKID_COOKIE = "_uetmsclkid";
|
|
8
|
+
export declare const RDT_UUID_COOKIE = "_rdt_uuid";
|
|
9
|
+
export declare const FBP_COOKIE = "_fbp";
|
|
10
|
+
export declare const OPPREF_COOKIE = "__oppref";
|
|
11
|
+
export declare const OBREF_COOKIE = "__obref";
|
|
12
|
+
/** OpenAI's consent cookie; `false` is the visitor's opt-out, on which oaiq deletes its cookies. */
|
|
13
|
+
export declare const OAIQ_CONSENT_COOKIE = "__oaiq_consent";
|
|
8
14
|
export type ParsedFbc = {
|
|
9
15
|
raw: string;
|
|
10
16
|
creationTime: number;
|
|
@@ -65,20 +71,27 @@ export type ResolveClickIdCookiesInput = {
|
|
|
65
71
|
cookieHeader?: string | null;
|
|
66
72
|
/** Overridable clock, primarily for tests. Defaults to `Date.now()`. */
|
|
67
73
|
now?: number;
|
|
68
|
-
/**
|
|
69
|
-
|
|
74
|
+
/**
|
|
75
|
+
* `Domain` attribute for the emitted cookies: the site's registrable domain, e.g.
|
|
76
|
+
* `.shware.io`, where the pixels write theirs. Required in production: a host-only cookie is a
|
|
77
|
+
* second cookie of the same name next to the pixel's, which the pixel then reads — or deletes,
|
|
78
|
+
* as Reddit's does at every subdomain level before writing its own. `null` for a host-only
|
|
79
|
+
* cookie, which only a local `localhost` setup should ask for.
|
|
80
|
+
*/
|
|
81
|
+
domain: string | null;
|
|
70
82
|
/** `Secure` attribute, default true. Set false only for local http testing. */
|
|
71
83
|
secure?: boolean;
|
|
72
84
|
/** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */
|
|
73
85
|
subdomainIndex?: number;
|
|
74
86
|
/**
|
|
75
|
-
* Re-issue
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
87
|
+
* Re-issue the stored click and browser ids on every call. On by default, as an ITP self-heal:
|
|
88
|
+
* cookies follow last-writer-wins, so when a pixel has rewritten one through `document.cookie`
|
|
89
|
+
* (Safari caps JS-written cookies at 7 days, 24h on an ad-decorated landing), re-issuing the
|
|
90
|
+
* long-lived HTTP cookie on the next navigation restores it — the same continuous-re-issue
|
|
91
|
+
* mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster). `_fbc` and
|
|
92
|
+
* `_gcl_*` are re-issued byte-for-byte at their *remaining* lifetime, so their window never
|
|
93
|
+
* slides (Meta warns on a moved fbclid); the others at the lifetime their pixel itself gives
|
|
94
|
+
* them on every page load (see each below).
|
|
82
95
|
*
|
|
83
96
|
* Set false to strictly follow Meta's documented conditional-write rule ("only set the cookie if
|
|
84
97
|
* it doesn't exist or the fbclid changed") — e.g. to keep pages CDN-cacheable, since the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/click-id/index.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/click-id/index.ts"],"mappings":";;qBAkDa;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;;qBAEA;YAED;EAAc;EAAa;EAAsB;;;;;;;;;wBAS7C,SACd,gCACA,eACC;;;;;wBAoBa,UAAU,gBAAgB,aAAa;YAI3C;EAAc;EAAa;EAAsB;;;;;;;;wBAQ7C,iBAAiB;;;;;;;wBAejB,SACd,gCACA,eACC;;wBAgBa,UAAU,iBAAiB;;wBAY3B,UAAU,mCAAmC;;;;;wBAQ7C,gBAAgB;;wBAOhB,iBAAiB;;;;;wBAQjB,cAAc;YAMlB;;EAEV;;EAEA;;EAEA;;;;;;;;EAQA;;EAEA;;EAEA;;;;;;;;;;;;;;;;EAgBA;;YAGU;;;;;EAKV,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;wBAuBc,sBACd,OAAO,6BACN;;wBAiJa,mBAAmB,SAAS"}
|
package/dist/click-id/index.mjs
CHANGED
|
@@ -30,6 +30,10 @@ import { parseCookie, stringifySetCookie } from "cookie";
|
|
|
30
30
|
*/
|
|
31
31
|
const FBC_TTL_MS = 7776e6;
|
|
32
32
|
const RDT_CID_TTL_MS = 7776e6;
|
|
33
|
+
const RDT_UUID_TTL_MS = 7776e6;
|
|
34
|
+
const FBP_TTL_MS = 7776e6;
|
|
35
|
+
const OPPREF_TTL_MS = 2592e6;
|
|
36
|
+
const OBREF_TTL_MS = 31536e6;
|
|
33
37
|
const MSCLKID_TTL_MS = 7776e6;
|
|
34
38
|
const GCL_TTL_MS = 7776e6;
|
|
35
39
|
const CLOCK_SKEW_MS = 864e5;
|
|
@@ -38,6 +42,12 @@ const RDT_CID_COOKIE = "_rdt_cid";
|
|
|
38
42
|
const GCL_AW_COOKIE = "_gcl_aw";
|
|
39
43
|
const GCL_GB_COOKIE = "_gcl_gb";
|
|
40
44
|
const UET_MSCLKID_COOKIE = "_uetmsclkid";
|
|
45
|
+
const RDT_UUID_COOKIE = "_rdt_uuid";
|
|
46
|
+
const FBP_COOKIE = "_fbp";
|
|
47
|
+
const OPPREF_COOKIE = "__oppref";
|
|
48
|
+
const OBREF_COOKIE = "__obref";
|
|
49
|
+
/** OpenAI's consent cookie; `false` is the visitor's opt-out, on which oaiq deletes its cookies. */
|
|
50
|
+
const OAIQ_CONSENT_COOKIE = "__oaiq_consent";
|
|
41
51
|
/**
|
|
42
52
|
* Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's
|
|
43
53
|
* Parameter Builder adds to the values it writes ("an appendix field at the end of each
|
|
@@ -162,7 +172,7 @@ function resolveClickIdCookies(input) {
|
|
|
162
172
|
path: "/",
|
|
163
173
|
secure,
|
|
164
174
|
sameSite: "lax",
|
|
165
|
-
domain
|
|
175
|
+
domain: domain ?? void 0
|
|
166
176
|
};
|
|
167
177
|
const set = (name, value, ttlMs) => cookies.push({
|
|
168
178
|
name,
|
|
@@ -218,7 +228,22 @@ function resolveClickIdCookies(input) {
|
|
|
218
228
|
if (urlRdtCid && urlRdtCid !== existingRdtCid) {
|
|
219
229
|
set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);
|
|
220
230
|
result.rdt_cid = urlRdtCid;
|
|
221
|
-
} else if (existingRdtCid)
|
|
231
|
+
} else if (existingRdtCid) {
|
|
232
|
+
result.rdt_cid = existingRdtCid;
|
|
233
|
+
if (refresh) set(RDT_CID_COOKIE, existingRdtCid, RDT_CID_TTL_MS);
|
|
234
|
+
}
|
|
235
|
+
const existingRdtUuid = jar["_rdt_uuid"] || void 0;
|
|
236
|
+
if (existingRdtUuid && refresh) set(RDT_UUID_COOKIE, existingRdtUuid, RDT_UUID_TTL_MS);
|
|
237
|
+
const existingFbp = jar["_fbp"] || void 0;
|
|
238
|
+
if (existingFbp && refresh) set(FBP_COOKIE, existingFbp, FBP_TTL_MS);
|
|
239
|
+
if (jar["__oaiq_consent"] !== "false") {
|
|
240
|
+
const urlOppref = params.get("oppref") || void 0;
|
|
241
|
+
const existingOppref = jar["__oppref"] || void 0;
|
|
242
|
+
if (urlOppref) set(OPPREF_COOKIE, urlOppref, OPPREF_TTL_MS);
|
|
243
|
+
else if (existingOppref && refresh) set(OPPREF_COOKIE, existingOppref, OPPREF_TTL_MS);
|
|
244
|
+
const existingObref = jar["__obref"] || void 0;
|
|
245
|
+
if (existingObref && refresh) set(OBREF_COOKIE, existingObref, OBREF_TTL_MS);
|
|
246
|
+
}
|
|
222
247
|
const urlMsclkid = params.get("msclkid")?.trim();
|
|
223
248
|
const existingMsclkid = parseUetMsclkid(jar[UET_MSCLKID_COOKIE]);
|
|
224
249
|
if (urlMsclkid && MSCLKID.test(urlMsclkid) && urlMsclkid.toLowerCase() !== existingMsclkid) {
|
|
@@ -235,6 +260,6 @@ function toSetCookieHeaders(cookies) {
|
|
|
235
260
|
return cookies.map((cookie) => stringifySetCookie(cookie));
|
|
236
261
|
}
|
|
237
262
|
//#endregion
|
|
238
|
-
export { FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, RDT_CID_COOKIE, UET_MSCLKID_COOKIE, formatFbc, formatGcl, formatMsclkid, formatUetMsclkid, isGoogleAdsGclid, isMsclkid, parseFbc, parseGcl, parseUetMsclkid, resolveClickIdCookies, toSetCookieHeaders };
|
|
263
|
+
export { FBC_COOKIE, FBP_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, OAIQ_CONSENT_COOKIE, OBREF_COOKIE, OPPREF_COOKIE, RDT_CID_COOKIE, RDT_UUID_COOKIE, UET_MSCLKID_COOKIE, formatFbc, formatGcl, formatMsclkid, formatUetMsclkid, isGoogleAdsGclid, isMsclkid, parseFbc, parseGcl, parseUetMsclkid, resolveClickIdCookies, toSetCookieHeaders };
|
|
239
264
|
|
|
240
265
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`, `_rdt_cid`,\n * `_uetmsclkid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * The Google pair follows the same architecture Google itself ships for server containers —\n * sGTM's Conversion Linker sets FPGCLAW via Set-Cookie for 90 days — and the sGTM ecosystem\n * productized for gtag's own cookies (stape Cookie Keeper re-issues `_gcl_*` over HTTP; vendor\n * case studies report 5–20% of Google Ads conversions recovered, up to +44% on the Safari\n * slice). One deployment caveat from WebKit's rules: the HTTP-cookie exemption holds only for\n * responses from the site's own infrastructure — a CNAME or third-party-IP host (a tagging\n * subdomain on someone else's cloud) gets its Set-Cookie silently capped to 7 days. This module\n * runs on the host's own document response, which is exactly the exempt case.\n *\n * references:\n * - https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n * - https://webkit.org/tracking-prevention/ (7d JS cookies, 24h on ad-decorated landings,\n * CNAME/third-party-IP HTTP cookies capped to 7d, same-origin HTTP cookies exempt)\n * - https://www.simoahava.com/analytics/google-ads-server-side-tagging-google-tag-manager/\n * (Conversion Linker → FPGCLAW via Set-Cookie, Google's own server-side equivalent)\n * - https://stape.io/helpdesk/documentation/cookie-keeper-power-up (the productized re-issue)\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// bat.js's own expiry for _uetmsclkid (`msClkIdExpirationTime`), the Microsoft Ads click window.\nconst MSCLKID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// gtag's own expiry for _gcl_* — 90 days, the length of a Google Ads click's upload window.\nconst GCL_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\nexport const GCL_AW_COOKIE = '_gcl_aw';\nexport const GCL_GB_COOKIE = '_gcl_gb';\nexport const UET_MSCLKID_COOKIE = '_uetmsclkid';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's\n * Parameter Builder adds to the values it writes (\"an appendix field at the end of each\n * parameter\"); it is not part of the click id, so `fbclid` is the fourth segment alone and the\n * value is passed on as `raw`, unchanged. Returns undefined for anything malformed.\n * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts.length > 5 || parts[0] !== 'fb') return undefined;\n if (parts.length === 5 && !parts[4]) return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts[3];\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ParsedGcl = { raw: string; creationTime: number; clickId: string };\n\n/**\n * Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or\n * `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on\n * Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.\n * ref: https://support.google.com/sa360/answer/7342044\n */\nexport function isGoogleAdsGclid(gclsrc: string | null | undefined): boolean {\n return !gclsrc || gclsrc === 'aw.ds';\n}\n\n// gtag's own value validator, extracted from the live gtag.js bundle (2026-09): its parser\n// accepts version \"GCL\" or \"1\", a /^\\d+$/ seconds timestamp, and a click id matching this.\n// Re-verify against gtag.js before changing anything here — the format is gtag's, not ours.\nconst GCL_CLICK_ID = /^[\\w-]+$/;\n\n/**\n * Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp\n * is in SECONDS, unlike `_fbc`'s milliseconds. gtag's parser accepts version `GCL` or `1` and\n * validates the segments exactly this way (`/^\\d+$/` timestamp, `/^[\\w-]+$/` click id); any\n * labels tail is preserved through `raw` so a re-issue stays byte-identical.\n */\nexport function parseGcl(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedGcl | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 3) return undefined;\n if (parts[0] !== 'GCL' && parts[0] !== '1') return undefined;\n const seconds = parts[1];\n const clickId = parts[2];\n if (seconds === undefined || !/^\\d+$/.test(seconds)) return undefined;\n if (clickId === undefined || !GCL_CLICK_ID.test(clickId)) return undefined;\n const creationTime = Number(seconds) * 1000;\n // Reject millisecond-precision or future-dated values as malformed.\n if (creationTime >= 1e15 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n return { raw, creationTime, clickId };\n}\n\n/** Build a fresh `_gcl_aw`/`_gcl_gb` value in gtag's own format. */\nexport function formatGcl(clickId: string, now: number): string {\n return `GCL.${Math.floor(now / 1000)}.${clickId}`;\n}\n\n// bat.js writes the click id as `_uet<msclkid>` and reads it back only when what follows the\n// prefix is at most 32 characters (`msClkIdCookieValuePrefix`, `lengthMsClkId`); a msclkid is a\n// 32-hex-digit UUID without dashes. Re-verify against bat.js before changing anything here —\n// the format is UET's, not ours.\nconst UET_MSCLKID_PREFIX = '_uet';\nconst MSCLKID = /^[0-9a-f]{32}$/i;\n\n/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */\nexport function isMsclkid(value: string | null | undefined): value is string {\n return !!value && MSCLKID.test(value);\n}\n\n/**\n * Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined\n * for anything bat.js itself would not read back.\n */\nexport function parseUetMsclkid(raw: string | undefined | null): string | undefined {\n if (!raw || !raw.startsWith(UET_MSCLKID_PREFIX)) return undefined;\n const clickId = raw.slice(UET_MSCLKID_PREFIX.length);\n return MSCLKID.test(clickId) ? clickId.toLowerCase() : undefined;\n}\n\n/** Build a `_uetmsclkid` value in bat.js's own format. */\nexport function formatUetMsclkid(clickId: string): string {\n return `${UET_MSCLKID_PREFIX}${clickId.toLowerCase()}`;\n}\n\n/**\n * The click id as the Conversions API wants it: a dashed, lowercase UUID. The URL and the cookie\n * carry the 32-hex form; a value in any other shape is returned untouched for the API to judge.\n */\nexport function formatMsclkid(clickId: string): string {\n const id = clickId.trim().toLowerCase();\n if (!MSCLKID.test(id)) return id;\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /** `Domain` attribute for the emitted cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` at its *remaining* lifetime on every call. On by default, as an\n * ITP self-heal: cookies follow last-writer-wins, so if the Meta Pixel overwrote `_fbc` via\n * `document.cookie` (Safari caps JS-written cookies on a fbclid-decorated landing to 24h),\n * re-issuing the long-lived HTTP cookie on the next navigation restores it — the same\n * continuous-re-issue mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster).\n * The value (and its creationTime) is byte-for-byte identical and the window never slides, so it\n * cannot trigger Meta's expired/modified-fbclid warnings.\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n /** The resolved Google Ads click id (from the URL or a still-valid `_gcl_aw`). */\n gclid?: string;\n /** The resolved iOS web-to-app click id (from the URL or a still-valid `_gcl_gb`). */\n wbraid?: string;\n /** The resolved Microsoft Ads click id (from the URL or `_uetmsclkid`), 32-hex form. */\n msclkid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Google Ads _gcl_aw / _gcl_gb ---\n // The same first-party HTTP persistence Google's own server-side Conversion Linker performs\n // (it sets FPGCLAW via Set-Cookie for 90 days), applied to gtag's cookies the way the sGTM\n // ecosystem does (stape Cookie Keeper re-issues _gcl_* over HTTP). Ownership is shared with\n // gtag, so the rules are stricter than for _fbc: a value this module cannot parse is left\n // untouched — never rewritten, never deleted — and a re-issue is byte-identical (labels tail\n // included) at the window's REMAINING lifetime, so the 90-day click window never slides.\n const gclsrc = params.get('gclsrc') || undefined;\n const google = [\n {\n cookie: GCL_AW_COOKIE,\n // gtag's own gating (`isGoogleAdsGclid`): a gclid enters _gcl_aw only when gclsrc is\n // absent or 'aw.ds' — 'ds'/'3p.ds' clicks are Search Ads 360's and belong to _gcl_dc.\n urlClickId: isGoogleAdsGclid(gclsrc) ? params.get('gclid') || undefined : undefined,\n assign: (value: string) => (result.gclid = value),\n },\n {\n cookie: GCL_GB_COOKIE,\n urlClickId: params.get('wbraid') || undefined,\n assign: (value: string) => (result.wbraid = value),\n },\n ];\n for (const { cookie, urlClickId, assign } of google) {\n const existing = parseGcl(jar[cookie], now);\n if (urlClickId && GCL_CLICK_ID.test(urlClickId) && urlClickId !== existing?.clickId) {\n // A new click always wins and opens a fresh 90-day window, in gtag's exact format.\n set(cookie, formatGcl(urlClickId, now), GCL_TTL_MS);\n assign(urlClickId);\n } else if (existing) {\n const remainingMs = existing.creationTime + GCL_TTL_MS - now;\n if (remainingMs > 0) {\n assign(existing.clickId);\n if (refresh) set(cookie, existing.raw, remainingMs);\n }\n // Expired: not returned and not re-issued, but gtag owns the deletion.\n }\n }\n\n // --- Reddit _rdt_cid ---\n // No embedded timestamp, so it can only be anchored at first capture; set it once from the URL and\n // otherwise leave the existing cookie untouched (re-issuing would slide its window).\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n }\n\n // --- Microsoft Ads _uetmsclkid ---\n // The UET tag's own cookie, in its own format, so the tag and this module read each other's\n // writes. Microsoft's ITP answer is this first-party cookie (plus a localStorage backup), but\n // bat.js writes it through `document.cookie`, which Safari caps at 7 days — 24 hours on a\n // landing page decorated by a classified domain, and a bing.com ad click is exactly that.\n // Persisting it here over HTTP on the document response restores the 90-day window.\n //\n // The re-issue (default on, see `refresh`) is the same self-heal as for `_fbc`, and it matters\n // more here: bat.js rewrites this cookie on EVERY page load, so without the re-issue the HTTP\n // copy is downgraded to a JS cookie on the very next page. There is no embedded timestamp to\n // compute a remaining lifetime from, so it is re-issued at a fresh 90 days — which is exactly\n // what bat.js itself does on each page (`msClkIdExpirationTime` from now), so the window\n // slides no more than the tag already slides it. A value this module cannot parse belongs to\n // the tag: never rewritten, never deleted.\n const urlMsclkid = params.get('msclkid')?.trim();\n const existingMsclkid = parseUetMsclkid(jar[UET_MSCLKID_COOKIE]);\n if (urlMsclkid && MSCLKID.test(urlMsclkid) && urlMsclkid.toLowerCase() !== existingMsclkid) {\n set(UET_MSCLKID_COOKIE, formatUetMsclkid(urlMsclkid), MSCLKID_TTL_MS);\n result.msclkid = urlMsclkid.toLowerCase();\n } else if (existingMsclkid) {\n result.msclkid = existingMsclkid;\n if (refresh) set(UET_MSCLKID_COOKIE, formatUetMsclkid(existingMsclkid), MSCLKID_TTL_MS);\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/click-id/index.ts"],"sourcesContent":["import { type SetCookie, parseCookie, stringifySetCookie } from 'cookie';\n\n/**\n * Server-side resolution of ad-click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`, `_rdt_cid`,\n * `_uetmsclkid`) from the incoming request.\n *\n * This is the framework-agnostic core meant to run on the *document* response (e.g. TanStack Start\n * server middleware, Next middleware). Setting `_fbc` via an HTTP `Set-Cookie` header on the top\n * document — rather than `document.cookie` on the client — is what Meta officially recommends and is\n * the only reliable way to keep the cookie alive for 90 days in Safari: ITP caps JavaScript-set\n * cookies on a fbclid-decorated landing page to 24 hours, and the document response is never\n * classified as CNAME/IP cloaking (it is the reference the browser measures cloaking against).\n *\n * The Google pair follows the same architecture Google itself ships for server containers —\n * sGTM's Conversion Linker sets FPGCLAW via Set-Cookie for 90 days — and the sGTM ecosystem\n * productized for gtag's own cookies (stape Cookie Keeper re-issues `_gcl_*` over HTTP; vendor\n * case studies report 5–20% of Google Ads conversions recovered, up to +44% on the Safari\n * slice). One deployment caveat from WebKit's rules: the HTTP-cookie exemption holds only for\n * responses from the site's own infrastructure — a CNAME or third-party-IP host (a tagging\n * subdomain on someone else's cloud) gets its Set-Cookie silently capped to 7 days. This module\n * runs on the host's own document response, which is exactly the exempt case.\n *\n * references:\n * - https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n * - https://webkit.org/tracking-prevention/ (7d JS cookies, 24h on ad-decorated landings,\n * CNAME/third-party-IP HTTP cookies capped to 7d, same-origin HTTP cookies exempt)\n * - https://www.simoahava.com/analytics/google-ads-server-side-tagging-google-tag-manager/\n * (Conversion Linker → FPGCLAW via Set-Cookie, Google's own server-side equivalent)\n * - https://stape.io/helpdesk/documentation/cookie-keeper-power-up (the productized re-issue)\n */\n\n// Meta's recommended _fbc cookie expiry. Events Manager warns (\"Server sending expired fbclid\")\n// when the embedded creationTime is older than 90 days and match quality/attribution may degrade;\n// no hard drop is documented.\nconst FBC_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Reddit's pixel.js rewrites `_rdt_cid` and `_rdt_uuid` for 90 days on every page load.\nconst RDT_CID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\nconst RDT_UUID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Meta's pixel rewrites `_fbp` for 90 days on every page load.\nconst FBP_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// OpenAI's oaiq: `__oppref` for 30 days (`720*60*60` s), `__obref` for 365 days.\nconst OPPREF_TTL_MS = 30 * 24 * 60 * 60 * 1000;\nconst OBREF_TTL_MS = 365 * 24 * 60 * 60 * 1000;\n// bat.js's own expiry for _uetmsclkid (`msClkIdExpirationTime`), the Microsoft Ads click window.\nconst MSCLKID_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// gtag's own expiry for _gcl_* — 90 days, the length of a Google Ads click's upload window.\nconst GCL_TTL_MS = 90 * 24 * 60 * 60 * 1000;\n// Tolerate a little clock skew when validating a creationTime against \"now\".\nconst CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;\n\nexport const FBC_COOKIE = '_fbc';\nexport const RDT_CID_COOKIE = '_rdt_cid';\nexport const GCL_AW_COOKIE = '_gcl_aw';\nexport const GCL_GB_COOKIE = '_gcl_gb';\nexport const UET_MSCLKID_COOKIE = '_uetmsclkid';\nexport const RDT_UUID_COOKIE = '_rdt_uuid';\nexport const FBP_COOKIE = '_fbp';\nexport const OPPREF_COOKIE = '__oppref';\nexport const OBREF_COOKIE = '__obref';\n/** OpenAI's consent cookie; `false` is the visitor's opt-out, on which oaiq deletes its cookies. */\nexport const OAIQ_CONSENT_COOKIE = '__oaiq_consent';\n\nexport type ParsedFbc = { raw: string; creationTime: number; fbclid: string };\n\n/**\n * Parse `fb.<subdomainIndex>.<creationTime>.<fbclid>[.<appendix>]`. The appendix is what Meta's\n * Parameter Builder adds to the values it writes (\"an appendix field at the end of each\n * parameter\"); it is not part of the click id, so `fbclid` is the fourth segment alone and the\n * value is passed on as `raw`, unchanged. Returns undefined for anything malformed.\n * ref: https://developers.facebook.com/docs/marketing-api/conversions-api/parameter-builder-library\n */\nexport function parseFbc(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedFbc | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 4 || parts.length > 5 || parts[0] !== 'fb') return undefined;\n if (parts.length === 5 && !parts[4]) return undefined;\n\n const creationTime = Number(parts[2]);\n const fbclid = parts[3];\n if (!fbclid) return undefined;\n // creationTime is UNIX ms; reject seconds-precision or future-dated values as malformed.\n if (!Number.isFinite(creationTime)) return undefined;\n if (creationTime < 1e12 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n\n return { raw, creationTime, fbclid };\n}\n\n/**\n * Build a fresh `_fbc` value. subdomainIndex is the cookie's domain level (com=0, example.com=1,\n * www.example.com=2); 1 is correct for an apex-hosted cookie.\n */\nexport function formatFbc(fbclid: string, now: number, subdomainIndex = 1): string {\n return `fb.${subdomainIndex}.${now}.${fbclid}`;\n}\n\nexport type ParsedGcl = { raw: string; creationTime: number; clickId: string };\n\n/**\n * Whether a URL's `gclid` is a Google Ads click, by its `gclsrc` — gtag's own gating: absent or\n * `aw.ds` is Google Ads; `ds` / `3p.ds` are Search Ads 360's (a click on another engine, or on\n * Google with auto-tagging off), which gtag keeps in `_gcl_dc` and not `_gcl_aw`.\n * ref: https://support.google.com/sa360/answer/7342044\n */\nexport function isGoogleAdsGclid(gclsrc: string | null | undefined): boolean {\n return !gclsrc || gclsrc === 'aw.ds';\n}\n\n// gtag's own value validator, extracted from the live gtag.js bundle (2026-09): its parser\n// accepts version \"GCL\" or \"1\", a /^\\d+$/ seconds timestamp, and a click id matching this.\n// Re-verify against gtag.js before changing anything here — the format is gtag's, not ours.\nconst GCL_CLICK_ID = /^[\\w-]+$/;\n\n/**\n * Parse a `_gcl_aw`/`_gcl_gb` value: `GCL.<seconds>.<clickId>[.<labels…>]` — note the timestamp\n * is in SECONDS, unlike `_fbc`'s milliseconds. gtag's parser accepts version `GCL` or `1` and\n * validates the segments exactly this way (`/^\\d+$/` timestamp, `/^[\\w-]+$/` click id); any\n * labels tail is preserved through `raw` so a re-issue stays byte-identical.\n */\nexport function parseGcl(\n raw: string | undefined | null,\n now: number = Date.now()\n): ParsedGcl | undefined {\n if (!raw) return undefined;\n const parts = raw.split('.');\n if (parts.length < 3) return undefined;\n if (parts[0] !== 'GCL' && parts[0] !== '1') return undefined;\n const seconds = parts[1];\n const clickId = parts[2];\n if (seconds === undefined || !/^\\d+$/.test(seconds)) return undefined;\n if (clickId === undefined || !GCL_CLICK_ID.test(clickId)) return undefined;\n const creationTime = Number(seconds) * 1000;\n // Reject millisecond-precision or future-dated values as malformed.\n if (creationTime >= 1e15 || creationTime > now + CLOCK_SKEW_MS) return undefined;\n return { raw, creationTime, clickId };\n}\n\n/** Build a fresh `_gcl_aw`/`_gcl_gb` value in gtag's own format. */\nexport function formatGcl(clickId: string, now: number): string {\n return `GCL.${Math.floor(now / 1000)}.${clickId}`;\n}\n\n// bat.js writes the click id as `_uet<msclkid>` and reads it back only when what follows the\n// prefix is at most 32 characters (`msClkIdCookieValuePrefix`, `lengthMsClkId`); a msclkid is a\n// 32-hex-digit UUID without dashes. Re-verify against bat.js before changing anything here —\n// the format is UET's, not ours.\nconst UET_MSCLKID_PREFIX = '_uet';\nconst MSCLKID = /^[0-9a-f]{32}$/i;\n\n/** Whether a value has the shape of a Microsoft click id: 32 hex digits, no dashes. */\nexport function isMsclkid(value: string | null | undefined): value is string {\n return !!value && MSCLKID.test(value);\n}\n\n/**\n * Parse a `_uetmsclkid` value: `_uet<32 hex>`. Returns the click id, lowercased, or undefined\n * for anything bat.js itself would not read back.\n */\nexport function parseUetMsclkid(raw: string | undefined | null): string | undefined {\n if (!raw || !raw.startsWith(UET_MSCLKID_PREFIX)) return undefined;\n const clickId = raw.slice(UET_MSCLKID_PREFIX.length);\n return MSCLKID.test(clickId) ? clickId.toLowerCase() : undefined;\n}\n\n/** Build a `_uetmsclkid` value in bat.js's own format. */\nexport function formatUetMsclkid(clickId: string): string {\n return `${UET_MSCLKID_PREFIX}${clickId.toLowerCase()}`;\n}\n\n/**\n * The click id as the Conversions API wants it: a dashed, lowercase UUID. The URL and the cookie\n * carry the 32-hex form; a value in any other shape is returned untouched for the API to judge.\n */\nexport function formatMsclkid(clickId: string): string {\n const id = clickId.trim().toLowerCase();\n if (!MSCLKID.test(id)) return id;\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;\n}\n\nexport type ResolveClickIdCookiesInput = {\n /** The absolute request URL (must include the query string). */\n url: string;\n /** The raw `Cookie` request header, if any. */\n cookieHeader?: string | null;\n /** Overridable clock, primarily for tests. Defaults to `Date.now()`. */\n now?: number;\n /**\n * `Domain` attribute for the emitted cookies: the site's registrable domain, e.g.\n * `.shware.io`, where the pixels write theirs. Required in production: a host-only cookie is a\n * second cookie of the same name next to the pixel's, which the pixel then reads — or deletes,\n * as Reddit's does at every subdomain level before writing its own. `null` for a host-only\n * cookie, which only a local `localhost` setup should ask for.\n */\n domain: string | null;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (see {@link formatFbc}). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue the stored click and browser ids on every call. On by default, as an ITP self-heal:\n * cookies follow last-writer-wins, so when a pixel has rewritten one through `document.cookie`\n * (Safari caps JS-written cookies at 7 days, 24h on an ad-decorated landing), re-issuing the\n * long-lived HTTP cookie on the next navigation restores it — the same continuous-re-issue\n * mitigation the sGTM ecosystem uses (stape Cookie Keeper, Cookie Monster). `_fbc` and\n * `_gcl_*` are re-issued byte-for-byte at their *remaining* lifetime, so their window never\n * slides (Meta warns on a moved fbclid); the others at the lifetime their pixel itself gives\n * them on every page load (see each below).\n *\n * Set false to strictly follow Meta's documented conditional-write rule (\"only set the cookie if\n * it doesn't exist or the fbclid changed\") — e.g. to keep pages CDN-cacheable, since the\n * re-issue attaches a per-user `Set-Cookie` (forcing `no-store`) to every response carrying an\n * `_fbc`.\n */\n refresh?: boolean;\n};\n\nexport type ResolveClickIdCookiesResult = {\n /**\n * Cookies to emit on the response, ready for `Set-Cookie`. Empty when nothing needs to change.\n * A `value: ''`, `maxAge: 0` entry is a deletion (an expired or malformed leftover).\n */\n cookies: SetCookie[];\n /** The resolved `_fbc` value, for immediate server-side use (e.g. Conversions API). */\n fbc?: string;\n /** The resolved `_rdt_cid` value. */\n rdt_cid?: string;\n /** The resolved Google Ads click id (from the URL or a still-valid `_gcl_aw`). */\n gclid?: string;\n /** The resolved iOS web-to-app click id (from the URL or a still-valid `_gcl_gb`). */\n wbraid?: string;\n /** The resolved Microsoft Ads click id (from the URL or `_uetmsclkid`), 32-hex form. */\n msclkid?: string;\n};\n\nfunction searchParams(url: string): URLSearchParams {\n try {\n return new URL(url).searchParams;\n } catch {\n // Fall back to a manual split so a relative or slightly malformed URL still yields the query.\n const q = url.indexOf('?');\n return new URLSearchParams(q !== -1 ? url.slice(q + 1) : '');\n }\n}\n\n/**\n * Resolve the click-id cookies to set on the current document response.\n *\n * `_fbc` follows Meta's documented conditional-write rule value-wise: a new fbclid (or an absent\n * cookie) opens a fresh 90-day window; a same-fbclid cookie keeps its original value and\n * `creationTime`. Expired (>90d) or malformed values are cleared instead of forwarded. By default,\n * ({@link ResolveClickIdCookiesInput.refresh}) a still-valid cookie is re-issued unchanged at its\n * *remaining* lifetime — never `now + 90d` — so a returning visitor's window cannot slide forward\n * (which is what makes Meta flag an expired fbclid).\n */\nexport function resolveClickIdCookies(\n input: ResolveClickIdCookiesInput\n): ResolveClickIdCookiesResult {\n const { url, cookieHeader, domain, secure = true, subdomainIndex = 1, refresh = true } = input;\n const now = input.now ?? Date.now();\n\n const params = searchParams(url);\n const jar = parseCookie(cookieHeader ?? '');\n const cookies: SetCookie[] = [];\n const result: ResolveClickIdCookiesResult = { cookies };\n\n const base = { path: '/', secure, sameSite: 'lax', domain: domain ?? undefined } as const;\n const set = (name: string, value: string, ttlMs: number) =>\n cookies.push({ name, value, maxAge: Math.floor(ttlMs / 1000), ...base });\n const del = (name: string) => cookies.push({ name, value: '', maxAge: 0, ...base });\n\n // --- Meta _fbc ---\n const urlFbclid = params.get('fbclid') || undefined;\n const existingFbc = parseFbc(jar[FBC_COOKIE], now);\n\n if (urlFbclid && urlFbclid !== existingFbc?.fbclid) {\n // A new click always wins and opens a fresh 90-day window.\n const raw = formatFbc(urlFbclid, now, subdomainIndex);\n set(FBC_COOKIE, raw, FBC_TTL_MS);\n result.fbc = raw;\n } else if (existingFbc) {\n const remainingMs = existingFbc.creationTime + FBC_TTL_MS - now;\n if (remainingMs <= 0) {\n del(FBC_COOKIE);\n } else {\n // Same fbclid: the value and creationTime are preserved (Meta's rule); refresh (default)\n // re-issues it unchanged at its remaining lifetime as an ITP self-heal.\n result.fbc = existingFbc.raw;\n if (refresh) set(FBC_COOKIE, existingFbc.raw, remainingMs);\n }\n } else if (jar[FBC_COOKIE]) {\n // Malformed leftover — clear it rather than forwarding it to Meta.\n del(FBC_COOKIE);\n }\n\n // --- Google Ads _gcl_aw / _gcl_gb ---\n // The same first-party HTTP persistence Google's own server-side Conversion Linker performs\n // (it sets FPGCLAW via Set-Cookie for 90 days), applied to gtag's cookies the way the sGTM\n // ecosystem does (stape Cookie Keeper re-issues _gcl_* over HTTP). Ownership is shared with\n // gtag, so the rules are stricter than for _fbc: a value this module cannot parse is left\n // untouched — never rewritten, never deleted — and a re-issue is byte-identical (labels tail\n // included) at the window's REMAINING lifetime, so the 90-day click window never slides.\n const gclsrc = params.get('gclsrc') || undefined;\n const google = [\n {\n cookie: GCL_AW_COOKIE,\n // gtag's own gating (`isGoogleAdsGclid`): a gclid enters _gcl_aw only when gclsrc is\n // absent or 'aw.ds' — 'ds'/'3p.ds' clicks are Search Ads 360's and belong to _gcl_dc.\n urlClickId: isGoogleAdsGclid(gclsrc) ? params.get('gclid') || undefined : undefined,\n assign: (value: string) => (result.gclid = value),\n },\n {\n cookie: GCL_GB_COOKIE,\n urlClickId: params.get('wbraid') || undefined,\n assign: (value: string) => (result.wbraid = value),\n },\n ];\n for (const { cookie, urlClickId, assign } of google) {\n const existing = parseGcl(jar[cookie], now);\n if (urlClickId && GCL_CLICK_ID.test(urlClickId) && urlClickId !== existing?.clickId) {\n // A new click always wins and opens a fresh 90-day window, in gtag's exact format.\n set(cookie, formatGcl(urlClickId, now), GCL_TTL_MS);\n assign(urlClickId);\n } else if (existing) {\n const remainingMs = existing.creationTime + GCL_TTL_MS - now;\n if (remainingMs > 0) {\n assign(existing.clickId);\n if (refresh) set(cookie, existing.raw, remainingMs);\n }\n // Expired: not returned and not re-issued, but gtag owns the deletion.\n }\n }\n\n // --- Reddit _rdt_cid / _rdt_uuid ---\n // Reddit's pixel.js rewrites both on every page load through `document.cookie` — the click id\n // from the URL, else the stored one, and the browser id — each for a fresh 90 days. So Reddit\n // slides the window itself, and the re-issue (default on, see `refresh`) does the same over\n // HTTP: without it the pixel's JS copy, capped at 7 days in Safari, is the only one left.\n const urlRdtCid = params.get('rdt_cid') || undefined;\n const existingRdtCid = jar[RDT_CID_COOKIE] || undefined;\n if (urlRdtCid && urlRdtCid !== existingRdtCid) {\n set(RDT_CID_COOKIE, urlRdtCid, RDT_CID_TTL_MS);\n result.rdt_cid = urlRdtCid;\n } else if (existingRdtCid) {\n result.rdt_cid = existingRdtCid;\n if (refresh) set(RDT_CID_COOKIE, existingRdtCid, RDT_CID_TTL_MS);\n }\n const existingRdtUuid = jar[RDT_UUID_COOKIE] || undefined;\n if (existingRdtUuid && refresh) set(RDT_UUID_COOKIE, existingRdtUuid, RDT_UUID_TTL_MS);\n\n // --- Meta _fbp ---\n // The pixel's browser id: created by the pixel, which rewrites it for 90 days on every page\n // load, as Reddit's does its own. Re-issued as it is, never created here.\n const existingFbp = jar[FBP_COOKIE] || undefined;\n if (existingFbp && refresh) set(FBP_COOKIE, existingFbp, FBP_TTL_MS);\n\n // --- OpenAI __oppref / __obref ---\n // oaiq writes `__oppref` only when the URL carries `oppref` (30 days), and `__obref` — its\n // browser reference — only when there is none (365 days); otherwise it reads them, through\n // `document.cookie`, so in Safari each is gone after 7 days and the browser starts over with a\n // new reference. The first is captured here from the URL as well, and both are re-issued as\n // they are, `__obref` never created. Unlike oaiq, the re-issue restarts their windows: their\n // values carry no creation time to count a remaining lifetime from, and OpenAI applies its own\n // attribution window to the click whatever the cookie's age. Nothing is written once the\n // visitor has opted out of oaiq, which deletes both cookies then.\n if (jar[OAIQ_CONSENT_COOKIE] !== 'false') {\n const urlOppref = params.get('oppref') || undefined;\n const existingOppref = jar[OPPREF_COOKIE] || undefined;\n if (urlOppref) set(OPPREF_COOKIE, urlOppref, OPPREF_TTL_MS);\n else if (existingOppref && refresh) set(OPPREF_COOKIE, existingOppref, OPPREF_TTL_MS);\n const existingObref = jar[OBREF_COOKIE] || undefined;\n if (existingObref && refresh) set(OBREF_COOKIE, existingObref, OBREF_TTL_MS);\n }\n\n // --- Microsoft Ads _uetmsclkid ---\n // The UET tag's own cookie, in its own format, so the tag and this module read each other's\n // writes. Microsoft's ITP answer is this first-party cookie (plus a localStorage backup), but\n // bat.js writes it through `document.cookie`, which Safari caps at 7 days — 24 hours on a\n // landing page decorated by a classified domain, and a bing.com ad click is exactly that.\n // Persisting it here over HTTP on the document response restores the 90-day window.\n //\n // The re-issue (default on, see `refresh`) is the same self-heal as for `_fbc`, and it matters\n // more here: bat.js rewrites this cookie on EVERY page load, so without the re-issue the HTTP\n // copy is downgraded to a JS cookie on the very next page. There is no embedded timestamp to\n // compute a remaining lifetime from, so it is re-issued at a fresh 90 days — which is exactly\n // what bat.js itself does on each page (`msClkIdExpirationTime` from now), so the window\n // slides no more than the tag already slides it. A value this module cannot parse belongs to\n // the tag: never rewritten, never deleted.\n const urlMsclkid = params.get('msclkid')?.trim();\n const existingMsclkid = parseUetMsclkid(jar[UET_MSCLKID_COOKIE]);\n if (urlMsclkid && MSCLKID.test(urlMsclkid) && urlMsclkid.toLowerCase() !== existingMsclkid) {\n set(UET_MSCLKID_COOKIE, formatUetMsclkid(urlMsclkid), MSCLKID_TTL_MS);\n result.msclkid = urlMsclkid.toLowerCase();\n } else if (existingMsclkid) {\n result.msclkid = existingMsclkid;\n if (refresh) set(UET_MSCLKID_COOKIE, formatUetMsclkid(existingMsclkid), MSCLKID_TTL_MS);\n }\n\n return result;\n}\n\n/** Serialize the resolved cookies into `Set-Cookie` header values. */\nexport function toSetCookieHeaders(cookies: SetCookie[]): string[] {\n return cookies.map((cookie) => stringifySetCookie(cookie));\n}\n"],"mappings":"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"}
|
package/dist/tanstack/index.cjs
CHANGED
|
@@ -2,5 +2,4 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_tanstack_analytics = require("./analytics.cjs");
|
|
3
3
|
const require_tanstack_middleware = require("./middleware.cjs");
|
|
4
4
|
exports.Analytics = require_tanstack_analytics.Analytics;
|
|
5
|
-
exports.clickIdMiddleware = require_tanstack_middleware.clickIdMiddleware;
|
|
6
5
|
exports.createClickIdMiddleware = require_tanstack_middleware.createClickIdMiddleware;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Analytics } from "./analytics.cjs";
|
|
2
|
-
import { ClickIdMiddlewareOptions,
|
|
3
|
-
export { Analytics, type ClickIdMiddlewareOptions,
|
|
2
|
+
import { ClickIdMiddlewareOptions, createClickIdMiddleware } from "./middleware.cjs";
|
|
3
|
+
export { Analytics, type ClickIdMiddlewareOptions, createClickIdMiddleware };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Analytics } from "./analytics.mjs";
|
|
2
|
-
import { ClickIdMiddlewareOptions,
|
|
3
|
-
export { Analytics, type ClickIdMiddlewareOptions,
|
|
2
|
+
import { ClickIdMiddlewareOptions, createClickIdMiddleware } from "./middleware.mjs";
|
|
3
|
+
export { Analytics, type ClickIdMiddlewareOptions, createClickIdMiddleware };
|
package/dist/tanstack/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Analytics } from "./analytics.mjs";
|
|
2
|
-
import {
|
|
3
|
-
export { Analytics,
|
|
2
|
+
import { createClickIdMiddleware } from "./middleware.mjs";
|
|
3
|
+
export { Analytics, createClickIdMiddleware };
|
|
@@ -3,9 +3,21 @@ const require_click_id_index = require("../click-id/index.cjs");
|
|
|
3
3
|
let _tanstack_react_start = require("@tanstack/react-start");
|
|
4
4
|
//#region src/tanstack/middleware.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
6
|
+
* Whether a response could be stored by a shared cache (a CDN) under its `Cache-Control`. One
|
|
7
|
+
* that is `private` or `no-store` already cannot, so a per-user `Set-Cookie` on it is safe as it
|
|
8
|
+
* is, and its own caching is kept — the gtag.js loader served through a first-party Google Tag
|
|
9
|
+
* Gateway (`private, max-age=…`) stays in the browser cache, where a browser never replays a
|
|
10
|
+
* `Set-Cookie`. Anything else — `public`, `s-maxage`, no header at all — might be shared, and is
|
|
11
|
+
* made uncacheable.
|
|
12
|
+
*/
|
|
13
|
+
function sharedCacheable(value) {
|
|
14
|
+
if (!value) return true;
|
|
15
|
+
return !value.toLowerCase().split(",").map((directive) => directive.trim()).some((directive) => directive === "private" || directive.startsWith("private=") || directive === "no-store");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* TanStack Start request middleware that persists the ad platforms' first-party cookies on the
|
|
19
|
+
* document response: `_fbc`, `_fbp`, `_gcl_aw` / `_gcl_gb`, `_rdt_cid`, `_rdt_uuid`,
|
|
20
|
+
* `_uetmsclkid`, `__oppref` and `__obref`.
|
|
9
21
|
*
|
|
10
22
|
* Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS
|
|
11
23
|
* runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for
|
|
@@ -17,13 +29,17 @@ let _tanstack_react_start = require("@tanstack/react-start");
|
|
|
17
29
|
* ```ts
|
|
18
30
|
* // start.ts
|
|
19
31
|
* import { createStart } from '@tanstack/react-start'
|
|
20
|
-
* import {
|
|
32
|
+
* import { createClickIdMiddleware } from '@shware/analytics/tanstack'
|
|
33
|
+
* const clickIdMiddleware = createClickIdMiddleware({
|
|
34
|
+
* domain: import.meta.env.DEV ? null : '.example.com',
|
|
35
|
+
* secure: !import.meta.env.DEV,
|
|
36
|
+
* })
|
|
21
37
|
* export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))
|
|
22
38
|
* ```
|
|
23
39
|
*
|
|
24
40
|
* reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
|
|
25
41
|
*/
|
|
26
|
-
function createClickIdMiddleware(options
|
|
42
|
+
function createClickIdMiddleware(options) {
|
|
27
43
|
const { cacheControl = "private, no-store" } = options;
|
|
28
44
|
return (0, _tanstack_react_start.createMiddleware)({ type: "request" }).server(async ({ request, next, handlerType }) => {
|
|
29
45
|
const result = await next();
|
|
@@ -39,15 +55,12 @@ function createClickIdMiddleware(options = {}) {
|
|
|
39
55
|
});
|
|
40
56
|
if (cookies.length > 0) {
|
|
41
57
|
for (const header of require_click_id_index.toSetCookieHeaders(cookies)) result.response.headers.append("set-cookie", header);
|
|
42
|
-
if (cacheControl !== false) result.response.headers.set("cache-control", cacheControl);
|
|
58
|
+
if (cacheControl !== false && sharedCacheable(result.response.headers.get("cache-control"))) result.response.headers.set("cache-control", cacheControl);
|
|
43
59
|
}
|
|
44
60
|
return result;
|
|
45
61
|
});
|
|
46
62
|
}
|
|
47
|
-
/** Ready-to-register middleware with default options. */
|
|
48
|
-
const clickIdMiddleware = createClickIdMiddleware();
|
|
49
63
|
//#endregion
|
|
50
|
-
exports.clickIdMiddleware = clickIdMiddleware;
|
|
51
64
|
exports.createClickIdMiddleware = createClickIdMiddleware;
|
|
52
65
|
|
|
53
66
|
//# sourceMappingURL=middleware.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.cjs","names":["createMiddleware","resolveClickIdCookies","toSetCookieHeaders"],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n
|
|
1
|
+
{"version":3,"file":"middleware.cjs","names":["createMiddleware","resolveClickIdCookies","toSetCookieHeaders"],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /**\n * `Domain` attribute for the cookies: the site's registrable domain, e.g. `.shware.io`, where the\n * pixels write theirs. Required — set it in production; `null` (host-only) is for `localhost`.\n * See `ResolveClickIdCookiesInput.domain`.\n */\n domain: string | null;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (com=0, example.com=1, www.example.com=2). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue the stored click and browser ids on every request as an ITP self-heal: the pixels\n * rewrite them through `document.cookie`, which Safari caps at 7 days (24 hours on an\n * ad-decorated landing), and the re-issue restores the long-lived HTTP cookie. On by default.\n * Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page response of a\n * visitor carrying one; set false to keep those pages CDN-cacheable. See\n * {@link resolveClickIdCookies}.\n */\n refresh?: boolean;\n /**\n * The `Cache-Control` for a response we attach cookies to when it could otherwise be stored by a\n * shared cache (default `private, no-store`): a per-user `Set-Cookie` must never end up on a\n * shared-cache entry, or one visitor's `_fbc` would be served to everyone. A response already\n * `private` or `no-store` keeps its own. Only set this false if you guarantee these responses\n * are never cached.\n */\n cacheControl?: string | false;\n /**\n * Consent gate. Return false to skip setting cookies for this request (e.g. before the visitor has\n * granted consent where required). Runs per request with the incoming `Request`.\n */\n shouldPersist?: (request: Request) => boolean;\n}\n\n/**\n * Whether a response could be stored by a shared cache (a CDN) under its `Cache-Control`. One\n * that is `private` or `no-store` already cannot, so a per-user `Set-Cookie` on it is safe as it\n * is, and its own caching is kept — the gtag.js loader served through a first-party Google Tag\n * Gateway (`private, max-age=…`) stays in the browser cache, where a browser never replays a\n * `Set-Cookie`. Anything else — `public`, `s-maxage`, no header at all — might be shared, and is\n * made uncacheable.\n */\nfunction sharedCacheable(value: string | null): boolean {\n if (!value) return true;\n const directives = value\n .toLowerCase()\n .split(',')\n .map((directive) => directive.trim());\n return !directives.some(\n (directive) =>\n directive === 'private' || directive.startsWith('private=') || directive === 'no-store'\n );\n}\n\n/**\n * TanStack Start request middleware that persists the ad platforms' first-party cookies on the\n * document response: `_fbc`, `_fbp`, `_gcl_aw` / `_gcl_gb`, `_rdt_cid`, `_rdt_uuid`,\n * `_uetmsclkid`, `__oppref` and `__obref`.\n *\n * Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS\n * runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for\n * 90 days in Safari: ITP caps JavaScript-set cookies on a fbclid-decorated landing page to 24\n * hours, and a document response is never classified as CNAME/IP cloaking (it is the reference the\n * browser measures cloaking against).\n *\n * Register it as a global request middleware:\n * ```ts\n * // start.ts\n * import { createStart } from '@tanstack/react-start'\n * import { createClickIdMiddleware } from '@shware/analytics/tanstack'\n * const clickIdMiddleware = createClickIdMiddleware({\n * domain: import.meta.env.DEV ? null : '.example.com',\n * secure: !import.meta.env.DEV,\n * })\n * export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))\n * ```\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\nexport function createClickIdMiddleware(options: ClickIdMiddlewareOptions) {\n const { cacheControl = 'private, no-store' } = options;\n\n return createMiddleware({ type: 'request' }).server(async ({ request, next, handlerType }) => {\n const result = await next();\n\n // Skip serverFn RPC responses. 'router' covers SSR document requests *and* custom server\n // routes (API endpoints) — those also get cookies when the URL carries a click id.\n if (handlerType !== 'router') return result;\n if (options.shouldPersist && !options.shouldPersist(request)) return result;\n\n const { cookies } = resolveClickIdCookies({\n url: request.url,\n cookieHeader: request.headers.get('cookie'),\n domain: options.domain,\n secure: options.secure,\n subdomainIndex: options.subdomainIndex,\n refresh: options.refresh ?? true,\n });\n\n if (cookies.length > 0) {\n for (const header of toSetCookieHeaders(cookies)) {\n result.response.headers.append('set-cookie', header);\n }\n if (cacheControl !== false && sharedCacheable(result.response.headers.get('cache-control'))) {\n result.response.headers.set('cache-control', cacheControl);\n }\n }\n\n return result;\n });\n}\n"],"mappings":";;;;;;;;;;;;AA8CA,SAAS,gBAAgB,OAA+B;CACtD,IAAI,CAAC,OAAO,OAAO;CAKnB,OAAO,CAJY,MAChB,YAAY,CAAC,CACb,MAAM,GAAG,CAAC,CACV,KAAK,cAAc,UAAU,KAAK,CACpB,CAAC,CAAC,MAChB,cACC,cAAc,aAAa,UAAU,WAAW,UAAU,KAAK,cAAc,UACjF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,wBAAwB,SAAmC;CACzE,MAAM,EAAE,eAAe,wBAAwB;CAE/C,QAAA,GAAOA,sBAAAA,iBAAAA,CAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,SAAS,MAAM,kBAAkB;EAC5F,MAAM,SAAS,MAAM,KAAK;EAI1B,IAAI,gBAAgB,UAAU,OAAO;EACrC,IAAI,QAAQ,iBAAiB,CAAC,QAAQ,cAAc,OAAO,GAAG,OAAO;EAErE,MAAM,EAAE,YAAYC,uBAAAA,sBAAsB;GACxC,KAAK,QAAQ;GACb,cAAc,QAAQ,QAAQ,IAAI,QAAQ;GAC1C,QAAQ,QAAQ;GAChB,QAAQ,QAAQ;GAChB,gBAAgB,QAAQ;GACxB,SAAS,QAAQ,WAAW;EAC9B,CAAC;EAED,IAAI,QAAQ,SAAS,GAAG;GACtB,KAAK,MAAM,UAAUC,uBAAAA,mBAAmB,OAAO,GAC7C,OAAO,SAAS,QAAQ,OAAO,cAAc,MAAM;GAErD,IAAI,iBAAiB,SAAS,gBAAgB,OAAO,SAAS,QAAQ,IAAI,eAAe,CAAC,GACxF,OAAO,SAAS,QAAQ,IAAI,iBAAiB,YAAY;EAE7D;EAEA,OAAO;CACT,CAAC;AACH"}
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
//#region src/tanstack/middleware.d.ts
|
|
2
2
|
export interface ClickIdMiddlewareOptions {
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `Domain` attribute for the cookies: the site's registrable domain, e.g. `.shware.io`, where the
|
|
5
|
+
* pixels write theirs. Required — set it in production; `null` (host-only) is for `localhost`.
|
|
6
|
+
* See `ResolveClickIdCookiesInput.domain`.
|
|
7
|
+
*/
|
|
8
|
+
domain: string | null;
|
|
5
9
|
/** `Secure` attribute, default true. Set false only for local http testing. */
|
|
6
10
|
secure?: boolean;
|
|
7
11
|
/** subdomainIndex for a freshly built `_fbc` (com=0, example.com=1, www.example.com=2). Default 1. */
|
|
8
12
|
subdomainIndex?: number;
|
|
9
13
|
/**
|
|
10
|
-
* Re-issue
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Re-issue the stored click and browser ids on every request as an ITP self-heal: the pixels
|
|
15
|
+
* rewrite them through `document.cookie`, which Safari caps at 7 days (24 hours on an
|
|
16
|
+
* ad-decorated landing), and the re-issue restores the long-lived HTTP cookie. On by default.
|
|
17
|
+
* Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page response of a
|
|
18
|
+
* visitor carrying one; set false to keep those pages CDN-cacheable. See
|
|
19
|
+
* {@link resolveClickIdCookies}.
|
|
15
20
|
*/
|
|
16
21
|
refresh?: boolean;
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
23
|
+
* The `Cache-Control` for a response we attach cookies to when it could otherwise be stored by a
|
|
24
|
+
* shared cache (default `private, no-store`): a per-user `Set-Cookie` must never end up on a
|
|
25
|
+
* shared-cache entry, or one visitor's `_fbc` would be served to everyone. A response already
|
|
26
|
+
* `private` or `no-store` keeps its own. Only set this false if you guarantee these responses
|
|
27
|
+
* are never cached.
|
|
21
28
|
*/
|
|
22
29
|
cacheControl?: string | false;
|
|
23
30
|
/**
|
|
@@ -27,9 +34,9 @@ export interface ClickIdMiddlewareOptions {
|
|
|
27
34
|
shouldPersist?: (request: Request) => boolean;
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
30
|
-
* TanStack Start request middleware that persists ad
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
37
|
+
* TanStack Start request middleware that persists the ad platforms' first-party cookies on the
|
|
38
|
+
* document response: `_fbc`, `_fbp`, `_gcl_aw` / `_gcl_gb`, `_rdt_cid`, `_rdt_uuid`,
|
|
39
|
+
* `_uetmsclkid`, `__oppref` and `__obref`.
|
|
33
40
|
*
|
|
34
41
|
* Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS
|
|
35
42
|
* runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for
|
|
@@ -41,14 +48,16 @@ export interface ClickIdMiddlewareOptions {
|
|
|
41
48
|
* ```ts
|
|
42
49
|
* // start.ts
|
|
43
50
|
* import { createStart } from '@tanstack/react-start'
|
|
44
|
-
* import {
|
|
51
|
+
* import { createClickIdMiddleware } from '@shware/analytics/tanstack'
|
|
52
|
+
* const clickIdMiddleware = createClickIdMiddleware({
|
|
53
|
+
* domain: import.meta.env.DEV ? null : '.example.com',
|
|
54
|
+
* secure: !import.meta.env.DEV,
|
|
55
|
+
* })
|
|
45
56
|
* export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))
|
|
46
57
|
* ```
|
|
47
58
|
*
|
|
48
59
|
* reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
|
|
49
60
|
*/
|
|
50
|
-
export declare function createClickIdMiddleware(options
|
|
51
|
-
/** Ready-to-register middleware with default options. */
|
|
52
|
-
export declare const clickIdMiddleware: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
61
|
+
export declare function createClickIdMiddleware(options: ClickIdMiddlewareOptions): import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
53
62
|
//#endregion
|
|
54
63
|
//# sourceMappingURL=middleware.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.cts","names":[],"sources":["../../src/tanstack/middleware.ts"],"mappings":";iBAGiB;;
|
|
1
|
+
{"version":3,"file":"middleware.d.cts","names":[],"sources":["../../src/tanstack/middleware.ts"],"mappings":";iBAGiB;;;;;;EAMf;;EAEA;;EAEA;;;;;;;;;EASA;;;;;;;;EAQA;;;;;EAKA,iBAAiB,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgDZ,wBAAwB,SAAS,2DAAwB"}
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
//#region src/tanstack/middleware.d.ts
|
|
2
2
|
export interface ClickIdMiddlewareOptions {
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `Domain` attribute for the cookies: the site's registrable domain, e.g. `.shware.io`, where the
|
|
5
|
+
* pixels write theirs. Required — set it in production; `null` (host-only) is for `localhost`.
|
|
6
|
+
* See `ResolveClickIdCookiesInput.domain`.
|
|
7
|
+
*/
|
|
8
|
+
domain: string | null;
|
|
5
9
|
/** `Secure` attribute, default true. Set false only for local http testing. */
|
|
6
10
|
secure?: boolean;
|
|
7
11
|
/** subdomainIndex for a freshly built `_fbc` (com=0, example.com=1, www.example.com=2). Default 1. */
|
|
8
12
|
subdomainIndex?: number;
|
|
9
13
|
/**
|
|
10
|
-
* Re-issue
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Re-issue the stored click and browser ids on every request as an ITP self-heal: the pixels
|
|
15
|
+
* rewrite them through `document.cookie`, which Safari caps at 7 days (24 hours on an
|
|
16
|
+
* ad-decorated landing), and the re-issue restores the long-lived HTTP cookie. On by default.
|
|
17
|
+
* Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page response of a
|
|
18
|
+
* visitor carrying one; set false to keep those pages CDN-cacheable. See
|
|
19
|
+
* {@link resolveClickIdCookies}.
|
|
15
20
|
*/
|
|
16
21
|
refresh?: boolean;
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
23
|
+
* The `Cache-Control` for a response we attach cookies to when it could otherwise be stored by a
|
|
24
|
+
* shared cache (default `private, no-store`): a per-user `Set-Cookie` must never end up on a
|
|
25
|
+
* shared-cache entry, or one visitor's `_fbc` would be served to everyone. A response already
|
|
26
|
+
* `private` or `no-store` keeps its own. Only set this false if you guarantee these responses
|
|
27
|
+
* are never cached.
|
|
21
28
|
*/
|
|
22
29
|
cacheControl?: string | false;
|
|
23
30
|
/**
|
|
@@ -27,9 +34,9 @@ export interface ClickIdMiddlewareOptions {
|
|
|
27
34
|
shouldPersist?: (request: Request) => boolean;
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
30
|
-
* TanStack Start request middleware that persists ad
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
37
|
+
* TanStack Start request middleware that persists the ad platforms' first-party cookies on the
|
|
38
|
+
* document response: `_fbc`, `_fbp`, `_gcl_aw` / `_gcl_gb`, `_rdt_cid`, `_rdt_uuid`,
|
|
39
|
+
* `_uetmsclkid`, `__oppref` and `__obref`.
|
|
33
40
|
*
|
|
34
41
|
* Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS
|
|
35
42
|
* runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for
|
|
@@ -41,14 +48,16 @@ export interface ClickIdMiddlewareOptions {
|
|
|
41
48
|
* ```ts
|
|
42
49
|
* // start.ts
|
|
43
50
|
* import { createStart } from '@tanstack/react-start'
|
|
44
|
-
* import {
|
|
51
|
+
* import { createClickIdMiddleware } from '@shware/analytics/tanstack'
|
|
52
|
+
* const clickIdMiddleware = createClickIdMiddleware({
|
|
53
|
+
* domain: import.meta.env.DEV ? null : '.example.com',
|
|
54
|
+
* secure: !import.meta.env.DEV,
|
|
55
|
+
* })
|
|
45
56
|
* export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))
|
|
46
57
|
* ```
|
|
47
58
|
*
|
|
48
59
|
* reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
|
|
49
60
|
*/
|
|
50
|
-
export declare function createClickIdMiddleware(options
|
|
51
|
-
/** Ready-to-register middleware with default options. */
|
|
52
|
-
export declare const clickIdMiddleware: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
61
|
+
export declare function createClickIdMiddleware(options: ClickIdMiddlewareOptions): import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
53
62
|
//#endregion
|
|
54
63
|
//# sourceMappingURL=middleware.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/tanstack/middleware.ts"],"mappings":";iBAGiB;;
|
|
1
|
+
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/tanstack/middleware.ts"],"mappings":";iBAGiB;;;;;;EAMf;;EAEA;;EAEA;;;;;;;;;EASA;;;;;;;;EAQA;;;;;EAKA,iBAAiB,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgDZ,wBAAwB,SAAS,2DAAwB"}
|
|
@@ -2,9 +2,21 @@ import { resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.mjs
|
|
|
2
2
|
import { createMiddleware } from "@tanstack/react-start";
|
|
3
3
|
//#region src/tanstack/middleware.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
5
|
+
* Whether a response could be stored by a shared cache (a CDN) under its `Cache-Control`. One
|
|
6
|
+
* that is `private` or `no-store` already cannot, so a per-user `Set-Cookie` on it is safe as it
|
|
7
|
+
* is, and its own caching is kept — the gtag.js loader served through a first-party Google Tag
|
|
8
|
+
* Gateway (`private, max-age=…`) stays in the browser cache, where a browser never replays a
|
|
9
|
+
* `Set-Cookie`. Anything else — `public`, `s-maxage`, no header at all — might be shared, and is
|
|
10
|
+
* made uncacheable.
|
|
11
|
+
*/
|
|
12
|
+
function sharedCacheable(value) {
|
|
13
|
+
if (!value) return true;
|
|
14
|
+
return !value.toLowerCase().split(",").map((directive) => directive.trim()).some((directive) => directive === "private" || directive.startsWith("private=") || directive === "no-store");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* TanStack Start request middleware that persists the ad platforms' first-party cookies on the
|
|
18
|
+
* document response: `_fbc`, `_fbp`, `_gcl_aw` / `_gcl_gb`, `_rdt_cid`, `_rdt_uuid`,
|
|
19
|
+
* `_uetmsclkid`, `__oppref` and `__obref`.
|
|
8
20
|
*
|
|
9
21
|
* Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS
|
|
10
22
|
* runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for
|
|
@@ -16,13 +28,17 @@ import { createMiddleware } from "@tanstack/react-start";
|
|
|
16
28
|
* ```ts
|
|
17
29
|
* // start.ts
|
|
18
30
|
* import { createStart } from '@tanstack/react-start'
|
|
19
|
-
* import {
|
|
31
|
+
* import { createClickIdMiddleware } from '@shware/analytics/tanstack'
|
|
32
|
+
* const clickIdMiddleware = createClickIdMiddleware({
|
|
33
|
+
* domain: import.meta.env.DEV ? null : '.example.com',
|
|
34
|
+
* secure: !import.meta.env.DEV,
|
|
35
|
+
* })
|
|
20
36
|
* export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))
|
|
21
37
|
* ```
|
|
22
38
|
*
|
|
23
39
|
* reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
|
|
24
40
|
*/
|
|
25
|
-
function createClickIdMiddleware(options
|
|
41
|
+
function createClickIdMiddleware(options) {
|
|
26
42
|
const { cacheControl = "private, no-store" } = options;
|
|
27
43
|
return createMiddleware({ type: "request" }).server(async ({ request, next, handlerType }) => {
|
|
28
44
|
const result = await next();
|
|
@@ -38,14 +54,12 @@ function createClickIdMiddleware(options = {}) {
|
|
|
38
54
|
});
|
|
39
55
|
if (cookies.length > 0) {
|
|
40
56
|
for (const header of toSetCookieHeaders(cookies)) result.response.headers.append("set-cookie", header);
|
|
41
|
-
if (cacheControl !== false) result.response.headers.set("cache-control", cacheControl);
|
|
57
|
+
if (cacheControl !== false && sharedCacheable(result.response.headers.get("cache-control"))) result.response.headers.set("cache-control", cacheControl);
|
|
42
58
|
}
|
|
43
59
|
return result;
|
|
44
60
|
});
|
|
45
61
|
}
|
|
46
|
-
/** Ready-to-register middleware with default options. */
|
|
47
|
-
const clickIdMiddleware = createClickIdMiddleware();
|
|
48
62
|
//#endregion
|
|
49
|
-
export {
|
|
63
|
+
export { createClickIdMiddleware };
|
|
50
64
|
|
|
51
65
|
//# sourceMappingURL=middleware.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n
|
|
1
|
+
{"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /**\n * `Domain` attribute for the cookies: the site's registrable domain, e.g. `.shware.io`, where the\n * pixels write theirs. Required — set it in production; `null` (host-only) is for `localhost`.\n * See `ResolveClickIdCookiesInput.domain`.\n */\n domain: string | null;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (com=0, example.com=1, www.example.com=2). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue the stored click and browser ids on every request as an ITP self-heal: the pixels\n * rewrite them through `document.cookie`, which Safari caps at 7 days (24 hours on an\n * ad-decorated landing), and the re-issue restores the long-lived HTTP cookie. On by default.\n * Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page response of a\n * visitor carrying one; set false to keep those pages CDN-cacheable. See\n * {@link resolveClickIdCookies}.\n */\n refresh?: boolean;\n /**\n * The `Cache-Control` for a response we attach cookies to when it could otherwise be stored by a\n * shared cache (default `private, no-store`): a per-user `Set-Cookie` must never end up on a\n * shared-cache entry, or one visitor's `_fbc` would be served to everyone. A response already\n * `private` or `no-store` keeps its own. Only set this false if you guarantee these responses\n * are never cached.\n */\n cacheControl?: string | false;\n /**\n * Consent gate. Return false to skip setting cookies for this request (e.g. before the visitor has\n * granted consent where required). Runs per request with the incoming `Request`.\n */\n shouldPersist?: (request: Request) => boolean;\n}\n\n/**\n * Whether a response could be stored by a shared cache (a CDN) under its `Cache-Control`. One\n * that is `private` or `no-store` already cannot, so a per-user `Set-Cookie` on it is safe as it\n * is, and its own caching is kept — the gtag.js loader served through a first-party Google Tag\n * Gateway (`private, max-age=…`) stays in the browser cache, where a browser never replays a\n * `Set-Cookie`. Anything else — `public`, `s-maxage`, no header at all — might be shared, and is\n * made uncacheable.\n */\nfunction sharedCacheable(value: string | null): boolean {\n if (!value) return true;\n const directives = value\n .toLowerCase()\n .split(',')\n .map((directive) => directive.trim());\n return !directives.some(\n (directive) =>\n directive === 'private' || directive.startsWith('private=') || directive === 'no-store'\n );\n}\n\n/**\n * TanStack Start request middleware that persists the ad platforms' first-party cookies on the\n * document response: `_fbc`, `_fbp`, `_gcl_aw` / `_gcl_gb`, `_rdt_cid`, `_rdt_uuid`,\n * `_uetmsclkid`, `__oppref` and `__obref`.\n *\n * Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS\n * runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for\n * 90 days in Safari: ITP caps JavaScript-set cookies on a fbclid-decorated landing page to 24\n * hours, and a document response is never classified as CNAME/IP cloaking (it is the reference the\n * browser measures cloaking against).\n *\n * Register it as a global request middleware:\n * ```ts\n * // start.ts\n * import { createStart } from '@tanstack/react-start'\n * import { createClickIdMiddleware } from '@shware/analytics/tanstack'\n * const clickIdMiddleware = createClickIdMiddleware({\n * domain: import.meta.env.DEV ? null : '.example.com',\n * secure: !import.meta.env.DEV,\n * })\n * export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))\n * ```\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\nexport function createClickIdMiddleware(options: ClickIdMiddlewareOptions) {\n const { cacheControl = 'private, no-store' } = options;\n\n return createMiddleware({ type: 'request' }).server(async ({ request, next, handlerType }) => {\n const result = await next();\n\n // Skip serverFn RPC responses. 'router' covers SSR document requests *and* custom server\n // routes (API endpoints) — those also get cookies when the URL carries a click id.\n if (handlerType !== 'router') return result;\n if (options.shouldPersist && !options.shouldPersist(request)) return result;\n\n const { cookies } = resolveClickIdCookies({\n url: request.url,\n cookieHeader: request.headers.get('cookie'),\n domain: options.domain,\n secure: options.secure,\n subdomainIndex: options.subdomainIndex,\n refresh: options.refresh ?? true,\n });\n\n if (cookies.length > 0) {\n for (const header of toSetCookieHeaders(cookies)) {\n result.response.headers.append('set-cookie', header);\n }\n if (cacheControl !== false && sharedCacheable(result.response.headers.get('cache-control'))) {\n result.response.headers.set('cache-control', cacheControl);\n }\n }\n\n return result;\n });\n}\n"],"mappings":";;;;;;;;;;;AA8CA,SAAS,gBAAgB,OAA+B;CACtD,IAAI,CAAC,OAAO,OAAO;CAKnB,OAAO,CAJY,MAChB,YAAY,CAAC,CACb,MAAM,GAAG,CAAC,CACV,KAAK,cAAc,UAAU,KAAK,CACpB,CAAC,CAAC,MAChB,cACC,cAAc,aAAa,UAAU,WAAW,UAAU,KAAK,cAAc,UACjF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,wBAAwB,SAAmC;CACzE,MAAM,EAAE,eAAe,wBAAwB;CAE/C,OAAO,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,SAAS,MAAM,kBAAkB;EAC5F,MAAM,SAAS,MAAM,KAAK;EAI1B,IAAI,gBAAgB,UAAU,OAAO;EACrC,IAAI,QAAQ,iBAAiB,CAAC,QAAQ,cAAc,OAAO,GAAG,OAAO;EAErE,MAAM,EAAE,YAAY,sBAAsB;GACxC,KAAK,QAAQ;GACb,cAAc,QAAQ,QAAQ,IAAI,QAAQ;GAC1C,QAAQ,QAAQ;GAChB,QAAQ,QAAQ;GAChB,gBAAgB,QAAQ;GACxB,SAAS,QAAQ,WAAW;EAC9B,CAAC;EAED,IAAI,QAAQ,SAAS,GAAG;GACtB,KAAK,MAAM,UAAU,mBAAmB,OAAO,GAC7C,OAAO,SAAS,QAAQ,OAAO,cAAc,MAAM;GAErD,IAAI,iBAAiB,SAAS,gBAAgB,OAAO,SAAS,QAAQ,IAAI,eAAe,CAAC,GACxF,OAAO,SAAS,QAAQ,IAAI,iBAAiB,YAAY;EAE7D;EAEA,OAAO;CACT,CAAC;AACH"}
|