@nodii/onboarding 0.1.0-dev.0 → 0.2.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 (68) hide show
  1. package/README.md +124 -0
  2. package/dist/{chunk-4SWOVHZ2.js → chunk-A3DE2IAH.js} +155 -18
  3. package/dist/chunk-A3DE2IAH.js.map +1 -0
  4. package/dist/{chunk-AFCJLUPK.js → chunk-DWFZA5T6.js} +19 -8
  5. package/dist/chunk-DWFZA5T6.js.map +1 -0
  6. package/dist/chunk-FIRSA7EH.js +3 -0
  7. package/dist/{chunk-ZDY2I6KY.js.map → chunk-FIRSA7EH.js.map} +1 -1
  8. package/dist/chunk-JFDQZ3AC.js +3 -0
  9. package/dist/chunk-JFDQZ3AC.js.map +1 -0
  10. package/dist/{chunk-34BQX2KR.js → chunk-KFHVZGX6.js} +61 -6
  11. package/dist/chunk-KFHVZGX6.js.map +1 -0
  12. package/dist/chunk-NDSBIB3N.js +99 -0
  13. package/dist/chunk-NDSBIB3N.js.map +1 -0
  14. package/dist/chunk-OLWR5COK.js +10 -0
  15. package/dist/chunk-OLWR5COK.js.map +1 -0
  16. package/dist/{chunk-SWMIIUXR.js → chunk-OVULPWNZ.js} +917 -158
  17. package/dist/chunk-OVULPWNZ.js.map +1 -0
  18. package/dist/chunk-SEKNVHD3.js +59 -0
  19. package/dist/chunk-SEKNVHD3.js.map +1 -0
  20. package/dist/chunk-T7EWURTF.js +321 -0
  21. package/dist/chunk-T7EWURTF.js.map +1 -0
  22. package/dist/{chunk-BQCTDU5V.js → chunk-UHOFGWI7.js} +90 -15
  23. package/dist/chunk-UHOFGWI7.js.map +1 -0
  24. package/dist/chunk-WKN4U7QF.js +35 -0
  25. package/dist/chunk-WKN4U7QF.js.map +1 -0
  26. package/dist/{chunk-AW7W3HAC.js → chunk-XJTJPBJB.js} +40 -3
  27. package/dist/chunk-XJTJPBJB.js.map +1 -0
  28. package/dist/chunk-ZR6X3GSM.js +186 -0
  29. package/dist/chunk-ZR6X3GSM.js.map +1 -0
  30. package/dist/client-NvqPpfQP.d.ts +86 -0
  31. package/dist/consent/index.d.ts +59 -0
  32. package/dist/consent/index.js +7 -0
  33. package/dist/consent/index.js.map +1 -0
  34. package/dist/controller-BLH3wnxl.d.ts +177 -0
  35. package/dist/dynamic/index.d.ts +65 -6
  36. package/dist/dynamic/index.js +5 -2
  37. package/dist/errors-vJkye4a0.d.ts +110 -0
  38. package/dist/form/index.d.ts +12 -4
  39. package/dist/form/index.js +2 -2
  40. package/dist/index-BMSbQGWP.d.ts +103 -0
  41. package/dist/index.d.ts +13 -7
  42. package/dist/index.js +13 -8
  43. package/dist/model-BI_I-D_V.d.ts +267 -0
  44. package/dist/{model-IEyDI54w.d.ts → model-CEwvHlWA.d.ts} +212 -46
  45. package/dist/render-EWQXF73I.js +5 -0
  46. package/dist/render-EWQXF73I.js.map +1 -0
  47. package/dist/schema-BrtjpXfL.d.ts +257 -0
  48. package/dist/{signup-client-Bl6eUrxR.d.ts → signup-client-CS95nkVO.d.ts} +152 -19
  49. package/dist/spec/index.d.ts +10 -81
  50. package/dist/spec/index.js +3 -2
  51. package/dist/submit/index.d.ts +7 -71
  52. package/dist/submit/index.js +4 -3
  53. package/dist/viewer/index.d.ts +4 -0
  54. package/dist/viewer/index.js +5 -0
  55. package/dist/viewer/index.js.map +1 -0
  56. package/dist/widget/index.d.ts +102 -20
  57. package/dist/widget/index.js +10 -7
  58. package/package.json +21 -2
  59. package/dist/chunk-34BQX2KR.js.map +0 -1
  60. package/dist/chunk-4SWOVHZ2.js.map +0 -1
  61. package/dist/chunk-AFCJLUPK.js.map +0 -1
  62. package/dist/chunk-AW7W3HAC.js.map +0 -1
  63. package/dist/chunk-BIFANV7M.js +0 -22
  64. package/dist/chunk-BIFANV7M.js.map +0 -1
  65. package/dist/chunk-BQCTDU5V.js.map +0 -1
  66. package/dist/chunk-SWMIIUXR.js.map +0 -1
  67. package/dist/chunk-ZDY2I6KY.js +0 -3
  68. package/dist/schema-TfyBU6JW.d.ts +0 -107
@@ -0,0 +1,321 @@
1
+ import { fetchDocumentContent } from './chunk-SEKNVHD3.js';
2
+ import { fetchWithTimeout } from './chunk-XJTJPBJB.js';
3
+ import createDOMPurify from 'dompurify';
4
+ import { marked } from 'marked';
5
+
6
+ var purifier = null;
7
+ function getPurifier() {
8
+ if (purifier) return purifier;
9
+ const win = globalThis.window ?? globalThis;
10
+ purifier = createDOMPurify(win);
11
+ return purifier;
12
+ }
13
+ var SCROLL_END_EPSILON = 2;
14
+ function clearNode(node) {
15
+ while (node.firstChild) node.removeChild(node.firstChild);
16
+ }
17
+ function boxHasContent(box) {
18
+ if ((box.textContent ?? "").trim().length > 0) return true;
19
+ return box.querySelector("canvas") !== null;
20
+ }
21
+ function makeScrollBox(control, consentKey) {
22
+ const box = document.createElement("div");
23
+ box.className = "nodii-onboarding-consent-scrollbox";
24
+ box.setAttribute("tabindex", "0");
25
+ box.setAttribute("role", "region");
26
+ const report = () => {
27
+ if (!boxHasContent(box)) return;
28
+ if (box.scrollHeight <= 0) return;
29
+ const ratio = box.scrollHeight - box.clientHeight <= 0 ? (
30
+ // Content is present AND genuinely fits without scrolling → "at end".
31
+ 1
32
+ ) : Math.min(
33
+ 1,
34
+ (box.scrollTop + box.clientHeight + SCROLL_END_EPSILON) / box.scrollHeight
35
+ );
36
+ control.reportScrollProgress(consentKey, ratio);
37
+ };
38
+ box.addEventListener("scroll", report);
39
+ return { box, reportScroll: report };
40
+ }
41
+ function isMarkdown(fetched, doc) {
42
+ return fetched.contentType.includes("markdown") || /\.mdx?(\?|#|$)/i.test(doc.downloadUrl);
43
+ }
44
+ var DANGEROUS_TAGS = /* @__PURE__ */ new Set([
45
+ "SCRIPT",
46
+ "IFRAME",
47
+ "OBJECT",
48
+ "EMBED",
49
+ "FORM",
50
+ "STYLE",
51
+ "LINK",
52
+ "META",
53
+ "BASE"
54
+ ]);
55
+ function normalizeUrlForSchemeCheck(value) {
56
+ let out = "";
57
+ for (let i = 0; i < value.length; i++) {
58
+ const cp = value.charCodeAt(i);
59
+ if (cp > 32 && cp !== 127) out += value[i];
60
+ }
61
+ return out.toLowerCase();
62
+ }
63
+ var DANGEROUS_URL_SCHEMES = ["javascript:", "vbscript:", "data:text/html"];
64
+ function hardenElement(root) {
65
+ const walker = (node) => {
66
+ const children = Array.from(node.children);
67
+ if (DANGEROUS_TAGS.has(node.tagName)) {
68
+ node.remove();
69
+ return;
70
+ }
71
+ for (const attr of Array.from(node.attributes)) {
72
+ const name = attr.name.toLowerCase();
73
+ const value = normalizeUrlForSchemeCheck(attr.value);
74
+ if (name.startsWith("on")) {
75
+ node.removeAttribute(attr.name);
76
+ continue;
77
+ }
78
+ if ((name === "href" || name === "src" || name === "xlink:href") && DANGEROUS_URL_SCHEMES.some((s) => value.startsWith(s))) {
79
+ node.removeAttribute(attr.name);
80
+ }
81
+ if (name === "srcdoc") node.removeAttribute(attr.name);
82
+ }
83
+ for (const child of children) walker(child);
84
+ };
85
+ for (const child of Array.from(root.children)) walker(child);
86
+ }
87
+ function renderSanitizedHtml(container, control, consentKey, html) {
88
+ clearNode(container);
89
+ const { box, reportScroll } = makeScrollBox(control, consentKey);
90
+ const body = document.createElement("div");
91
+ body.className = "nodii-onboarding-consent-doc-body";
92
+ body.innerHTML = getPurifier().sanitize(html, {
93
+ FORBID_TAGS: ["iframe", "object", "embed", "form"],
94
+ FORBID_ATTR: ["srcdoc"],
95
+ ADD_ATTR: ["target", "rel"]
96
+ });
97
+ hardenElement(body);
98
+ box.appendChild(body);
99
+ container.appendChild(box);
100
+ reportScroll();
101
+ }
102
+ function renderPlainText(container, control, consentKey, text) {
103
+ clearNode(container);
104
+ const { box, reportScroll } = makeScrollBox(control, consentKey);
105
+ const body = document.createElement("div");
106
+ body.className = "nodii-onboarding-consent-doc-body";
107
+ body.textContent = text;
108
+ box.appendChild(body);
109
+ container.appendChild(box);
110
+ reportScroll();
111
+ }
112
+ function renderOpenGateAffordance(container, control, consentKey, doc, note) {
113
+ clearNode(container);
114
+ const link = document.createElement("a");
115
+ link.className = "nodii-onboarding-consent-open";
116
+ link.href = doc.downloadUrl;
117
+ link.target = "_blank";
118
+ link.rel = "noopener noreferrer";
119
+ link.textContent = `Open ${doc.title}`;
120
+ const markOpened = () => control.markOpened(consentKey);
121
+ link.addEventListener("click", markOpened);
122
+ link.addEventListener("keydown", (event) => {
123
+ if (event.key === "Enter") markOpened();
124
+ });
125
+ const row = document.createElement("div");
126
+ row.className = "nodii-onboarding-consent-open-row";
127
+ row.appendChild(link);
128
+ if (note) {
129
+ const noteEl = document.createElement("p");
130
+ noteEl.className = "nodii-onboarding-consent-note";
131
+ noteEl.textContent = note;
132
+ row.appendChild(noteEl);
133
+ }
134
+ container.appendChild(row);
135
+ }
136
+ function renderUnrenderableError(container, doc, onRetry) {
137
+ clearNode(container);
138
+ const wrap = document.createElement("div");
139
+ wrap.className = "nodii-onboarding-consent-unrenderable";
140
+ wrap.setAttribute("role", "alert");
141
+ const msg = document.createElement("p");
142
+ msg.className = "nodii-onboarding-consent-error";
143
+ msg.textContent = `\u201C${doc.title}\u201D could not be loaded, so it cannot be accepted yet. Please retry; if it keeps failing, contact support.`;
144
+ wrap.appendChild(msg);
145
+ const retry = document.createElement("button");
146
+ retry.type = "button";
147
+ retry.className = "nodii-onboarding-consent-retry";
148
+ retry.textContent = "Retry loading document";
149
+ retry.addEventListener("click", onRetry);
150
+ wrap.appendChild(retry);
151
+ const link = document.createElement("a");
152
+ link.className = "nodii-onboarding-consent-open";
153
+ link.href = doc.downloadUrl;
154
+ link.target = "_blank";
155
+ link.rel = "noopener noreferrer";
156
+ link.textContent = `Open ${doc.title} in a new tab`;
157
+ wrap.appendChild(link);
158
+ container.appendChild(wrap);
159
+ }
160
+ async function renderPdf(container, control, consentKey, doc, blob, onRetry) {
161
+ const fail = () => handleRenderFailure(container, control, consentKey, doc, onRetry);
162
+ let pdfjs;
163
+ try {
164
+ pdfjs = await import('pdfjs-dist');
165
+ } catch {
166
+ return fail();
167
+ }
168
+ try {
169
+ await configurePdfWorker(pdfjs);
170
+ const data = new Uint8Array(await blob.arrayBuffer());
171
+ const pdf = await pdfjs.getDocument({ data }).promise;
172
+ const body = document.createElement("div");
173
+ body.className = "nodii-onboarding-consent-doc-body nodii-consent-pdf";
174
+ for (let pageNo = 1; pageNo <= pdf.numPages; pageNo++) {
175
+ const page = await pdf.getPage(pageNo);
176
+ const viewport = page.getViewport({ scale: 1.2 });
177
+ const canvas = document.createElement("canvas");
178
+ canvas.className = "nodii-consent-pdf-page";
179
+ canvas.width = Math.ceil(viewport.width);
180
+ canvas.height = Math.ceil(viewport.height);
181
+ const ctx = canvas.getContext("2d");
182
+ if (ctx) {
183
+ await page.render({ canvas, canvasContext: ctx, viewport }).promise;
184
+ }
185
+ body.appendChild(canvas);
186
+ }
187
+ if (typeof container.isConnected === "boolean" && !container.isConnected) {
188
+ return { renderedAs: "unrenderable" };
189
+ }
190
+ if (body.childElementCount === 0) return fail();
191
+ clearNode(container);
192
+ const { box, reportScroll } = makeScrollBox(control, consentKey);
193
+ box.appendChild(body);
194
+ container.appendChild(box);
195
+ reportScroll();
196
+ return { renderedAs: "pdf" };
197
+ } catch {
198
+ return fail();
199
+ }
200
+ }
201
+ var pdfWorkerReady = null;
202
+ var WORKER_PROBE_TIMEOUT_MS = 5e3;
203
+ function resolvePackagedWorkerUrl() {
204
+ const WORKER_SPEC = "pdfjs-dist/build/pdf.worker.min.mjs";
205
+ const meta = import.meta;
206
+ if (typeof meta.resolve === "function") {
207
+ try {
208
+ const resolved = meta.resolve(WORKER_SPEC);
209
+ if (resolved) return resolved;
210
+ } catch {
211
+ }
212
+ }
213
+ try {
214
+ return new URL(WORKER_SPEC, import.meta.url).href;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+ function configurePdfWorker(pdfjs) {
220
+ pdfWorkerReady ??= (async () => {
221
+ const opts = pdfjs.GlobalWorkerOptions;
222
+ if (opts.workerPort || opts.workerSrc) return;
223
+ opts.workerSrc = await _pickPdfWorkerSrc(
224
+ resolvePackagedWorkerUrl(),
225
+ pdfjs.version ?? ""
226
+ );
227
+ })();
228
+ return pdfWorkerReady;
229
+ }
230
+ async function _pickPdfWorkerSrc(packagedUrl, apiVersion, fetchImpl = globalThis.fetch, timeoutMs = WORKER_PROBE_TIMEOUT_MS) {
231
+ const cdn = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${apiVersion}/build/pdf.worker.min.mjs`;
232
+ if (!packagedUrl) return cdn;
233
+ if (!/^https?:/i.test(packagedUrl)) return packagedUrl;
234
+ try {
235
+ const res = await fetchWithTimeout(packagedUrl, {
236
+ credentials: "omit",
237
+ fetchImpl,
238
+ timeoutMs
239
+ });
240
+ const type = res.headers.get("content-type") ?? "";
241
+ await res.body?.cancel().catch(() => {
242
+ });
243
+ return res.ok && /javascript|ecmascript/i.test(type) ? packagedUrl : cdn;
244
+ } catch {
245
+ return cdn;
246
+ }
247
+ }
248
+ function handleRenderFailure(container, control, consentKey, doc, onRetry) {
249
+ if (doc.acceptanceLevel === "open_then_accept") {
250
+ renderOpenGateAffordance(
251
+ container,
252
+ control,
253
+ consentKey,
254
+ doc,
255
+ "The inline preview is unavailable. Opening the document in a new tab satisfies this step."
256
+ );
257
+ return { renderedAs: "open-affordance" };
258
+ }
259
+ renderUnrenderableError(container, doc, onRetry);
260
+ return { renderedAs: "unrenderable" };
261
+ }
262
+ async function renderConsentDocument(container, doc, control, options) {
263
+ const { consentKey } = options;
264
+ const onRetry = () => {
265
+ void renderConsentDocument(container, doc, control, options);
266
+ };
267
+ let fetched;
268
+ try {
269
+ fetched = await fetchDocumentContent(doc, {
270
+ fetch: options.fetch,
271
+ timeoutMs: options.timeoutMs
272
+ });
273
+ } catch {
274
+ return handleRenderFailure(container, control, consentKey, doc, onRetry);
275
+ }
276
+ if (typeof container.isConnected === "boolean" && !container.isConnected) {
277
+ return { renderedAs: "unrenderable" };
278
+ }
279
+ if (doc.acceptanceLevel === "open_then_accept") {
280
+ renderOpenGateAffordance(
281
+ container,
282
+ control,
283
+ consentKey,
284
+ doc,
285
+ "Open the document in a new tab to review it \u2014 that satisfies this step."
286
+ );
287
+ return { renderedAs: "open-affordance" };
288
+ }
289
+ if (fetched.kind === "pdf" && fetched.blob) {
290
+ return renderPdf(
291
+ container,
292
+ control,
293
+ consentKey,
294
+ doc,
295
+ fetched.blob,
296
+ onRetry
297
+ );
298
+ }
299
+ if (fetched.kind === "binary") {
300
+ return handleRenderFailure(container, control, consentKey, doc, onRetry);
301
+ }
302
+ const text = fetched.text ?? "";
303
+ if (isMarkdown(fetched, doc)) {
304
+ const html = await marked.parse(text, { async: true });
305
+ renderSanitizedHtml(container, control, consentKey, html);
306
+ return { renderedAs: "markdown" };
307
+ }
308
+ if (fetched.kind === "html") {
309
+ renderSanitizedHtml(container, control, consentKey, text);
310
+ return { renderedAs: "html" };
311
+ }
312
+ renderPlainText(container, control, consentKey, text);
313
+ return { renderedAs: "text" };
314
+ }
315
+ function _resetPdfWorkerState() {
316
+ pdfWorkerReady = null;
317
+ }
318
+
319
+ export { _pickPdfWorkerSrc, _resetPdfWorkerState, renderConsentDocument };
320
+ //# sourceMappingURL=chunk-T7EWURTF.js.map
321
+ //# sourceMappingURL=chunk-T7EWURTF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/viewer/render.ts"],"names":[],"mappings":"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","file":"chunk-T7EWURTF.js","sourcesContent":["/**\n * Default ConsentPack document viewer (D543 §5) — @nodii/onboarding/viewer.\n *\n * A SEPARATE, opt-in module built on STANDARD libraries so the base SDK stays\n * lean: consumers only pay for the viewer if they render our default consent\n * components. The headless controller (@nodii/onboarding/consent) knows nothing\n * about this — it just receives reportScrollProgress / markScrolledToEnd /\n * markOpened from whatever renders the doc.\n *\n * Renderers (per fetched artifact — NO IFRAME anywhere; everything is inline,\n * same-origin, so `scroll_then_accept` scroll tracking is REAL):\n * - text/html → sanitize with DOMPurify, inject into a scroll <div>.\n * - markdown → marked → DOMPurify → inject into a scroll <div>.\n * - text/plain → escaped text (textContent) in a scroll <div>.\n * - application/pdf → PDF.js (pdfjs-dist), rendered to <canvas> + a text\n * layer, page by page, in a scroll <div>. PDF.js is LAZY-loaded via a\n * dynamic import() ONLY when a PDF doc is actually present, so it never\n * bloats the base bundle.\n *\n * The scroll container's scroll-to-end (and the PDF/open fallbacks) drive the\n * consent controller so the gate works for real. A failed fetch/render\n * degrades to an \"open in a new tab\" affordance (marks the doc opened /\n * scrolled) so every doc stays consentable.\n *\n * Bundle discipline: DOMPurify + marked are imported at module top (this is\n * the VIEWER entry, not the base SDK — the base `@nodii/onboarding` never\n * statically imports this file). pdfjs-dist is imported ONLY via dynamic\n * import() inside renderPdf, so even the viewer entry doesn't eagerly load it.\n */\n\nimport createDOMPurify from \"dompurify\";\nimport { marked } from \"marked\";\nimport type { ConsentController } from \"../consent/controller.js\";\nimport type { ConsentDocument } from \"../consent/model.js\";\nimport { fetchWithTimeout } from \"../submit/http.js\";\nimport {\n fetchDocumentContent,\n type FetchedConsentDocument,\n} from \"../consent/document.js\";\n\n/**\n * DOMPurify bound to the current window. The default import is BOTH a\n * pre-bound instance AND a factory `(window) => instance`. Binding it\n * explicitly to `globalThis` (which happy-dom / a real browser populate with\n * `window`) guarantees a working DOM even in environments where DOMPurify's\n * implicit global detection misfires (e.g. bun + happy-dom under test), where\n * an unbound instance silently passes HTML through UN-sanitized. Lazily\n * memoized so we only bind once.\n */\nlet purifier: ReturnType<typeof createDOMPurify> | null = null;\nfunction getPurifier(): ReturnType<typeof createDOMPurify> {\n if (purifier) return purifier;\n const win = (globalThis as { window?: unknown }).window ?? globalThis;\n // createDOMPurify is callable as a factory; when the arg is a window-like\n // object it returns an instance bound to it.\n purifier = (\n createDOMPurify as unknown as (\n w: unknown,\n ) => ReturnType<typeof createDOMPurify>\n )(win);\n return purifier;\n}\n\nexport interface ConsentDocumentViewerOptions {\n /** Injectable fetch for the presigned artifact GET (defaults to global). */\n fetch?: typeof globalThis.fetch;\n /** Per-request timeout in ms (default from fetchDocumentContent). */\n timeoutMs?: number;\n /**\n * The `key` the controller identifies this document by (doc_type |\n * custom:<key>). The viewer reports scroll/open evidence under this key.\n */\n consentKey: string;\n}\n\n/** Result of rendering — lets the caller know how the doc was displayed. */\nexport interface ConsentDocumentViewerResult {\n /**\n * How the artifact was rendered.\n *\n * - html | markdown | text | pdf → rendered inline (gate is driven by real\n * scroll/open).\n * - open-affordance → an `open_then_accept` doc that opens in a new tab (the\n * real-open IS its gate; satisfies ONLY `opened`).\n * - unrenderable → a GATED doc (scroll_then_accept) that could NOT be\n * rendered inline. The gate stays UNSATISFIABLE — accept is disabled and a\n * clear error is shown. NO evidence is ever fabricated (COMPLIANCE, D543).\n */\n renderedAs:\n \"html\" | \"markdown\" | \"text\" | \"pdf\" | \"open-affordance\" | \"unrenderable\";\n}\n\nconst SCROLL_END_EPSILON = 2;\n\nfunction clearNode(node: HTMLElement): void {\n while (node.firstChild) node.removeChild(node.firstChild);\n}\n\n/**\n * Does the box hold ACTUAL readable rendered content? A \"content fits without\n * scrolling\" ratio of 1 is only legitimate when there is genuinely something to\n * have read. Content = non-empty text (HTML/markdown/text render) OR at least\n * one rendered PDF page canvas. A bare wrapper `<div>` with empty text is NOT\n * content — so an empty-artifact doc (e.g. a 0-byte or all-whitespace body, or\n * a not-yet-populated box) can NEVER satisfy the gate. This is the P0a\n * by-construction guarantee: the ratio-1 branch depends on rendered-content\n * presence, not on geometry a headless runtime cannot measure — fabrication is\n * impossible in EVERY runtime.\n */\nfunction boxHasContent(box: HTMLElement): boolean {\n if ((box.textContent ?? \"\").trim().length > 0) return true;\n // PDF pages carry no text — a rendered page canvas counts as content.\n return box.querySelector(\"canvas\") !== null;\n}\n\n/**\n * A scroll <div> that reports scroll progress to the controller. Returns the\n * box AND a `reportScroll` fn the caller MUST invoke ONLY AFTER real content is\n * appended — never at creation. `report` refuses to satisfy the \"fits without\n * scrolling\" case unless the box actually holds content, so an empty or\n * pre-render box can never latch scrolled_to_end (P0a — by construction).\n */\nfunction makeScrollBox(\n control: ConsentController,\n consentKey: string,\n): { box: HTMLDivElement; reportScroll: () => void } {\n const box = document.createElement(\"div\");\n box.className = \"nodii-onboarding-consent-scrollbox\";\n box.setAttribute(\"tabindex\", \"0\");\n box.setAttribute(\"role\", \"region\");\n const report = (): void => {\n // COMPLIANCE (P0a): an empty / not-yet-rendered box can NEVER satisfy the\n // gate — there is nothing to have read. This holds in EVERY runtime,\n // independent of scroll geometry (which a headless DOM cannot measure).\n if (!boxHasContent(box)) return;\n // Unmeasured container (no layout yet / happy-dom) → do NOT report a\n // fits-without-scroll: scrollHeight 0 is not evidence the user reached the\n // end. A real scroll event still reports its measured ratio.\n if (box.scrollHeight <= 0) return;\n const ratio =\n box.scrollHeight - box.clientHeight <= 0\n ? // Content is present AND genuinely fits without scrolling → \"at end\".\n 1\n : Math.min(\n 1,\n (box.scrollTop + box.clientHeight + SCROLL_END_EPSILON) /\n box.scrollHeight,\n );\n control.reportScrollProgress(consentKey, ratio);\n };\n box.addEventListener(\"scroll\", report);\n // NOTE: no report is scheduled at creation. The caller invokes reportScroll()\n // AFTER content is appended (short-doc auto-unlock then only fires with\n // content present). This is what makes P0a a construction guarantee.\n return { box, reportScroll: report };\n}\n\n/** Detect markdown by content-type or a .md-looking downloadUrl. */\nfunction isMarkdown(\n fetched: FetchedConsentDocument,\n doc: Pick<ConsentDocument, \"downloadUrl\">,\n): boolean {\n return (\n fetched.contentType.includes(\"markdown\") ||\n /\\.mdx?(\\?|#|$)/i.test(doc.downloadUrl)\n );\n}\n\n/** Tags that must never survive into the rendered consent document. */\nconst DANGEROUS_TAGS = new Set([\n \"SCRIPT\",\n \"IFRAME\",\n \"OBJECT\",\n \"EMBED\",\n \"FORM\",\n \"STYLE\",\n \"LINK\",\n \"META\",\n \"BASE\",\n]);\n\n/**\n * Defense-in-depth pass AFTER DOMPurify: walk the parsed DOM and remove any\n * dangerous element, every on* event-handler attribute, and javascript:/data:\n * URLs. DOMPurify is the primary, audited sanitizer, but this deterministic\n * DOM walk (a) hardens against any DOMPurify config gap and (b) is provably\n * correct in EVERY runtime — including ones where DOMPurify's own DOM-based\n * pass is a no-op (e.g. certain SSR/test DOMs). Belt-and-suspenders, not a\n * hand-rolled replacement for DOMPurify.\n */\n/**\n * Normalize a URL for scheme inspection the way a browser does BEFORE\n * resolving it: strip ALL ASCII whitespace and control characters (code\n * points <= 0x20, plus DEL 0x7F) anywhere in the string, not just the ends.\n * Browsers ignore these inside a URL, so a scheme split by an embedded tab/\n * newline/NUL (e.g. jav<TAB>ascript:) still executes; a mere .trim() leaves\n * the embedded ones and the startsWith('javascript:') check is bypassed.\n * Char-code filtering avoids embedding literal control bytes in a regex.\n */\nfunction normalizeUrlForSchemeCheck(value: string): string {\n let out = \"\";\n for (let i = 0; i < value.length; i++) {\n const cp = value.charCodeAt(i);\n if (cp > 0x20 && cp !== 0x7f) out += value[i];\n }\n return out.toLowerCase();\n}\n\nconst DANGEROUS_URL_SCHEMES = [\"javascript:\", \"vbscript:\", \"data:text/html\"];\n\nfunction hardenElement(root: HTMLElement): void {\n const walker = (node: Element): void => {\n // Copy children first — removal mutates the live list.\n const children = Array.from(node.children);\n if (DANGEROUS_TAGS.has(node.tagName)) {\n node.remove();\n return;\n }\n for (const attr of Array.from(node.attributes)) {\n const name = attr.name.toLowerCase();\n // Whitespace/control-stripped, lower-cased value for scheme inspection.\n const value = normalizeUrlForSchemeCheck(attr.value);\n if (name.startsWith(\"on\")) {\n node.removeAttribute(attr.name);\n continue;\n }\n if (\n (name === \"href\" || name === \"src\" || name === \"xlink:href\") &&\n DANGEROUS_URL_SCHEMES.some((s) => value.startsWith(s))\n ) {\n node.removeAttribute(attr.name);\n }\n if (name === \"srcdoc\") node.removeAttribute(attr.name);\n }\n for (const child of children) walker(child);\n };\n for (const child of Array.from(root.children)) walker(child);\n}\n\n/** Render sanitized HTML inline into a scroll box. */\nfunction renderSanitizedHtml(\n container: HTMLElement,\n control: ConsentController,\n consentKey: string,\n html: string,\n): void {\n clearNode(container);\n const { box, reportScroll } = makeScrollBox(control, consentKey);\n const body = document.createElement(\"div\");\n body.className = \"nodii-onboarding-consent-doc-body\";\n // Primary: DOMPurify (the standard, audited Cure53 sanitizer). FORBID_TAGS\n // on <iframe>/<object>/<embed>/<form> so a doc can't smuggle a frame in.\n body.innerHTML = getPurifier().sanitize(html, {\n FORBID_TAGS: [\"iframe\", \"object\", \"embed\", \"form\"],\n FORBID_ATTR: [\"srcdoc\"],\n ADD_ATTR: [\"target\", \"rel\"],\n });\n // Defense-in-depth: a deterministic DOM walk that removes any residual\n // dangerous node/attr, correct in every runtime (see hardenElement).\n hardenElement(body);\n box.appendChild(body);\n container.appendChild(box);\n // Content is now IN the box → the short-doc auto-unlock may fire (P0a: report\n // ONLY after content is present, never at box creation).\n reportScroll();\n}\n\n/** Render escaped plain text inline into a scroll box. */\nfunction renderPlainText(\n container: HTMLElement,\n control: ConsentController,\n consentKey: string,\n text: string,\n): void {\n clearNode(container);\n const { box, reportScroll } = makeScrollBox(control, consentKey);\n const body = document.createElement(\"div\");\n body.className = \"nodii-onboarding-consent-doc-body\";\n body.textContent = text; // safe-by-construction (no markup interpretation)\n box.appendChild(body);\n container.appendChild(box);\n reportScroll(); // only after content is present (P0a)\n}\n\n/**\n * The LEGITIMATE gate affordance for an `open_then_accept` doc: an open-in-new\n * tab link. Opening the doc in a new tab IS the gate for this level, so a REAL\n * navigation satisfies ONLY `opened` (never `scrolled_to_end`). A native\n * <a target=\"_blank\"> navigates on click AND on Enter (keyboard activation of\n * an anchor) — we mark `opened` on those, which correspond to a real open. We\n * do NOT bind Space (Space does not activate an anchor → no navigation → no\n * open → must not mark opened; binding it would fabricate evidence).\n *\n * This is used both as the primary affordance for open_then_accept and as the\n * failure fallback for open_then_accept when inline preview is unavailable —\n * either way the gate is genuinely the real open.\n */\nfunction renderOpenGateAffordance(\n container: HTMLElement,\n control: ConsentController,\n consentKey: string,\n doc: Pick<ConsentDocument, \"downloadUrl\" | \"title\">,\n note?: string,\n): void {\n clearNode(container);\n const link = document.createElement(\"a\");\n link.className = \"nodii-onboarding-consent-open\";\n link.href = doc.downloadUrl;\n link.target = \"_blank\";\n link.rel = \"noopener noreferrer\";\n link.textContent = `Open ${doc.title}`;\n // Mark opened ONLY on real activation of the anchor (click / Enter). No\n // Space handler — Space does not navigate an <a>, so it is not a real open.\n const markOpened = (): void => control.markOpened(consentKey);\n link.addEventListener(\"click\", markOpened);\n link.addEventListener(\"keydown\", (event) => {\n if ((event as KeyboardEvent).key === \"Enter\") markOpened();\n });\n const row = document.createElement(\"div\");\n row.className = \"nodii-onboarding-consent-open-row\";\n row.appendChild(link);\n if (note) {\n const noteEl = document.createElement(\"p\");\n noteEl.className = \"nodii-onboarding-consent-note\";\n noteEl.textContent = note;\n row.appendChild(noteEl);\n }\n container.appendChild(row);\n}\n\n/**\n * COMPLIANCE-CRITICAL (P0, D543): a GATED doc whose acceptance requires SEEING\n * the content inline (scroll_then_accept) could NOT be rendered inline (fetch\n * failure, expired presigned URL, CORS, octet-stream, PDF.js failure, viewer\n * unavailable). The gate MUST stay UNSATISFIABLE — the SDK NEVER fabricates\n * `scrolled_to_end` on a mere click. Render a clear error + a Retry, and an\n * \"Open in a new tab\" link that is INFORMATIONAL ONLY (it does NOT satisfy the\n * scroll gate — reading in a new tab is not evidence the user scrolled the\n * rendered doc to the end for this level). Accept stays disabled; the operator\n * must fix the artifact/URL for this doc to become acceptable.\n */\nfunction renderUnrenderableError(\n container: HTMLElement,\n doc: Pick<ConsentDocument, \"downloadUrl\" | \"title\">,\n onRetry: () => void,\n): void {\n clearNode(container);\n const wrap = document.createElement(\"div\");\n wrap.className = \"nodii-onboarding-consent-unrenderable\";\n wrap.setAttribute(\"role\", \"alert\");\n\n const msg = document.createElement(\"p\");\n msg.className = \"nodii-onboarding-consent-error\";\n msg.textContent = `“${doc.title}” could not be loaded, so it cannot be accepted yet. Please retry; if it keeps failing, contact support.`;\n wrap.appendChild(msg);\n\n const retry = document.createElement(\"button\");\n retry.type = \"button\";\n retry.className = \"nodii-onboarding-consent-retry\";\n retry.textContent = \"Retry loading document\";\n retry.addEventListener(\"click\", onRetry);\n wrap.appendChild(retry);\n\n // Informational only — opening in a new tab does NOT satisfy the scroll gate.\n const link = document.createElement(\"a\");\n link.className = \"nodii-onboarding-consent-open\";\n link.href = doc.downloadUrl;\n link.target = \"_blank\";\n link.rel = \"noopener noreferrer\";\n link.textContent = `Open ${doc.title} in a new tab`;\n wrap.appendChild(link);\n\n container.appendChild(wrap);\n}\n\n/**\n * Render a PDF via PDF.js into a scroll box (canvas per page). pdfjs-dist is\n * LAZY-loaded here (dynamic import) so it never enters the base bundle. On any\n * failure (import/worker/CSP/parse/zero-page), route through handleRenderFailure\n * — for a scroll_then_accept doc that is UNRENDERABLE (gate stays unsatisfiable,\n * never fabricated); for open_then_accept it is the real-open affordance (P0).\n * The scroll box is only attached (and progress only reported) AFTER pages are\n * rendered, so an empty/pre-render box can never satisfy the gate (P0a).\n */\nasync function renderPdf(\n container: HTMLElement,\n control: ConsentController,\n consentKey: string,\n doc: ConsentDocument,\n blob: Blob,\n onRetry: () => void,\n): Promise<ConsentDocumentViewerResult> {\n const fail = (): ConsentDocumentViewerResult =>\n handleRenderFailure(container, control, consentKey, doc, onRetry);\n\n let pdfjs: typeof import(\"pdfjs-dist\");\n try {\n pdfjs = await import(\"pdfjs-dist\");\n } catch {\n return fail();\n }\n\n try {\n // Configure the worker so getDocument does not reject on\n // \"No GlobalWorkerOptions.workerSrc specified\" (pdfjs-dist v6). Resolve the\n // packaged worker via a bundler-friendly URL; if that fails, fall back to\n // the CDN for the exact installed version (never leaves the worker unset).\n await configurePdfWorker(pdfjs);\n const data = new Uint8Array(await blob.arrayBuffer());\n const pdf = await pdfjs.getDocument({ data }).promise;\n\n // Render EVERY page into a DETACHED body FIRST — the scroll box is not put\n // into the container (and reportScroll is not called) until real content is\n // present (P0a: an empty/pre-render box can never satisfy the gate, even in\n // a real browser where an empty 360px box measures scrollHeight==clientH).\n const body = document.createElement(\"div\");\n body.className = \"nodii-onboarding-consent-doc-body nodii-consent-pdf\";\n for (let pageNo = 1; pageNo <= pdf.numPages; pageNo++) {\n const page = await pdf.getPage(pageNo);\n const viewport = page.getViewport({ scale: 1.2 });\n const canvas = document.createElement(\"canvas\");\n canvas.className = \"nodii-consent-pdf-page\";\n canvas.width = Math.ceil(viewport.width);\n canvas.height = Math.ceil(viewport.height);\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n await page.render({ canvas, canvasContext: ctx, viewport }).promise;\n }\n body.appendChild(canvas);\n }\n\n // Lifecycle guard: if the container was detached while we awaited the PDF\n // (stage re-render / unmount / a Retry started a fresh render), abandon —\n // don't mutate an orphaned tree or fight a concurrent render (P2 lifecycle).\n if (typeof container.isConnected === \"boolean\" && !container.isConnected) {\n return { renderedAs: \"unrenderable\" };\n }\n // A zero-page or all-empty PDF has no readable content → treat as a render\n // failure (never satisfy a gate on an empty box).\n if (body.childElementCount === 0) return fail();\n\n clearNode(container);\n const { box, reportScroll } = makeScrollBox(control, consentKey);\n box.appendChild(body);\n container.appendChild(box);\n // Content is now present → the short-PDF auto-unlock may fire (P0a).\n reportScroll();\n return { renderedAs: \"pdf\" };\n } catch {\n return fail();\n }\n}\n\n/** The session's one worker configuration, shared by concurrent renders. */\nlet pdfWorkerReady: Promise<void> | null = null;\n\n/** How long the packaged-worker probe may take before the CDN is used. */\nconst WORKER_PROBE_TIMEOUT_MS = 5000;\n\ninterface WorkerGlobalOptions {\n workerSrc?: string;\n workerPort?: unknown;\n}\n\n/**\n * Resolve the packaged pdf worker module to a LOADABLE URL, robust across\n * runtimes:\n * 1. `import.meta.resolve(...)` — real module resolution (Node/bun ESM);\n * resolves through node_modules to the actual file (the plain\n * `new URL(spec, import.meta.url)` does NOT — it's relative to THIS file,\n * which 404s, the exact P2 dead-fallback bug).\n * 2. `new URL(\"pdfjs-dist/build/pdf.worker.min.mjs\", import.meta.url)`, a\n * candidate only. A bundler does NOT rewrite this bare-specifier form: in\n * a Vite build it points at `/assets/pdfjs-dist/...`, which does not exist,\n * and a SPA host answers it with its HTML page (proven on the Nodii\n * website, 2026-10-25). The caller probes it before use.\n * Returns null if neither yields a URL (→ caller falls back to the CDN).\n */\nfunction resolvePackagedWorkerUrl(): string | null {\n const WORKER_SPEC = \"pdfjs-dist/build/pdf.worker.min.mjs\";\n const meta = import.meta as ImportMeta & {\n resolve?: (spec: string) => string;\n };\n if (typeof meta.resolve === \"function\") {\n try {\n const resolved = meta.resolve(WORKER_SPEC);\n if (resolved) return resolved;\n } catch {\n // fall through to the bundler-rewrite form\n }\n }\n try {\n return new URL(WORKER_SPEC, import.meta.url).href;\n } catch {\n return null;\n }\n}\n\n/**\n * Ensure pdfjs has a LOADABLE worker before getDocument (P2). Real pdfjs-dist\n * v6 rejects getDocument without one, and merely SETTING `workerSrc` is not the\n * same as the worker LOADING: the naive `new URL('pdfjs-dist/build/...',\n * import.meta.url)` resolves RELATIVE TO THIS MODULE (not through\n * node_modules), so it points at a path that 404s at load — the dead-fallback\n * bug P2 flags. We resolve the worker to a genuinely-loadable URL instead\n * (`import.meta.resolve` → real module resolution; else the bundler-rewrite\n * `new URL` form; else the version-pinned CDN).\n *\n * We set `workerSrc` (not `workerPort`): a resolved file/asset/CDN URL loads in\n * BOTH a real browser (bundler-served or CDN) AND Node/bun (resolved file\n * path) — proven by the [P2] real-pdfjs test which renders a real PDF with the\n * worker configured here and NO pre-set workerSrc. If the worker still can't\n * load (offline + unresolvable), getDocument rejects and the caller degrades\n * to `unrenderable` — a gated PDF that can't render stays unacceptable, NEVER\n * fabricated (P0). Idempotent per session.\n */\nfunction configurePdfWorker(pdfjs: typeof import(\"pdfjs-dist\")): Promise<void> {\n pdfWorkerReady ??= (async () => {\n const opts = pdfjs.GlobalWorkerOptions as WorkerGlobalOptions;\n // A consumer (or a prior call) already wired a worker → respect it.\n if (opts.workerPort || opts.workerSrc) return;\n opts.workerSrc = await _pickPdfWorkerSrc(\n resolvePackagedWorkerUrl(),\n (pdfjs as { version?: string }).version ?? \"\",\n );\n })();\n return pdfWorkerReady;\n}\n\n/**\n * The worker URL to use: the packaged candidate when it really serves a\n * script, else the CDN build of the exact API version that is loaded (so the\n * API and the worker can never disagree on version).\n *\n * An http(s) candidate is fetched and judged the way the browser will judge a\n * module worker: a 2xx with a JavaScript MIME type. A host's HTML fallback page\n * or a 404 fails, so a bundled page no longer ends up with a worker that cannot\n * load and every scroll-to-accept PDF unrenderable. A file: or blob: candidate\n * comes from the runtime's own module resolution and is used as is.\n *\n * The probe is bounded like every other request the SDK makes: its answer is\n * kept for the session and awaited before every PDF, so a host that accepts\n * the connection and then stalls must cost one timeout, not every PDF on the\n * page. A probe that times out counts as \"not a script\".\n *\n * Exported for tests only.\n */\nexport async function _pickPdfWorkerSrc(\n packagedUrl: string | null,\n apiVersion: string,\n fetchImpl: typeof globalThis.fetch = globalThis.fetch,\n timeoutMs = WORKER_PROBE_TIMEOUT_MS,\n): Promise<string> {\n const cdn = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${apiVersion}/build/pdf.worker.min.mjs`;\n if (!packagedUrl) return cdn;\n if (!/^https?:/i.test(packagedUrl)) return packagedUrl;\n try {\n const res = await fetchWithTimeout(packagedUrl, {\n credentials: \"omit\",\n fetchImpl,\n timeoutMs,\n });\n const type = res.headers.get(\"content-type\") ?? \"\";\n // Only the verdict is needed; the worker load fetches it again.\n await res.body?.cancel().catch(() => {});\n return res.ok && /javascript|ecmascript/i.test(type) ? packagedUrl : cdn;\n } catch {\n return cdn;\n }\n}\n\n/**\n * COMPLIANCE-CRITICAL failure router (P0): a doc could not be rendered inline.\n * - open_then_accept → the real-open affordance (opening IS its gate; satisfies\n * ONLY `opened`). This is legitimate, not a fabrication.\n * - scroll_then_accept → UNRENDERABLE: the gate stays unsatisfiable, a clear\n * error + Retry is shown, accept is disabled. NEVER fabricate scrolled_to_end.\n * - checkbox → no gate (already satisfiable): show the error + a plain open link\n * for reference; accept remains enabled (the checkbox gate needs no evidence).\n */\nfunction handleRenderFailure(\n container: HTMLElement,\n control: ConsentController,\n consentKey: string,\n doc: ConsentDocument,\n onRetry: () => void,\n): ConsentDocumentViewerResult {\n if (doc.acceptanceLevel === \"open_then_accept\") {\n renderOpenGateAffordance(\n container,\n control,\n consentKey,\n doc,\n \"The inline preview is unavailable. Opening the document in a new tab satisfies this step.\",\n );\n return { renderedAs: \"open-affordance\" };\n }\n // scroll_then_accept (and checkbox — harmless there): show the error. For a\n // scroll gate this leaves it UNSATISFIABLE (accept stays disabled).\n renderUnrenderableError(container, doc, onRetry);\n return { renderedAs: \"unrenderable\" };\n}\n\n/**\n * Fetch + render a consent document INLINE into `container`, wiring its scroll\n * container (or the legitimate open-affordance) to the consent controller so\n * `scroll_then_accept` / `open_then_accept` gates work for real. NO iframe.\n *\n * On ANY inline-render failure for a `scroll_then_accept` doc the gate stays\n * UNSATISFIABLE (never fabricates scrolled_to_end, D543 §5 COMPLIANCE) — the\n * caller's Retry re-invokes this.\n *\n * @returns how the doc was rendered (for the caller's telemetry/UX).\n */\nexport async function renderConsentDocument(\n container: HTMLElement,\n doc: ConsentDocument,\n control: ConsentController,\n options: ConsentDocumentViewerOptions,\n): Promise<ConsentDocumentViewerResult> {\n const { consentKey } = options;\n const onRetry = (): void => {\n void renderConsentDocument(container, doc, control, options);\n };\n\n let fetched: FetchedConsentDocument;\n try {\n fetched = await fetchDocumentContent(doc, {\n fetch: options.fetch,\n timeoutMs: options.timeoutMs,\n });\n } catch {\n return handleRenderFailure(container, control, consentKey, doc, onRetry);\n }\n\n // If the container was detached while we awaited the fetch (the consent stage\n // re-rendered, or the widget unmounted), skip DOM work — a stale render must\n // not mutate an orphaned node or leak an unhandled error.\n if (typeof container.isConnected === \"boolean\" && !container.isConnected) {\n return { renderedAs: \"unrenderable\" };\n }\n\n // open_then_accept: opening the doc in a new tab IS the gate — scrolling an\n // inline render does NOT satisfy it (reportScrollProgress no-ops for this\n // level). So on a SUCCESSFUL fetch we ALSO present the open affordance (which\n // marks `opened` on a real open) so the gate is satisfiable through the\n // default viewer (P1). We do not attempt an inline scroll gate here.\n if (doc.acceptanceLevel === \"open_then_accept\") {\n renderOpenGateAffordance(\n container,\n control,\n consentKey,\n doc,\n \"Open the document in a new tab to review it — that satisfies this step.\",\n );\n return { renderedAs: \"open-affordance\" };\n }\n\n if (fetched.kind === \"pdf\" && fetched.blob) {\n return renderPdf(\n container,\n control,\n consentKey,\n doc,\n fetched.blob,\n onRetry,\n );\n }\n if (fetched.kind === \"binary\") {\n // A binary/octet-stream artifact cannot be previewed inline → same\n // compliance rule: gated docs stay unsatisfiable, never click-to-satisfy.\n return handleRenderFailure(container, control, consentKey, doc, onRetry);\n }\n // text or html\n const text = fetched.text ?? \"\";\n if (isMarkdown(fetched, doc)) {\n const html = await marked.parse(text, { async: true });\n renderSanitizedHtml(container, control, consentKey, html);\n return { renderedAs: \"markdown\" };\n }\n if (fetched.kind === \"html\") {\n renderSanitizedHtml(container, control, consentKey, text);\n return { renderedAs: \"html\" };\n }\n renderPlainText(container, control, consentKey, text);\n return { renderedAs: \"text\" };\n}\n\n/** Reset the module-level pdf-worker latch (tests). */\nexport function _resetPdfWorkerState(): void {\n pdfWorkerReady = null;\n}\n"]}
@@ -1,5 +1,6 @@
1
- import { validateLeadsPayload } from './chunk-BIFANV7M.js';
2
- import { DEFAULT_REQUEST_TIMEOUT_MS, OnboardingValidationError, fetchWithTimeout, OnboardingNetworkError, OnboardingBotChallengeError, OnboardingConflictError, OnboardingRateLimitError, OnboardingServerError, OnboardingSpecStaleError } from './chunk-AW7W3HAC.js';
1
+ import { validateLeadsPayload } from './chunk-WKN4U7QF.js';
2
+ import { isSubdomainLabelShape, SUBDOMAIN_SHAPE_MESSAGE } from './chunk-OLWR5COK.js';
3
+ import { DEFAULT_REQUEST_TIMEOUT_MS, OnboardingValidationError, fetchWithTimeout, OnboardingNetworkError, OnboardingBotChallengeError, OnboardingConflictError, OnboardingRateLimitError, OnboardingServerError, OnboardingConsentIncompleteError, OnboardingSpecStaleError, OnboardingConsentStaleError, OnboardingSubdomainTakenError } from './chunk-XJTJPBJB.js';
3
4
  import { z } from 'zod/v3';
4
5
 
5
6
  // src/submit/client.ts
@@ -36,10 +37,17 @@ async function sleep(ms) {
36
37
  }
37
38
  function computePayloadFingerprint(values) {
38
39
  const canonical = JSON.stringify([
39
- values.name,
40
+ values.companyName,
41
+ values.contactName,
40
42
  values.email,
41
43
  values.phone ?? null,
42
- values.message ?? null
44
+ values.message ?? null,
45
+ [...values.interestModules ?? []].sort(),
46
+ values.sizeBand ?? null,
47
+ values.attribution ?? null,
48
+ values.privacyNoticeAcknowledged,
49
+ values.marketingOptIn,
50
+ values.intakeKind
43
51
  ]);
44
52
  let hash = 2166136261;
45
53
  for (let i = 0; i < canonical.length; i++) {
@@ -90,7 +98,7 @@ async function submitLead(values, options) {
90
98
  let lastError = null;
91
99
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
92
100
  options.onAttempt?.(attempt);
93
- const botToken = await resolveBotTokenGuarded(options.botToken);
101
+ const botToken = options.botToken === void 0 ? void 0 : await resolveBotTokenGuarded(options.botToken);
94
102
  const parsed = validateLeadsPayload({ ...base, botToken });
95
103
  if (!parsed.success) {
96
104
  const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(payload)"}: ${i.message}`).join("; ");
@@ -154,23 +162,38 @@ async function submitLead(values, options) {
154
162
  var SUBMIT_PATH = "/api/v1/public/onboarding/submit";
155
163
  var MAX_ATTEMPTS2 = 3;
156
164
  var RETRY_DELAYS_MS2 = [1e3, 2e3];
165
+ var consentSubmitDocumentSchema = z.object({
166
+ legalDocumentId: z.string().min(1, "legalDocumentId is required"),
167
+ versionNo: z.number().int().nonnegative(),
168
+ accepted: z.literal(true, {
169
+ errorMap: () => ({ message: "a consent document must be accepted" })
170
+ }),
171
+ // evidence keys stay snake_case (the only snake bits in the wire shape).
172
+ evidence: z.object({
173
+ scrolled_to_end: z.boolean().optional(),
174
+ opened: z.boolean().optional()
175
+ }).strict()
176
+ }).strict();
157
177
  var signupWireSchema = z.object({
158
178
  specVersion: z.string().min(1, "specVersion is required"),
159
179
  answers: z.record(z.unknown()),
160
180
  selectedServices: z.array(z.string()).optional(),
161
- botToken: z.string().min(1, "botToken is required"),
181
+ botToken: z.string().min(1, "botToken is required").optional(),
162
182
  consent: z.object({
163
183
  tosAccepted: z.literal(true, {
164
184
  errorMap: () => ({ message: "Terms of Service must be accepted" })
165
185
  }),
166
186
  privacyAccepted: z.literal(true, {
167
187
  errorMap: () => ({ message: "Privacy Policy must be accepted" })
168
- })
188
+ }),
189
+ documents: z.array(consentSubmitDocumentSchema).optional()
169
190
  }).strict(),
170
191
  credentials: z.object({
171
192
  email: z.string().email("A valid account email is required"),
172
- password: z.string().min(12, "Password must be at least 12 characters").max(256, "Password must be at most 256 characters")
173
- }).strict()
193
+ displayName: z.string().trim().min(1).max(200).optional()
194
+ }).strict(),
195
+ organisationName: z.string().trim().min(1).max(200).optional(),
196
+ subdomain: z.string().trim().min(1).max(63).refine(isSubdomainLabelShape, SUBDOMAIN_SHAPE_MESSAGE)
174
197
  }).strict();
175
198
  function computeSignupFingerprint(body) {
176
199
  const canonical = JSON.stringify([
@@ -179,9 +202,20 @@ function computeSignupFingerprint(body) {
179
202
  Object.keys(body.answers).sort().map((k) => [k, body.answers[k]]),
180
203
  [...body.selectedServices ?? []].sort(),
181
204
  body.credentials.email,
182
- body.credentials.password,
205
+ body.credentials.displayName ?? null,
206
+ body.organisationName ?? null,
207
+ body.subdomain,
183
208
  body.consent.tosAccepted,
184
- body.consent.privacyAccepted
209
+ body.consent.privacyAccepted,
210
+ // Consent documents fold into the fingerprint (sorted by legalDocumentId
211
+ // for order-independence) — re-accepting a changed doc after 409
212
+ // consent_stale is a NEW logical submission and must rotate the key.
213
+ [...body.consent.documents ?? []].sort((a, b) => a.legalDocumentId.localeCompare(b.legalDocumentId)).map((d) => [
214
+ d.legalDocumentId,
215
+ d.versionNo,
216
+ d.evidence.scrolled_to_end ?? false,
217
+ d.evidence.opened ?? false
218
+ ])
185
219
  ]);
186
220
  let hash = 2166136261;
187
221
  for (let i = 0; i < canonical.length; i++) {
@@ -200,6 +234,22 @@ function parseRetryAfter2(response) {
200
234
  const seconds = Number.parseInt(header, 10);
201
235
  return Number.isNaN(seconds) ? null : seconds;
202
236
  }
237
+ function parseStaleConsentDocuments(value) {
238
+ if (!Array.isArray(value)) return [];
239
+ const out = [];
240
+ for (const entry of value) {
241
+ if (entry === null || typeof entry !== "object") continue;
242
+ const e = entry;
243
+ if (typeof e["legalDocumentId"] !== "string") continue;
244
+ const doc = { legalDocumentId: e["legalDocumentId"] };
245
+ if (typeof e["docType"] === "string") doc.docType = e["docType"];
246
+ if (typeof e["currentVersionNo"] === "number") {
247
+ doc.currentVersionNo = e["currentVersionNo"];
248
+ }
249
+ out.push(doc);
250
+ }
251
+ return out;
252
+ }
203
253
  async function sleep2(ms) {
204
254
  return new Promise((resolve) => setTimeout(resolve, ms));
205
255
  }
@@ -249,7 +299,7 @@ async function submitSignup(body, options) {
249
299
  let lastError = null;
250
300
  for (let attempt = 1; attempt <= MAX_ATTEMPTS2; attempt++) {
251
301
  options.onAttempt?.(attempt);
252
- const botToken = await resolveBotTokenGuarded(options.botToken);
302
+ const botToken = options.botToken === void 0 ? void 0 : await resolveBotTokenGuarded(options.botToken);
253
303
  const parsed = signupWireSchema.safeParse({ ...base, botToken });
254
304
  if (!parsed.success) {
255
305
  const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(payload)"}: ${i.message}`).join("; ");
@@ -282,19 +332,44 @@ async function submitSignup(body, options) {
282
332
  if (response.status === 403) {
283
333
  throw new OnboardingBotChallengeError();
284
334
  }
335
+ if (response.status === 400) {
336
+ let code400;
337
+ let message400;
338
+ try {
339
+ const body400 = await response.json();
340
+ code400 = body400["code"];
341
+ const m = body400["message"];
342
+ if (typeof m === "string" && m !== "") message400 = m;
343
+ } catch {
344
+ }
345
+ if (code400 === "consent_incomplete") {
346
+ throw new OnboardingConsentIncompleteError(message400);
347
+ }
348
+ throw new OnboardingValidationError(
349
+ message400 ?? "The server rejected the submission (400). Please review your inputs."
350
+ );
351
+ }
285
352
  if (response.status === 409) {
286
353
  let code;
287
354
  let currentSpecVersion = "unknown";
355
+ let staleDocuments = [];
288
356
  try {
289
357
  const body409 = await response.json();
290
358
  code = body409["code"];
291
359
  const v = body409["currentSpecVersion"];
292
360
  if (typeof v === "string" && v !== "") currentSpecVersion = v;
361
+ staleDocuments = parseStaleConsentDocuments(body409["staleDocuments"]);
293
362
  } catch {
294
363
  }
295
364
  if (code === "spec_stale") {
296
365
  throw new OnboardingSpecStaleError(currentSpecVersion);
297
366
  }
367
+ if (code === "consent_stale") {
368
+ throw new OnboardingConsentStaleError(staleDocuments);
369
+ }
370
+ if (code === "subdomain_taken") {
371
+ throw new OnboardingSubdomainTakenError();
372
+ }
298
373
  throw new OnboardingConflictError(409);
299
374
  }
300
375
  if (response.status === 429) {
@@ -322,6 +397,6 @@ async function submitSignup(body, options) {
322
397
  throw lastError ?? new OnboardingNetworkError();
323
398
  }
324
399
 
325
- export { computePayloadFingerprint, computeSignupFingerprint, generateIdempotencyKey, resolveBotToken, signupWireSchema, submitLead, submitSignup };
326
- //# sourceMappingURL=chunk-BQCTDU5V.js.map
327
- //# sourceMappingURL=chunk-BQCTDU5V.js.map
400
+ export { computePayloadFingerprint, computeSignupFingerprint, consentSubmitDocumentSchema, generateIdempotencyKey, resolveBotToken, signupWireSchema, submitLead, submitSignup };
401
+ //# sourceMappingURL=chunk-UHOFGWI7.js.map
402
+ //# sourceMappingURL=chunk-UHOFGWI7.js.map