@mrrocket/sdk 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +36 -1
  3. package/dist/api-client.d.ts +5 -4
  4. package/dist/api-client.js +12 -8
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/api.d.ts +4 -1
  7. package/dist/api.js +2 -1
  8. package/dist/api.js.map +1 -1
  9. package/dist/batch.d.ts +6 -4
  10. package/dist/batch.js +3 -3
  11. package/dist/batch.js.map +1 -1
  12. package/dist/browser/mrrocket-flows.js +2 -2
  13. package/dist/browser/mrrocket-forms.js +1 -1
  14. package/dist/browser/mrrocket-messenger.js +2 -2
  15. package/dist/browser/mrrocket-push.js +1 -1
  16. package/dist/browser/mrrocket-replay.js +3 -3
  17. package/dist/browser/mrrocket.js +1 -1
  18. package/dist/client.d.ts +10 -2
  19. package/dist/client.js +36 -15
  20. package/dist/client.js.map +1 -1
  21. package/dist/context.d.ts +5 -1
  22. package/dist/context.js.map +1 -1
  23. package/dist/core.d.ts +1 -1
  24. package/dist/core.js +1 -1
  25. package/dist/core.js.map +1 -1
  26. package/dist/errors.d.ts +5 -1
  27. package/dist/errors.js +8 -2
  28. package/dist/errors.js.map +1 -1
  29. package/dist/flows-web.d.ts +2 -0
  30. package/dist/flows-web.js +4 -1
  31. package/dist/flows-web.js.map +1 -1
  32. package/dist/flows.d.ts +10 -0
  33. package/dist/flows.js +2 -0
  34. package/dist/flows.js.map +1 -1
  35. package/dist/forms.d.ts +4 -0
  36. package/dist/forms.js +4 -1
  37. package/dist/forms.js.map +1 -1
  38. package/dist/identity.d.ts +42 -0
  39. package/dist/identity.js +24 -0
  40. package/dist/identity.js.map +1 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/messenger-ui.d.ts +2 -0
  44. package/dist/messenger-ui.js +2 -2
  45. package/dist/messenger-ui.js.map +1 -1
  46. package/dist/messenger.js +3 -2
  47. package/dist/messenger.js.map +1 -1
  48. package/dist/push-web.d.ts +2 -13
  49. package/dist/push-web.js +4 -2
  50. package/dist/push-web.js.map +1 -1
  51. package/dist/react-native/index.d.cts +61 -9
  52. package/dist/react-native/index.d.ts +61 -9
  53. package/dist/react-native/index.js +107 -28
  54. package/dist/react-native/index.js.map +1 -1
  55. package/dist/replay.d.ts +4 -1
  56. package/dist/replay.js +8 -2
  57. package/dist/replay.js.map +1 -1
  58. package/dist/server.d.ts +2 -0
  59. package/dist/server.js +2 -0
  60. package/dist/server.js.map +1 -1
  61. package/dist/user-hash.d.ts +27 -0
  62. package/dist/user-hash.js +24 -0
  63. package/dist/user-hash.js.map +1 -0
  64. package/package.json +5 -1
package/dist/replay.d.ts CHANGED
@@ -18,6 +18,8 @@ export interface ReplayOptions {
18
18
  distinctId?: string;
19
19
  /** Whether `distinctId` comes from identify() (a person) rather than the visitor cookie. */
20
20
  identified?: boolean;
21
+ /** The identity hash of `distinctId` (0.16.0), sent with every chunk while the recording is theirs. */
22
+ userHash?: string;
21
23
  sampleRate: number;
22
24
  warn: (message: string) => void;
23
25
  }
@@ -27,7 +29,8 @@ export interface ReplayHandle {
27
29
  click(c: ClickInfo, rage: boolean): void;
28
30
  /** A client-side navigation. */
29
31
  page(path: string): void;
30
- identify(id: string): void;
32
+ /** The person the site identified, with their identity hash when it has one (0.16.0). */
33
+ identify(id: string, userHash?: string): void;
31
34
  /** Stops recording. `discard` (consent withdrawn) drops everything not sent yet. */
32
35
  stop(discard?: boolean): void;
33
36
  }
package/dist/replay.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/replay.ts
4
+ import { warnUnsigned } from "./identity.js";
4
5
  import {
5
6
  BUFFER_MS,
6
7
  CHECKOUT_MS,
@@ -28,6 +29,7 @@ var startReplay = async (o) => {
28
29
  let trigger = mode === "upload" ? "sample" : null;
29
30
  let distinctId = o.distinctId;
30
31
  let identified = o.identified === true;
32
+ let userHash = o.userHash;
31
33
  let start = null;
32
34
  let seq = 0;
33
35
  let stopped = false;
@@ -66,7 +68,8 @@ var startReplay = async (o) => {
66
68
  if (sync && "sendBeacon" in navigator && navigator.sendBeacon(url, body)) return;
67
69
  try {
68
70
  const r = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: sync || body.length < 6e4 });
69
- if (r.status === 403 || r.status === 409 || r.status === 413) end(true);
71
+ warnUnsigned(r.status);
72
+ if (r.status === 401 || r.status === 403 || r.status === 409 || r.status === 413) end(true);
70
73
  } catch {
71
74
  o.warn(`Could not reach ${o.host || "this origin"} to upload a recording.`);
72
75
  }
@@ -85,6 +88,7 @@ var startReplay = async (o) => {
85
88
  sessionId: o.sessionId,
86
89
  distinctId,
87
90
  identified,
91
+ ...userHash ? { userHash } : {},
88
92
  start,
89
93
  seq: seq++,
90
94
  consent: { replay: true },
@@ -187,7 +191,9 @@ var startReplay = async (o) => {
187
191
  }
188
192
  },
189
193
  page: (path) => !stopped && marks.push({ t: Date.now(), kind: "pageview", path: path.slice(0, 300) || "/" }),
190
- identify: (id) => {
194
+ identify: (id, hash) => {
195
+ if (hash) userHash = hash;
196
+ else if (id !== distinctId) userHash = void 0;
191
197
  distinctId = id;
192
198
  identified = true;
193
199
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/replay.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The session recorder. The provider imports this file (and rrweb's recorder with it) only when a visit records: the\n * product has recordings on and the visitor gave replay consent. Pages without recording never load it.\n *\n * A visit in the product's sample uploads from its start. Any other visit keeps the last minute in memory and uploads\n * it only when something happens worth watching: a JS error, a rage click, or a `purchase…` / `checkout…` event; from\n * then on it uploads like a sampled one. Chunks go every 5 seconds or 64 KB, gzip'd where the browser can, to\n * `POST {host}/api/sdk/replay`; the last one leaves with sendBeacon when the page hides. A recording ends after 30\n * minutes. Withdrawing consent stops it and drops what was not sent.\n */\nimport type { ClickInfo } from \"./capture\";\nimport {\n BUFFER_MS,\n CHECKOUT_MS,\n FLUSH_BYTES,\n MAX_RECORDING_MS,\n MAX_REQUEST_BYTES,\n type ReplayEvent,\n type ReplayTrigger,\n RingBuffer,\n chunkDue,\n inSample,\n recordOptions,\n toBase64,\n triggerOfEvent,\n} from \"./replay-core\";\n\nexport interface ReplayOptions {\n host: string;\n apiKey: string;\n /** The SDK's session id; every chunk carries it, so the recording joins the session's events. */\n sessionId: string;\n /** The identify() id or the cookie id; missing cookieless until identify(). */\n distinctId?: string;\n /** Whether `distinctId` comes from identify() (a person) rather than the visitor cookie. */\n identified?: boolean;\n sampleRate: number;\n warn: (message: string) => void;\n}\n\nexport interface ReplayHandle {\n /** A tracked event; `purchase…` and `checkout…` upload the buffered minute. */\n event(name: string): void;\n click(c: ClickInfo, rage: boolean): void;\n /** A client-side navigation. */\n page(path: string): void;\n identify(id: string): void;\n /** Stops recording. `discard` (consent withdrawn) drops everything not sent yet. */\n stop(discard?: boolean): void;\n}\n\n/** One line of the masked event list the server keeps next to the recording (pages, clicks, rage clicks, errors). */\ninterface Mark {\n t: number;\n kind: \"pageview\" | \"click\" | \"rage\" | \"error\";\n path: string;\n label?: string;\n x?: number;\n y?: number;\n vw?: number;\n dead?: boolean;\n}\n\ninterface Held extends ReplayEvent {\n json: string;\n}\n\nconst clip = (s: unknown, n: number) => String(s ?? \"\").slice(0, n);\nconst pathNow = () => window.location.pathname.slice(0, 300) || \"/\";\n\nconst gzip = async (text: string) => {\n const stream = new Blob([text]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n};\n\nexport const startReplay = async (o: ReplayOptions): Promise<ReplayHandle | null> => {\n if (typeof window === \"undefined\") return null;\n const { record } = await import(\"@rrweb/record\");\n\n const url = `${o.host}/api/sdk/replay`;\n let mode: \"upload\" | \"buffer\" = inSample(o.sessionId, o.sampleRate) ? \"upload\" : \"buffer\";\n let trigger: ReplayTrigger | null = mode === \"upload\" ? \"sample\" : null;\n let distinctId = o.distinctId;\n let identified = o.identified === true;\n let start: number | null = null;\n let seq = 0;\n let stopped = false;\n const begun = Date.now();\n const ring = new RingBuffer<Held>();\n let pending: Held[] = [];\n let pendingBytes = 0;\n let lastFlush = Date.now();\n let marks: Mark[] = [{ t: Date.now(), kind: \"pageview\", path: pathNow() }];\n let queue: Promise<void> = Promise.resolve();\n\n const end = (discard: boolean) => {\n if (stopped) return;\n if (!discard && mode === \"upload\") flush(false);\n stopped = true;\n stopRecording?.();\n window.clearInterval(tick);\n window.clearInterval(checkout);\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n window.removeEventListener(\"pagehide\", onHide);\n document.removeEventListener(\"visibilitychange\", onVisibility);\n if (console.error === patched) console.error = original;\n pending = [];\n ring.clear();\n marks = [];\n };\n\n const send = async (payload: Record<string, unknown>, events: string, sync: boolean) => {\n let body: string;\n if (!sync && typeof CompressionStream !== \"undefined\") body = JSON.stringify({ ...payload, enc: \"gzip\", data: toBase64(await gzip(events)) });\n else body = JSON.stringify({ ...payload, enc: \"json\", data: events });\n if (body.length > MAX_REQUEST_BYTES) {\n o.warn(\"A recording chunk was over 1 MB (a very large page). The recording stopped.\");\n end(true);\n return;\n }\n if (sync && \"sendBeacon\" in navigator && navigator.sendBeacon(url, body)) return;\n try {\n const r = await fetch(url, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body, keepalive: sync || body.length < 60_000 });\n // Recordings turned off, consent missing, or the recording reached its size cap: stop for this visit.\n if (r.status === 403 || r.status === 409 || r.status === 413) end(true);\n } catch {\n o.warn(`Could not reach ${o.host || \"this origin\"} to upload a recording.`);\n }\n };\n\n function flush(sync: boolean) {\n if (!pending.length) return;\n const events = pending;\n pending = [];\n pendingBytes = 0;\n lastFlush = Date.now();\n start ??= events[0].timestamp;\n const chunkMarks = marks;\n marks = [];\n const payload = {\n key: o.apiKey,\n sessionId: o.sessionId,\n distinctId,\n identified,\n start,\n seq: seq++,\n consent: { replay: true },\n trigger,\n final: sync,\n meta: {\n first: events[0].timestamp,\n last: events[events.length - 1].timestamp,\n vw: window.innerWidth,\n vh: window.innerHeight,\n marks: chunkMarks,\n },\n };\n const json = `[${events.map((e) => e.json).join(\",\")}]`;\n if (sync) void send(payload, json, true);\n else queue = queue.then(() => send(payload, json, false));\n }\n\n const fire = (kind: Exclude<ReplayTrigger, \"sample\">) => {\n if (stopped || mode === \"upload\") return;\n mode = \"upload\";\n trigger = kind;\n window.clearInterval(checkout);\n pending = ring.drain();\n pendingBytes = pending.reduce((s, e) => s + e.json.length, 0);\n flush(false);\n };\n\n const emit = (e: ReplayEvent) => {\n if (stopped) return;\n if (Date.now() - (start ?? begun) > MAX_RECORDING_MS) {\n end(false);\n return;\n }\n const json = JSON.stringify(e);\n const item: Held = { type: e.type, timestamp: e.timestamp, json };\n if (mode === \"buffer\") {\n ring.push(item, json.length);\n const from = ring.first ?? e.timestamp - BUFFER_MS;\n marks = marks.filter((m) => m.t >= from);\n return;\n }\n pending.push(item);\n pendingBytes += json.length;\n if (pendingBytes >= FLUSH_BYTES) flush(false);\n };\n\n const original = console.error;\n const patched = (...args: unknown[]) => {\n try {\n record.addCustomEvent(\"$console\", { level: \"error\", text: clip(args.map((a) => (a instanceof Error ? a.message : typeof a === \"string\" ? a : \"[object]\")).join(\" \"), 300) });\n } catch {\n // Recording must never break the page's own logging.\n }\n original.apply(console, args);\n };\n const onError = (e: ErrorEvent) => {\n const message = clip(e.message || \"Script error\", 200);\n marks.push({ t: Date.now(), kind: \"error\", path: pathNow(), label: message });\n try {\n record.addCustomEvent(\"$error\", { message });\n } catch {\n // ignore\n }\n fire(\"error\");\n };\n const onRejection = (e: PromiseRejectionEvent) => {\n const r = e.reason as { message?: unknown } | undefined;\n const message = clip(typeof r?.message === \"string\" ? r.message : \"Unhandled promise rejection\", 200);\n marks.push({ t: Date.now(), kind: \"error\", path: pathNow(), label: message });\n try {\n record.addCustomEvent(\"$error\", { message });\n } catch {\n // ignore\n }\n fire(\"error\");\n };\n const onHide = () => mode === \"upload\" && flush(true);\n const onVisibility = () => document.visibilityState === \"hidden\" && mode === \"upload\" && flush(false);\n\n const stopRecording = record(recordOptions(emit) as unknown as Parameters<typeof record>[0]);\n if (!stopRecording) return null;\n console.error = patched;\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n window.addEventListener(\"pagehide\", onHide);\n document.addEventListener(\"visibilitychange\", onVisibility);\n const tick = window.setInterval(() => {\n if (Date.now() - (start ?? begun) > MAX_RECORDING_MS) end(false);\n else if (mode === \"upload\" && chunkDue(pendingBytes, Date.now() - lastFlush)) flush(false);\n }, 1000);\n // A buffering visit snapshots every 30 seconds, so the buffer always starts at a full snapshot within the minute.\n const checkout = mode === \"buffer\" ? window.setInterval(() => record.takeFullSnapshot(true), CHECKOUT_MS) : 0;\n\n return {\n event: (name) => {\n const t = triggerOfEvent(name);\n if (t) fire(t);\n },\n click: (c, rage) => {\n if (stopped) return;\n const t = Date.now();\n const path = pathNow();\n const label = c.label || c.selector.split(\" > \").pop() || \"\";\n marks.push({ t, kind: \"click\", path, label, x: c.x, y: c.y, vw: c.vw, dead: !c.interactive });\n if (rage) {\n marks.push({ t, kind: \"rage\", path, label, x: c.x, y: c.y, vw: c.vw });\n fire(\"rage\");\n }\n },\n page: (path) => !stopped && marks.push({ t: Date.now(), kind: \"pageview\", path: path.slice(0, 300) || \"/\" }),\n identify: (id) => {\n distinctId = id;\n identified = true;\n },\n stop: (discard = false) => end(discard),\n };\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/replay.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The session recorder. The provider imports this file (and rrweb's recorder with it) only when a visit records: the\n * product has recordings on and the visitor gave replay consent. Pages without recording never load it.\n *\n * A visit in the product's sample uploads from its start. Any other visit keeps the last minute in memory and uploads\n * it only when something happens worth watching: a JS error, a rage click, or a `purchase…` / `checkout…` event; from\n * then on it uploads like a sampled one. Chunks go every 5 seconds or 64 KB, gzip'd where the browser can, to\n * `POST {host}/api/sdk/replay`; the last one leaves with sendBeacon when the page hides. A recording ends after 30\n * minutes. Withdrawing consent stops it and drops what was not sent.\n */\nimport type { ClickInfo } from \"./capture\";\nimport { warnUnsigned } from \"./identity\";\nimport {\n BUFFER_MS,\n CHECKOUT_MS,\n FLUSH_BYTES,\n MAX_RECORDING_MS,\n MAX_REQUEST_BYTES,\n type ReplayEvent,\n type ReplayTrigger,\n RingBuffer,\n chunkDue,\n inSample,\n recordOptions,\n toBase64,\n triggerOfEvent,\n} from \"./replay-core\";\n\nexport interface ReplayOptions {\n host: string;\n apiKey: string;\n /** The SDK's session id; every chunk carries it, so the recording joins the session's events. */\n sessionId: string;\n /** The identify() id or the cookie id; missing cookieless until identify(). */\n distinctId?: string;\n /** Whether `distinctId` comes from identify() (a person) rather than the visitor cookie. */\n identified?: boolean;\n /** The identity hash of `distinctId` (0.16.0), sent with every chunk while the recording is theirs. */\n userHash?: string;\n sampleRate: number;\n warn: (message: string) => void;\n}\n\nexport interface ReplayHandle {\n /** A tracked event; `purchase…` and `checkout…` upload the buffered minute. */\n event(name: string): void;\n click(c: ClickInfo, rage: boolean): void;\n /** A client-side navigation. */\n page(path: string): void;\n /** The person the site identified, with their identity hash when it has one (0.16.0). */\n identify(id: string, userHash?: string): void;\n /** Stops recording. `discard` (consent withdrawn) drops everything not sent yet. */\n stop(discard?: boolean): void;\n}\n\n/** One line of the masked event list the server keeps next to the recording (pages, clicks, rage clicks, errors). */\ninterface Mark {\n t: number;\n kind: \"pageview\" | \"click\" | \"rage\" | \"error\";\n path: string;\n label?: string;\n x?: number;\n y?: number;\n vw?: number;\n dead?: boolean;\n}\n\ninterface Held extends ReplayEvent {\n json: string;\n}\n\nconst clip = (s: unknown, n: number) => String(s ?? \"\").slice(0, n);\nconst pathNow = () => window.location.pathname.slice(0, 300) || \"/\";\n\nconst gzip = async (text: string) => {\n const stream = new Blob([text]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n};\n\nexport const startReplay = async (o: ReplayOptions): Promise<ReplayHandle | null> => {\n if (typeof window === \"undefined\") return null;\n const { record } = await import(\"@rrweb/record\");\n\n const url = `${o.host}/api/sdk/replay`;\n let mode: \"upload\" | \"buffer\" = inSample(o.sessionId, o.sampleRate) ? \"upload\" : \"buffer\";\n let trigger: ReplayTrigger | null = mode === \"upload\" ? \"sample\" : null;\n let distinctId = o.distinctId;\n let identified = o.identified === true;\n let userHash = o.userHash;\n let start: number | null = null;\n let seq = 0;\n let stopped = false;\n const begun = Date.now();\n const ring = new RingBuffer<Held>();\n let pending: Held[] = [];\n let pendingBytes = 0;\n let lastFlush = Date.now();\n let marks: Mark[] = [{ t: Date.now(), kind: \"pageview\", path: pathNow() }];\n let queue: Promise<void> = Promise.resolve();\n\n const end = (discard: boolean) => {\n if (stopped) return;\n if (!discard && mode === \"upload\") flush(false);\n stopped = true;\n stopRecording?.();\n window.clearInterval(tick);\n window.clearInterval(checkout);\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n window.removeEventListener(\"pagehide\", onHide);\n document.removeEventListener(\"visibilitychange\", onVisibility);\n if (console.error === patched) console.error = original;\n pending = [];\n ring.clear();\n marks = [];\n };\n\n const send = async (payload: Record<string, unknown>, events: string, sync: boolean) => {\n let body: string;\n if (!sync && typeof CompressionStream !== \"undefined\") body = JSON.stringify({ ...payload, enc: \"gzip\", data: toBase64(await gzip(events)) });\n else body = JSON.stringify({ ...payload, enc: \"json\", data: events });\n if (body.length > MAX_REQUEST_BYTES) {\n o.warn(\"A recording chunk was over 1 MB (a very large page). The recording stopped.\");\n end(true);\n return;\n }\n if (sync && \"sendBeacon\" in navigator && navigator.sendBeacon(url, body)) return;\n try {\n const r = await fetch(url, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body, keepalive: sync || body.length < 60_000 });\n warnUnsigned(r.status);\n // Recordings turned off, consent missing, the recording reached its size cap, or the person's identity hash\n // is missing or wrong (401): stop for this visit.\n if (r.status === 401 || r.status === 403 || r.status === 409 || r.status === 413) end(true);\n } catch {\n o.warn(`Could not reach ${o.host || \"this origin\"} to upload a recording.`);\n }\n };\n\n function flush(sync: boolean) {\n if (!pending.length) return;\n const events = pending;\n pending = [];\n pendingBytes = 0;\n lastFlush = Date.now();\n start ??= events[0].timestamp;\n const chunkMarks = marks;\n marks = [];\n const payload = {\n key: o.apiKey,\n sessionId: o.sessionId,\n distinctId,\n identified,\n ...(userHash ? { userHash } : {}),\n start,\n seq: seq++,\n consent: { replay: true },\n trigger,\n final: sync,\n meta: {\n first: events[0].timestamp,\n last: events[events.length - 1].timestamp,\n vw: window.innerWidth,\n vh: window.innerHeight,\n marks: chunkMarks,\n },\n };\n const json = `[${events.map((e) => e.json).join(\",\")}]`;\n if (sync) void send(payload, json, true);\n else queue = queue.then(() => send(payload, json, false));\n }\n\n const fire = (kind: Exclude<ReplayTrigger, \"sample\">) => {\n if (stopped || mode === \"upload\") return;\n mode = \"upload\";\n trigger = kind;\n window.clearInterval(checkout);\n pending = ring.drain();\n pendingBytes = pending.reduce((s, e) => s + e.json.length, 0);\n flush(false);\n };\n\n const emit = (e: ReplayEvent) => {\n if (stopped) return;\n if (Date.now() - (start ?? begun) > MAX_RECORDING_MS) {\n end(false);\n return;\n }\n const json = JSON.stringify(e);\n const item: Held = { type: e.type, timestamp: e.timestamp, json };\n if (mode === \"buffer\") {\n ring.push(item, json.length);\n const from = ring.first ?? e.timestamp - BUFFER_MS;\n marks = marks.filter((m) => m.t >= from);\n return;\n }\n pending.push(item);\n pendingBytes += json.length;\n if (pendingBytes >= FLUSH_BYTES) flush(false);\n };\n\n const original = console.error;\n const patched = (...args: unknown[]) => {\n try {\n record.addCustomEvent(\"$console\", { level: \"error\", text: clip(args.map((a) => (a instanceof Error ? a.message : typeof a === \"string\" ? a : \"[object]\")).join(\" \"), 300) });\n } catch {\n // Recording must never break the page's own logging.\n }\n original.apply(console, args);\n };\n const onError = (e: ErrorEvent) => {\n const message = clip(e.message || \"Script error\", 200);\n marks.push({ t: Date.now(), kind: \"error\", path: pathNow(), label: message });\n try {\n record.addCustomEvent(\"$error\", { message });\n } catch {\n // ignore\n }\n fire(\"error\");\n };\n const onRejection = (e: PromiseRejectionEvent) => {\n const r = e.reason as { message?: unknown } | undefined;\n const message = clip(typeof r?.message === \"string\" ? r.message : \"Unhandled promise rejection\", 200);\n marks.push({ t: Date.now(), kind: \"error\", path: pathNow(), label: message });\n try {\n record.addCustomEvent(\"$error\", { message });\n } catch {\n // ignore\n }\n fire(\"error\");\n };\n const onHide = () => mode === \"upload\" && flush(true);\n const onVisibility = () => document.visibilityState === \"hidden\" && mode === \"upload\" && flush(false);\n\n const stopRecording = record(recordOptions(emit) as unknown as Parameters<typeof record>[0]);\n if (!stopRecording) return null;\n console.error = patched;\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n window.addEventListener(\"pagehide\", onHide);\n document.addEventListener(\"visibilitychange\", onVisibility);\n const tick = window.setInterval(() => {\n if (Date.now() - (start ?? begun) > MAX_RECORDING_MS) end(false);\n else if (mode === \"upload\" && chunkDue(pendingBytes, Date.now() - lastFlush)) flush(false);\n }, 1000);\n // A buffering visit snapshots every 30 seconds, so the buffer always starts at a full snapshot within the minute.\n const checkout = mode === \"buffer\" ? window.setInterval(() => record.takeFullSnapshot(true), CHECKOUT_MS) : 0;\n\n return {\n event: (name) => {\n const t = triggerOfEvent(name);\n if (t) fire(t);\n },\n click: (c, rage) => {\n if (stopped) return;\n const t = Date.now();\n const path = pathNow();\n const label = c.label || c.selector.split(\" > \").pop() || \"\";\n marks.push({ t, kind: \"click\", path, label, x: c.x, y: c.y, vw: c.vw, dead: !c.interactive });\n if (rage) {\n marks.push({ t, kind: \"rage\", path, label, x: c.x, y: c.y, vw: c.vw });\n fire(\"rage\");\n }\n },\n page: (path) => !stopped && marks.push({ t: Date.now(), kind: \"pageview\", path: path.slice(0, 300) || \"/\" }),\n identify: (id, hash) => {\n // A hash belongs to its id: another id without one goes unsigned.\n if (hash) userHash = hash;\n else if (id !== distinctId) userHash = undefined;\n distinctId = id;\n identified = true;\n },\n stop: (discard = false) => end(discard),\n };\n};\n"],"mappings":"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","names":[]}
package/dist/server.d.ts CHANGED
@@ -55,6 +55,8 @@ export declare const getFlagVariant: (key: string, { config, cookies, distinctId
55
55
  /** Every text change for this request, to render `<Text>` defaults server-side. */
56
56
  export declare const getTexts: ({ config, searchParams, cookies, distinctId }: RequestInfo) => Record<string, string>;
57
57
  export * from "./core.js";
58
+ /** Identity verification (0.16.0): sign a user id for `identify(id, props, { hash })` with the product's server key. */
59
+ export { type UserHashOptions, userHash } from "./user-hash.js";
58
60
  export { type AttackKind, type SecurityOptions, attackIn, ipOf, reportAuthDenied, reportFailedLogin, reportSuspiciousRequest, watchRequest } from "./security.js";
59
61
  /** Affiliate referrals on the server: read the `mrr_ref` cookie in the route that creates a Stripe Checkout Session. */
60
62
  export { type CheckoutReferral, type Referral, REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from "./referral.js";
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/server.ts
2
2
  import { EMBED_PARAM, ID_COOKIE, PREVIEW_PARAM, envKey, flagFor, parsePreview, resolveHost, resolveVariant, textChanges } from "./core.js";
3
3
  export * from "./core.js";
4
+ import { userHash } from "./user-hash.js";
4
5
  import { attackIn, ipOf, reportAuthDenied, reportFailedLogin, reportSuspiciousRequest, watchRequest } from "./security.js";
5
6
  import { REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from "./referral.js";
6
7
  var CONFIG_TIMEOUT_MS = 2500;
@@ -52,6 +53,7 @@ export {
52
53
  reportAuthDenied,
53
54
  reportFailedLogin,
54
55
  reportSuspiciousRequest,
56
+ userHash,
55
57
  watchRequest
56
58
  };
57
59
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { type CockpitConfig, EMBED_PARAM, type FlagPerson, ID_COOKIE, PREVIEW_PARAM, type VariantId, envKey, flagFor, parsePreview, resolveHost, resolveVariant, textChanges } from \"./core\";\n\n/**\n * Server helpers for the App Router: load the experiments once per request, then render the right variant on the\n * first paint (no flicker) and hand the same config to `<CockpitProvider config>` so the browser agrees.\n */\n\ntype SearchParams = Record<string, string | string[] | undefined>;\ninterface CookieReader {\n get(name: string): { value: string } | undefined;\n}\n\n/** How long the server waits for MRRocket before it renders with control. */\nconst CONFIG_TIMEOUT_MS = 2500;\n\n/**\n * The experiments for a public key. `null` when MRRocket cannot be reached, answers with an error or is slower than\n * `timeoutMs`: everyone then sees control and the page still renders. Runs on Node, the Edge runtime and Cloudflare\n * Workers (only `fetch` and `AbortController`).\n *\n * `apiKey` and `host` default to `NEXT_PUBLIC_MRROCKET_KEY` and `NEXT_PUBLIC_MRROCKET_HOST`.\n */\nexport const getCockpitConfig = async ({ apiKey, host, timeoutMs = CONFIG_TIMEOUT_MS }: { apiKey?: string; host?: string; timeoutMs?: number } = {}): Promise<CockpitConfig | null> => {\n const key = apiKey ?? envKey();\n if (!key) return null;\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n // `next.revalidate: 0` keeps this request uncached in Next.js. Not `cache: \"no-store\"`: Cloudflare Workers\n // throws on the `cache` field of a fetch unless a compatibility flag is on, and every config would be null.\n const res = await fetch(`${resolveHost(host)}/api/sdk/config?key=${encodeURIComponent(key)}`, {\n signal: ctl.signal,\n next: { revalidate: 0 },\n } as RequestInit);\n if (!res.ok) return null;\n const config = (await res.json()) as CockpitConfig | null;\n return config && Array.isArray(config.experiments) ? config : null;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n};\n\n/** Forced variants and the embed flag from a page's search params, for previews in Cockpit. */\nexport const previewFrom = (searchParams: SearchParams | undefined) => {\n const embed = searchParams?.[EMBED_PARAM];\n return { preview: parsePreview(searchParams?.[PREVIEW_PARAM]), embedded: (Array.isArray(embed) ? embed[0] : embed) === \"1\" };\n};\n\n/** The visitor's id from the `ck_id` cookie, if the browser has one yet (only in consent \"cookies\" mode). */\nexport const distinctIdFrom = (cookies: CookieReader | undefined) => cookies?.get(ID_COOKIE)?.value ?? \"anonymous\";\n\n/**\n * Who the visitor is for this request. Pass `distinctId` (your signed-in user id, the same you give\n * `<CockpitProvider distinctId>`) when there is no `ck_id` cookie, as in the default cookieless mode.\n */\ninterface RequestInfo {\n config: CockpitConfig | null;\n searchParams?: SearchParams;\n cookies?: CookieReader;\n distinctId?: string;\n}\n\n/** The variant of one experiment for this request. */\nexport const getVariant = (key: string, { config, searchParams, cookies, distinctId }: RequestInfo): VariantId =>\n resolveVariant(key, config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\n/**\n * A feature flag for this request: true while it is on for the visitor, false while it is off or unknown (or MRRocket\n * could not be reached). `person` takes the properties the flag's audience reads (\"Email\", \"Plan\" …), as you pass them\n * to identify(). For a multivariate flag use `getFlagVariant`.\n */\nexport const getFlag = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): boolean =>\n flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person) !== false;\n\n/** The variant of a multivariate flag for this request, or null while it is off. */\nexport const getFlagVariant = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): string | null => {\n const v = flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person);\n return typeof v === \"string\" ? v : null;\n};\n\n/** Every text change for this request, to render `<Text>` defaults server-side. */\nexport const getTexts = ({ config, searchParams, cookies, distinctId }: RequestInfo) =>\n textChanges(config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\nexport * from \"./core\";\nexport { type AttackKind, type SecurityOptions, attackIn, ipOf, reportAuthDenied, reportFailedLogin, reportSuspiciousRequest, watchRequest } from \"./security\";\n/** Affiliate referrals on the server: read the `mrr_ref` cookie in the route that creates a Stripe Checkout Session. */\nexport { type CheckoutReferral, type Referral, REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from \"./referral\";\n"],"mappings":";AAAA,SAA6B,aAA8B,WAAW,eAA+B,QAAQ,SAAS,cAAc,aAAa,gBAAgB,mBAAmB;AAsFpL,cAAc;AACd,SAAgD,UAAU,MAAM,kBAAkB,mBAAmB,yBAAyB,oBAAoB;AAElJ,SAA+C,iBAAiB,oBAAoB,qBAAqB,oBAAoB;AA5E7H,IAAM,oBAAoB;AASnB,IAAM,mBAAmB,OAAO,EAAE,QAAQ,MAAM,YAAY,kBAAkB,IAA4D,CAAC,MAAqC;AACnL,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,IAAI,gBAAgB;AAChC,QAAM,QAAQ,WAAW,MAAM,IAAI,MAAM,GAAG,SAAS;AACrD,MAAI;AAGA,UAAM,MAAM,MAAM,MAAM,GAAG,YAAY,IAAI,CAAC,uBAAuB,mBAAmB,GAAG,CAAC,IAAI;AAAA,MAC1F,QAAQ,IAAI;AAAA,MACZ,MAAM,EAAE,YAAY,EAAE;AAAA,IAC1B,CAAgB;AAChB,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,SAAU,MAAM,IAAI,KAAK;AAC/B,WAAO,UAAU,MAAM,QAAQ,OAAO,WAAW,IAAI,SAAS;AAAA,EAClE,QAAQ;AACJ,WAAO;AAAA,EACX,UAAE;AACE,iBAAa,KAAK;AAAA,EACtB;AACJ;AAGO,IAAM,cAAc,CAAC,iBAA2C;AACnE,QAAM,QAAQ,eAAe,WAAW;AACxC,SAAO,EAAE,SAAS,aAAa,eAAe,aAAa,CAAC,GAAG,WAAW,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI,WAAW,IAAI;AAC/H;AAGO,IAAM,iBAAiB,CAAC,YAAsC,SAAS,IAAI,SAAS,GAAG,SAAS;AAchG,IAAM,aAAa,CAAC,KAAa,EAAE,QAAQ,cAAc,SAAS,WAAW,MAChF,eAAe,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;AAOjG,IAAM,UAAU,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MACvE,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM,MAAM;AAGrE,IAAM,iBAAiB,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MAA4D;AAC1I,QAAM,IAAI,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM;AAC5E,SAAO,OAAO,MAAM,WAAW,IAAI;AACvC;AAGO,IAAM,WAAW,CAAC,EAAE,QAAQ,cAAc,SAAS,WAAW,MACjE,YAAY,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { type CockpitConfig, EMBED_PARAM, type FlagPerson, ID_COOKIE, PREVIEW_PARAM, type VariantId, envKey, flagFor, parsePreview, resolveHost, resolveVariant, textChanges } from \"./core\";\n\n/**\n * Server helpers for the App Router: load the experiments once per request, then render the right variant on the\n * first paint (no flicker) and hand the same config to `<CockpitProvider config>` so the browser agrees.\n */\n\ntype SearchParams = Record<string, string | string[] | undefined>;\ninterface CookieReader {\n get(name: string): { value: string } | undefined;\n}\n\n/** How long the server waits for MRRocket before it renders with control. */\nconst CONFIG_TIMEOUT_MS = 2500;\n\n/**\n * The experiments for a public key. `null` when MRRocket cannot be reached, answers with an error or is slower than\n * `timeoutMs`: everyone then sees control and the page still renders. Runs on Node, the Edge runtime and Cloudflare\n * Workers (only `fetch` and `AbortController`).\n *\n * `apiKey` and `host` default to `NEXT_PUBLIC_MRROCKET_KEY` and `NEXT_PUBLIC_MRROCKET_HOST`.\n */\nexport const getCockpitConfig = async ({ apiKey, host, timeoutMs = CONFIG_TIMEOUT_MS }: { apiKey?: string; host?: string; timeoutMs?: number } = {}): Promise<CockpitConfig | null> => {\n const key = apiKey ?? envKey();\n if (!key) return null;\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n // `next.revalidate: 0` keeps this request uncached in Next.js. Not `cache: \"no-store\"`: Cloudflare Workers\n // throws on the `cache` field of a fetch unless a compatibility flag is on, and every config would be null.\n const res = await fetch(`${resolveHost(host)}/api/sdk/config?key=${encodeURIComponent(key)}`, {\n signal: ctl.signal,\n next: { revalidate: 0 },\n } as RequestInit);\n if (!res.ok) return null;\n const config = (await res.json()) as CockpitConfig | null;\n return config && Array.isArray(config.experiments) ? config : null;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n};\n\n/** Forced variants and the embed flag from a page's search params, for previews in Cockpit. */\nexport const previewFrom = (searchParams: SearchParams | undefined) => {\n const embed = searchParams?.[EMBED_PARAM];\n return { preview: parsePreview(searchParams?.[PREVIEW_PARAM]), embedded: (Array.isArray(embed) ? embed[0] : embed) === \"1\" };\n};\n\n/** The visitor's id from the `ck_id` cookie, if the browser has one yet (only in consent \"cookies\" mode). */\nexport const distinctIdFrom = (cookies: CookieReader | undefined) => cookies?.get(ID_COOKIE)?.value ?? \"anonymous\";\n\n/**\n * Who the visitor is for this request. Pass `distinctId` (your signed-in user id, the same you give\n * `<CockpitProvider distinctId>`) when there is no `ck_id` cookie, as in the default cookieless mode.\n */\ninterface RequestInfo {\n config: CockpitConfig | null;\n searchParams?: SearchParams;\n cookies?: CookieReader;\n distinctId?: string;\n}\n\n/** The variant of one experiment for this request. */\nexport const getVariant = (key: string, { config, searchParams, cookies, distinctId }: RequestInfo): VariantId =>\n resolveVariant(key, config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\n/**\n * A feature flag for this request: true while it is on for the visitor, false while it is off or unknown (or MRRocket\n * could not be reached). `person` takes the properties the flag's audience reads (\"Email\", \"Plan\" …), as you pass them\n * to identify(). For a multivariate flag use `getFlagVariant`.\n */\nexport const getFlag = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): boolean =>\n flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person) !== false;\n\n/** The variant of a multivariate flag for this request, or null while it is off. */\nexport const getFlagVariant = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): string | null => {\n const v = flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person);\n return typeof v === \"string\" ? v : null;\n};\n\n/** Every text change for this request, to render `<Text>` defaults server-side. */\nexport const getTexts = ({ config, searchParams, cookies, distinctId }: RequestInfo) =>\n textChanges(config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\nexport * from \"./core\";\n/** Identity verification (0.16.0): sign a user id for `identify(id, props, { hash })` with the product's server key. */\nexport { type UserHashOptions, userHash } from \"./user-hash\";\nexport { type AttackKind, type SecurityOptions, attackIn, ipOf, reportAuthDenied, reportFailedLogin, reportSuspiciousRequest, watchRequest } from \"./security\";\n/** Affiliate referrals on the server: read the `mrr_ref` cookie in the route that creates a Stripe Checkout Session. */\nexport { type CheckoutReferral, type Referral, REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from \"./referral\";\n"],"mappings":";AAAA,SAA6B,aAA8B,WAAW,eAA+B,QAAQ,SAAS,cAAc,aAAa,gBAAgB,mBAAmB;AAsFpL,cAAc;AAEd,SAA+B,gBAAgB;AAC/C,SAAgD,UAAU,MAAM,kBAAkB,mBAAmB,yBAAyB,oBAAoB;AAElJ,SAA+C,iBAAiB,oBAAoB,qBAAqB,oBAAoB;AA9E7H,IAAM,oBAAoB;AASnB,IAAM,mBAAmB,OAAO,EAAE,QAAQ,MAAM,YAAY,kBAAkB,IAA4D,CAAC,MAAqC;AACnL,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,IAAI,gBAAgB;AAChC,QAAM,QAAQ,WAAW,MAAM,IAAI,MAAM,GAAG,SAAS;AACrD,MAAI;AAGA,UAAM,MAAM,MAAM,MAAM,GAAG,YAAY,IAAI,CAAC,uBAAuB,mBAAmB,GAAG,CAAC,IAAI;AAAA,MAC1F,QAAQ,IAAI;AAAA,MACZ,MAAM,EAAE,YAAY,EAAE;AAAA,IAC1B,CAAgB;AAChB,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,SAAU,MAAM,IAAI,KAAK;AAC/B,WAAO,UAAU,MAAM,QAAQ,OAAO,WAAW,IAAI,SAAS;AAAA,EAClE,QAAQ;AACJ,WAAO;AAAA,EACX,UAAE;AACE,iBAAa,KAAK;AAAA,EACtB;AACJ;AAGO,IAAM,cAAc,CAAC,iBAA2C;AACnE,QAAM,QAAQ,eAAe,WAAW;AACxC,SAAO,EAAE,SAAS,aAAa,eAAe,aAAa,CAAC,GAAG,WAAW,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI,WAAW,IAAI;AAC/H;AAGO,IAAM,iBAAiB,CAAC,YAAsC,SAAS,IAAI,SAAS,GAAG,SAAS;AAchG,IAAM,aAAa,CAAC,KAAa,EAAE,QAAQ,cAAc,SAAS,WAAW,MAChF,eAAe,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;AAOjG,IAAM,UAAU,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MACvE,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM,MAAM;AAGrE,IAAM,iBAAiB,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MAA4D;AAC1I,QAAM,IAAI,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM;AAC5E,SAAO,OAAO,MAAM,WAAW,IAAI;AACvC;AAGO,IAAM,WAAW,CAAC,EAAE,QAAQ,cAAc,SAAS,WAAW,MACjE,YAAY,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;","names":[]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Identity verification (0.16.0), the server side: signs a user id so MRRocket can tell that requests for it come from
3
+ * that user's own session, not from someone who only knows the id. Like Intercom's identity verification.
4
+ *
5
+ * // app/layout.tsx (a server component)
6
+ * const hash = user ? await userHash(user.id) : undefined;
7
+ * <CockpitProvider distinctId={user?.id} userHash={hash}> // or identify(user.id, props, { hash }) in the browser
8
+ *
9
+ * The formula, for a backend in another language (the server key is `mrr_sk_…`, MRRocket › Settings › Products ›
10
+ * Sign-in; MRRocket keeps only its SHA-256, which is why that is the HMAC key):
11
+ *
12
+ * userHash = lowercase hex( HMAC-SHA256( key = SHA-256(server key) as its 32 raw bytes, message = user id as UTF-8 ) )
13
+ *
14
+ * Only on your server: the server key must never reach a browser or an app bundle. WebCrypto only (`crypto.subtle`), so
15
+ * it runs on Node 18+ (Next.js provides `crypto` there; plain Node 18 needs `globalThis.crypto ??=
16
+ * require("node:crypto").webcrypto`), the Edge runtime, Cloudflare Workers, Deno and Bun.
17
+ */
18
+ export interface UserHashOptions {
19
+ /** The product's server key (`mrr_sk_…`). Defaults to `MRROCKET_SERVER_KEY`. */
20
+ serverKey?: string;
21
+ }
22
+ /**
23
+ * The hash of `userId` for `identify(id, props, { hash })` (64 lowercase hex characters). Throws when there is no
24
+ * server key (pass `serverKey` or set `MRROCKET_SERVER_KEY`) or no WebCrypto, so a missing setting shows at once on
25
+ * your server instead of as refused requests in the browser.
26
+ */
27
+ export declare const userHash: (userId: string, { serverKey }?: UserHashOptions) => Promise<string>;
@@ -0,0 +1,24 @@
1
+ // src/user-hash.ts
2
+ var envServerKey = () => {
3
+ try {
4
+ return typeof process !== "undefined" ? process.env.MRROCKET_SERVER_KEY : void 0;
5
+ } catch {
6
+ return void 0;
7
+ }
8
+ };
9
+ var hex = (buf) => Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
10
+ var userHash = async (userId, { serverKey } = {}) => {
11
+ if (typeof userId !== "string" || !userId) throw new Error("userHash: the user id must be a non-empty string (the same id you pass to identify).");
12
+ const key = (serverKey ?? envServerKey() ?? "").trim();
13
+ if (!key) throw new Error("userHash: no server key. Set MRROCKET_SERVER_KEY (MRRocket \u203A Settings \u203A Products \u203A your product \u203A Sign-in) or pass { serverKey }.");
14
+ const subtle = globalThis.crypto?.subtle;
15
+ if (!subtle) throw new Error('userHash needs WebCrypto (crypto.subtle). On plain Node 18 add: globalThis.crypto ??= require("node:crypto").webcrypto');
16
+ const utf8 = new TextEncoder();
17
+ const signingKey = await subtle.digest("SHA-256", utf8.encode(key));
18
+ const hmac = await subtle.importKey("raw", signingKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
19
+ return hex(await subtle.sign("HMAC", hmac, utf8.encode(userId)));
20
+ };
21
+ export {
22
+ userHash
23
+ };
24
+ //# sourceMappingURL=user-hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/user-hash.ts"],"sourcesContent":["/**\n * Identity verification (0.16.0), the server side: signs a user id so MRRocket can tell that requests for it come from\n * that user's own session, not from someone who only knows the id. Like Intercom's identity verification.\n *\n * // app/layout.tsx (a server component)\n * const hash = user ? await userHash(user.id) : undefined;\n * <CockpitProvider distinctId={user?.id} userHash={hash}> // or identify(user.id, props, { hash }) in the browser\n *\n * The formula, for a backend in another language (the server key is `mrr_sk_…`, MRRocket › Settings › Products ›\n * Sign-in; MRRocket keeps only its SHA-256, which is why that is the HMAC key):\n *\n * userHash = lowercase hex( HMAC-SHA256( key = SHA-256(server key) as its 32 raw bytes, message = user id as UTF-8 ) )\n *\n * Only on your server: the server key must never reach a browser or an app bundle. WebCrypto only (`crypto.subtle`), so\n * it runs on Node 18+ (Next.js provides `crypto` there; plain Node 18 needs `globalThis.crypto ??=\n * require(\"node:crypto\").webcrypto`), the Edge runtime, Cloudflare Workers, Deno and Bun.\n */\n\nexport interface UserHashOptions {\n /** The product's server key (`mrr_sk_…`). Defaults to `MRROCKET_SERVER_KEY`. */\n serverKey?: string;\n}\n\nconst envServerKey = () => {\n try {\n return typeof process !== \"undefined\" ? process.env.MRROCKET_SERVER_KEY : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst hex = (buf: ArrayBuffer) => Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n/**\n * The hash of `userId` for `identify(id, props, { hash })` (64 lowercase hex characters). Throws when there is no\n * server key (pass `serverKey` or set `MRROCKET_SERVER_KEY`) or no WebCrypto, so a missing setting shows at once on\n * your server instead of as refused requests in the browser.\n */\nexport const userHash = async (userId: string, { serverKey }: UserHashOptions = {}): Promise<string> => {\n if (typeof userId !== \"string\" || !userId) throw new Error(\"userHash: the user id must be a non-empty string (the same id you pass to identify).\");\n const key = (serverKey ?? envServerKey() ?? \"\").trim();\n if (!key) throw new Error(\"userHash: no server key. Set MRROCKET_SERVER_KEY (MRRocket › Settings › Products › your product › Sign-in) or pass { serverKey }.\");\n const subtle = (globalThis as { crypto?: Crypto }).crypto?.subtle;\n if (!subtle) throw new Error('userHash needs WebCrypto (crypto.subtle). On plain Node 18 add: globalThis.crypto ??= require(\"node:crypto\").webcrypto');\n const utf8 = new TextEncoder();\n const signingKey = await subtle.digest(\"SHA-256\", utf8.encode(key));\n const hmac = await subtle.importKey(\"raw\", signingKey, { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n return hex(await subtle.sign(\"HMAC\", hmac, utf8.encode(userId)));\n};\n"],"mappings":";AAuBA,IAAM,eAAe,MAAM;AACvB,MAAI;AACA,WAAO,OAAO,YAAY,cAAc,QAAQ,IAAI,sBAAsB;AAAA,EAC9E,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAEA,IAAM,MAAM,CAAC,QAAqB,MAAM,KAAK,IAAI,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAO1G,IAAM,WAAW,OAAO,QAAgB,EAAE,UAAU,IAAqB,CAAC,MAAuB;AACpG,MAAI,OAAO,WAAW,YAAY,CAAC,OAAQ,OAAM,IAAI,MAAM,sFAAsF;AACjJ,QAAM,OAAO,aAAa,aAAa,KAAK,IAAI,KAAK;AACrD,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uJAAmI;AAC7J,QAAM,SAAU,WAAmC,QAAQ;AAC3D,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,wHAAwH;AACrJ,QAAM,OAAO,IAAI,YAAY;AAC7B,QAAM,aAAa,MAAM,OAAO,OAAO,WAAW,KAAK,OAAO,GAAG,CAAC;AAClE,QAAM,OAAO,MAAM,OAAO,UAAU,OAAO,YAAY,EAAE,MAAM,QAAQ,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;AACzG,SAAO,IAAI,MAAM,OAAO,KAAK,QAAQ,MAAM,KAAK,OAAO,MAAM,CAAC,CAAC;AACnE;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrocket/sdk",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "MRRocket SDK: page views, events, heatmap clicks, session recordings, A/B tests, feature flags, error tracking, feedback board, surveys, a chat messenger, a cancel flow, onboarding tours and checklists, in-app messages, push notifications and redirect tests that your MRRocket agents can run and read. Next.js (@mrrocket/sdk/next), React Native and Expo (@mrrocket/sdk/react-native), and a script tag for any website.",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -100,6 +100,7 @@
100
100
  "expo-localization": ">=14",
101
101
  "expo-notifications": ">=0.20",
102
102
  "expo-router": ">=3",
103
+ "expo-store-review": ">=6",
103
104
  "next": ">=15",
104
105
  "react": ">=18",
105
106
  "react-dom": ">=18",
@@ -130,6 +131,9 @@
130
131
  "expo-router": {
131
132
  "optional": true
132
133
  },
134
+ "expo-store-review": {
135
+ "optional": true
136
+ },
133
137
  "next": {
134
138
  "optional": true
135
139
  },