@parag.more.withsurface.com/forms-sdk 0.1.4

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 (177) hide show
  1. package/README.md +63 -0
  2. package/dist/index.cjs +2737 -0
  3. package/dist/index.cjs.map +7 -0
  4. package/dist/index.mjs +2714 -0
  5. package/dist/index.mjs.map +7 -0
  6. package/dist/react/index.cjs +44 -0
  7. package/dist/react/index.cjs.map +7 -0
  8. package/dist/react/index.mjs +21 -0
  9. package/dist/react/index.mjs.map +7 -0
  10. package/dist/surface-forms-sdk.min.js +2 -0
  11. package/dist/surface-forms-sdk.min.js.map +7 -0
  12. package/dist/types/bind/bindForm.d.ts +43 -0
  13. package/dist/types/bind/bindForm.d.ts.map +1 -0
  14. package/dist/types/bind/bindForm.test.d.ts +2 -0
  15. package/dist/types/bind/bindForm.test.d.ts.map +1 -0
  16. package/dist/types/bind/bookingWidget.d.ts +35 -0
  17. package/dist/types/bind/bookingWidget.d.ts.map +1 -0
  18. package/dist/types/bind/bookingWidget.test.d.ts +2 -0
  19. package/dist/types/bind/bookingWidget.test.d.ts.map +1 -0
  20. package/dist/types/core/client/emailValidation.d.ts +23 -0
  21. package/dist/types/core/client/emailValidation.d.ts.map +1 -0
  22. package/dist/types/core/client/emailValidation.test.d.ts +2 -0
  23. package/dist/types/core/client/emailValidation.test.d.ts.map +1 -0
  24. package/dist/types/core/client/responses.d.ts +35 -0
  25. package/dist/types/core/client/responses.d.ts.map +1 -0
  26. package/dist/types/core/client/responses.test.d.ts +2 -0
  27. package/dist/types/core/client/responses.test.d.ts.map +1 -0
  28. package/dist/types/core/client/runtimeConfig.contract.test.d.ts +2 -0
  29. package/dist/types/core/client/runtimeConfig.contract.test.d.ts.map +1 -0
  30. package/dist/types/core/client/runtimeConfig.d.ts +72 -0
  31. package/dist/types/core/client/runtimeConfig.d.ts.map +1 -0
  32. package/dist/types/core/client/runtimeConfig.test.d.ts +2 -0
  33. package/dist/types/core/client/runtimeConfig.test.d.ts.map +1 -0
  34. package/dist/types/core/client/scheduler.contract.test.d.ts +2 -0
  35. package/dist/types/core/client/scheduler.contract.test.d.ts.map +1 -0
  36. package/dist/types/core/client/scheduler.d.ts +121 -0
  37. package/dist/types/core/client/scheduler.d.ts.map +1 -0
  38. package/dist/types/core/client/scheduler.test.d.ts +2 -0
  39. package/dist/types/core/client/scheduler.test.d.ts.map +1 -0
  40. package/dist/types/core/client/transport.d.ts +56 -0
  41. package/dist/types/core/client/transport.d.ts.map +1 -0
  42. package/dist/types/core/client/transport.test.d.ts +2 -0
  43. package/dist/types/core/client/transport.test.d.ts.map +1 -0
  44. package/dist/types/core/engine/answerAudit.d.ts +21 -0
  45. package/dist/types/core/engine/answerAudit.d.ts.map +1 -0
  46. package/dist/types/core/engine/answerAudit.test.d.ts +2 -0
  47. package/dist/types/core/engine/answerAudit.test.d.ts.map +1 -0
  48. package/dist/types/core/engine/answerStates.contract.test.d.ts +2 -0
  49. package/dist/types/core/engine/answerStates.contract.test.d.ts.map +1 -0
  50. package/dist/types/core/engine/answerStates.d.ts +92 -0
  51. package/dist/types/core/engine/answerStates.d.ts.map +1 -0
  52. package/dist/types/core/engine/formEngine.d.ts +145 -0
  53. package/dist/types/core/engine/formEngine.d.ts.map +1 -0
  54. package/dist/types/core/engine/formEngine.test.d.ts +2 -0
  55. package/dist/types/core/engine/formEngine.test.d.ts.map +1 -0
  56. package/dist/types/core/engine/redirect.d.ts +36 -0
  57. package/dist/types/core/engine/redirect.d.ts.map +1 -0
  58. package/dist/types/core/engine/redirect.test.d.ts +2 -0
  59. package/dist/types/core/engine/redirect.test.d.ts.map +1 -0
  60. package/dist/types/core/engine/responsePayload.contract.test.d.ts +2 -0
  61. package/dist/types/core/engine/responsePayload.contract.test.d.ts.map +1 -0
  62. package/dist/types/core/engine/responsePayload.d.ts +74 -0
  63. package/dist/types/core/engine/responsePayload.d.ts.map +1 -0
  64. package/dist/types/core/engine/responsePayload.test.d.ts +2 -0
  65. package/dist/types/core/engine/responsePayload.test.d.ts.map +1 -0
  66. package/dist/types/core/engine/urlData.contract.test.d.ts +2 -0
  67. package/dist/types/core/engine/urlData.contract.test.d.ts.map +1 -0
  68. package/dist/types/core/engine/urlData.d.ts +30 -0
  69. package/dist/types/core/engine/urlData.d.ts.map +1 -0
  70. package/dist/types/core/engine/urlData.test.d.ts +2 -0
  71. package/dist/types/core/engine/urlData.test.d.ts.map +1 -0
  72. package/dist/types/core/engine/urlPrefill.d.ts +21 -0
  73. package/dist/types/core/engine/urlPrefill.d.ts.map +1 -0
  74. package/dist/types/core/engine/urlPrefill.test.d.ts +2 -0
  75. package/dist/types/core/engine/urlPrefill.test.d.ts.map +1 -0
  76. package/dist/types/core/events/dropoffDetection.d.ts +21 -0
  77. package/dist/types/core/events/dropoffDetection.d.ts.map +1 -0
  78. package/dist/types/core/events/dropoffDetection.test.d.ts +2 -0
  79. package/dist/types/core/events/dropoffDetection.test.d.ts.map +1 -0
  80. package/dist/types/core/events/emitter.d.ts +23 -0
  81. package/dist/types/core/events/emitter.d.ts.map +1 -0
  82. package/dist/types/core/events/emitter.test.d.ts +2 -0
  83. package/dist/types/core/events/emitter.test.d.ts.map +1 -0
  84. package/dist/types/core/events/formJourneyTracker.contract.test.d.ts +2 -0
  85. package/dist/types/core/events/formJourneyTracker.contract.test.d.ts.map +1 -0
  86. package/dist/types/core/events/formJourneyTracker.d.ts +69 -0
  87. package/dist/types/core/events/formJourneyTracker.d.ts.map +1 -0
  88. package/dist/types/core/events/formJourneyTracker.test.d.ts +2 -0
  89. package/dist/types/core/events/formJourneyTracker.test.d.ts.map +1 -0
  90. package/dist/types/core/events/pushEvents.contract.test.d.ts +2 -0
  91. package/dist/types/core/events/pushEvents.contract.test.d.ts.map +1 -0
  92. package/dist/types/core/events/pushEvents.d.ts +62 -0
  93. package/dist/types/core/events/pushEvents.d.ts.map +1 -0
  94. package/dist/types/core/events/pushEvents.test.d.ts +2 -0
  95. package/dist/types/core/events/pushEvents.test.d.ts.map +1 -0
  96. package/dist/types/core/events/startedDetection.d.ts +11 -0
  97. package/dist/types/core/events/startedDetection.d.ts.map +1 -0
  98. package/dist/types/core/events/startedDetection.test.d.ts +2 -0
  99. package/dist/types/core/events/startedDetection.test.d.ts.map +1 -0
  100. package/dist/types/core/events/viewDetection.d.ts +15 -0
  101. package/dist/types/core/events/viewDetection.d.ts.map +1 -0
  102. package/dist/types/core/events/viewDetection.test.d.ts +2 -0
  103. package/dist/types/core/events/viewDetection.test.d.ts.map +1 -0
  104. package/dist/types/core/identity/browserFingerprint.d.ts +40 -0
  105. package/dist/types/core/identity/browserFingerprint.d.ts.map +1 -0
  106. package/dist/types/core/identity/browserFingerprint.test.d.ts +2 -0
  107. package/dist/types/core/identity/browserFingerprint.test.d.ts.map +1 -0
  108. package/dist/types/core/identity/ensureIdentified.d.ts +27 -0
  109. package/dist/types/core/identity/ensureIdentified.d.ts.map +1 -0
  110. package/dist/types/core/identity/ensureIdentified.test.d.ts +2 -0
  111. package/dist/types/core/identity/ensureIdentified.test.d.ts.map +1 -0
  112. package/dist/types/core/identity/identify.contract.test.d.ts +2 -0
  113. package/dist/types/core/identity/identify.contract.test.d.ts.map +1 -0
  114. package/dist/types/core/identity/identify.d.ts +48 -0
  115. package/dist/types/core/identity/identify.d.ts.map +1 -0
  116. package/dist/types/core/identity/identify.test.d.ts +2 -0
  117. package/dist/types/core/identity/identify.test.d.ts.map +1 -0
  118. package/dist/types/core/identity/journey.contract.test.d.ts +2 -0
  119. package/dist/types/core/identity/journey.contract.test.d.ts.map +1 -0
  120. package/dist/types/core/identity/journey.d.ts +28 -0
  121. package/dist/types/core/identity/journey.d.ts.map +1 -0
  122. package/dist/types/core/identity/journey.test.d.ts +2 -0
  123. package/dist/types/core/identity/journey.test.d.ts.map +1 -0
  124. package/dist/types/core/identity/journey.testStubs.d.ts +5 -0
  125. package/dist/types/core/identity/journey.testStubs.d.ts.map +1 -0
  126. package/dist/types/core/identity/leadDataCache.d.ts +27 -0
  127. package/dist/types/core/identity/leadDataCache.d.ts.map +1 -0
  128. package/dist/types/core/identity/leadDataCache.test.d.ts +2 -0
  129. package/dist/types/core/identity/leadDataCache.test.d.ts.map +1 -0
  130. package/dist/types/enrichment/attachEnrichment.d.ts +35 -0
  131. package/dist/types/enrichment/attachEnrichment.d.ts.map +1 -0
  132. package/dist/types/enrichment/attachEnrichment.test.d.ts +2 -0
  133. package/dist/types/enrichment/attachEnrichment.test.d.ts.map +1 -0
  134. package/dist/types/index.d.ts +28 -0
  135. package/dist/types/index.d.ts.map +1 -0
  136. package/dist/types/react/index.d.ts +2 -0
  137. package/dist/types/react/index.d.ts.map +1 -0
  138. package/dist/types/react/ssr.test.d.ts +2 -0
  139. package/dist/types/react/ssr.test.d.ts.map +1 -0
  140. package/dist/types/react/useSurfaceForm.d.ts +25 -0
  141. package/dist/types/react/useSurfaceForm.d.ts.map +1 -0
  142. package/dist/types/react/useSurfaceForm.test.d.ts +2 -0
  143. package/dist/types/react/useSurfaceForm.test.d.ts.map +1 -0
  144. package/dist/types/surfaceForms.d.ts +120 -0
  145. package/dist/types/surfaceForms.d.ts.map +1 -0
  146. package/dist/types/surfaceForms.test.d.ts +2 -0
  147. package/dist/types/surfaceForms.test.d.ts.map +1 -0
  148. package/dist/types/tracking/attachTracking.d.ts +35 -0
  149. package/dist/types/tracking/attachTracking.d.ts.map +1 -0
  150. package/dist/types/tracking/attachTracking.test.d.ts +2 -0
  151. package/dist/types/tracking/attachTracking.test.d.ts.map +1 -0
  152. package/dist/types/tracking/conversionRules.contract.test.d.ts +2 -0
  153. package/dist/types/tracking/conversionRules.contract.test.d.ts.map +1 -0
  154. package/dist/types/tracking/conversionRules.d.ts +87 -0
  155. package/dist/types/tracking/conversionRules.d.ts.map +1 -0
  156. package/dist/types/tracking/conversionRules.test.d.ts +2 -0
  157. package/dist/types/tracking/conversionRules.test.d.ts.map +1 -0
  158. package/dist/types/tracking/vendors.d.ts +46 -0
  159. package/dist/types/tracking/vendors.d.ts.map +1 -0
  160. package/dist/types/tracking/vendors.test.d.ts +2 -0
  161. package/dist/types/tracking/vendors.test.d.ts.map +1 -0
  162. package/dist/types/validation/attachEmailValidation.d.ts +8 -0
  163. package/dist/types/validation/attachEmailValidation.d.ts.map +1 -0
  164. package/dist/types/validation/attachEmailValidation.test.d.ts +2 -0
  165. package/dist/types/validation/attachEmailValidation.test.d.ts.map +1 -0
  166. package/docs/api.md +310 -0
  167. package/docs/reference/binding.md +116 -0
  168. package/docs/reference/clients.md +212 -0
  169. package/docs/reference/engine.md +193 -0
  170. package/docs/reference/events.md +93 -0
  171. package/docs/reference/identity.md +116 -0
  172. package/llms.txt +317 -0
  173. package/package.json +25 -0
  174. package/skills/surface-forms-sdk/SKILL.md +153 -0
  175. package/skills/surface-forms-sdk/debugging.md +64 -0
  176. package/skills/surface-forms-sdk/editing.md +67 -0
  177. package/skills/surface-forms-sdk/patterns.md +222 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,2714 @@
1
+ // src/core/events/viewDetection.ts
2
+ var observeFormView = (element, onViewed) => {
3
+ if (typeof IntersectionObserver === "undefined") {
4
+ onViewed();
5
+ return () => void 0;
6
+ }
7
+ let viewed = false;
8
+ const observer = new IntersectionObserver((entries) => {
9
+ const [entry] = entries;
10
+ if (!entry?.isIntersecting || viewed) return;
11
+ viewed = true;
12
+ observer.disconnect();
13
+ onViewed();
14
+ });
15
+ observer.observe(element);
16
+ return () => observer.disconnect();
17
+ };
18
+
19
+ // src/bind/bindForm.ts
20
+ var fieldSelector = "input, select, textarea";
21
+ var resolveFields = (element) => {
22
+ if (element.matches(fieldSelector)) {
23
+ return [element];
24
+ }
25
+ return Array.from(element.querySelectorAll(fieldSelector)).filter(
26
+ (field) => field.closest("[data-question-id]") === element
27
+ );
28
+ };
29
+ var fieldNameFor = (element, field, questionType) => {
30
+ const explicit = element.getAttribute("data-field-name") ?? field.getAttribute("data-field-name");
31
+ if (explicit) return explicit;
32
+ if (questionType === "Dropdown" && field instanceof HTMLSelectElement) return "selected";
33
+ if (field instanceof HTMLInputElement && field.type === "email") return "email";
34
+ return "input";
35
+ };
36
+ var questionTypeFor = (element, field) => {
37
+ const explicit = element.getAttribute("data-question-type") ?? field.getAttribute("data-question-type");
38
+ if (explicit) return explicit;
39
+ if (field instanceof HTMLInputElement && field.type === "email") return "EmailForm";
40
+ return "ShortInput";
41
+ };
42
+ var fieldValue = (field) => {
43
+ if (field instanceof HTMLInputElement && (field.type === "checkbox" || field.type === "radio")) {
44
+ return field.type === "checkbox" ? field.checked : field.value;
45
+ }
46
+ return field.value;
47
+ };
48
+ var isChoiceControl = (field) => field instanceof HTMLInputElement && (field.type === "radio" || field.type === "checkbox");
49
+ var selectedValueAndIndex = (options) => {
50
+ const index = options.findIndex((option) => option.checked);
51
+ return { value: index === -1 ? "" : options[index].value, index };
52
+ };
53
+ var CHOICE_ANSWER_BUILDERS = {
54
+ MultipleOptionsQuestion: (options) => ({
55
+ choices: options.map((option, index) => ({ key: option.value, value: option.checked, index }))
56
+ }),
57
+ MultipleChoiceQuestion: (options) => ({
58
+ choices: options.map((option) => ({ key: option.value, value: option.checked }))
59
+ }),
60
+ MMChoiceQuestion: (options) => ({
61
+ choices: options.map((option) => ({ key: option.value, value: option.checked }))
62
+ }),
63
+ GraphicOptions: selectedValueAndIndex,
64
+ MultipleChoiceSingleAnswer: selectedValueAndIndex
65
+ };
66
+ var bindForm = ({ engine, root, expectedQuestionIds }) => {
67
+ const teardowns = [];
68
+ const boundQuestionIds = /* @__PURE__ */ new Set();
69
+ const listen = (target, type, handler) => {
70
+ target.addEventListener(type, handler);
71
+ teardowns.push(() => target.removeEventListener(type, handler));
72
+ };
73
+ for (const element of Array.from(root.querySelectorAll("[data-question-id]"))) {
74
+ const questionId = element.getAttribute("data-question-id");
75
+ const fields = resolveFields(element);
76
+ if (fields.length === 0) continue;
77
+ boundQuestionIds.add(questionId);
78
+ const groupType = questionTypeFor(element, fields[0]);
79
+ const buildChoiceAnswer = CHOICE_ANSWER_BUILDERS[groupType];
80
+ const choiceControls = fields.filter(isChoiceControl);
81
+ if (buildChoiceAnswer && choiceControls.length > 0) {
82
+ const apply = () => engine.setAnswer(questionId, { type: groupType, ...buildChoiceAnswer(choiceControls) });
83
+ for (const control of choiceControls) {
84
+ listen(control, "change", apply);
85
+ }
86
+ if (choiceControls.some((control) => control.checked)) apply();
87
+ continue;
88
+ }
89
+ for (const field of fields) {
90
+ const type = questionTypeFor(element, field);
91
+ const fieldName = fieldNameFor(element, field, type);
92
+ const apply = () => engine.setAnswer(questionId, { type, [fieldName]: fieldValue(field) });
93
+ listen(field, "input", apply);
94
+ listen(field, "change", apply);
95
+ const initial = fieldValue(field);
96
+ if (initial !== "" && initial !== false) apply();
97
+ }
98
+ }
99
+ const unbound = (expectedQuestionIds ?? []).filter((id) => !boundQuestionIds.has(id));
100
+ if (unbound.length > 0) {
101
+ console.warn(
102
+ `Surface SDK: form record declares questionIds with no bound element: ${unbound.join(", ")}. Answers for these will never be captured \u2014 check data-question-id attributes (validate_form_html catches this at authoring time).`
103
+ );
104
+ }
105
+ const navigationMarkerSelector = "[data-surface-goto-step], .surface-disqualify-button, .surface-submit-button, .surface-next-button";
106
+ for (const button of Array.from(root.querySelectorAll(navigationMarkerSelector))) {
107
+ const gotoStepId = button.getAttribute("data-surface-goto-step");
108
+ const markers = [
109
+ gotoStepId != null ? "goto" : null,
110
+ button.classList.contains("surface-disqualify-button") ? "disqualify" : null,
111
+ button.classList.contains("surface-submit-button") ? "submit" : null,
112
+ button.classList.contains("surface-next-button") ? "next" : null
113
+ ].filter((marker) => marker != null);
114
+ const action = markers[0];
115
+ if (markers.length > 1) {
116
+ console.warn(
117
+ `Surface SDK: element carries multiple navigation markers (${markers.join(
118
+ ", "
119
+ )}); only "${action}" is bound.`
120
+ );
121
+ }
122
+ listen(button, "click", (event) => {
123
+ event.preventDefault();
124
+ if (action === "goto") {
125
+ void engine.goToStep(gotoStepId).catch((error) => console.warn(`Surface SDK: ${String(error)}`));
126
+ } else if (action === "disqualify") {
127
+ void engine.disqualify();
128
+ } else if (action === "submit") {
129
+ void engine.submit();
130
+ } else {
131
+ void engine.next();
132
+ }
133
+ });
134
+ }
135
+ if (root instanceof HTMLFormElement) {
136
+ listen(root, "submit", (event) => {
137
+ event.preventDefault();
138
+ void engine.submit();
139
+ });
140
+ }
141
+ const stepContainers = Array.from(root.querySelectorAll("[data-step-id]"));
142
+ const showCurrentStep = () => {
143
+ const { currentStepId } = engine.state();
144
+ for (const container of stepContainers) {
145
+ container.hidden = container.getAttribute("data-step-id") !== currentStepId;
146
+ }
147
+ };
148
+ if (stepContainers.length > 0) {
149
+ showCurrentStep();
150
+ teardowns.push(engine.on("stepChanged", showCurrentStep));
151
+ }
152
+ teardowns.push(observeFormView(root, () => engine.markViewed()));
153
+ return () => teardowns.forEach((teardown) => teardown());
154
+ };
155
+
156
+ // src/bind/bookingWidget.ts
157
+ var currentMonth = () => {
158
+ const now = /* @__PURE__ */ new Date();
159
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
160
+ };
161
+ var monthLabel = (month) => {
162
+ const [year, monthNum] = month.split("-").map(Number);
163
+ return new Date(year, monthNum - 1, 1).toLocaleDateString(void 0, { month: "long", year: "numeric" });
164
+ };
165
+ var dateLabel = (date) => {
166
+ const [year, monthNum, day] = date.split("-").map(Number);
167
+ return new Date(year, monthNum - 1, day).toLocaleDateString(void 0, {
168
+ weekday: "short",
169
+ month: "short",
170
+ day: "numeric"
171
+ });
172
+ };
173
+ var shiftMonth = (month, delta) => {
174
+ const [year, monthNum] = month.split("-").map(Number);
175
+ const shifted = new Date(year, monthNum - 1 + delta, 1);
176
+ return `${shifted.getFullYear()}-${String(shifted.getMonth() + 1).padStart(2, "0")}`;
177
+ };
178
+ var mountBookingWidget = ({
179
+ container,
180
+ scheduler,
181
+ eventTypeId,
182
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
183
+ duration,
184
+ prefill,
185
+ identity,
186
+ formatters,
187
+ onComplete
188
+ }) => {
189
+ const formatMonth = formatters?.month ?? monthLabel;
190
+ const formatDate = formatters?.date ?? dateLabel;
191
+ const formatTime = formatters?.time ?? ((slot) => new Date(slot.start).toLocaleTimeString(void 0, {
192
+ hour: "numeric",
193
+ minute: "2-digit",
194
+ timeZone: timezone
195
+ }));
196
+ const root = document.createElement("div");
197
+ root.className = "surface-booking";
198
+ container.appendChild(root);
199
+ let month = currentMonth();
200
+ let dates = [];
201
+ let selectedDate = null;
202
+ let selectedSlot = null;
203
+ let destroyed = false;
204
+ let generation = 0;
205
+ const el = (tag, className, text) => {
206
+ const node = document.createElement(tag);
207
+ node.className = className;
208
+ if (text != null) node.textContent = text;
209
+ return node;
210
+ };
211
+ const render = (children) => {
212
+ if (destroyed) return;
213
+ root.replaceChildren(...children);
214
+ };
215
+ const renderError = (message, retry) => {
216
+ const error = el("p", "surface-booking-error", message);
217
+ const retryButton = el("button", "surface-booking-retry", "Try again");
218
+ retryButton.type = "button";
219
+ retryButton.addEventListener("click", retry);
220
+ render([error, retryButton]);
221
+ };
222
+ const renderDates = async () => {
223
+ const requestedMonth = month;
224
+ const gen = ++generation;
225
+ render([el("p", "surface-booking-loading", "Loading availability\u2026")]);
226
+ const result = await scheduler.getMonthAvailability({
227
+ eventTypeId,
228
+ month: requestedMonth,
229
+ timezone,
230
+ duration
231
+ });
232
+ if (destroyed || gen !== generation) return;
233
+ if (!result.ok) {
234
+ renderError("Unable to load availability.", () => void renderDates());
235
+ return;
236
+ }
237
+ dates = result.data.data?.dates ?? [];
238
+ const heading = el("p", "surface-booking-month", formatMonth(requestedMonth));
239
+ heading.dataset.value = requestedMonth;
240
+ const nav = el("div", "surface-booking-month-nav");
241
+ for (const [label, delta] of [
242
+ ["Previous month", -1],
243
+ ["Next month", 1]
244
+ ]) {
245
+ const button = el("button", "surface-booking-month-button", label);
246
+ button.type = "button";
247
+ button.addEventListener("click", () => {
248
+ month = shiftMonth(month, delta);
249
+ void renderDates();
250
+ });
251
+ nav.appendChild(button);
252
+ }
253
+ const list = el("ul", "surface-booking-dates");
254
+ for (const date of dates.filter((d) => d.slots && d.slots.length > 0)) {
255
+ const item = el("li", "surface-booking-date");
256
+ const button = el("button", "surface-booking-date-button", formatDate(date.date));
257
+ button.dataset.value = date.date;
258
+ button.type = "button";
259
+ button.addEventListener("click", () => {
260
+ selectedDate = date;
261
+ void renderSlots();
262
+ });
263
+ item.appendChild(button);
264
+ list.appendChild(item);
265
+ }
266
+ if (list.childElementCount === 0) {
267
+ list.appendChild(el("li", "surface-booking-no-dates", "No availability this month."));
268
+ }
269
+ render([heading, nav, list]);
270
+ };
271
+ const renderSlots = async () => {
272
+ if (!selectedDate) return;
273
+ const gen = ++generation;
274
+ let slots = selectedDate.slots ?? [];
275
+ if (slots.length === 0) {
276
+ const result = await scheduler.getSlots({ eventTypeId, date: selectedDate.date, timezone, duration });
277
+ if (destroyed || gen !== generation) return;
278
+ if (!result.ok) {
279
+ renderError("Unable to load times.", () => void renderSlots());
280
+ return;
281
+ }
282
+ slots = result.data.data?.slots ?? [];
283
+ }
284
+ const heading = el("p", "surface-booking-day", formatDate(selectedDate.date));
285
+ heading.dataset.value = selectedDate.date;
286
+ const back = el("button", "surface-booking-back", "Back");
287
+ back.type = "button";
288
+ back.addEventListener("click", () => void renderDates());
289
+ const list = el("ul", "surface-booking-slots");
290
+ for (const slot of slots) {
291
+ const item = el("li", "surface-booking-slot");
292
+ const button = el("button", "surface-booking-slot-button", formatTime(slot));
293
+ button.dataset.value = slot.start;
294
+ button.type = "button";
295
+ button.addEventListener("click", () => {
296
+ selectedSlot = slot;
297
+ renderForm();
298
+ });
299
+ item.appendChild(button);
300
+ list.appendChild(item);
301
+ }
302
+ render([heading, back, list]);
303
+ };
304
+ const renderForm = (errorMessage) => {
305
+ if (!selectedSlot) return;
306
+ const form = el("form", "surface-booking-form");
307
+ const name = el("input", "surface-booking-name");
308
+ name.name = "name";
309
+ name.placeholder = "Name";
310
+ name.value = prefill?.name ?? "";
311
+ const email = el("input", "surface-booking-email");
312
+ email.type = "email";
313
+ email.name = "email";
314
+ email.placeholder = "Email";
315
+ email.value = prefill?.email ?? "";
316
+ const submit = el("button", "surface-booking-submit", "Book");
317
+ submit.type = "submit";
318
+ form.append(name, email, submit);
319
+ if (errorMessage) form.appendChild(el("p", "surface-booking-error", errorMessage));
320
+ form.addEventListener("submit", (event) => {
321
+ event.preventDefault();
322
+ void (async () => {
323
+ submit.disabled = true;
324
+ const result = await scheduler.book({
325
+ eventTypeId,
326
+ startTime: selectedSlot.start,
327
+ endTime: selectedSlot.end,
328
+ timezone,
329
+ inviteeName: name.value,
330
+ inviteeEmail: email.value,
331
+ duration,
332
+ ...identity
333
+ });
334
+ if (destroyed) return;
335
+ if (!result.ok) {
336
+ renderForm("Booking failed. Please try again.");
337
+ return;
338
+ }
339
+ render([el("p", "surface-booking-booked", "Booked!")]);
340
+ onComplete(result.data);
341
+ })();
342
+ });
343
+ render([form]);
344
+ };
345
+ void renderDates();
346
+ return () => {
347
+ destroyed = true;
348
+ root.remove();
349
+ };
350
+ };
351
+
352
+ // src/core/client/emailValidation.ts
353
+ var VALIDATION_PATH = "/api/v1/edge/emailValidation";
354
+ var failOpenResult = (email) => ({
355
+ email,
356
+ isValid: true,
357
+ status: "unknown",
358
+ reason: "validation_failed",
359
+ failed: true
360
+ });
361
+ var resultOf = (email, row) => {
362
+ if (!row || typeof row.isValid !== "boolean") return failOpenResult(email);
363
+ return {
364
+ email,
365
+ isValid: row.isValid,
366
+ status: row.status,
367
+ reason: row.reason,
368
+ ...row.blacklistEntryIds?.length ? { blacklistEntryIds: row.blacklistEntryIds } : {},
369
+ ...row.validationSuccessfullyRan === false ? { failed: true } : {}
370
+ };
371
+ };
372
+ var createEmailValidator = ({
373
+ transport,
374
+ environmentId,
375
+ preview = false
376
+ }) => {
377
+ const cache = /* @__PURE__ */ new Map();
378
+ const validateBatch = (emails) => {
379
+ const normalized = [...new Set(emails.map((email) => email.trim()).filter((email) => email !== ""))];
380
+ if (preview) {
381
+ for (const email of normalized) {
382
+ if (!cache.has(email)) {
383
+ cache.set(email, Promise.resolve({ email, isValid: true, status: "preview" }));
384
+ }
385
+ }
386
+ }
387
+ const pending = normalized.filter((email) => !cache.has(email));
388
+ if (pending.length > 0) {
389
+ const request = transport.post(VALIDATION_PATH, { emails: pending, environmentId }).then((result) => result.ok ? result.data.data ?? {} : {}).catch(() => ({}));
390
+ for (const email of pending) {
391
+ cache.set(
392
+ email,
393
+ request.then((rows) => resultOf(email, rows[email]))
394
+ );
395
+ }
396
+ }
397
+ return Promise.all(normalized.map((email) => cache.get(email)));
398
+ };
399
+ return {
400
+ validateBatch,
401
+ validate: async (email) => {
402
+ const [result] = await validateBatch([email]);
403
+ return result ?? failOpenResult(email.trim());
404
+ }
405
+ };
406
+ };
407
+
408
+ // src/core/client/responses.ts
409
+ var CREATE_PATH = "/api/v1/responses/";
410
+ var UPDATE_PATH = "/api/v1/responses/update";
411
+ var GET_PATH = "/api/v1/responses/getResponse";
412
+ var submitResponse = async (transport, payload) => {
413
+ const result = await transport.post(CREATE_PATH, payload, { credentials: true });
414
+ if (!result.ok) return result;
415
+ const id = result.data.data?.id;
416
+ if (!id) {
417
+ return { ok: false, error: { kind: "http", status: 200, body: result.data } };
418
+ }
419
+ return {
420
+ ok: true,
421
+ data: {
422
+ responseId: id,
423
+ leadId: result.data.data?.meta?.leadId ?? null,
424
+ resumeToken: result.data.data?.resumeToken ?? null
425
+ }
426
+ };
427
+ };
428
+ var updateResponse = (transport, payload) => {
429
+ transport.beacon(UPDATE_PATH, payload);
430
+ };
431
+ var updateResponseConfirmed = (transport, payload) => transport.post(UPDATE_PATH, payload, { keepalive: true, credentials: true });
432
+ var isAnswerRow = (row) => typeof row?.questionId === "string" && typeof row?.response === "object" && row.response !== null;
433
+ var fetchStoredResponse = async (transport, { responseId, resumeToken }) => {
434
+ const result = await transport.get(
435
+ `${GET_PATH}?rid=${encodeURIComponent(responseId)}&rt=${encodeURIComponent(resumeToken)}`
436
+ );
437
+ if (!result.ok) return result;
438
+ const rows = Array.isArray(result.data.data?.data) ? result.data.data.data : [];
439
+ return {
440
+ ok: true,
441
+ data: { answers: rows.filter(isAnswerRow).filter((row) => row.response.type !== "URL_DATA") }
442
+ };
443
+ };
444
+
445
+ // src/core/client/runtimeConfig.ts
446
+ var fetchRuntimeConfig = (transport, formId, { preview = false, previewToken } = {}) => transport.get(
447
+ `/api/v1/public/forms/${formId}/runtime-config${// The signed token (from the create_form / get_sdk_binding_map replies)
448
+ // unlocks the DRAFT config of a never-published form; without it the
449
+ // server serves the published config.
450
+ preview ? `?preview=1${previewToken ? `&pt=${encodeURIComponent(previewToken)}` : ""}` : ""}`
451
+ );
452
+ var flatComponentsOf = (step) => {
453
+ const components = step.components;
454
+ if (Array.isArray(components)) return components;
455
+ const layout = components?.layout;
456
+ if (!Array.isArray(layout)) return [];
457
+ return layout.flatMap((layoutRow) => {
458
+ const row = layoutRow.row;
459
+ if (!Array.isArray(row)) return [];
460
+ return row.map(
461
+ (item) => item.type === "GridLayoutRowItem" ? item.component : item
462
+ );
463
+ });
464
+ };
465
+ var stepsOf = (config) => (config.steps ?? []).map((step) => ({
466
+ id: step.id,
467
+ ...step.endStepKind !== void 0 && { endStepKind: step.endStepKind }
468
+ }));
469
+ var statefulQuestionsOf = (config) => (config.steps ?? []).flatMap(flatComponentsOf).flatMap((component) => {
470
+ const typed = component;
471
+ if (typeof typed?.state?.questionId !== "string") return [];
472
+ return [
473
+ {
474
+ questionId: typed.state.questionId,
475
+ ...typeof typed.type === "string" && { componentType: typed.type }
476
+ }
477
+ ];
478
+ });
479
+ var statefulQuestionIdsOf = (config) => statefulQuestionsOf(config).map((question) => question.questionId);
480
+ var enrichmentSourcesOf = (config) => (config.steps ?? []).flatMap(flatComponentsOf).flatMap((component) => {
481
+ const typed = component;
482
+ if (typeof typed?.state?.questionId !== "string") return [];
483
+ if (typed.type === "IdentityInfo") {
484
+ const source = typeof typed.content?.enrichmentSourceField === "string" ? typed.content.enrichmentSourceField : "emailAddress";
485
+ return [{ questionId: typed.state.questionId, fieldNames: [.../* @__PURE__ */ new Set([source, "websiteUrl"])] }];
486
+ }
487
+ if (typed.type === "EmailForm") {
488
+ return [{ questionId: typed.state.questionId, fieldNames: ["email"] }];
489
+ }
490
+ return [];
491
+ });
492
+ var hasDeclaredEnrichmentSource = (config) => (config.steps ?? []).flatMap(flatComponentsOf).some(
493
+ (component) => typeof component?.content?.enrichmentSourceField === "string"
494
+ );
495
+ var hasEnabledEnrichmentProvider = (settings) => {
496
+ const enrichment = settings?.enrichment;
497
+ if (enrichment?.version !== 2 || !Array.isArray(enrichment.providers)) return false;
498
+ return enrichment.providers.some(
499
+ (provider) => provider?.enabled === true && Array.isArray(provider.enabledFields) && provider.enabledFields.length > 0
500
+ );
501
+ };
502
+
503
+ // src/core/client/scheduler.ts
504
+ var toParams = (entries) => new URLSearchParams(
505
+ Object.fromEntries(Object.entries(entries).filter(([, value]) => value != null))
506
+ );
507
+ var createSchedulerClient = ({
508
+ transport,
509
+ preview = false
510
+ }) => {
511
+ const previewParam = preview ? "true" : void 0;
512
+ const unwrapBooking = (result) => {
513
+ if (!result.ok) return result;
514
+ const booking = result.data.data;
515
+ if (!booking?.id) return { ok: false, error: { kind: "http", status: 200, body: result.data } };
516
+ return { ok: true, data: booking };
517
+ };
518
+ return {
519
+ getPublicEventType: (eventTypeId) => transport.get(`/api/v1/scheduler/event-types/${eventTypeId}/public`),
520
+ getAvailabilitySummary: ({ eventTypeId, timezone }) => {
521
+ const params = new URLSearchParams({
522
+ eventTypeId,
523
+ timezone: timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone
524
+ });
525
+ return transport.get(`/api/v1/scheduler/availability-summary?${params}`);
526
+ },
527
+ getMonthAvailability: ({ eventTypeId, month, timezone, duration, hostId, ownerEmail }) => {
528
+ const [year, monthNum] = month.split("-").map(Number);
529
+ const params = toParams({
530
+ eventTypeId,
531
+ month: String(monthNum),
532
+ year: String(year),
533
+ timezone,
534
+ duration: duration ? String(duration) : void 0,
535
+ preview: previewParam,
536
+ hostId,
537
+ ownerEmail
538
+ });
539
+ return transport.get(`/api/v1/scheduler/availability?${params}`);
540
+ },
541
+ getSlots: ({ eventTypeId, date, timezone, duration, hostId, ownerEmail }) => {
542
+ const params = toParams({
543
+ eventTypeId,
544
+ date,
545
+ timezone,
546
+ duration: duration ? String(duration) : void 0,
547
+ preview: previewParam,
548
+ hostId,
549
+ ownerEmail
550
+ });
551
+ return transport.get(`/api/v1/scheduler/slots?${params}`);
552
+ },
553
+ book: async ({
554
+ eventTypeId,
555
+ startTime,
556
+ endTime,
557
+ timezone,
558
+ inviteeName,
559
+ inviteeEmail,
560
+ duration,
561
+ intakeResponses,
562
+ guestEmails,
563
+ captchaToken,
564
+ responseId,
565
+ leadSessionId,
566
+ fingerprint,
567
+ hostRouting
568
+ }) => {
569
+ if (preview) {
570
+ return {
571
+ ok: true,
572
+ data: {
573
+ id: "preview-booking-" + Date.now(),
574
+ startTime,
575
+ // Native fabricates from the selected slot (BookingForm: slot.end).
576
+ endTime,
577
+ timezone,
578
+ inviteeName: inviteeName || "Test User",
579
+ inviteeEmail: inviteeEmail || "test@example.com",
580
+ meetingJoinLink: null,
581
+ cancelToken: "preview-cancel-" + Date.now(),
582
+ rescheduleToken: "preview-reschedule-" + Date.now(),
583
+ attendees: [{ name: "Host", email: "host@example.com", isHost: true }]
584
+ }
585
+ };
586
+ }
587
+ const result = await transport.post("/api/v1/scheduler/book", {
588
+ eventTypeId,
589
+ startTime,
590
+ timezone,
591
+ inviteeName,
592
+ inviteeEmail: inviteeEmail.trim(),
593
+ ...duration ? { duration } : {},
594
+ ...intakeResponses && Object.keys(intakeResponses).length > 0 ? { intakeResponses } : {},
595
+ ...guestEmails && guestEmails.length > 0 ? { guestEmails } : {},
596
+ ...captchaToken ? { captchaToken } : {},
597
+ meta: {
598
+ source: "form",
599
+ ...responseId ? { responseId } : {},
600
+ ...leadSessionId ? { leadSessionId } : {},
601
+ ...fingerprint ? { fingerprint } : {},
602
+ ...hostRouting ? { hostRouting } : {}
603
+ }
604
+ });
605
+ return unwrapBooking(result);
606
+ },
607
+ reschedule: async ({ bookingId, rescheduleToken, newStartTime, timezone }) => {
608
+ const result = await transport.post(
609
+ `/api/v1/scheduler/bookings/${bookingId}/reschedule`,
610
+ { rescheduleToken, newStartTime, timezone }
611
+ );
612
+ return unwrapBooking(result);
613
+ }
614
+ };
615
+ };
616
+ var toSchedulerAnswerState = (booking) => {
617
+ const hostAttendee = booking.attendees?.find((attendee) => attendee.isHost);
618
+ return {
619
+ type: "DynamicScheduler",
620
+ eventScheduled: true,
621
+ uri: booking.id,
622
+ meetingTime: booking.startTime,
623
+ meetingEndTime: booking.endTime,
624
+ ownerEmails: hostAttendee?.email ?? "",
625
+ guestEmails: booking.inviteeEmail,
626
+ inviteeName: booking.inviteeName,
627
+ googleMeetLink: booking.meetingJoinLink ?? ""
628
+ };
629
+ };
630
+
631
+ // src/core/client/transport.ts
632
+ var DEFAULT_API_BASE_URL = "https://forms.withsurface.com";
633
+ var SDK_VERSION = "0.1.0";
634
+ var versionedHeaders = (headers = {}) => ({
635
+ ...headers,
636
+ "X-Surface-Sdk-Version": SDK_VERSION
637
+ });
638
+ var versionedUrl = (url) => `${url}${url.includes("?") ? "&" : "?"}sdv=${SDK_VERSION}`;
639
+ var createTransport = ({
640
+ apiBaseUrl = DEFAULT_API_BASE_URL
641
+ } = {}) => {
642
+ const baseUrl = apiBaseUrl.replace(/\/$/, "");
643
+ const toUrl = (path) => `${baseUrl}${path}`;
644
+ const get = async (path) => {
645
+ try {
646
+ const response = await fetch(toUrl(path), { headers: versionedHeaders() });
647
+ if (!response.ok) {
648
+ const body = await response.json().catch(() => void 0);
649
+ return { ok: false, error: { kind: "http", status: response.status, body } };
650
+ }
651
+ return { ok: true, data: await response.json() };
652
+ } catch (cause) {
653
+ return { ok: false, error: { kind: "network", cause } };
654
+ }
655
+ };
656
+ const post = async (path, payload, opts) => {
657
+ try {
658
+ const response = await fetch(toUrl(path), {
659
+ method: "POST",
660
+ headers: versionedHeaders({ "Content-Type": "application/json" }),
661
+ body: JSON.stringify(payload),
662
+ ...opts?.keepalive ? { keepalive: true } : {},
663
+ // The `${formId}_pa` person-attributes cookie (repeat-visitor signals)
664
+ // lives on the API origin; only credentialed requests carry it from a
665
+ // customer-hosted page (Decisions #12 — the endpoints echo the origin).
666
+ ...opts?.credentials ? { credentials: "include" } : {}
667
+ });
668
+ if (!response.ok) {
669
+ const body = await response.json().catch(() => void 0);
670
+ return { ok: false, error: { kind: "http", status: response.status, body } };
671
+ }
672
+ return { ok: true, data: await response.json() };
673
+ } catch (cause) {
674
+ return { ok: false, error: { kind: "network", cause } };
675
+ }
676
+ };
677
+ const beacon = (path, payload) => {
678
+ let serialized;
679
+ try {
680
+ serialized = JSON.stringify(payload);
681
+ } catch {
682
+ return false;
683
+ }
684
+ const url = versionedUrl(toUrl(path));
685
+ try {
686
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
687
+ const queued = navigator.sendBeacon(url, new Blob([serialized], { type: "application/json" }));
688
+ if (queued) return true;
689
+ }
690
+ } catch {
691
+ }
692
+ try {
693
+ void fetch(url, {
694
+ method: "POST",
695
+ keepalive: true,
696
+ // sendBeacon always sends credentials; the fallback must match, or the
697
+ // update path's `${formId}_pa` cookie round-trip depends on which
698
+ // delivery won.
699
+ credentials: "include",
700
+ headers: versionedHeaders({ "Content-Type": "application/json" }),
701
+ body: serialized
702
+ }).catch(() => void 0);
703
+ } catch {
704
+ }
705
+ return false;
706
+ };
707
+ return { get, post, beacon };
708
+ };
709
+
710
+ // src/core/engine/answerStates.ts
711
+ var IDENTITY_INFO_ANSWER_KEYS = [
712
+ "firstName",
713
+ "lastName",
714
+ "fullName",
715
+ "emailAddress",
716
+ "workEmailAddress",
717
+ "phoneNumber",
718
+ "companyName",
719
+ "numberOfEmployees",
720
+ "websiteUrl",
721
+ "title",
722
+ "streetAddress",
723
+ "aptSuiteEtc",
724
+ "city",
725
+ "state",
726
+ "country",
727
+ "zipCode",
728
+ "companyRevenue",
729
+ "profilePictureUrl"
730
+ ];
731
+
732
+ // src/core/events/emitter.ts
733
+ var createEmitter = () => {
734
+ const handlers = /* @__PURE__ */ new Map();
735
+ return {
736
+ on: (event, handler) => {
737
+ const existing = handlers.get(event) ?? /* @__PURE__ */ new Set();
738
+ existing.add(handler);
739
+ handlers.set(event, existing);
740
+ return () => {
741
+ existing.delete(handler);
742
+ };
743
+ },
744
+ emit: (event, payload) => {
745
+ handlers.get(event)?.forEach((handler) => {
746
+ try {
747
+ handler(payload);
748
+ } catch (error) {
749
+ console.error(`Surface SDK: "${String(event)}" handler threw`, error);
750
+ }
751
+ });
752
+ }
753
+ };
754
+ };
755
+
756
+ // src/core/events/pushEvents.ts
757
+ var PUSH_EVENT_PATH = "/api/v1/pushEvent";
758
+ var KNOWN_BOT_UA = /bot|crawl|spider|slurp|baidu|bingpreview|googlebot|yandex|duckduck|ahrefs|semrush|facebookexternalhit|embedly|quora link preview|pinterest|twitterbot|linkedinbot|whatsapp|telegram|discord|headless|lighthouse|pagespeed/i;
759
+ var isLikelyBot = () => {
760
+ const ua = navigator.userAgent;
761
+ if (!ua) return false;
762
+ return KNOWN_BOT_UA.test(ua);
763
+ };
764
+ var hasRequiredFields = (event) => Boolean(event?.type && event?.entityType && event?.entityId);
765
+ var buildStepTransitionEventBatch = ({
766
+ sessionId,
767
+ formId,
768
+ currentStepId,
769
+ nextStepId,
770
+ stepCompletionTime
771
+ }) => ({
772
+ events: [
773
+ {
774
+ type: "form_step_submitted",
775
+ entityType: "form_step",
776
+ entityId: formId,
777
+ stepId: currentStepId,
778
+ leadSessionId: sessionId,
779
+ properties: [{ key: "stepCompletionTime", value: stepCompletionTime }]
780
+ },
781
+ {
782
+ type: "form_step_viewed",
783
+ entityType: "form_step",
784
+ entityId: formId,
785
+ stepId: nextStepId,
786
+ leadSessionId: sessionId,
787
+ properties: []
788
+ }
789
+ ]
790
+ });
791
+ var createPushEventClient = ({
792
+ transport,
793
+ preview = false
794
+ }) => ({
795
+ push: (request) => {
796
+ if (preview) return;
797
+ if (typeof navigator === "undefined") return;
798
+ const events = "events" in request ? request.events : [request];
799
+ if (isLikelyBot()) return;
800
+ if (events.some((event) => !hasRequiredFields(event))) return;
801
+ transport.beacon(PUSH_EVENT_PATH, request);
802
+ }
803
+ });
804
+
805
+ // src/core/events/startedDetection.ts
806
+ var createStartedDetector = (initial) => {
807
+ const snapshot = deepClone(initial);
808
+ let started = false;
809
+ return (current) => {
810
+ if (started) return false;
811
+ if (deepEqual(snapshot, current)) return false;
812
+ started = true;
813
+ return true;
814
+ };
815
+ };
816
+ var deepClone = (value) => {
817
+ if (Array.isArray(value)) return value.map(deepClone);
818
+ if (value !== null && typeof value === "object") {
819
+ const clone = {};
820
+ for (const [key, entry] of Object.entries(value)) clone[key] = deepClone(entry);
821
+ return clone;
822
+ }
823
+ return value;
824
+ };
825
+ var deepEqual = (a, b) => {
826
+ if (Object.is(a, b)) return true;
827
+ if (Array.isArray(a) || Array.isArray(b)) {
828
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
829
+ return a.every((entry, index) => deepEqual(entry, b[index]));
830
+ }
831
+ if (a !== null && b !== null && typeof a === "object" && typeof b === "object") {
832
+ const aKeys = Object.keys(a);
833
+ const bKeys = Object.keys(b);
834
+ if (aKeys.length !== bKeys.length) return false;
835
+ return aKeys.every(
836
+ (key) => deepEqual(a[key], b[key])
837
+ );
838
+ }
839
+ return false;
840
+ };
841
+
842
+ // src/core/engine/responsePayload.ts
843
+ var buildResponsesArray = (answers, externalStates) => {
844
+ const responses = [];
845
+ let enrichedFields = {};
846
+ let bookingId;
847
+ for (const step in answers) {
848
+ const stepState = answers[step];
849
+ for (const questionId in stepState) {
850
+ const entry = stepState[questionId];
851
+ if (!bookingId && entry?.eventScheduled === true && typeof entry.uri === "string" && entry.uri.length > 0 && !entry.uri.includes("calendly.com") && !entry.uri.includes("cal.com")) {
852
+ bookingId = entry.uri;
853
+ }
854
+ responses.push({ questionId, response: { ...stepState[questionId] } });
855
+ }
856
+ }
857
+ for (const stateId in externalStates) {
858
+ const externalState = externalStates[stateId];
859
+ if (externalState.type === "SCHEDULER_AVAILABILITY_DATA") continue;
860
+ responses.push({ questionId: stateId, response: { ...externalState } });
861
+ if (externalState.type === "APOLLO_EMAIL_ENRICHMENT" && externalState.processed) {
862
+ enrichedFields = externalState.data ?? {};
863
+ }
864
+ }
865
+ return { responses, enrichedFields, bookingId };
866
+ };
867
+ var buildBasePayload = ({
868
+ formId,
869
+ answers,
870
+ externalStates,
871
+ submittingOnStepId,
872
+ finished,
873
+ invalidResponse,
874
+ trackCookies,
875
+ meta = {},
876
+ personAttributeEnrichedFields,
877
+ fingerprint,
878
+ leadSessionId
879
+ }) => {
880
+ const { responses, enrichedFields, bookingId } = buildResponsesArray(answers, externalStates);
881
+ const filteredMeta = trackCookies ? meta : Object.fromEntries(Object.entries(meta).filter(([key]) => key !== "cookies"));
882
+ return {
883
+ responses,
884
+ enrichedFields: personAttributeEnrichedFields ?? enrichedFields ?? {},
885
+ formId,
886
+ finished,
887
+ invalidResponse,
888
+ stepId: submittingOnStepId,
889
+ fingerprint,
890
+ leadSessionId,
891
+ meta: bookingId ? { ...filteredMeta, bookingId } : filteredMeta
892
+ };
893
+ };
894
+ var createSubmitResponsePayload = (options) => ({
895
+ ...buildBasePayload(options),
896
+ responseShapeVersion: "2.0"
897
+ });
898
+ var createUpdateResponsePayload = (options) => ({
899
+ ...buildBasePayload(options),
900
+ responseId: options.responseId
901
+ });
902
+
903
+ // src/core/engine/formEngine.ts
904
+ var createFormEngine = ({
905
+ formId,
906
+ environmentId,
907
+ steps,
908
+ transport,
909
+ resolveNextStep,
910
+ trackPartialResponses = true,
911
+ trackCookies = false,
912
+ externalStates = {},
913
+ meta = {},
914
+ identity,
915
+ pushEvents,
916
+ formJourney,
917
+ preview = false,
918
+ resume
919
+ }) => {
920
+ if (steps.length === 0) throw new Error("createFormEngine requires at least one step");
921
+ const resolve = resolveNextStep ?? ((currentStepId2) => {
922
+ const index = steps.findIndex((step) => step.id === currentStepId2);
923
+ const following = steps[index + 1];
924
+ return following ? { stepId: following.id, reason: "linear" } : "END";
925
+ });
926
+ const emitter = createEmitter();
927
+ const storeListeners = /* @__PURE__ */ new Set();
928
+ let snapshotDirty = true;
929
+ let snapshot;
930
+ const notify = () => {
931
+ snapshotDirty = true;
932
+ storeListeners.forEach((listener) => listener());
933
+ };
934
+ const currentIdentity = () => identity?.() ?? {};
935
+ const answers = {};
936
+ let currentStepId = steps[0].id;
937
+ let responseId = resume?.responseId ?? null;
938
+ let resumeToken = null;
939
+ if (resume?.answers) {
940
+ const seeded = {};
941
+ for (const [questionId, state] of Object.entries(resume.answers)) {
942
+ seeded[questionId] = { ...state };
943
+ }
944
+ answers[currentStepId] = seeded;
945
+ }
946
+ let finished = false;
947
+ let terminalWrite = null;
948
+ let viewed = false;
949
+ const createdAt = Date.now();
950
+ const history2 = [];
951
+ const hasStarted = createStartedDetector(answers);
952
+ formJourney?.setFormId(formId);
953
+ if (responseId != null) formJourney?.setResponseId(responseId);
954
+ formJourney?.recordEntry(currentStepId);
955
+ const write = async (submittingOnStepId, isFinished, invalid) => {
956
+ if (preview) return;
957
+ const payloadOptions = {
958
+ formId,
959
+ answers,
960
+ externalStates,
961
+ submittingOnStepId,
962
+ finished: isFinished,
963
+ invalidResponse: invalid,
964
+ trackCookies,
965
+ meta,
966
+ fingerprint: currentIdentity().fingerprint,
967
+ leadSessionId: currentIdentity().leadSessionId
968
+ };
969
+ if (responseId == null) {
970
+ const result = await submitResponse(transport, createSubmitResponsePayload(payloadOptions));
971
+ if (!result.ok) {
972
+ if (result.error.kind === "network") {
973
+ pushEvents?.push({
974
+ type: "response_submission_error",
975
+ entityType: "response",
976
+ entityId: formId,
977
+ stepId: submittingOnStepId,
978
+ leadSessionId: currentIdentity().leadSessionId,
979
+ properties: [{ key: "error", value: "network" }]
980
+ });
981
+ }
982
+ emitter.emit("error", { scope: "submit", error: result.error });
983
+ return;
984
+ }
985
+ responseId = result.data.responseId;
986
+ resumeToken = result.data.resumeToken;
987
+ formJourney?.setResponseId(responseId);
988
+ pushEvents?.push({
989
+ type: "response_submission_created",
990
+ entityType: "response",
991
+ entityId: formId,
992
+ stepId: submittingOnStepId,
993
+ leadSessionId: currentIdentity().leadSessionId,
994
+ properties: [{ key: "responseId", value: responseId }]
995
+ });
996
+ } else if (isFinished) {
997
+ const result = await updateResponseConfirmed(
998
+ transport,
999
+ createUpdateResponsePayload({ ...payloadOptions, responseId })
1000
+ );
1001
+ if (!result.ok) {
1002
+ updateResponse(transport, createUpdateResponsePayload({ ...payloadOptions, responseId }));
1003
+ emitter.emit("error", { scope: "submit", error: result.error });
1004
+ return;
1005
+ }
1006
+ } else {
1007
+ updateResponse(transport, createUpdateResponsePayload({ ...payloadOptions, responseId }));
1008
+ }
1009
+ notify();
1010
+ emitter.emit("saved", { responseId, finished: isFinished, resumeToken });
1011
+ };
1012
+ let writeQueue = Promise.resolve();
1013
+ const enqueueWrite = (task) => {
1014
+ const run = writeQueue.then(() => finished ? void 0 : task());
1015
+ writeQueue = run.catch(() => void 0);
1016
+ return run;
1017
+ };
1018
+ const amendAfterFinish = () => {
1019
+ if (!finished || terminalWrite == null) return;
1020
+ const amend = terminalWrite;
1021
+ writeQueue = writeQueue.then(() => {
1022
+ if (responseId != null) return write(amend.stepId, true, amend.invalid);
1023
+ }).catch(() => void 0);
1024
+ };
1025
+ let pendingAdvance = null;
1026
+ const advance = (submitting) => {
1027
+ if (finished) return Promise.resolve();
1028
+ if (pendingAdvance) {
1029
+ return submitting ? pendingAdvance.then(() => advance(true)) : pendingAdvance;
1030
+ }
1031
+ const tracked = enqueueWrite(() => advanceNow(submitting)).finally(() => {
1032
+ if (pendingAdvance === tracked) pendingAdvance = null;
1033
+ });
1034
+ pendingAdvance = tracked;
1035
+ return tracked;
1036
+ };
1037
+ const rewindTo = (targetStepId) => {
1038
+ const from = currentStepId;
1039
+ while (history2.length > 0 && currentStepId !== targetStepId) {
1040
+ currentStepId = history2.pop();
1041
+ }
1042
+ notify();
1043
+ formJourney?.recordTransition({ fromStepId: from, toStepId: currentStepId, reason: "back_nav" });
1044
+ emitter.emit("stepChanged", { fromStepId: from, toStepId: currentStepId });
1045
+ };
1046
+ const goToStep = (targetStepId) => {
1047
+ if (!steps.some((step) => step.id === targetStepId)) {
1048
+ return Promise.reject(new Error(`goToStep: no step with id "${targetStepId}"`));
1049
+ }
1050
+ if (finished) return Promise.resolve();
1051
+ if (pendingAdvance) {
1052
+ return pendingAdvance.then(() => goToStep(targetStepId));
1053
+ }
1054
+ if (targetStepId === currentStepId) return Promise.resolve();
1055
+ if (history2.includes(targetStepId)) {
1056
+ rewindTo(targetStepId);
1057
+ return Promise.resolve();
1058
+ }
1059
+ const tracked = enqueueWrite(async () => {
1060
+ if (targetStepId === currentStepId) return;
1061
+ emitter.emit("stepCompleted", { stepId: currentStepId });
1062
+ await moveForwardNow({ stepId: targetStepId, reason: "conditional_match" });
1063
+ }).finally(() => {
1064
+ if (pendingAdvance === tracked) pendingAdvance = null;
1065
+ });
1066
+ pendingAdvance = tracked;
1067
+ return tracked;
1068
+ };
1069
+ const disqualify = () => {
1070
+ if (finished) return Promise.resolve();
1071
+ if (pendingAdvance) {
1072
+ return pendingAdvance.then(() => disqualify());
1073
+ }
1074
+ const tracked = enqueueWrite(() => finishNow(true)).finally(() => {
1075
+ if (pendingAdvance === tracked) pendingAdvance = null;
1076
+ });
1077
+ pendingAdvance = tracked;
1078
+ return tracked;
1079
+ };
1080
+ const moveForwardNow = async (target) => {
1081
+ const leavingStepId = currentStepId;
1082
+ pushEvents?.push(
1083
+ buildStepTransitionEventBatch({
1084
+ sessionId: currentIdentity().leadSessionId,
1085
+ formId,
1086
+ currentStepId: leavingStepId,
1087
+ nextStepId: target.stepId,
1088
+ stepCompletionTime: (Date.now() - createdAt) / 1e3
1089
+ })
1090
+ );
1091
+ formJourney?.recordTransition({
1092
+ fromStepId: leavingStepId,
1093
+ toStepId: target.stepId,
1094
+ reason: target.reason ?? "linear"
1095
+ });
1096
+ if (trackPartialResponses) await write(leavingStepId, false, false);
1097
+ history2.push(leavingStepId);
1098
+ currentStepId = target.stepId;
1099
+ notify();
1100
+ emitter.emit("stepChanged", { fromStepId: leavingStepId, toStepId: currentStepId });
1101
+ };
1102
+ const revealEndingStep = (kind) => {
1103
+ const ending = steps.find((step) => step.endStepKind === kind);
1104
+ if (!ending || ending.id === currentStepId) return;
1105
+ const from = currentStepId;
1106
+ history2.push(from);
1107
+ currentStepId = ending.id;
1108
+ notify();
1109
+ emitter.emit("stepChanged", { fromStepId: from, toStepId: currentStepId });
1110
+ };
1111
+ const finishNow = async (invalid) => {
1112
+ const leavingStepId = currentStepId;
1113
+ finished = true;
1114
+ terminalWrite = { stepId: leavingStepId, invalid };
1115
+ await write(leavingStepId, true, invalid);
1116
+ revealEndingStep(invalid ? "disqualified" : "thank_you");
1117
+ if (invalid) {
1118
+ formJourney?.recordTerminal({
1119
+ outcome: "disqualified",
1120
+ disqualification: { reason: "conditional_match", atStepId: leavingStepId }
1121
+ });
1122
+ emitter.emit("disqualified", { stepId: leavingStepId });
1123
+ } else {
1124
+ formJourney?.recordTerminal({ outcome: "completed" });
1125
+ pushEvents?.push({
1126
+ type: "form_completed",
1127
+ entityType: "form",
1128
+ entityId: formId,
1129
+ stepId: null,
1130
+ leadSessionId: currentIdentity().leadSessionId,
1131
+ properties: [],
1132
+ environmentId,
1133
+ leadId: currentIdentity().leadId
1134
+ });
1135
+ emitter.emit("completed", { formId });
1136
+ }
1137
+ notify();
1138
+ };
1139
+ const advanceNow = async (submitting) => {
1140
+ const resolved = resolve(currentStepId, answers);
1141
+ const isFinished = submitting || resolved === "END" || resolved === "INVALID" || resolved == null;
1142
+ emitter.emit("stepCompleted", { stepId: currentStepId });
1143
+ if (!isFinished) {
1144
+ await moveForwardNow(resolved);
1145
+ return;
1146
+ }
1147
+ await finishNow(resolved === "INVALID");
1148
+ };
1149
+ return {
1150
+ setAnswer: (questionId, state) => {
1151
+ const stepState = answers[currentStepId] ?? (answers[currentStepId] = {});
1152
+ stepState[questionId] = { ...stepState[questionId], ...state };
1153
+ if (hasStarted(answers)) {
1154
+ pushEvents?.push({
1155
+ type: "form_started",
1156
+ entityType: "form",
1157
+ entityId: formId,
1158
+ stepId: null,
1159
+ leadSessionId: currentIdentity().leadSessionId,
1160
+ properties: []
1161
+ });
1162
+ emitter.emit("started", { formId });
1163
+ }
1164
+ notify();
1165
+ },
1166
+ setExternalState: (id, state) => {
1167
+ externalStates[id] = state;
1168
+ notify();
1169
+ amendAfterFinish();
1170
+ },
1171
+ mergeMeta: (partial) => {
1172
+ Object.assign(meta, partial);
1173
+ amendAfterFinish();
1174
+ },
1175
+ next: () => advance(false),
1176
+ goToStep,
1177
+ submit: () => advance(true),
1178
+ disqualify,
1179
+ back: () => {
1180
+ const previous = history2.pop();
1181
+ if (previous == null || finished) return;
1182
+ const from = currentStepId;
1183
+ currentStepId = previous;
1184
+ notify();
1185
+ formJourney?.recordTransition({ fromStepId: from, toStepId: previous, reason: "back_nav" });
1186
+ emitter.emit("stepChanged", { fromStepId: from, toStepId: previous });
1187
+ },
1188
+ savePartial: () => {
1189
+ const submittingOnStepId = currentStepId;
1190
+ return enqueueWrite(() => write(submittingOnStepId, false, false));
1191
+ },
1192
+ // Reads the queue at call time (it grows via amendAfterFinish), so this
1193
+ // settles after everything already queued — including post-finish amends.
1194
+ flush: () => writeQueue.then(() => void 0),
1195
+ markViewed: () => {
1196
+ if (viewed) return;
1197
+ viewed = true;
1198
+ pushEvents?.push({
1199
+ type: "form_viewed",
1200
+ entityType: "form",
1201
+ entityId: formId,
1202
+ stepId: null,
1203
+ leadSessionId: currentIdentity().leadSessionId,
1204
+ properties: [],
1205
+ environmentId,
1206
+ leadId: currentIdentity().leadId
1207
+ });
1208
+ pushEvents?.push({
1209
+ type: "form_step_viewed",
1210
+ entityType: "form_step",
1211
+ entityId: formId,
1212
+ stepId: steps[0].id,
1213
+ leadSessionId: currentIdentity().leadSessionId,
1214
+ properties: []
1215
+ });
1216
+ emitter.emit("viewed", { formId });
1217
+ },
1218
+ on: emitter.on,
1219
+ state: () => {
1220
+ if (snapshotDirty) {
1221
+ snapshot = { currentStepId, answers, externalStates, responseId, resumeToken, finished };
1222
+ snapshotDirty = false;
1223
+ }
1224
+ return snapshot;
1225
+ },
1226
+ subscribe: (listener) => {
1227
+ storeListeners.add(listener);
1228
+ return () => storeListeners.delete(listener);
1229
+ }
1230
+ };
1231
+ };
1232
+
1233
+ // src/core/engine/redirect.ts
1234
+ var DEFAULT_REDIRECT_TIMEOUT_MS = 400;
1235
+ var addProtocolToUrl = (url, protocol = "https") => {
1236
+ if (!url) return url;
1237
+ if (url.startsWith(protocol)) return url;
1238
+ return `${protocol}://${url}`;
1239
+ };
1240
+ var buildRedirectUrl = (url, { leadId, sessionId } = {}) => {
1241
+ if (!url || !url.trim()) return "";
1242
+ try {
1243
+ const redirectUrl = new URL(addProtocolToUrl(url));
1244
+ if (leadId) redirectUrl.searchParams.set("leadId", leadId);
1245
+ if (sessionId) redirectUrl.searchParams.set("sessionId", sessionId);
1246
+ return redirectUrl.toString();
1247
+ } catch {
1248
+ return url;
1249
+ }
1250
+ };
1251
+ var performRedirect = ({
1252
+ url,
1253
+ redirectType = "CurrentWindow",
1254
+ leadId,
1255
+ sessionId,
1256
+ preview = false
1257
+ }) => {
1258
+ if (!url || preview) return;
1259
+ if (typeof window === "undefined") return;
1260
+ const redirectUrl = buildRedirectUrl(url, { leadId, sessionId });
1261
+ if (!redirectUrl) return;
1262
+ if (redirectType === "CurrentWindow") {
1263
+ window.location.href = redirectUrl;
1264
+ } else if (redirectType === "NewTab") {
1265
+ window.open(redirectUrl, "_blank");
1266
+ }
1267
+ };
1268
+
1269
+ // src/core/engine/urlData.ts
1270
+ var constructUrlParamsObject = (url) => {
1271
+ const urlObj = new URL(url);
1272
+ if (!urlObj.search) return {};
1273
+ const paramsObject = {};
1274
+ new URLSearchParams(urlObj.search).forEach((value, key) => {
1275
+ paramsObject[key] = value;
1276
+ });
1277
+ return paramsObject;
1278
+ };
1279
+ var parseCookiesObject = (cookieString) => {
1280
+ const cookies = {};
1281
+ for (const cookie of cookieString.split(";")) {
1282
+ const trimmed = cookie.trim();
1283
+ const eqIndex = trimmed.indexOf("=");
1284
+ if (eqIndex <= 0) continue;
1285
+ const value = trimmed.substring(eqIndex + 1);
1286
+ try {
1287
+ cookies[trimmed.substring(0, eqIndex)] = decodeURIComponent(value);
1288
+ } catch {
1289
+ cookies[trimmed.substring(0, eqIndex)] = value;
1290
+ }
1291
+ }
1292
+ return cookies;
1293
+ };
1294
+ var buildUrlData = ({
1295
+ defaultUrl,
1296
+ trackCookies = false
1297
+ } = {}) => {
1298
+ const inBrowser = typeof window !== "undefined";
1299
+ const urlData = {
1300
+ type: "URL_DATA",
1301
+ fullUrl: defaultUrl ?? (inBrowser ? window.location.href : void 0),
1302
+ referrer: inBrowser && typeof document !== "undefined" ? document.referrer || "" : void 0
1303
+ };
1304
+ if (urlData.fullUrl) {
1305
+ try {
1306
+ urlData.params = constructUrlParamsObject(urlData.fullUrl);
1307
+ } catch {
1308
+ }
1309
+ }
1310
+ if (trackCookies) {
1311
+ urlData.cookies = inBrowser && typeof document !== "undefined" ? parseCookiesObject(document.cookie) : {};
1312
+ } else {
1313
+ urlData.cookies = {};
1314
+ }
1315
+ return urlData;
1316
+ };
1317
+
1318
+ // src/core/engine/urlPrefill.ts
1319
+ var parseQuestionId = (input) => {
1320
+ const firstUnderscore = input.indexOf("_");
1321
+ return {
1322
+ originalId: input,
1323
+ questionId: firstUnderscore >= 0 ? input.slice(0, firstUnderscore).trim() : input,
1324
+ fieldName: firstUnderscore >= 0 ? input.slice(firstUnderscore + 1).trim() : null
1325
+ };
1326
+ };
1327
+ var RESERVED_PARAMS = /* @__PURE__ */ new Set(["url", "referrer", "cookies"]);
1328
+ var parsePrefillParams = (search) => {
1329
+ const params = typeof search === "string" ? new URLSearchParams(search) : search;
1330
+ let questionIds = null;
1331
+ for (const id of params.keys()) {
1332
+ if (RESERVED_PARAMS.has(id)) continue;
1333
+ const { originalId, questionId, fieldName } = parseQuestionId(id);
1334
+ if (fieldName == null) continue;
1335
+ const prefill = { originalId, questionId, fieldName, value: params.get(id) };
1336
+ questionIds ?? (questionIds = /* @__PURE__ */ Object.create(null));
1337
+ questionIds[questionId] = [...questionIds[questionId] ?? [], prefill];
1338
+ }
1339
+ return questionIds;
1340
+ };
1341
+
1342
+ // src/core/events/dropoffDetection.ts
1343
+ var observeDropoff = (pushEventClient, getState) => {
1344
+ if (typeof window === "undefined") return () => void 0;
1345
+ const handlePageHide = (event) => {
1346
+ const { started, formId, stepId, sessionId } = getState();
1347
+ if (!started || event.persisted) return;
1348
+ pushEventClient.push({
1349
+ type: "form_step_dropoff",
1350
+ entityType: "form_step",
1351
+ entityId: formId,
1352
+ stepId,
1353
+ leadSessionId: sessionId,
1354
+ properties: []
1355
+ });
1356
+ };
1357
+ window.addEventListener("pagehide", handlePageHide);
1358
+ return () => window.removeEventListener("pagehide", handlePageHide);
1359
+ };
1360
+
1361
+ // src/core/events/formJourneyTracker.ts
1362
+ var TRACK_PATH = "/api/v1/form-journey/track";
1363
+ var MAX_VISITS = 50;
1364
+ var TERMINAL_OUTCOMES = /* @__PURE__ */ new Set([
1365
+ "completed",
1366
+ "disqualified",
1367
+ "dropped_off",
1368
+ "partial"
1369
+ ]);
1370
+ var createFormJourneyTracker = ({
1371
+ transport,
1372
+ preview = false
1373
+ }) => {
1374
+ const state = {
1375
+ responseId: null,
1376
+ formId: null,
1377
+ nextOrdinal: 1,
1378
+ enteredAt: /* @__PURE__ */ new Map(),
1379
+ pendingClose: null,
1380
+ visits: [],
1381
+ truncated: false,
1382
+ terminal: {},
1383
+ frozen: false,
1384
+ flushScheduled: false
1385
+ };
1386
+ const flushNow = () => {
1387
+ state.flushScheduled = false;
1388
+ if (preview) return;
1389
+ if (!state.responseId || !state.formId) return;
1390
+ if (state.visits.length === 0 && !state.terminal.outcome) return;
1391
+ transport.beacon(TRACK_PATH, {
1392
+ responseId: state.responseId,
1393
+ formId: state.formId,
1394
+ events: state.visits,
1395
+ ...state.terminal
1396
+ });
1397
+ state.terminal = {};
1398
+ };
1399
+ const flush = () => {
1400
+ if (state.flushScheduled) return;
1401
+ state.flushScheduled = true;
1402
+ queueMicrotask(() => {
1403
+ if (state.flushScheduled) flushNow();
1404
+ });
1405
+ };
1406
+ const closePrevious = (nowIso) => {
1407
+ if (!state.pendingClose) return;
1408
+ const { ordinal, stepId } = state.pendingClose;
1409
+ const entered = state.enteredAt.get(ordinal);
1410
+ const existing = state.visits.find((visit) => visit.ordinal === ordinal);
1411
+ const durationMs = entered ? Date.parse(nowIso) - Date.parse(entered) : null;
1412
+ if (existing) {
1413
+ existing.exitedAt = nowIso;
1414
+ existing.durationMs = durationMs;
1415
+ } else {
1416
+ state.visits.push({
1417
+ ordinal,
1418
+ stepId,
1419
+ enteredAt: entered ?? nowIso,
1420
+ exitedAt: nowIso,
1421
+ durationMs,
1422
+ fromStepId: null,
1423
+ transitionReason: "initial"
1424
+ });
1425
+ }
1426
+ state.pendingClose = null;
1427
+ };
1428
+ const pushVisit = (visit) => {
1429
+ if (state.visits.length >= MAX_VISITS) {
1430
+ state.truncated = true;
1431
+ return false;
1432
+ }
1433
+ state.visits.push(visit);
1434
+ return true;
1435
+ };
1436
+ return {
1437
+ hasRecordedTransition: () => state.nextOrdinal > 1,
1438
+ setResponseId: (id) => {
1439
+ state.responseId = id;
1440
+ flush();
1441
+ },
1442
+ setFormId: (id) => {
1443
+ state.formId = id;
1444
+ flush();
1445
+ },
1446
+ recordEntry: (stepId) => {
1447
+ if (state.frozen || state.truncated) return;
1448
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1449
+ if (state.nextOrdinal === 1) {
1450
+ state.enteredAt.set(1, now);
1451
+ state.pendingClose = { ordinal: 1, stepId };
1452
+ state.nextOrdinal = 2;
1453
+ }
1454
+ },
1455
+ recordTransition: ({ fromStepId, toStepId, reason, transitionMeta }) => {
1456
+ if (state.frozen || state.truncated) return;
1457
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1458
+ closePrevious(now);
1459
+ const ordinal = state.nextOrdinal++;
1460
+ state.enteredAt.set(ordinal, now);
1461
+ const pushed = pushVisit({
1462
+ ordinal,
1463
+ stepId: toStepId,
1464
+ enteredAt: now,
1465
+ exitedAt: null,
1466
+ durationMs: null,
1467
+ fromStepId,
1468
+ transitionReason: reason,
1469
+ transitionMeta: transitionMeta ?? null
1470
+ });
1471
+ state.pendingClose = pushed ? { ordinal, stepId: toStepId } : null;
1472
+ flush();
1473
+ },
1474
+ recordTerminal: ({ outcome, disqualification }) => {
1475
+ if (state.frozen) return;
1476
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1477
+ closePrevious(now);
1478
+ state.terminal = {
1479
+ outcome,
1480
+ disqualification: disqualification ? { reason: disqualification.reason, matched: disqualification.matched } : void 0,
1481
+ disqualifiedAtStepId: disqualification?.atStepId,
1482
+ disqualifiedTriggerStepId: disqualification?.triggerStepId
1483
+ };
1484
+ if (TERMINAL_OUTCOMES.has(outcome)) state.frozen = true;
1485
+ if (outcome === "in_progress") flushNow();
1486
+ else flush();
1487
+ }
1488
+ };
1489
+ };
1490
+
1491
+ // src/core/identity/browserFingerprint.ts
1492
+ async function getHash(input) {
1493
+ const data = new TextEncoder().encode(input);
1494
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1495
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
1496
+ }
1497
+ var getBrowserFingerprint = async (environmentId) => {
1498
+ const userAgentData = navigator.userAgentData || {};
1499
+ const fields = {
1500
+ deviceType: /Mobi|Android/i.test(navigator.userAgent) ? "Mobile" : "Desktop",
1501
+ screen: {
1502
+ width: screen.width,
1503
+ height: screen.height,
1504
+ colorDepth: screen.colorDepth
1505
+ },
1506
+ userAgent: navigator.userAgent,
1507
+ browser: userAgentData.brands || userAgentData.uaList || [{ brand: "unknown", version: "unknown" }],
1508
+ os: userAgentData.platform || "unknown",
1509
+ language: navigator.language,
1510
+ ...navigator.plugins != null ? { plugins: Array.from(navigator.plugins).map((plugin) => plugin.name) } : {},
1511
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1512
+ environmentId
1513
+ };
1514
+ return { ...fields, id: await getHash(JSON.stringify(fields)) };
1515
+ };
1516
+
1517
+ // src/core/identity/identify.ts
1518
+ var IDENTIFY_PATH = "/api/v1/lead/identify";
1519
+ var buildIdentifyPayload = ({
1520
+ environmentId,
1521
+ fingerprintId,
1522
+ pageUrl,
1523
+ source = "sdkForm",
1524
+ formId,
1525
+ leadId = null,
1526
+ sessionIdFromParams = null,
1527
+ referrer
1528
+ }) => {
1529
+ const url = new URL(pageUrl);
1530
+ return {
1531
+ environmentId,
1532
+ source,
1533
+ sourceURL: url.href.replace(/\/$/, ""),
1534
+ sourceURLDomain: url.hostname,
1535
+ sourceURLPath: url.pathname.replace(/\/$/, ""),
1536
+ sourceUrlSearchParams: url.search,
1537
+ fingerprint: fingerprintId,
1538
+ formId,
1539
+ leadId,
1540
+ sessionIdFromParams,
1541
+ ...referrer != null ? { referrer } : {}
1542
+ };
1543
+ };
1544
+ var identifyLead = async (transport, payload) => {
1545
+ const result = await transport.post(IDENTIFY_PATH, payload, {
1546
+ keepalive: true,
1547
+ credentials: true
1548
+ });
1549
+ if (!result.ok) return null;
1550
+ const data = result.data.data?.data;
1551
+ if (!data?.leadId || !data?.sessionId) return null;
1552
+ return {
1553
+ leadId: data.leadId,
1554
+ sessionId: data.sessionId,
1555
+ existing: data.existing,
1556
+ leadHasExistingBookingAttr: data.leadHasExistingBookingAttr,
1557
+ leadHasExistingEmailAttr: data.leadHasExistingEmailAttr
1558
+ };
1559
+ };
1560
+
1561
+ // src/core/identity/leadDataCache.ts
1562
+ var LEAD_DATA_STORAGE_KEY = "surfaceLeadData";
1563
+ var LEAD_DATA_TTL_MS = 10 * 60 * 1e3;
1564
+ var readLeadData = () => {
1565
+ if (typeof localStorage === "undefined") return null;
1566
+ try {
1567
+ const raw = localStorage.getItem(LEAD_DATA_STORAGE_KEY);
1568
+ if (!raw) return null;
1569
+ const item = JSON.parse(raw);
1570
+ if (Date.now() > (item.expiry ?? 0)) {
1571
+ localStorage.removeItem(LEAD_DATA_STORAGE_KEY);
1572
+ return null;
1573
+ }
1574
+ return {
1575
+ leadId: item.leadId,
1576
+ leadSessionId: item.leadSessionId,
1577
+ fingerprint: item.fingerprint,
1578
+ landingPageUrl: item.landingPageUrl,
1579
+ expiry: item.expiry
1580
+ };
1581
+ } catch {
1582
+ return null;
1583
+ }
1584
+ };
1585
+ var writeLeadData = (data) => {
1586
+ if (typeof localStorage === "undefined") return;
1587
+ try {
1588
+ localStorage.setItem(
1589
+ LEAD_DATA_STORAGE_KEY,
1590
+ JSON.stringify({ ...data, expiry: Date.now() + LEAD_DATA_TTL_MS })
1591
+ );
1592
+ } catch {
1593
+ }
1594
+ };
1595
+
1596
+ // src/core/identity/ensureIdentified.ts
1597
+ var ensureIdentified = async ({
1598
+ transport,
1599
+ environmentId,
1600
+ formId,
1601
+ pageUrl,
1602
+ source,
1603
+ preview = false
1604
+ }) => {
1605
+ if (preview) return null;
1606
+ if (typeof window === "undefined") return null;
1607
+ const cached = readLeadData();
1608
+ if (cached?.leadSessionId && cached?.fingerprint) {
1609
+ return cached;
1610
+ }
1611
+ const landingPageUrl = pageUrl ?? window.location.href;
1612
+ const fingerprint = await getBrowserFingerprint(environmentId);
1613
+ const result = await identifyLead(
1614
+ transport,
1615
+ buildIdentifyPayload({
1616
+ environmentId,
1617
+ fingerprintId: fingerprint.id,
1618
+ pageUrl: landingPageUrl,
1619
+ formId,
1620
+ source,
1621
+ leadId: cached?.leadId ?? null,
1622
+ sessionIdFromParams: cached?.leadSessionId ?? null
1623
+ })
1624
+ );
1625
+ if (result == null) return null;
1626
+ const leadData = {
1627
+ leadId: result.leadId,
1628
+ leadSessionId: result.sessionId,
1629
+ fingerprint: fingerprint.id,
1630
+ landingPageUrl
1631
+ };
1632
+ writeLeadData(leadData);
1633
+ return leadData;
1634
+ };
1635
+
1636
+ // src/core/identity/journey.ts
1637
+ var JOURNEY_COOKIE_NAME = "surface_journey_id";
1638
+ var RECENT_VISIT_COOKIE_NAME = "surface_recent_visit";
1639
+ var JOURNEY_COOKIE_MAX_AGE = 5184e3;
1640
+ var RECENT_VISIT_COOKIE_MAX_AGE = 86400;
1641
+ var TRACK_PATH2 = "/api/v1/lead/track";
1642
+ var getCookie = (name) => {
1643
+ if (typeof document === "undefined") return null;
1644
+ for (const cookie of document.cookie.split(";")) {
1645
+ const trimmed = cookie.trim();
1646
+ const eqIndex = trimmed.indexOf("=");
1647
+ if (eqIndex === -1) continue;
1648
+ if (trimmed.substring(0, eqIndex) !== name) continue;
1649
+ const value = trimmed.substring(eqIndex + 1);
1650
+ try {
1651
+ return decodeURIComponent(value);
1652
+ } catch {
1653
+ return value;
1654
+ }
1655
+ }
1656
+ return null;
1657
+ };
1658
+ var setCookie = (name, value, maxAge, domain) => {
1659
+ if (typeof document === "undefined") return;
1660
+ const domainAttr = domain ? `; domain=${domain}` : "";
1661
+ document.cookie = `${name}=${encodeURIComponent(
1662
+ value
1663
+ )}; path=/; max-age=${maxAge}; samesite=lax${domainAttr}`;
1664
+ };
1665
+ var getJourneyCookieDomain = () => {
1666
+ const hostname = typeof window === "undefined" ? "" : window.location?.hostname ?? "";
1667
+ if (!hostname || !hostname.includes(".")) return void 0;
1668
+ const parts = hostname.split(".");
1669
+ return "." + (parts.length === 2 ? hostname : parts.slice(1).join("."));
1670
+ };
1671
+ var createJourneyTracker = ({
1672
+ transport,
1673
+ environmentId
1674
+ }) => {
1675
+ let journeyId = getCookie(JOURNEY_COOKIE_NAME);
1676
+ const refreshJourneyCookie = () => {
1677
+ if (!journeyId) return;
1678
+ setCookie(JOURNEY_COOKIE_NAME, journeyId, JOURNEY_COOKIE_MAX_AGE, getJourneyCookieDomain());
1679
+ };
1680
+ const buildPayload = (url) => {
1681
+ const leadData = readLeadData();
1682
+ return {
1683
+ ...journeyId ? { id: journeyId } : {},
1684
+ data: {
1685
+ type: "page_view",
1686
+ payload: {
1687
+ url,
1688
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1689
+ referrer: typeof document === "undefined" ? "" : document.referrer || ""
1690
+ }
1691
+ },
1692
+ metadata: {
1693
+ environmentId,
1694
+ ...leadData ? {
1695
+ ...leadData.leadId ? { leadId: leadData.leadId } : {},
1696
+ ...leadData.leadSessionId ? { leadSessionId: leadData.leadSessionId } : {},
1697
+ fingerprint: leadData.fingerprint,
1698
+ ...leadData.landingPageUrl ? { landingPageUrl: leadData.landingPageUrl } : {}
1699
+ } : {}
1700
+ }
1701
+ };
1702
+ };
1703
+ const trackPageView = async (url) => {
1704
+ if (typeof window === "undefined") return;
1705
+ const pageUrl = url ?? window.location.href;
1706
+ if (getCookie(RECENT_VISIT_COOKIE_NAME) === pageUrl) return;
1707
+ const payload = buildPayload(pageUrl);
1708
+ if (journeyId && transport.beacon(TRACK_PATH2, payload)) {
1709
+ refreshJourneyCookie();
1710
+ } else {
1711
+ const result = await transport.post(TRACK_PATH2, payload);
1712
+ if (result.ok && result.data.data?.id) {
1713
+ journeyId = result.data.data.id;
1714
+ }
1715
+ refreshJourneyCookie();
1716
+ }
1717
+ setCookie(RECENT_VISIT_COOKIE_NAME, pageUrl, RECENT_VISIT_COOKIE_MAX_AGE, getJourneyCookieDomain());
1718
+ };
1719
+ const observeRouteChanges = () => {
1720
+ if (typeof window === "undefined") return () => void 0;
1721
+ let currentUrl = window.location.href;
1722
+ const notify = () => {
1723
+ const newUrl = window.location.href;
1724
+ if (newUrl === currentUrl) return;
1725
+ currentUrl = newUrl;
1726
+ void trackPageView(newUrl);
1727
+ };
1728
+ window.addEventListener("popstate", notify);
1729
+ const originalPushState = history.pushState;
1730
+ const originalReplaceState = history.replaceState;
1731
+ history.pushState = function(...args) {
1732
+ originalPushState.apply(history, args);
1733
+ setTimeout(notify, 0);
1734
+ };
1735
+ history.replaceState = function(...args) {
1736
+ originalReplaceState.apply(history, args);
1737
+ setTimeout(notify, 0);
1738
+ };
1739
+ return () => {
1740
+ window.removeEventListener("popstate", notify);
1741
+ history.pushState = originalPushState;
1742
+ history.replaceState = originalReplaceState;
1743
+ };
1744
+ };
1745
+ const clear = () => {
1746
+ const domain = getJourneyCookieDomain();
1747
+ const domainAttr = domain ? `; domain=${domain}` : "";
1748
+ if (typeof document === "undefined") return;
1749
+ document.cookie = `${JOURNEY_COOKIE_NAME}=; path=/; max-age=0; samesite=lax${domainAttr}`;
1750
+ document.cookie = `${RECENT_VISIT_COOKIE_NAME}=; path=/; max-age=0; samesite=lax${domainAttr}`;
1751
+ journeyId = null;
1752
+ };
1753
+ return { trackPageView, observeRouteChanges, clear };
1754
+ };
1755
+
1756
+ // src/enrichment/attachEnrichment.ts
1757
+ var ENRICHMENT_PATH = "/api/v1/services/enrichment/realtime";
1758
+ var ENRICHMENT_STATE_ID = "selfServeEnrichment";
1759
+ var DEBOUNCE_MS = 1e3;
1760
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
1761
+ var DOMAIN_PATTERN = /^(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+(?:\.[a-z0-9-]+)+)/i;
1762
+ var enrichmentInputOf = (value) => {
1763
+ const trimmed = value.trim();
1764
+ if (EMAIL_PATTERN.test(trimmed)) return { email: trimmed };
1765
+ const domain = trimmed.match(DOMAIN_PATTERN)?.[1];
1766
+ return domain ? { domain } : null;
1767
+ };
1768
+ var attachEnrichment = ({
1769
+ engine,
1770
+ transport,
1771
+ formId,
1772
+ sources
1773
+ }) => {
1774
+ const processed = /* @__PURE__ */ new Set();
1775
+ const inFlight = /* @__PURE__ */ new Set();
1776
+ let pendingTimer;
1777
+ let pendingValue;
1778
+ let detached = false;
1779
+ const fire = async (value) => {
1780
+ const input = enrichmentInputOf(value);
1781
+ if (!input) return;
1782
+ const key = input.email ?? input.domain;
1783
+ if (processed.has(key) || inFlight.has(key)) return;
1784
+ inFlight.add(key);
1785
+ try {
1786
+ const result = await transport.post(ENRICHMENT_PATH, {
1787
+ formId,
1788
+ ...input
1789
+ });
1790
+ if (detached || !result.ok) return;
1791
+ const data = result.data.data ?? {};
1792
+ if (Object.keys(data).length === 0) return;
1793
+ processed.add(key);
1794
+ const existing = engine.state().externalStates[ENRICHMENT_STATE_ID];
1795
+ const existingData = existing?.type === "APOLLO_EMAIL_ENRICHMENT" ? existing.data ?? {} : {};
1796
+ engine.setExternalState(ENRICHMENT_STATE_ID, {
1797
+ type: "APOLLO_EMAIL_ENRICHMENT",
1798
+ processed: true,
1799
+ // existing keys win: a later trigger (e.g. a website after the email)
1800
+ // only fills gaps, never overwrites resolved fields.
1801
+ data: { ...data, ...existingData }
1802
+ });
1803
+ } finally {
1804
+ inFlight.delete(key);
1805
+ }
1806
+ };
1807
+ const candidateValues = () => {
1808
+ const values = [];
1809
+ const flat = {};
1810
+ for (const stepAnswers of Object.values(engine.state().answers)) Object.assign(flat, stepAnswers);
1811
+ for (const { questionId, fieldNames } of sources) {
1812
+ const state = flat[questionId];
1813
+ if (!state) continue;
1814
+ for (const fieldName of fieldNames) {
1815
+ const value = state[fieldName];
1816
+ if (typeof value === "string" && value.trim().length > 0) values.push(value);
1817
+ }
1818
+ }
1819
+ return values;
1820
+ };
1821
+ const scheduleLatest = () => {
1822
+ const latest = candidateValues().find((value) => {
1823
+ const input = enrichmentInputOf(value);
1824
+ return input != null && !processed.has(input.email ?? input.domain);
1825
+ });
1826
+ if (latest === void 0 || latest === pendingValue) return;
1827
+ pendingValue = latest;
1828
+ clearTimeout(pendingTimer);
1829
+ pendingTimer = setTimeout(() => {
1830
+ pendingValue = void 0;
1831
+ void fire(latest);
1832
+ }, DEBOUNCE_MS);
1833
+ };
1834
+ const flushPending = () => {
1835
+ if (pendingValue === void 0) return;
1836
+ clearTimeout(pendingTimer);
1837
+ const value = pendingValue;
1838
+ pendingValue = void 0;
1839
+ void fire(value);
1840
+ };
1841
+ const teardowns = [
1842
+ engine.subscribe(scheduleLatest),
1843
+ engine.on("stepCompleted", flushPending),
1844
+ engine.on("completed", flushPending)
1845
+ ];
1846
+ return () => {
1847
+ detached = true;
1848
+ clearTimeout(pendingTimer);
1849
+ teardowns.forEach((teardown) => teardown());
1850
+ };
1851
+ };
1852
+
1853
+ // src/core/engine/answerAudit.ts
1854
+ var CHOICE_LIST_TYPES = /* @__PURE__ */ new Set(["MultipleOptionsQuestion", "MultipleChoiceQuestion", "MMChoiceQuestion"]);
1855
+ var isValidChoiceEntry = (entry, needsIndex) => typeof entry === "object" && entry !== null && typeof entry.key === "string" && typeof entry.value === "boolean" && (!needsIndex || typeof entry.index === "number");
1856
+ var auditAnswer = ({
1857
+ questions,
1858
+ questionId,
1859
+ merged
1860
+ }) => {
1861
+ const prefix = `Surface SDK [debug]: setAnswer("${questionId}")`;
1862
+ const expected = questions.size > 0 ? questions.get(questionId) : void 0;
1863
+ if (questions.size > 0 && !expected) {
1864
+ console.warn(`${prefix} \u2014 the record has no stateful question with this id; the answer will be dropped.`);
1865
+ console.debug(`Surface SDK [debug]: answer ${questionId}`, merged);
1866
+ return;
1867
+ }
1868
+ if (merged.type === void 0) {
1869
+ console.warn(
1870
+ `${prefix} \u2014 state has no "type" discriminator; the wire schema rejects untyped states. Include type: "${expected?.componentType ?? "<ComponentType>"}".`
1871
+ );
1872
+ } else if (expected?.componentType !== void 0 && merged.type !== expected.componentType) {
1873
+ console.warn(
1874
+ `${prefix} \u2014 state type "${String(merged.type)}" but the record's component is ${expected.componentType}.`
1875
+ );
1876
+ }
1877
+ const effectiveType = expected?.componentType ?? merged.type;
1878
+ if (typeof effectiveType === "string" && CHOICE_LIST_TYPES.has(effectiveType)) {
1879
+ const needsIndex = effectiveType === "MultipleOptionsQuestion";
1880
+ const malformed = !Array.isArray(merged.choices) || merged.choices.some((entry) => !isValidChoiceEntry(entry, needsIndex));
1881
+ if (malformed) {
1882
+ console.warn(
1883
+ `${prefix} \u2014 ${effectiveType} answers need the ENTIRE option list under "choices" as {key, value${needsIndex ? ", index" : ""}} objects, not ${JSON.stringify(merged.choices ?? merged)}. See get_sdk_binding_map's answerShape.`
1884
+ );
1885
+ }
1886
+ }
1887
+ console.debug(`Surface SDK [debug]: answer ${questionId}`, merged);
1888
+ };
1889
+
1890
+ // src/tracking/conversionRules.ts
1891
+ var DEFAULT_META_EVENT_NAME = "Lead";
1892
+ var DEFAULT_GA4_EVENT_NAME = "generate_lead";
1893
+ var DEFAULT_OPENAI_EVENT_NAME = "lead_created";
1894
+ var X_EVENT_ID_REGEX = /^tw-([a-z0-9]+)-([a-z0-9]+)$/i;
1895
+ var parseXPixelId = (eventId) => {
1896
+ const match = eventId?.trim().match(X_EVENT_ID_REGEX);
1897
+ return match ? match[1] : null;
1898
+ };
1899
+ var isNumericId = (value) => /^\d+$/.test(value?.trim() ?? "");
1900
+ var isRuleFireable = (rule) => {
1901
+ if (rule.enabled === false) return false;
1902
+ if ((rule.trigger ?? "reached_qualified") === "reached_step" && !rule.triggerStepId?.trim()) return false;
1903
+ switch (rule.provider) {
1904
+ case "x":
1905
+ return X_EVENT_ID_REGEX.test(rule.eventId?.trim() ?? "");
1906
+ case "meta":
1907
+ case "openai":
1908
+ return !!rule.pixelId?.trim();
1909
+ case "ga4":
1910
+ return !!rule.measurementId?.trim();
1911
+ case "linkedin":
1912
+ return isNumericId(rule.partnerId) && isNumericId(rule.conversionId);
1913
+ default:
1914
+ return false;
1915
+ }
1916
+ };
1917
+ var getParamValue = (params, name) => {
1918
+ const lowerName = name.toLowerCase();
1919
+ for (const [key, value] of Object.entries(params)) {
1920
+ if (key.toLowerCase() === lowerName) return value;
1921
+ }
1922
+ return void 0;
1923
+ };
1924
+ var matchesCondition = (condition, params) => {
1925
+ const value = getParamValue(params, condition.param)?.trim() ?? "";
1926
+ const values = condition.values ?? [];
1927
+ switch (condition.operator) {
1928
+ case "is_present":
1929
+ return value.length > 0;
1930
+ case "equals_any":
1931
+ return values.some((v) => v.trim().toLowerCase() === value.toLowerCase());
1932
+ case "contains":
1933
+ return value.length > 0 && values.some((v) => value.toLowerCase().includes(v.trim().toLowerCase()));
1934
+ default:
1935
+ return false;
1936
+ }
1937
+ };
1938
+ var matchesConditions = (conditions, params, operator = "and") => {
1939
+ if (conditions.length === 0) return true;
1940
+ return operator === "or" ? conditions.some((c) => matchesCondition(c, params)) : conditions.every((c) => matchesCondition(c, params));
1941
+ };
1942
+ var ruleToConversionEvent = (rule) => {
1943
+ if (!isRuleFireable(rule)) return null;
1944
+ switch (rule.provider) {
1945
+ case "x": {
1946
+ const pixelId = parseXPixelId(rule.eventId);
1947
+ if (!pixelId) return null;
1948
+ return { provider: "x", event: { event_id: rule.eventId.trim(), pixel_id: pixelId } };
1949
+ }
1950
+ case "meta":
1951
+ return {
1952
+ provider: "meta",
1953
+ event: {
1954
+ pixel_id: rule.pixelId.trim(),
1955
+ event_name: rule.eventName?.trim() || DEFAULT_META_EVENT_NAME
1956
+ }
1957
+ };
1958
+ case "openai":
1959
+ return {
1960
+ provider: "openai",
1961
+ event: {
1962
+ pixel_id: rule.pixelId.trim(),
1963
+ event_name: rule.eventName?.trim() || DEFAULT_OPENAI_EVENT_NAME
1964
+ }
1965
+ };
1966
+ case "ga4":
1967
+ return {
1968
+ provider: "ga4",
1969
+ event: {
1970
+ measurement_id: rule.measurementId.trim(),
1971
+ event_name: rule.eventName?.trim() || DEFAULT_GA4_EVENT_NAME
1972
+ }
1973
+ };
1974
+ case "linkedin":
1975
+ return {
1976
+ provider: "linkedin",
1977
+ event: { partner_id: rule.partnerId.trim(), conversion_id: rule.conversionId.trim() }
1978
+ };
1979
+ default:
1980
+ return null;
1981
+ }
1982
+ };
1983
+ var selectFiringEvents = (rules, trigger, params, stepId) => {
1984
+ const results = [];
1985
+ for (const rule of rules) {
1986
+ if ((rule.trigger ?? "reached_qualified") !== trigger) continue;
1987
+ if (trigger === "reached_step" && (!stepId || rule.triggerStepId !== stepId)) continue;
1988
+ if (!matchesConditions(rule.conditions ?? [], params, rule.conditionsOperator ?? "and")) continue;
1989
+ const conversion = ruleToConversionEvent(rule);
1990
+ if (conversion) results.push({ rule, conversion });
1991
+ }
1992
+ return results;
1993
+ };
1994
+ var ensureTwq = (win, pixelId) => {
1995
+ if (!win.twq) {
1996
+ const s = win.twq = function(...args) {
1997
+ s.exe ? s.exe.apply(s, args) : s.queue.push(args);
1998
+ };
1999
+ s.version = "1.1";
2000
+ s.queue = [];
2001
+ const el = win.document.createElement("script");
2002
+ el.async = true;
2003
+ el.src = "https://static.ads-twitter.com/uwt.js";
2004
+ win.document.head.appendChild(el);
2005
+ }
2006
+ win.twq("config", pixelId);
2007
+ };
2008
+ var ensureFbq = (win, pixelId) => {
2009
+ if (!win.fbq) {
2010
+ const n = win.fbq = function(...args) {
2011
+ n.callMethod ? n.callMethod.apply(n, args) : n.queue.push(args);
2012
+ };
2013
+ if (!win._fbq) win._fbq = n;
2014
+ n.push = n;
2015
+ n.loaded = true;
2016
+ n.version = "2.0";
2017
+ n.queue = [];
2018
+ const el = win.document.createElement("script");
2019
+ el.async = true;
2020
+ el.src = "https://connect.facebook.net/en_US/fbevents.js";
2021
+ win.document.head.appendChild(el);
2022
+ }
2023
+ win.fbq("init", pixelId);
2024
+ };
2025
+ var ensureOaiq = (win, pixelId) => {
2026
+ if (!win.oaiq) {
2027
+ const q = win.oaiq = function(...args) {
2028
+ q.q.push(args);
2029
+ };
2030
+ q.q = [];
2031
+ const el = win.document.createElement("script");
2032
+ el.async = true;
2033
+ el.src = "https://bzrcdn.openai.com/sdk/oaiq.min.js";
2034
+ win.document.head.appendChild(el);
2035
+ }
2036
+ win.oaiq("init", { pixelId });
2037
+ };
2038
+ var ensureGtag = (win, measurementId) => {
2039
+ if (!win.gtag) {
2040
+ win.dataLayer = win.dataLayer || [];
2041
+ win.gtag = function(...args) {
2042
+ win.dataLayer.push(args);
2043
+ };
2044
+ const el = win.document.createElement("script");
2045
+ el.async = true;
2046
+ el.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
2047
+ win.document.head.appendChild(el);
2048
+ win.gtag("js", /* @__PURE__ */ new Date());
2049
+ }
2050
+ win.gtag("config", measurementId, { send_page_view: false });
2051
+ };
2052
+ var ensureLintrk = (win, partnerId) => {
2053
+ if (!win.lintrk) {
2054
+ win._linkedin_partner_id = partnerId;
2055
+ const l = win.lintrk = function(a, b) {
2056
+ l.q.push([a, b]);
2057
+ };
2058
+ l.q = [];
2059
+ const el = win.document.createElement("script");
2060
+ el.async = true;
2061
+ el.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
2062
+ win.document.head.appendChild(el);
2063
+ }
2064
+ win._linkedin_data_partner_ids = win._linkedin_data_partner_ids || [];
2065
+ if (!win._linkedin_data_partner_ids.includes(partnerId)) {
2066
+ win._linkedin_data_partner_ids.push(partnerId);
2067
+ }
2068
+ };
2069
+ var fireConversion = (win, conversion, ctx) => {
2070
+ const w = win;
2071
+ try {
2072
+ switch (conversion.provider) {
2073
+ case "x": {
2074
+ ensureTwq(w, conversion.event.pixel_id);
2075
+ w.twq("event", conversion.event.event_id, {
2076
+ conversion_id: ctx.responseId,
2077
+ ...ctx.twclid ? { twclid: ctx.twclid } : {},
2078
+ ...ctx.email ? { email_address: ctx.email } : {}
2079
+ });
2080
+ return true;
2081
+ }
2082
+ case "meta": {
2083
+ ensureFbq(w, conversion.event.pixel_id);
2084
+ w.fbq("track", conversion.event.event_name, {}, { eventID: ctx.responseId });
2085
+ return true;
2086
+ }
2087
+ case "openai": {
2088
+ ensureOaiq(w, conversion.event.pixel_id);
2089
+ w.oaiq(
2090
+ "measure",
2091
+ conversion.event.event_name,
2092
+ { type: "customer_action" },
2093
+ { event_id: ctx.responseId }
2094
+ );
2095
+ return true;
2096
+ }
2097
+ case "ga4": {
2098
+ ensureGtag(w, conversion.event.measurement_id);
2099
+ w.gtag("event", conversion.event.event_name, {
2100
+ transaction_id: ctx.responseId,
2101
+ send_to: conversion.event.measurement_id
2102
+ });
2103
+ return true;
2104
+ }
2105
+ case "linkedin": {
2106
+ ensureLintrk(w, conversion.event.partner_id);
2107
+ w.lintrk("track", { conversion_id: conversion.event.conversion_id });
2108
+ return true;
2109
+ }
2110
+ default:
2111
+ return false;
2112
+ }
2113
+ } catch {
2114
+ return false;
2115
+ }
2116
+ };
2117
+
2118
+ // src/tracking/vendors.ts
2119
+ var scriptPresent = (win, src) => win.document.querySelector(`script[src="${src}"]`) != null;
2120
+ var injectScriptOnce = (win, src, parent, id) => {
2121
+ if (scriptPresent(win, src)) return;
2122
+ const el = win.document.createElement("script");
2123
+ el.async = true;
2124
+ el.src = src;
2125
+ if (id) el.id = id;
2126
+ win.document[parent].appendChild(el);
2127
+ };
2128
+ var initGtm = (win, gtmId) => {
2129
+ const src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(gtmId)}`;
2130
+ if (scriptPresent(win, src)) return;
2131
+ win.dataLayer = win.dataLayer || [];
2132
+ win.dataLayer.push({ "gtm.start": (/* @__PURE__ */ new Date()).getTime(), event: "gtm.js" });
2133
+ injectScriptOnce(win, src, "head");
2134
+ };
2135
+ var initGa4 = (win, measurementId) => {
2136
+ const src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
2137
+ if (scriptPresent(win, src)) return;
2138
+ win.dataLayer = win.dataLayer || [];
2139
+ if (!win.gtag) {
2140
+ win.gtag = function() {
2141
+ win.dataLayer.push(arguments);
2142
+ };
2143
+ win.gtag("js", /* @__PURE__ */ new Date());
2144
+ }
2145
+ injectScriptOnce(win, src, "head");
2146
+ const isEmbeddedInIframe = win.self !== win.top;
2147
+ win.gtag("config", measurementId, {
2148
+ ...isEmbeddedInIframe && { cookie_flags: "SameSite=None;Secure;Partitioned" }
2149
+ });
2150
+ };
2151
+ var initMetaPixel = (win, pixelId) => {
2152
+ if (!win.fbq) {
2153
+ const n = win.fbq = function(...args) {
2154
+ n.callMethod ? n.callMethod.apply(n, args) : n.queue.push(args);
2155
+ };
2156
+ if (!win._fbq) win._fbq = n;
2157
+ n.push = n;
2158
+ n.loaded = true;
2159
+ n.version = "2.0";
2160
+ n.queue = [];
2161
+ injectScriptOnce(win, "https://connect.facebook.net/en_US/fbevents.js", "head");
2162
+ }
2163
+ win.fbq("init", pixelId);
2164
+ };
2165
+ var initHubspot = (win, trackingId) => {
2166
+ injectScriptOnce(win, `https://js.hs-scripts.com/${trackingId}.js`, "body", "hs-script-loader");
2167
+ };
2168
+ var createVendorTracker = (win, settings) => {
2169
+ const w = win;
2170
+ if (settings.gtmId) initGtm(w, settings.gtmId);
2171
+ if (settings.gaMeasurementId) initGa4(w, settings.gaMeasurementId);
2172
+ if (settings.metaPixelId) initMetaPixel(w, settings.metaPixelId);
2173
+ if (settings.hubspotTracking?.trackingId) initHubspot(w, settings.hubspotTracking.trackingId);
2174
+ const dataLayerPush = (event) => {
2175
+ if (!settings.gtmId) return;
2176
+ w.dataLayer = w.dataLayer || [];
2177
+ w.dataLayer.push(event);
2178
+ };
2179
+ const gtagEvent = (name, params) => {
2180
+ if (!settings.gaMeasurementId) return;
2181
+ w.gtag?.("event", name, params);
2182
+ };
2183
+ const fbqTrackCustom = (name, data) => {
2184
+ if (!settings.metaPixelId) return;
2185
+ w.fbq?.("trackCustom", name, data);
2186
+ };
2187
+ return {
2188
+ formStarted: (formId, urlData) => {
2189
+ dataLayerPush({ event: "SurfaceFormStarted", formId, ...urlData });
2190
+ fbqTrackCustom("SurfaceFormStarted", { formId, ...urlData });
2191
+ gtagEvent("SurfaceFormStarted", { formId, ...urlData });
2192
+ },
2193
+ stepSubmitted: (payload) => {
2194
+ dataLayerPush({ event: "SurfaceFormStepSubmitted", ...payload });
2195
+ fbqTrackCustom("SurfaceFormStepSubmitted", payload);
2196
+ gtagEvent("SurfaceFormStepSubmitted", payload);
2197
+ },
2198
+ formCompleted: (formId, urlData) => {
2199
+ dataLayerPush({ event: "SurfaceFormSubmitEvent", formId, ...urlData });
2200
+ fbqTrackCustom("SurfaceFormCompleted", { formId, ...urlData });
2201
+ gtagEvent("SurfaceFormCompleted", { formId, ...urlData });
2202
+ }
2203
+ };
2204
+ };
2205
+
2206
+ // src/tracking/attachTracking.ts
2207
+ var parseLocationParams = (win) => {
2208
+ try {
2209
+ return Object.fromEntries(new URLSearchParams(win.location.search).entries());
2210
+ } catch {
2211
+ return {};
2212
+ }
2213
+ };
2214
+ var getParam = (params, name) => {
2215
+ const lower = name.toLowerCase();
2216
+ for (const [key, value] of Object.entries(params)) {
2217
+ if (key.toLowerCase() === lower) return value?.trim() || void 0;
2218
+ }
2219
+ return void 0;
2220
+ };
2221
+ var pickClickIds = (params) => ({
2222
+ twclid: getParam(params, "twclid"),
2223
+ fbclid: getParam(params, "fbclid"),
2224
+ gclid: getParam(params, "gclid"),
2225
+ li_fat_id: getParam(params, "li_fat_id")
2226
+ });
2227
+ var EMAIL_ANSWER_KEYS = ["email", "emailAddress", "workEmailAddress"];
2228
+ var findEmail = (answers) => {
2229
+ for (const state of Object.values(answers)) {
2230
+ for (const key of EMAIL_ANSWER_KEYS) {
2231
+ const value = state[key];
2232
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
2233
+ }
2234
+ }
2235
+ return void 0;
2236
+ };
2237
+ var hasBookedMeeting = (answers) => Object.values(answers).some((state) => state.eventScheduled === true);
2238
+ var flatAnswers = (engine) => {
2239
+ const flat = {};
2240
+ for (const stepAnswers of Object.values(engine.state().answers)) {
2241
+ Object.assign(flat, stepAnswers);
2242
+ }
2243
+ return flat;
2244
+ };
2245
+ var attachTracking = ({ engine, formId, settings, win }) => {
2246
+ const w = win ?? window;
2247
+ const tracker = createVendorTracker(w, settings);
2248
+ const rules = settings.adsConversions ?? [];
2249
+ const params = parseLocationParams(w);
2250
+ const urlData = { url: w.location.href, urlParams: params };
2251
+ const firedRuleIds = /* @__PURE__ */ new Set();
2252
+ const processedStepIds = /* @__PURE__ */ new Set();
2253
+ const pendingStepIds = /* @__PURE__ */ new Set();
2254
+ const pendingEventTriggers = /* @__PURE__ */ new Set();
2255
+ let pendingTerminal = false;
2256
+ let firedTerminal = false;
2257
+ let firedStepSubmitted = false;
2258
+ let firedEmail = false;
2259
+ let firedMeeting = false;
2260
+ const freshSession = engine.state().responseId == null;
2261
+ let firedStarted = !freshSession;
2262
+ const fire = (selected, responseId) => {
2263
+ const fresh = selected.filter(({ rule }) => !firedRuleIds.has(rule.id));
2264
+ if (fresh.length === 0) return;
2265
+ fresh.forEach(({ rule }) => firedRuleIds.add(rule.id));
2266
+ const ctx = { responseId, email: findEmail(flatAnswers(engine)), ...pickClickIds(params) };
2267
+ fresh.forEach(({ conversion }) => fireConversion(w, conversion, ctx));
2268
+ };
2269
+ const fireTrigger = (trigger, responseId) => {
2270
+ if (!responseId) {
2271
+ pendingEventTriggers.add(trigger);
2272
+ return;
2273
+ }
2274
+ fire(selectFiringEvents(rules, trigger, params), responseId);
2275
+ };
2276
+ const fireStep = (stepId, responseId) => {
2277
+ if (!responseId) {
2278
+ pendingStepIds.add(stepId);
2279
+ return;
2280
+ }
2281
+ fire(selectFiringEvents(rules, "reached_step", params, stepId), responseId);
2282
+ };
2283
+ const fireTerminal = (responseId) => {
2284
+ if (!responseId) {
2285
+ pendingTerminal = true;
2286
+ return;
2287
+ }
2288
+ firedTerminal = true;
2289
+ pendingTerminal = false;
2290
+ const byRule = /* @__PURE__ */ new Map();
2291
+ for (const trigger of ["reached_qualified", "form_submitted"]) {
2292
+ for (const hit of selectFiringEvents(rules, trigger, params)) {
2293
+ byRule.set(hit.rule.id, hit);
2294
+ }
2295
+ }
2296
+ fire(Array.from(byRule.values()), responseId);
2297
+ };
2298
+ const detectAnswerMoments = () => {
2299
+ const answers = flatAnswers(engine);
2300
+ if (!firedEmail && freshSession && findEmail(answers) !== void 0) {
2301
+ firedEmail = true;
2302
+ fireTrigger("email_provided", engine.state().responseId);
2303
+ }
2304
+ if (!firedMeeting && freshSession && hasBookedMeeting(answers)) {
2305
+ firedMeeting = true;
2306
+ fireTrigger("meeting_booked", engine.state().responseId);
2307
+ }
2308
+ };
2309
+ const teardowns = [];
2310
+ teardowns.push(
2311
+ engine.on("started", () => {
2312
+ tracker.formStarted(formId, urlData);
2313
+ })
2314
+ );
2315
+ let pendingStepSubmitted;
2316
+ teardowns.push(
2317
+ engine.on("stepCompleted", ({ stepId }) => {
2318
+ const answers = flatAnswers(engine);
2319
+ pendingStepSubmitted = {
2320
+ stepId,
2321
+ formId,
2322
+ emailProvided: findEmail(answers) !== void 0,
2323
+ meetingBooked: hasBookedMeeting(answers),
2324
+ ...urlData
2325
+ };
2326
+ if (!firedStepSubmitted && freshSession) {
2327
+ firedStepSubmitted = true;
2328
+ fireTrigger("step_submitted", engine.state().responseId);
2329
+ }
2330
+ })
2331
+ );
2332
+ teardowns.push(
2333
+ engine.on("stepChanged", ({ toStepId }) => {
2334
+ if (pendingStepSubmitted) {
2335
+ if (!engine.state().finished) tracker.stepSubmitted(pendingStepSubmitted);
2336
+ pendingStepSubmitted = void 0;
2337
+ }
2338
+ if (processedStepIds.has(toStepId)) return;
2339
+ processedStepIds.add(toStepId);
2340
+ fireStep(toStepId, engine.state().responseId);
2341
+ })
2342
+ );
2343
+ teardowns.push(
2344
+ engine.on("completed", () => {
2345
+ pendingStepSubmitted = void 0;
2346
+ tracker.formCompleted(formId, urlData);
2347
+ if (!firedTerminal) fireTerminal(engine.state().responseId);
2348
+ })
2349
+ );
2350
+ teardowns.push(
2351
+ engine.on("saved", ({ responseId }) => {
2352
+ if (!firedStarted) {
2353
+ firedStarted = true;
2354
+ fireTrigger("form_started", responseId);
2355
+ }
2356
+ if (pendingTerminal) fireTerminal(responseId);
2357
+ if (pendingStepIds.size > 0) {
2358
+ const stepIds = Array.from(pendingStepIds);
2359
+ pendingStepIds.clear();
2360
+ stepIds.forEach((stepId) => fireStep(stepId, responseId));
2361
+ }
2362
+ if (pendingEventTriggers.size > 0) {
2363
+ const triggers = Array.from(pendingEventTriggers);
2364
+ pendingEventTriggers.clear();
2365
+ triggers.forEach((trigger) => fireTrigger(trigger, responseId));
2366
+ }
2367
+ })
2368
+ );
2369
+ teardowns.push(engine.subscribe(detectAnswerMoments));
2370
+ const initialStepId = engine.state().currentStepId;
2371
+ processedStepIds.add(initialStepId);
2372
+ fireStep(initialStepId, engine.state().responseId);
2373
+ detectAnswerMoments();
2374
+ return () => teardowns.forEach((teardown) => teardown());
2375
+ };
2376
+
2377
+ // src/validation/attachEmailValidation.ts
2378
+ var DEBOUNCE_MS2 = 300;
2379
+ var EMAIL_ANSWER_KEYS2 = ["email", "emailAddress", "workEmailAddress"];
2380
+ var EMAIL_PATTERN2 = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
2381
+ var attachEmailValidation = ({ engine, validator }) => {
2382
+ const requested = /* @__PURE__ */ new Set();
2383
+ const results = /* @__PURE__ */ new Map();
2384
+ let pendingTimer;
2385
+ let pendingScheduled = false;
2386
+ let lastMetaKey;
2387
+ let detached = false;
2388
+ const candidateEmails = () => {
2389
+ const emails = [];
2390
+ for (const stepAnswers of Object.values(engine.state().answers)) {
2391
+ for (const state of Object.values(stepAnswers)) {
2392
+ for (const key of EMAIL_ANSWER_KEYS2) {
2393
+ const value = state[key];
2394
+ if (typeof value === "string" && EMAIL_PATTERN2.test(value.trim())) emails.push(value.trim());
2395
+ }
2396
+ }
2397
+ }
2398
+ return emails;
2399
+ };
2400
+ const writeMeta = () => {
2401
+ const current = new Set(candidateEmails());
2402
+ const verdicts = [...results.values()].filter((verdict) => current.has(verdict.email));
2403
+ if (verdicts.length === 0) return;
2404
+ const metaKey = JSON.stringify(verdicts.map((v) => [v.email, v.isValid, v.failed === true]));
2405
+ if (metaKey === lastMetaKey) return;
2406
+ lastMetaKey = metaKey;
2407
+ const detailed = verdicts.find((verdict) => verdict.status && verdict.reason);
2408
+ engine.mergeMeta({
2409
+ isEmailValid: verdicts.some((verdict) => verdict.isValid),
2410
+ emailValidation: detailed ? {
2411
+ status: detailed.status,
2412
+ reason: detailed.reason,
2413
+ ...detailed.blacklistEntryIds?.length && { blacklistEntryIds: detailed.blacklistEntryIds }
2414
+ } : void 0,
2415
+ emailValidationFailed: verdicts.some((verdict) => verdict.failed === true)
2416
+ });
2417
+ };
2418
+ const fire = async () => {
2419
+ const fresh = candidateEmails().filter((email) => !requested.has(email));
2420
+ if (fresh.length === 0) return;
2421
+ fresh.forEach((email) => requested.add(email));
2422
+ const verdicts = await validator.validateBatch(fresh);
2423
+ if (detached) return;
2424
+ verdicts.forEach((verdict) => results.set(verdict.email, verdict));
2425
+ writeMeta();
2426
+ };
2427
+ const scheduleLatest = () => {
2428
+ if (!candidateEmails().some((email) => !requested.has(email))) {
2429
+ writeMeta();
2430
+ return;
2431
+ }
2432
+ pendingScheduled = true;
2433
+ clearTimeout(pendingTimer);
2434
+ pendingTimer = setTimeout(() => {
2435
+ pendingScheduled = false;
2436
+ void fire();
2437
+ }, DEBOUNCE_MS2);
2438
+ };
2439
+ const flushPending = () => {
2440
+ if (!pendingScheduled) return;
2441
+ pendingScheduled = false;
2442
+ clearTimeout(pendingTimer);
2443
+ void fire();
2444
+ };
2445
+ const teardowns = [
2446
+ engine.subscribe(scheduleLatest),
2447
+ engine.on("stepCompleted", flushPending),
2448
+ engine.on("completed", flushPending)
2449
+ ];
2450
+ return () => {
2451
+ detached = true;
2452
+ clearTimeout(pendingTimer);
2453
+ teardowns.forEach((teardown) => teardown());
2454
+ };
2455
+ };
2456
+
2457
+ // src/surfaceForms.ts
2458
+ var RESERVED_META_KEYS = /* @__PURE__ */ new Set([
2459
+ "surfaceTagStatus",
2460
+ "isEmailValid",
2461
+ "emailValidation",
2462
+ "emailValidationFailed"
2463
+ ]);
2464
+ var SurfaceForms = {
2465
+ init: async ({
2466
+ environmentId,
2467
+ formId,
2468
+ steps,
2469
+ settings = {},
2470
+ apiBaseUrl,
2471
+ container,
2472
+ expectedQuestionIds,
2473
+ resolveNextStep,
2474
+ urlDataStateId,
2475
+ source,
2476
+ journey = false,
2477
+ emailValidation = false,
2478
+ preview = false,
2479
+ previewToken,
2480
+ debug = false,
2481
+ resume,
2482
+ meta
2483
+ }) => {
2484
+ const transport = createTransport({ apiBaseUrl });
2485
+ let runtimeConfig;
2486
+ if (!steps) {
2487
+ const fetched = await fetchRuntimeConfig(transport, formId, { preview, previewToken });
2488
+ if (!fetched.ok || !fetched.data.data) {
2489
+ const baseUrl = (apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
2490
+ const detail = !fetched.ok ? fetched.error.kind === "http" ? `HTTP ${fetched.error.status}` : "network error" : "empty response";
2491
+ throw new Error(
2492
+ `SurfaceForms.init: could not fetch runtime config for form "${formId}" (${detail} from ${baseUrl}/api/v1/public/forms/${formId}/runtime-config). If this environment is not served from ${DEFAULT_API_BASE_URL}, pass apiBaseUrl \u2014 the get_sdk_binding_map MCP tool returns the correct value.`
2493
+ );
2494
+ }
2495
+ runtimeConfig = fetched.data.data;
2496
+ steps = stepsOf(runtimeConfig.config);
2497
+ expectedQuestionIds = expectedQuestionIds ?? statefulQuestionsOf(runtimeConfig.config).map((q) => q.questionId);
2498
+ const fetchedSettings = runtimeConfig.settings ?? {};
2499
+ settings = {
2500
+ trackPartialResponses: fetchedSettings.trackPartialResponses,
2501
+ trackCookies: fetchedSettings.trackCookie,
2502
+ gtmId: fetchedSettings.gtmId,
2503
+ gaMeasurementId: fetchedSettings.gaMeasurementId,
2504
+ metaPixelId: fetchedSettings.metaPixelId,
2505
+ hubspotTracking: fetchedSettings.hubspotTracking,
2506
+ adsConversions: fetchedSettings.adsConversions,
2507
+ ...settings
2508
+ };
2509
+ }
2510
+ const trackCookies = settings.trackCookies ?? false;
2511
+ let engineResume;
2512
+ if (resume && !preview) {
2513
+ const stored = await fetchStoredResponse(transport, resume);
2514
+ if (stored.ok) {
2515
+ engineResume = {
2516
+ responseId: resume.responseId,
2517
+ answers: Object.fromEntries(stored.data.answers.map((row) => [row.questionId, row.response]))
2518
+ };
2519
+ } else {
2520
+ console.warn(
2521
+ `Surface SDK: resume fetch for response "${resume.responseId}" failed \u2014 starting a fresh session.`
2522
+ );
2523
+ }
2524
+ }
2525
+ let identity = await ensureIdentified({ transport, environmentId, formId, source, preview });
2526
+ const pushEvents = createPushEventClient({ transport, preview });
2527
+ const formJourney = createFormJourneyTracker({ transport, preview });
2528
+ const engine = createFormEngine({
2529
+ formId,
2530
+ environmentId,
2531
+ steps,
2532
+ transport,
2533
+ resolveNextStep,
2534
+ trackPartialResponses: settings.trackPartialResponses ?? true,
2535
+ trackCookies,
2536
+ externalStates: urlDataStateId ? { [urlDataStateId]: buildUrlData({ trackCookies }) } : {},
2537
+ // Native forms report tag detection here; for the SDK the answer is static —
2538
+ // the SDK IS the runtime — and it keys the responses-table "SDK" badge, so
2539
+ // callers cannot override it (Decisions #8).
2540
+ meta: { ...meta, surfaceTagStatus: "sdk" },
2541
+ identity: () => ({
2542
+ fingerprint: identity?.fingerprint,
2543
+ leadSessionId: identity?.leadSessionId ?? void 0,
2544
+ leadId: identity?.leadId ?? void 0
2545
+ }),
2546
+ pushEvents,
2547
+ formJourney,
2548
+ preview,
2549
+ resume: engineResume
2550
+ });
2551
+ const teardowns = [];
2552
+ let auditedEngine = engine;
2553
+ if (debug) {
2554
+ const questions = new Map(
2555
+ (runtimeConfig ? statefulQuestionsOf(runtimeConfig.config) : []).map((q) => [q.questionId, q])
2556
+ );
2557
+ auditedEngine = {
2558
+ ...engine,
2559
+ setAnswer: (questionId, state) => {
2560
+ const stepId = engine.state().currentStepId;
2561
+ engine.setAnswer(questionId, state);
2562
+ const merged = engine.state().answers[stepId]?.[questionId] ?? state;
2563
+ auditAnswer({ questions, questionId, merged });
2564
+ }
2565
+ };
2566
+ for (const event of [
2567
+ "viewed",
2568
+ "started",
2569
+ "stepChanged",
2570
+ "stepCompleted",
2571
+ "completed",
2572
+ "disqualified",
2573
+ "saved",
2574
+ "error"
2575
+ ]) {
2576
+ teardowns.push(
2577
+ engine.on(event, (payload) => console.debug(`Surface SDK [debug]: ${event}`, payload))
2578
+ );
2579
+ }
2580
+ }
2581
+ let started = false;
2582
+ teardowns.push(
2583
+ engine.on("started", () => {
2584
+ started = true;
2585
+ })
2586
+ );
2587
+ teardowns.push(
2588
+ observeDropoff(pushEvents, () => ({
2589
+ started,
2590
+ formId,
2591
+ stepId: engine.state().currentStepId,
2592
+ sessionId: identity?.leadSessionId ?? void 0
2593
+ }))
2594
+ );
2595
+ if (container) {
2596
+ teardowns.push(bindForm({ engine: auditedEngine, root: container, expectedQuestionIds }));
2597
+ }
2598
+ const hasTrackingConfig = !!settings.gtmId || !!settings.gaMeasurementId || !!settings.metaPixelId || !!settings.hubspotTracking?.trackingId || (settings.adsConversions?.length ?? 0) > 0;
2599
+ if (hasTrackingConfig && !preview && typeof window !== "undefined") {
2600
+ teardowns.push(attachTracking({ engine, formId, settings }));
2601
+ }
2602
+ const enrichmentSources = runtimeConfig ? enrichmentSourcesOf(runtimeConfig.config) : [];
2603
+ const enrichmentActive = runtimeConfig != null && enrichmentSources.length > 0 && hasEnabledEnrichmentProvider(runtimeConfig.settings);
2604
+ if (enrichmentActive && !preview) {
2605
+ teardowns.push(attachEnrichment({ engine, transport, formId, sources: enrichmentSources }));
2606
+ } else if (debug && runtimeConfig && !enrichmentActive && hasDeclaredEnrichmentSource(runtimeConfig.config)) {
2607
+ console.warn(
2608
+ "Surface SDK: the record declares an enrichmentSourceField, but this environment has no enabled enrichment provider \u2014 no enrichment will run (capabilities.enrichment is false)."
2609
+ );
2610
+ }
2611
+ const emailValidator = createEmailValidator({ transport, environmentId, preview });
2612
+ if (emailValidation && !preview) {
2613
+ teardowns.push(attachEmailValidation({ engine, validator: emailValidator }));
2614
+ }
2615
+ let journeyTracker;
2616
+ if (journey && !preview) {
2617
+ journeyTracker = createJourneyTracker({ transport, environmentId });
2618
+ void journeyTracker.trackPageView();
2619
+ teardowns.push(journeyTracker.observeRouteChanges());
2620
+ }
2621
+ return {
2622
+ setAnswer: auditedEngine.setAnswer,
2623
+ setExternalState: engine.setExternalState,
2624
+ next: engine.next,
2625
+ back: engine.back,
2626
+ goToStep: engine.goToStep,
2627
+ submit: engine.submit,
2628
+ disqualify: engine.disqualify,
2629
+ savePartial: engine.savePartial,
2630
+ flush: engine.flush,
2631
+ markViewed: engine.markViewed,
2632
+ mergeMeta: (partial) => {
2633
+ const entries = Object.entries(partial);
2634
+ const reserved = entries.filter(([key]) => RESERVED_META_KEYS.has(key));
2635
+ if (reserved.length > 0) {
2636
+ console.warn(
2637
+ `Surface SDK: mergeMeta dropped reserved key(s) ${reserved.map(([key]) => key).join(", ")} \u2014 these are SDK-owned response markers.`
2638
+ );
2639
+ }
2640
+ engine.mergeMeta(Object.fromEntries(entries.filter(([key]) => !RESERVED_META_KEYS.has(key))));
2641
+ },
2642
+ capabilities: {
2643
+ enrichment: enrichmentActive,
2644
+ emailValidation,
2645
+ tracking: hasTrackingConfig
2646
+ },
2647
+ on: engine.on,
2648
+ state: engine.state,
2649
+ subscribe: engine.subscribe,
2650
+ identify: async () => {
2651
+ const fresh = await ensureIdentified({ transport, environmentId, formId, source, preview });
2652
+ if (fresh) identity = fresh;
2653
+ return fresh;
2654
+ },
2655
+ validateEmail: emailValidator.validate,
2656
+ runtimeConfig,
2657
+ destroy: () => teardowns.forEach((teardown) => teardown())
2658
+ };
2659
+ }
2660
+ };
2661
+ export {
2662
+ DEFAULT_API_BASE_URL,
2663
+ DEFAULT_REDIRECT_TIMEOUT_MS,
2664
+ ENRICHMENT_STATE_ID,
2665
+ IDENTITY_INFO_ANSWER_KEYS,
2666
+ JOURNEY_COOKIE_NAME,
2667
+ LEAD_DATA_STORAGE_KEY,
2668
+ LEAD_DATA_TTL_MS,
2669
+ RECENT_VISIT_COOKIE_NAME,
2670
+ SurfaceForms,
2671
+ attachEmailValidation,
2672
+ attachEnrichment,
2673
+ bindForm,
2674
+ buildIdentifyPayload,
2675
+ buildRedirectUrl,
2676
+ buildResponsesArray,
2677
+ buildStepTransitionEventBatch,
2678
+ buildUrlData,
2679
+ constructUrlParamsObject,
2680
+ createEmailValidator,
2681
+ createEmitter,
2682
+ createFormEngine,
2683
+ createFormJourneyTracker,
2684
+ createJourneyTracker,
2685
+ createPushEventClient,
2686
+ createSchedulerClient,
2687
+ createStartedDetector,
2688
+ createSubmitResponsePayload,
2689
+ createTransport,
2690
+ createUpdateResponsePayload,
2691
+ enrichmentSourcesOf,
2692
+ ensureIdentified,
2693
+ fetchRuntimeConfig,
2694
+ getBrowserFingerprint,
2695
+ getHash,
2696
+ hasEnabledEnrichmentProvider,
2697
+ identifyLead,
2698
+ mountBookingWidget,
2699
+ observeDropoff,
2700
+ observeFormView,
2701
+ parseCookiesObject,
2702
+ parsePrefillParams,
2703
+ parseQuestionId,
2704
+ performRedirect,
2705
+ readLeadData,
2706
+ statefulQuestionIdsOf,
2707
+ statefulQuestionsOf,
2708
+ stepsOf,
2709
+ submitResponse,
2710
+ toSchedulerAnswerState,
2711
+ updateResponse,
2712
+ writeLeadData
2713
+ };
2714
+ //# sourceMappingURL=index.mjs.map