@flamingo-stack/openframe-frontend-core 0.0.672 → 0.0.673-2406.7239.1

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 (164) hide show
  1. package/dist/{chunk-YNGAJGYB.cjs → chunk-6LWIJJJ6.cjs} +410 -6
  2. package/dist/{chunk-3PVOF3IB.js.map → chunk-6LWIJJJ6.cjs.map} +1 -1
  3. package/dist/{chunk-XLJNYTUW.cjs → chunk-6YSDCC3J.cjs} +56 -55
  4. package/dist/chunk-6YSDCC3J.cjs.map +1 -0
  5. package/dist/{chunk-GV7R2NTS.cjs → chunk-76DQOFZG.cjs} +23 -23
  6. package/dist/{chunk-GV7R2NTS.cjs.map → chunk-76DQOFZG.cjs.map} +1 -1
  7. package/dist/{chunk-UUOBCUHW.cjs → chunk-7ALCRUZ3.cjs} +3 -3
  8. package/dist/{chunk-UUOBCUHW.cjs.map → chunk-7ALCRUZ3.cjs.map} +1 -1
  9. package/dist/{chunk-PWPYCJ33.js → chunk-7RQZ2FTF.js} +2 -2
  10. package/dist/{chunk-XDTU23P4.js → chunk-ALSCESHG.js} +2 -2
  11. package/dist/{chunk-HVEX4BSC.cjs → chunk-AUQ4UITE.cjs} +60 -45
  12. package/dist/chunk-AUQ4UITE.cjs.map +1 -0
  13. package/dist/{chunk-CBCETEB4.cjs → chunk-E6B34U5Q.cjs} +6 -6
  14. package/dist/{chunk-CBCETEB4.cjs.map → chunk-E6B34U5Q.cjs.map} +1 -1
  15. package/dist/{chunk-H54F476P.cjs → chunk-FK3VSQ35.cjs} +3 -3
  16. package/dist/{chunk-H54F476P.cjs.map → chunk-FK3VSQ35.cjs.map} +1 -1
  17. package/dist/{chunk-HOEVEB7N.cjs → chunk-GCOVKIE7.cjs} +40 -29
  18. package/dist/chunk-GCOVKIE7.cjs.map +1 -0
  19. package/dist/{chunk-42OVTTOM.js → chunk-GIYAUKCY.js} +18 -3
  20. package/dist/{chunk-42OVTTOM.js.map → chunk-GIYAUKCY.js.map} +1 -1
  21. package/dist/{chunk-TQPKNG2E.js → chunk-INVZNRWB.js} +3 -3
  22. package/dist/{chunk-ZRIVCLPR.js → chunk-JD5TKRCU.js} +1 -1
  23. package/dist/chunk-JD5TKRCU.js.map +1 -0
  24. package/dist/{chunk-IBGHJLNT.js → chunk-JF4ZQZZJ.js} +2 -2
  25. package/dist/{chunk-SNWAN52K.cjs → chunk-JJDPSYBV.cjs} +3 -3
  26. package/dist/{chunk-SNWAN52K.cjs.map → chunk-JJDPSYBV.cjs.map} +1 -1
  27. package/dist/{chunk-D55IENZJ.cjs → chunk-LCMT2GZK.cjs} +1 -1
  28. package/dist/chunk-LCMT2GZK.cjs.map +1 -0
  29. package/dist/{chunk-AL4I5HHB.js → chunk-MMDGSGVE.js} +4 -4
  30. package/dist/{chunk-O5PYXQ6H.js → chunk-N3Q4FMMB.js} +3 -3
  31. package/dist/{chunk-RD3I5PBB.cjs → chunk-NUVRMZW5.cjs} +15 -15
  32. package/dist/{chunk-RD3I5PBB.cjs.map → chunk-NUVRMZW5.cjs.map} +1 -1
  33. package/dist/{chunk-T3KXCOB2.js → chunk-OIE6GKS3.js} +2 -2
  34. package/dist/{chunk-TFSGYKWG.js → chunk-OZL6FSB2.js} +28 -17
  35. package/dist/chunk-OZL6FSB2.js.map +1 -0
  36. package/dist/{chunk-NETIHUEY.cjs → chunk-P2A3KAYW.cjs} +10 -10
  37. package/dist/{chunk-NETIHUEY.cjs.map → chunk-P2A3KAYW.cjs.map} +1 -1
  38. package/dist/{chunk-6S2KLT7O.js → chunk-Q4KRZOQR.js} +4 -4
  39. package/dist/{chunk-BODMICIH.cjs → chunk-S7W7MTBM.cjs} +40 -40
  40. package/dist/{chunk-BODMICIH.cjs.map → chunk-S7W7MTBM.cjs.map} +1 -1
  41. package/dist/{chunk-3PVOF3IB.js → chunk-SGK4Q2W2.js} +407 -3
  42. package/dist/chunk-SGK4Q2W2.js.map +1 -0
  43. package/dist/{chunk-VZRVQEMT.cjs → chunk-SYTDEWZJ.cjs} +7 -7
  44. package/dist/{chunk-VZRVQEMT.cjs.map → chunk-SYTDEWZJ.cjs.map} +1 -1
  45. package/dist/{chunk-Z6E45TJM.js → chunk-TDCIGINK.js} +5 -5
  46. package/dist/{chunk-UOQRNZIB.cjs → chunk-TQATQ4CX.cjs} +17 -17
  47. package/dist/{chunk-UOQRNZIB.cjs.map → chunk-TQATQ4CX.cjs.map} +1 -1
  48. package/dist/{chunk-PSL2WBEO.cjs → chunk-TT6O7BUZ.cjs} +33 -33
  49. package/dist/{chunk-PSL2WBEO.cjs.map → chunk-TT6O7BUZ.cjs.map} +1 -1
  50. package/dist/{chunk-JREG4T6K.cjs → chunk-U7BJC4WF.cjs} +3 -3
  51. package/dist/{chunk-JREG4T6K.cjs.map → chunk-U7BJC4WF.cjs.map} +1 -1
  52. package/dist/{chunk-EKABOXNB.js → chunk-UIKMPCHW.js} +2 -2
  53. package/dist/{chunk-7KM34VWL.js → chunk-XMJT3QZ2.js} +3 -3
  54. package/dist/{chunk-4AZWVS7F.js → chunk-XSOL5ISO.js} +7 -6
  55. package/dist/{chunk-4AZWVS7F.js.map → chunk-XSOL5ISO.js.map} +1 -1
  56. package/dist/{chunk-KSBDARII.js → chunk-Y2ASYQ3P.js} +5 -5
  57. package/dist/components/case-studies/index.cjs +17 -14
  58. package/dist/components/case-studies/index.cjs.map +1 -1
  59. package/dist/components/case-studies/index.js +8 -5
  60. package/dist/components/case-studies/index.js.map +1 -1
  61. package/dist/components/case-studies/share-experience-section.d.ts.map +1 -1
  62. package/dist/components/contact/contact-form.d.ts +7 -1
  63. package/dist/components/contact/contact-form.d.ts.map +1 -1
  64. package/dist/components/contact/index.cjs +5 -5
  65. package/dist/components/contact/index.js +4 -4
  66. package/dist/components/docs/index.cjs +8 -8
  67. package/dist/components/docs/index.js +7 -7
  68. package/dist/components/embeds/index.cjs +5 -5
  69. package/dist/components/embeds/index.js +4 -4
  70. package/dist/components/faq/index.cjs +5 -5
  71. package/dist/components/faq/index.js +4 -4
  72. package/dist/components/features/index.cjs +4 -4
  73. package/dist/components/features/index.js +3 -3
  74. package/dist/components/features/waitlist-form.d.ts +7 -2
  75. package/dist/components/features/waitlist-form.d.ts.map +1 -1
  76. package/dist/components/help-center-pages/index.cjs +54 -52
  77. package/dist/components/help-center-pages/index.cjs.map +1 -1
  78. package/dist/components/help-center-pages/index.js +17 -15
  79. package/dist/components/help-center-pages/index.js.map +1 -1
  80. package/dist/components/help-center-pages/trust-center-sections.d.ts.map +1 -1
  81. package/dist/components/index.cjs +35 -35
  82. package/dist/components/index.js +14 -14
  83. package/dist/components/meeting-scheduler/booking-form.d.ts +7 -1
  84. package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
  85. package/dist/components/meeting-scheduler/index.cjs +53 -31
  86. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  87. package/dist/components/meeting-scheduler/index.d.ts +5 -1
  88. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  89. package/dist/components/meeting-scheduler/index.js +31 -9
  90. package/dist/components/meeting-scheduler/index.js.map +1 -1
  91. package/dist/components/navigation/index.cjs +7 -7
  92. package/dist/components/navigation/index.js +6 -6
  93. package/dist/components/onboarding-guides/index.cjs +7 -7
  94. package/dist/components/onboarding-guides/index.js +6 -6
  95. package/dist/components/tickets/help-center-create-form.d.ts.map +1 -1
  96. package/dist/components/tickets/index.cjs +8 -8
  97. package/dist/components/tickets/index.js +7 -7
  98. package/dist/components/ui/index.cjs +4 -4
  99. package/dist/components/ui/index.js +3 -3
  100. package/dist/contexts/endpoints-runtime-context.d.ts +5 -0
  101. package/dist/contexts/endpoints-runtime-context.d.ts.map +1 -1
  102. package/dist/contexts/index.cjs +2 -2
  103. package/dist/contexts/index.js +1 -1
  104. package/dist/hooks/index.cjs +9 -3
  105. package/dist/hooks/index.cjs.map +1 -1
  106. package/dist/hooks/index.d.ts +2 -0
  107. package/dist/hooks/index.d.ts.map +1 -1
  108. package/dist/hooks/index.js +8 -2
  109. package/dist/hooks/use-form-rescue.d.ts +43 -0
  110. package/dist/hooks/use-form-rescue.d.ts.map +1 -0
  111. package/dist/hooks/use-rescued-form.d.ts +28 -0
  112. package/dist/hooks/use-rescued-form.d.ts.map +1 -0
  113. package/dist/index.cjs +60 -8
  114. package/dist/index.cjs.map +1 -1
  115. package/dist/index.js +59 -7
  116. package/dist/index.js.map +1 -1
  117. package/dist/utils/form-rescue.cjs +142 -0
  118. package/dist/utils/form-rescue.cjs.map +1 -0
  119. package/dist/utils/form-rescue.d.ts +133 -0
  120. package/dist/utils/form-rescue.d.ts.map +1 -0
  121. package/dist/utils/form-rescue.js +118 -0
  122. package/dist/utils/form-rescue.js.map +1 -0
  123. package/dist/utils/index.cjs +138 -0
  124. package/dist/utils/index.cjs.map +1 -1
  125. package/dist/utils/index.d.ts +1 -0
  126. package/dist/utils/index.d.ts.map +1 -1
  127. package/dist/utils/index.js +116 -1
  128. package/dist/utils/index.js.map +1 -1
  129. package/package.json +7 -1
  130. package/src/components/case-studies/share-experience-section.tsx +2 -0
  131. package/src/components/contact/contact-form.tsx +33 -12
  132. package/src/components/features/waitlist-form.tsx +27 -2
  133. package/src/components/help-center-pages/trust-center-sections.tsx +2 -0
  134. package/src/components/meeting-scheduler/booking-form.tsx +30 -2
  135. package/src/components/meeting-scheduler/index.tsx +10 -1
  136. package/src/components/tickets/help-center-create-form.tsx +2 -0
  137. package/src/contexts/endpoints-runtime-context.tsx +5 -0
  138. package/src/hooks/__tests__/use-form-rescue.test.tsx +189 -0
  139. package/src/hooks/__tests__/use-rescued-form.test.tsx +91 -0
  140. package/src/hooks/index.ts +4 -0
  141. package/src/hooks/use-form-rescue.ts +330 -0
  142. package/src/hooks/use-rescued-form.ts +71 -0
  143. package/src/utils/__tests__/form-rescue.test.ts +124 -0
  144. package/src/utils/form-rescue.ts +237 -0
  145. package/src/utils/index.ts +5 -0
  146. package/dist/chunk-D55IENZJ.cjs.map +0 -1
  147. package/dist/chunk-HOEVEB7N.cjs.map +0 -1
  148. package/dist/chunk-HVEX4BSC.cjs.map +0 -1
  149. package/dist/chunk-TFSGYKWG.js.map +0 -1
  150. package/dist/chunk-XLJNYTUW.cjs.map +0 -1
  151. package/dist/chunk-YNGAJGYB.cjs.map +0 -1
  152. package/dist/chunk-ZRIVCLPR.js.map +0 -1
  153. /package/dist/{chunk-PWPYCJ33.js.map → chunk-7RQZ2FTF.js.map} +0 -0
  154. /package/dist/{chunk-XDTU23P4.js.map → chunk-ALSCESHG.js.map} +0 -0
  155. /package/dist/{chunk-TQPKNG2E.js.map → chunk-INVZNRWB.js.map} +0 -0
  156. /package/dist/{chunk-IBGHJLNT.js.map → chunk-JF4ZQZZJ.js.map} +0 -0
  157. /package/dist/{chunk-AL4I5HHB.js.map → chunk-MMDGSGVE.js.map} +0 -0
  158. /package/dist/{chunk-O5PYXQ6H.js.map → chunk-N3Q4FMMB.js.map} +0 -0
  159. /package/dist/{chunk-T3KXCOB2.js.map → chunk-OIE6GKS3.js.map} +0 -0
  160. /package/dist/{chunk-6S2KLT7O.js.map → chunk-Q4KRZOQR.js.map} +0 -0
  161. /package/dist/{chunk-Z6E45TJM.js.map → chunk-TDCIGINK.js.map} +0 -0
  162. /package/dist/{chunk-EKABOXNB.js.map → chunk-UIKMPCHW.js.map} +0 -0
  163. /package/dist/{chunk-7KM34VWL.js.map → chunk-XMJT3QZ2.js.map} +0 -0
  164. /package/dist/{chunk-KSBDARII.js.map → chunk-Y2ASYQ3P.js.map} +0 -0
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ // src/utils/form-rescue.ts
4
+ var FORM_ID_PATTERN = /^[a-z][a-z0-9_]{1,47}$/;
5
+ var definitions = /* @__PURE__ */ new Map();
6
+ function defineRescueForm(definition) {
7
+ if (!FORM_ID_PATTERN.test(definition.id)) throw new Error(`[form-rescue] invalid form id "${definition.id}"`);
8
+ const existing = definitions.get(definition.id);
9
+ if (existing) {
10
+ if (existing.label !== definition.label) {
11
+ throw new Error(`[form-rescue] form id "${definition.id}" is already defined as "${existing.label}"`);
12
+ }
13
+ return existing;
14
+ }
15
+ const frozen = Object.freeze({ id: definition.id, label: definition.label });
16
+ definitions.set(frozen.id, frozen);
17
+ return frozen;
18
+ }
19
+ function getRescueForm(id) {
20
+ return typeof id === "string" ? definitions.get(id) ?? null : null;
21
+ }
22
+ function listRescueForms() {
23
+ return [...definitions.values()];
24
+ }
25
+ var RESCUE_FORMS = {
26
+ contact: defineRescueForm({ id: "contact", label: "Contact form" }),
27
+ caseStudyPitch: defineRescueForm({ id: "case_study_pitch", label: "Case study pitch" }),
28
+ dataRoomRequest: defineRescueForm({ id: "data_room_request", label: "Data room request" }),
29
+ trustCenterRequest: defineRescueForm({ id: "trust_center_request", label: "Trust Center document request" }),
30
+ meetingBooking: defineRescueForm({ id: "meeting_booking", label: "Meeting booking" }),
31
+ waitlist: defineRescueForm({ id: "waitlist", label: "Waitlist" })
32
+ };
33
+ var FORM_RESCUE_VALUE_FIELDS = [
34
+ "email",
35
+ "name",
36
+ "firstName",
37
+ "lastName",
38
+ "company",
39
+ "jobtitle",
40
+ "companySize",
41
+ "linkedin_url"
42
+ ];
43
+ var EXCLUDED_FIELD_PATTERN = /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;
44
+ function isExcludedRescueField(name) {
45
+ return EXCLUDED_FIELD_PATTERN.test(name);
46
+ }
47
+ var FORM_RESCUE_MAX_VALUE_CHARS = 200;
48
+ var FORM_RESCUE_DEBOUNCE_MS = 1500;
49
+ var FORM_RESCUE_MAX_FIELDS = 40;
50
+ var FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
51
+ var FORM_RESCUE_RESUME_FIELD = "form_resume_token";
52
+ var FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD];
53
+ var FORM_RESCUE_RESUME_PARAM = "resume";
54
+ var FORM_DRAFT_STATUSES = ["started", "rescuable", "rescued", "submitted"];
55
+ var FORM_RESCUE_SKIP_REASONS = {
56
+ finishedElsewhere: "finished_elsewhere",
57
+ alreadyRescued: "already_rescued",
58
+ internal: "internal_email"
59
+ };
60
+ var EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
61
+ function isRescueEmail(value) {
62
+ return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
63
+ }
64
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
65
+ function isFormAttemptId(value) {
66
+ return typeof value === "string" && UUID_PATTERN.test(value);
67
+ }
68
+ function isFilled(value) {
69
+ if (value == null) return false;
70
+ if (typeof value === "string") return value.trim().length > 0;
71
+ if (typeof value === "boolean") return value;
72
+ if (Array.isArray(value)) return value.length > 0;
73
+ if (typeof value === "object") return Object.values(value).some(isFilled);
74
+ return true;
75
+ }
76
+ function sanitizeRescueValues(values) {
77
+ const out = {};
78
+ if (!values || typeof values !== "object") return out;
79
+ for (const key of FORM_RESCUE_VALUE_FIELDS) {
80
+ if (isExcludedRescueField(key)) continue;
81
+ const raw = values[key];
82
+ if (typeof raw !== "string") continue;
83
+ const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
84
+ if (trimmed) out[key] = trimmed;
85
+ }
86
+ return out;
87
+ }
88
+ function filledRescueFields(values, fieldNames) {
89
+ return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
90
+ }
91
+ function rescueCompletionPct(filledCount, totalCount) {
92
+ if (totalCount <= 0) return 0;
93
+ return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
94
+ }
95
+ function sanitizeRescueFieldName(value) {
96
+ if (typeof value !== "string") return null;
97
+ const trimmed = value.trim();
98
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
99
+ }
100
+ var FORM_RESCUE_EVENTS = {
101
+ started: "form_started",
102
+ progress: "form_progress",
103
+ leadCaptured: "form_lead_captured",
104
+ abandoned: "form_abandoned",
105
+ submitted: "form_submitted",
106
+ rescued: "form_rescued",
107
+ resumed: "form_resumed"
108
+ };
109
+ function captureFormRescueEvent(event, properties) {
110
+ try {
111
+ if (typeof window === "undefined") return;
112
+ const ph = window.posthog;
113
+ if (typeof ph?.capture === "function") ph.capture(event, properties);
114
+ } catch {
115
+ }
116
+ }
117
+
118
+ exports.FORM_DRAFT_STATUSES = FORM_DRAFT_STATUSES;
119
+ exports.FORM_RESCUE_ATTEMPT_FIELD = FORM_RESCUE_ATTEMPT_FIELD;
120
+ exports.FORM_RESCUE_DEBOUNCE_MS = FORM_RESCUE_DEBOUNCE_MS;
121
+ exports.FORM_RESCUE_EVENTS = FORM_RESCUE_EVENTS;
122
+ exports.FORM_RESCUE_MAX_FIELDS = FORM_RESCUE_MAX_FIELDS;
123
+ exports.FORM_RESCUE_MAX_VALUE_CHARS = FORM_RESCUE_MAX_VALUE_CHARS;
124
+ exports.FORM_RESCUE_RESUME_FIELD = FORM_RESCUE_RESUME_FIELD;
125
+ exports.FORM_RESCUE_RESUME_PARAM = FORM_RESCUE_RESUME_PARAM;
126
+ exports.FORM_RESCUE_SKIP_REASONS = FORM_RESCUE_SKIP_REASONS;
127
+ exports.FORM_RESCUE_SUBMIT_KEYS = FORM_RESCUE_SUBMIT_KEYS;
128
+ exports.FORM_RESCUE_VALUE_FIELDS = FORM_RESCUE_VALUE_FIELDS;
129
+ exports.RESCUE_FORMS = RESCUE_FORMS;
130
+ exports.captureFormRescueEvent = captureFormRescueEvent;
131
+ exports.defineRescueForm = defineRescueForm;
132
+ exports.filledRescueFields = filledRescueFields;
133
+ exports.getRescueForm = getRescueForm;
134
+ exports.isExcludedRescueField = isExcludedRescueField;
135
+ exports.isFormAttemptId = isFormAttemptId;
136
+ exports.isRescueEmail = isRescueEmail;
137
+ exports.listRescueForms = listRescueForms;
138
+ exports.rescueCompletionPct = rescueCompletionPct;
139
+ exports.sanitizeRescueFieldName = sanitizeRescueFieldName;
140
+ exports.sanitizeRescueValues = sanitizeRescueValues;
141
+ //# sourceMappingURL=form-rescue.cjs.map
142
+ //# sourceMappingURL=form-rescue.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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","file":"form-rescue.cjs","sourcesContent":["/**\n * Form rescue — the shared rules for saving a half-filled public form so the\n * team can follow up with a visitor who left before submitting.\n *\n * Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the\n * host's draft endpoint import the SAME allowlist, exclusions, caps and\n * completion math, so the server re-applies exactly the filter the client ran\n * and a rename here reaches both sides at once. Also exported through the\n * granular subpath `./utils/form-rescue` for server-only consumers.\n *\n * What a draft may carry:\n * - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),\n * trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;\n * - for every other field, only whether it is filled (never its value);\n * - nothing whose name looks like a credential or payment field\n * (`isExcludedRescueField`), whatever the allowlist says.\n *\n * Field values go to the host only, never to analytics: the PostHog events\n * (`captureFormRescueEvent`) carry ids, field names and percentages.\n */\n\n/**\n * A rescuable form, defined ONCE beside the form that uses it (the strategy a\n * form plugs in with). The browser sends `id`; the host validates it with\n * `getRescueForm` and shows `label` to the team. A new form is one\n * `defineRescueForm` call: no central list, no host change.\n */\nexport interface FormRescueDefinition {\n /** Stable wire id: lower snake case, stored on every draft of this form. */\n readonly id: string;\n /** What the team reads on the alert (\"Contact form\"). */\n readonly label: string;\n}\n\nconst FORM_ID_PATTERN = /^[a-z][a-z0-9_]{1,47}$/;\nconst definitions = new Map<string, FormRescueDefinition>();\n\n/**\n * Register a rescuable form. Idempotent for the same id and label (a module\n * evaluated twice registers once); a second, different definition of one id\n * throws, so two forms can never share a draft vocabulary.\n */\nexport function defineRescueForm(definition: FormRescueDefinition): FormRescueDefinition {\n if (!FORM_ID_PATTERN.test(definition.id)) throw new Error(`[form-rescue] invalid form id \"${definition.id}\"`);\n const existing = definitions.get(definition.id);\n if (existing) {\n if (existing.label !== definition.label) {\n throw new Error(`[form-rescue] form id \"${definition.id}\" is already defined as \"${existing.label}\"`);\n }\n return existing;\n }\n const frozen = Object.freeze({ id: definition.id, label: definition.label });\n definitions.set(frozen.id, frozen);\n return frozen;\n}\n\n/** The registered definition for a wire id, or null (unknown ids are refused by the host). */\nexport function getRescueForm(id: unknown): FormRescueDefinition | null {\n return typeof id === 'string' ? (definitions.get(id) ?? null) : null;\n}\n\n/** Every registered form, for a host listing them. */\nexport function listRescueForms(): FormRescueDefinition[] {\n return [...definitions.values()];\n}\n\n/** The lib's own public forms. A host defines its own forms the same way, beside them. */\nexport const RESCUE_FORMS = {\n contact: defineRescueForm({ id: 'contact', label: 'Contact form' }),\n caseStudyPitch: defineRescueForm({ id: 'case_study_pitch', label: 'Case study pitch' }),\n dataRoomRequest: defineRescueForm({ id: 'data_room_request', label: 'Data room request' }),\n trustCenterRequest: defineRescueForm({ id: 'trust_center_request', label: 'Trust Center document request' }),\n meetingBooking: defineRescueForm({ id: 'meeting_booking', label: 'Meeting booking' }),\n waitlist: defineRescueForm({ id: 'waitlist', label: 'Waitlist' }),\n} as const;\n\n/**\n * Fields whose VALUES a draft keeps: who the visitor is and where they work.\n * Covers both naming schemes in use (the contact form's `name` / `companySize`,\n * the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).\n * Free text (`message`) and phone numbers are reported as filled only.\n */\nexport const FORM_RESCUE_VALUE_FIELDS = [\n 'email',\n 'name',\n 'firstName',\n 'lastName',\n 'company',\n 'jobtitle',\n 'companySize',\n 'linkedin_url',\n] as const;\n\n/** Never send or store these, whatever the allowlist says. */\nconst EXCLUDED_FIELD_PATTERN =\n /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;\n\nexport function isExcludedRescueField(name: string): boolean {\n return EXCLUDED_FIELD_PATTERN.test(name);\n}\n\n/** Longest value a draft keeps per field. */\nexport const FORM_RESCUE_MAX_VALUE_CHARS = 200;\n/** Save this long after the visitor stops typing. */\nexport const FORM_RESCUE_DEBOUNCE_MS = 1500;\n/** Most fields a draft reports (filled names), a bound on the payload. */\nexport const FORM_RESCUE_MAX_FIELDS = 40;\n\n/** Body keys a form's SUBMIT carries so the host can close the matching draft. */\nexport const FORM_RESCUE_ATTEMPT_FIELD = 'form_attempt_id';\nexport const FORM_RESCUE_RESUME_FIELD = 'form_resume_token';\n/** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */\nexport const FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD] as const;\n\n/** Query parameter a resume link carries. */\nexport const FORM_RESCUE_RESUME_PARAM = 'resume';\n\n/** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */\nexport const FORM_DRAFT_STATUSES = ['started', 'rescuable', 'rescued', 'submitted'] as const;\nexport type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];\n\n/** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */\nexport const FORM_RESCUE_SKIP_REASONS = {\n finishedElsewhere: 'finished_elsewhere',\n alreadyRescued: 'already_rescued',\n internal: 'internal_email',\n} as const;\nexport type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];\n\n/** Same shape the host's `contact_submissions.email` check accepts. */\nconst EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/;\n\nexport function isRescueEmail(value: unknown): value is string {\n return typeof value === 'string' && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function isFormAttemptId(value: unknown): value is string {\n return typeof value === 'string' && UUID_PATTERN.test(value);\n}\n\nfunction isFilled(value: unknown): boolean {\n if (value == null) return false;\n if (typeof value === 'string') return value.trim().length > 0;\n if (typeof value === 'boolean') return value;\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === 'object') return Object.values(value as Record<string, unknown>).some(isFilled);\n return true;\n}\n\n/**\n * Keep only allowlisted, non-excluded string values, trimmed and capped.\n * The ONE filter, run by the browser before sending and by the host before storing.\n */\nexport function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!values || typeof values !== 'object') return out;\n for (const key of FORM_RESCUE_VALUE_FIELDS) {\n if (isExcludedRescueField(key)) continue;\n const raw = values[key];\n if (typeof raw !== 'string') continue;\n const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);\n if (trimmed) out[key] = trimmed;\n }\n return out;\n}\n\n/** Names of the given fields that hold something, credentials never included. */\nexport function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[] {\n return fieldNames\n .filter(name => !isExcludedRescueField(name) && isFilled(values[name]))\n .slice(0, FORM_RESCUE_MAX_FIELDS);\n}\n\nexport function rescueCompletionPct(filledCount: number, totalCount: number): number {\n if (totalCount <= 0) return 0;\n return Math.max(0, Math.min(100, Math.round((filledCount / totalCount) * 100)));\n}\n\n/** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */\nexport function sanitizeRescueFieldName(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;\n}\n\n/** A form's progress at one moment: allowlisted values, filled field names, position. */\nexport interface FormDraftProgress {\n values: Record<string, string>;\n fields_filled: string[];\n last_field: string | null;\n completion_pct: number;\n}\n\n/** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */\nexport interface FormDraftSaveRequest extends FormDraftProgress {\n /** The attempt's idempotency key: every save of one attempt updates one draft. */\n attempt_id: string;\n /** A `FormRescueDefinition.id`. */\n form_id: string;\n source_path: string;\n utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;\n}\n\n/** What the host answers a resume link with: the allowlisted values, nothing internal. */\nexport interface FormDraftResumeResponse {\n form_id: string;\n values: Record<string, string>;\n}\n\n/** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */\nexport const FORM_RESCUE_EVENTS = {\n started: 'form_started',\n progress: 'form_progress',\n leadCaptured: 'form_lead_captured',\n abandoned: 'form_abandoned',\n submitted: 'form_submitted',\n rescued: 'form_rescued',\n resumed: 'form_resumed',\n} as const;\n\ntype AnalyticsValue = string | number | boolean | string[] | null;\n\n/**\n * Send one form event to PostHog when the page loaded it (GTM exposes\n * `window.posthog`); a no-op otherwise. Never throws.\n */\nexport function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void {\n try {\n if (typeof window === 'undefined') return;\n const ph = (window as unknown as { posthog?: { capture?: (e: string, p: object) => void } }).posthog;\n if (typeof ph?.capture === 'function') ph.capture(event, properties);\n } catch {\n // Analytics never breaks a form.\n }\n}\n"]}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Form rescue — the shared rules for saving a half-filled public form so the
3
+ * team can follow up with a visitor who left before submitting.
4
+ *
5
+ * Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the
6
+ * host's draft endpoint import the SAME allowlist, exclusions, caps and
7
+ * completion math, so the server re-applies exactly the filter the client ran
8
+ * and a rename here reaches both sides at once. Also exported through the
9
+ * granular subpath `./utils/form-rescue` for server-only consumers.
10
+ *
11
+ * What a draft may carry:
12
+ * - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),
13
+ * trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;
14
+ * - for every other field, only whether it is filled (never its value);
15
+ * - nothing whose name looks like a credential or payment field
16
+ * (`isExcludedRescueField`), whatever the allowlist says.
17
+ *
18
+ * Field values go to the host only, never to analytics: the PostHog events
19
+ * (`captureFormRescueEvent`) carry ids, field names and percentages.
20
+ */
21
+ /**
22
+ * A rescuable form, defined ONCE beside the form that uses it (the strategy a
23
+ * form plugs in with). The browser sends `id`; the host validates it with
24
+ * `getRescueForm` and shows `label` to the team. A new form is one
25
+ * `defineRescueForm` call: no central list, no host change.
26
+ */
27
+ export interface FormRescueDefinition {
28
+ /** Stable wire id: lower snake case, stored on every draft of this form. */
29
+ readonly id: string;
30
+ /** What the team reads on the alert ("Contact form"). */
31
+ readonly label: string;
32
+ }
33
+ /**
34
+ * Register a rescuable form. Idempotent for the same id and label (a module
35
+ * evaluated twice registers once); a second, different definition of one id
36
+ * throws, so two forms can never share a draft vocabulary.
37
+ */
38
+ export declare function defineRescueForm(definition: FormRescueDefinition): FormRescueDefinition;
39
+ /** The registered definition for a wire id, or null (unknown ids are refused by the host). */
40
+ export declare function getRescueForm(id: unknown): FormRescueDefinition | null;
41
+ /** Every registered form, for a host listing them. */
42
+ export declare function listRescueForms(): FormRescueDefinition[];
43
+ /** The lib's own public forms. A host defines its own forms the same way, beside them. */
44
+ export declare const RESCUE_FORMS: {
45
+ readonly contact: FormRescueDefinition;
46
+ readonly caseStudyPitch: FormRescueDefinition;
47
+ readonly dataRoomRequest: FormRescueDefinition;
48
+ readonly trustCenterRequest: FormRescueDefinition;
49
+ readonly meetingBooking: FormRescueDefinition;
50
+ readonly waitlist: FormRescueDefinition;
51
+ };
52
+ /**
53
+ * Fields whose VALUES a draft keeps: who the visitor is and where they work.
54
+ * Covers both naming schemes in use (the contact form's `name` / `companySize`,
55
+ * the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).
56
+ * Free text (`message`) and phone numbers are reported as filled only.
57
+ */
58
+ export declare const FORM_RESCUE_VALUE_FIELDS: readonly ["email", "name", "firstName", "lastName", "company", "jobtitle", "companySize", "linkedin_url"];
59
+ export declare function isExcludedRescueField(name: string): boolean;
60
+ /** Longest value a draft keeps per field. */
61
+ export declare const FORM_RESCUE_MAX_VALUE_CHARS = 200;
62
+ /** Save this long after the visitor stops typing. */
63
+ export declare const FORM_RESCUE_DEBOUNCE_MS = 1500;
64
+ /** Most fields a draft reports (filled names), a bound on the payload. */
65
+ export declare const FORM_RESCUE_MAX_FIELDS = 40;
66
+ /** Body keys a form's SUBMIT carries so the host can close the matching draft. */
67
+ export declare const FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
68
+ export declare const FORM_RESCUE_RESUME_FIELD = "form_resume_token";
69
+ /** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */
70
+ export declare const FORM_RESCUE_SUBMIT_KEYS: readonly ["form_attempt_id", "form_resume_token"];
71
+ /** Query parameter a resume link carries. */
72
+ export declare const FORM_RESCUE_RESUME_PARAM = "resume";
73
+ /** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */
74
+ export declare const FORM_DRAFT_STATUSES: readonly ["started", "rescuable", "rescued", "submitted"];
75
+ export type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];
76
+ /** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */
77
+ export declare const FORM_RESCUE_SKIP_REASONS: {
78
+ readonly finishedElsewhere: "finished_elsewhere";
79
+ readonly alreadyRescued: "already_rescued";
80
+ readonly internal: "internal_email";
81
+ };
82
+ export type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];
83
+ export declare function isRescueEmail(value: unknown): value is string;
84
+ export declare function isFormAttemptId(value: unknown): value is string;
85
+ /**
86
+ * Keep only allowlisted, non-excluded string values, trimmed and capped.
87
+ * The ONE filter, run by the browser before sending and by the host before storing.
88
+ */
89
+ export declare function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string>;
90
+ /** Names of the given fields that hold something, credentials never included. */
91
+ export declare function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[];
92
+ export declare function rescueCompletionPct(filledCount: number, totalCount: number): number;
93
+ /** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */
94
+ export declare function sanitizeRescueFieldName(value: unknown): string | null;
95
+ /** A form's progress at one moment: allowlisted values, filled field names, position. */
96
+ export interface FormDraftProgress {
97
+ values: Record<string, string>;
98
+ fields_filled: string[];
99
+ last_field: string | null;
100
+ completion_pct: number;
101
+ }
102
+ /** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */
103
+ export interface FormDraftSaveRequest extends FormDraftProgress {
104
+ /** The attempt's idempotency key: every save of one attempt updates one draft. */
105
+ attempt_id: string;
106
+ /** A `FormRescueDefinition.id`. */
107
+ form_id: string;
108
+ source_path: string;
109
+ utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;
110
+ }
111
+ /** What the host answers a resume link with: the allowlisted values, nothing internal. */
112
+ export interface FormDraftResumeResponse {
113
+ form_id: string;
114
+ values: Record<string, string>;
115
+ }
116
+ /** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */
117
+ export declare const FORM_RESCUE_EVENTS: {
118
+ readonly started: "form_started";
119
+ readonly progress: "form_progress";
120
+ readonly leadCaptured: "form_lead_captured";
121
+ readonly abandoned: "form_abandoned";
122
+ readonly submitted: "form_submitted";
123
+ readonly rescued: "form_rescued";
124
+ readonly resumed: "form_resumed";
125
+ };
126
+ type AnalyticsValue = string | number | boolean | string[] | null;
127
+ /**
128
+ * Send one form event to PostHog when the page loaded it (GTM exposes
129
+ * `window.posthog`); a no-op otherwise. Never throws.
130
+ */
131
+ export declare function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void;
132
+ export {};
133
+ //# sourceMappingURL=form-rescue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-rescue.d.ts","sourceRoot":"","sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,118 @@
1
+ // src/utils/form-rescue.ts
2
+ var FORM_ID_PATTERN = /^[a-z][a-z0-9_]{1,47}$/;
3
+ var definitions = /* @__PURE__ */ new Map();
4
+ function defineRescueForm(definition) {
5
+ if (!FORM_ID_PATTERN.test(definition.id)) throw new Error(`[form-rescue] invalid form id "${definition.id}"`);
6
+ const existing = definitions.get(definition.id);
7
+ if (existing) {
8
+ if (existing.label !== definition.label) {
9
+ throw new Error(`[form-rescue] form id "${definition.id}" is already defined as "${existing.label}"`);
10
+ }
11
+ return existing;
12
+ }
13
+ const frozen = Object.freeze({ id: definition.id, label: definition.label });
14
+ definitions.set(frozen.id, frozen);
15
+ return frozen;
16
+ }
17
+ function getRescueForm(id) {
18
+ return typeof id === "string" ? definitions.get(id) ?? null : null;
19
+ }
20
+ function listRescueForms() {
21
+ return [...definitions.values()];
22
+ }
23
+ var RESCUE_FORMS = {
24
+ contact: defineRescueForm({ id: "contact", label: "Contact form" }),
25
+ caseStudyPitch: defineRescueForm({ id: "case_study_pitch", label: "Case study pitch" }),
26
+ dataRoomRequest: defineRescueForm({ id: "data_room_request", label: "Data room request" }),
27
+ trustCenterRequest: defineRescueForm({ id: "trust_center_request", label: "Trust Center document request" }),
28
+ meetingBooking: defineRescueForm({ id: "meeting_booking", label: "Meeting booking" }),
29
+ waitlist: defineRescueForm({ id: "waitlist", label: "Waitlist" })
30
+ };
31
+ var FORM_RESCUE_VALUE_FIELDS = [
32
+ "email",
33
+ "name",
34
+ "firstName",
35
+ "lastName",
36
+ "company",
37
+ "jobtitle",
38
+ "companySize",
39
+ "linkedin_url"
40
+ ];
41
+ var EXCLUDED_FIELD_PATTERN = /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;
42
+ function isExcludedRescueField(name) {
43
+ return EXCLUDED_FIELD_PATTERN.test(name);
44
+ }
45
+ var FORM_RESCUE_MAX_VALUE_CHARS = 200;
46
+ var FORM_RESCUE_DEBOUNCE_MS = 1500;
47
+ var FORM_RESCUE_MAX_FIELDS = 40;
48
+ var FORM_RESCUE_ATTEMPT_FIELD = "form_attempt_id";
49
+ var FORM_RESCUE_RESUME_FIELD = "form_resume_token";
50
+ var FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD];
51
+ var FORM_RESCUE_RESUME_PARAM = "resume";
52
+ var FORM_DRAFT_STATUSES = ["started", "rescuable", "rescued", "submitted"];
53
+ var FORM_RESCUE_SKIP_REASONS = {
54
+ finishedElsewhere: "finished_elsewhere",
55
+ alreadyRescued: "already_rescued",
56
+ internal: "internal_email"
57
+ };
58
+ var EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
59
+ function isRescueEmail(value) {
60
+ return typeof value === "string" && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());
61
+ }
62
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
63
+ function isFormAttemptId(value) {
64
+ return typeof value === "string" && UUID_PATTERN.test(value);
65
+ }
66
+ function isFilled(value) {
67
+ if (value == null) return false;
68
+ if (typeof value === "string") return value.trim().length > 0;
69
+ if (typeof value === "boolean") return value;
70
+ if (Array.isArray(value)) return value.length > 0;
71
+ if (typeof value === "object") return Object.values(value).some(isFilled);
72
+ return true;
73
+ }
74
+ function sanitizeRescueValues(values) {
75
+ const out = {};
76
+ if (!values || typeof values !== "object") return out;
77
+ for (const key of FORM_RESCUE_VALUE_FIELDS) {
78
+ if (isExcludedRescueField(key)) continue;
79
+ const raw = values[key];
80
+ if (typeof raw !== "string") continue;
81
+ const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);
82
+ if (trimmed) out[key] = trimmed;
83
+ }
84
+ return out;
85
+ }
86
+ function filledRescueFields(values, fieldNames) {
87
+ return fieldNames.filter((name) => !isExcludedRescueField(name) && isFilled(values[name])).slice(0, FORM_RESCUE_MAX_FIELDS);
88
+ }
89
+ function rescueCompletionPct(filledCount, totalCount) {
90
+ if (totalCount <= 0) return 0;
91
+ return Math.max(0, Math.min(100, Math.round(filledCount / totalCount * 100)));
92
+ }
93
+ function sanitizeRescueFieldName(value) {
94
+ if (typeof value !== "string") return null;
95
+ const trimmed = value.trim();
96
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;
97
+ }
98
+ var FORM_RESCUE_EVENTS = {
99
+ started: "form_started",
100
+ progress: "form_progress",
101
+ leadCaptured: "form_lead_captured",
102
+ abandoned: "form_abandoned",
103
+ submitted: "form_submitted",
104
+ rescued: "form_rescued",
105
+ resumed: "form_resumed"
106
+ };
107
+ function captureFormRescueEvent(event, properties) {
108
+ try {
109
+ if (typeof window === "undefined") return;
110
+ const ph = window.posthog;
111
+ if (typeof ph?.capture === "function") ph.capture(event, properties);
112
+ } catch {
113
+ }
114
+ }
115
+
116
+ export { FORM_DRAFT_STATUSES, FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_DEBOUNCE_MS, FORM_RESCUE_EVENTS, FORM_RESCUE_MAX_FIELDS, FORM_RESCUE_MAX_VALUE_CHARS, FORM_RESCUE_RESUME_FIELD, FORM_RESCUE_RESUME_PARAM, FORM_RESCUE_SKIP_REASONS, FORM_RESCUE_SUBMIT_KEYS, FORM_RESCUE_VALUE_FIELDS, RESCUE_FORMS, captureFormRescueEvent, defineRescueForm, filledRescueFields, getRescueForm, isExcludedRescueField, isFormAttemptId, isRescueEmail, listRescueForms, rescueCompletionPct, sanitizeRescueFieldName, sanitizeRescueValues };
117
+ //# sourceMappingURL=form-rescue.js.map
118
+ //# sourceMappingURL=form-rescue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/form-rescue.ts"],"names":[],"mappings":"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","file":"form-rescue.js","sourcesContent":["/**\n * Form rescue — the shared rules for saving a half-filled public form so the\n * team can follow up with a visitor who left before submitting.\n *\n * Pure and server-safe: the browser hook (`hooks/use-form-rescue`) and the\n * host's draft endpoint import the SAME allowlist, exclusions, caps and\n * completion math, so the server re-applies exactly the filter the client ran\n * and a rename here reaches both sides at once. Also exported through the\n * granular subpath `./utils/form-rescue` for server-only consumers.\n *\n * What a draft may carry:\n * - values of ALLOWLISTED contact fields only (`FORM_RESCUE_VALUE_FIELDS`),\n * trimmed and capped at `FORM_RESCUE_MAX_VALUE_CHARS`;\n * - for every other field, only whether it is filled (never its value);\n * - nothing whose name looks like a credential or payment field\n * (`isExcludedRescueField`), whatever the allowlist says.\n *\n * Field values go to the host only, never to analytics: the PostHog events\n * (`captureFormRescueEvent`) carry ids, field names and percentages.\n */\n\n/**\n * A rescuable form, defined ONCE beside the form that uses it (the strategy a\n * form plugs in with). The browser sends `id`; the host validates it with\n * `getRescueForm` and shows `label` to the team. A new form is one\n * `defineRescueForm` call: no central list, no host change.\n */\nexport interface FormRescueDefinition {\n /** Stable wire id: lower snake case, stored on every draft of this form. */\n readonly id: string;\n /** What the team reads on the alert (\"Contact form\"). */\n readonly label: string;\n}\n\nconst FORM_ID_PATTERN = /^[a-z][a-z0-9_]{1,47}$/;\nconst definitions = new Map<string, FormRescueDefinition>();\n\n/**\n * Register a rescuable form. Idempotent for the same id and label (a module\n * evaluated twice registers once); a second, different definition of one id\n * throws, so two forms can never share a draft vocabulary.\n */\nexport function defineRescueForm(definition: FormRescueDefinition): FormRescueDefinition {\n if (!FORM_ID_PATTERN.test(definition.id)) throw new Error(`[form-rescue] invalid form id \"${definition.id}\"`);\n const existing = definitions.get(definition.id);\n if (existing) {\n if (existing.label !== definition.label) {\n throw new Error(`[form-rescue] form id \"${definition.id}\" is already defined as \"${existing.label}\"`);\n }\n return existing;\n }\n const frozen = Object.freeze({ id: definition.id, label: definition.label });\n definitions.set(frozen.id, frozen);\n return frozen;\n}\n\n/** The registered definition for a wire id, or null (unknown ids are refused by the host). */\nexport function getRescueForm(id: unknown): FormRescueDefinition | null {\n return typeof id === 'string' ? (definitions.get(id) ?? null) : null;\n}\n\n/** Every registered form, for a host listing them. */\nexport function listRescueForms(): FormRescueDefinition[] {\n return [...definitions.values()];\n}\n\n/** The lib's own public forms. A host defines its own forms the same way, beside them. */\nexport const RESCUE_FORMS = {\n contact: defineRescueForm({ id: 'contact', label: 'Contact form' }),\n caseStudyPitch: defineRescueForm({ id: 'case_study_pitch', label: 'Case study pitch' }),\n dataRoomRequest: defineRescueForm({ id: 'data_room_request', label: 'Data room request' }),\n trustCenterRequest: defineRescueForm({ id: 'trust_center_request', label: 'Trust Center document request' }),\n meetingBooking: defineRescueForm({ id: 'meeting_booking', label: 'Meeting booking' }),\n waitlist: defineRescueForm({ id: 'waitlist', label: 'Waitlist' }),\n} as const;\n\n/**\n * Fields whose VALUES a draft keeps: who the visitor is and where they work.\n * Covers both naming schemes in use (the contact form's `name` / `companySize`,\n * the meeting booking's HubSpot `firstName` / `company` / `jobtitle`).\n * Free text (`message`) and phone numbers are reported as filled only.\n */\nexport const FORM_RESCUE_VALUE_FIELDS = [\n 'email',\n 'name',\n 'firstName',\n 'lastName',\n 'company',\n 'jobtitle',\n 'companySize',\n 'linkedin_url',\n] as const;\n\n/** Never send or store these, whatever the allowlist says. */\nconst EXCLUDED_FIELD_PATTERN =\n /pass(word|code)?|card|cvv|cvc|ssn|social.?security|iban|bank|routing|token|secret|otp|pin$/i;\n\nexport function isExcludedRescueField(name: string): boolean {\n return EXCLUDED_FIELD_PATTERN.test(name);\n}\n\n/** Longest value a draft keeps per field. */\nexport const FORM_RESCUE_MAX_VALUE_CHARS = 200;\n/** Save this long after the visitor stops typing. */\nexport const FORM_RESCUE_DEBOUNCE_MS = 1500;\n/** Most fields a draft reports (filled names), a bound on the payload. */\nexport const FORM_RESCUE_MAX_FIELDS = 40;\n\n/** Body keys a form's SUBMIT carries so the host can close the matching draft. */\nexport const FORM_RESCUE_ATTEMPT_FIELD = 'form_attempt_id';\nexport const FORM_RESCUE_RESUME_FIELD = 'form_resume_token';\n/** Every rescue key that rides in a submit body. Hosts forwarding a payload upstream strip by THIS array. */\nexport const FORM_RESCUE_SUBMIT_KEYS = [FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_RESUME_FIELD] as const;\n\n/** Query parameter a resume link carries. */\nexport const FORM_RESCUE_RESUME_PARAM = 'resume';\n\n/** Draft lifecycle. `submitted` is final; `rescued` can still become `submitted`. */\nexport const FORM_DRAFT_STATUSES = ['started', 'rescuable', 'rescued', 'submitted'] as const;\nexport type FormDraftStatus = (typeof FORM_DRAFT_STATUSES)[number];\n\n/** Why a due draft was settled WITHOUT alerting the team (stored on the draft). */\nexport const FORM_RESCUE_SKIP_REASONS = {\n finishedElsewhere: 'finished_elsewhere',\n alreadyRescued: 'already_rescued',\n internal: 'internal_email',\n} as const;\nexport type FormRescueSkipReason = (typeof FORM_RESCUE_SKIP_REASONS)[keyof typeof FORM_RESCUE_SKIP_REASONS];\n\n/** Same shape the host's `contact_submissions.email` check accepts. */\nconst EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/;\n\nexport function isRescueEmail(value: unknown): value is string {\n return typeof value === 'string' && value.length <= FORM_RESCUE_MAX_VALUE_CHARS && EMAIL_PATTERN.test(value.trim());\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function isFormAttemptId(value: unknown): value is string {\n return typeof value === 'string' && UUID_PATTERN.test(value);\n}\n\nfunction isFilled(value: unknown): boolean {\n if (value == null) return false;\n if (typeof value === 'string') return value.trim().length > 0;\n if (typeof value === 'boolean') return value;\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === 'object') return Object.values(value as Record<string, unknown>).some(isFilled);\n return true;\n}\n\n/**\n * Keep only allowlisted, non-excluded string values, trimmed and capped.\n * The ONE filter, run by the browser before sending and by the host before storing.\n */\nexport function sanitizeRescueValues(values: Record<string, unknown> | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!values || typeof values !== 'object') return out;\n for (const key of FORM_RESCUE_VALUE_FIELDS) {\n if (isExcludedRescueField(key)) continue;\n const raw = values[key];\n if (typeof raw !== 'string') continue;\n const trimmed = raw.trim().slice(0, FORM_RESCUE_MAX_VALUE_CHARS);\n if (trimmed) out[key] = trimmed;\n }\n return out;\n}\n\n/** Names of the given fields that hold something, credentials never included. */\nexport function filledRescueFields(values: Record<string, unknown>, fieldNames: readonly string[]): string[] {\n return fieldNames\n .filter(name => !isExcludedRescueField(name) && isFilled(values[name]))\n .slice(0, FORM_RESCUE_MAX_FIELDS);\n}\n\nexport function rescueCompletionPct(filledCount: number, totalCount: number): number {\n if (totalCount <= 0) return 0;\n return Math.max(0, Math.min(100, Math.round((filledCount / totalCount) * 100)));\n}\n\n/** A field NAME the host stores (`last_field`, `fields_filled`); bounded, printable. */\nexport function sanitizeRescueFieldName(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_.-]{1,64}$/.test(trimmed) && !isExcludedRescueField(trimmed) ? trimmed : null;\n}\n\n/** A form's progress at one moment: allowlisted values, filled field names, position. */\nexport interface FormDraftProgress {\n values: Record<string, string>;\n fields_filled: string[];\n last_field: string | null;\n completion_pct: number;\n}\n\n/** What the browser sends on every save (`POST <formDraftsUrl>`), beside the humanity signals. */\nexport interface FormDraftSaveRequest extends FormDraftProgress {\n /** The attempt's idempotency key: every save of one attempt updates one draft. */\n attempt_id: string;\n /** A `FormRescueDefinition.id`. */\n form_id: string;\n source_path: string;\n utm?: Partial<Record<'source' | 'medium' | 'campaign' | 'content' | 'term', string>>;\n}\n\n/** What the host answers a resume link with: the allowlisted values, nothing internal. */\nexport interface FormDraftResumeResponse {\n form_id: string;\n values: Record<string, string>;\n}\n\n/** The PostHog events this feature emits. Properties are ids, names and numbers, never values. */\nexport const FORM_RESCUE_EVENTS = {\n started: 'form_started',\n progress: 'form_progress',\n leadCaptured: 'form_lead_captured',\n abandoned: 'form_abandoned',\n submitted: 'form_submitted',\n rescued: 'form_rescued',\n resumed: 'form_resumed',\n} as const;\n\ntype AnalyticsValue = string | number | boolean | string[] | null;\n\n/**\n * Send one form event to PostHog when the page loaded it (GTM exposes\n * `window.posthog`); a no-op otherwise. Never throws.\n */\nexport function captureFormRescueEvent(event: string, properties: Record<string, AnalyticsValue>): void {\n try {\n if (typeof window === 'undefined') return;\n const ph = (window as unknown as { posthog?: { capture?: (e: string, p: object) => void } }).posthog;\n if (typeof ph?.capture === 'function') ph.capture(event, properties);\n } catch {\n // Analytics never breaks a form.\n }\n}\n"]}