@reopt-ai/data-contract 0.6.1 → 0.6.2

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.
Files changed (41) hide show
  1. package/dist/{chunk-JB4MJQX7.cjs → chunk-AOS53KAV.cjs} +2 -2
  2. package/dist/{chunk-JB4MJQX7.cjs.map → chunk-AOS53KAV.cjs.map} +1 -1
  3. package/dist/{chunk-VGB2VJO4.cjs → chunk-BA52YA7N.cjs} +4 -4
  4. package/dist/{chunk-VGB2VJO4.cjs.map → chunk-BA52YA7N.cjs.map} +1 -1
  5. package/dist/{chunk-EZ2TA4S6.js → chunk-DUJSYMYY.js} +2 -2
  6. package/dist/{chunk-GCQZNME3.js → chunk-FMOKJTQD.js} +13 -6
  7. package/dist/chunk-FMOKJTQD.js.map +1 -0
  8. package/dist/{chunk-PHPTCLRU.cjs → chunk-FQSCQSMP.cjs} +14 -7
  9. package/dist/chunk-FQSCQSMP.cjs.map +1 -0
  10. package/dist/{chunk-HLQAZ6JW.js → chunk-ZCKVMUIQ.js} +2 -2
  11. package/dist/{chunk-HLQAZ6JW.js.map → chunk-ZCKVMUIQ.js.map} +1 -1
  12. package/dist/client.cjs +11 -11
  13. package/dist/client.d.cts +1 -1
  14. package/dist/client.d.ts +1 -1
  15. package/dist/client.js +3 -3
  16. package/dist/control.cjs +1 -1
  17. package/dist/control.d.cts +6 -6
  18. package/dist/control.d.ts +6 -6
  19. package/dist/control.js +1 -1
  20. package/dist/identity.cjs +3 -3
  21. package/dist/identity.d.cts +10 -3
  22. package/dist/identity.d.ts +10 -3
  23. package/dist/identity.js +2 -2
  24. package/dist/index.cjs +2 -2
  25. package/dist/index.d.cts +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/ingest.cjs +4 -4
  29. package/dist/ingest.d.cts +6 -6
  30. package/dist/ingest.d.ts +6 -6
  31. package/dist/ingest.js +3 -3
  32. package/dist/query.cjs +1 -1
  33. package/dist/query.d.cts +5 -5
  34. package/dist/query.d.ts +5 -5
  35. package/dist/query.js +1 -1
  36. package/dist/{version-BzeRTpta.d.cts → version-BANKB6Og.d.cts} +1 -1
  37. package/dist/{version-BzeRTpta.d.ts → version-BANKB6Og.d.ts} +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-GCQZNME3.js.map +0 -1
  40. package/dist/chunk-PHPTCLRU.cjs.map +0 -1
  41. /package/dist/{chunk-EZ2TA4S6.js.map → chunk-DUJSYMYY.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/version.ts
2
- var CONTRACT_VERSION = "0.6.1";
2
+ var CONTRACT_VERSION = "0.6.2";
3
3
  var CONTRACT_VERSION_HEADER = "x-reopt-contract-version";
4
4
  var CLIENT_ID_HEADER = "reopt-client-id";
5
5
  var CLIENT_SECRET_HEADER = "reopt-client-secret";
@@ -16,4 +16,4 @@ var REQUEST_ID_HEADER = "x-request-id";
16
16
 
17
17
 
18
18
  exports.CONTRACT_VERSION = CONTRACT_VERSION; exports.CONTRACT_VERSION_HEADER = CONTRACT_VERSION_HEADER; exports.CLIENT_ID_HEADER = CLIENT_ID_HEADER; exports.CLIENT_SECRET_HEADER = CLIENT_SECRET_HEADER; exports.WRITE_KEY_HEADER = WRITE_KEY_HEADER; exports.DEVICE_ID_HEADER = DEVICE_ID_HEADER; exports.REQUEST_ID_HEADER = REQUEST_ID_HEADER;
19
- //# sourceMappingURL=chunk-JB4MJQX7.cjs.map
19
+ //# sourceMappingURL=chunk-AOS53KAV.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-JB4MJQX7.cjs","../src/version.ts"],"names":[],"mappings":"AAAA;ACgBO,IAAM,iBAAA,EAAmB,OAAA;AAGzB,IAAM,wBAAA,EAA0B,0BAAA;AAGhC,IAAM,iBAAA,EAAmB,iBAAA;AACzB,IAAM,qBAAA,EAAuB,qBAAA;AAC7B,IAAM,iBAAA,EAAmB,iBAAA;AACzB,IAAM,iBAAA,EAAmB,iBAAA;AACzB,IAAM,kBAAA,EAAoB,cAAA;ADlBjC;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,kVAAC","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-JB4MJQX7.cjs","sourcesContent":[null,"/**\n * Wire version and header names — the part of the contract every party\n * needs and none of them should pay zod for. Both `.` and `./identity`\n * re-export these; browser bundles import them through `./identity` so the\n * schema modules stay out of the client graph.\n */\n\n/**\n * Contract version. Bumped on any breaking change to a schema in this package.\n * The server echoes the version it was built against in `x-reopt-contract-version`;\n * `createDataClient` surfaces a mismatch rather than guessing.\n *\n * Must equal the package version (`package-identity.test.ts` enforces it), so\n * every publish is a wire bump: consumers that check for exact equality —\n * every SDK — must upgrade in lockstep with the server.\n */\nexport const CONTRACT_VERSION = \"0.6.1\";\n\n/** Response header carrying the server's contract version. */\nexport const CONTRACT_VERSION_HEADER = \"x-reopt-contract-version\";\n\n/** Request header names shared by the ingest and query planes. */\nexport const CLIENT_ID_HEADER = \"reopt-client-id\";\nexport const CLIENT_SECRET_HEADER = \"reopt-client-secret\";\nexport const WRITE_KEY_HEADER = \"reopt-write-key\";\nexport const DEVICE_ID_HEADER = \"reopt-device-id\";\nexport const REQUEST_ID_HEADER = \"x-request-id\";\n"]}
1
+ {"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-AOS53KAV.cjs","../src/version.ts"],"names":[],"mappings":"AAAA;ACgBO,IAAM,iBAAA,EAAmB,OAAA;AAGzB,IAAM,wBAAA,EAA0B,0BAAA;AAGhC,IAAM,iBAAA,EAAmB,iBAAA;AACzB,IAAM,qBAAA,EAAuB,qBAAA;AAC7B,IAAM,iBAAA,EAAmB,iBAAA;AACzB,IAAM,iBAAA,EAAmB,iBAAA;AACzB,IAAM,kBAAA,EAAoB,cAAA;ADlBjC;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,kVAAC","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-AOS53KAV.cjs","sourcesContent":[null,"/**\n * Wire version and header names — the part of the contract every party\n * needs and none of them should pay zod for. Both `.` and `./identity`\n * re-export these; browser bundles import them through `./identity` so the\n * schema modules stay out of the client graph.\n */\n\n/**\n * Contract version. Bumped on any breaking change to a schema in this package.\n * The server echoes the version it was built against in `x-reopt-contract-version`;\n * `createDataClient` surfaces a mismatch rather than guessing.\n *\n * Must equal the package version (`package-identity.test.ts` enforces it), so\n * every publish is a wire bump: consumers that check for exact equality —\n * every SDK — must upgrade in lockstep with the server.\n */\nexport const CONTRACT_VERSION = \"0.6.2\";\n\n/** Response header carrying the server's contract version. */\nexport const CONTRACT_VERSION_HEADER = \"x-reopt-contract-version\";\n\n/** Request header names shared by the ingest and query planes. */\nexport const CLIENT_ID_HEADER = \"reopt-client-id\";\nexport const CLIENT_SECRET_HEADER = \"reopt-client-secret\";\nexport const WRITE_KEY_HEADER = \"reopt-write-key\";\nexport const DEVICE_ID_HEADER = \"reopt-device-id\";\nexport const REQUEST_ID_HEADER = \"x-request-id\";\n"]}
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunkPHPTCLRUcjs = require('./chunk-PHPTCLRU.cjs');
3
+ var _chunkFQSCQSMPcjs = require('./chunk-FQSCQSMP.cjs');
4
4
 
5
5
 
6
6
  var _chunk3EEANNYNcjs = require('./chunk-3EEANNYN.cjs');
@@ -30,7 +30,7 @@ var zClientEventMetadata = {
30
30
  * header is authoritative and a body field would be trivially forged.
31
31
  * Same character set as every identity id.
32
32
  */
33
- deviceId: _zod.z.string().regex(_chunkPHPTCLRUcjs.IDENTITY_ID_PATTERN, "expected an identity id").optional()
33
+ deviceId: _zod.z.string().regex(_chunkFQSCQSMPcjs.IDENTITY_ID_PATTERN, "expected an identity id").optional()
34
34
  };
35
35
  var zTrackPayload = _zod.z.object({
36
36
  name: _zod.z.string().min(1).max(200),
@@ -96,7 +96,7 @@ var ingestCounts = {
96
96
  rejected: _zod.z.array(zIngestRejection)
97
97
  };
98
98
  var zSessionCredential = _zod.z.object({
99
- id: _zod.z.string().regex(_chunkPHPTCLRUcjs.IDENTITY_ID_PATTERN),
99
+ id: _zod.z.string().regex(_chunkFQSCQSMPcjs.IDENTITY_ID_PATTERN),
100
100
  token: _zod.z.string().regex(/^[A-Za-z0-9_-]{43}$/)
101
101
  });
102
102
  var zIngestOkResponse = _zod.z.object({
@@ -176,4 +176,4 @@ var zIngestError = _zod.z.object({
176
176
 
177
177
 
178
178
  exports.MAX_TRACK_PAYLOAD_BYTES = MAX_TRACK_PAYLOAD_BYTES; exports.zTrackPayload = zTrackPayload; exports.zIdentifyPayload = zIdentifyPayload; exports.zIncrementPayload = zIncrementPayload; exports.zDecrementPayload = zDecrementPayload; exports.zTrackHandlerPayload = zTrackHandlerPayload; exports.INGEST_MODES = INGEST_MODES; exports.zIngestMode = zIngestMode; exports.INGEST_REJECTION_REASONS = INGEST_REJECTION_REASONS; exports.zIngestRejectionReason = zIngestRejectionReason; exports.zIngestRejection = zIngestRejection; exports.zSessionCredential = zSessionCredential; exports.zIngestOkResponse = zIngestOkResponse; exports.zIngestAcceptedResponse = zIngestAcceptedResponse; exports.zIngestResponse = zIngestResponse; exports.reconcileIngestResponse = reconcileIngestResponse; exports.INGEST_ERROR_CODES = INGEST_ERROR_CODES; exports.zIngestErrorCode = zIngestErrorCode; exports.zIngestError = zIngestError;
179
- //# sourceMappingURL=chunk-VGB2VJO4.cjs.map
179
+ //# sourceMappingURL=chunk-BA52YA7N.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-VGB2VJO4.cjs","../src/ingest.ts"],"names":[],"mappings":"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","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-VGB2VJO4.cjs","sourcesContent":[null,"/**\n * `POST /api/track` — the ingest contract.\n *\n * The request shape is shared by both credential modes. The *response* shape\n * is where server-ingest differs: it reports per-event rejections instead of\n * failing a whole batch, because a forwarder that stalls on one malformed row\n * stops forwarding forever.\n *\n * Absorbed from the former internal `@reopt/ingest-contract`; that package is\n * now a deprecated re-export of this module.\n */\nimport { z } from \"zod\";\nimport { IDENTITY_ID_PATTERN } from \"./identity.js\";\n\nexport {\n AUTO_EVENT_NAMES,\n AUTO_EVENT_PROPERTIES,\n AUTO_EVENT_ROLLUP_KEYS,\n RESERVED_EVENT_NAMES,\n SCROLL_DEPTH_BUCKETS,\n scrollDepthBucket,\n type AutoEventName,\n type ScrollDepthBucket,\n} from \"./events.js\";\nimport { RESERVED_EVENT_NAMES } from \"./events.js\";\n\n/** Hard cap enforced by the route before the body is even parsed. */\nexport const MAX_TRACK_PAYLOAD_BYTES = 512_000;\n\nconst zClientEventMetadata = {\n /**\n * Client-generated UUID. This is the project-scoped idempotency key stored as\n * `RawEvent.clientEventId`; the server uses a separate globally unique raw\n * event id internally. Re-sending the same eventId to the same project is\n * always safe and always reported as a duplicate.\n */\n eventId: z.uuid(),\n /** Event time, epoch milliseconds. */\n timestamp: z.number().int().nonnegative(),\n /**\n * The browser device this event belongs to, for events a server records\n * on a visitor's behalf (a form submission, an order). Lets one server\n * batch carry events from many visitors — a forwarder cannot split its\n * batches per device without breaking its failure model and its\n * `accepted + duplicates + rejected === sent` invariant.\n *\n * Honoured in `server` mode only, where it takes precedence over the\n * `reopt-device-id` request header. Ignored in `browser` mode: there the\n * header is authoritative and a body field would be trivially forged.\n * Same character set as every identity id.\n */\n deviceId: z.string().regex(IDENTITY_ID_PATTERN, \"expected an identity id\").optional(),\n};\n\nexport const zTrackPayload = z\n .object({\n name: z.string().min(1).max(200),\n properties: z.record(z.string(), z.unknown()).optional(),\n profileId: z.string().max(500).or(z.number()).optional(),\n })\n .refine((data) => !RESERVED_EVENT_NAMES.includes(data.name as never), {\n message: `Event name cannot be one of the reserved names: ${RESERVED_EVENT_NAMES.join(\", \")}`,\n path: [\"name\"],\n });\n\nexport const zIdentifyPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n firstName: z.string().max(200).optional(),\n lastName: z.string().max(200).optional(),\n email: z.email().max(320).optional(),\n avatar: z.url().max(2000).optional(),\n properties: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport const zIncrementPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n property: z.string().min(1).max(200),\n value: z.number().positive().optional(),\n});\n\nexport const zDecrementPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n property: z.string().min(1).max(200),\n value: z.number().positive().optional(),\n});\n\nexport const zTrackHandlerPayload = z.discriminatedUnion(\"type\", [\n z.object({ type: z.literal(\"track\"), payload: zTrackPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"identify\"), payload: zIdentifyPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"increment\"), payload: zIncrementPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"decrement\"), payload: zDecrementPayload, ...zClientEventMetadata }),\n]);\n\nexport type ITrackPayload = z.infer<typeof zTrackPayload>;\nexport type IIdentifyPayload = z.infer<typeof zIdentifyPayload>;\nexport type IIncrementPayload = z.infer<typeof zIncrementPayload>;\nexport type IDecrementPayload = z.infer<typeof zDecrementPayload>;\nexport type ITrackHandlerPayload = z.infer<typeof zTrackHandlerPayload>;\n\n// ─── Ingest mode ────────────────────────────────────────────────────────────\n\n/**\n * Decided by the credential presented, never by a header the caller sets:\n * `reopt-write-key` alone → `browser`, `reopt-client-id` + `reopt-client-secret`\n * → `server`. A separate mode header could disagree with the credential; this\n * cannot.\n *\n * `browser` keeps the 5-second click dedup and device-scoped sessions.\n * `server` drops both and relies solely on `eventId` idempotency — a server\n * batch legitimately contains the same event name many times within one second,\n * and its \"device\" is a process, not a person.\n */\nexport const INGEST_MODES = [\"browser\", \"server\"] as const;\nexport const zIngestMode = z.enum(INGEST_MODES);\nexport type IngestMode = z.infer<typeof zIngestMode>;\n\n// ─── Response ───────────────────────────────────────────────────────────────\n\n/**\n * Why a single event in a batch was dropped. These are permanent: re-sending\n * the same row produces the same rejection, so a forwarder should count it as\n * skipped and advance its cursor rather than retry.\n */\nexport const INGEST_REJECTION_REASONS = [\n /** Failed the zod schema (bad eventId, missing name, wrong types…). */\n \"validation_failed\",\n /** Used one of RESERVED_EVENT_NAMES. */\n \"reserved_name\",\n /** Another row earlier in the same batch already claimed this eventId. */\n \"duplicate_in_batch\",\n] as const;\nexport const zIngestRejectionReason = z.enum(INGEST_REJECTION_REASONS);\nexport type IngestRejectionReason = z.infer<typeof zIngestRejectionReason>;\n\nexport const zIngestRejection = z.object({\n /** May be absent or malformed on `validation_failed` — hence `string`, not `uuid`. */\n eventId: z.string(),\n /** Position in the submitted batch. The only reliable identifier when eventId itself is bad. */\n index: z.number().int().nonnegative(),\n reason: zIngestRejectionReason,\n message: z.string().optional(),\n});\nexport type IngestRejection = z.infer<typeof zIngestRejection>;\n\nconst ingestCounts = {\n /** Raw events newly persisted by this request. */\n accepted: z.number().int().nonnegative(),\n /**\n * Events the server already had. In `server` mode this is exactly the\n * eventId-idempotency count; in `browser` mode it also includes the\n * 5-second click dedup.\n */\n duplicates: z.number().int().nonnegative(),\n /** Always `[]` in `browser` mode, where a bad row fails the whole batch. */\n rejected: z.array(zIngestRejection),\n};\n\n/**\n * The session ingest filed this batch under, with the token that lets the\n * browser (or a server acting for it) join the same session later with\n * authority — see `signSessionToken` in `./identity`. Browser mode only;\n * absent when the batch carried no sessionable event.\n */\nexport const zSessionCredential = z.object({\n id: z.string().regex(IDENTITY_ID_PATTERN),\n token: z.string().regex(/^[A-Za-z0-9_-]{43}$/),\n});\nexport type SessionCredential = z.infer<typeof zSessionCredential>;\n\n/** 200 — accepted and handed to the materialize queue. */\nexport const zIngestOkResponse = z.object({\n status: z.literal(\"ok\"),\n requestId: z.string(),\n mode: zIngestMode,\n ...ingestCounts,\n queued: z.boolean(),\n session: zSessionCredential.optional(),\n});\n\n/**\n * 202 — raw events landed, but enqueueing the materialize task failed. No data\n * is lost: the `replay-unmaterialized` cron picks these up. A forwarder should\n * treat this as success and advance its cursor. Carries the same counts as 200\n * so the reconciliation below works here too.\n */\nexport const zIngestAcceptedResponse = z.object({\n status: z.literal(\"accepted\"),\n requestId: z.string(),\n mode: zIngestMode,\n ...ingestCounts,\n backgroundQueued: z.literal(false),\n replay: z.literal(\"unmaterialized-raw-events\"),\n session: zSessionCredential.optional(),\n});\n\nexport const zIngestResponse = z.discriminatedUnion(\"status\", [zIngestOkResponse, zIngestAcceptedResponse]);\n\nexport type IngestOkResponse = z.infer<typeof zIngestOkResponse>;\nexport type IngestAcceptedResponse = z.infer<typeof zIngestAcceptedResponse>;\nexport type IngestResponse = z.infer<typeof zIngestResponse>;\n\n/**\n * The reconciliation a forwarder runs on every 2xx:\n *\n * accepted + duplicates + rejected.length === events sent\n *\n * A mismatch means the server silently dropped something, which is exactly the\n * failure mode this contract exists to make impossible to miss.\n */\nexport function reconcileIngestResponse(response: IngestResponse, sentCount: number): boolean {\n return response.accepted + response.duplicates + response.rejected.length === sentCount;\n}\n\n// ─── Errors ─────────────────────────────────────────────────────────────────\n\n/**\n * Stable machine-readable codes. Branch on these, never on `error`/`message`,\n * which are prose and may be reworded.\n */\nexport const INGEST_ERROR_CODES = [\n /** 401 — unknown or mismatched credentials. Stop and alert; do not retry. */\n \"unauthorized\",\n /** 400 — body was not valid JSON. */\n \"invalid_json\",\n /** 400 — request exceeded MAX_TRACK_PAYLOAD_BYTES. Split the batch. */\n \"payload_too_large\",\n /** 400 — zero events submitted. */\n \"empty_batch\",\n /** 400 — batch contained a `type: \"alias\"` event, which is not supported. */\n \"alias_unsupported\",\n /** 400 — the project has no organization; it cannot be billed or quota-checked. */\n \"project_without_organization\",\n /** 400 — `browser` mode only: a row failed validation. `server` mode reports these in `rejected[]`. */\n \"validation_failed\",\n /** 429 — per-project request rate limit. `Retry-After` set. Pause this tick, keep the cursor. */\n \"rate_limited\",\n /** 429 — the organization's monthly event quota is exhausted. `Retry-After` set (capped at 1h). Alert: a human must raise the limit. */\n \"quota_exceeded\",\n /** 410 — the project is being deleted. Permanent; stop sending. */\n \"project_purging\",\n /** 500 — unexpected server failure. Back off and retry. */\n \"internal_error\",\n] as const;\nexport const zIngestErrorCode = z.enum(INGEST_ERROR_CODES);\nexport type IngestErrorCode = z.infer<typeof zIngestErrorCode>;\n\nexport const zIngestError = z.object({\n status: z.number().int(),\n code: zIngestErrorCode,\n error: z.string(),\n message: z.string().optional(),\n /** zod issues, when `code === \"validation_failed\"`. */\n errors: z.unknown().optional(),\n requestId: z.string().optional(),\n});\nexport type IngestError = z.infer<typeof zIngestError>;\n"]}
1
+ {"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-BA52YA7N.cjs","../src/ingest.ts"],"names":[],"mappings":"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","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-BA52YA7N.cjs","sourcesContent":[null,"/**\n * `POST /api/track` — the ingest contract.\n *\n * The request shape is shared by both credential modes. The *response* shape\n * is where server-ingest differs: it reports per-event rejections instead of\n * failing a whole batch, because a forwarder that stalls on one malformed row\n * stops forwarding forever.\n *\n * Absorbed from the former internal `@reopt/ingest-contract`; that package is\n * now a deprecated re-export of this module.\n */\nimport { z } from \"zod\";\nimport { IDENTITY_ID_PATTERN } from \"./identity.js\";\n\nexport {\n AUTO_EVENT_NAMES,\n AUTO_EVENT_PROPERTIES,\n AUTO_EVENT_ROLLUP_KEYS,\n RESERVED_EVENT_NAMES,\n SCROLL_DEPTH_BUCKETS,\n scrollDepthBucket,\n type AutoEventName,\n type ScrollDepthBucket,\n} from \"./events.js\";\nimport { RESERVED_EVENT_NAMES } from \"./events.js\";\n\n/** Hard cap enforced by the route before the body is even parsed. */\nexport const MAX_TRACK_PAYLOAD_BYTES = 512_000;\n\nconst zClientEventMetadata = {\n /**\n * Client-generated UUID. This is the project-scoped idempotency key stored as\n * `RawEvent.clientEventId`; the server uses a separate globally unique raw\n * event id internally. Re-sending the same eventId to the same project is\n * always safe and always reported as a duplicate.\n */\n eventId: z.uuid(),\n /** Event time, epoch milliseconds. */\n timestamp: z.number().int().nonnegative(),\n /**\n * The browser device this event belongs to, for events a server records\n * on a visitor's behalf (a form submission, an order). Lets one server\n * batch carry events from many visitors — a forwarder cannot split its\n * batches per device without breaking its failure model and its\n * `accepted + duplicates + rejected === sent` invariant.\n *\n * Honoured in `server` mode only, where it takes precedence over the\n * `reopt-device-id` request header. Ignored in `browser` mode: there the\n * header is authoritative and a body field would be trivially forged.\n * Same character set as every identity id.\n */\n deviceId: z.string().regex(IDENTITY_ID_PATTERN, \"expected an identity id\").optional(),\n};\n\nexport const zTrackPayload = z\n .object({\n name: z.string().min(1).max(200),\n properties: z.record(z.string(), z.unknown()).optional(),\n profileId: z.string().max(500).or(z.number()).optional(),\n })\n .refine((data) => !RESERVED_EVENT_NAMES.includes(data.name as never), {\n message: `Event name cannot be one of the reserved names: ${RESERVED_EVENT_NAMES.join(\", \")}`,\n path: [\"name\"],\n });\n\nexport const zIdentifyPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n firstName: z.string().max(200).optional(),\n lastName: z.string().max(200).optional(),\n email: z.email().max(320).optional(),\n avatar: z.url().max(2000).optional(),\n properties: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport const zIncrementPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n property: z.string().min(1).max(200),\n value: z.number().positive().optional(),\n});\n\nexport const zDecrementPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n property: z.string().min(1).max(200),\n value: z.number().positive().optional(),\n});\n\nexport const zTrackHandlerPayload = z.discriminatedUnion(\"type\", [\n z.object({ type: z.literal(\"track\"), payload: zTrackPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"identify\"), payload: zIdentifyPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"increment\"), payload: zIncrementPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"decrement\"), payload: zDecrementPayload, ...zClientEventMetadata }),\n]);\n\nexport type ITrackPayload = z.infer<typeof zTrackPayload>;\nexport type IIdentifyPayload = z.infer<typeof zIdentifyPayload>;\nexport type IIncrementPayload = z.infer<typeof zIncrementPayload>;\nexport type IDecrementPayload = z.infer<typeof zDecrementPayload>;\nexport type ITrackHandlerPayload = z.infer<typeof zTrackHandlerPayload>;\n\n// ─── Ingest mode ────────────────────────────────────────────────────────────\n\n/**\n * Decided by the credential presented, never by a header the caller sets:\n * `reopt-write-key` alone → `browser`, `reopt-client-id` + `reopt-client-secret`\n * → `server`. A separate mode header could disagree with the credential; this\n * cannot.\n *\n * `browser` keeps the 5-second click dedup and device-scoped sessions.\n * `server` drops both and relies solely on `eventId` idempotency — a server\n * batch legitimately contains the same event name many times within one second,\n * and its \"device\" is a process, not a person.\n */\nexport const INGEST_MODES = [\"browser\", \"server\"] as const;\nexport const zIngestMode = z.enum(INGEST_MODES);\nexport type IngestMode = z.infer<typeof zIngestMode>;\n\n// ─── Response ───────────────────────────────────────────────────────────────\n\n/**\n * Why a single event in a batch was dropped. These are permanent: re-sending\n * the same row produces the same rejection, so a forwarder should count it as\n * skipped and advance its cursor rather than retry.\n */\nexport const INGEST_REJECTION_REASONS = [\n /** Failed the zod schema (bad eventId, missing name, wrong types…). */\n \"validation_failed\",\n /** Used one of RESERVED_EVENT_NAMES. */\n \"reserved_name\",\n /** Another row earlier in the same batch already claimed this eventId. */\n \"duplicate_in_batch\",\n] as const;\nexport const zIngestRejectionReason = z.enum(INGEST_REJECTION_REASONS);\nexport type IngestRejectionReason = z.infer<typeof zIngestRejectionReason>;\n\nexport const zIngestRejection = z.object({\n /** May be absent or malformed on `validation_failed` — hence `string`, not `uuid`. */\n eventId: z.string(),\n /** Position in the submitted batch. The only reliable identifier when eventId itself is bad. */\n index: z.number().int().nonnegative(),\n reason: zIngestRejectionReason,\n message: z.string().optional(),\n});\nexport type IngestRejection = z.infer<typeof zIngestRejection>;\n\nconst ingestCounts = {\n /** Raw events newly persisted by this request. */\n accepted: z.number().int().nonnegative(),\n /**\n * Events the server already had. In `server` mode this is exactly the\n * eventId-idempotency count; in `browser` mode it also includes the\n * 5-second click dedup.\n */\n duplicates: z.number().int().nonnegative(),\n /** Always `[]` in `browser` mode, where a bad row fails the whole batch. */\n rejected: z.array(zIngestRejection),\n};\n\n/**\n * The session ingest filed this batch under, with the token that lets the\n * browser (or a server acting for it) join the same session later with\n * authority — see `signSessionToken` in `./identity`. Browser mode only;\n * absent when the batch carried no sessionable event.\n */\nexport const zSessionCredential = z.object({\n id: z.string().regex(IDENTITY_ID_PATTERN),\n token: z.string().regex(/^[A-Za-z0-9_-]{43}$/),\n});\nexport type SessionCredential = z.infer<typeof zSessionCredential>;\n\n/** 200 — accepted and handed to the materialize queue. */\nexport const zIngestOkResponse = z.object({\n status: z.literal(\"ok\"),\n requestId: z.string(),\n mode: zIngestMode,\n ...ingestCounts,\n queued: z.boolean(),\n session: zSessionCredential.optional(),\n});\n\n/**\n * 202 — raw events landed, but enqueueing the materialize task failed. No data\n * is lost: the `replay-unmaterialized` cron picks these up. A forwarder should\n * treat this as success and advance its cursor. Carries the same counts as 200\n * so the reconciliation below works here too.\n */\nexport const zIngestAcceptedResponse = z.object({\n status: z.literal(\"accepted\"),\n requestId: z.string(),\n mode: zIngestMode,\n ...ingestCounts,\n backgroundQueued: z.literal(false),\n replay: z.literal(\"unmaterialized-raw-events\"),\n session: zSessionCredential.optional(),\n});\n\nexport const zIngestResponse = z.discriminatedUnion(\"status\", [zIngestOkResponse, zIngestAcceptedResponse]);\n\nexport type IngestOkResponse = z.infer<typeof zIngestOkResponse>;\nexport type IngestAcceptedResponse = z.infer<typeof zIngestAcceptedResponse>;\nexport type IngestResponse = z.infer<typeof zIngestResponse>;\n\n/**\n * The reconciliation a forwarder runs on every 2xx:\n *\n * accepted + duplicates + rejected.length === events sent\n *\n * A mismatch means the server silently dropped something, which is exactly the\n * failure mode this contract exists to make impossible to miss.\n */\nexport function reconcileIngestResponse(response: IngestResponse, sentCount: number): boolean {\n return response.accepted + response.duplicates + response.rejected.length === sentCount;\n}\n\n// ─── Errors ─────────────────────────────────────────────────────────────────\n\n/**\n * Stable machine-readable codes. Branch on these, never on `error`/`message`,\n * which are prose and may be reworded.\n */\nexport const INGEST_ERROR_CODES = [\n /** 401 — unknown or mismatched credentials. Stop and alert; do not retry. */\n \"unauthorized\",\n /** 400 — body was not valid JSON. */\n \"invalid_json\",\n /** 400 — request exceeded MAX_TRACK_PAYLOAD_BYTES. Split the batch. */\n \"payload_too_large\",\n /** 400 — zero events submitted. */\n \"empty_batch\",\n /** 400 — batch contained a `type: \"alias\"` event, which is not supported. */\n \"alias_unsupported\",\n /** 400 — the project has no organization; it cannot be billed or quota-checked. */\n \"project_without_organization\",\n /** 400 — `browser` mode only: a row failed validation. `server` mode reports these in `rejected[]`. */\n \"validation_failed\",\n /** 429 — per-project request rate limit. `Retry-After` set. Pause this tick, keep the cursor. */\n \"rate_limited\",\n /** 429 — the organization's monthly event quota is exhausted. `Retry-After` set (capped at 1h). Alert: a human must raise the limit. */\n \"quota_exceeded\",\n /** 410 — the project is being deleted. Permanent; stop sending. */\n \"project_purging\",\n /** 500 — unexpected server failure. Back off and retry. */\n \"internal_error\",\n] as const;\nexport const zIngestErrorCode = z.enum(INGEST_ERROR_CODES);\nexport type IngestErrorCode = z.infer<typeof zIngestErrorCode>;\n\nexport const zIngestError = z.object({\n status: z.number().int(),\n code: zIngestErrorCode,\n error: z.string(),\n message: z.string().optional(),\n /** zod issues, when `code === \"validation_failed\"`. */\n errors: z.unknown().optional(),\n requestId: z.string().optional(),\n});\nexport type IngestError = z.infer<typeof zIngestError>;\n"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IDENTITY_ID_PATTERN
3
- } from "./chunk-GCQZNME3.js";
3
+ } from "./chunk-FMOKJTQD.js";
4
4
  import {
5
5
  RESERVED_EVENT_NAMES
6
6
  } from "./chunk-FP2X3PRK.js";
@@ -176,4 +176,4 @@ export {
176
176
  zIngestErrorCode,
177
177
  zIngestError
178
178
  };
179
- //# sourceMappingURL=chunk-EZ2TA4S6.js.map
179
+ //# sourceMappingURL=chunk-DUJSYMYY.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DEVICE_ID_HEADER
3
- } from "./chunk-HLQAZ6JW.js";
3
+ } from "./chunk-ZCKVMUIQ.js";
4
4
 
5
5
  // src/identity.ts
6
6
  var SESSION_ID_HEADER = "reopt-session-id";
@@ -25,11 +25,18 @@ function isValidIdentityId(value) {
25
25
  return typeof value === "string" && IDENTITY_ID_PATTERN.test(value);
26
26
  }
27
27
  function decode(raw) {
28
- try {
29
- return decodeURIComponent(raw);
30
- } catch {
31
- return raw;
28
+ let text = raw;
29
+ for (let round = 0; round < 3 && /%[0-9A-Fa-f]{2}/.test(text); round++) {
30
+ try {
31
+ const next = decodeURIComponent(text);
32
+ if (next === text) break;
33
+ text = next;
34
+ } catch {
35
+ break;
36
+ }
37
+ if (text.startsWith("{")) break;
32
38
  }
39
+ return text;
33
40
  }
34
41
  function parseDeviceCookie(raw) {
35
42
  if (!raw) return null;
@@ -150,4 +157,4 @@ export {
150
157
  signSessionToken,
151
158
  verifySessionToken
152
159
  };
153
- //# sourceMappingURL=chunk-GCQZNME3.js.map
160
+ //# sourceMappingURL=chunk-FMOKJTQD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"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;\n /** Last identified profile. A hint for the server SDK, never trusted by ingest. */\n profileId?: string;\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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","names":[]}
@@ -1,10 +1,10 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkJB4MJQX7cjs = require('./chunk-JB4MJQX7.cjs');
3
+ var _chunkAOS53KAVcjs = require('./chunk-AOS53KAV.cjs');
4
4
 
5
5
  // src/identity.ts
6
6
  var SESSION_ID_HEADER = "reopt-session-id";
7
- var IDENTITY_REQUEST_HEADERS = [_chunkJB4MJQX7cjs.DEVICE_ID_HEADER, SESSION_ID_HEADER];
7
+ var IDENTITY_REQUEST_HEADERS = [_chunkAOS53KAVcjs.DEVICE_ID_HEADER, SESSION_ID_HEADER];
8
8
  var COOKIE_MAX_AGE_SECONDS = 400 * 24 * 60 * 60;
9
9
  var OPT_OUT_CONSENT_CATEGORY = "analytics";
10
10
  var COOKIE_PREFIX = "reopt_";
@@ -25,11 +25,18 @@ function isValidIdentityId(value) {
25
25
  return typeof value === "string" && IDENTITY_ID_PATTERN.test(value);
26
26
  }
27
27
  function decode(raw) {
28
- try {
29
- return decodeURIComponent(raw);
30
- } catch (e) {
31
- return raw;
28
+ let text = raw;
29
+ for (let round = 0; round < 3 && /%[0-9A-Fa-f]{2}/.test(text); round++) {
30
+ try {
31
+ const next = decodeURIComponent(text);
32
+ if (next === text) break;
33
+ text = next;
34
+ } catch (e) {
35
+ break;
36
+ }
37
+ if (text.startsWith("{")) break;
32
38
  }
39
+ return text;
33
40
  }
34
41
  function parseDeviceCookie(raw) {
35
42
  if (!raw) return null;
@@ -150,4 +157,4 @@ async function verifySessionToken(secret, credential) {
150
157
 
151
158
 
152
159
  exports.SESSION_ID_HEADER = SESSION_ID_HEADER; exports.IDENTITY_REQUEST_HEADERS = IDENTITY_REQUEST_HEADERS; exports.COOKIE_MAX_AGE_SECONDS = COOKIE_MAX_AGE_SECONDS; exports.OPT_OUT_CONSENT_CATEGORY = OPT_OUT_CONSENT_CATEGORY; exports.deviceCookieName = deviceCookieName; exports.consentCookieName = consentCookieName; exports.IDENTITY_ID_PATTERN = IDENTITY_ID_PATTERN; exports.isValidIdentityId = isValidIdentityId; exports.parseDeviceCookie = parseDeviceCookie; exports.serializeDeviceCookie = serializeDeviceCookie; exports.parseConsentCookie = parseConsentCookie; exports.serializeConsentCookie = serializeConsentCookie; exports.isOptedOut = isOptedOut; exports.SESSION_HEADER_SEPARATOR = SESSION_HEADER_SEPARATOR; exports.formatSessionHeader = formatSessionHeader; exports.parseSessionHeader = parseSessionHeader; exports.signSessionToken = signSessionToken; exports.verifySessionToken = verifySessionToken;
153
- //# sourceMappingURL=chunk-PHPTCLRU.cjs.map
160
+ //# sourceMappingURL=chunk-FQSCQSMP.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-FQSCQSMP.cjs","../src/identity.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACA;ACgCO,IAAM,kBAAA,EAAoB,kBAAA;AAG1B,IAAM,yBAAA,EAA2B,CAAC,kCAAA,EAAkB,iBAAiB,CAAA;AAMrE,IAAM,uBAAA,EAAyB,IAAA,EAAM,GAAA,EAAK,GAAA,EAAK,EAAA;AAG/C,IAAM,yBAAA,EAA2B,WAAA;AAExC,IAAM,cAAA,EAAgB,QAAA;AAOtB,SAAS,UAAA,CAAW,QAAA,EAA0B;AAC5C,EAAA,GAAA,CAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAM,IAAI,KAAA,CAAM,qDAAqD,CAAA;AAAA,EACvE;AACA,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,iBAAA,EAAmB,GAAG,CAAA;AAChD;AAUO,SAAS,gBAAA,CAAiB,QAAA,EAA0B;AACzD,EAAA,OAAO,CAAA,EAAA;AACT;AAGgB;AACP,EAAA;AACT;AAsCa;AAEG;AACP,EAAA;AACT;AAOS;AACH,EAAA;AACJ,EAAA;AACM,IAAA;AACF,MAAA;AACI,MAAA;AACJ,MAAA;AACF,IAAA;AACE,MAAA;AACF,IAAA;AACI,IAAA;AACN,EAAA;AACO,EAAA;AACT;AASgB;AACT,EAAA;AACC,EAAA;AACD,EAAA;AAEA,EAAA;AACH,IAAA;AACF,EAAA;AAEI,EAAA;AACA,EAAA;AACF,IAAA;AACM,EAAA;AACN,IAAA;AACF,EAAA;AACI,EAAA;AAEE,EAAA;AACD,EAAA;AAEC,EAAA;AACF,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAQgB;AACT,EAAA;AACG,IAAA;AACR,EAAA;AACM,EAAA;AACF,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAOgB;AACT,EAAA;AACC,EAAA;AACD,EAAA;AAED,EAAA;AACA,EAAA;AACF,IAAA;AACM,EAAA;AACN,IAAA;AACF,EAAA;AACI,EAAA;AAEE,EAAA;AACN,EAAA;AACM,IAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEgB;AACR,EAAA;AACN,EAAA;AACM,IAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAQgB;AACP,EAAA;AACT;AAgBa;AAQG;AACP,EAAA;AACT;AAGgB;AACT,EAAA;AACC,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACC,EAAA;AACT;AAEM;AAEG;AACH,EAAA;AACJ,EAAA;AACM,EAAA;AACC,EAAA;AACT;AAEA;AACS,EAAA;AACT;AAGA;AACO,EAAA;AACC,EAAA;AACC,EAAA;AACT;AAOA;AACO,EAAA;AACC,EAAA;AACF,EAAA;AACA,EAAA;AACJ,EAAA;AACE,IAAA;AACF,EAAA;AACO,EAAA;AACT;AD3KU;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-FQSCQSMP.cjs","sourcesContent":[null,"/**\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;\n /** Last identified profile. A hint for the server SDK, never trusted by ingest. */\n profileId?: string;\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"]}
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var CONTRACT_VERSION = "0.6.1";
2
+ var CONTRACT_VERSION = "0.6.2";
3
3
  var CONTRACT_VERSION_HEADER = "x-reopt-contract-version";
4
4
  var CLIENT_ID_HEADER = "reopt-client-id";
5
5
  var CLIENT_SECRET_HEADER = "reopt-client-secret";
@@ -16,4 +16,4 @@ export {
16
16
  DEVICE_ID_HEADER,
17
17
  REQUEST_ID_HEADER
18
18
  };
19
- //# sourceMappingURL=chunk-HLQAZ6JW.js.map
19
+ //# sourceMappingURL=chunk-ZCKVMUIQ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Wire version and header names — the part of the contract every party\n * needs and none of them should pay zod for. Both `.` and `./identity`\n * re-export these; browser bundles import them through `./identity` so the\n * schema modules stay out of the client graph.\n */\n\n/**\n * Contract version. Bumped on any breaking change to a schema in this package.\n * The server echoes the version it was built against in `x-reopt-contract-version`;\n * `createDataClient` surfaces a mismatch rather than guessing.\n *\n * Must equal the package version (`package-identity.test.ts` enforces it), so\n * every publish is a wire bump: consumers that check for exact equality —\n * every SDK — must upgrade in lockstep with the server.\n */\nexport const CONTRACT_VERSION = \"0.6.1\";\n\n/** Response header carrying the server's contract version. */\nexport const CONTRACT_VERSION_HEADER = \"x-reopt-contract-version\";\n\n/** Request header names shared by the ingest and query planes. */\nexport const CLIENT_ID_HEADER = \"reopt-client-id\";\nexport const CLIENT_SECRET_HEADER = \"reopt-client-secret\";\nexport const WRITE_KEY_HEADER = \"reopt-write-key\";\nexport const DEVICE_ID_HEADER = \"reopt-device-id\";\nexport const REQUEST_ID_HEADER = \"x-request-id\";\n"],"mappings":";AAgBO,IAAM,mBAAmB;AAGzB,IAAM,0BAA0B;AAGhC,IAAM,mBAAmB;AACzB,IAAM,uBAAuB;AAC7B,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,oBAAoB;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Wire version and header names — the part of the contract every party\n * needs and none of them should pay zod for. Both `.` and `./identity`\n * re-export these; browser bundles import them through `./identity` so the\n * schema modules stay out of the client graph.\n */\n\n/**\n * Contract version. Bumped on any breaking change to a schema in this package.\n * The server echoes the version it was built against in `x-reopt-contract-version`;\n * `createDataClient` surfaces a mismatch rather than guessing.\n *\n * Must equal the package version (`package-identity.test.ts` enforces it), so\n * every publish is a wire bump: consumers that check for exact equality —\n * every SDK — must upgrade in lockstep with the server.\n */\nexport const CONTRACT_VERSION = \"0.6.2\";\n\n/** Response header carrying the server's contract version. */\nexport const CONTRACT_VERSION_HEADER = \"x-reopt-contract-version\";\n\n/** Request header names shared by the ingest and query planes. */\nexport const CLIENT_ID_HEADER = \"reopt-client-id\";\nexport const CLIENT_SECRET_HEADER = \"reopt-client-secret\";\nexport const WRITE_KEY_HEADER = \"reopt-write-key\";\nexport const DEVICE_ID_HEADER = \"reopt-device-id\";\nexport const REQUEST_ID_HEADER = \"x-request-id\";\n"],"mappings":";AAgBO,IAAM,mBAAmB;AAGzB,IAAM,0BAA0B;AAGhC,IAAM,mBAAmB;AACzB,IAAM,uBAAuB;AAC7B,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,oBAAoB;","names":[]}
package/dist/client.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkVGB2VJO4cjs = require('./chunk-VGB2VJO4.cjs');
3
+ var _chunkBA52YA7Ncjs = require('./chunk-BA52YA7N.cjs');
4
4
 
5
5
 
6
6
 
@@ -24,14 +24,14 @@ var _chunkFUGY5ANJcjs = require('./chunk-FUGY5ANJ.cjs');
24
24
 
25
25
 
26
26
  var _chunkYSAEOYPDcjs = require('./chunk-YSAEOYPD.cjs');
27
- require('./chunk-PHPTCLRU.cjs');
27
+ require('./chunk-FQSCQSMP.cjs');
28
28
 
29
29
 
30
30
 
31
31
 
32
32
 
33
33
 
34
- var _chunkJB4MJQX7cjs = require('./chunk-JB4MJQX7.cjs');
34
+ var _chunkAOS53KAVcjs = require('./chunk-AOS53KAV.cjs');
35
35
  require('./chunk-3EEANNYN.cjs');
36
36
 
37
37
  // src/client.ts
@@ -114,7 +114,7 @@ function createDataClient(options) {
114
114
  maxDelayMs: _nullishCoalesce(_optionalChain([options, 'access', _11 => _11.retry, 'optionalAccess', _12 => _12.maxDelayMs]), () => ( DEFAULTS.maxDelayMs)),
115
115
  respectRetryAfter: _nullishCoalesce(_optionalChain([options, 'access', _13 => _13.retry, 'optionalAccess', _14 => _14.respectRetryAfter]), () => ( true))
116
116
  };
117
- const userAgent = options.userAgent ? `reopt-data-contract/${_chunkJB4MJQX7cjs.CONTRACT_VERSION} ${options.userAgent}` : `reopt-data-contract/${_chunkJB4MJQX7cjs.CONTRACT_VERSION}`;
117
+ const userAgent = options.userAgent ? `reopt-data-contract/${_chunkAOS53KAVcjs.CONTRACT_VERSION} ${options.userAgent}` : `reopt-data-contract/${_chunkAOS53KAVcjs.CONTRACT_VERSION}`;
118
118
  function requireClientCredentials() {
119
119
  if (!options.clientId || !options.clientSecret) {
120
120
  throw new (0, _chunkYSAEOYPDcjs.DataApiError)({
@@ -139,7 +139,7 @@ function createDataClient(options) {
139
139
  headers: {
140
140
  "content-type": "application/json",
141
141
  "user-agent": userAgent,
142
- ...options.clientId && options.clientSecret ? { [_chunkJB4MJQX7cjs.CLIENT_ID_HEADER]: options.clientId, [_chunkJB4MJQX7cjs.CLIENT_SECRET_HEADER]: options.clientSecret } : {},
142
+ ...options.clientId && options.clientSecret ? { [_chunkAOS53KAVcjs.CLIENT_ID_HEADER]: options.clientId, [_chunkAOS53KAVcjs.CLIENT_SECRET_HEADER]: options.clientSecret } : {},
143
143
  ...extraHeaders
144
144
  },
145
145
  body: JSON.stringify(body),
@@ -170,16 +170,16 @@ function createDataClient(options) {
170
170
  }
171
171
  throw lastError;
172
172
  }
173
- const requestId = _nullishCoalesce(response.headers.get(_chunkJB4MJQX7cjs.REQUEST_ID_HEADER), () => ( void 0));
174
- const serverContractVersion = response.headers.get(_chunkJB4MJQX7cjs.CONTRACT_VERSION_HEADER);
173
+ const requestId = _nullishCoalesce(response.headers.get(_chunkAOS53KAVcjs.REQUEST_ID_HEADER), () => ( void 0));
174
+ const serverContractVersion = response.headers.get(_chunkAOS53KAVcjs.CONTRACT_VERSION_HEADER);
175
175
  const rawBody = await response.text();
176
- if (serverContractVersion !== _chunkJB4MJQX7cjs.CONTRACT_VERSION) {
176
+ if (serverContractVersion !== _chunkAOS53KAVcjs.CONTRACT_VERSION) {
177
177
  throw new (0, _chunkYSAEOYPDcjs.DataApiError)({
178
- message: serverContractVersion ? `reopt-data contract ${serverContractVersion} does not match client contract ${_chunkJB4MJQX7cjs.CONTRACT_VERSION}` : "reopt-data response did not declare its contract version",
178
+ message: serverContractVersion ? `reopt-data contract ${serverContractVersion} does not match client contract ${_chunkAOS53KAVcjs.CONTRACT_VERSION}` : "reopt-data response did not declare its contract version",
179
179
  status: response.status,
180
180
  code: "contract_mismatch",
181
181
  requestId,
182
- details: { expected: _chunkJB4MJQX7cjs.CONTRACT_VERSION, received: serverContractVersion }
182
+ details: { expected: _chunkAOS53KAVcjs.CONTRACT_VERSION, received: serverContractVersion }
183
183
  });
184
184
  }
185
185
  if (response.ok) {
@@ -221,7 +221,7 @@ function createDataClient(options) {
221
221
  return {
222
222
  ingest: {
223
223
  track(events, init) {
224
- return post(TRACK_PATH, events, (value) => _chunkVGB2VJO4cjs.zIngestResponse.parse(value), _optionalChain([init, 'optionalAccess', _21 => _21.signal]));
224
+ return post(TRACK_PATH, events, (value) => _chunkBA52YA7Ncjs.zIngestResponse.parse(value), _optionalChain([init, 'optionalAccess', _21 => _21.signal]));
225
225
  }
226
226
  },
227
227
  query: {
package/dist/client.d.cts CHANGED
@@ -3,7 +3,7 @@ import { ITrackHandlerPayload, IngestResponse } from './ingest.cjs';
3
3
  import { CreateOrganizationInput, CreateOrganizationResponse, RotateOrganizationKeyResponse, UpdateOrganizationQuotaInput, UpdateOrganizationQuotaResponse, CreateProjectInput, CreateProjectResponse, RotateClientSecretResponse, DeleteProjectInput, DeleteProjectResponse, UpsertEventMetaInput, EventMetaResponse } from './control.cjs';
4
4
  import { EventsTimeseriesInput, EventsTimeseriesResponse, FunnelInput, FunnelResponse, RetentionInput, RetentionResponse, TrafficOverviewInput, TrafficOverviewResponse, TrafficSourcesInput, TrafficSourcesResponse } from './query.cjs';
5
5
  import 'zod';
6
- import './version-BzeRTpta.cjs';
6
+ import './version-BANKB6Og.cjs';
7
7
  import './events.cjs';
8
8
 
9
9
  /**
package/dist/client.d.ts CHANGED
@@ -3,7 +3,7 @@ import { ITrackHandlerPayload, IngestResponse } from './ingest.js';
3
3
  import { CreateOrganizationInput, CreateOrganizationResponse, RotateOrganizationKeyResponse, UpdateOrganizationQuotaInput, UpdateOrganizationQuotaResponse, CreateProjectInput, CreateProjectResponse, RotateClientSecretResponse, DeleteProjectInput, DeleteProjectResponse, UpsertEventMetaInput, EventMetaResponse } from './control.js';
4
4
  import { EventsTimeseriesInput, EventsTimeseriesResponse, FunnelInput, FunnelResponse, RetentionInput, RetentionResponse, TrafficOverviewInput, TrafficOverviewResponse, TrafficSourcesInput, TrafficSourcesResponse } from './query.js';
5
5
  import 'zod';
6
- import './version-BzeRTpta.js';
6
+ import './version-BANKB6Og.js';
7
7
  import './events.js';
8
8
 
9
9
  /**
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  zIngestResponse
3
- } from "./chunk-EZ2TA4S6.js";
3
+ } from "./chunk-DUJSYMYY.js";
4
4
  import {
5
5
  CONTROL_API_PATHS,
6
6
  ORG_KEY_HEADER,
@@ -24,14 +24,14 @@ import {
24
24
  import {
25
25
  DataApiError
26
26
  } from "./chunk-33BCA2LH.js";
27
- import "./chunk-GCQZNME3.js";
27
+ import "./chunk-FMOKJTQD.js";
28
28
  import {
29
29
  CLIENT_ID_HEADER,
30
30
  CLIENT_SECRET_HEADER,
31
31
  CONTRACT_VERSION,
32
32
  CONTRACT_VERSION_HEADER,
33
33
  REQUEST_ID_HEADER
34
- } from "./chunk-HLQAZ6JW.js";
34
+ } from "./chunk-ZCKVMUIQ.js";
35
35
  import "./chunk-FP2X3PRK.js";
36
36
 
37
37
  // src/client.ts
package/dist/control.cjs CHANGED
@@ -26,7 +26,7 @@
26
26
  var _chunkLA5F5KR6cjs = require('./chunk-LA5F5KR6.cjs');
27
27
  require('./chunk-FUGY5ANJ.cjs');
28
28
  require('./chunk-YSAEOYPD.cjs');
29
- require('./chunk-JB4MJQX7.cjs');
29
+ require('./chunk-AOS53KAV.cjs');
30
30
 
31
31
 
32
32
 
@@ -181,27 +181,27 @@ declare const zEventMetaResponse: z.ZodObject<{
181
181
  type EventMetaResponse = z.infer<typeof zEventMetaResponse>;
182
182
  declare const CONTROL_ERROR_CODES: readonly ["unauthorized", "organization_scope_mismatch", "external_id_conflict", "project_external_id_mismatch", "not_found", "validation_failed", "rate_limited", "internal_error"];
183
183
  declare const zControlErrorCode: z.ZodEnum<{
184
- validation_failed: "validation_failed";
185
184
  unauthorized: "unauthorized";
186
- rate_limited: "rate_limited";
187
- internal_error: "internal_error";
188
185
  organization_scope_mismatch: "organization_scope_mismatch";
189
186
  external_id_conflict: "external_id_conflict";
190
187
  project_external_id_mismatch: "project_external_id_mismatch";
191
188
  not_found: "not_found";
189
+ validation_failed: "validation_failed";
190
+ rate_limited: "rate_limited";
191
+ internal_error: "internal_error";
192
192
  }>;
193
193
  type ControlErrorCode = z.infer<typeof zControlErrorCode>;
194
194
  declare const zControlError: z.ZodObject<{
195
195
  status: z.ZodNumber;
196
196
  code: z.ZodEnum<{
197
- validation_failed: "validation_failed";
198
197
  unauthorized: "unauthorized";
199
- rate_limited: "rate_limited";
200
- internal_error: "internal_error";
201
198
  organization_scope_mismatch: "organization_scope_mismatch";
202
199
  external_id_conflict: "external_id_conflict";
203
200
  project_external_id_mismatch: "project_external_id_mismatch";
204
201
  not_found: "not_found";
202
+ validation_failed: "validation_failed";
203
+ rate_limited: "rate_limited";
204
+ internal_error: "internal_error";
205
205
  }>;
206
206
  error: z.ZodString;
207
207
  message: z.ZodOptional<z.ZodString>;
package/dist/control.d.ts CHANGED
@@ -181,27 +181,27 @@ declare const zEventMetaResponse: z.ZodObject<{
181
181
  type EventMetaResponse = z.infer<typeof zEventMetaResponse>;
182
182
  declare const CONTROL_ERROR_CODES: readonly ["unauthorized", "organization_scope_mismatch", "external_id_conflict", "project_external_id_mismatch", "not_found", "validation_failed", "rate_limited", "internal_error"];
183
183
  declare const zControlErrorCode: z.ZodEnum<{
184
- validation_failed: "validation_failed";
185
184
  unauthorized: "unauthorized";
186
- rate_limited: "rate_limited";
187
- internal_error: "internal_error";
188
185
  organization_scope_mismatch: "organization_scope_mismatch";
189
186
  external_id_conflict: "external_id_conflict";
190
187
  project_external_id_mismatch: "project_external_id_mismatch";
191
188
  not_found: "not_found";
189
+ validation_failed: "validation_failed";
190
+ rate_limited: "rate_limited";
191
+ internal_error: "internal_error";
192
192
  }>;
193
193
  type ControlErrorCode = z.infer<typeof zControlErrorCode>;
194
194
  declare const zControlError: z.ZodObject<{
195
195
  status: z.ZodNumber;
196
196
  code: z.ZodEnum<{
197
- validation_failed: "validation_failed";
198
197
  unauthorized: "unauthorized";
199
- rate_limited: "rate_limited";
200
- internal_error: "internal_error";
201
198
  organization_scope_mismatch: "organization_scope_mismatch";
202
199
  external_id_conflict: "external_id_conflict";
203
200
  project_external_id_mismatch: "project_external_id_mismatch";
204
201
  not_found: "not_found";
202
+ validation_failed: "validation_failed";
203
+ rate_limited: "rate_limited";
204
+ internal_error: "internal_error";
205
205
  }>;
206
206
  error: z.ZodString;
207
207
  message: z.ZodOptional<z.ZodString>;
package/dist/control.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  } from "./chunk-IY4MXH35.js";
27
27
  import "./chunk-L3WJGIKD.js";
28
28
  import "./chunk-33BCA2LH.js";
29
- import "./chunk-HLQAZ6JW.js";
29
+ import "./chunk-ZCKVMUIQ.js";
30
30
  export {
31
31
  CONTROL_API_PATHS,
32
32
  CONTROL_ERROR_CODES,
package/dist/identity.cjs CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
 
19
19
 
20
- var _chunkPHPTCLRUcjs = require('./chunk-PHPTCLRU.cjs');
20
+ var _chunkFQSCQSMPcjs = require('./chunk-FQSCQSMP.cjs');
21
21
 
22
22
 
23
23
 
@@ -26,7 +26,7 @@ var _chunkPHPTCLRUcjs = require('./chunk-PHPTCLRU.cjs');
26
26
 
27
27
 
28
28
 
29
- var _chunkJB4MJQX7cjs = require('./chunk-JB4MJQX7.cjs');
29
+ var _chunkAOS53KAVcjs = require('./chunk-AOS53KAV.cjs');
30
30
 
31
31
 
32
32
 
@@ -53,5 +53,5 @@ var _chunkJB4MJQX7cjs = require('./chunk-JB4MJQX7.cjs');
53
53
 
54
54
 
55
55
 
56
- exports.CLIENT_ID_HEADER = _chunkJB4MJQX7cjs.CLIENT_ID_HEADER; exports.CLIENT_SECRET_HEADER = _chunkJB4MJQX7cjs.CLIENT_SECRET_HEADER; exports.CONTRACT_VERSION = _chunkJB4MJQX7cjs.CONTRACT_VERSION; exports.CONTRACT_VERSION_HEADER = _chunkJB4MJQX7cjs.CONTRACT_VERSION_HEADER; exports.COOKIE_MAX_AGE_SECONDS = _chunkPHPTCLRUcjs.COOKIE_MAX_AGE_SECONDS; exports.DEVICE_ID_HEADER = _chunkJB4MJQX7cjs.DEVICE_ID_HEADER; exports.IDENTITY_ID_PATTERN = _chunkPHPTCLRUcjs.IDENTITY_ID_PATTERN; exports.IDENTITY_REQUEST_HEADERS = _chunkPHPTCLRUcjs.IDENTITY_REQUEST_HEADERS; exports.OPT_OUT_CONSENT_CATEGORY = _chunkPHPTCLRUcjs.OPT_OUT_CONSENT_CATEGORY; exports.REQUEST_ID_HEADER = _chunkJB4MJQX7cjs.REQUEST_ID_HEADER; exports.SESSION_HEADER_SEPARATOR = _chunkPHPTCLRUcjs.SESSION_HEADER_SEPARATOR; exports.SESSION_ID_HEADER = _chunkPHPTCLRUcjs.SESSION_ID_HEADER; exports.WRITE_KEY_HEADER = _chunkJB4MJQX7cjs.WRITE_KEY_HEADER; exports.consentCookieName = _chunkPHPTCLRUcjs.consentCookieName; exports.deviceCookieName = _chunkPHPTCLRUcjs.deviceCookieName; exports.formatSessionHeader = _chunkPHPTCLRUcjs.formatSessionHeader; exports.isOptedOut = _chunkPHPTCLRUcjs.isOptedOut; exports.isValidIdentityId = _chunkPHPTCLRUcjs.isValidIdentityId; exports.parseConsentCookie = _chunkPHPTCLRUcjs.parseConsentCookie; exports.parseDeviceCookie = _chunkPHPTCLRUcjs.parseDeviceCookie; exports.parseSessionHeader = _chunkPHPTCLRUcjs.parseSessionHeader; exports.serializeConsentCookie = _chunkPHPTCLRUcjs.serializeConsentCookie; exports.serializeDeviceCookie = _chunkPHPTCLRUcjs.serializeDeviceCookie; exports.signSessionToken = _chunkPHPTCLRUcjs.signSessionToken; exports.verifySessionToken = _chunkPHPTCLRUcjs.verifySessionToken;
56
+ exports.CLIENT_ID_HEADER = _chunkAOS53KAVcjs.CLIENT_ID_HEADER; exports.CLIENT_SECRET_HEADER = _chunkAOS53KAVcjs.CLIENT_SECRET_HEADER; exports.CONTRACT_VERSION = _chunkAOS53KAVcjs.CONTRACT_VERSION; exports.CONTRACT_VERSION_HEADER = _chunkAOS53KAVcjs.CONTRACT_VERSION_HEADER; exports.COOKIE_MAX_AGE_SECONDS = _chunkFQSCQSMPcjs.COOKIE_MAX_AGE_SECONDS; exports.DEVICE_ID_HEADER = _chunkAOS53KAVcjs.DEVICE_ID_HEADER; exports.IDENTITY_ID_PATTERN = _chunkFQSCQSMPcjs.IDENTITY_ID_PATTERN; exports.IDENTITY_REQUEST_HEADERS = _chunkFQSCQSMPcjs.IDENTITY_REQUEST_HEADERS; exports.OPT_OUT_CONSENT_CATEGORY = _chunkFQSCQSMPcjs.OPT_OUT_CONSENT_CATEGORY; exports.REQUEST_ID_HEADER = _chunkAOS53KAVcjs.REQUEST_ID_HEADER; exports.SESSION_HEADER_SEPARATOR = _chunkFQSCQSMPcjs.SESSION_HEADER_SEPARATOR; exports.SESSION_ID_HEADER = _chunkFQSCQSMPcjs.SESSION_ID_HEADER; exports.WRITE_KEY_HEADER = _chunkAOS53KAVcjs.WRITE_KEY_HEADER; exports.consentCookieName = _chunkFQSCQSMPcjs.consentCookieName; exports.deviceCookieName = _chunkFQSCQSMPcjs.deviceCookieName; exports.formatSessionHeader = _chunkFQSCQSMPcjs.formatSessionHeader; exports.isOptedOut = _chunkFQSCQSMPcjs.isOptedOut; exports.isValidIdentityId = _chunkFQSCQSMPcjs.isValidIdentityId; exports.parseConsentCookie = _chunkFQSCQSMPcjs.parseConsentCookie; exports.parseDeviceCookie = _chunkFQSCQSMPcjs.parseDeviceCookie; exports.parseSessionHeader = _chunkFQSCQSMPcjs.parseSessionHeader; exports.serializeConsentCookie = _chunkFQSCQSMPcjs.serializeConsentCookie; exports.serializeDeviceCookie = _chunkFQSCQSMPcjs.serializeDeviceCookie; exports.signSessionToken = _chunkFQSCQSMPcjs.signSessionToken; exports.verifySessionToken = _chunkFQSCQSMPcjs.verifySessionToken;
57
57
  //# sourceMappingURL=identity.cjs.map
@@ -1,4 +1,4 @@
1
- export { C as CLIENT_ID_HEADER, a as CLIENT_SECRET_HEADER, b as CONTRACT_VERSION, c as CONTRACT_VERSION_HEADER, D as DEVICE_ID_HEADER, R as REQUEST_ID_HEADER, W as WRITE_KEY_HEADER } from './version-BzeRTpta.cjs';
1
+ export { C as CLIENT_ID_HEADER, a as CLIENT_SECRET_HEADER, b as CONTRACT_VERSION, c as CONTRACT_VERSION_HEADER, D as DEVICE_ID_HEADER, R as REQUEST_ID_HEADER, W as WRITE_KEY_HEADER } from './version-BANKB6Og.cjs';
2
2
 
3
3
  /**
4
4
  * Request header carrying the browser's current session id. The browser SDK
@@ -18,9 +18,16 @@ declare const IDENTITY_REQUEST_HEADERS: readonly ["reopt-device-id", "reopt-sess
18
18
  declare const COOKIE_MAX_AGE_SECONDS: number;
19
19
  /** Consent category whose refusal means "send nothing at all". */
20
20
  declare const OPT_OUT_CONSENT_CATEGORY = "analytics";
21
- /** `reopt_<writeKey>_device` — device id plus session/profile hints. */
21
+ /**
22
+ * `reopt_<writeKey>_device` — device id plus session/profile hints.
23
+ *
24
+ * The write key is **sanitized**: any character outside `[A-Za-z0-9_-]`
25
+ * becomes `_` (see `cookieSafe`), so do not derive prefix/suffix pieces by
26
+ * probing this with a marker character — the marker is folded too. Always
27
+ * call this with the real write key and compare whole names.
28
+ */
22
29
  declare function deviceCookieName(writeKey: string): string;
23
- /** `reopt_<writeKey>_consent` — per-category consent decisions. */
30
+ /** `reopt_<writeKey>_consent` — per-category consent decisions. Same sanitizing as `deviceCookieName`. */
24
31
  declare function consentCookieName(writeKey: string): string;
25
32
  /** Everything the device cookie may carry. Only `deviceId` is required. */
26
33
  interface DeviceCookieState {
@@ -1,4 +1,4 @@
1
- export { C as CLIENT_ID_HEADER, a as CLIENT_SECRET_HEADER, b as CONTRACT_VERSION, c as CONTRACT_VERSION_HEADER, D as DEVICE_ID_HEADER, R as REQUEST_ID_HEADER, W as WRITE_KEY_HEADER } from './version-BzeRTpta.js';
1
+ export { C as CLIENT_ID_HEADER, a as CLIENT_SECRET_HEADER, b as CONTRACT_VERSION, c as CONTRACT_VERSION_HEADER, D as DEVICE_ID_HEADER, R as REQUEST_ID_HEADER, W as WRITE_KEY_HEADER } from './version-BANKB6Og.js';
2
2
 
3
3
  /**
4
4
  * Request header carrying the browser's current session id. The browser SDK
@@ -18,9 +18,16 @@ declare const IDENTITY_REQUEST_HEADERS: readonly ["reopt-device-id", "reopt-sess
18
18
  declare const COOKIE_MAX_AGE_SECONDS: number;
19
19
  /** Consent category whose refusal means "send nothing at all". */
20
20
  declare const OPT_OUT_CONSENT_CATEGORY = "analytics";
21
- /** `reopt_<writeKey>_device` — device id plus session/profile hints. */
21
+ /**
22
+ * `reopt_<writeKey>_device` — device id plus session/profile hints.
23
+ *
24
+ * The write key is **sanitized**: any character outside `[A-Za-z0-9_-]`
25
+ * becomes `_` (see `cookieSafe`), so do not derive prefix/suffix pieces by
26
+ * probing this with a marker character — the marker is folded too. Always
27
+ * call this with the real write key and compare whole names.
28
+ */
22
29
  declare function deviceCookieName(writeKey: string): string;
23
- /** `reopt_<writeKey>_consent` — per-category consent decisions. */
30
+ /** `reopt_<writeKey>_consent` — per-category consent decisions. Same sanitizing as `deviceCookieName`. */
24
31
  declare function consentCookieName(writeKey: string): string;
25
32
  /** Everything the device cookie may carry. Only `deviceId` is required. */
26
33
  interface DeviceCookieState {
package/dist/identity.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  serializeDeviceCookie,
18
18
  signSessionToken,
19
19
  verifySessionToken
20
- } from "./chunk-GCQZNME3.js";
20
+ } from "./chunk-FMOKJTQD.js";
21
21
  import {
22
22
  CLIENT_ID_HEADER,
23
23
  CLIENT_SECRET_HEADER,
@@ -26,7 +26,7 @@ import {
26
26
  DEVICE_ID_HEADER,
27
27
  REQUEST_ID_HEADER,
28
28
  WRITE_KEY_HEADER
29
- } from "./chunk-HLQAZ6JW.js";
29
+ } from "./chunk-ZCKVMUIQ.js";
30
30
  export {
31
31
  CLIENT_ID_HEADER,
32
32
  CLIENT_SECRET_HEADER,
package/dist/index.cjs CHANGED
@@ -11,7 +11,7 @@ var _chunkYSAEOYPDcjs = require('./chunk-YSAEOYPD.cjs');
11
11
 
12
12
 
13
13
 
14
- var _chunkJB4MJQX7cjs = require('./chunk-JB4MJQX7.cjs');
14
+ var _chunkAOS53KAVcjs = require('./chunk-AOS53KAV.cjs');
15
15
 
16
16
 
17
17
 
@@ -23,5 +23,5 @@ var _chunkJB4MJQX7cjs = require('./chunk-JB4MJQX7.cjs');
23
23
 
24
24
 
25
25
 
26
- exports.CLIENT_ID_HEADER = _chunkJB4MJQX7cjs.CLIENT_ID_HEADER; exports.CLIENT_SECRET_HEADER = _chunkJB4MJQX7cjs.CLIENT_SECRET_HEADER; exports.CONTRACT_VERSION = _chunkJB4MJQX7cjs.CONTRACT_VERSION; exports.CONTRACT_VERSION_HEADER = _chunkJB4MJQX7cjs.CONTRACT_VERSION_HEADER; exports.DEVICE_ID_HEADER = _chunkJB4MJQX7cjs.DEVICE_ID_HEADER; exports.DataApiError = _chunkYSAEOYPDcjs.DataApiError; exports.REQUEST_ID_HEADER = _chunkJB4MJQX7cjs.REQUEST_ID_HEADER; exports.WRITE_KEY_HEADER = _chunkJB4MJQX7cjs.WRITE_KEY_HEADER; exports.zDate = _chunkYSAEOYPDcjs.zDate; exports.zInstant = _chunkYSAEOYPDcjs.zInstant;
26
+ exports.CLIENT_ID_HEADER = _chunkAOS53KAVcjs.CLIENT_ID_HEADER; exports.CLIENT_SECRET_HEADER = _chunkAOS53KAVcjs.CLIENT_SECRET_HEADER; exports.CONTRACT_VERSION = _chunkAOS53KAVcjs.CONTRACT_VERSION; exports.CONTRACT_VERSION_HEADER = _chunkAOS53KAVcjs.CONTRACT_VERSION_HEADER; exports.DEVICE_ID_HEADER = _chunkAOS53KAVcjs.DEVICE_ID_HEADER; exports.DataApiError = _chunkYSAEOYPDcjs.DataApiError; exports.REQUEST_ID_HEADER = _chunkAOS53KAVcjs.REQUEST_ID_HEADER; exports.WRITE_KEY_HEADER = _chunkAOS53KAVcjs.WRITE_KEY_HEADER; exports.zDate = _chunkYSAEOYPDcjs.zDate; exports.zInstant = _chunkYSAEOYPDcjs.zInstant;
27
27
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- export { C as CLIENT_ID_HEADER, a as CLIENT_SECRET_HEADER, b as CONTRACT_VERSION, c as CONTRACT_VERSION_HEADER, D as DEVICE_ID_HEADER, R as REQUEST_ID_HEADER, W as WRITE_KEY_HEADER } from './version-BzeRTpta.cjs';
2
+ export { C as CLIENT_ID_HEADER, a as CLIENT_SECRET_HEADER, b as CONTRACT_VERSION, c as CONTRACT_VERSION_HEADER, D as DEVICE_ID_HEADER, R as REQUEST_ID_HEADER, W as WRITE_KEY_HEADER } from './version-BANKB6Og.cjs';
3
3
 
4
4
  /**
5
5
  * `@reopt-ai/data-contract` — the wire contract between reopt-data and its
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- export { C as CLIENT_ID_HEADER, a as CLIENT_SECRET_HEADER, b as CONTRACT_VERSION, c as CONTRACT_VERSION_HEADER, D as DEVICE_ID_HEADER, R as REQUEST_ID_HEADER, W as WRITE_KEY_HEADER } from './version-BzeRTpta.js';
2
+ export { C as CLIENT_ID_HEADER, a as CLIENT_SECRET_HEADER, b as CONTRACT_VERSION, c as CONTRACT_VERSION_HEADER, D as DEVICE_ID_HEADER, R as REQUEST_ID_HEADER, W as WRITE_KEY_HEADER } from './version-BANKB6Og.js';
3
3
 
4
4
  /**
5
5
  * `@reopt-ai/data-contract` — the wire contract between reopt-data and its
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  DEVICE_ID_HEADER,
12
12
  REQUEST_ID_HEADER,
13
13
  WRITE_KEY_HEADER
14
- } from "./chunk-HLQAZ6JW.js";
14
+ } from "./chunk-ZCKVMUIQ.js";
15
15
  export {
16
16
  CLIENT_ID_HEADER,
17
17
  CLIENT_SECRET_HEADER,
package/dist/ingest.cjs CHANGED
@@ -18,9 +18,9 @@
18
18
 
19
19
 
20
20
 
21
- var _chunkVGB2VJO4cjs = require('./chunk-VGB2VJO4.cjs');
22
- require('./chunk-PHPTCLRU.cjs');
23
- require('./chunk-JB4MJQX7.cjs');
21
+ var _chunkBA52YA7Ncjs = require('./chunk-BA52YA7N.cjs');
22
+ require('./chunk-FQSCQSMP.cjs');
23
+ require('./chunk-AOS53KAV.cjs');
24
24
 
25
25
 
26
26
 
@@ -55,5 +55,5 @@ var _chunk3EEANNYNcjs = require('./chunk-3EEANNYN.cjs');
55
55
 
56
56
 
57
57
 
58
- exports.AUTO_EVENT_NAMES = _chunk3EEANNYNcjs.AUTO_EVENT_NAMES; exports.AUTO_EVENT_PROPERTIES = _chunk3EEANNYNcjs.AUTO_EVENT_PROPERTIES; exports.AUTO_EVENT_ROLLUP_KEYS = _chunk3EEANNYNcjs.AUTO_EVENT_ROLLUP_KEYS; exports.INGEST_ERROR_CODES = _chunkVGB2VJO4cjs.INGEST_ERROR_CODES; exports.INGEST_MODES = _chunkVGB2VJO4cjs.INGEST_MODES; exports.INGEST_REJECTION_REASONS = _chunkVGB2VJO4cjs.INGEST_REJECTION_REASONS; exports.MAX_TRACK_PAYLOAD_BYTES = _chunkVGB2VJO4cjs.MAX_TRACK_PAYLOAD_BYTES; exports.RESERVED_EVENT_NAMES = _chunk3EEANNYNcjs.RESERVED_EVENT_NAMES; exports.SCROLL_DEPTH_BUCKETS = _chunk3EEANNYNcjs.SCROLL_DEPTH_BUCKETS; exports.reconcileIngestResponse = _chunkVGB2VJO4cjs.reconcileIngestResponse; exports.scrollDepthBucket = _chunk3EEANNYNcjs.scrollDepthBucket; exports.zDecrementPayload = _chunkVGB2VJO4cjs.zDecrementPayload; exports.zIdentifyPayload = _chunkVGB2VJO4cjs.zIdentifyPayload; exports.zIncrementPayload = _chunkVGB2VJO4cjs.zIncrementPayload; exports.zIngestAcceptedResponse = _chunkVGB2VJO4cjs.zIngestAcceptedResponse; exports.zIngestError = _chunkVGB2VJO4cjs.zIngestError; exports.zIngestErrorCode = _chunkVGB2VJO4cjs.zIngestErrorCode; exports.zIngestMode = _chunkVGB2VJO4cjs.zIngestMode; exports.zIngestOkResponse = _chunkVGB2VJO4cjs.zIngestOkResponse; exports.zIngestRejection = _chunkVGB2VJO4cjs.zIngestRejection; exports.zIngestRejectionReason = _chunkVGB2VJO4cjs.zIngestRejectionReason; exports.zIngestResponse = _chunkVGB2VJO4cjs.zIngestResponse; exports.zSessionCredential = _chunkVGB2VJO4cjs.zSessionCredential; exports.zTrackHandlerPayload = _chunkVGB2VJO4cjs.zTrackHandlerPayload; exports.zTrackPayload = _chunkVGB2VJO4cjs.zTrackPayload;
58
+ exports.AUTO_EVENT_NAMES = _chunk3EEANNYNcjs.AUTO_EVENT_NAMES; exports.AUTO_EVENT_PROPERTIES = _chunk3EEANNYNcjs.AUTO_EVENT_PROPERTIES; exports.AUTO_EVENT_ROLLUP_KEYS = _chunk3EEANNYNcjs.AUTO_EVENT_ROLLUP_KEYS; exports.INGEST_ERROR_CODES = _chunkBA52YA7Ncjs.INGEST_ERROR_CODES; exports.INGEST_MODES = _chunkBA52YA7Ncjs.INGEST_MODES; exports.INGEST_REJECTION_REASONS = _chunkBA52YA7Ncjs.INGEST_REJECTION_REASONS; exports.MAX_TRACK_PAYLOAD_BYTES = _chunkBA52YA7Ncjs.MAX_TRACK_PAYLOAD_BYTES; exports.RESERVED_EVENT_NAMES = _chunk3EEANNYNcjs.RESERVED_EVENT_NAMES; exports.SCROLL_DEPTH_BUCKETS = _chunk3EEANNYNcjs.SCROLL_DEPTH_BUCKETS; exports.reconcileIngestResponse = _chunkBA52YA7Ncjs.reconcileIngestResponse; exports.scrollDepthBucket = _chunk3EEANNYNcjs.scrollDepthBucket; exports.zDecrementPayload = _chunkBA52YA7Ncjs.zDecrementPayload; exports.zIdentifyPayload = _chunkBA52YA7Ncjs.zIdentifyPayload; exports.zIncrementPayload = _chunkBA52YA7Ncjs.zIncrementPayload; exports.zIngestAcceptedResponse = _chunkBA52YA7Ncjs.zIngestAcceptedResponse; exports.zIngestError = _chunkBA52YA7Ncjs.zIngestError; exports.zIngestErrorCode = _chunkBA52YA7Ncjs.zIngestErrorCode; exports.zIngestMode = _chunkBA52YA7Ncjs.zIngestMode; exports.zIngestOkResponse = _chunkBA52YA7Ncjs.zIngestOkResponse; exports.zIngestRejection = _chunkBA52YA7Ncjs.zIngestRejection; exports.zIngestRejectionReason = _chunkBA52YA7Ncjs.zIngestRejectionReason; exports.zIngestResponse = _chunkBA52YA7Ncjs.zIngestResponse; exports.zSessionCredential = _chunkBA52YA7Ncjs.zSessionCredential; exports.zTrackHandlerPayload = _chunkBA52YA7Ncjs.zTrackHandlerPayload; exports.zTrackPayload = _chunkBA52YA7Ncjs.zTrackPayload;
59
59
  //# sourceMappingURL=ingest.cjs.map
package/dist/ingest.d.cts CHANGED
@@ -264,33 +264,33 @@ declare function reconcileIngestResponse(response: IngestResponse, sentCount: nu
264
264
  */
265
265
  declare const INGEST_ERROR_CODES: readonly ["unauthorized", "invalid_json", "payload_too_large", "empty_batch", "alias_unsupported", "project_without_organization", "validation_failed", "rate_limited", "quota_exceeded", "project_purging", "internal_error"];
266
266
  declare const zIngestErrorCode: z.ZodEnum<{
267
- validation_failed: "validation_failed";
268
267
  unauthorized: "unauthorized";
268
+ validation_failed: "validation_failed";
269
+ rate_limited: "rate_limited";
270
+ internal_error: "internal_error";
269
271
  invalid_json: "invalid_json";
270
272
  payload_too_large: "payload_too_large";
271
273
  empty_batch: "empty_batch";
272
274
  alias_unsupported: "alias_unsupported";
273
275
  project_without_organization: "project_without_organization";
274
- rate_limited: "rate_limited";
275
276
  quota_exceeded: "quota_exceeded";
276
277
  project_purging: "project_purging";
277
- internal_error: "internal_error";
278
278
  }>;
279
279
  type IngestErrorCode = z.infer<typeof zIngestErrorCode>;
280
280
  declare const zIngestError: z.ZodObject<{
281
281
  status: z.ZodNumber;
282
282
  code: z.ZodEnum<{
283
- validation_failed: "validation_failed";
284
283
  unauthorized: "unauthorized";
284
+ validation_failed: "validation_failed";
285
+ rate_limited: "rate_limited";
286
+ internal_error: "internal_error";
285
287
  invalid_json: "invalid_json";
286
288
  payload_too_large: "payload_too_large";
287
289
  empty_batch: "empty_batch";
288
290
  alias_unsupported: "alias_unsupported";
289
291
  project_without_organization: "project_without_organization";
290
- rate_limited: "rate_limited";
291
292
  quota_exceeded: "quota_exceeded";
292
293
  project_purging: "project_purging";
293
- internal_error: "internal_error";
294
294
  }>;
295
295
  error: z.ZodString;
296
296
  message: z.ZodOptional<z.ZodString>;
package/dist/ingest.d.ts CHANGED
@@ -264,33 +264,33 @@ declare function reconcileIngestResponse(response: IngestResponse, sentCount: nu
264
264
  */
265
265
  declare const INGEST_ERROR_CODES: readonly ["unauthorized", "invalid_json", "payload_too_large", "empty_batch", "alias_unsupported", "project_without_organization", "validation_failed", "rate_limited", "quota_exceeded", "project_purging", "internal_error"];
266
266
  declare const zIngestErrorCode: z.ZodEnum<{
267
- validation_failed: "validation_failed";
268
267
  unauthorized: "unauthorized";
268
+ validation_failed: "validation_failed";
269
+ rate_limited: "rate_limited";
270
+ internal_error: "internal_error";
269
271
  invalid_json: "invalid_json";
270
272
  payload_too_large: "payload_too_large";
271
273
  empty_batch: "empty_batch";
272
274
  alias_unsupported: "alias_unsupported";
273
275
  project_without_organization: "project_without_organization";
274
- rate_limited: "rate_limited";
275
276
  quota_exceeded: "quota_exceeded";
276
277
  project_purging: "project_purging";
277
- internal_error: "internal_error";
278
278
  }>;
279
279
  type IngestErrorCode = z.infer<typeof zIngestErrorCode>;
280
280
  declare const zIngestError: z.ZodObject<{
281
281
  status: z.ZodNumber;
282
282
  code: z.ZodEnum<{
283
- validation_failed: "validation_failed";
284
283
  unauthorized: "unauthorized";
284
+ validation_failed: "validation_failed";
285
+ rate_limited: "rate_limited";
286
+ internal_error: "internal_error";
285
287
  invalid_json: "invalid_json";
286
288
  payload_too_large: "payload_too_large";
287
289
  empty_batch: "empty_batch";
288
290
  alias_unsupported: "alias_unsupported";
289
291
  project_without_organization: "project_without_organization";
290
- rate_limited: "rate_limited";
291
292
  quota_exceeded: "quota_exceeded";
292
293
  project_purging: "project_purging";
293
- internal_error: "internal_error";
294
294
  }>;
295
295
  error: z.ZodString;
296
296
  message: z.ZodOptional<z.ZodString>;
package/dist/ingest.js CHANGED
@@ -18,9 +18,9 @@ import {
18
18
  zSessionCredential,
19
19
  zTrackHandlerPayload,
20
20
  zTrackPayload
21
- } from "./chunk-EZ2TA4S6.js";
22
- import "./chunk-GCQZNME3.js";
23
- import "./chunk-HLQAZ6JW.js";
21
+ } from "./chunk-DUJSYMYY.js";
22
+ import "./chunk-FMOKJTQD.js";
23
+ import "./chunk-ZCKVMUIQ.js";
24
24
  import {
25
25
  AUTO_EVENT_NAMES,
26
26
  AUTO_EVENT_PROPERTIES,
package/dist/query.cjs CHANGED
@@ -41,7 +41,7 @@ var _chunkFUGY5ANJcjs = require('./chunk-FUGY5ANJ.cjs');
41
41
 
42
42
 
43
43
  var _chunkYSAEOYPDcjs = require('./chunk-YSAEOYPD.cjs');
44
- require('./chunk-JB4MJQX7.cjs');
44
+ require('./chunk-AOS53KAV.cjs');
45
45
 
46
46
 
47
47
 
package/dist/query.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export { zDate, zInstant } from './index.cjs';
3
- import './version-BzeRTpta.cjs';
3
+ import './version-BANKB6Og.cjs';
4
4
 
5
5
  /**
6
6
  * `POST /api/v1/query/*` — the read contract.
@@ -629,11 +629,11 @@ declare const zTrafficSourcesResponse: z.ZodObject<{
629
629
  type TrafficSourcesResponse = z.infer<typeof zTrafficSourcesResponse>;
630
630
  declare const QUERY_ERROR_CODES: readonly ["unauthorized", "project_scope_mismatch", "query_scope_missing", "validation_failed", "unsupported_combination", "project_purging", "rate_limited", "query_timeout", "internal_error"];
631
631
  declare const zQueryErrorCode: z.ZodEnum<{
632
- validation_failed: "validation_failed";
633
632
  unauthorized: "unauthorized";
633
+ validation_failed: "validation_failed";
634
634
  rate_limited: "rate_limited";
635
- project_purging: "project_purging";
636
635
  internal_error: "internal_error";
636
+ project_purging: "project_purging";
637
637
  project_scope_mismatch: "project_scope_mismatch";
638
638
  query_scope_missing: "query_scope_missing";
639
639
  unsupported_combination: "unsupported_combination";
@@ -643,11 +643,11 @@ type QueryErrorCode = z.infer<typeof zQueryErrorCode>;
643
643
  declare const zQueryError: z.ZodObject<{
644
644
  status: z.ZodNumber;
645
645
  code: z.ZodEnum<{
646
- validation_failed: "validation_failed";
647
646
  unauthorized: "unauthorized";
647
+ validation_failed: "validation_failed";
648
648
  rate_limited: "rate_limited";
649
- project_purging: "project_purging";
650
649
  internal_error: "internal_error";
650
+ project_purging: "project_purging";
651
651
  project_scope_mismatch: "project_scope_mismatch";
652
652
  query_scope_missing: "query_scope_missing";
653
653
  unsupported_combination: "unsupported_combination";
package/dist/query.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export { zDate, zInstant } from './index.js';
3
- import './version-BzeRTpta.js';
3
+ import './version-BANKB6Og.js';
4
4
 
5
5
  /**
6
6
  * `POST /api/v1/query/*` — the read contract.
@@ -629,11 +629,11 @@ declare const zTrafficSourcesResponse: z.ZodObject<{
629
629
  type TrafficSourcesResponse = z.infer<typeof zTrafficSourcesResponse>;
630
630
  declare const QUERY_ERROR_CODES: readonly ["unauthorized", "project_scope_mismatch", "query_scope_missing", "validation_failed", "unsupported_combination", "project_purging", "rate_limited", "query_timeout", "internal_error"];
631
631
  declare const zQueryErrorCode: z.ZodEnum<{
632
- validation_failed: "validation_failed";
633
632
  unauthorized: "unauthorized";
633
+ validation_failed: "validation_failed";
634
634
  rate_limited: "rate_limited";
635
- project_purging: "project_purging";
636
635
  internal_error: "internal_error";
636
+ project_purging: "project_purging";
637
637
  project_scope_mismatch: "project_scope_mismatch";
638
638
  query_scope_missing: "query_scope_missing";
639
639
  unsupported_combination: "unsupported_combination";
@@ -643,11 +643,11 @@ type QueryErrorCode = z.infer<typeof zQueryErrorCode>;
643
643
  declare const zQueryError: z.ZodObject<{
644
644
  status: z.ZodNumber;
645
645
  code: z.ZodEnum<{
646
- validation_failed: "validation_failed";
647
646
  unauthorized: "unauthorized";
647
+ validation_failed: "validation_failed";
648
648
  rate_limited: "rate_limited";
649
- project_purging: "project_purging";
650
649
  internal_error: "internal_error";
650
+ project_purging: "project_purging";
651
651
  project_scope_mismatch: "project_scope_mismatch";
652
652
  query_scope_missing: "query_scope_missing";
653
653
  unsupported_combination: "unsupported_combination";
package/dist/query.js CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  zDate,
42
42
  zInstant
43
43
  } from "./chunk-33BCA2LH.js";
44
- import "./chunk-HLQAZ6JW.js";
44
+ import "./chunk-ZCKVMUIQ.js";
45
45
  export {
46
46
  QUERY_API_PATHS,
47
47
  QUERY_ERROR_CODES,
@@ -13,7 +13,7 @@
13
13
  * every publish is a wire bump: consumers that check for exact equality —
14
14
  * every SDK — must upgrade in lockstep with the server.
15
15
  */
16
- declare const CONTRACT_VERSION = "0.6.1";
16
+ declare const CONTRACT_VERSION = "0.6.2";
17
17
  /** Response header carrying the server's contract version. */
18
18
  declare const CONTRACT_VERSION_HEADER = "x-reopt-contract-version";
19
19
  /** Request header names shared by the ingest and query planes. */
@@ -13,7 +13,7 @@
13
13
  * every publish is a wire bump: consumers that check for exact equality —
14
14
  * every SDK — must upgrade in lockstep with the server.
15
15
  */
16
- declare const CONTRACT_VERSION = "0.6.1";
16
+ declare const CONTRACT_VERSION = "0.6.2";
17
17
  /** Response header carrying the server's contract version. */
18
18
  declare const CONTRACT_VERSION_HEADER = "x-reopt-contract-version";
19
19
  /** Request header names shared by the ingest and query planes. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reopt-ai/data-contract",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Wire contract (zod schemas + typed client) for the reopt-data ingest and query APIs",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- {"version":3,"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/** `reopt_<writeKey>_device` — device id plus session/profile hints. */\nexport function deviceCookieName(writeKey: string): string {\n return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_device`;\n}\n\n/** `reopt_<writeKey>_consent` — per-category consent decisions. */\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;\n /** Last identified profile. A hint for the server SDK, never trusted by ingest. */\n profileId?: string;\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\nfunction decode(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\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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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-PHPTCLRU.cjs","../src/identity.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACA;ACgCO,IAAM,kBAAA,EAAoB,kBAAA;AAG1B,IAAM,yBAAA,EAA2B,CAAC,kCAAA,EAAkB,iBAAiB,CAAA;AAMrE,IAAM,uBAAA,EAAyB,IAAA,EAAM,GAAA,EAAK,GAAA,EAAK,EAAA;AAG/C,IAAM,yBAAA,EAA2B,WAAA;AAExC,IAAM,cAAA,EAAgB,QAAA;AAOtB,SAAS,UAAA,CAAW,QAAA,EAA0B;AAC5C,EAAA,GAAA,CAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAM,IAAI,KAAA,CAAM,qDAAqD,CAAA;AAAA,EACvE;AACA,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,iBAAA,EAAmB,GAAG,CAAA;AAChD;AAGO,SAAS,gBAAA,CAAiB,QAAA,EAA0B;AACzD,EAAA,OAAO,CAAA,EAAA;AACT;AAGgB;AACP,EAAA;AACT;AAsCa;AAEG;AACP,EAAA;AACT;AAES;AACH,EAAA;AACF,IAAA;AACM,EAAA;AACN,IAAA;AACF,EAAA;AACF;AASgB;AACT,EAAA;AACC,EAAA;AACD,EAAA;AAEA,EAAA;AACH,IAAA;AACF,EAAA;AAEI,EAAA;AACA,EAAA;AACF,IAAA;AACM,EAAA;AACN,IAAA;AACF,EAAA;AACI,EAAA;AAEE,EAAA;AACD,EAAA;AAEC,EAAA;AACF,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAQgB;AACT,EAAA;AACG,IAAA;AACR,EAAA;AACM,EAAA;AACF,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAOgB;AACT,EAAA;AACC,EAAA;AACD,EAAA;AAED,EAAA;AACA,EAAA;AACF,IAAA;AACM,EAAA;AACN,IAAA;AACF,EAAA;AACI,EAAA;AAEE,EAAA;AACN,EAAA;AACM,IAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEgB;AACR,EAAA;AACN,EAAA;AACM,IAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAQgB;AACP,EAAA;AACT;AAgBa;AAQG;AACP,EAAA;AACT;AAGgB;AACT,EAAA;AACC,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACC,EAAA;AACT;AAEM;AAEG;AACH,EAAA;AACJ,EAAA;AACM,EAAA;AACC,EAAA;AACT;AAEA;AACS,EAAA;AACT;AAGA;AACO,EAAA;AACC,EAAA;AACC,EAAA;AACT;AAOA;AACO,EAAA;AACC,EAAA;AACF,EAAA;AACA,EAAA;AACJ,EAAA;AACE,IAAA;AACF,EAAA;AACO,EAAA;AACT;AD/JU;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-PHPTCLRU.cjs","sourcesContent":[null,"/**\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/** `reopt_<writeKey>_device` — device id plus session/profile hints. */\nexport function deviceCookieName(writeKey: string): string {\n return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_device`;\n}\n\n/** `reopt_<writeKey>_consent` — per-category consent decisions. */\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;\n /** Last identified profile. A hint for the server SDK, never trusted by ingest. */\n profileId?: string;\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\nfunction decode(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\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"]}