@nodii/onboarding 0.1.0-dev.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/chunk-AZE3NPSY.js +186 -0
- package/dist/chunk-AZE3NPSY.js.map +1 -0
- package/dist/{chunk-BQCTDU5V.js → chunk-BMQINH6O.js} +89 -15
- package/dist/chunk-BMQINH6O.js.map +1 -0
- package/dist/{chunk-34BQX2KR.js → chunk-F75ZJERM.js} +41 -6
- package/dist/chunk-F75ZJERM.js.map +1 -0
- package/dist/chunk-FIRSA7EH.js +3 -0
- package/dist/{chunk-ZDY2I6KY.js.map → chunk-FIRSA7EH.js.map} +1 -1
- package/dist/{chunk-SWMIIUXR.js → chunk-IPAKPLIF.js} +590 -92
- package/dist/chunk-IPAKPLIF.js.map +1 -0
- package/dist/chunk-JFDQZ3AC.js +3 -0
- package/dist/chunk-JFDQZ3AC.js.map +1 -0
- package/dist/{chunk-4SWOVHZ2.js → chunk-KQTS47FC.js} +129 -17
- package/dist/chunk-KQTS47FC.js.map +1 -0
- package/dist/chunk-LVJM4CTG.js +306 -0
- package/dist/chunk-LVJM4CTG.js.map +1 -0
- package/dist/chunk-SEKNVHD3.js +59 -0
- package/dist/chunk-SEKNVHD3.js.map +1 -0
- package/dist/chunk-SIMLMQPT.js +37 -0
- package/dist/chunk-SIMLMQPT.js.map +1 -0
- package/dist/chunk-U56GQITG.js +79 -0
- package/dist/chunk-U56GQITG.js.map +1 -0
- package/dist/{chunk-AFCJLUPK.js → chunk-U6AXKGYQ.js} +14 -5
- package/dist/chunk-U6AXKGYQ.js.map +1 -0
- package/dist/{chunk-AW7W3HAC.js → chunk-XJTJPBJB.js} +40 -3
- package/dist/chunk-XJTJPBJB.js.map +1 -0
- package/dist/client-CqE2dF9A.d.ts +86 -0
- package/dist/consent/index.d.ts +59 -0
- package/dist/consent/index.js +7 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/controller-BQb62D0N.d.ts +177 -0
- package/dist/dynamic/index.d.ts +57 -6
- package/dist/dynamic/index.js +4 -2
- package/dist/errors-vJkye4a0.d.ts +110 -0
- package/dist/form/index.d.ts +2 -2
- package/dist/form/index.js +2 -2
- package/dist/index-Dqfa890U.d.ts +83 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +12 -8
- package/dist/{model-IEyDI54w.d.ts → model-ESLSEbi0.d.ts} +199 -46
- package/dist/model-mnjAEtKe.d.ts +253 -0
- package/dist/render-DC7P2BGQ.js +5 -0
- package/dist/render-DC7P2BGQ.js.map +1 -0
- package/dist/schema-BrXb9gnh.d.ts +267 -0
- package/dist/{signup-client-Bl6eUrxR.d.ts → signup-client-BL5DfarJ.d.ts} +152 -19
- package/dist/spec/index.d.ts +10 -81
- package/dist/spec/index.js +3 -2
- package/dist/submit/index.d.ts +7 -71
- package/dist/submit/index.js +3 -3
- package/dist/viewer/index.d.ts +4 -0
- package/dist/viewer/index.js +5 -0
- package/dist/viewer/index.js.map +1 -0
- package/dist/widget/index.d.ts +50 -7
- package/dist/widget/index.js +9 -7
- package/package.json +21 -2
- package/dist/chunk-34BQX2KR.js.map +0 -1
- package/dist/chunk-4SWOVHZ2.js.map +0 -1
- package/dist/chunk-AFCJLUPK.js.map +0 -1
- package/dist/chunk-AW7W3HAC.js.map +0 -1
- package/dist/chunk-BIFANV7M.js +0 -22
- package/dist/chunk-BIFANV7M.js.map +0 -1
- package/dist/chunk-BQCTDU5V.js.map +0 -1
- package/dist/chunk-SWMIIUXR.js.map +0 -1
- package/dist/chunk-ZDY2I6KY.js +0 -3
- package/dist/schema-TfyBU6JW.d.ts +0 -107
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @nodii/onboarding
|
|
2
|
+
|
|
3
|
+
Browser SDK for the Nodii self-signup onboarding flow (D540 / D543): headless
|
|
4
|
+
form + consent state, reCAPTCHA integration, submit clients, and default
|
|
5
|
+
themeable vanilla-TS widgets.
|
|
6
|
+
|
|
7
|
+
Subpath exports:
|
|
8
|
+
|
|
9
|
+
- `@nodii/onboarding` — root barrel (most-common items)
|
|
10
|
+
- `@nodii/onboarding/form` — Phase-1 contact-form headless state
|
|
11
|
+
- `@nodii/onboarding/spec` — spec fetch client + D540 model
|
|
12
|
+
- `@nodii/onboarding/dynamic` — spec→Zod derivation + dynamic signup stepper
|
|
13
|
+
- `@nodii/onboarding/consent` — **headless** ConsentPack controller + gates (D543)
|
|
14
|
+
- `@nodii/onboarding/viewer` — **opt-in** default document viewer (DOMPurify /
|
|
15
|
+
marked / PDF.js). Separate subpath so the base bundle stays lean.
|
|
16
|
+
- `@nodii/onboarding/submit` — submit clients + typed error taxonomy
|
|
17
|
+
- `@nodii/onboarding/recaptcha` — reCAPTCHA v3 loader
|
|
18
|
+
- `@nodii/onboarding/widget` — default themeable widgets (contact + signup +
|
|
19
|
+
consent step)
|
|
20
|
+
|
|
21
|
+
## Tests
|
|
22
|
+
|
|
23
|
+
### Fast unit + happy-dom (default)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun run test # bun:test — headless controller, submit clients, widgets
|
|
27
|
+
# (happy-dom), spec model, bundle discipline, no-NUL guard
|
|
28
|
+
bun run typecheck # tsc --noEmit (src)
|
|
29
|
+
bun run lint # bun lint + bun fmt check
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Browser tests (Playwright / real Chromium) — COMPLIANCE AUTHORITY
|
|
33
|
+
|
|
34
|
+
The D543 consent scroll-gate, the PDF.js worker load, and DOMPurify-in-a-real-
|
|
35
|
+
DOM are **layout-dependent** and cannot be validated under happy-dom (no layout
|
|
36
|
+
engine). The Playwright spec in `browser-tests/` is the **authority** for those
|
|
37
|
+
compliance paths — it renders the real SDK bundle in Chromium against genuine
|
|
38
|
+
layout and proves:
|
|
39
|
+
|
|
40
|
+
1. **scroll gate** — a long doc's gate is CLOSED before scroll and OPENS only
|
|
41
|
+
after a real scroll-to-end (real geometry); `buildConsentPayload` throws
|
|
42
|
+
before / succeeds (with `scrolled_to_end`) after.
|
|
43
|
+
2. **P0a fabrication guard** — a short doc satisfies only WITH content present;
|
|
44
|
+
an empty/pre-render box never satisfies.
|
|
45
|
+
3. **PDF worker** — the worker is actually FETCHED + loaded (no pre-set
|
|
46
|
+
`workerSrc`); pages render to `<canvas>`; the scroll gate opens only after a
|
|
47
|
+
real scroll.
|
|
48
|
+
4. **open_then_accept** — the open affordance satisfies `opened` and only `opened`.
|
|
49
|
+
5. **DOMPurify** — `<iframe>` / `<script>` / `<object>` are stripped in a real
|
|
50
|
+
browser (closes the "DOMPurify may be inert under bun+happy-dom" gap).
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# One-time (and in CI): install the Chromium browser binary.
|
|
54
|
+
bunx playwright install chromium
|
|
55
|
+
|
|
56
|
+
# Build the browser bundle + run the Playwright spec.
|
|
57
|
+
bun run test:browser
|
|
58
|
+
|
|
59
|
+
# Typecheck the browser harness + spec.
|
|
60
|
+
bun run typecheck:browser
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**CI implication:** `bun run test:browser` requires a Chromium binary, so a CI
|
|
64
|
+
job that runs it MUST add a `bunx playwright install --with-deps chromium` step
|
|
65
|
+
before it (the browser is NOT downloaded by `bun install`). The default
|
|
66
|
+
`ts-ci.yml` `Test` step runs `bun run test` (happy-dom, fast) per package and
|
|
67
|
+
does NOT run the browser spec; wire the browser spec as a dedicated job/step,
|
|
68
|
+
e.g.:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
- name: Install Chromium for browser tests
|
|
72
|
+
run: cd ts/onboarding && bunx playwright install --with-deps chromium
|
|
73
|
+
- name: Browser tests (consent compliance authority)
|
|
74
|
+
run: cd ts/onboarding && bun run test:browser
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Playwright build artifacts (`browser-tests/.bundle/`, `test-results/`,
|
|
78
|
+
`playwright-report/`) are gitignored.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { consentDocKey } from './chunk-U56GQITG.js';
|
|
2
|
+
|
|
3
|
+
// src/consent/controller.ts
|
|
4
|
+
var SCROLL_END_THRESHOLD = 0.99;
|
|
5
|
+
function initialGateSatisfied(level) {
|
|
6
|
+
return level === "checkbox";
|
|
7
|
+
}
|
|
8
|
+
function gateSatisfiedFor(d) {
|
|
9
|
+
switch (d.doc.acceptanceLevel) {
|
|
10
|
+
case "checkbox":
|
|
11
|
+
return true;
|
|
12
|
+
case "scroll_then_accept":
|
|
13
|
+
return d.scrolledToEnd;
|
|
14
|
+
case "open_then_accept":
|
|
15
|
+
return d.opened;
|
|
16
|
+
default:
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function evidenceOf(d) {
|
|
21
|
+
switch (d.doc.acceptanceLevel) {
|
|
22
|
+
case "scroll_then_accept":
|
|
23
|
+
return { scrolled_to_end: d.scrolledToEnd };
|
|
24
|
+
case "open_then_accept":
|
|
25
|
+
return { opened: d.opened };
|
|
26
|
+
default:
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function createConsentController(pack) {
|
|
31
|
+
let docs = (pack?.documents ?? []).map((doc) => ({
|
|
32
|
+
doc,
|
|
33
|
+
key: consentDocKey(doc),
|
|
34
|
+
accepted: false,
|
|
35
|
+
scrolledToEnd: false,
|
|
36
|
+
scrollProgress: 0,
|
|
37
|
+
opened: false
|
|
38
|
+
}));
|
|
39
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
40
|
+
function find(key) {
|
|
41
|
+
return docs.find((d) => d.key === key);
|
|
42
|
+
}
|
|
43
|
+
function snapshot() {
|
|
44
|
+
const documents = docs.map((d) => ({
|
|
45
|
+
doc: d.doc,
|
|
46
|
+
key: d.key,
|
|
47
|
+
acceptanceLevel: d.doc.acceptanceLevel,
|
|
48
|
+
required: d.doc.required,
|
|
49
|
+
gateSatisfied: gateSatisfiedFor(d),
|
|
50
|
+
accepted: d.accepted,
|
|
51
|
+
evidence: evidenceOf(d),
|
|
52
|
+
scrollProgress: d.scrollProgress
|
|
53
|
+
}));
|
|
54
|
+
return { documents, complete: computeComplete() };
|
|
55
|
+
}
|
|
56
|
+
function computeComplete() {
|
|
57
|
+
for (const d of docs) {
|
|
58
|
+
if (d.doc.required && !(d.accepted && gateSatisfiedFor(d))) return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
function emit() {
|
|
63
|
+
const state = snapshot();
|
|
64
|
+
for (const listener of listeners) listener(state);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
getState() {
|
|
68
|
+
return snapshot();
|
|
69
|
+
},
|
|
70
|
+
isEmpty() {
|
|
71
|
+
return docs.length === 0;
|
|
72
|
+
},
|
|
73
|
+
isComplete() {
|
|
74
|
+
return computeComplete();
|
|
75
|
+
},
|
|
76
|
+
canSubmit() {
|
|
77
|
+
return computeComplete();
|
|
78
|
+
},
|
|
79
|
+
isGateSatisfied(key) {
|
|
80
|
+
const d = find(key);
|
|
81
|
+
return d ? gateSatisfiedFor(d) : false;
|
|
82
|
+
},
|
|
83
|
+
reportScrollProgress(key, ratio) {
|
|
84
|
+
const d = find(key);
|
|
85
|
+
if (!d || d.doc.acceptanceLevel !== "scroll_then_accept") return;
|
|
86
|
+
const clamped = Number.isFinite(ratio) ? Math.min(1, Math.max(0, ratio)) : 0;
|
|
87
|
+
const reachedEnd = clamped >= SCROLL_END_THRESHOLD;
|
|
88
|
+
const nextProgress = Math.max(d.scrollProgress, clamped);
|
|
89
|
+
const nextScrolledToEnd = d.scrolledToEnd || reachedEnd;
|
|
90
|
+
if (nextProgress === d.scrollProgress && nextScrolledToEnd === d.scrolledToEnd) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
d.scrollProgress = nextProgress;
|
|
94
|
+
d.scrolledToEnd = nextScrolledToEnd;
|
|
95
|
+
emit();
|
|
96
|
+
},
|
|
97
|
+
markScrolledToEnd(key) {
|
|
98
|
+
const d = find(key);
|
|
99
|
+
if (!d || d.doc.acceptanceLevel !== "scroll_then_accept") return;
|
|
100
|
+
if (d.scrolledToEnd && d.scrollProgress >= 1) return;
|
|
101
|
+
d.scrolledToEnd = true;
|
|
102
|
+
d.scrollProgress = 1;
|
|
103
|
+
emit();
|
|
104
|
+
},
|
|
105
|
+
markOpened(key) {
|
|
106
|
+
const d = find(key);
|
|
107
|
+
if (!d || d.doc.acceptanceLevel !== "open_then_accept") return;
|
|
108
|
+
if (d.opened) return;
|
|
109
|
+
d.opened = true;
|
|
110
|
+
emit();
|
|
111
|
+
},
|
|
112
|
+
setAccepted(key, accepted) {
|
|
113
|
+
const d = find(key);
|
|
114
|
+
if (!d) return;
|
|
115
|
+
if (accepted) {
|
|
116
|
+
if (!gateSatisfiedFor(d)) return;
|
|
117
|
+
if (d.accepted) return;
|
|
118
|
+
d.accepted = true;
|
|
119
|
+
} else {
|
|
120
|
+
if (!d.accepted) return;
|
|
121
|
+
d.accepted = false;
|
|
122
|
+
}
|
|
123
|
+
emit();
|
|
124
|
+
},
|
|
125
|
+
buildConsentPayload() {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const d of docs) {
|
|
128
|
+
const gate = gateSatisfiedFor(d);
|
|
129
|
+
if (d.doc.required && !(d.accepted && gate)) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`buildConsentPayload: cannot build a consent payload \u2014 required document "${d.key}" (${d.doc.acceptanceLevel}) is not accepted at its gate. Never fabricate acceptance; surface the consent gate to the user first.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (d.accepted) {
|
|
135
|
+
out.push({
|
|
136
|
+
legalDocumentId: d.doc.legalDocumentId,
|
|
137
|
+
versionNo: d.doc.versionNo,
|
|
138
|
+
accepted: true,
|
|
139
|
+
evidence: evidenceOf(d)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
},
|
|
145
|
+
applyNewPack(newPack) {
|
|
146
|
+
const prevByKey = new Map(docs.map((d) => [d.key, d]));
|
|
147
|
+
const resetKeys = [];
|
|
148
|
+
docs = (newPack?.documents ?? []).map((doc) => {
|
|
149
|
+
const key = consentDocKey(doc);
|
|
150
|
+
const prev = prevByKey.get(key);
|
|
151
|
+
const unchanged = prev !== void 0 && prev.doc.versionNo === doc.versionNo && prev.doc.contentHash === doc.contentHash && prev.doc.acceptanceLevel === doc.acceptanceLevel && prev.doc.required === doc.required;
|
|
152
|
+
if (unchanged) {
|
|
153
|
+
return {
|
|
154
|
+
doc,
|
|
155
|
+
key,
|
|
156
|
+
accepted: prev.accepted,
|
|
157
|
+
scrolledToEnd: prev.scrolledToEnd,
|
|
158
|
+
scrollProgress: prev.scrollProgress,
|
|
159
|
+
opened: prev.opened
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (prev?.accepted) resetKeys.push(key);
|
|
163
|
+
return {
|
|
164
|
+
doc,
|
|
165
|
+
key,
|
|
166
|
+
accepted: false,
|
|
167
|
+
scrolledToEnd: false,
|
|
168
|
+
scrollProgress: 0,
|
|
169
|
+
opened: false
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
emit();
|
|
173
|
+
return resetKeys;
|
|
174
|
+
},
|
|
175
|
+
subscribe(listener) {
|
|
176
|
+
listeners.add(listener);
|
|
177
|
+
return () => {
|
|
178
|
+
listeners.delete(listener);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { SCROLL_END_THRESHOLD, createConsentController, initialGateSatisfied };
|
|
185
|
+
//# sourceMappingURL=chunk-AZE3NPSY.js.map
|
|
186
|
+
//# sourceMappingURL=chunk-AZE3NPSY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/consent/controller.ts"],"names":[],"mappings":"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","file":"chunk-AZE3NPSY.js","sourcesContent":["/**\n * Headless ConsentPack controller (D543 §2/§5).\n *\n * Framework-agnostic consent state for the signup flow's consent stage.\n * Exposes, per document: { doc, accepted, gateSatisfied, evidence } and\n * enforces the per-document ACCEPTANCE-LEVEL GATE (D543 §2):\n *\n * - checkbox — accept is enable-able immediately (basic clickwrap).\n * - scroll_then_accept — accept is DISABLED until the rendered doc is\n * scrolled to the end (evidence.scrolled_to_end).\n * - open_then_accept — accept is DISABLED until the doc is opened/viewed\n * (evidence.opened).\n *\n * The gate is a UX guard (D543 §5: \"Client evidence is UX-only; the server\n * re-validates\"). But the controller ENFORCES it locally so a UI cannot\n * present an accept affordance the gate hasn't unlocked, and so a headless\n * caller cannot assemble a payload that claims an unsatisfied gate.\n *\n * COMPLIANCE-CRITICAL (same discipline as the stepper's consent-fabrication\n * fix): `accept(key)` is REFUSED when the gate is not yet satisfied, and\n * `buildConsentPayload()` THROWS if ANY required doc is not accepted at its\n * gate. The controller NEVER fabricates acceptance or evidence — it only\n * records what the user genuinely did. `isComplete()` is true only when every\n * REQUIRED doc is accepted with its gate satisfied.\n *\n * Same subscribe/mutate contract as the dynamic stepper so the default\n * component layer renders it and tenants can build their own UI against the\n * exported control surface (D543 §5 headless flexibility).\n */\n\nimport {\n consentDocKey,\n type AcceptanceLevel,\n type ConsentDocument,\n type ConsentPack,\n} from \"./model.js\";\n\n/**\n * Evidence for a document's gate. Only the field relevant to the doc's\n * acceptanceLevel is ever set true — a checkbox doc carries no evidence.\n * Mirrors the D543 §4 submit `evidence:{scrolled_to_end?, opened?}` — these\n * two keys stay snake_case (the ONLY snake bits in the consent wire shape).\n */\nexport interface ConsentEvidence {\n scrolled_to_end?: boolean;\n opened?: boolean;\n}\n\n/**\n * Per-document view of the consent state — the framework-agnostic \"row model\"\n * a consumer binds their own rendering to (TanStack-style). NO DOM here.\n */\nexport interface ConsentDocState {\n /** The document descriptor (from the spec's consentPack). */\n doc: ConsentDocument;\n /** Stable client-side key (docType | custom:<customKey>). */\n key: string;\n /** The document's acceptance level, surfaced for convenience. */\n acceptanceLevel: AcceptanceLevel;\n /** Whether the document is required, surfaced for convenience. */\n required: boolean;\n /**\n * Whether the acceptance-level GATE is satisfied — i.e. accept is\n * enable-able. checkbox ⇒ always true; scroll ⇒ scrolled-to-end;\n * open ⇒ opened.\n */\n gateSatisfied: boolean;\n /** Whether the user has genuinely accepted (only possible past the gate). */\n accepted: boolean;\n /** The evidence collected so far (drives gateSatisfied for gated levels). */\n evidence: ConsentEvidence;\n /**\n * Scroll progress in [0,1] for a `scroll_then_accept` doc (0 for other\n * levels). A consumer's scroll handler feeds this via reportScrollProgress;\n * reaching ~1 flips gateSatisfied. Purely a UX-progress read (e.g. a\n * progress bar); the gate itself keys on scrolledToEnd evidence.\n */\n scrollProgress: number;\n}\n\nexport interface ConsentState {\n /** All documents in spec order. */\n documents: ConsentDocState[];\n /** True iff every REQUIRED doc is accepted with its gate satisfied. */\n complete: boolean;\n}\n\nexport type ConsentStateListener = (state: ConsentState) => void;\n\n/**\n * One element of the D543 §4 submit `consent.documents` array — CANONICAL\n * camelCase (legalDocumentId, versionNo, accepted); `evidence` keys stay\n * snake_case (scrolled_to_end / opened).\n */\nexport interface ConsentSubmitDocument {\n legalDocumentId: string;\n versionNo: number;\n accepted: true;\n evidence: ConsentEvidence;\n}\n\nexport interface ConsentController {\n getState(): ConsentState;\n /** True iff there are no documents to consent to (stage is skippable). */\n isEmpty(): boolean;\n /** True iff every required doc is accepted with its gate satisfied. */\n isComplete(): boolean;\n /**\n * Alias of isComplete() — TanStack-style ergonomic read: \"can the consumer\n * submit the consent step?\" (all required docs accepted at their gate).\n */\n canSubmit(): boolean;\n /**\n * Whether the given document's gate is satisfied (accept is enable-able).\n * Unknown key ⇒ false.\n */\n isGateSatisfied(key: string): boolean;\n /**\n * Report scroll progress in [0,1] for a `scroll_then_accept` doc — the\n * consumer's OWN scroll handler feeds this (e.g. on its same-origin scroll\n * container). Ratio ≥ SCROLL_END_THRESHOLD marks scrolled-to-end (opens the\n * gate); otherwise it just updates scrollProgress for UX. Clamped to [0,1].\n * No-op for other levels and unknown keys.\n */\n reportScrollProgress(key: string, ratio: number): void;\n /**\n * Mark a `scroll_then_accept` doc as scrolled to the end (evidence). No-op\n * for other levels (their gate isn't scroll-driven) and unknown keys.\n * Equivalent to reportScrollProgress(key, 1).\n */\n markScrolledToEnd(key: string): void;\n /**\n * Mark an `open_then_accept` doc as opened/viewed (evidence). No-op for\n * other levels and unknown keys.\n */\n markOpened(key: string): void;\n /**\n * Set a document's accepted flag. Accepting is REFUSED (accepted stays\n * false) when the gate is not satisfied — a UI cannot record acceptance a\n * user could not legitimately give. Unaccepting is always allowed.\n */\n setAccepted(key: string, accepted: boolean): void;\n /**\n * Build the D543 §4 submit array from the ACTUAL consent state.\n *\n * @throws {Error} if any REQUIRED doc is not accepted with its gate\n * satisfied — never fabricates acceptance/evidence (compliance-critical).\n * Optional docs that were accepted ARE included; optional docs left\n * unaccepted are omitted.\n */\n buildConsentPayload(): ConsentSubmitDocument[];\n /**\n * Swap in a refetched consentPack after 409 consent_stale. For every doc\n * whose (versionNo, contentHash) CHANGED, acceptance + evidence are\n * RESET (re-acceptance required — NEVER auto-accept a changed doc, D543 §4\n * / task §4). Unchanged docs keep their state. Returns the keys whose\n * consent was reset so the UI can highlight what changed.\n */\n applyNewPack(pack: ConsentPack | undefined): string[];\n subscribe(listener: ConsentStateListener): () => void;\n}\n\n/**\n * Scroll ratio at/above which a `scroll_then_accept` doc counts as\n * scrolled-to-end. Slightly below 1 so sub-pixel rounding on the consumer's\n * scroll math (scrollTop+clientHeight vs scrollHeight) still opens the gate.\n */\nexport const SCROLL_END_THRESHOLD = 0.99;\n\n/** checkbox docs have no gate; gated levels start unsatisfied. */\nfunction initialGateSatisfied(level: AcceptanceLevel): boolean {\n return level === \"checkbox\";\n}\n\ninterface InternalDoc {\n doc: ConsentDocument;\n key: string;\n accepted: boolean;\n scrolledToEnd: boolean;\n scrollProgress: number;\n opened: boolean;\n}\n\nfunction gateSatisfiedFor(d: InternalDoc): boolean {\n switch (d.doc.acceptanceLevel) {\n case \"checkbox\":\n return true;\n case \"scroll_then_accept\":\n return d.scrolledToEnd;\n case \"open_then_accept\":\n return d.opened;\n default:\n // Unreachable — the level enum is exhaustive and validated at parse.\n // Fail CLOSED: an unknown level is never auto-satisfied.\n return false;\n }\n}\n\nfunction evidenceOf(d: InternalDoc): ConsentEvidence {\n switch (d.doc.acceptanceLevel) {\n case \"scroll_then_accept\":\n return { scrolled_to_end: d.scrolledToEnd };\n case \"open_then_accept\":\n return { opened: d.opened };\n default:\n return {};\n }\n}\n\n/**\n * Create a headless consent controller for a spec's consentPack.\n *\n * @param pack the spec's `consentPack` (or undefined ⇒ an empty controller;\n * the stage is skipped and `isComplete()` / `isEmpty()` are true).\n *\n * @example\n * ```ts\n * const consent = createConsentController(spec.consentPack);\n * consent.subscribe(render);\n * consent.markScrolledToEnd(\"tos\"); // scroll_then_accept gate\n * consent.setAccepted(\"tos\", true); // now allowed\n * const docs = consent.buildConsentPayload(); // throws if any required unmet\n * ```\n */\nexport function createConsentController(\n pack: ConsentPack | undefined,\n): ConsentController {\n let docs: InternalDoc[] = (pack?.documents ?? []).map((doc) => ({\n doc,\n key: consentDocKey(doc),\n accepted: false,\n scrolledToEnd: false,\n scrollProgress: 0,\n opened: false,\n }));\n\n const listeners = new Set<ConsentStateListener>();\n\n function find(key: string): InternalDoc | undefined {\n return docs.find((d) => d.key === key);\n }\n\n function snapshot(): ConsentState {\n const documents: ConsentDocState[] = docs.map((d) => ({\n doc: d.doc,\n key: d.key,\n acceptanceLevel: d.doc.acceptanceLevel,\n required: d.doc.required,\n gateSatisfied: gateSatisfiedFor(d),\n accepted: d.accepted,\n evidence: evidenceOf(d),\n scrollProgress: d.scrollProgress,\n }));\n return { documents, complete: computeComplete() };\n }\n\n function computeComplete(): boolean {\n for (const d of docs) {\n if (d.doc.required && !(d.accepted && gateSatisfiedFor(d))) return false;\n }\n return true;\n }\n\n function emit(): void {\n const state = snapshot();\n for (const listener of listeners) listener(state);\n }\n\n return {\n getState() {\n return snapshot();\n },\n\n isEmpty() {\n return docs.length === 0;\n },\n\n isComplete() {\n return computeComplete();\n },\n\n canSubmit() {\n return computeComplete();\n },\n\n isGateSatisfied(key) {\n const d = find(key);\n return d ? gateSatisfiedFor(d) : false;\n },\n\n reportScrollProgress(key, ratio) {\n const d = find(key);\n if (!d || d.doc.acceptanceLevel !== \"scroll_then_accept\") return;\n const clamped = Number.isFinite(ratio)\n ? Math.min(1, Math.max(0, ratio))\n : 0;\n const reachedEnd = clamped >= SCROLL_END_THRESHOLD;\n // Progress never regresses below a prior high-water mark that already\n // opened the gate — once scrolled-to-end, a scroll-back must not re-lock\n // acceptance (matches how clickwrap gates latch).\n const nextProgress = Math.max(d.scrollProgress, clamped);\n const nextScrolledToEnd = d.scrolledToEnd || reachedEnd;\n if (\n nextProgress === d.scrollProgress &&\n nextScrolledToEnd === d.scrolledToEnd\n ) {\n return;\n }\n d.scrollProgress = nextProgress;\n d.scrolledToEnd = nextScrolledToEnd;\n emit();\n },\n\n markScrolledToEnd(key) {\n const d = find(key);\n if (!d || d.doc.acceptanceLevel !== \"scroll_then_accept\") return;\n if (d.scrolledToEnd && d.scrollProgress >= 1) return;\n d.scrolledToEnd = true;\n d.scrollProgress = 1;\n emit();\n },\n\n markOpened(key) {\n const d = find(key);\n if (!d || d.doc.acceptanceLevel !== \"open_then_accept\") return;\n if (d.opened) return;\n d.opened = true;\n emit();\n },\n\n setAccepted(key, accepted) {\n const d = find(key);\n if (!d) return;\n if (accepted) {\n // COMPLIANCE: refuse to record acceptance a user could not give —\n // the gate must be satisfied first. A UI that offers accept before\n // the gate is unlocked cannot fabricate consent through this control.\n if (!gateSatisfiedFor(d)) return;\n if (d.accepted) return;\n d.accepted = true;\n } else {\n if (!d.accepted) return;\n d.accepted = false;\n }\n emit();\n },\n\n buildConsentPayload() {\n const out: ConsentSubmitDocument[] = [];\n for (const d of docs) {\n const gate = gateSatisfiedFor(d);\n if (d.doc.required && !(d.accepted && gate)) {\n throw new Error(\n `buildConsentPayload: cannot build a consent payload — required document \"${d.key}\" (${d.doc.acceptanceLevel}) is not accepted at its gate. Never fabricate acceptance; surface the consent gate to the user first.`,\n );\n }\n // Include every ACCEPTED doc (required or optional). An accepted doc\n // always has its gate satisfied (setAccepted refuses otherwise), so\n // its evidence is trustworthy.\n if (d.accepted) {\n out.push({\n legalDocumentId: d.doc.legalDocumentId,\n versionNo: d.doc.versionNo,\n accepted: true,\n evidence: evidenceOf(d),\n });\n }\n }\n return out;\n },\n\n applyNewPack(newPack) {\n const prevByKey = new Map(docs.map((d) => [d.key, d]));\n const resetKeys: string[] = [];\n\n docs = (newPack?.documents ?? []).map((doc) => {\n const key = consentDocKey(doc);\n const prev = prevByKey.get(key);\n // A doc is UNCHANGED iff the same key AND the same live version AND the\n // same content hash AND the same GATE SHAPE (acceptanceLevel + required).\n // Only then may prior acceptance carry over. A gate ESCALATION at the\n // same version/hash (e.g. checkbox → scroll_then_accept, or required\n // flipping true) would otherwise keep accepted:true with an unsatisfied\n // gate — violating the \"an accepted doc always has its gate satisfied\"\n // invariant and making buildConsentPayload emit accepted:true with\n // insufficient evidence → an unrecoverable server-side gate rejection\n // (P2). Treat any gate-shape change as \"changed\" → reset + re-lock.\n const unchanged =\n prev !== undefined &&\n prev.doc.versionNo === doc.versionNo &&\n prev.doc.contentHash === doc.contentHash &&\n prev.doc.acceptanceLevel === doc.acceptanceLevel &&\n prev.doc.required === doc.required;\n if (unchanged) {\n return {\n doc,\n key,\n accepted: prev.accepted,\n scrolledToEnd: prev.scrolledToEnd,\n scrollProgress: prev.scrollProgress,\n opened: prev.opened,\n };\n }\n // Changed (or new) doc → require fresh acceptance. NEVER auto-accept.\n if (prev?.accepted) resetKeys.push(key);\n return {\n doc,\n key,\n accepted: false,\n scrolledToEnd: false,\n scrollProgress: 0,\n opened: false,\n };\n });\n\n emit();\n return resetKeys;\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** Re-export the gate-init helper for the widget's initial affordance state. */\nexport { initialGateSatisfied };\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { validateLeadsPayload } from './chunk-
|
|
2
|
-
import { DEFAULT_REQUEST_TIMEOUT_MS, OnboardingValidationError, fetchWithTimeout, OnboardingNetworkError, OnboardingBotChallengeError, OnboardingConflictError, OnboardingRateLimitError, OnboardingServerError, OnboardingSpecStaleError } from './chunk-
|
|
1
|
+
import { validateLeadsPayload } from './chunk-SIMLMQPT.js';
|
|
2
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, OnboardingValidationError, fetchWithTimeout, OnboardingNetworkError, OnboardingBotChallengeError, OnboardingConflictError, OnboardingRateLimitError, OnboardingServerError, OnboardingConsentIncompleteError, OnboardingSpecStaleError, OnboardingConsentStaleError, OnboardingSubdomainTakenError } from './chunk-XJTJPBJB.js';
|
|
3
3
|
import { z } from 'zod/v3';
|
|
4
4
|
|
|
5
5
|
// src/submit/client.ts
|
|
@@ -36,10 +36,17 @@ async function sleep(ms) {
|
|
|
36
36
|
}
|
|
37
37
|
function computePayloadFingerprint(values) {
|
|
38
38
|
const canonical = JSON.stringify([
|
|
39
|
-
values.
|
|
39
|
+
values.companyName,
|
|
40
|
+
values.contactName,
|
|
40
41
|
values.email,
|
|
41
42
|
values.phone ?? null,
|
|
42
|
-
values.message ?? null
|
|
43
|
+
values.message ?? null,
|
|
44
|
+
[...values.interestModules ?? []].sort(),
|
|
45
|
+
values.sizeBand ?? null,
|
|
46
|
+
values.attribution ?? null,
|
|
47
|
+
values.privacyNoticeAcknowledged,
|
|
48
|
+
values.marketingOptIn,
|
|
49
|
+
values.intakeKind
|
|
43
50
|
]);
|
|
44
51
|
let hash = 2166136261;
|
|
45
52
|
for (let i = 0; i < canonical.length; i++) {
|
|
@@ -90,7 +97,7 @@ async function submitLead(values, options) {
|
|
|
90
97
|
let lastError = null;
|
|
91
98
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
92
99
|
options.onAttempt?.(attempt);
|
|
93
|
-
const botToken = await resolveBotTokenGuarded(options.botToken);
|
|
100
|
+
const botToken = options.botToken === void 0 ? void 0 : await resolveBotTokenGuarded(options.botToken);
|
|
94
101
|
const parsed = validateLeadsPayload({ ...base, botToken });
|
|
95
102
|
if (!parsed.success) {
|
|
96
103
|
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(payload)"}: ${i.message}`).join("; ");
|
|
@@ -154,23 +161,38 @@ async function submitLead(values, options) {
|
|
|
154
161
|
var SUBMIT_PATH = "/api/v1/public/onboarding/submit";
|
|
155
162
|
var MAX_ATTEMPTS2 = 3;
|
|
156
163
|
var RETRY_DELAYS_MS2 = [1e3, 2e3];
|
|
164
|
+
var consentSubmitDocumentSchema = z.object({
|
|
165
|
+
legalDocumentId: z.string().min(1, "legalDocumentId is required"),
|
|
166
|
+
versionNo: z.number().int().nonnegative(),
|
|
167
|
+
accepted: z.literal(true, {
|
|
168
|
+
errorMap: () => ({ message: "a consent document must be accepted" })
|
|
169
|
+
}),
|
|
170
|
+
// evidence keys stay snake_case (the only snake bits in the wire shape).
|
|
171
|
+
evidence: z.object({
|
|
172
|
+
scrolled_to_end: z.boolean().optional(),
|
|
173
|
+
opened: z.boolean().optional()
|
|
174
|
+
}).strict()
|
|
175
|
+
}).strict();
|
|
157
176
|
var signupWireSchema = z.object({
|
|
158
177
|
specVersion: z.string().min(1, "specVersion is required"),
|
|
159
178
|
answers: z.record(z.unknown()),
|
|
160
179
|
selectedServices: z.array(z.string()).optional(),
|
|
161
|
-
botToken: z.string().min(1, "botToken is required"),
|
|
180
|
+
botToken: z.string().min(1, "botToken is required").optional(),
|
|
162
181
|
consent: z.object({
|
|
163
182
|
tosAccepted: z.literal(true, {
|
|
164
183
|
errorMap: () => ({ message: "Terms of Service must be accepted" })
|
|
165
184
|
}),
|
|
166
185
|
privacyAccepted: z.literal(true, {
|
|
167
186
|
errorMap: () => ({ message: "Privacy Policy must be accepted" })
|
|
168
|
-
})
|
|
187
|
+
}),
|
|
188
|
+
documents: z.array(consentSubmitDocumentSchema).optional()
|
|
169
189
|
}).strict(),
|
|
170
190
|
credentials: z.object({
|
|
171
191
|
email: z.string().email("A valid account email is required"),
|
|
172
|
-
|
|
173
|
-
}).strict()
|
|
192
|
+
displayName: z.string().trim().min(1).max(200).optional()
|
|
193
|
+
}).strict(),
|
|
194
|
+
organisationName: z.string().trim().min(1).max(200).optional(),
|
|
195
|
+
subdomain: z.string().trim().min(1).max(63)
|
|
174
196
|
}).strict();
|
|
175
197
|
function computeSignupFingerprint(body) {
|
|
176
198
|
const canonical = JSON.stringify([
|
|
@@ -179,9 +201,20 @@ function computeSignupFingerprint(body) {
|
|
|
179
201
|
Object.keys(body.answers).sort().map((k) => [k, body.answers[k]]),
|
|
180
202
|
[...body.selectedServices ?? []].sort(),
|
|
181
203
|
body.credentials.email,
|
|
182
|
-
body.credentials.
|
|
204
|
+
body.credentials.displayName ?? null,
|
|
205
|
+
body.organisationName ?? null,
|
|
206
|
+
body.subdomain,
|
|
183
207
|
body.consent.tosAccepted,
|
|
184
|
-
body.consent.privacyAccepted
|
|
208
|
+
body.consent.privacyAccepted,
|
|
209
|
+
// Consent documents fold into the fingerprint (sorted by legalDocumentId
|
|
210
|
+
// for order-independence) — re-accepting a changed doc after 409
|
|
211
|
+
// consent_stale is a NEW logical submission and must rotate the key.
|
|
212
|
+
[...body.consent.documents ?? []].sort((a, b) => a.legalDocumentId.localeCompare(b.legalDocumentId)).map((d) => [
|
|
213
|
+
d.legalDocumentId,
|
|
214
|
+
d.versionNo,
|
|
215
|
+
d.evidence.scrolled_to_end ?? false,
|
|
216
|
+
d.evidence.opened ?? false
|
|
217
|
+
])
|
|
185
218
|
]);
|
|
186
219
|
let hash = 2166136261;
|
|
187
220
|
for (let i = 0; i < canonical.length; i++) {
|
|
@@ -200,6 +233,22 @@ function parseRetryAfter2(response) {
|
|
|
200
233
|
const seconds = Number.parseInt(header, 10);
|
|
201
234
|
return Number.isNaN(seconds) ? null : seconds;
|
|
202
235
|
}
|
|
236
|
+
function parseStaleConsentDocuments(value) {
|
|
237
|
+
if (!Array.isArray(value)) return [];
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const entry of value) {
|
|
240
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
241
|
+
const e = entry;
|
|
242
|
+
if (typeof e["legalDocumentId"] !== "string") continue;
|
|
243
|
+
const doc = { legalDocumentId: e["legalDocumentId"] };
|
|
244
|
+
if (typeof e["docType"] === "string") doc.docType = e["docType"];
|
|
245
|
+
if (typeof e["currentVersionNo"] === "number") {
|
|
246
|
+
doc.currentVersionNo = e["currentVersionNo"];
|
|
247
|
+
}
|
|
248
|
+
out.push(doc);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
203
252
|
async function sleep2(ms) {
|
|
204
253
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
205
254
|
}
|
|
@@ -249,7 +298,7 @@ async function submitSignup(body, options) {
|
|
|
249
298
|
let lastError = null;
|
|
250
299
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS2; attempt++) {
|
|
251
300
|
options.onAttempt?.(attempt);
|
|
252
|
-
const botToken = await resolveBotTokenGuarded(options.botToken);
|
|
301
|
+
const botToken = options.botToken === void 0 ? void 0 : await resolveBotTokenGuarded(options.botToken);
|
|
253
302
|
const parsed = signupWireSchema.safeParse({ ...base, botToken });
|
|
254
303
|
if (!parsed.success) {
|
|
255
304
|
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(payload)"}: ${i.message}`).join("; ");
|
|
@@ -282,19 +331,44 @@ async function submitSignup(body, options) {
|
|
|
282
331
|
if (response.status === 403) {
|
|
283
332
|
throw new OnboardingBotChallengeError();
|
|
284
333
|
}
|
|
334
|
+
if (response.status === 400) {
|
|
335
|
+
let code400;
|
|
336
|
+
let message400;
|
|
337
|
+
try {
|
|
338
|
+
const body400 = await response.json();
|
|
339
|
+
code400 = body400["code"];
|
|
340
|
+
const m = body400["message"];
|
|
341
|
+
if (typeof m === "string" && m !== "") message400 = m;
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
if (code400 === "consent_incomplete") {
|
|
345
|
+
throw new OnboardingConsentIncompleteError(message400);
|
|
346
|
+
}
|
|
347
|
+
throw new OnboardingValidationError(
|
|
348
|
+
message400 ?? "The server rejected the submission (400). Please review your inputs."
|
|
349
|
+
);
|
|
350
|
+
}
|
|
285
351
|
if (response.status === 409) {
|
|
286
352
|
let code;
|
|
287
353
|
let currentSpecVersion = "unknown";
|
|
354
|
+
let staleDocuments = [];
|
|
288
355
|
try {
|
|
289
356
|
const body409 = await response.json();
|
|
290
357
|
code = body409["code"];
|
|
291
358
|
const v = body409["currentSpecVersion"];
|
|
292
359
|
if (typeof v === "string" && v !== "") currentSpecVersion = v;
|
|
360
|
+
staleDocuments = parseStaleConsentDocuments(body409["staleDocuments"]);
|
|
293
361
|
} catch {
|
|
294
362
|
}
|
|
295
363
|
if (code === "spec_stale") {
|
|
296
364
|
throw new OnboardingSpecStaleError(currentSpecVersion);
|
|
297
365
|
}
|
|
366
|
+
if (code === "consent_stale") {
|
|
367
|
+
throw new OnboardingConsentStaleError(staleDocuments);
|
|
368
|
+
}
|
|
369
|
+
if (code === "subdomain_taken") {
|
|
370
|
+
throw new OnboardingSubdomainTakenError();
|
|
371
|
+
}
|
|
298
372
|
throw new OnboardingConflictError(409);
|
|
299
373
|
}
|
|
300
374
|
if (response.status === 429) {
|
|
@@ -322,6 +396,6 @@ async function submitSignup(body, options) {
|
|
|
322
396
|
throw lastError ?? new OnboardingNetworkError();
|
|
323
397
|
}
|
|
324
398
|
|
|
325
|
-
export { computePayloadFingerprint, computeSignupFingerprint, generateIdempotencyKey, resolveBotToken, signupWireSchema, submitLead, submitSignup };
|
|
326
|
-
//# sourceMappingURL=chunk-
|
|
327
|
-
//# sourceMappingURL=chunk-
|
|
399
|
+
export { computePayloadFingerprint, computeSignupFingerprint, consentSubmitDocumentSchema, generateIdempotencyKey, resolveBotToken, signupWireSchema, submitLead, submitSignup };
|
|
400
|
+
//# sourceMappingURL=chunk-BMQINH6O.js.map
|
|
401
|
+
//# sourceMappingURL=chunk-BMQINH6O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/submit/client.ts","../src/submit/signup-client.ts"],"names":["MAX_ATTEMPTS","RETRY_DELAYS_MS","buildUrl","parseRetryAfter","sleep"],"mappings":"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","file":"chunk-BMQINH6O.js","sourcesContent":["/**\n * Submit client for POST /api/v1/public/onboarding/leads.\n *\n * Contract per D540 + hub REST interface 8686dd33:\n * - Idempotency-Key header REQUIRED (uuid v4, generated ONCE per logical\n * submission, REUSED on retries — same key = exactly-once delivery).\n * - Outgoing payload validated through leadsWireSchema (STRICT — unknown\n * keys rejected, matching additionalProperties:false) BEFORE any fetch.\n * - botToken in request body (D537).\n * - Response: { accepted: true } on 200/202 (empty/non-JSON 2xx body is\n * treated as acceptance; a JSON body that does not confirm acceptance\n * throws OnboardingServerError — see parseSuccessBody).\n * - 403 bot_challenge_failed → throw OnboardingBotChallengeError (no retry)\n * - 409 spec_stale (D540) → throw OnboardingSpecStaleError (no retry)\n * - 422 → throw OnboardingValidationError (no retry)\n * - 429 → throw OnboardingRateLimitError (no auto-retry)\n * - 5xx → retry with SAME idempotency key, capped backoff\n * - network error → retry with SAME idempotency key, capped backoff\n *\n * Max retries: 3 attempts total (1 initial + 2 retries).\n * Backoff: 1s, 2s (capped at 2 retries).\n */\n\nimport { validateLeadsPayload } from \"../form/schema.js\";\nimport type { ContactFormValues } from \"../form/schema.js\";\nimport {\n OnboardingBotChallengeError,\n OnboardingConflictError,\n OnboardingNetworkError,\n OnboardingRateLimitError,\n OnboardingServerError,\n OnboardingValidationError,\n} from \"./errors.js\";\nimport { DEFAULT_REQUEST_TIMEOUT_MS, fetchWithTimeout } from \"./http.js\";\n\nconst LEADS_PATH = \"/api/v1/public/onboarding/leads\";\nconst MAX_ATTEMPTS = 3;\nconst RETRY_DELAYS_MS = [1000, 2000] as const;\n\n/**\n * Resolve a bot token inside the retry loop, mapping ANY provider failure to\n * a typed OnboardingBotChallengeError. The provider is usually\n * executeRecaptcha, which throws a RecaptchaError if reCAPTCHA is unreachable\n * — that raw error must NOT escape submitLead/submitSignup (the @throws\n * contract is Onboarding* only) and must not silently burn the retry budget.\n */\nexport async function resolveBotTokenGuarded(\n source: BotTokenSource,\n): Promise<string> {\n try {\n return await resolveBotToken(source);\n } catch (err) {\n throw new OnboardingBotChallengeError(\n err instanceof Error\n ? `Could not obtain a bot-challenge token: ${err.message}`\n : \"Could not obtain a bot-challenge token.\",\n );\n }\n}\n\n/**\n * A bot token, or a provider that mints a FRESH one per attempt.\n *\n * reCAPTCHA tokens are SINGLE-USE — the server consumes them at siteverify.\n * If a 5xx happens AFTER the token was consumed, resending the same token on\n * retry gets 403. Pass a provider function to mint a fresh token per attempt;\n * a bare string is back-compat but the caller then OWNS that risk.\n */\nexport type BotTokenSource = string | (() => string | Promise<string>);\n\nexport async function resolveBotToken(source: BotTokenSource): Promise<string> {\n return typeof source === \"function\" ? source() : source;\n}\n\nexport interface LeadsSubmitOptions {\n baseUrl: string;\n /**\n * reCAPTCHA token (D537), or a provider called ONCE PER ATTEMPT to mint a\n * fresh token (recommended — tokens are single-use; a fixed string on a\n * retried request risks 403 after the first attempt consumed it).\n */\n botToken?: BotTokenSource;\n /**\n * Stable idempotency key for this submission. Generate ONCE with\n * `generateIdempotencyKey()` and pass the same key for retries.\n * The server chains this key through to D538 for exactly-once delivery.\n */\n idempotencyKey: string;\n /** Optional custom fetch implementation (defaults to global fetch). */\n fetch?: typeof globalThis.fetch;\n /**\n * Per-attempt request timeout in ms (default 15s). A stalled intake aborts\n * with OnboardingNetworkError so the widget re-enables instead of hanging.\n */\n timeoutMs?: number;\n /** Optional onAttempt callback for observability (attempt number, 1-based). */\n onAttempt?: (attempt: number) => void;\n}\n\nexport interface LeadsSubmitResult {\n accepted: true;\n}\n\n/**\n * Generate a stable idempotency key for one logical submission.\n * Uses the Web Crypto API (available in all modern browsers + Bun/Node >= 19).\n * Call ONCE per user submission, pass the SAME key on retries.\n */\nexport function generateIdempotencyKey(): string {\n // crypto.randomUUID() is available in all modern browsers, Bun, and Node >= 14.17\n return globalThis.crypto.randomUUID();\n}\n\nfunction buildUrl(baseUrl: string, path: string): string {\n const base = baseUrl.endsWith(\"/\") ? baseUrl.slice(0, -1) : baseUrl;\n return `${base}${path}`;\n}\n\nfunction parseRetryAfter(response: Response): number | null {\n const header = response.headers.get(\"retry-after\");\n if (!header) return null;\n const seconds = Number.parseInt(header, 10);\n return Number.isNaN(seconds) ? null : seconds;\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Compute a stable fingerprint of the outgoing lead payload, EXCLUDING\n * botToken (which is fresh per attempt by design).\n *\n * The widget uses this to decide whether a resubmission is a RETRY of the\n * same logical submission (same fingerprint → reuse the Idempotency-Key)\n * or a NEW logical submission (fingerprint changed because the user edited\n * fields → mint a NEW key). This mirrors the server's D538 payload-match\n * guard: same key + different payload is INVALID_ARGUMENT server-side.\n *\n * FNV-1a 32-bit over a stable JSON serialization of [name, email, phone,\n * message]. Collision risk is irrelevant here — a collision only means an\n * unnecessary key reuse across two submissions from the same browser tab.\n */\nexport function computePayloadFingerprint(values: ContactFormValues): string {\n const canonical = JSON.stringify([\n values.companyName,\n values.contactName,\n values.email,\n values.phone ?? null,\n values.message ?? null,\n [...(values.interestModules ?? [])].sort(),\n values.sizeBand ?? null,\n values.attribution ?? null,\n values.privacyNoticeAcknowledged,\n values.marketingOptIn,\n values.intakeKind,\n ]);\n let hash = 0x811c9dc5;\n for (let i = 0; i < canonical.length; i++) {\n hash ^= canonical.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/**\n * Parse a success-status (200/202) response body.\n *\n * Documented behavior (one rule, tested):\n * - EMPTY body or NON-JSON body → treated as acceptance. The locked\n * contract response is `{accepted:true}` and the status is already\n * 2xx; load balancers / CDNs occasionally strip or replace bodies on\n * 202, and a 2xx from the intake IS the acceptance signal.\n * - A PARSEABLE JSON body that is not `{accepted: true}` (e.g.\n * `{accepted:false}` or an unrelated shape) → OnboardingServerError.\n * A server-signaled soft-rejection on a success status must NOT be\n * masked as success. Not auto-retried (the status was 2xx; the server\n * state is ambiguous — the caller decides).\n */\nasync function parseSuccessBody(\n response: Response,\n): Promise<LeadsSubmitResult> {\n let text: string;\n try {\n text = await response.text();\n } catch {\n // Unreadable body on a 2xx — acceptance per the rule above.\n return { accepted: true };\n }\n if (text.trim() === \"\") {\n return { accepted: true };\n }\n let json: unknown;\n try {\n json = JSON.parse(text);\n } catch {\n // Non-JSON body on a 2xx (LB/CDN interstitial) — acceptance.\n return { accepted: true };\n }\n if (\n typeof json === \"object\" &&\n json !== null &&\n (json as { accepted?: unknown }).accepted === true\n ) {\n return { accepted: true };\n }\n throw new OnboardingServerError(\n response.status,\n `Success status ${response.status} but response body did not confirm acceptance.`,\n );\n}\n\n/**\n * Submit a contact-form lead to the onboarding intake.\n *\n * The outgoing payload is validated through leadsWireSchema in STRICT mode\n * before any network I/O — unknown keys are rejected client-side, exactly\n * mirroring the server's additionalProperties:false.\n *\n * Handles retry semantics: 5xx and network errors are retried up to\n * MAX_ATTEMPTS total, always with the SAME idempotencyKey to guarantee\n * exactly-once server-side processing (D538 idempotency chain).\n *\n * 403, 409, 422 and 429 are NOT retried — caller handles them.\n *\n * @throws {OnboardingValidationError} if the payload fails the strict wire\n * schema (unknown keys, constraint violations) — thrown BEFORE any fetch\n * @throws {OnboardingBotChallengeError} on 403, or if the bot-token provider\n * throws (reCAPTCHA unreachable) on any attempt\n * @throws {OnboardingConflictError} on 409 (leads has no spec_stale concept)\n * @throws {OnboardingRateLimitError} on 429\n * @throws {OnboardingServerError} on 5xx after all retries exhausted, or on\n * a 2xx whose JSON body does not confirm acceptance\n * @throws {OnboardingNetworkError} on network failure OR timeout after all\n * retries exhausted\n * @throws {OnboardingValidationError} on 422 (server-side re-validation)\n */\nexport async function submitLead(\n values: ContactFormValues,\n options: LeadsSubmitOptions,\n): Promise<LeadsSubmitResult> {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const url = buildUrl(options.baseUrl, LEADS_PATH);\n\n // Normalize non-token fields once (empty optionals → omitted). The token\n // is attached PER ATTEMPT below so a retry can carry a FRESH single-use\n // token; the body is therefore serialized inside the loop.\n const base: Record<string, unknown> = { ...values };\n if (base[\"phone\"] === \"\" || base[\"phone\"] === undefined) {\n delete base[\"phone\"];\n }\n if (base[\"message\"] === \"\" || base[\"message\"] === undefined) {\n delete base[\"message\"];\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"Idempotency-Key\": options.idempotencyKey,\n };\n\n let lastError: unknown = null;\n\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n options.onAttempt?.(attempt);\n\n // Mint a fresh token per attempt (single-use tokens; see BotTokenSource).\n // Guarded: a provider failure (reCAPTCHA down) becomes a typed\n // OnboardingBotChallengeError, never a raw RecaptchaError escaping.\n const botToken =\n options.botToken === undefined\n ? undefined\n : await resolveBotTokenGuarded(options.botToken);\n\n // Strict wire-schema validation BEFORE the network I/O of THIS attempt.\n // Extra keys are REJECTED (additionalProperties:false), not stripped.\n const parsed = validateLeadsPayload({ ...base, botToken });\n if (!parsed.success) {\n const detail = parsed.error.issues\n .map((i) => `${i.path.join(\".\") || \"(payload)\"}: ${i.message}`)\n .join(\"; \");\n throw new OnboardingValidationError(\n `Payload failed wire-schema validation: ${detail}`,\n );\n }\n const body = JSON.stringify(parsed.data);\n\n let response: Response;\n\n try {\n response = await fetchWithTimeout(url, {\n method: \"POST\",\n headers,\n body,\n timeoutMs,\n fetchImpl,\n });\n } catch (err) {\n // Timeout (abort) and network failures are both retryable transient.\n lastError = new OnboardingNetworkError(err);\n const delay = RETRY_DELAYS_MS[attempt - 1];\n if (attempt < MAX_ATTEMPTS && delay !== undefined) {\n await sleep(delay);\n continue;\n }\n throw lastError;\n }\n\n // 200/202 → success (body parse guarded; see parseSuccessBody)\n if (response.status === 200 || response.status === 202) {\n return parseSuccessBody(response);\n }\n\n // 403 → bot challenge failed — NOT retryable\n if (response.status === 403) {\n throw new OnboardingBotChallengeError();\n }\n\n // 409 on /leads is NOT spec_stale (the leads payload has no specVersion;\n // spec_stale is a /submit-only concept per D540). A leads 409 is a\n // generic conflict (e.g. a D538 same-key concurrent-processing race) —\n // surface OnboardingConflictError so headless consumers switching on\n // `kind` get the right diagnosis, not a \"form has been updated\" message.\n if (response.status === 409) {\n throw new OnboardingConflictError(409);\n }\n\n // 429 → rate limited — NOT auto-retried (caller decides when to retry)\n if (response.status === 429) {\n const retryAfter = parseRetryAfter(response);\n throw new OnboardingRateLimitError(retryAfter);\n }\n\n // 422 → validation error (client-side should have caught this)\n if (response.status === 422) {\n throw new OnboardingValidationError(\n \"Server rejected the submission (422). Please check your inputs.\",\n );\n }\n\n // 5xx → retryable server error\n if (response.status >= 500) {\n lastError = new OnboardingServerError(response.status);\n const delay = RETRY_DELAYS_MS[attempt - 1];\n if (attempt < MAX_ATTEMPTS && delay !== undefined) {\n await sleep(delay);\n continue;\n }\n throw lastError;\n }\n\n // Any other unexpected status\n throw new OnboardingServerError(\n response.status,\n `Unexpected response status ${response.status}`,\n );\n }\n\n // Should not be reachable; guard for type safety\n throw lastError ?? new OnboardingNetworkError();\n}\n","/**\n * Submit client for POST /api/v1/public/onboarding/submit (D540 §2).\n *\n * Contract per the LOCKED hub REST interface (PublicOnboardingSubmitRequest):\n * - Body: { specVersion, answers, selectedServices?, botToken,\n * consent:{tosAccepted:true, privacyAccepted:true},\n * credentials:{email, displayName?}, organisationName?, subdomain }\n * additionalProperties: false — validated STRICTLY client-side before\n * any fetch (unknown keys are rejected, not stripped).\n * - Idempotency-Key header REQUIRED (same D538 discipline as leads:\n * stable across retries of the SAME payload; the widget mints a new\n * key when the payload fingerprint changes).\n * - specVersion policy is reject-stale: 409 → OnboardingSpecStaleError\n * carrying the server's current version. The CALLER refetches the spec\n * and surfaces re-validation to the user — NEVER silent auto-resubmit.\n * - Response: { accepted:true, terminalStage, verificationEmailSent? }.\n *\n * Success-body rule for /submit (DIFFERENT from /leads, deliberately):\n * the response carries REQUIRED routing data (terminalStage decides what\n * the UI shows next), so a 2xx with an empty/unparseable/garbage body is\n * an OnboardingServerError — the SDK will not fabricate a terminal stage.\n * (/leads tolerates body-less 2xx because its only signal IS the status.)\n */\n\nimport { z } from \"zod/v3\";\nimport type { TerminalStage } from \"../spec/model.js\";\nimport type { ConsentSubmitDocument } from \"../consent/controller.js\";\nimport {\n OnboardingBotChallengeError,\n OnboardingConflictError,\n OnboardingConsentIncompleteError,\n OnboardingConsentStaleError,\n OnboardingNetworkError,\n OnboardingRateLimitError,\n OnboardingServerError,\n OnboardingSpecStaleError,\n OnboardingSubdomainTakenError,\n OnboardingValidationError,\n type StaleConsentDocument,\n} from \"./errors.js\";\nimport { type BotTokenSource, resolveBotTokenGuarded } from \"./client.js\";\nimport { DEFAULT_REQUEST_TIMEOUT_MS, fetchWithTimeout } from \"./http.js\";\n\nconst SUBMIT_PATH = \"/api/v1/public/onboarding/submit\";\nconst MAX_ATTEMPTS = 3;\nconst RETRY_DELAYS_MS = [1000, 2000] as const;\n\n/**\n * STRICT wire schema for ONE element of the D543 §4 consent.documents array.\n * `accepted` is const-true (an unaccepted doc is never sent — the consent\n * controller omits it), and `evidence` carries only the gate-relevant flags.\n */\nexport const consentSubmitDocumentSchema = z\n .object({\n legalDocumentId: z.string().min(1, \"legalDocumentId is required\"),\n versionNo: z.number().int().nonnegative(),\n accepted: z.literal(true, {\n errorMap: () => ({ message: \"a consent document must be accepted\" }),\n }),\n // evidence keys stay snake_case (the only snake bits in the wire shape).\n evidence: z\n .object({\n scrolled_to_end: z.boolean().optional(),\n opened: z.boolean().optional(),\n })\n .strict(),\n })\n .strict();\n\n/**\n * STRICT wire schema — mirrors PublicOnboardingSubmitRequest exactly\n * (additionalProperties: false; consent booleans are const-true; passwordless\n * per D1010, with the workspace subdomain required by D836).\n *\n * D543 §4: `consent` extends from booleans to a per-document array. The\n * `documents` array is AUTHORITATIVE (the server re-validates it); the\n * `tosAccepted`/`privacyAccepted` booleans are kept as a DERIVED convenience\n * (D543 \"kept as a derived convenience only\"). `documents` is OPTIONAL on the\n * wire for backward-compat — a pre-ConsentPack onboarder serves no consentPack\n * and submits the booleans alone (D543 \"pre-ConsentPack submits still parse\").\n */\nexport const signupWireSchema = z\n .object({\n specVersion: z.string().min(1, \"specVersion is required\"),\n answers: z.record(z.unknown()),\n selectedServices: z.array(z.string()).optional(),\n botToken: z.string().min(1, \"botToken is required\").optional(),\n consent: z\n .object({\n tosAccepted: z.literal(true, {\n errorMap: () => ({ message: \"Terms of Service must be accepted\" }),\n }),\n privacyAccepted: z.literal(true, {\n errorMap: () => ({ message: \"Privacy Policy must be accepted\" }),\n }),\n documents: z.array(consentSubmitDocumentSchema).optional(),\n })\n .strict(),\n credentials: z\n .object({\n email: z.string().email(\"A valid account email is required\"),\n displayName: z.string().trim().min(1).max(200).optional(),\n })\n .strict(),\n organisationName: z.string().trim().min(1).max(200).optional(),\n subdomain: z.string().trim().min(1).max(63),\n })\n .strict();\n\nexport type SignupWirePayload = z.infer<typeof signupWireSchema>;\n\n/** The payload the caller assembles (botToken attached by the transport). */\nexport interface SignupSubmitBody {\n specVersion: string;\n answers: Record<string, unknown>;\n selectedServices?: string[];\n /**\n * D543 §4 consent. `documents` (the per-doc array) is authoritative when\n * present; the booleans are the derived convenience. `documents` is omitted\n * for a pre-ConsentPack onboarder (backward-compat).\n */\n consent: {\n tosAccepted: true;\n privacyAccepted: true;\n documents?: ConsentSubmitDocument[];\n };\n credentials: { email: string; displayName?: string };\n organisationName?: string;\n subdomain: string;\n}\n\nexport interface SignupSubmitOptions {\n baseUrl: string;\n /**\n * reCAPTCHA token (D537), or a provider called ONCE PER ATTEMPT to mint a\n * fresh token. Tokens are single-use (consumed at siteverify), so a fixed\n * string on a retried request risks 403 after the first attempt consumed\n * it — pass a provider for retry-safe recovery.\n */\n botToken?: BotTokenSource;\n /** Stable idempotency key — same discipline as submitLead. */\n idempotencyKey: string;\n fetch?: typeof globalThis.fetch;\n /**\n * Per-attempt request timeout in ms (default 15s). A stalled intake aborts\n * with OnboardingNetworkError so the widget re-enables instead of hanging.\n */\n timeoutMs?: number;\n onAttempt?: (attempt: number) => void;\n}\n\nexport interface SignupSubmitResult {\n accepted: true;\n /** Stage the existing engine landed the applicant at (D540 §2). */\n terminalStage: TerminalStage;\n verificationEmailSent?: boolean;\n}\n\n/**\n * Stable fingerprint of a signup submission, EXCLUDING botToken (fresh per\n * attempt by design). Same role as computePayloadFingerprint for leads:\n * same fingerprint ⇒ retry (reuse the Idempotency-Key); changed\n * fingerprint ⇒ new logical submission (mint a new key) — mirroring the\n * server's D538 payload-match guard.\n */\nexport function computeSignupFingerprint(body: SignupSubmitBody): string {\n const canonical = JSON.stringify([\n body.specVersion,\n // Sort answer keys for stability regardless of insertion order.\n Object.keys(body.answers)\n .sort()\n .map((k) => [k, body.answers[k]]),\n [...(body.selectedServices ?? [])].sort(),\n body.credentials.email,\n body.credentials.displayName ?? null,\n body.organisationName ?? null,\n body.subdomain,\n body.consent.tosAccepted,\n body.consent.privacyAccepted,\n // Consent documents fold into the fingerprint (sorted by legalDocumentId\n // for order-independence) — re-accepting a changed doc after 409\n // consent_stale is a NEW logical submission and must rotate the key.\n [...(body.consent.documents ?? [])]\n .sort((a, b) => a.legalDocumentId.localeCompare(b.legalDocumentId))\n .map((d) => [\n d.legalDocumentId,\n d.versionNo,\n d.evidence.scrolled_to_end ?? false,\n d.evidence.opened ?? false,\n ]),\n ]);\n let hash = 0x811c9dc5;\n for (let i = 0; i < canonical.length; i++) {\n hash ^= canonical.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n\nfunction buildUrl(baseUrl: string, path: string): string {\n const base = baseUrl.endsWith(\"/\") ? baseUrl.slice(0, -1) : baseUrl;\n return `${base}${path}`;\n}\n\nfunction parseRetryAfter(response: Response): number | null {\n const header = response.headers.get(\"retry-after\");\n if (!header) return null;\n const seconds = Number.parseInt(header, 10);\n return Number.isNaN(seconds) ? null : seconds;\n}\n\n/**\n * Parse the OPTIONAL `staleDocuments` array on a consent_stale 409 body\n * (D543 §4). CANONICAL camelCase ONLY — { legalDocumentId, docType?,\n * currentVersionNo? } — NO snake_case hedging (P2b), matching the sibling\n * spec_stale error's \"no field hedging\" discipline: hedging both cases lets a\n * mismatched server shape pass silently and defeats the canonical-camelCase\n * pin. The server may omit the array entirely (caller then refetches + diffs);\n * an entry lacking a string `legalDocumentId` is skipped. Never throws.\n */\nfunction parseStaleConsentDocuments(value: unknown): StaleConsentDocument[] {\n if (!Array.isArray(value)) return [];\n const out: StaleConsentDocument[] = [];\n for (const entry of value) {\n if (entry === null || typeof entry !== \"object\") continue;\n const e = entry as Record<string, unknown>;\n if (typeof e[\"legalDocumentId\"] !== \"string\") continue;\n const doc: StaleConsentDocument = { legalDocumentId: e[\"legalDocumentId\"] };\n if (typeof e[\"docType\"] === \"string\") doc.docType = e[\"docType\"];\n if (typeof e[\"currentVersionNo\"] === \"number\") {\n doc.currentVersionNo = e[\"currentVersionNo\"];\n }\n out.push(doc);\n }\n return out;\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nconst TERMINAL_STAGES: readonly TerminalStage[] = [\n \"prospect\",\n \"inactive\",\n \"active\",\n];\n\n/** Parse the REQUIRED submit response shape; anything else → ServerError. */\nasync function parseSubmitSuccess(\n response: Response,\n): Promise<SignupSubmitResult> {\n let json: unknown;\n try {\n json = await response.json();\n } catch {\n throw new OnboardingServerError(\n response.status,\n `Success status ${response.status} but the submit response body was not JSON — terminalStage is unknown.`,\n );\n }\n if (\n typeof json === \"object\" &&\n json !== null &&\n (json as { accepted?: unknown }).accepted === true\n ) {\n const stage = (json as { terminalStage?: unknown }).terminalStage;\n if (\n typeof stage === \"string\" &&\n (TERMINAL_STAGES as readonly string[]).includes(stage)\n ) {\n const verificationEmailSent = (\n json as { verificationEmailSent?: unknown }\n ).verificationEmailSent;\n return {\n accepted: true,\n terminalStage: stage as TerminalStage,\n ...(typeof verificationEmailSent === \"boolean\"\n ? { verificationEmailSent }\n : {}),\n };\n }\n }\n throw new OnboardingServerError(\n response.status,\n `Success status ${response.status} but the submit response did not carry {accepted:true, terminalStage}.`,\n );\n}\n\n/**\n * Submit a full signup to the onboarding intake.\n *\n * @throws {OnboardingValidationError} strict wire-schema failure — BEFORE any fetch\n * @throws {OnboardingBotChallengeError} on 403, or if the bot-token provider\n * throws (reCAPTCHA unreachable) on any attempt\n * @throws {OnboardingSpecStaleError} on 409 with code:\"spec_stale\" (caller\n * refetches spec); carries the server's currentSpecVersion\n * @throws {OnboardingConsentStaleError} on 409 with code:\"consent_stale\"\n * (D543 §4 — a required legal doc changed; caller refetches spec + requires\n * re-acceptance of the changed docs, NEVER auto-accepts); carries\n * staleDocuments\n * @throws {OnboardingConsentIncompleteError} on 400 with\n * code:\"consent_incomplete\" (D543 §4 — server found a required doc missing/\n * unaccepted/evidence-insufficient)\n * @throws {OnboardingConflictError} on a 409 that is NOT spec_stale/consent_stale\n * @throws {OnboardingRateLimitError} on 429\n * @throws {OnboardingValidationError} on 422 (server re-validation) or a\n * generic 400 that is NOT consent_incomplete\n * @throws {OnboardingServerError} on 5xx after retries, or a 2xx without\n * the required {accepted:true, terminalStage} body\n * @throws {OnboardingNetworkError} on network failure OR timeout after retries\n */\nexport async function submitSignup(\n body: SignupSubmitBody,\n options: SignupSubmitOptions,\n): Promise<SignupSubmitResult> {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const url = buildUrl(options.baseUrl, SUBMIT_PATH);\n\n // Non-token fields normalized once; token attached PER ATTEMPT (single-use)\n // so a retry carries a fresh token — the body is serialized inside the loop.\n const base: Record<string, unknown> = { ...body };\n if (base[\"selectedServices\"] === undefined) {\n delete base[\"selectedServices\"];\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"Idempotency-Key\": options.idempotencyKey,\n };\n\n let lastError: unknown = null;\n\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n options.onAttempt?.(attempt);\n\n // Guarded: a token-provider failure (reCAPTCHA unreachable on a retry)\n // becomes a typed OnboardingBotChallengeError, never a raw RecaptchaError.\n const botToken =\n options.botToken === undefined\n ? undefined\n : await resolveBotTokenGuarded(options.botToken);\n const parsed = signupWireSchema.safeParse({ ...base, botToken });\n if (!parsed.success) {\n const detail = parsed.error.issues\n .map((i) => `${i.path.join(\".\") || \"(payload)\"}: ${i.message}`)\n .join(\"; \");\n throw new OnboardingValidationError(\n `Signup payload failed wire-schema validation: ${detail}`,\n );\n }\n const wireBody = JSON.stringify(parsed.data);\n\n let response: Response;\n try {\n response = await fetchWithTimeout(url, {\n method: \"POST\",\n headers,\n body: wireBody,\n timeoutMs,\n fetchImpl,\n });\n } catch (err) {\n lastError = new OnboardingNetworkError(err);\n const delay = RETRY_DELAYS_MS[attempt - 1];\n if (attempt < MAX_ATTEMPTS && delay !== undefined) {\n await sleep(delay);\n continue;\n }\n throw lastError;\n }\n\n if (response.status === 200 || response.status === 202) {\n return parseSubmitSuccess(response);\n }\n\n if (response.status === 403) {\n throw new OnboardingBotChallengeError();\n }\n\n // 400 — the D543 §4 PINNED consent_incomplete body is\n // { error: true, code: \"consent_incomplete\", ... }\n // The server's authoritative re-validation found a required live document\n // that was missing / unaccepted / evidence-insufficient. Any OTHER 400 is\n // a generic validation error (routes the user to review inputs).\n if (response.status === 400) {\n let code400: unknown;\n let message400: string | undefined;\n try {\n const body400 = (await response.json()) as Record<string, unknown>;\n code400 = body400[\"code\"];\n const m = body400[\"message\"];\n if (typeof m === \"string\" && m !== \"\") message400 = m;\n } catch {\n // keep defaults; a body-less 400 is a generic validation error\n }\n if (code400 === \"consent_incomplete\") {\n throw new OnboardingConsentIncompleteError(message400);\n }\n throw new OnboardingValidationError(\n message400 ??\n \"The server rejected the submission (400). Please review your inputs.\",\n );\n }\n\n // 409 — TWO pinned bodies (D540 spec_stale + D543 §4 consent_stale, its\n // sibling):\n // { error: true, code: \"spec_stale\", currentSpecVersion: string }\n // { error: true, code: \"consent_stale\", staleDocuments?: [...] }\n // ONLY a 409 carrying one of these codes maps to a typed stale error; any\n // other 409 is a generic conflict.\n if (response.status === 409) {\n let code: unknown;\n let currentSpecVersion = \"unknown\";\n let staleDocuments: StaleConsentDocument[] = [];\n try {\n const body409 = (await response.json()) as Record<string, unknown>;\n code = body409[\"code\"];\n const v = body409[\"currentSpecVersion\"];\n if (typeof v === \"string\" && v !== \"\") currentSpecVersion = v;\n staleDocuments = parseStaleConsentDocuments(body409[\"staleDocuments\"]);\n } catch {\n // keep defaults; a body-less 409 is treated as a generic conflict\n }\n if (code === \"spec_stale\") {\n throw new OnboardingSpecStaleError(currentSpecVersion);\n }\n if (code === \"consent_stale\") {\n throw new OnboardingConsentStaleError(staleDocuments);\n }\n if (code === \"subdomain_taken\") {\n throw new OnboardingSubdomainTakenError();\n }\n throw new OnboardingConflictError(409);\n }\n\n if (response.status === 429) {\n throw new OnboardingRateLimitError(parseRetryAfter(response));\n }\n\n if (response.status === 422) {\n throw new OnboardingValidationError(\n \"The server rejected the submission (422). Please review your inputs.\",\n );\n }\n\n if (response.status >= 500) {\n lastError = new OnboardingServerError(response.status);\n const delay = RETRY_DELAYS_MS[attempt - 1];\n if (attempt < MAX_ATTEMPTS && delay !== undefined) {\n await sleep(delay);\n continue;\n }\n throw lastError;\n }\n\n throw new OnboardingServerError(\n response.status,\n `Unexpected response status ${response.status}`,\n );\n }\n\n throw lastError ?? new OnboardingNetworkError();\n}\n"]}
|