@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.
Files changed (66) hide show
  1. package/README.md +78 -0
  2. package/dist/chunk-AZE3NPSY.js +186 -0
  3. package/dist/chunk-AZE3NPSY.js.map +1 -0
  4. package/dist/{chunk-BQCTDU5V.js → chunk-BMQINH6O.js} +89 -15
  5. package/dist/chunk-BMQINH6O.js.map +1 -0
  6. package/dist/{chunk-34BQX2KR.js → chunk-F75ZJERM.js} +41 -6
  7. package/dist/chunk-F75ZJERM.js.map +1 -0
  8. package/dist/chunk-FIRSA7EH.js +3 -0
  9. package/dist/{chunk-ZDY2I6KY.js.map → chunk-FIRSA7EH.js.map} +1 -1
  10. package/dist/{chunk-SWMIIUXR.js → chunk-IPAKPLIF.js} +590 -92
  11. package/dist/chunk-IPAKPLIF.js.map +1 -0
  12. package/dist/chunk-JFDQZ3AC.js +3 -0
  13. package/dist/chunk-JFDQZ3AC.js.map +1 -0
  14. package/dist/{chunk-4SWOVHZ2.js → chunk-KQTS47FC.js} +129 -17
  15. package/dist/chunk-KQTS47FC.js.map +1 -0
  16. package/dist/chunk-LVJM4CTG.js +306 -0
  17. package/dist/chunk-LVJM4CTG.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-SIMLMQPT.js +37 -0
  21. package/dist/chunk-SIMLMQPT.js.map +1 -0
  22. package/dist/chunk-U56GQITG.js +79 -0
  23. package/dist/chunk-U56GQITG.js.map +1 -0
  24. package/dist/{chunk-AFCJLUPK.js → chunk-U6AXKGYQ.js} +14 -5
  25. package/dist/chunk-U6AXKGYQ.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/client-CqE2dF9A.d.ts +86 -0
  29. package/dist/consent/index.d.ts +59 -0
  30. package/dist/consent/index.js +7 -0
  31. package/dist/consent/index.js.map +1 -0
  32. package/dist/controller-BQb62D0N.d.ts +177 -0
  33. package/dist/dynamic/index.d.ts +57 -6
  34. package/dist/dynamic/index.js +4 -2
  35. package/dist/errors-vJkye4a0.d.ts +110 -0
  36. package/dist/form/index.d.ts +2 -2
  37. package/dist/form/index.js +2 -2
  38. package/dist/index-Dqfa890U.d.ts +83 -0
  39. package/dist/index.d.ts +12 -6
  40. package/dist/index.js +12 -8
  41. package/dist/{model-IEyDI54w.d.ts → model-ESLSEbi0.d.ts} +199 -46
  42. package/dist/model-mnjAEtKe.d.ts +253 -0
  43. package/dist/render-DC7P2BGQ.js +5 -0
  44. package/dist/render-DC7P2BGQ.js.map +1 -0
  45. package/dist/schema-BrXb9gnh.d.ts +267 -0
  46. package/dist/{signup-client-Bl6eUrxR.d.ts → signup-client-BL5DfarJ.d.ts} +152 -19
  47. package/dist/spec/index.d.ts +10 -81
  48. package/dist/spec/index.js +3 -2
  49. package/dist/submit/index.d.ts +7 -71
  50. package/dist/submit/index.js +3 -3
  51. package/dist/viewer/index.d.ts +4 -0
  52. package/dist/viewer/index.js +5 -0
  53. package/dist/viewer/index.js.map +1 -0
  54. package/dist/widget/index.d.ts +50 -7
  55. package/dist/widget/index.js +9 -7
  56. package/package.json +21 -2
  57. package/dist/chunk-34BQX2KR.js.map +0 -1
  58. package/dist/chunk-4SWOVHZ2.js.map +0 -1
  59. package/dist/chunk-AFCJLUPK.js.map +0 -1
  60. package/dist/chunk-AW7W3HAC.js.map +0 -1
  61. package/dist/chunk-BIFANV7M.js +0 -22
  62. package/dist/chunk-BIFANV7M.js.map +0 -1
  63. package/dist/chunk-BQCTDU5V.js.map +0 -1
  64. package/dist/chunk-SWMIIUXR.js.map +0 -1
  65. package/dist/chunk-ZDY2I6KY.js +0 -3
  66. package/dist/schema-TfyBU6JW.d.ts +0 -107
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/form/schema.ts"],"names":[],"mappings":";;;AAyBO,IAAM,iBAAA,GAAoB,EAAE,MAAA,CAAO;AAAA,EACxC,MAAM,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,GAAG,EAAE,QAAA,EAAS;AAAA,EACnC,WAAA,EAAa,CAAA,CACV,MAAA,EAAO,CACP,GAAA,CAAI,GAAG,0BAA0B,CAAA,CACjC,GAAA,CAAI,GAAA,EAAK,8CAA8C,CAAA;AAAA,EAC1D,WAAA,EAAa,CAAA,CACV,MAAA,EAAO,CACP,GAAA,CAAI,GAAG,0BAA0B,CAAA,CACjC,GAAA,CAAI,GAAA,EAAK,8CAA8C,CAAA;AAAA,EAC1D,KAAA,EAAO,CAAA,CAAE,MAAA,EAAO,CAAE,MAAM,mCAAmC,CAAA;AAAA,EAC3D,KAAA,EAAO,CAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC3B,OAAA,EAAS,EACN,MAAA,EAAO,CACP,IAAI,GAAA,EAAM,0CAA0C,EACpD,QAAA,EAAS;AAAA,EACZ,eAAA,EAAiB,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,QAAO,CAAE,GAAA,CAAI,CAAC,CAAC,EAAE,GAAA,CAAI,EAAE,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAA;AAAA,EAC9D,UAAU,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,EAAE,EAAE,QAAA,EAAS;AAAA,EACtC,WAAA,EAAa,EACV,MAAA,CAAO;AAAA,IACN,cAAc,CAAA,CAAE,MAAA,CAAO,EAAE,MAAA,EAAQ,EAAE,QAAA,EAAS;AAAA,IAC5C,eAAe,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,IAAI,EAAE,QAAA,EAAS;AAAA,IAC7C,aAAa,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,IAAI,EAAE,QAAA,EAAS;AAAA,IAC3C,QAAQ,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,GAAG,EAAE,QAAA;AAAS,GACtC,CAAA,CACA,MAAA,EAAO,CACP,QAAA,EAAS;AAAA,EACZ,yBAAA,EAA2B,EACxB,OAAA,EAAQ,CACR,OAAO,CAAC,KAAA,KAAU,OAAO,4CAA4C,CAAA;AAAA,EACxE,cAAA,EAAgB,EAAE,OAAA,EAAQ;AAAA,EAC1B,YAAY,CAAA,CAAE,IAAA,CAAK,CAAC,SAAA,EAAW,SAAS,CAAC,CAAA;AAAA;AAAA,EAEzC,SAAS,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AAC7B,CAAC;AAYM,IAAM,eAAA,GAAkB,kBAC5B,MAAA,CAAO;AAAA,EACN,QAAA,EAAU,EAAE,MAAA,EAAO,CAAE,IAAI,CAAA,EAAG,sBAAsB,EAAE,QAAA;AACtD,CAAC,EACA,MAAA;AAQI,SAAS,oBAAoB,MAAA,EAAiB;AACnD,EAAA,OAAO,iBAAA,CAAkB,UAAU,MAAM,CAAA;AAC3C;AAQO,SAAS,qBAAqB,OAAA,EAAkB;AACrD,EAAA,OAAO,eAAA,CAAgB,UAAU,OAAO,CAAA;AAC1C","file":"chunk-SIMLMQPT.js","sourcesContent":["/**\n * Contact-form Zod schema — mirrors the LOCKED hub REST contract exactly:\n * POST /api/v1/public/onboarding/leads\n * Schema: PublicOnboardingLeadsRequest (hub id 8686dd33)\n *\n * Constraints (per hub JSON Schema):\n * name — string, 1-200 chars, required\n * email — email format, required\n * phone — string, optional (no server-side constraint beyond string)\n * message — string, ≤4000 chars, optional\n * botToken — string, required (reCAPTCHA token, D537)\n *\n * additionalProperties: false\n *\n * The schema intentionally does NOT include botToken in the public-facing\n * ContactFormValues type — that field is injected by the submit client right\n * before the POST (the widget/form never exposes it to the user).\n */\n\nimport { z } from \"zod/v3\";\n\n/**\n * The user-visible contact form fields. Validated client-side for UX.\n * Server re-validates authoritatively (D540 §2).\n */\nexport const contactFormSchema = z.object({\n name: z.string().max(200).optional(),\n companyName: z\n .string()\n .min(1, \"Company name is required\")\n .max(200, \"Company name must be 200 characters or fewer\"),\n contactName: z\n .string()\n .min(1, \"Contact name is required\")\n .max(200, \"Contact name must be 200 characters or fewer\"),\n email: z.string().email(\"A valid email address is required\"),\n phone: z.string().optional(),\n message: z\n .string()\n .max(4000, \"Message must be 4000 characters or fewer\")\n .optional(),\n interestModules: z.array(z.string().min(1)).max(32).default([]),\n sizeBand: z.string().max(80).optional(),\n attribution: z\n .object({\n campaignTags: z.record(z.string()).optional(),\n referringSite: z.string().max(2048).optional(),\n landingPage: z.string().max(2048).optional(),\n button: z.string().max(120).optional(),\n })\n .strict()\n .optional(),\n privacyNoticeAcknowledged: z\n .boolean()\n .refine((value) => value, \"Privacy notice acknowledgement is required\"),\n marketingOptIn: z.boolean(),\n intakeKind: z.enum([\"contact\", \"enquiry\"]),\n // Q46 default: a visually-hidden field. Humans leave it empty.\n website: z.string().max(0).optional(),\n});\n\nexport type ContactFormValues = z.infer<typeof contactFormSchema>;\n\n/**\n * The full wire payload — contactFormSchema + botToken.\n *\n * STRICT mode: unknown keys are REJECTED (not stripped), mirroring the\n * server's `additionalProperties: false`. `submitLead` runs every outgoing\n * payload through this schema before the POST — an extra key never reaches\n * the wire; it fails fast client-side with OnboardingValidationError.\n */\nexport const leadsWireSchema = contactFormSchema\n .extend({\n botToken: z.string().min(1, \"botToken is required\").optional(),\n })\n .strict();\n\nexport type LeadsWirePayload = z.infer<typeof leadsWireSchema>;\n\n/**\n * Validate user-visible form values (no botToken required here).\n * Returns a Zod SafeParseReturnType (inferred).\n */\nexport function validateContactForm(values: unknown) {\n return contactFormSchema.safeParse(values);\n}\n\n/**\n * Validate the full wire payload (including botToken) before submission.\n * STRICT: unknown keys are rejected, matching additionalProperties:false.\n * This is the exact validation `submitLead` applies on the wire path.\n * Returns a Zod SafeParseReturnType (inferred).\n */\nexport function validateLeadsPayload(payload: unknown) {\n return leadsWireSchema.safeParse(payload);\n}\n"]}
@@ -0,0 +1,79 @@
1
+ import { z } from 'zod/v3';
2
+
3
+ // src/consent/model.ts
4
+ var CUSTOM_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
5
+ var ACCEPTANCE_LEVELS = [
6
+ "checkbox",
7
+ "scroll_then_accept",
8
+ "open_then_accept"
9
+ ];
10
+ var acceptanceLevelSchema = z.enum(ACCEPTANCE_LEVELS);
11
+ var DOC_TYPES = ["tos", "privacy", "dpa", "custom"];
12
+ var consentDocumentSchema = z.object({
13
+ // Server-side opaque registry id (D543 §3 legal_documents.id). Echoed
14
+ // verbatim on submit as consent.documents[].legalDocumentId (D543 §4);
15
+ // the SDK never synthesizes it. See DRIFT 5e3df7a3 (module doc).
16
+ legalDocumentId: z.string().min(1),
17
+ docType: z.enum(DOC_TYPES),
18
+ // Nullable (NOT omitted): non-custom docs send customKey:null.
19
+ customKey: z.string().min(1).regex(
20
+ CUSTOM_KEY_PATTERN,
21
+ "customKey must contain only letters, digits, underscore, or hyphen"
22
+ ).nullable(),
23
+ title: z.string().min(1),
24
+ // Monotonic version of the live document. This is the SUBMIT PIN: it is
25
+ // echoed on submit (consent.documents[].versionNo) and the server matches
26
+ // it against the current live version (mismatch ⇒ 409 consent_stale). Any
27
+ // content change publishes a NEW version, so versionNo is a complete pin.
28
+ versionNo: z.number().int().nonnegative(),
29
+ // sha256 of the live artifact — spec-side integrity metadata (P4). NOT
30
+ // echoed on submit: versionNo is the pin (a content change = new version),
31
+ // so the submit array carries versionNo only, per the canonical wire shape.
32
+ // The SDK exposes contentHash so a consumer can display/verify integrity;
33
+ // the server does NOT require it on the submit array.
34
+ contentHash: z.string().min(1),
35
+ // Short-TTL presigned GET for the artifact bytes (D543 §4 / 04-pii).
36
+ downloadUrl: z.string().min(1),
37
+ acceptanceLevel: acceptanceLevelSchema,
38
+ required: z.boolean()
39
+ }).strict().superRefine((doc, ctx) => {
40
+ if (doc.docType === "custom" && doc.customKey === null) {
41
+ ctx.addIssue({
42
+ code: z.ZodIssueCode.custom,
43
+ path: ["customKey"],
44
+ message: "customKey is required (non-null) when docType is 'custom'"
45
+ });
46
+ }
47
+ if (doc.docType !== "custom" && doc.customKey !== null) {
48
+ ctx.addIssue({
49
+ code: z.ZodIssueCode.custom,
50
+ path: ["customKey"],
51
+ message: "customKey must be null when docType is not 'custom'"
52
+ });
53
+ }
54
+ });
55
+ var consentPackSchema = z.object({
56
+ documents: z.array(consentDocumentSchema)
57
+ }).strict().superRefine((pack, ctx) => {
58
+ const seen = /* @__PURE__ */ new Set();
59
+ for (let i = 0; i < pack.documents.length; i++) {
60
+ const doc = pack.documents[i];
61
+ if (!doc) continue;
62
+ const key = consentDocKey(doc);
63
+ if (seen.has(key)) {
64
+ ctx.addIssue({
65
+ code: z.ZodIssueCode.custom,
66
+ path: ["documents", i],
67
+ message: `duplicate consent document key "${key}"`
68
+ });
69
+ }
70
+ seen.add(key);
71
+ }
72
+ });
73
+ function consentDocKey(doc) {
74
+ return doc.docType === "custom" ? `custom:${doc.customKey ?? ""}` : doc.docType;
75
+ }
76
+
77
+ export { ACCEPTANCE_LEVELS, DOC_TYPES, acceptanceLevelSchema, consentDocKey, consentDocumentSchema, consentPackSchema };
78
+ //# sourceMappingURL=chunk-U56GQITG.js.map
79
+ //# sourceMappingURL=chunk-U56GQITG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/consent/model.ts"],"names":[],"mappings":"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","file":"chunk-U56GQITG.js","sourcesContent":["/**\n * D543 §4 ConsentPack spec model — the `consentPack` block that GET\n * /api/v1/public/onboarding/spec grows on top of the D540 shape:\n *\n * consentPack: {\n * documents: [\n * { doc_type, custom_key?, title, version_no, content_hash,\n * download_url, acceptance_level, required }\n * ]\n * }\n *\n * The intake projects ONLY `live` versions into this block (D543 §4);\n * `download_url` is a short-TTL presigned GET the SDK renders/opens.\n *\n * `acceptance_level` (D543 §2) is a per-document GATE:\n * - \"checkbox\" — a single accept checkbox (basic clickwrap).\n * - \"scroll_then_accept\" — accept disabled until the doc is scrolled to\n * the end (client asserts evidence.scrolled_to_end).\n * - \"open_then_accept\" — accept disabled until the doc is opened/viewed\n * (client asserts evidence.opened).\n * (E-signature is a deferred future level, D543 §2 — NOT modeled here.)\n *\n * FAIL-CLOSED (R1 + D543 §5 / D540 §1): a malformed consentPack throws\n * OnboardingSpecInvalidError at parse time — never rendered half-parsed\n * (a dropped `required` doc would let a user submit without consent, and a\n * missing `content_hash`/`version_no` would break the server's re-validation\n * match). The SDK refuses to render a consent step it cannot trust.\n *\n * DOM-ID HARDENING: a document's DOM-id token is derived from `doc_type`\n * (a fixed enum — safe) or, for custom docs, from `custom_key` (free-form\n * from the served spec). Custom keys are interpolated into element ids and\n * (historically) selectors by the widget layer, so they are constrained to\n * the SAME strict identifier alphabet as field keys (CUSTOM_KEY_PATTERN,\n * identical to the spec model's FIELD_KEY_PATTERN) and REJECTED at parse time\n * if they carry CSS/selector metacharacters. This is layer 1; the widget also\n * CSS.escapes every id token as defense-in-depth.\n *\n * SUBMIT PIN (P4): the submit array carries {legalDocumentId, versionNo,\n * accepted, evidence} — the server matches on legalDocumentId + versionNo\n * (contentHash is spec-side integrity metadata, NOT echoed on submit; a\n * content change publishes a new versionNo, so versionNo is a complete pin).\n *\n * DRIFT 5e3df7a3 (2026-07-14, consumer-visible) — RESOLVED by the canonical\n * shape: D543 §4 as literally written omitted `legal_document_id` from the\n * spec block, yet the submit array is KEYED on it (the server matches\n * legalDocumentId + versionNo against the live version). The\n * canonical alignment (both tiers, camelCase) INCLUDES `legalDocumentId` in\n * the spec `consentPack.documents[]`, exactly as the drift proposed. This\n * model REQUIRES it (fail-closed OnboardingSpecInvalidError if missing).\n */\n\nimport { z } from \"zod/v3\";\n\n/**\n * DOM-id-safe custom-key alphabet. INTENTIONALLY identical to the spec\n * model's FIELD_KEY_PATTERN (`/^[A-Za-z0-9_-]+$/`), duplicated here rather\n * than imported to avoid a spec↔consent import cycle (spec/model.ts imports\n * consentPackSchema from this file). Any change to one must mirror the other.\n */\nconst CUSTOM_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/** D543 §2 acceptance-level enum. Extensible (adding a level is additive). */\nexport const ACCEPTANCE_LEVELS = [\n \"checkbox\",\n \"scroll_then_accept\",\n \"open_then_accept\",\n] as const;\n\nexport type AcceptanceLevel = (typeof ACCEPTANCE_LEVELS)[number];\n\nexport const acceptanceLevelSchema = z.enum(ACCEPTANCE_LEVELS);\n\n/** D543 §3 docType enum. */\nexport const DOC_TYPES = [\"tos\", \"privacy\", \"dpa\", \"custom\"] as const;\nexport type ConsentDocType = (typeof DOC_TYPES)[number];\n\n/**\n * One live legal document projected into the consent step. Mirrors the D543\n * §4 `consentPack.documents[]` element — CANONICAL camelCase wire shape (both\n * tenant + platform tiers aligned to this): { legalDocumentId, docType,\n * customKey (nullable), title, versionNo, contentHash, downloadUrl,\n * acceptanceLevel, required }. The `acceptanceLevel` enum VALUES stay snake\n * (checkbox | scroll_then_accept | open_then_accept).\n *\n * Cross-field invariant (D543 §3): `customKey` is a non-null identifier when\n * `docType === \"custom\"` and MUST be null otherwise (a canonical tos/privacy/\n * dpa doc is keyed by docType; only custom docs carry a key). The custom-key\n * alphabet is DOM-id safe.\n */\nexport const consentDocumentSchema = z\n .object({\n // Server-side opaque registry id (D543 §3 legal_documents.id). Echoed\n // verbatim on submit as consent.documents[].legalDocumentId (D543 §4);\n // the SDK never synthesizes it. See DRIFT 5e3df7a3 (module doc).\n legalDocumentId: z.string().min(1),\n docType: z.enum(DOC_TYPES),\n // Nullable (NOT omitted): non-custom docs send customKey:null.\n customKey: z\n .string()\n .min(1)\n .regex(\n CUSTOM_KEY_PATTERN,\n \"customKey must contain only letters, digits, underscore, or hyphen\",\n )\n .nullable(),\n title: z.string().min(1),\n // Monotonic version of the live document. This is the SUBMIT PIN: it is\n // echoed on submit (consent.documents[].versionNo) and the server matches\n // it against the current live version (mismatch ⇒ 409 consent_stale). Any\n // content change publishes a NEW version, so versionNo is a complete pin.\n versionNo: z.number().int().nonnegative(),\n // sha256 of the live artifact — spec-side integrity metadata (P4). NOT\n // echoed on submit: versionNo is the pin (a content change = new version),\n // so the submit array carries versionNo only, per the canonical wire shape.\n // The SDK exposes contentHash so a consumer can display/verify integrity;\n // the server does NOT require it on the submit array.\n contentHash: z.string().min(1),\n // Short-TTL presigned GET for the artifact bytes (D543 §4 / 04-pii).\n downloadUrl: z.string().min(1),\n acceptanceLevel: acceptanceLevelSchema,\n required: z.boolean(),\n })\n .strict()\n .superRefine((doc, ctx) => {\n if (doc.docType === \"custom\" && doc.customKey === null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"customKey\"],\n message: \"customKey is required (non-null) when docType is 'custom'\",\n });\n }\n if (doc.docType !== \"custom\" && doc.customKey !== null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"customKey\"],\n message: \"customKey must be null when docType is not 'custom'\",\n });\n }\n });\n\nexport type ConsentDocument = z.infer<typeof consentDocumentSchema>;\n\n/**\n * The `consentPack` block. `documents` may be empty (an onboarder with no\n * required legal docs) — the consent stage is then omitted from the flow.\n *\n * Uniqueness: no two documents may share the same STABLE key (docType, or\n * custom:<customKey> for custom docs). A duplicate would collide their DOM\n * ids AND their consent records — fail-closed at parse (the server keys the\n * registry on (scope, docType) / (scope, customKey) per D543 §3, so a\n * duplicate here is a projection bug, not a renderable spec).\n */\nexport const consentPackSchema = z\n .object({\n documents: z.array(consentDocumentSchema),\n })\n .strict()\n .superRefine((pack, ctx) => {\n const seen = new Set<string>();\n for (let i = 0; i < pack.documents.length; i++) {\n const doc = pack.documents[i];\n if (!doc) continue;\n const key = consentDocKey(doc);\n if (seen.has(key)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [\"documents\", i],\n message: `duplicate consent document key \"${key}\"`,\n });\n }\n seen.add(key);\n }\n });\n\nexport type ConsentPack = z.infer<typeof consentPackSchema>;\n\n/**\n * Stable, collision-proof identity for a consent document, used as the map\n * key in the controller and the DOM-id token in the widget. `docType` is a\n * fixed enum; `custom:` is prefixed for custom docs so a customKey of\n * \"tos\" can never alias the canonical tos doc.\n *\n * Note: this is NOT the submit key — the submit array keys on the server's\n * opaque `legalDocumentId` (D543 §4), which the SDK does not synthesize.\n * This is a purely CLIENT-side identity for state + DOM.\n */\nexport function consentDocKey(doc: {\n docType: ConsentDocType;\n customKey?: string | null | undefined;\n}): string {\n return doc.docType === \"custom\"\n ? `custom:${doc.customKey ?? \"\"}`\n : doc.docType;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { contactFormSchema } from './chunk-BIFANV7M.js';
1
+ import { contactFormSchema } from './chunk-SIMLMQPT.js';
2
2
 
3
3
  // src/form/state.ts
4
4
  function zodIssuesToFieldErrors(issues) {
@@ -8,10 +8,19 @@ function zodIssuesToFieldErrors(issues) {
8
8
  }));
9
9
  }
10
10
  var INITIAL_VALUES = {
11
- name: "",
11
+ name: void 0,
12
+ companyName: "",
13
+ contactName: "",
12
14
  email: "",
13
15
  phone: void 0,
14
- message: void 0
16
+ message: void 0,
17
+ interestModules: [],
18
+ sizeBand: void 0,
19
+ attribution: void 0,
20
+ privacyNoticeAcknowledged: false,
21
+ marketingOptIn: false,
22
+ intakeKind: "enquiry",
23
+ website: void 0
15
24
  };
16
25
  function createContactFormState() {
17
26
  let state = {
@@ -88,5 +97,5 @@ function createContactFormState() {
88
97
  }
89
98
 
90
99
  export { createContactFormState };
91
- //# sourceMappingURL=chunk-AFCJLUPK.js.map
92
- //# sourceMappingURL=chunk-AFCJLUPK.js.map
100
+ //# sourceMappingURL=chunk-U6AXKGYQ.js.map
101
+ //# sourceMappingURL=chunk-U6AXKGYQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/form/state.ts"],"names":[],"mappings":"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","file":"chunk-U6AXKGYQ.js","sourcesContent":["/**\n * Headless form state manager for the contact form.\n *\n * Designed to be framework-agnostic — callers supply a change callback\n * and call the returned mutators. The widget layer uses this internally;\n * framework integrations (React, Vue, etc.) can wrap it in their own hooks.\n */\n\nimport type { ZodIssue } from \"zod/v3\";\nimport { contactFormSchema, type ContactFormValues } from \"./schema.js\";\n\nexport interface FieldError {\n path: string;\n message: string;\n}\n\nexport type FormStatus = \"idle\" | \"submitting\" | \"success\" | \"error\";\n\nexport interface ContactFormState {\n values: ContactFormValues;\n /** Per-field validation errors from the last validate() call. */\n errors: FieldError[];\n /** Cross-field / server error message. */\n serverError: string | null;\n status: FormStatus;\n /** True if values have been touched at least once. */\n dirty: boolean;\n}\n\nexport type ContactFormStateListener = (state: ContactFormState) => void;\n\nexport interface ContactFormControl {\n /** Current snapshot of form state. */\n getState(): ContactFormState;\n /** Update a single field value and re-validate if dirty. */\n setValue<K extends keyof ContactFormValues>(\n field: K,\n value: ContactFormValues[K],\n ): void;\n /**\n * Run full client-side validation. Returns true if valid.\n * Also marks the form dirty so future setValue calls auto-validate.\n */\n validate(): boolean;\n /** Mark the form as submitting. Called by the submit client. */\n setSubmitting(): void;\n /** Mark the form as successfully submitted. */\n setSuccess(): void;\n /** Set a server-side error (e.g. 429 rate limited). */\n setError(message: string): void;\n /** Reset to initial idle state. */\n reset(): void;\n /** Subscribe to state changes. Returns unsubscribe function. */\n subscribe(listener: ContactFormStateListener): () => void;\n}\n\nfunction zodIssuesToFieldErrors(issues: ZodIssue[]): FieldError[] {\n return issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n }));\n}\n\nconst INITIAL_VALUES: ContactFormValues = {\n name: undefined,\n companyName: \"\",\n contactName: \"\",\n email: \"\",\n phone: undefined,\n message: undefined,\n interestModules: [],\n sizeBand: undefined,\n attribution: undefined,\n privacyNoticeAcknowledged: false,\n marketingOptIn: false,\n intakeKind: \"enquiry\",\n website: undefined,\n};\n\n/**\n * Create a new headless contact form state controller.\n *\n * @example\n * ```ts\n * const form = createContactFormState();\n * form.subscribe((state) => renderUI(state));\n * form.setValue(\"name\", \"Alice\");\n * const valid = form.validate();\n * if (valid) await submitContact(form.getState().values, botToken);\n * ```\n */\nexport function createContactFormState(): ContactFormControl {\n let state: ContactFormState = {\n values: { ...INITIAL_VALUES },\n errors: [],\n serverError: null,\n status: \"idle\",\n dirty: false,\n };\n\n const listeners = new Set<ContactFormStateListener>();\n\n function emit(): void {\n for (const listener of listeners) {\n listener({ ...state, values: { ...state.values } });\n }\n }\n\n function runValidation(): FieldError[] {\n const result = contactFormSchema.safeParse(state.values);\n if (result.success) return [];\n return zodIssuesToFieldErrors(result.error.issues);\n }\n\n // Once validate() has run at least once, EVERY subsequent setValue\n // revalidates — errors both clear when a field is fixed AND reappear\n // when a fixed field is broken again. (Previously the revalidation was\n // gated on errors.length > 0, so fix-then-rebreak silently showed no\n // error until the next explicit validate().)\n let hasValidated = false;\n\n return {\n getState() {\n return { ...state, values: { ...state.values } };\n },\n\n setValue(field, value) {\n state = {\n ...state,\n values: { ...state.values, [field]: value },\n dirty: true,\n };\n if (hasValidated) {\n state = { ...state, errors: runValidation() };\n }\n emit();\n },\n\n validate() {\n hasValidated = true;\n const errors = runValidation();\n state = { ...state, errors, dirty: true };\n emit();\n return errors.length === 0;\n },\n\n setSubmitting() {\n state = { ...state, status: \"submitting\", serverError: null, errors: [] };\n emit();\n },\n\n setSuccess() {\n state = { ...state, status: \"success\", serverError: null };\n emit();\n },\n\n setError(message) {\n state = { ...state, status: \"error\", serverError: message };\n emit();\n },\n\n reset() {\n hasValidated = false;\n state = {\n values: { ...INITIAL_VALUES },\n errors: [],\n serverError: null,\n status: \"idle\",\n dirty: false,\n };\n emit();\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"]}
@@ -29,6 +29,16 @@ var OnboardingConflictError = class extends OnboardingError {
29
29
  this.name = "OnboardingConflictError";
30
30
  }
31
31
  };
32
+ var OnboardingSubdomainTakenError = class extends OnboardingError {
33
+ constructor() {
34
+ super(
35
+ "subdomain_taken",
36
+ "That workspace address is already taken. Choose another.",
37
+ 409
38
+ );
39
+ this.name = "OnboardingSubdomainTakenError";
40
+ }
41
+ };
32
42
  var OnboardingRateLimitError = class extends OnboardingError {
33
43
  retryAfterSeconds;
34
44
  constructor(retryAfterSeconds = null) {
@@ -53,6 +63,33 @@ var OnboardingSpecStaleError = class extends OnboardingError {
53
63
  this.currentSpecVersion = currentSpecVersion;
54
64
  }
55
65
  };
66
+ var OnboardingConsentStaleError = class extends OnboardingError {
67
+ /**
68
+ * The documents the server reports as changed (may be empty when the
69
+ * server does not enumerate them — the caller then refetches + diffs).
70
+ */
71
+ staleDocuments;
72
+ constructor(staleDocuments = []) {
73
+ const which = staleDocuments.length > 0 ? ` (${staleDocuments.map((d) => d.docType ?? d.legalDocumentId ?? "a document").join(", ")})` : "";
74
+ super(
75
+ "consent_stale",
76
+ `One or more legal documents were updated${which}. Please review and re-accept the updated documents, then try again.`,
77
+ 409
78
+ );
79
+ this.name = "OnboardingConsentStaleError";
80
+ this.staleDocuments = staleDocuments;
81
+ }
82
+ };
83
+ var OnboardingConsentIncompleteError = class extends OnboardingError {
84
+ constructor(message) {
85
+ super(
86
+ "consent_incomplete",
87
+ message ?? "Consent is incomplete (400 consent_incomplete). One or more required documents were not accepted at their required level.",
88
+ 400
89
+ );
90
+ this.name = "OnboardingConsentIncompleteError";
91
+ }
92
+ };
56
93
  var OnboardingServerError = class extends OnboardingError {
57
94
  constructor(statusCode, message) {
58
95
  super(
@@ -149,6 +186,6 @@ function withTimeout(op, timeoutMs, onTimeout) {
149
186
  });
150
187
  }
151
188
 
152
- export { DEFAULT_REQUEST_TIMEOUT_MS, OnboardingBotChallengeError, OnboardingConflictError, OnboardingError, OnboardingNetworkError, OnboardingRateLimitError, OnboardingServerError, OnboardingSpecInvalidError, OnboardingSpecStaleError, OnboardingValidationError, fetchWithTimeout, withTimeout };
153
- //# sourceMappingURL=chunk-AW7W3HAC.js.map
154
- //# sourceMappingURL=chunk-AW7W3HAC.js.map
189
+ export { DEFAULT_REQUEST_TIMEOUT_MS, OnboardingBotChallengeError, OnboardingConflictError, OnboardingConsentIncompleteError, OnboardingConsentStaleError, OnboardingError, OnboardingNetworkError, OnboardingRateLimitError, OnboardingServerError, OnboardingSpecInvalidError, OnboardingSpecStaleError, OnboardingSubdomainTakenError, OnboardingValidationError, fetchWithTimeout, withTimeout };
190
+ //# sourceMappingURL=chunk-XJTJPBJB.js.map
191
+ //# sourceMappingURL=chunk-XJTJPBJB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/submit/errors.ts","../src/submit/http.ts"],"names":[],"mappings":"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","file":"chunk-XJTJPBJB.js","sourcesContent":["/**\n * Error taxonomy for the onboarding submit client.\n *\n * Maps HTTP status codes and server error bodies to typed errors:\n * 403 bot_challenge_failed → OnboardingBotChallengeError\n * 409 spec_stale (SUBMIT) → OnboardingSpecStaleError (submit endpoint only,\n * body { error:true, code:\"spec_stale\",\n * currentSpecVersion })\n * 409 (other / LEADS) → OnboardingConflictError (generic conflict)\n * 429 → OnboardingRateLimitError\n * 5xx → OnboardingServerError (retryable)\n * network / timeout → OnboardingNetworkError\n */\n\nexport type OnboardingErrorKind =\n | \"bot_challenge_failed\"\n | \"spec_stale\"\n | \"spec_invalid\"\n | \"consent_stale\"\n | \"consent_incomplete\"\n | \"subdomain_taken\"\n | \"conflict\"\n | \"rate_limited\"\n | \"server_error\"\n | \"network_error\"\n | \"validation_error\";\n\nexport class OnboardingError extends Error {\n public readonly kind: OnboardingErrorKind;\n public readonly statusCode: number | null;\n\n constructor(\n kind: OnboardingErrorKind,\n message: string,\n statusCode: number | null = null,\n ) {\n super(message);\n this.name = \"OnboardingError\";\n this.kind = kind;\n this.statusCode = statusCode;\n }\n}\n\nexport class OnboardingBotChallengeError extends OnboardingError {\n constructor(message?: string) {\n super(\n \"bot_challenge_failed\",\n message ??\n \"Bot challenge verification failed (403 bot_challenge_failed). Refresh the CAPTCHA and try again.\",\n 403,\n );\n this.name = \"OnboardingBotChallengeError\";\n }\n}\n\n/**\n * A 409 that is NOT spec_stale. On the /leads path there is no spec_stale\n * concept (the payload carries no specVersion); on /submit, a 409 that\n * lacks `code:\"spec_stale\"` also lands here. Distinct from\n * OnboardingSpecStaleError so headless consumers switching on `kind` get the\n * correct diagnosis (a generic conflict, e.g. a D538 same-key concurrent\n * race) rather than a \"form has been updated, refresh\" recovery message.\n */\nexport class OnboardingConflictError extends OnboardingError {\n constructor(statusCode = 409, message?: string) {\n super(\n \"conflict\",\n message ??\n `Request conflicted with the current server state (${statusCode}).`,\n statusCode,\n );\n this.name = \"OnboardingConflictError\";\n }\n}\n\nexport class OnboardingSubdomainTakenError extends OnboardingError {\n constructor() {\n super(\n \"subdomain_taken\",\n \"That workspace address is already taken. Choose another.\",\n 409,\n );\n this.name = \"OnboardingSubdomainTakenError\";\n }\n}\n\nexport class OnboardingRateLimitError extends OnboardingError {\n public readonly retryAfterSeconds: number | null;\n\n constructor(retryAfterSeconds: number | null = null) {\n super(\n \"rate_limited\",\n retryAfterSeconds !== null\n ? `Rate limited. Retry after ${retryAfterSeconds}s.`\n : \"Rate limited. Please wait before retrying.\",\n 429,\n );\n this.name = \"OnboardingRateLimitError\";\n this.retryAfterSeconds = retryAfterSeconds;\n }\n}\n\n/**\n * A /submit 409 whose body carries `code:\"spec_stale\"` (D540 reject-stale).\n * The PINNED body shape (confirmed against the crm intake) is exactly:\n * { error: true, code: \"spec_stale\", currentSpecVersion: string }\n * The version is read ONLY from `currentSpecVersion` — no field hedging.\n * Submit-endpoint only; /leads 409s are OnboardingConflictError.\n */\nexport class OnboardingSpecStaleError extends OnboardingError {\n public readonly currentSpecVersion: string;\n\n constructor(currentSpecVersion: string) {\n super(\n \"spec_stale\",\n `The onboarding form has been updated (new version: ${currentSpecVersion}). Please refresh and try again.`,\n 409,\n );\n this.name = \"OnboardingSpecStaleError\";\n this.currentSpecVersion = currentSpecVersion;\n }\n}\n\n/**\n * A /submit 409 whose body carries `code:\"consent_stale\"` (D543 §4 —\n * SIBLING to spec_stale). Raised when a required legal document's live\n * version/content_hash changed between spec-fetch and submit, so the echoed\n * consent no longer matches the live version.\n *\n * PINNED body shape (D543 §4, mirroring spec_stale):\n * { error: true, code: \"consent_stale\", staleDocuments?: [...] }\n * where each staleDocuments entry identifies a changed doc so the caller can\n * tell the user WHICH docs changed. The caller refetches the spec and\n * REQUIRES re-acceptance of the changed docs — the SDK NEVER auto-accepts a\n * changed doc (D543 §4). Submit-endpoint only.\n */\nexport interface StaleConsentDocument {\n /** The server registry id of the changed doc (D543 §3 legal_documents.id). */\n legalDocumentId?: string;\n /** doc_type of the changed doc, when the server includes it. */\n docType?: string;\n /** The new live version_no, when the server includes it. */\n currentVersionNo?: number;\n}\n\nexport class OnboardingConsentStaleError extends OnboardingError {\n /**\n * The documents the server reports as changed (may be empty when the\n * server does not enumerate them — the caller then refetches + diffs).\n */\n public readonly staleDocuments: StaleConsentDocument[];\n\n constructor(staleDocuments: StaleConsentDocument[] = []) {\n const which =\n staleDocuments.length > 0\n ? ` (${staleDocuments\n .map((d) => d.docType ?? d.legalDocumentId ?? \"a document\")\n .join(\", \")})`\n : \"\";\n super(\n \"consent_stale\",\n `One or more legal documents were updated${which}. Please review and re-accept the updated documents, then try again.`,\n 409,\n );\n this.name = \"OnboardingConsentStaleError\";\n this.staleDocuments = staleDocuments;\n }\n}\n\n/**\n * A /submit 400 whose body carries `code:\"consent_incomplete\"` (D543 §4).\n * The server's authoritative re-validation found a required live document\n * that was missing, not accepted, or whose evidence did not satisfy its\n * acceptance_level gate. Distinct from a generic 422 so headless consumers\n * switching on `kind` route the user back to the consent step specifically.\n */\nexport class OnboardingConsentIncompleteError extends OnboardingError {\n constructor(message?: string) {\n super(\n \"consent_incomplete\",\n message ??\n \"Consent is incomplete (400 consent_incomplete). One or more required documents were not accepted at their required level.\",\n 400,\n );\n this.name = \"OnboardingConsentIncompleteError\";\n }\n}\n\nexport class OnboardingServerError extends OnboardingError {\n constructor(statusCode: number, message?: string) {\n super(\n \"server_error\",\n message ?? `Server error (${statusCode}). The request will be retried.`,\n statusCode,\n );\n this.name = \"OnboardingServerError\";\n }\n}\n\nexport class OnboardingNetworkError extends OnboardingError {\n constructor(cause?: unknown) {\n super(\n \"network_error\",\n \"Network error — check your connection and try again.\",\n null,\n );\n this.name = \"OnboardingNetworkError\";\n if (cause instanceof Error) {\n this.cause = cause;\n }\n }\n}\n\nexport class OnboardingValidationError extends OnboardingError {\n constructor(message: string) {\n super(\"validation_error\", message, 422);\n this.name = \"OnboardingValidationError\";\n }\n}\n\n/**\n * The spec endpoint returned a body that does not conform to the LOCKED\n * D540 §1 response shape. This is a server-side contract violation — the\n * SDK refuses to render a form from a malformed spec (a half-parsed spec\n * could silently drop required fields).\n */\nexport class OnboardingSpecInvalidError extends OnboardingError {\n constructor(detail: string) {\n super(\n \"spec_invalid\",\n `The onboarding spec response does not match the D540 contract: ${detail}`,\n null,\n );\n this.name = \"OnboardingSpecInvalidError\";\n }\n}\n","/**\n * Shared HTTP helper: fetch bounded by a timeout via AbortController.\n *\n * Every SDK wire call (spec fetch, leads submit, signup submit) MUST be\n * bounded — the widgets flip to a disabled/\"Sending…\"/\"Loading…\" state\n * BEFORE the await, so an intake that accepts the connection and then stalls\n * would otherwise hang the UI forever. On timeout the fetch is aborted and a\n * typed OnboardingNetworkError is raised by the caller (which maps abort/\n * network failures uniformly).\n */\n\n/** Default per-request timeout. Generous enough for a slow-but-alive intake. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;\n\nexport interface TimeoutFetchOptions extends RequestInit {\n /** Per-request timeout in ms. Default DEFAULT_REQUEST_TIMEOUT_MS. */\n timeoutMs?: number;\n /** The fetch implementation to use (defaults to globalThis.fetch). */\n fetchImpl?: typeof globalThis.fetch;\n}\n\n/**\n * fetch() with a hard timeout. Aborts the request when the deadline elapses;\n * the resulting AbortError propagates to the caller (who wraps it as an\n * OnboardingNetworkError). A caller-supplied `signal` is respected too — if\n * either the caller aborts OR the timeout fires, the request is aborted.\n */\nexport async function fetchWithTimeout(\n url: string,\n options: TimeoutFetchOptions = {},\n): Promise<Response> {\n const {\n timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,\n fetchImpl = globalThis.fetch,\n signal: callerSignal,\n ...init\n } = options;\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new Error(`request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n // Forward a caller abort into our controller.\n if (callerSignal) {\n if (callerSignal.aborted) {\n clearTimeout(timer);\n controller.abort(callerSignal.reason);\n } else {\n callerSignal.addEventListener(\n \"abort\",\n () => controller.abort(callerSignal.reason),\n { once: true },\n );\n }\n }\n\n try {\n return await fetchImpl(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Run a promise with a timeout. On timeout, rejects with `onTimeout()`'s\n * error. Used to bound calls that don't take an AbortSignal (grecaptcha\n * .execute()). The underlying op is NOT cancelled (grecaptcha has no abort),\n * but the caller no longer waits on it — the widget re-enables.\n */\nexport function withTimeout<T>(\n op: Promise<T>,\n timeoutMs: number,\n onTimeout: () => Error,\n): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n reject(onTimeout());\n }, timeoutMs);\n op.then(\n (v) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(v);\n },\n (err) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n reject(err);\n },\n );\n });\n}\n"]}
@@ -0,0 +1,86 @@
1
+ import { a as OnboardingSpec } from './model-ESLSEbi0.js';
2
+
3
+ /**
4
+ * Config-fetch client for GET /api/v1/public/onboarding/spec (D540 §1/§3).
5
+ *
6
+ * - The onboarder is derived SERVER-side from the request Host (D536) —
7
+ * it is NOT sent by the client. The `onboarder` value here is purely the
8
+ * CLIENT-side cache key (D540 §3: "cache key (onboarder, specVersion)").
9
+ * - The spec response is validated against the LOCKED D540 shape before
10
+ * it is returned; a malformed spec throws OnboardingSpecInvalidError.
11
+ * - Cache semantics: every successful fetch is stored under
12
+ * (onboarder, specVersion) and as the onboarder's "latest". The versioned
13
+ * entries let the 409 spec_stale flow diff the previously-served version
14
+ * against the refetched one (see `diffSpecFields` + the signup widget's
15
+ * handleSpecStale, which reads the cached prior version before refetching).
16
+ */
17
+
18
+ /**
19
+ * In-memory spec cache keyed by (onboarder, specVersion), with a "latest"
20
+ * pointer per onboarder. Purely client-side; no TTL — the reject-stale
21
+ * policy (409 spec_stale on submit) is the authoritative staleness signal,
22
+ * and a refetch overwrites "latest".
23
+ */
24
+ declare class OnboardingSpecCache {
25
+ private readonly byVersion;
26
+ private readonly latest;
27
+ /**
28
+ * Collision-proof composite key. Uses JSON.stringify of the tuple so an
29
+ * onboarder or version containing the delimiter can never alias another
30
+ * entry — and, critically, contains NO control characters (an earlier
31
+ * NUL-separator made git treat this whole file as binary/undiffable).
32
+ */
33
+ private static versionKey;
34
+ get(onboarder: string, specVersion: string): OnboardingSpec | undefined;
35
+ getLatest(onboarder: string): OnboardingSpec | undefined;
36
+ set(onboarder: string, spec: OnboardingSpec): void;
37
+ clear(): void;
38
+ }
39
+ /** Module-level default cache (one per page). Injectable for tests/apps. */
40
+ declare const defaultSpecCache: OnboardingSpecCache;
41
+ /**
42
+ * Diff two spec versions' signup field sets — used by the 409 spec_stale
43
+ * flow to tell the user WHAT changed. Compares by field key across all
44
+ * signup steps.
45
+ */
46
+ interface SpecFieldDiff {
47
+ added: string[];
48
+ removed: string[];
49
+ changed: string[];
50
+ }
51
+ declare function diffSpecFields(prev: OnboardingSpec, next: OnboardingSpec): SpecFieldDiff;
52
+ interface SpecFetchOptions {
53
+ baseUrl: string;
54
+ /**
55
+ * Client-side cache key ONLY (never transmitted; trust is host-derived
56
+ * per D536). Use the onboarder slug/id your embed is configured with.
57
+ */
58
+ onboarder: string;
59
+ /** Custom fetch implementation (defaults to globalThis.fetch). */
60
+ fetch?: typeof globalThis.fetch;
61
+ /** Cache to read/write (defaults to the module-level cache). */
62
+ cache?: OnboardingSpecCache;
63
+ /**
64
+ * Request timeout in ms (default 15s). A stalled intake aborts with
65
+ * OnboardingNetworkError so the boot doesn't hang on "Loading signup form…".
66
+ */
67
+ timeoutMs?: number;
68
+ /**
69
+ * When true, return the cached "latest" for this onboarder if present
70
+ * WITHOUT a network round-trip. Default false (spec fetches are cheap
71
+ * and the server bumps specVersion on any change). The signup widget's
72
+ * boot passes preferCached:true so a remount reuses the loaded spec.
73
+ */
74
+ preferCached?: boolean;
75
+ }
76
+ /**
77
+ * Fetch (and validate, and cache) the onboarding spec.
78
+ *
79
+ * @throws {OnboardingSpecInvalidError} response does not match D540 §1
80
+ * @throws {OnboardingRateLimitError} on 429
81
+ * @throws {OnboardingServerError} on any other non-2xx status
82
+ * @throws {OnboardingNetworkError} on network failure OR timeout
83
+ */
84
+ declare function fetchOnboardingSpec(options: SpecFetchOptions): Promise<OnboardingSpec>;
85
+
86
+ export { OnboardingSpecCache as O, type SpecFetchOptions as S, type SpecFieldDiff as a, diffSpecFields as b, defaultSpecCache as d, fetchOnboardingSpec as f };
@@ -0,0 +1,59 @@
1
+ import { b as ConsentDocument } from '../model-mnjAEtKe.js';
2
+ export { A as ACCEPTANCE_LEVELS, a as AcceptanceLevel, C as ConsentDocType, c as ConsentPack, D as DOC_TYPES, d as acceptanceLevelSchema, e as consentDocKey, f as consentDocumentSchema, g as consentPackSchema } from '../model-mnjAEtKe.js';
3
+ export { C as ConsentController, a as ConsentDocState, b as ConsentEvidence, c as ConsentState, d as ConsentStateListener, e as ConsentSubmitDocument, S as SCROLL_END_THRESHOLD, f as createConsentController, i as initialGateSatisfied } from '../controller-BQb62D0N.js';
4
+ import { d as OnboardingError } from '../errors-vJkye4a0.js';
5
+ import 'zod/v3';
6
+
7
+ /**
8
+ * Headless document-content fetch helper (D543 §5).
9
+ *
10
+ * The consent controller is DOM-free; document RENDERING is the consumer's
11
+ * responsibility (TanStack model — you render the row, we own the state). For
12
+ * `scroll_then_accept` gating to work the doc must be rendered SAME-ORIGIN and
13
+ * inline (a cross-origin iframe cannot report scroll position), so consumers
14
+ * fetch the presigned artifact and render its bytes/text themselves.
15
+ *
16
+ * This helper does the fetch + classification so a consumer doesn't wrestle
17
+ * CORS/content-type detection. It returns the artifact's content-type plus the
18
+ * body as text (for HTML/plain) or a Blob (for PDF/binary). It NEVER renders
19
+ * or sanitizes — the consumer (or the default component) decides how to display
20
+ * it (e.g. sanitize+inject HTML, or PDF.js a blob, or degrade to open-in-tab).
21
+ *
22
+ * A short-TTL presigned GET may fail (expired URL, CORS, offline); the caller
23
+ * gets a typed OnboardingDocumentFetchError so a failed preview degrades
24
+ * gracefully (e.g. fall back to an "open in a new tab" affordance) without
25
+ * breaking the consent gate.
26
+ */
27
+
28
+ /** How the fetched artifact should be treated by a renderer. */
29
+ type ConsentDocumentKind = "html" | "text" | "pdf" | "binary";
30
+ interface FetchedConsentDocument {
31
+ /** The raw `Content-Type` header (lower-cased, params stripped). */
32
+ contentType: string;
33
+ /** Classification for the renderer. */
34
+ kind: ConsentDocumentKind;
35
+ /** Present for kind "html" | "text". */
36
+ text?: string;
37
+ /** Present for kind "pdf" | "binary". */
38
+ blob?: Blob;
39
+ }
40
+ interface FetchDocumentContentOptions {
41
+ fetch?: typeof globalThis.fetch;
42
+ /** Per-request timeout (default 15s) — a stalled CDN never hangs the UI. */
43
+ timeoutMs?: number;
44
+ }
45
+ /** A failed presigned artifact fetch (expired URL, CORS, offline, non-2xx). */
46
+ declare class OnboardingDocumentFetchError extends OnboardingError {
47
+ readonly url: string;
48
+ constructor(url: string, detail: string);
49
+ }
50
+ /**
51
+ * Fetch a consent document's presigned artifact and return its content-type +
52
+ * body (text for HTML/plain, Blob for PDF/binary).
53
+ *
54
+ * @throws {OnboardingDocumentFetchError} on network failure, timeout, CORS,
55
+ * or a non-2xx response — the caller degrades gracefully (open-in-tab).
56
+ */
57
+ declare function fetchDocumentContent(doc: Pick<ConsentDocument, "downloadUrl">, options?: FetchDocumentContentOptions): Promise<FetchedConsentDocument>;
58
+
59
+ export { ConsentDocument, type ConsentDocumentKind, type FetchDocumentContentOptions, type FetchedConsentDocument, OnboardingDocumentFetchError, fetchDocumentContent };
@@ -0,0 +1,7 @@
1
+ import '../chunk-FIRSA7EH.js';
2
+ export { SCROLL_END_THRESHOLD, createConsentController, initialGateSatisfied } from '../chunk-AZE3NPSY.js';
3
+ export { ACCEPTANCE_LEVELS, DOC_TYPES, acceptanceLevelSchema, consentDocKey, consentDocumentSchema, consentPackSchema } from '../chunk-U56GQITG.js';
4
+ export { OnboardingDocumentFetchError, fetchDocumentContent } from '../chunk-SEKNVHD3.js';
5
+ import '../chunk-XJTJPBJB.js';
6
+ //# sourceMappingURL=index.js.map
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,177 @@
1
+ import { b as ConsentDocument, a as AcceptanceLevel, c as ConsentPack } from './model-mnjAEtKe.js';
2
+
3
+ /**
4
+ * Headless ConsentPack controller (D543 §2/§5).
5
+ *
6
+ * Framework-agnostic consent state for the signup flow's consent stage.
7
+ * Exposes, per document: { doc, accepted, gateSatisfied, evidence } and
8
+ * enforces the per-document ACCEPTANCE-LEVEL GATE (D543 §2):
9
+ *
10
+ * - checkbox — accept is enable-able immediately (basic clickwrap).
11
+ * - scroll_then_accept — accept is DISABLED until the rendered doc is
12
+ * scrolled to the end (evidence.scrolled_to_end).
13
+ * - open_then_accept — accept is DISABLED until the doc is opened/viewed
14
+ * (evidence.opened).
15
+ *
16
+ * The gate is a UX guard (D543 §5: "Client evidence is UX-only; the server
17
+ * re-validates"). But the controller ENFORCES it locally so a UI cannot
18
+ * present an accept affordance the gate hasn't unlocked, and so a headless
19
+ * caller cannot assemble a payload that claims an unsatisfied gate.
20
+ *
21
+ * COMPLIANCE-CRITICAL (same discipline as the stepper's consent-fabrication
22
+ * fix): `accept(key)` is REFUSED when the gate is not yet satisfied, and
23
+ * `buildConsentPayload()` THROWS if ANY required doc is not accepted at its
24
+ * gate. The controller NEVER fabricates acceptance or evidence — it only
25
+ * records what the user genuinely did. `isComplete()` is true only when every
26
+ * REQUIRED doc is accepted with its gate satisfied.
27
+ *
28
+ * Same subscribe/mutate contract as the dynamic stepper so the default
29
+ * component layer renders it and tenants can build their own UI against the
30
+ * exported control surface (D543 §5 headless flexibility).
31
+ */
32
+
33
+ /**
34
+ * Evidence for a document's gate. Only the field relevant to the doc's
35
+ * acceptanceLevel is ever set true — a checkbox doc carries no evidence.
36
+ * Mirrors the D543 §4 submit `evidence:{scrolled_to_end?, opened?}` — these
37
+ * two keys stay snake_case (the ONLY snake bits in the consent wire shape).
38
+ */
39
+ interface ConsentEvidence {
40
+ scrolled_to_end?: boolean;
41
+ opened?: boolean;
42
+ }
43
+ /**
44
+ * Per-document view of the consent state — the framework-agnostic "row model"
45
+ * a consumer binds their own rendering to (TanStack-style). NO DOM here.
46
+ */
47
+ interface ConsentDocState {
48
+ /** The document descriptor (from the spec's consentPack). */
49
+ doc: ConsentDocument;
50
+ /** Stable client-side key (docType | custom:<customKey>). */
51
+ key: string;
52
+ /** The document's acceptance level, surfaced for convenience. */
53
+ acceptanceLevel: AcceptanceLevel;
54
+ /** Whether the document is required, surfaced for convenience. */
55
+ required: boolean;
56
+ /**
57
+ * Whether the acceptance-level GATE is satisfied — i.e. accept is
58
+ * enable-able. checkbox ⇒ always true; scroll ⇒ scrolled-to-end;
59
+ * open ⇒ opened.
60
+ */
61
+ gateSatisfied: boolean;
62
+ /** Whether the user has genuinely accepted (only possible past the gate). */
63
+ accepted: boolean;
64
+ /** The evidence collected so far (drives gateSatisfied for gated levels). */
65
+ evidence: ConsentEvidence;
66
+ /**
67
+ * Scroll progress in [0,1] for a `scroll_then_accept` doc (0 for other
68
+ * levels). A consumer's scroll handler feeds this via reportScrollProgress;
69
+ * reaching ~1 flips gateSatisfied. Purely a UX-progress read (e.g. a
70
+ * progress bar); the gate itself keys on scrolledToEnd evidence.
71
+ */
72
+ scrollProgress: number;
73
+ }
74
+ interface ConsentState {
75
+ /** All documents in spec order. */
76
+ documents: ConsentDocState[];
77
+ /** True iff every REQUIRED doc is accepted with its gate satisfied. */
78
+ complete: boolean;
79
+ }
80
+ type ConsentStateListener = (state: ConsentState) => void;
81
+ /**
82
+ * One element of the D543 §4 submit `consent.documents` array — CANONICAL
83
+ * camelCase (legalDocumentId, versionNo, accepted); `evidence` keys stay
84
+ * snake_case (scrolled_to_end / opened).
85
+ */
86
+ interface ConsentSubmitDocument {
87
+ legalDocumentId: string;
88
+ versionNo: number;
89
+ accepted: true;
90
+ evidence: ConsentEvidence;
91
+ }
92
+ interface ConsentController {
93
+ getState(): ConsentState;
94
+ /** True iff there are no documents to consent to (stage is skippable). */
95
+ isEmpty(): boolean;
96
+ /** True iff every required doc is accepted with its gate satisfied. */
97
+ isComplete(): boolean;
98
+ /**
99
+ * Alias of isComplete() — TanStack-style ergonomic read: "can the consumer
100
+ * submit the consent step?" (all required docs accepted at their gate).
101
+ */
102
+ canSubmit(): boolean;
103
+ /**
104
+ * Whether the given document's gate is satisfied (accept is enable-able).
105
+ * Unknown key ⇒ false.
106
+ */
107
+ isGateSatisfied(key: string): boolean;
108
+ /**
109
+ * Report scroll progress in [0,1] for a `scroll_then_accept` doc — the
110
+ * consumer's OWN scroll handler feeds this (e.g. on its same-origin scroll
111
+ * container). Ratio ≥ SCROLL_END_THRESHOLD marks scrolled-to-end (opens the
112
+ * gate); otherwise it just updates scrollProgress for UX. Clamped to [0,1].
113
+ * No-op for other levels and unknown keys.
114
+ */
115
+ reportScrollProgress(key: string, ratio: number): void;
116
+ /**
117
+ * Mark a `scroll_then_accept` doc as scrolled to the end (evidence). No-op
118
+ * for other levels (their gate isn't scroll-driven) and unknown keys.
119
+ * Equivalent to reportScrollProgress(key, 1).
120
+ */
121
+ markScrolledToEnd(key: string): void;
122
+ /**
123
+ * Mark an `open_then_accept` doc as opened/viewed (evidence). No-op for
124
+ * other levels and unknown keys.
125
+ */
126
+ markOpened(key: string): void;
127
+ /**
128
+ * Set a document's accepted flag. Accepting is REFUSED (accepted stays
129
+ * false) when the gate is not satisfied — a UI cannot record acceptance a
130
+ * user could not legitimately give. Unaccepting is always allowed.
131
+ */
132
+ setAccepted(key: string, accepted: boolean): void;
133
+ /**
134
+ * Build the D543 §4 submit array from the ACTUAL consent state.
135
+ *
136
+ * @throws {Error} if any REQUIRED doc is not accepted with its gate
137
+ * satisfied — never fabricates acceptance/evidence (compliance-critical).
138
+ * Optional docs that were accepted ARE included; optional docs left
139
+ * unaccepted are omitted.
140
+ */
141
+ buildConsentPayload(): ConsentSubmitDocument[];
142
+ /**
143
+ * Swap in a refetched consentPack after 409 consent_stale. For every doc
144
+ * whose (versionNo, contentHash) CHANGED, acceptance + evidence are
145
+ * RESET (re-acceptance required — NEVER auto-accept a changed doc, D543 §4
146
+ * / task §4). Unchanged docs keep their state. Returns the keys whose
147
+ * consent was reset so the UI can highlight what changed.
148
+ */
149
+ applyNewPack(pack: ConsentPack | undefined): string[];
150
+ subscribe(listener: ConsentStateListener): () => void;
151
+ }
152
+ /**
153
+ * Scroll ratio at/above which a `scroll_then_accept` doc counts as
154
+ * scrolled-to-end. Slightly below 1 so sub-pixel rounding on the consumer's
155
+ * scroll math (scrollTop+clientHeight vs scrollHeight) still opens the gate.
156
+ */
157
+ declare const SCROLL_END_THRESHOLD = 0.99;
158
+ /** checkbox docs have no gate; gated levels start unsatisfied. */
159
+ declare function initialGateSatisfied(level: AcceptanceLevel): boolean;
160
+ /**
161
+ * Create a headless consent controller for a spec's consentPack.
162
+ *
163
+ * @param pack the spec's `consentPack` (or undefined ⇒ an empty controller;
164
+ * the stage is skipped and `isComplete()` / `isEmpty()` are true).
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const consent = createConsentController(spec.consentPack);
169
+ * consent.subscribe(render);
170
+ * consent.markScrolledToEnd("tos"); // scroll_then_accept gate
171
+ * consent.setAccepted("tos", true); // now allowed
172
+ * const docs = consent.buildConsentPayload(); // throws if any required unmet
173
+ * ```
174
+ */
175
+ declare function createConsentController(pack: ConsentPack | undefined): ConsentController;
176
+
177
+ export { type ConsentController as C, SCROLL_END_THRESHOLD as S, type ConsentDocState as a, type ConsentEvidence as b, type ConsentState as c, type ConsentStateListener as d, type ConsentSubmitDocument as e, createConsentController as f, initialGateSatisfied as i };