@reopt-ai/data-contract 0.6.3 → 0.7.0
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/dist/client.cjs +228 -321
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +57 -71
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.ts +57 -71
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +228 -322
- package/dist/client.js.map +1 -1
- package/dist/control.cjs +158 -54
- package/dist/control.cjs.map +1 -1
- package/dist/control.d.cts +121 -137
- package/dist/control.d.cts.map +1 -0
- package/dist/control.d.ts +121 -137
- package/dist/control.d.ts.map +1 -0
- package/dist/control.js +134 -54
- package/dist/control.js.map +1 -1
- package/dist/events-BUIeU60t.d.cts +142 -0
- package/dist/events-BUIeU60t.d.cts.map +1 -0
- package/dist/events-BUIeU60t.d.ts +142 -0
- package/dist/events-BUIeU60t.d.ts.map +1 -0
- package/dist/events.cjs +110 -15
- package/dist/events.cjs.map +1 -1
- package/dist/events.d.cts +2 -59
- package/dist/events.d.ts +2 -59
- package/dist/events.js +101 -15
- package/dist/events.js.map +1 -1
- package/dist/identity.cjs +150 -55
- package/dist/identity.cjs.map +1 -1
- package/dist/identity.d.cts +21 -20
- package/dist/identity.d.cts.map +1 -0
- package/dist/identity.d.ts +21 -20
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +126 -56
- package/dist/identity.js.map +1 -1
- package/dist/index.cjs +46 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -54
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +37 -54
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -25
- package/dist/index.js.map +1 -1
- package/dist/ingest.cjs +152 -57
- package/dist/ingest.cjs.map +1 -1
- package/dist/ingest.d.cts +182 -193
- package/dist/ingest.d.cts.map +1 -0
- package/dist/ingest.d.ts +182 -193
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +126 -57
- package/dist/ingest.js.map +1 -1
- package/dist/query.cjs +284 -84
- package/dist/query.cjs.map +1 -1
- package/dist/query.d.cts +456 -469
- package/dist/query.d.cts.map +1 -0
- package/dist/query.d.ts +456 -469
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +245 -84
- package/dist/query.js.map +1 -1
- package/dist/report.cjs +104 -74
- package/dist/report.cjs.map +1 -1
- package/dist/report.d.cts +182 -136
- package/dist/report.d.cts.map +1 -0
- package/dist/report.d.ts +182 -136
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +93 -74
- package/dist/report.js.map +1 -1
- package/dist/version-B_0qb1_K.cjs +53 -0
- package/dist/version-B_0qb1_K.cjs.map +1 -0
- package/dist/version-C09Cyf4R.js +12 -0
- package/dist/version-C09Cyf4R.js.map +1 -0
- package/dist/{version-DJ2Grb_N.d.ts → version-CGujUGWd.d.cts} +5 -3
- package/dist/version-CGujUGWd.d.cts.map +1 -0
- package/dist/{version-DJ2Grb_N.d.cts → version-CGujUGWd.d.ts} +5 -3
- package/dist/version-CGujUGWd.d.ts.map +1 -0
- package/package.json +69 -28
- package/dist/chunk-3EEANNYN.cjs +0 -62
- package/dist/chunk-3EEANNYN.cjs.map +0 -1
- package/dist/chunk-7W33YGRO.js +0 -160
- package/dist/chunk-7W33YGRO.js.map +0 -1
- package/dist/chunk-F2RNXJGO.cjs +0 -19
- package/dist/chunk-F2RNXJGO.cjs.map +0 -1
- package/dist/chunk-FP2X3PRK.js +0 -62
- package/dist/chunk-FP2X3PRK.js.map +0 -1
- package/dist/chunk-JCEWUSUM.js +0 -53
- package/dist/chunk-JCEWUSUM.js.map +0 -1
- package/dist/chunk-L3JPDRVG.cjs +0 -160
- package/dist/chunk-L3JPDRVG.cjs.map +0 -1
- package/dist/chunk-L4KBQAOO.js +0 -204
- package/dist/chunk-L4KBQAOO.js.map +0 -1
- package/dist/chunk-NJ4JKNBX.js +0 -361
- package/dist/chunk-NJ4JKNBX.js.map +0 -1
- package/dist/chunk-ORNTN6AK.cjs +0 -53
- package/dist/chunk-ORNTN6AK.cjs.map +0 -1
- package/dist/chunk-PSUINLW7.js +0 -19
- package/dist/chunk-PSUINLW7.js.map +0 -1
- package/dist/chunk-RU26QFM2.js +0 -179
- package/dist/chunk-RU26QFM2.js.map +0 -1
- package/dist/chunk-XDH5QQYF.cjs +0 -361
- package/dist/chunk-XDH5QQYF.cjs.map +0 -1
- package/dist/chunk-YCTGHWTP.cjs +0 -204
- package/dist/chunk-YCTGHWTP.cjs.map +0 -1
- package/dist/chunk-ZI6A3WQP.cjs +0 -179
- package/dist/chunk-ZI6A3WQP.cjs.map +0 -1
package/dist/events.js
CHANGED
|
@@ -1,17 +1,103 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
RESERVED_EVENT_NAMES,
|
|
14
|
-
SCROLL_DEPTH_BUCKETS,
|
|
15
|
-
scrollDepthBucket
|
|
1
|
+
//#region src/events.ts
|
|
2
|
+
const RESERVED_EVENT_NAMES = [
|
|
3
|
+
"session_start",
|
|
4
|
+
"session_end",
|
|
5
|
+
"screen_view"
|
|
6
|
+
];
|
|
7
|
+
const AUTO_EVENT_NAMES = {
|
|
8
|
+
pageview: "$pageview",
|
|
9
|
+
screenView: "$screen_view",
|
|
10
|
+
pageleave: "$pageleave",
|
|
11
|
+
webVitals: "$web_vitals",
|
|
12
|
+
exception: "$exception"
|
|
16
13
|
};
|
|
14
|
+
const AUTO_EVENT_PROPERTIES = {
|
|
15
|
+
[AUTO_EVENT_NAMES.pageview]: [
|
|
16
|
+
"path",
|
|
17
|
+
"origin",
|
|
18
|
+
"title",
|
|
19
|
+
"referrer",
|
|
20
|
+
"search",
|
|
21
|
+
"utm_source",
|
|
22
|
+
"utm_medium",
|
|
23
|
+
"utm_campaign",
|
|
24
|
+
"utm_term",
|
|
25
|
+
"utm_content"
|
|
26
|
+
],
|
|
27
|
+
[AUTO_EVENT_NAMES.screenView]: ["screen_name"],
|
|
28
|
+
[AUTO_EVENT_NAMES.pageleave]: [
|
|
29
|
+
"path",
|
|
30
|
+
"origin",
|
|
31
|
+
"duration",
|
|
32
|
+
"scroll_depth",
|
|
33
|
+
"scroll_depth_bucket"
|
|
34
|
+
],
|
|
35
|
+
[AUTO_EVENT_NAMES.webVitals]: [
|
|
36
|
+
"metric_name",
|
|
37
|
+
"metric_id",
|
|
38
|
+
"value",
|
|
39
|
+
"delta",
|
|
40
|
+
"rating",
|
|
41
|
+
"navigation_type",
|
|
42
|
+
"path"
|
|
43
|
+
],
|
|
44
|
+
[AUTO_EVENT_NAMES.exception]: [
|
|
45
|
+
"$exception_type",
|
|
46
|
+
"$exception_message",
|
|
47
|
+
"$exception_stack",
|
|
48
|
+
"$exception_source",
|
|
49
|
+
"$exception_handled",
|
|
50
|
+
"$exception_list",
|
|
51
|
+
"$exception_level",
|
|
52
|
+
"$exception_fingerprint",
|
|
53
|
+
"$exception_steps",
|
|
54
|
+
"path"
|
|
55
|
+
]
|
|
56
|
+
};
|
|
57
|
+
const AUTO_EVENT_ROLLUP_KEYS = {
|
|
58
|
+
[AUTO_EVENT_NAMES.pageview]: [
|
|
59
|
+
"path",
|
|
60
|
+
"origin",
|
|
61
|
+
"referrer",
|
|
62
|
+
"utm_source",
|
|
63
|
+
"utm_medium",
|
|
64
|
+
"utm_campaign"
|
|
65
|
+
],
|
|
66
|
+
[AUTO_EVENT_NAMES.screenView]: ["screen_name"],
|
|
67
|
+
[AUTO_EVENT_NAMES.pageleave]: [
|
|
68
|
+
"path",
|
|
69
|
+
"origin",
|
|
70
|
+
"scroll_depth_bucket"
|
|
71
|
+
],
|
|
72
|
+
[AUTO_EVENT_NAMES.webVitals]: [
|
|
73
|
+
"metric_name",
|
|
74
|
+
"rating",
|
|
75
|
+
"navigation_type",
|
|
76
|
+
"path"
|
|
77
|
+
],
|
|
78
|
+
[AUTO_EVENT_NAMES.exception]: [
|
|
79
|
+
"$exception_type",
|
|
80
|
+
"$exception_source",
|
|
81
|
+
"$exception_handled",
|
|
82
|
+
"path",
|
|
83
|
+
"$exception_fingerprint"
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
const RELEASE_PROPERTY = "$release_id";
|
|
87
|
+
const MAX_EXCEPTION_CHAIN = 4;
|
|
88
|
+
const MAX_EXCEPTION_FRAMES = 50;
|
|
89
|
+
const SCROLL_DEPTH_BUCKETS = [
|
|
90
|
+
25,
|
|
91
|
+
50,
|
|
92
|
+
75,
|
|
93
|
+
100
|
|
94
|
+
];
|
|
95
|
+
function scrollDepthBucket(depth) {
|
|
96
|
+
let reached = "0";
|
|
97
|
+
for (const threshold of SCROLL_DEPTH_BUCKETS) if (depth >= threshold) reached = `${threshold}`;
|
|
98
|
+
return reached;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { AUTO_EVENT_NAMES, AUTO_EVENT_PROPERTIES, AUTO_EVENT_ROLLUP_KEYS, MAX_EXCEPTION_CHAIN, MAX_EXCEPTION_FRAMES, RELEASE_PROPERTY, RESERVED_EVENT_NAMES, SCROLL_DEPTH_BUCKETS, scrollDepthBucket };
|
|
102
|
+
|
|
17
103
|
//# sourceMappingURL=events.js.map
|
package/dist/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../src/events.ts"],"sourcesContent":["/**\n * `@reopt-ai/data-contract/events` — event names and property keys, with no\n * zod. Browser SDKs import this so their production bundles stay free of\n * schema code; `./ingest` re-exports everything here.\n */\n\n/** Event names the pipeline reserves for its own session bookkeeping. */\nexport const RESERVED_EVENT_NAMES = [\"session_start\", \"session_end\", \"screen_view\"] as const;\n\n/**\n * Events the SDKs emit on their own. One declaration, so a consumer that\n * registers them in its event catalogue (to make them roll up) cannot drift\n * from what the SDK actually sends — a drifted name silently falls back to\n * raw queries, which is slower but not visibly wrong.\n */\nexport const AUTO_EVENT_NAMES = {\n pageview: \"$pageview\",\n screenView: \"$screen_view\",\n pageleave: \"$pageleave\",\n webVitals: \"$web_vitals\",\n exception: \"$exception\",\n} as const;\nexport type AutoEventName = (typeof AUTO_EVENT_NAMES)[keyof typeof AUTO_EVENT_NAMES];\n\n/**\n * Property keys each automatic event carries. Consumers derive their\n * breakdown / rollup property lists from here rather than retyping them.\n * `path`, `origin`, `referrer` and `utm_*` are the keys the ingest pipeline\n * reads into session and event columns.\n */\nexport const AUTO_EVENT_PROPERTIES = {\n [AUTO_EVENT_NAMES.pageview]: [\n \"path\",\n \"origin\",\n \"title\",\n \"referrer\",\n \"search\",\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_term\",\n \"utm_content\",\n ],\n [AUTO_EVENT_NAMES.screenView]: [\"screen_name\"],\n [AUTO_EVENT_NAMES.pageleave]: [\"path\", \"origin\", \"duration\", \"scroll_depth\", \"scroll_depth_bucket\"],\n [AUTO_EVENT_NAMES.webVitals]: [\"metric_name\", \"metric_id\", \"value\", \"delta\", \"rating\", \"navigation_type\", \"path\"],\n // `$`-prefixed on purpose: these are reserved, SDK-owned keys with fixed\n // semantics, distinguished from a host's own snake_case vocabulary the\n // same way the event names are. Not a typo — do not strip the prefix.\n [AUTO_EVENT_NAMES.exception]: [\n \"$exception_type\",\n \"$exception_message\",\n \"$exception_stack\",\n \"$exception_source\",\n \"$exception_handled\",\n // Structured form. The flat keys above stay for every consumer written\n // against them, and because V1 fingerprinting and the rollup keys read them.\n \"$exception_list\",\n \"$exception_level\",\n \"$exception_fingerprint\",\n \"$exception_steps\",\n \"path\",\n ],\n} as const satisfies Record<AutoEventName, readonly string[]>;\n\n/**\n * The subset of {@link AUTO_EVENT_PROPERTIES} that is a sensible rollup\n * dimension: low cardinality, categorical. Per-measurement ids and\n * continuous values (`metric_id`, `value`, `delta`, `duration`,\n * `scroll_depth`) are deliberately absent — as dimensions they would make\n * the rollup larger than the raw table it summarises. Derive\n * `rollupProperties` from here, not from the full property list.\n */\nexport const AUTO_EVENT_ROLLUP_KEYS = {\n [AUTO_EVENT_NAMES.pageview]: [\"path\", \"origin\", \"referrer\", \"utm_source\", \"utm_medium\", \"utm_campaign\"],\n [AUTO_EVENT_NAMES.screenView]: [\"screen_name\"],\n [AUTO_EVENT_NAMES.pageleave]: [\"path\", \"origin\", \"scroll_depth_bucket\"],\n [AUTO_EVENT_NAMES.webVitals]: [\"metric_name\", \"rating\", \"navigation_type\", \"path\"],\n [AUTO_EVENT_NAMES.exception]: [\n \"$exception_type\",\n \"$exception_source\",\n \"$exception_handled\",\n \"path\",\n // The one high-ish cardinality key in this table, and deliberately so: a\n // fingerprint is one per *issue*, not one per occurrence, so its cardinality\n // is the size of the issue list — hundreds, where `path` on a busy site is\n // thousands. Rolling it up is what lets the issue list read a whole month\n // without scanning every exception event. The per-day `__other__` fold still\n // applies, and the read refuses the rollup when it has folded (see\n // `errorFingerprintVolumes`), because a folded fingerprint would report zero\n // rather than \"unknown\".\n \"$exception_fingerprint\",\n ],\n} as const satisfies Record<AutoEventName, readonly string[]>;\n\n// ─── Exceptions ─────────────────────────────────────────────────────────────\n\n/**\n * How deep a `cause` chain is carried, and how many frames of each exception.\n *\n * Both caps are part of the contract rather than each SDK's own opinion: the\n * server fingerprints exactly what it receives, so a client that sent more\n * frames than the server hashes would group differently from one that sent\n * fewer, for the same error.\n */\n/**\n * The property carrying the host's build identity.\n *\n * Not in {@link AUTO_EVENT_PROPERTIES}: it is not specific to any automatic\n * event — every event the SDK sends carries it once `release` is configured,\n * the same way a global property does. Error tracking is what reads it, to say\n * which build an issue first and last appeared in and whether a fix held.\n */\nexport const RELEASE_PROPERTY = \"$release_id\";\n\nexport const MAX_EXCEPTION_CHAIN = 4;\nexport const MAX_EXCEPTION_FRAMES = 50;\n\n/**\n * One stack frame, in the shape OTel and Sentry both use.\n *\n * snake_case because it is a wire format, not a JS object — renaming these on\n * the way in or out is how two SDKs end up disagreeing about the same frame.\n */\nexport interface ExceptionFrame {\n /** The file the frame ran in. Absent for a frame no parser could place. */\n filename?: string;\n /** Empty or absent for an anonymous frame. */\n function?: string;\n /** Dotted module path, where the runtime has one. */\n module?: string;\n lineno?: number;\n colno?: number;\n /**\n * The frame belongs to the app rather than a dependency, the runtime, or a\n * browser extension. Drives the stack panel's default filter, and V1\n * fingerprinting.\n */\n in_app?: boolean;\n /**\n * A source map resolved `function` to its original name. Fingerprinting then\n * drops line and column, because a resolved name survives a rebuild that\n * shifts them.\n */\n resolved?: boolean;\n /** Source context, when a symbolication step has it (WS-E). */\n context_line?: string;\n pre_context?: string[];\n post_context?: string[];\n /** Build-injected chunk id, so a source map can be matched without the URL. */\n chunk_id?: string;\n}\n\n/** Where an exception came from, and whether anyone caught it. */\nexport interface ExceptionMechanism {\n /** False means nothing caught it — the error reached the runtime's handler. */\n handled: boolean;\n type: \"onerror\" | \"unhandledrejection\" | \"manual\" | \"console\" | \"nextjs_onRequestError\";\n /** The Error was constructed by the SDK to carry a non-Error throw. */\n synthetic?: boolean;\n}\n\n/** One exception in a cause chain. */\nexport interface ExceptionEntry {\n type: string;\n value: string;\n mechanism: ExceptionMechanism;\n stacktrace?: {\n frames: ExceptionFrame[];\n /** `raw` means the frames are as the engine reported them — not symbolicated. */\n type: \"raw\";\n };\n}\n\n/** A breadcrumb leading up to an exception (A5). */\nexport interface ExceptionStep {\n /** Epoch milliseconds. */\n timestamp: number;\n category: \"navigation\" | \"click\" | \"fetch\" | \"console\" | \"custom\";\n message: string;\n data?: Record<string, unknown>;\n}\n\nexport type ExceptionLevel = \"fatal\" | \"error\" | \"warning\" | \"info\";\n\n/**\n * `$pageleave.scroll_depth` is the exact 0–100 percentage; `scroll_depth_bucket`\n * is the deepest of these thresholds reached, as a string — the value to\n * break down by. `\"0\"` means the visitor never reached the first threshold.\n */\nexport const SCROLL_DEPTH_BUCKETS = [25, 50, 75, 100] as const;\nexport type ScrollDepthBucket = \"0\" | `${(typeof SCROLL_DEPTH_BUCKETS)[number]}`;\n\nexport function scrollDepthBucket(depth: number): ScrollDepthBucket {\n let reached: ScrollDepthBucket = \"0\";\n for (const threshold of SCROLL_DEPTH_BUCKETS) {\n if (depth >= threshold) reached = `${threshold}`;\n }\n return reached;\n}\n"],"mappings":";AAOA,MAAa,uBAAuB;CAAC;CAAiB;CAAe;AAAa;AAQlF,MAAa,mBAAmB;CAC9B,UAAU;CACV,YAAY;CACZ,WAAW;CACX,WAAW;CACX,WAAW;AACb;AASA,MAAa,wBAAwB;EAClC,iBAAiB,WAAW;EAC3B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;EACC,iBAAiB,aAAa,CAAC,aAAa;EAC5C,iBAAiB,YAAY;EAAC;EAAQ;EAAU;EAAY;EAAgB;CAAqB;EACjG,iBAAiB,YAAY;EAAC;EAAe;EAAa;EAAS;EAAS;EAAU;EAAmB;CAAM;EAI/G,iBAAiB,YAAY;EAC5B;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;CACF;AACF;AAUA,MAAa,yBAAyB;EACnC,iBAAiB,WAAW;EAAC;EAAQ;EAAU;EAAY;EAAc;EAAc;CAAc;EACrG,iBAAiB,aAAa,CAAC,aAAa;EAC5C,iBAAiB,YAAY;EAAC;EAAQ;EAAU;CAAqB;EACrE,iBAAiB,YAAY;EAAC;EAAe;EAAU;EAAmB;CAAM;EAChF,iBAAiB,YAAY;EAC5B;EACA;EACA;EACA;EASA;CACF;AACF;AAoBA,MAAa,mBAAmB;AAEhC,MAAa,sBAAsB;AACnC,MAAa,uBAAuB;AA0EpC,MAAa,uBAAuB;CAAC;CAAI;CAAI;CAAI;AAAG;AAGpD,SAAgB,kBAAkB,OAAkC;CAClE,IAAI,UAA6B;CACjC,KAAK,MAAM,aAAa,sBACtB,IAAI,SAAS,WAAW,UAAU,GAAG;CAEvC,OAAO;AACT"}
|
package/dist/identity.cjs
CHANGED
|
@@ -1,57 +1,152 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_version = require("./version-B_0qb1_K.cjs");
|
|
3
|
+
//#region src/identity.ts
|
|
4
|
+
const SESSION_ID_HEADER = "reopt-session-id";
|
|
5
|
+
const IDENTITY_REQUEST_HEADERS = [require_version.DEVICE_ID_HEADER, SESSION_ID_HEADER];
|
|
6
|
+
const COOKIE_MAX_AGE_SECONDS = 3456e4;
|
|
7
|
+
const OPT_OUT_CONSENT_CATEGORY = "analytics";
|
|
8
|
+
const COOKIE_PREFIX = "reopt_";
|
|
9
|
+
function cookieSafe(writeKey) {
|
|
10
|
+
if (!writeKey) throw new Error("[reopt] writeKey is required to derive cookie names");
|
|
11
|
+
return writeKey.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
12
|
+
}
|
|
13
|
+
function deviceCookieName(writeKey) {
|
|
14
|
+
return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_device`;
|
|
15
|
+
}
|
|
16
|
+
function consentCookieName(writeKey) {
|
|
17
|
+
return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_consent`;
|
|
18
|
+
}
|
|
19
|
+
const IDENTITY_ID_PATTERN = /^[A-Za-z0-9._~-]{1,200}$/;
|
|
20
|
+
function isValidIdentityId(value) {
|
|
21
|
+
return typeof value === "string" && IDENTITY_ID_PATTERN.test(value);
|
|
22
|
+
}
|
|
23
|
+
function decode(raw) {
|
|
24
|
+
let text = raw;
|
|
25
|
+
for (let round = 0; round < 3 && /%[0-9A-Fa-f]{2}/.test(text); round++) {
|
|
26
|
+
try {
|
|
27
|
+
const next = decodeURIComponent(text);
|
|
28
|
+
if (next === text) break;
|
|
29
|
+
text = next;
|
|
30
|
+
} catch {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (text.startsWith("{")) break;
|
|
34
|
+
}
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
function parseDeviceCookie(raw) {
|
|
38
|
+
if (!raw) return null;
|
|
39
|
+
const text = decode(raw.trim());
|
|
40
|
+
if (!text) return null;
|
|
41
|
+
if (!text.startsWith("{")) return isValidIdentityId(text) ? { deviceId: text } : null;
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(text);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
49
|
+
const candidate = parsed;
|
|
50
|
+
if (!isValidIdentityId(candidate.deviceId)) return null;
|
|
51
|
+
const state = { deviceId: candidate.deviceId };
|
|
52
|
+
if (isValidIdentityId(candidate.sessionId)) state.sessionId = candidate.sessionId;
|
|
53
|
+
if (isValidIdentityId(candidate.profileId)) state.profileId = candidate.profileId;
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
function serializeDeviceCookie(state) {
|
|
57
|
+
if (!isValidIdentityId(state.deviceId)) throw new Error("[reopt] deviceId must be a non-empty string without control characters");
|
|
58
|
+
const envelope = { deviceId: state.deviceId };
|
|
59
|
+
if (isValidIdentityId(state.sessionId)) envelope.sessionId = state.sessionId;
|
|
60
|
+
if (isValidIdentityId(state.profileId)) envelope.profileId = state.profileId;
|
|
61
|
+
return encodeURIComponent(JSON.stringify(envelope));
|
|
62
|
+
}
|
|
63
|
+
function parseConsentCookie(raw) {
|
|
64
|
+
if (!raw) return null;
|
|
65
|
+
const text = decode(raw.trim());
|
|
66
|
+
if (!text.startsWith("{")) return null;
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(text);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
74
|
+
const state = {};
|
|
75
|
+
for (const [category, decision] of Object.entries(parsed)) if (typeof decision === "boolean" && isValidIdentityId(category)) state[category] = decision;
|
|
76
|
+
return state;
|
|
77
|
+
}
|
|
78
|
+
function serializeConsentCookie(state) {
|
|
79
|
+
const clean = {};
|
|
80
|
+
for (const [category, decision] of Object.entries(state)) if (typeof decision === "boolean" && isValidIdentityId(category)) clean[category] = decision;
|
|
81
|
+
return encodeURIComponent(JSON.stringify(clean));
|
|
82
|
+
}
|
|
83
|
+
function isOptedOut(consent) {
|
|
84
|
+
return consent?.[OPT_OUT_CONSENT_CATEGORY] === false;
|
|
85
|
+
}
|
|
86
|
+
const SESSION_HEADER_SEPARATOR = ".";
|
|
87
|
+
function formatSessionHeader(credential) {
|
|
88
|
+
return `${credential.id}.${credential.token}`;
|
|
89
|
+
}
|
|
90
|
+
function parseSessionHeader(value) {
|
|
91
|
+
if (!value) return null;
|
|
92
|
+
const at = value.lastIndexOf(".");
|
|
93
|
+
if (at <= 0 || at === value.length - 1) return null;
|
|
94
|
+
const id = value.slice(0, at);
|
|
95
|
+
const token = value.slice(at + 1);
|
|
96
|
+
return isValidIdentityId(id) && /^[A-Za-z0-9_-]{43}$/.test(token) ? {
|
|
97
|
+
id,
|
|
98
|
+
token
|
|
99
|
+
} : null;
|
|
100
|
+
}
|
|
101
|
+
const encoder = new TextEncoder();
|
|
102
|
+
function base64url(bytes) {
|
|
103
|
+
let binary = "";
|
|
104
|
+
for (const byte of new Uint8Array(bytes)) binary += String.fromCharCode(byte);
|
|
105
|
+
return (typeof btoa === "function" ? btoa(binary) : Buffer.from(binary, "binary").toString("base64")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
106
|
+
}
|
|
107
|
+
async function hmacKey(secret) {
|
|
108
|
+
return crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
109
|
+
name: "HMAC",
|
|
110
|
+
hash: "SHA-256"
|
|
111
|
+
}, false, ["sign"]);
|
|
112
|
+
}
|
|
113
|
+
async function signSessionToken(secret, sessionId) {
|
|
114
|
+
if (!secret) throw new Error("[reopt] a session signing secret is required");
|
|
115
|
+
return base64url(await crypto.subtle.sign("HMAC", await hmacKey(secret), encoder.encode(sessionId)));
|
|
116
|
+
}
|
|
117
|
+
async function verifySessionToken(secret, credential) {
|
|
118
|
+
if (!secret || !credential) return false;
|
|
119
|
+
const expected = await signSessionToken(secret, credential.id);
|
|
120
|
+
if (expected.length !== credential.token.length) return false;
|
|
121
|
+
let diff = 0;
|
|
122
|
+
for (let index = 0; index < expected.length; index++) diff |= expected.charCodeAt(index) ^ credential.token.charCodeAt(index);
|
|
123
|
+
return diff === 0;
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
exports.CLIENT_ID_HEADER = require_version.CLIENT_ID_HEADER;
|
|
127
|
+
exports.CLIENT_SECRET_HEADER = require_version.CLIENT_SECRET_HEADER;
|
|
128
|
+
exports.CONTRACT_VERSION = require_version.CONTRACT_VERSION;
|
|
129
|
+
exports.CONTRACT_VERSION_HEADER = require_version.CONTRACT_VERSION_HEADER;
|
|
130
|
+
exports.COOKIE_MAX_AGE_SECONDS = COOKIE_MAX_AGE_SECONDS;
|
|
131
|
+
exports.DEVICE_ID_HEADER = require_version.DEVICE_ID_HEADER;
|
|
132
|
+
exports.IDENTITY_ID_PATTERN = IDENTITY_ID_PATTERN;
|
|
133
|
+
exports.IDENTITY_REQUEST_HEADERS = IDENTITY_REQUEST_HEADERS;
|
|
134
|
+
exports.OPT_OUT_CONSENT_CATEGORY = OPT_OUT_CONSENT_CATEGORY;
|
|
135
|
+
exports.REQUEST_ID_HEADER = require_version.REQUEST_ID_HEADER;
|
|
136
|
+
exports.SESSION_HEADER_SEPARATOR = SESSION_HEADER_SEPARATOR;
|
|
137
|
+
exports.SESSION_ID_HEADER = SESSION_ID_HEADER;
|
|
138
|
+
exports.WRITE_KEY_HEADER = require_version.WRITE_KEY_HEADER;
|
|
139
|
+
exports.consentCookieName = consentCookieName;
|
|
140
|
+
exports.deviceCookieName = deviceCookieName;
|
|
141
|
+
exports.formatSessionHeader = formatSessionHeader;
|
|
142
|
+
exports.isOptedOut = isOptedOut;
|
|
143
|
+
exports.isValidIdentityId = isValidIdentityId;
|
|
144
|
+
exports.parseConsentCookie = parseConsentCookie;
|
|
145
|
+
exports.parseDeviceCookie = parseDeviceCookie;
|
|
146
|
+
exports.parseSessionHeader = parseSessionHeader;
|
|
147
|
+
exports.serializeConsentCookie = serializeConsentCookie;
|
|
148
|
+
exports.serializeDeviceCookie = serializeDeviceCookie;
|
|
149
|
+
exports.signSessionToken = signSessionToken;
|
|
150
|
+
exports.verifySessionToken = verifySessionToken;
|
|
2
151
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
var _chunkL3JPDRVGcjs = require('./chunk-L3JPDRVG.cjs');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
var _chunkF2RNXJGOcjs = require('./chunk-F2RNXJGO.cjs');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
exports.CLIENT_ID_HEADER = _chunkF2RNXJGOcjs.CLIENT_ID_HEADER; exports.CLIENT_SECRET_HEADER = _chunkF2RNXJGOcjs.CLIENT_SECRET_HEADER; exports.CONTRACT_VERSION = _chunkF2RNXJGOcjs.CONTRACT_VERSION; exports.CONTRACT_VERSION_HEADER = _chunkF2RNXJGOcjs.CONTRACT_VERSION_HEADER; exports.COOKIE_MAX_AGE_SECONDS = _chunkL3JPDRVGcjs.COOKIE_MAX_AGE_SECONDS; exports.DEVICE_ID_HEADER = _chunkF2RNXJGOcjs.DEVICE_ID_HEADER; exports.IDENTITY_ID_PATTERN = _chunkL3JPDRVGcjs.IDENTITY_ID_PATTERN; exports.IDENTITY_REQUEST_HEADERS = _chunkL3JPDRVGcjs.IDENTITY_REQUEST_HEADERS; exports.OPT_OUT_CONSENT_CATEGORY = _chunkL3JPDRVGcjs.OPT_OUT_CONSENT_CATEGORY; exports.REQUEST_ID_HEADER = _chunkF2RNXJGOcjs.REQUEST_ID_HEADER; exports.SESSION_HEADER_SEPARATOR = _chunkL3JPDRVGcjs.SESSION_HEADER_SEPARATOR; exports.SESSION_ID_HEADER = _chunkL3JPDRVGcjs.SESSION_ID_HEADER; exports.WRITE_KEY_HEADER = _chunkF2RNXJGOcjs.WRITE_KEY_HEADER; exports.consentCookieName = _chunkL3JPDRVGcjs.consentCookieName; exports.deviceCookieName = _chunkL3JPDRVGcjs.deviceCookieName; exports.formatSessionHeader = _chunkL3JPDRVGcjs.formatSessionHeader; exports.isOptedOut = _chunkL3JPDRVGcjs.isOptedOut; exports.isValidIdentityId = _chunkL3JPDRVGcjs.isValidIdentityId; exports.parseConsentCookie = _chunkL3JPDRVGcjs.parseConsentCookie; exports.parseDeviceCookie = _chunkL3JPDRVGcjs.parseDeviceCookie; exports.parseSessionHeader = _chunkL3JPDRVGcjs.parseSessionHeader; exports.serializeConsentCookie = _chunkL3JPDRVGcjs.serializeConsentCookie; exports.serializeDeviceCookie = _chunkL3JPDRVGcjs.serializeDeviceCookie; exports.signSessionToken = _chunkL3JPDRVGcjs.signSessionToken; exports.verifySessionToken = _chunkL3JPDRVGcjs.verifySessionToken;
|
|
57
152
|
//# sourceMappingURL=identity.cjs.map
|
package/dist/identity.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/identity.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,oqDAAC","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/identity.cjs"}
|
|
1
|
+
{"version":3,"file":"identity.cjs","names":["DEVICE_ID_HEADER"],"sources":["../src/identity.ts"],"sourcesContent":["/**\n * `@reopt-ai/data-contract/identity` — the names and encodings that carry a\n * visitor's identity between the browser SDK, the server SDK, the Next.js\n * proxy and the ingest pipeline.\n *\n * Every one of those parties reads or writes the same cookies and headers.\n * None of them depends on another (ingest does not import the SDKs), so this\n * module is the only place a name may be spelled out. A party that hardcodes\n * `reopt_<key>_device` instead of calling `deviceCookieName()` fails silently:\n * the browser sets one cookie, the server looks for another, and every\n * visitor is counted as new. `packages/db/src/__tests__/redis-namespace.test.ts`\n * guards Redis key prefixes for exactly the same reason.\n *\n * Nothing here touches `document`, `fetch` or Node built-ins — it runs in\n * browsers, edge runtimes and Node alike.\n */\nimport { DEVICE_ID_HEADER } from \"./version.js\";\n\nexport {\n CLIENT_ID_HEADER,\n CLIENT_SECRET_HEADER,\n CONTRACT_VERSION,\n CONTRACT_VERSION_HEADER,\n DEVICE_ID_HEADER,\n REQUEST_ID_HEADER,\n WRITE_KEY_HEADER,\n} from \"./version.js\";\n\n/**\n * Request header carrying the browser's current session id. The browser SDK\n * adds it to same-origin requests so a server-side `track()` can land in the\n * session the user is actually in; the server SDK forwards it to ingest.\n *\n * Ingest treats it as a *hint*, never as authority — a browser can put any\n * value here.\n */\nexport const SESSION_ID_HEADER = \"reopt-session-id\";\n\n/** Every request header the identity protocol may carry, for CORS allow-lists. */\nexport const IDENTITY_REQUEST_HEADERS = [DEVICE_ID_HEADER, SESSION_ID_HEADER] as const;\n\n/**\n * 400 days — the longest lifetime Chrome will honour. Anything longer is\n * silently clamped to this, so asking for more only hides the real expiry.\n */\nexport const COOKIE_MAX_AGE_SECONDS = 400 * 24 * 60 * 60;\n\n/** Consent category whose refusal means \"send nothing at all\". */\nexport const OPT_OUT_CONSENT_CATEGORY = \"analytics\";\n\nconst COOKIE_PREFIX = \"reopt_\";\n\n/**\n * Cookie names may not contain separators (`=`, `;`, `,`, whitespace) or\n * control characters. A write key is opaque to us, so anything outside the\n * safe set is folded to `_` — deterministically, so both ends agree.\n */\nfunction cookieSafe(writeKey: string): string {\n if (!writeKey) {\n throw new Error(\"[reopt] writeKey is required to derive cookie names\");\n }\n return writeKey.replace(/[^A-Za-z0-9_-]/g, \"_\");\n}\n\n/**\n * `reopt_<writeKey>_device` — device id plus session/profile hints.\n *\n * The write key is **sanitized**: any character outside `[A-Za-z0-9_-]`\n * becomes `_` (see `cookieSafe`), so do not derive prefix/suffix pieces by\n * probing this with a marker character — the marker is folded too. Always\n * call this with the real write key and compare whole names.\n */\nexport function deviceCookieName(writeKey: string): string {\n return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_device`;\n}\n\n/** `reopt_<writeKey>_consent` — per-category consent decisions. Same sanitizing as `deviceCookieName`. */\nexport function consentCookieName(writeKey: string): string {\n return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_consent`;\n}\n\n/** Everything the device cookie may carry. Only `deviceId` is required. */\nexport interface DeviceCookieState {\n deviceId: string;\n /** Current session, if the browser has one. */\n sessionId?: string | undefined;\n /** Last identified profile. A hint for the server SDK, never trusted by ingest. */\n profileId?: string | undefined;\n}\n\n/** Per-category consent. `false` withdraws, `true` grants, absent = undecided. */\nexport type ConsentCookieState = Record<string, boolean>;\n\n/**\n * What the server hands the browser SDK so the first render already agrees\n * with the server about who the visitor is. Plain JSON — it crosses the\n * RSC → client component boundary as a prop.\n */\nexport interface ReoptBootstrap {\n /** From the device cookie (seeded by the proxy on first visit). */\n deviceId: string;\n /** Session hint, or `null` when the browser has not started one. */\n sessionId: string | null;\n /** Result of the server's `getProfileId` resolver, or `null`. */\n profileId: string | null;\n consent: ConsentCookieState;\n /** Server clock at render time, so a skewed device clock can be corrected. */\n serverTimeMs: number;\n}\n\n/**\n * Ids are opaque, but a cookie is attacker-controlled input (a sibling\n * subdomain can set one). The value ends up in an HTTP header, and\n * `Headers.set` throws on anything outside ISO-8859-1 — so a single odd\n * cookie would break every `fetch` the tracing patch touches. Only the URL\n * \"unreserved\" set is accepted; every id this SDK mints is a UUID.\n */\nexport const IDENTITY_ID_PATTERN = /^[A-Za-z0-9._~-]{1,200}$/;\n\nexport function isValidIdentityId(value: unknown): value is string {\n return typeof value === \"string\" && IDENTITY_ID_PATTERN.test(value);\n}\n\n/**\n * Decodes the envelope. Tolerates one extra layer (`%257B…`) — a cookie\n * setter that encodes an already-encoded value is a mistake we have shipped\n * once, and a visitor's identity should survive it rather than be reminted.\n */\nfunction decode(raw: string): string {\n let text = raw;\n for (let round = 0; round < 3 && /%[0-9A-Fa-f]{2}/.test(text); round++) {\n try {\n const next = decodeURIComponent(text);\n if (next === text) break;\n text = next;\n } catch {\n break;\n }\n if (text.startsWith(\"{\")) break;\n }\n return text;\n}\n\n/**\n * Reads a device cookie value. Accepts the JSON envelope written by\n * `serializeDeviceCookie` and, for cookies seeded before the envelope existed,\n * a bare device id. Returns `null` for anything else — a malformed cookie is\n * treated as absent, never as an error, so a corrupted value cannot take the\n * page down.\n */\nexport function parseDeviceCookie(raw: string | null | undefined): DeviceCookieState | null {\n if (!raw) return null;\n const text = decode(raw.trim());\n if (!text) return null;\n\n if (!text.startsWith(\"{\")) {\n return isValidIdentityId(text) ? { deviceId: text } : null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n\n const candidate = parsed as Record<string, unknown>;\n if (!isValidIdentityId(candidate.deviceId)) return null;\n\n const state: DeviceCookieState = { deviceId: candidate.deviceId };\n if (isValidIdentityId(candidate.sessionId)) state.sessionId = candidate.sessionId;\n if (isValidIdentityId(candidate.profileId)) state.profileId = candidate.profileId;\n return state;\n}\n\n/**\n * Cookie values may not contain `;`, `,` or whitespace, and JSON contains\n * quotes and commas. The envelope is therefore URI-encoded; `parseDeviceCookie`\n * reverses it. Optional fields are omitted rather than written as `null` so\n * the cookie stays as small as the state it carries.\n */\nexport function serializeDeviceCookie(state: DeviceCookieState): string {\n if (!isValidIdentityId(state.deviceId)) {\n throw new Error(\"[reopt] deviceId must be a non-empty string without control characters\");\n }\n const envelope: DeviceCookieState = { deviceId: state.deviceId };\n if (isValidIdentityId(state.sessionId)) envelope.sessionId = state.sessionId;\n if (isValidIdentityId(state.profileId)) envelope.profileId = state.profileId;\n return encodeURIComponent(JSON.stringify(envelope));\n}\n\n/**\n * Reads a consent cookie. Only boolean entries survive; anything else in the\n * object is dropped rather than failing the whole cookie, because one odd key\n * must not erase a user's recorded opt-out.\n */\nexport function parseConsentCookie(raw: string | null | undefined): ConsentCookieState | null {\n if (!raw) return null;\n const text = decode(raw.trim());\n if (!text.startsWith(\"{\")) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return null;\n\n const state: ConsentCookieState = {};\n for (const [category, decision] of Object.entries(parsed as Record<string, unknown>)) {\n if (typeof decision === \"boolean\" && isValidIdentityId(category)) {\n state[category] = decision;\n }\n }\n return state;\n}\n\nexport function serializeConsentCookie(state: ConsentCookieState): string {\n const clean: ConsentCookieState = {};\n for (const [category, decision] of Object.entries(state)) {\n if (typeof decision === \"boolean\" && isValidIdentityId(category)) {\n clean[category] = decision;\n }\n }\n return encodeURIComponent(JSON.stringify(clean));\n}\n\n/**\n * \"Opted out\" means the visitor refused the `analytics` category. An absent\n * cookie or an undecided category is *not* an opt-out — it is the default\n * state of every first visit, and treating it as refusal would make consent\n * banners mandatory for every integration.\n */\nexport function isOptedOut(consent: ConsentCookieState | null | undefined): boolean {\n return consent?.[OPT_OUT_CONSENT_CATEGORY] === false;\n}\n\n// ─── Session tokens ───────────────────────────────────────────────────────────\n\n/**\n * Sessions are assigned by ingest, not by the browser. To let a later request\n * — from the same browser, or from a server acting for it — join that session\n * with authority, ingest hands the browser its session id together with an\n * HMAC over it, and the browser sends both back as `reopt-session-id`. An\n * unsigned or wrongly signed value is a hint at most: ingest falls back to\n * deciding the session from the device, exactly as if the header were absent.\n *\n * The token is `base64url(HMAC-SHA256(secret, sessionId))`. The secret is\n * per project and never leaves the server. WebCrypto only, so this runs in\n * browsers, edge runtimes and Node alike.\n */\nexport const SESSION_HEADER_SEPARATOR = \".\";\n\nexport interface SessionCredential {\n id: string;\n token: string;\n}\n\n/** `<id>.<token>` — what travels in `reopt-session-id`. */\nexport function formatSessionHeader(credential: SessionCredential): string {\n return `${credential.id}${SESSION_HEADER_SEPARATOR}${credential.token}`;\n}\n\n/** Splits a `reopt-session-id` value. `null` unless both parts are well-formed. */\nexport function parseSessionHeader(value: string | null | undefined): SessionCredential | null {\n if (!value) return null;\n const at = value.lastIndexOf(SESSION_HEADER_SEPARATOR);\n if (at <= 0 || at === value.length - 1) return null;\n const id = value.slice(0, at);\n const token = value.slice(at + 1);\n return isValidIdentityId(id) && /^[A-Za-z0-9_-]{43}$/.test(token) ? { id, token } : null;\n}\n\nconst encoder = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n let binary = \"\";\n for (const byte of new Uint8Array(bytes)) binary += String.fromCharCode(byte);\n const b64 = typeof btoa === \"function\" ? btoa(binary) : Buffer.from(binary, \"binary\").toString(\"base64\");\n return b64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmacKey(secret: string): Promise<CryptoKey> {\n return crypto.subtle.importKey(\"raw\", encoder.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n}\n\n/** Issues the token for `sessionId`. Server-side only — needs the project secret. */\nexport async function signSessionToken(secret: string, sessionId: string): Promise<string> {\n if (!secret) throw new Error(\"[reopt] a session signing secret is required\");\n const signature = await crypto.subtle.sign(\"HMAC\", await hmacKey(secret), encoder.encode(sessionId));\n return base64url(signature);\n}\n\n/**\n * Constant-time check of a presented token. Recomputes and compares the\n * digests byte by byte rather than using `subtle.verify` with a decoded\n * token, so a malformed token can never throw its way past the check.\n */\nexport async function verifySessionToken(secret: string, credential: SessionCredential): Promise<boolean> {\n if (!secret || !credential) return false;\n const expected = await signSessionToken(secret, credential.id);\n if (expected.length !== credential.token.length) return false;\n let diff = 0;\n for (let index = 0; index < expected.length; index++) {\n diff |= expected.charCodeAt(index) ^ credential.token.charCodeAt(index);\n }\n return diff === 0;\n}\n"],"mappings":"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"}
|
package/dist/identity.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { a as DEVICE_ID_HEADER, i as CONTRACT_VERSION_HEADER, n as CLIENT_SECRET_HEADER, o as REQUEST_ID_HEADER, r as CONTRACT_VERSION, s as WRITE_KEY_HEADER, t as CLIENT_ID_HEADER } from "./version-CGujUGWd.cjs";
|
|
2
|
+
//#region src/identity.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Request header carrying the browser's current session id. The browser SDK
|
|
5
5
|
* adds it to same-origin requests so a server-side `track()` can land in the
|
|
@@ -31,11 +31,11 @@ declare function deviceCookieName(writeKey: string): string;
|
|
|
31
31
|
declare function consentCookieName(writeKey: string): string;
|
|
32
32
|
/** Everything the device cookie may carry. Only `deviceId` is required. */
|
|
33
33
|
interface DeviceCookieState {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
deviceId: string;
|
|
35
|
+
/** Current session, if the browser has one. */
|
|
36
|
+
sessionId?: string | undefined;
|
|
37
|
+
/** Last identified profile. A hint for the server SDK, never trusted by ingest. */
|
|
38
|
+
profileId?: string | undefined;
|
|
39
39
|
}
|
|
40
40
|
/** Per-category consent. `false` withdraws, `true` grants, absent = undecided. */
|
|
41
41
|
type ConsentCookieState = Record<string, boolean>;
|
|
@@ -45,15 +45,15 @@ type ConsentCookieState = Record<string, boolean>;
|
|
|
45
45
|
* RSC → client component boundary as a prop.
|
|
46
46
|
*/
|
|
47
47
|
interface ReoptBootstrap {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
/** From the device cookie (seeded by the proxy on first visit). */
|
|
49
|
+
deviceId: string;
|
|
50
|
+
/** Session hint, or `null` when the browser has not started one. */
|
|
51
|
+
sessionId: string | null;
|
|
52
|
+
/** Result of the server's `getProfileId` resolver, or `null`. */
|
|
53
|
+
profileId: string | null;
|
|
54
|
+
consent: ConsentCookieState;
|
|
55
|
+
/** Server clock at render time, so a skewed device clock can be corrected. */
|
|
56
|
+
serverTimeMs: number;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Ids are opaque, but a cookie is attacker-controlled input (a sibling
|
|
@@ -107,8 +107,8 @@ declare function isOptedOut(consent: ConsentCookieState | null | undefined): boo
|
|
|
107
107
|
*/
|
|
108
108
|
declare const SESSION_HEADER_SEPARATOR = ".";
|
|
109
109
|
interface SessionCredential {
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
id: string;
|
|
111
|
+
token: string;
|
|
112
112
|
}
|
|
113
113
|
/** `<id>.<token>` — what travels in `reopt-session-id`. */
|
|
114
114
|
declare function formatSessionHeader(credential: SessionCredential): string;
|
|
@@ -122,5 +122,6 @@ declare function signSessionToken(secret: string, sessionId: string): Promise<st
|
|
|
122
122
|
* token, so a malformed token can never throw its way past the check.
|
|
123
123
|
*/
|
|
124
124
|
declare function verifySessionToken(secret: string, credential: SessionCredential): Promise<boolean>;
|
|
125
|
-
|
|
126
|
-
export { COOKIE_MAX_AGE_SECONDS,
|
|
125
|
+
//#endregion
|
|
126
|
+
export { CLIENT_ID_HEADER, CLIENT_SECRET_HEADER, CONTRACT_VERSION, CONTRACT_VERSION_HEADER, COOKIE_MAX_AGE_SECONDS, ConsentCookieState, DEVICE_ID_HEADER, DeviceCookieState, IDENTITY_ID_PATTERN, IDENTITY_REQUEST_HEADERS, OPT_OUT_CONSENT_CATEGORY, REQUEST_ID_HEADER, ReoptBootstrap, SESSION_HEADER_SEPARATOR, SESSION_ID_HEADER, SessionCredential, WRITE_KEY_HEADER, consentCookieName, deviceCookieName, formatSessionHeader, isOptedOut, isValidIdentityId, parseConsentCookie, parseDeviceCookie, parseSessionHeader, serializeConsentCookie, serializeDeviceCookie, signSessionToken, verifySessionToken };
|
|
127
|
+
//# sourceMappingURL=identity.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.cts","names":[],"sources":["../src/identity.ts"],"mappings":";;;;;;;;;;cAoCa;;cAGA;;;;;cAMA;;cAGA;;;;;;;;;iBAwBG,iBAAiB;;iBAKjB,kBAAkB;;UAKjB;EACf;;EAEA;;EAEA;;;KAIU,qBAAqB;;;;;;UAOhB;;EAEf;;EAEA;;EAEA;EACA,SAAS;;EAET;;;;;;;;;cAUW,qBAAmB;iBAEhB,kBAAkB,iBAAiB;;;;;;;;iBA+BnC,kBAAkB,iCAAiC;;;;;;;iBAgCnD,sBAAsB,OAAO;;;;;;iBAe7B,mBAAmB,iCAAiC;iBAsBpD,uBAAuB,OAAO;;;;;;;iBAgB9B,WAAW,SAAS;;;;;;;;;;;;;cAkBvB;UAEI;EACf;EACA;;;iBAIc,oBAAoB,YAAY;;iBAKhC,mBAAmB,mCAAmC;;iBAuBhD,iBAAiB,gBAAgB,oBAAoB;;;;;;iBAWrD,mBAAmB,gBAAgB,YAAY,oBAAoB"}
|
package/dist/identity.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { a as DEVICE_ID_HEADER, i as CONTRACT_VERSION_HEADER, n as CLIENT_SECRET_HEADER, o as REQUEST_ID_HEADER, r as CONTRACT_VERSION, s as WRITE_KEY_HEADER, t as CLIENT_ID_HEADER } from "./version-CGujUGWd.js";
|
|
2
|
+
//#region src/identity.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Request header carrying the browser's current session id. The browser SDK
|
|
5
5
|
* adds it to same-origin requests so a server-side `track()` can land in the
|
|
@@ -31,11 +31,11 @@ declare function deviceCookieName(writeKey: string): string;
|
|
|
31
31
|
declare function consentCookieName(writeKey: string): string;
|
|
32
32
|
/** Everything the device cookie may carry. Only `deviceId` is required. */
|
|
33
33
|
interface DeviceCookieState {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
deviceId: string;
|
|
35
|
+
/** Current session, if the browser has one. */
|
|
36
|
+
sessionId?: string | undefined;
|
|
37
|
+
/** Last identified profile. A hint for the server SDK, never trusted by ingest. */
|
|
38
|
+
profileId?: string | undefined;
|
|
39
39
|
}
|
|
40
40
|
/** Per-category consent. `false` withdraws, `true` grants, absent = undecided. */
|
|
41
41
|
type ConsentCookieState = Record<string, boolean>;
|
|
@@ -45,15 +45,15 @@ type ConsentCookieState = Record<string, boolean>;
|
|
|
45
45
|
* RSC → client component boundary as a prop.
|
|
46
46
|
*/
|
|
47
47
|
interface ReoptBootstrap {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
/** From the device cookie (seeded by the proxy on first visit). */
|
|
49
|
+
deviceId: string;
|
|
50
|
+
/** Session hint, or `null` when the browser has not started one. */
|
|
51
|
+
sessionId: string | null;
|
|
52
|
+
/** Result of the server's `getProfileId` resolver, or `null`. */
|
|
53
|
+
profileId: string | null;
|
|
54
|
+
consent: ConsentCookieState;
|
|
55
|
+
/** Server clock at render time, so a skewed device clock can be corrected. */
|
|
56
|
+
serverTimeMs: number;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Ids are opaque, but a cookie is attacker-controlled input (a sibling
|
|
@@ -107,8 +107,8 @@ declare function isOptedOut(consent: ConsentCookieState | null | undefined): boo
|
|
|
107
107
|
*/
|
|
108
108
|
declare const SESSION_HEADER_SEPARATOR = ".";
|
|
109
109
|
interface SessionCredential {
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
id: string;
|
|
111
|
+
token: string;
|
|
112
112
|
}
|
|
113
113
|
/** `<id>.<token>` — what travels in `reopt-session-id`. */
|
|
114
114
|
declare function formatSessionHeader(credential: SessionCredential): string;
|
|
@@ -122,5 +122,6 @@ declare function signSessionToken(secret: string, sessionId: string): Promise<st
|
|
|
122
122
|
* token, so a malformed token can never throw its way past the check.
|
|
123
123
|
*/
|
|
124
124
|
declare function verifySessionToken(secret: string, credential: SessionCredential): Promise<boolean>;
|
|
125
|
-
|
|
126
|
-
export { COOKIE_MAX_AGE_SECONDS,
|
|
125
|
+
//#endregion
|
|
126
|
+
export { CLIENT_ID_HEADER, CLIENT_SECRET_HEADER, CONTRACT_VERSION, CONTRACT_VERSION_HEADER, COOKIE_MAX_AGE_SECONDS, ConsentCookieState, DEVICE_ID_HEADER, DeviceCookieState, IDENTITY_ID_PATTERN, IDENTITY_REQUEST_HEADERS, OPT_OUT_CONSENT_CATEGORY, REQUEST_ID_HEADER, ReoptBootstrap, SESSION_HEADER_SEPARATOR, SESSION_ID_HEADER, SessionCredential, WRITE_KEY_HEADER, consentCookieName, deviceCookieName, formatSessionHeader, isOptedOut, isValidIdentityId, parseConsentCookie, parseDeviceCookie, parseSessionHeader, serializeConsentCookie, serializeDeviceCookie, signSessionToken, verifySessionToken };
|
|
127
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","names":[],"sources":["../src/identity.ts"],"mappings":";;;;;;;;;;cAoCa;;cAGA;;;;;cAMA;;cAGA;;;;;;;;;iBAwBG,iBAAiB;;iBAKjB,kBAAkB;;UAKjB;EACf;;EAEA;;EAEA;;;KAIU,qBAAqB;;;;;;UAOhB;;EAEf;;EAEA;;EAEA;EACA,SAAS;;EAET;;;;;;;;;cAUW,qBAAmB;iBAEhB,kBAAkB,iBAAiB;;;;;;;;iBA+BnC,kBAAkB,iCAAiC;;;;;;;iBAgCnD,sBAAsB,OAAO;;;;;;iBAe7B,mBAAmB,iCAAiC;iBAsBpD,uBAAuB,OAAO;;;;;;;iBAgB9B,WAAW,SAAS;;;;;;;;;;;;;cAkBvB;UAEI;EACf;EACA;;;iBAIc,oBAAoB,YAAY;;iBAKhC,mBAAmB,mCAAmC;;iBAuBhD,iBAAiB,gBAAgB,oBAAoB;;;;;;iBAWrD,mBAAmB,gBAAgB,YAAY,oBAAoB"}
|