@enricai/barnacle 1.3.1 → 1.4.2

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 (140) hide show
  1. package/README.md +35 -1
  2. package/dist/api/errors.d.ts.map +1 -1
  3. package/dist/api/errors.js.map +1 -1
  4. package/dist/api/helpers/envelope.js.map +1 -1
  5. package/dist/api/helpers/reply.js.map +1 -1
  6. package/dist/api/plugins/auth.d.ts.map +1 -1
  7. package/dist/api/plugins/auth.js.map +1 -1
  8. package/dist/api/plugins/error-handler.d.ts.map +1 -1
  9. package/dist/api/plugins/error-handler.js.map +1 -1
  10. package/dist/api/plugins/request-context.d.ts.map +1 -1
  11. package/dist/api/plugins/request-context.js.map +1 -1
  12. package/dist/api/routes/health.js.map +1 -1
  13. package/dist/api/routes/plugins-introspection.js.map +1 -1
  14. package/dist/api/schemas/common.d.ts +12 -12
  15. package/dist/api/schemas/common.d.ts.map +1 -1
  16. package/dist/api/schemas/common.js.map +1 -1
  17. package/dist/cache/keyed-ttl-cache.js.map +1 -1
  18. package/dist/cache/response-cache.js.map +1 -1
  19. package/dist/config.js.map +1 -1
  20. package/dist/lib/applicant-payload.d.ts +3 -3
  21. package/dist/lib/bedrock.js.map +1 -1
  22. package/dist/lib/case-insensitive-headers.js.map +1 -1
  23. package/dist/lib/chromium-client-hints.js.map +1 -1
  24. package/dist/lib/cookie-value.js.map +1 -1
  25. package/dist/lib/datadog.js.map +1 -1
  26. package/dist/lib/dd-metrics.js.map +1 -1
  27. package/dist/lib/dispatch-metrics.d.ts.map +1 -1
  28. package/dist/lib/dispatch-metrics.js.map +1 -1
  29. package/dist/lib/env.js.map +1 -1
  30. package/dist/lib/errors.js.map +1 -1
  31. package/dist/lib/http.js.map +1 -1
  32. package/dist/lib/llm/anthropic-client.js.map +1 -1
  33. package/dist/lib/llm/judge.js.map +1 -1
  34. package/dist/lib/llm/judges/error-messages.js.map +1 -1
  35. package/dist/lib/llm/judges/invalid-fields.js.map +1 -1
  36. package/dist/lib/llm/judges/modal-priority.js.map +1 -1
  37. package/dist/lib/llm/judges/select-option.js.map +1 -1
  38. package/dist/lib/llm/judges/verify-submit.js.map +1 -1
  39. package/dist/lib/llm/schemas.d.ts +7 -7
  40. package/dist/lib/llm/schemas.js.map +1 -1
  41. package/dist/lib/logging.js.map +1 -1
  42. package/dist/lib/multipart.js.map +1 -1
  43. package/dist/lib/option-matcher.js.map +1 -1
  44. package/dist/lib/phone.js.map +1 -1
  45. package/dist/lib/random.js.map +1 -1
  46. package/dist/lib/statsd.js.map +1 -1
  47. package/dist/lib/telemetry/call-capture.d.ts +6 -6
  48. package/dist/lib/telemetry/call-capture.js.map +1 -1
  49. package/dist/lib/telemetry/run-state.js.map +1 -1
  50. package/dist/lib/telemetry/s3-sink.js.map +1 -1
  51. package/dist/lib/telemetry/submission-capture.js.map +1 -1
  52. package/dist/lib/telemetry/telemetry-paths.js.map +1 -1
  53. package/dist/lib/tracking-click.d.ts.map +1 -1
  54. package/dist/lib/tracking-click.js +5 -1
  55. package/dist/lib/tracking-click.js.map +1 -1
  56. package/dist/lib/url-capture.js.map +1 -1
  57. package/dist/lib/us-states.js.map +1 -1
  58. package/dist/lib/zod-multipart.js.map +1 -1
  59. package/dist/plugins/config-plugin.d.ts +1 -0
  60. package/dist/plugins/config-plugin.d.ts.map +1 -1
  61. package/dist/plugins/config-plugin.js +4 -0
  62. package/dist/plugins/config-plugin.js.map +1 -1
  63. package/dist/plugins/discover.d.ts.map +1 -1
  64. package/dist/plugins/discover.js +1 -0
  65. package/dist/plugins/discover.js.map +1 -1
  66. package/dist/plugins/json-schema-to-zod.d.ts.map +1 -1
  67. package/dist/plugins/json-schema-to-zod.js.map +1 -1
  68. package/dist/plugins/loader.d.ts.map +1 -1
  69. package/dist/plugins/loader.js +12 -2
  70. package/dist/plugins/loader.js.map +1 -1
  71. package/dist/recon/load-vocabulary.d.ts +24 -0
  72. package/dist/recon/load-vocabulary.d.ts.map +1 -0
  73. package/dist/recon/load-vocabulary.js +71 -0
  74. package/dist/recon/load-vocabulary.js.map +1 -0
  75. package/dist/recon/vocabulary.d.ts +62 -0
  76. package/dist/recon/vocabulary.d.ts.map +1 -0
  77. package/dist/recon/vocabulary.js +34 -0
  78. package/dist/recon/vocabulary.js.map +1 -0
  79. package/dist/scraper/behavioral-signals.js.map +1 -1
  80. package/dist/scraper/errors.d.ts.map +1 -1
  81. package/dist/scraper/errors.js.map +1 -1
  82. package/dist/scraper/fixtures.js.map +1 -1
  83. package/dist/scraper/flow-runner.js.map +1 -1
  84. package/dist/scraper/graphql-client.js.map +1 -1
  85. package/dist/scraper/http-client.d.ts +41 -0
  86. package/dist/scraper/http-client.d.ts.map +1 -1
  87. package/dist/scraper/http-client.js +49 -2
  88. package/dist/scraper/http-client.js.map +1 -1
  89. package/dist/scraper/http-status-classifier.js.map +1 -1
  90. package/dist/scraper/metrics.js.map +1 -1
  91. package/dist/scraper/navigate.js.map +1 -1
  92. package/dist/scraper/oracle-sentinels.js.map +1 -1
  93. package/dist/scraper/parse-json-response.js.map +1 -1
  94. package/dist/scraper/pool.d.ts +2 -3
  95. package/dist/scraper/pool.d.ts.map +1 -1
  96. package/dist/scraper/pool.js.map +1 -1
  97. package/dist/scraper/rate-limited-json-client.js.map +1 -1
  98. package/dist/scraper/raw-fetch.js.map +1 -1
  99. package/dist/scraper/require-response-field.js.map +1 -1
  100. package/dist/scraper/retry.js.map +1 -1
  101. package/dist/scraper/session-browserbase.d.ts +15 -4
  102. package/dist/scraper/session-browserbase.d.ts.map +1 -1
  103. package/dist/scraper/session-browserbase.js +16 -0
  104. package/dist/scraper/session-browserbase.js.map +1 -1
  105. package/dist/scraper/session-shared.d.ts +16 -0
  106. package/dist/scraper/session-shared.d.ts.map +1 -1
  107. package/dist/scraper/session-shared.js.map +1 -1
  108. package/dist/scraper/session-steel.js.map +1 -1
  109. package/dist/scraper/session-warmup.js.map +1 -1
  110. package/dist/scraper/session.d.ts +6 -6
  111. package/dist/scraper/session.d.ts.map +1 -1
  112. package/dist/scraper/session.js +7 -1
  113. package/dist/scraper/session.js.map +1 -1
  114. package/dist/scraper/stagehand-guard.d.ts.map +1 -1
  115. package/dist/scraper/stagehand-guard.js.map +1 -1
  116. package/dist/scraper/throttle.js.map +1 -1
  117. package/dist/scripts/judge-llm-batch.js.map +1 -1
  118. package/dist/scripts/llm-heal.js.map +1 -1
  119. package/dist/scripts/migrate-telemetry-dir-names.js.map +1 -1
  120. package/dist/scripts/recon-browser.js.map +1 -1
  121. package/dist/scripts/recon-generate.d.ts +92 -6
  122. package/dist/scripts/recon-generate.d.ts.map +1 -1
  123. package/dist/scripts/recon-generate.js +302 -71
  124. package/dist/scripts/recon-generate.js.map +1 -1
  125. package/dist/scripts/recon-heal.js.map +1 -1
  126. package/dist/scripts/recon-shared.js.map +1 -1
  127. package/dist/server.js.map +1 -1
  128. package/dist/site-plugin.d.ts +9 -0
  129. package/dist/site-plugin.d.ts.map +1 -1
  130. package/dist/testing/batch-email-confirmation.js.map +1 -1
  131. package/dist/testing/batch-report.js.map +1 -1
  132. package/dist/testing/integration-runner.js.map +1 -1
  133. package/dist/testing/mock-fetch-response.js.map +1 -1
  134. package/dist/testing/persona-fixture.d.ts.map +1 -1
  135. package/dist/testing/resume-fixture.js.map +1 -1
  136. package/dist/testmail/client.js.map +1 -1
  137. package/dist/testmail/errors.d.ts.map +1 -1
  138. package/dist/testmail/errors.js.map +1 -1
  139. package/dist/types/dispatch-metrics.d.ts +2 -2
  140. package/package.json +17 -2
@@ -20,16 +20,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.resolveStepPayloadField = resolveStepPayloadField;
21
21
  exports.inferZodSchemaFromSamples = inferZodSchemaFromSamples;
22
22
  exports.selectPayloadAction = selectPayloadAction;
23
+ exports.indexStateValues = indexStateValues;
24
+ exports.compileActionSteps = compileActionSteps;
23
25
  exports.loadQuestionPromptKeywords = loadQuestionPromptKeywords;
24
26
  exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
25
27
  exports.emitContractTs = emitContractTs;
26
28
  exports.emitConfigManifest = emitConfigManifest;
27
29
  exports.emitBrowserFlowTs = emitBrowserFlowTs;
30
+ exports.emitIndexTs = emitIndexTs;
28
31
  const node_fs_1 = require("node:fs");
29
32
  const node_path_1 = require("node:path");
30
33
  const errors_1 = require("../lib/errors");
31
34
  const logging_1 = require("../lib/logging");
35
+ const plugin_api_version_1 = require("../plugins/plugin-api-version");
32
36
  const plugin_manifest_envelope_1 = require("../plugins/plugin-manifest-envelope");
37
+ const load_vocabulary_1 = require("../recon/load-vocabulary");
38
+ const vocabulary_1 = require("../recon/vocabulary");
33
39
  const recon_shared_1 = require("../scripts/recon-shared");
34
40
  const logger = (0, logging_1.getScriptLogger)("recon-generate");
35
41
  // ── helpers ──────────────────────────────────────────────────────────────────
@@ -40,46 +46,52 @@ function toPascalCase(siteId) {
40
46
  .join("");
41
47
  }
42
48
  /**
43
- * Instruction labels that name a NON-candidate field a reference-contact, an
44
- * employment-history row, the applicant's signature, etc. When any of these
45
- * matches we leave the step literal so the generator never splices the primary
46
- * applicant's PII into a field that must hold someone/something else. Matched
47
- * against the English label in the instruction, never the recon constant value.
48
- */
49
- const PAYLOAD_FIELD_EXCLUSIONS = [
50
- /reference\s*#?\s*\d/i,
51
- /employment history/i,
52
- /\bcompany (name|phone)\b/i,
53
- /\bemployer\b/i,
54
- /signature/i,
55
- /\bfull name\b/i,
56
- /today'?s date/i,
57
- /school|institution|degree|major|education/i,
58
- // Screening-question shapes: "For '<question>' select '<answer>'" and any
59
- // step framed around a 'question'. Here the first quoted string is the
60
- // question label, not a candidate value — a label word inside the question
61
- // (e.g. "...licensed in this state?") must NOT trigger a splice that would
62
- // overwrite the question quote. Candidate-fill steps say "fill in the X
63
- // field with '...'" / "Select '...' from the X dropdown" instead.
64
- /^\s*for\s+'/i,
65
- /\bquestion\b/i,
66
- ];
67
- /**
68
- * Ordered candidate-field label table. Each entry maps an English-label regex
69
- * to the PascalCase payload field the generator should splice. Order matters:
70
- * the first matching row wins, so more-specific labels precede broader ones.
49
+ * The recruiting vocabulary the engine used to hardcode, kept only so consumers
50
+ * who have not yet passed `--vocabulary` keep working through the 1.x line.
51
+ *
52
+ * @deprecated Pass `--vocabulary <specifier>` instead. The engine cannot know
53
+ * what any site's forms mean, and guessing mis-fires on every non-recruiting
54
+ * domain (a cruise site's "Select the departure port from the Country dropdown"
55
+ * became `${payload.Country}`). Slated for deletion in 2.0.0, after which an
56
+ * absent vocabulary on a spliceable flow is a hard error. See `src/recon/vocabulary.ts`.
71
57
  */
72
- const PAYLOAD_FIELD_TABLE = [
73
- [/\bfirst name\b/i, "FirstName"],
74
- [/\blast name\b/i, "LastName"],
75
- [/\b(e-?mail|email address)\b/i, "Email"],
76
- [/\b(mobile phone|primary phone|phone number|mobile)\b/i, "MobilePhone"],
77
- [/\b(street address|address line 1)\b/i, "AddressLine1"],
78
- [/\bcity\b/i, "City"],
79
- [/\b(state|province|state\/region)\b/i, "State"],
80
- [/\b(zip|postal)\b/i, "PostalCode"],
81
- [/\bcountry\b/i, "Country"],
82
- ];
58
+ const DEPRECATED_BUILTIN_ATS_VOCABULARY = {
59
+ // Naming the subject is what separates "…select the test candidate's state"
60
+ // (fill with the caller's data) from "…the departure port from the Country
61
+ // dropdown" (a search facet that merely says Country).
62
+ subject: /\b(the\s+)?(test\s+)?(candidate|applicant)'?s\b/i,
63
+ exclusions: [
64
+ /reference\s*#?\s*\d/i,
65
+ /employment history/i,
66
+ /\bcompany (name|phone)\b/i,
67
+ /\bemployer\b/i,
68
+ /signature/i,
69
+ /\bfull name\b/i,
70
+ /today'?s date/i,
71
+ /school|institution|degree|major|education/i,
72
+ // Screening-question shapes: "For '<question>' select '<answer>'" and any
73
+ // step framed around a 'question'. Here the first quoted string is the
74
+ // question label, not a candidate value — a label word inside the question
75
+ // (e.g. "...licensed in this state?") must NOT trigger a splice that would
76
+ // overwrite the question quote. Candidate-fill steps say "fill in the X
77
+ // field with '...'" / "Select '...' from the X dropdown" instead.
78
+ /^\s*for\s+'/i,
79
+ /\bquestion\b/i,
80
+ // A secondary phone must not fill the primary MobilePhone field.
81
+ /\bsecondary\b[^.]*\bphone\b/i,
82
+ ],
83
+ table: [
84
+ [/\bfirst name\b/i, "FirstName"],
85
+ [/\blast name\b/i, "LastName"],
86
+ [/\b(e-?mail|email address)\b/i, "Email"],
87
+ [/\b(mobile phone|primary phone|phone number|mobile)\b/i, "MobilePhone"],
88
+ [/\b(street address|address line 1)\b/i, "AddressLine1"],
89
+ [/\bcity\b/i, "City"],
90
+ [/\b(state|province|state\/region)\b/i, "State"],
91
+ [/\b(zip|postal)\b/i, "PostalCode"],
92
+ [/\bcountry\b/i, "Country"],
93
+ ],
94
+ };
83
95
  /**
84
96
  * Decide whether a flow step should splice a runtime `payload.<field>` value in
85
97
  * place of the frozen recon constant baked into its instruction. Exists so
@@ -92,27 +104,29 @@ const PAYLOAD_FIELD_TABLE = [
92
104
  * @param instruction the flow step's plain-English instruction
93
105
  * @param explicit an optional flow-authored `payloadField` override (wins outright)
94
106
  * @param forceNone when true, force a literal step (the `payloadFieldNone` opt-out)
107
+ * @param vocabulary the consumer's domain vocabulary; defaults to the deprecated
108
+ * built-in recruiting table so 1.x consumers who pass nothing keep working
95
109
  * @returns the PascalCase payload field name to splice, or null to keep literal
96
110
  */
97
- function resolveStepPayloadField(instruction, explicit, forceNone) {
111
+ function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary = DEPRECATED_BUILTIN_ATS_VOCABULARY) {
98
112
  if (forceNone)
99
113
  return null;
100
114
  if (explicit)
101
115
  return explicit;
102
- // Nothing to splice unless the instruction actually carries a recon constant:
103
- // a quoted literal, the ${RECON_EMAIL} token, or a dropdown/select phrasing.
104
- const hasSpliceable = /'[^']*'/.test(instruction) ||
105
- /\$\{RECON_EMAIL\}/.test(instruction) ||
106
- /\bdropdown\b/i.test(instruction) ||
107
- /\bselect\b[^.]*\bfrom\b/i.test(instruction);
116
+ // A quoted literal or ${RECON_EMAIL} IS the recon constant this step would
117
+ // replace, so it is spliceable on its own.
118
+ const hasQuotedConstant = /'[^']*'/.test(instruction) || /\$\{RECON_EMAIL\}/.test(instruction);
119
+ // A dropdown step carries no constant to replace, so a label match alone can't
120
+ // tell "select the test candidate's state" (the caller's data) from "select the
121
+ // departure port from the Country dropdown" (a facet that merely says Country).
122
+ // Requiring the subject is what keeps this from mis-firing off-domain.
123
+ const isDropdownStep = /\bdropdown\b/i.test(instruction) || /\bselect\b[^.]*\bfrom\b/i.test(instruction);
124
+ const hasSpliceable = hasQuotedConstant || (isDropdownStep && vocabulary.subject.test(instruction));
108
125
  if (!hasSpliceable)
109
126
  return null;
110
- if (PAYLOAD_FIELD_EXCLUSIONS.some((rx) => rx.test(instruction)))
111
- return null;
112
- // "Secondary Phone Number" must not fill the primary MobilePhone field.
113
- if (/\bsecondary\b/i.test(instruction) && /\bphone\b/i.test(instruction))
127
+ if (vocabulary.exclusions.some((rx) => rx.test(instruction)))
114
128
  return null;
115
- for (const [rx, field] of PAYLOAD_FIELD_TABLE) {
129
+ for (const [rx, field] of vocabulary.table) {
116
130
  if (rx.test(instruction))
117
131
  return field;
118
132
  }
@@ -1047,12 +1061,35 @@ const MAX_STATE_VALUE_LENGTH = 256;
1047
1061
  * does not exist". Closed set, literal-string match — never expand to
1048
1062
  * pattern-based detection (would trip the no-regex-on-open-sets rule). */
1049
1063
  const PLACEHOLDER_STATE_VALUES = new Set(["00000000-0000-0000-0000-000000000000"]);
1064
+ /**
1065
+ * Splits a raw `Set-Cookie` response-header string into `name`/`value` pairs.
1066
+ * Captures store `responseHeaders` as a flat `Record<string, string>`
1067
+ * (see recon-shared.ts's `Capture`), so multiple `Set-Cookie` headers from the
1068
+ * same response — if the recon browser's CDP session folds them together —
1069
+ * would already have lost their individual boundaries before reaching here;
1070
+ * this only recovers name/value pairs from whatever single string survives.
1071
+ */
1072
+ function* walkSetCookiePairs(rawSetCookie) {
1073
+ const pair = rawSetCookie.split(";", 1)[0] ?? "";
1074
+ const eq = pair.indexOf("=");
1075
+ if (eq === -1)
1076
+ return;
1077
+ const name = pair.slice(0, eq).trim();
1078
+ const value = pair.slice(eq + 1).trim();
1079
+ if (name && value)
1080
+ yield { name, value };
1081
+ }
1050
1082
  /**
1051
1083
  * Walks every capture's response (including GETs — formHistoryId-style values
1052
1084
  * may originate in a state-load GET, not a POST). Indexes every string leaf
1053
1085
  * whose length is in [MIN, MAX], recording the EARLIEST capture index that
1054
1086
  * produced it. Later occurrences of the same value reuse the earliest origin.
1055
1087
  *
1088
+ * Also indexes response-header/cookie-origin values (e.g. a `Set-Cookie`
1089
+ * auth token) the same way, tagged with `headerOrigin` instead of a body
1090
+ * `path` — this is what lets a stateful API's token-mint response feed a
1091
+ * later call's `Cookie` header via `compileActionSteps`.
1092
+ *
1056
1093
  * The index is intentionally permissive — it doesn't try to shape-match
1057
1094
  * "what looks like a token" because token shapes are an open set across the
1058
1095
  * web. Authoritative filtering happens downstream in `compileActionSteps`,
@@ -1063,6 +1100,8 @@ const PLACEHOLDER_STATE_VALUES = new Set(["00000000-0000-0000-0000-000000000000"
1063
1100
  * the LATER non-placeholder occurrence at the same JSON path becomes the
1064
1101
  * canonical binding instead.
1065
1102
  */
1103
+ /** Exported for unit testing — lets tests exercise the produces[] walk (body
1104
+ * AND header/cookie origins) directly against synthetic Capture sequences. */
1066
1105
  function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndices = new Set()) {
1067
1106
  const index = new Map();
1068
1107
  // First pass: identify the earliest origin among ACTION captures for each
@@ -1075,10 +1114,32 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
1075
1114
  const haveActionFilter = actionCaptureIndices.size > 0;
1076
1115
  for (let i = 0; i < captures.length; i++) {
1077
1116
  const c = captures[i];
1078
- if (c.responseBody === undefined || c.responseBody === null)
1079
- continue;
1080
1117
  if (haveActionFilter && !actionCaptureIndices.has(i))
1081
1118
  continue;
1119
+ // Headers/cookies are indexed regardless of responseBody presence — a
1120
+ // token-mint call like disneycruise's `authz/private` returns `{}` and
1121
+ // carries its whole payload in `Set-Cookie`.
1122
+ const rawSetCookie = Object.entries(c.responseHeaders).find(([k]) => k.toLowerCase() === "set-cookie")?.[1];
1123
+ if (rawSetCookie !== undefined) {
1124
+ for (const { name, value } of walkSetCookiePairs(rawSetCookie)) {
1125
+ if (value.length < MIN_STATE_VALUE_LENGTH)
1126
+ continue;
1127
+ if (value.length > MAX_STATE_VALUE_LENGTH)
1128
+ continue;
1129
+ if (PLACEHOLDER_STATE_VALUES.has(value))
1130
+ continue;
1131
+ if (!index.has(value)) {
1132
+ index.set(value, {
1133
+ value,
1134
+ originIndex: i,
1135
+ path: [],
1136
+ headerOrigin: { sourceHeader: "set-cookie", cookieName: name },
1137
+ });
1138
+ }
1139
+ }
1140
+ }
1141
+ if (c.responseBody === undefined || c.responseBody === null)
1142
+ continue;
1082
1143
  // For GET captures, only index UUID-shaped strings. GET captures (today,
1083
1144
  // only the form-schema fetch inserted as an action step) surface stable
1084
1145
  // structural identifiers — UUIDs that downstream POSTs need to thread.
@@ -1115,9 +1176,18 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
1115
1176
  function isValidJsIdentifier(s) {
1116
1177
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
1117
1178
  }
1118
- /** Converts a path like ["Auth","Token"] to a JS access expression ".Auth.Token". */
1179
+ /**
1180
+ * Converts a path like ["Auth","Token"] to a JS access expression ".Auth.Token".
1181
+ * Bracket segments (numeric array indices, non-identifier keys) get a trailing
1182
+ * `!` — under `noUncheckedIndexedAccess` an array/index-signature access types
1183
+ * as `T | undefined`, and this accessor is only ever used against a real Zod-
1184
+ * inferred array/object type (payload fields, captured response bodies), never
1185
+ * against the object-literal assertion types `pathToAssertionType` builds (those
1186
+ * use known string-literal keys, which `noUncheckedIndexedAccess` does not
1187
+ * widen). Dot segments stay bare since object property access isn't affected.
1188
+ */
1119
1189
  function pathToAccessor(path) {
1120
- return path.map((p) => (isValidJsIdentifier(p) ? `.${p}` : `[${JSON.stringify(p)}]`)).join("");
1190
+ return path.map((p) => (isValidJsIdentifier(p) ? `.${p}` : `[${JSON.stringify(p)}]!`)).join("");
1121
1191
  }
1122
1192
  /**
1123
1193
  * Builds a nested TypeScript assertion type matching a JSON path. e.g.
@@ -1151,26 +1221,75 @@ function pathToVarName(path) {
1151
1221
  * var name, the state values its response produces (used by downstream steps),
1152
1222
  * and a multipart flag (request body bytes not captured).
1153
1223
  */
1224
+ /** Exported for unit testing — see `indexStateValues`. */
1154
1225
  function compileActionSteps(actions, stateIndex) {
1155
1226
  const usedValues = new Set();
1227
+ // Maps a used state value to the request-header NAME that carries it, for
1228
+ // values whose consuming reference is a request header (not the URL/body).
1229
+ // A header-origin produce needs this as its `targetHeader` — the header the
1230
+ // *next* httpClient call must send the bound value back on. Only the first
1231
+ // consuming header name observed wins; a value used in more than one distinct
1232
+ // header downstream isn't a shape this models (see http-client.ts's `bind`,
1233
+ // which is single-target per binding).
1234
+ const usedValueTargetHeader = new Map();
1156
1235
  // Pre-scan: collect all state values referenced by ANY action's URL/headers/body
1157
1236
  // so we only "produce" the values that are actually consumed downstream.
1158
1237
  for (const { capture } of actions) {
1159
1238
  const haystacks = [capture.url];
1160
- for (const v of Object.values(capture.requestHeaders))
1161
- haystacks.push(v);
1162
1239
  if (capture.requestPostData)
1163
1240
  haystacks.push(capture.requestPostData);
1164
1241
  for (const sv of stateIndex.values()) {
1165
1242
  if (haystacks.some((h) => h.includes(sv.value)))
1166
1243
  usedValues.add(sv.value);
1167
1244
  }
1245
+ for (const [headerName, headerValue] of Object.entries(capture.requestHeaders)) {
1246
+ for (const sv of stateIndex.values()) {
1247
+ if (!headerValue.includes(sv.value))
1248
+ continue;
1249
+ usedValues.add(sv.value);
1250
+ if (!usedValueTargetHeader.has(sv.value)) {
1251
+ usedValueTargetHeader.set(sv.value, headerName);
1252
+ }
1253
+ }
1254
+ }
1168
1255
  }
1169
1256
  let lastHost = null;
1170
1257
  return actions.map(({ capture, index }, i) => {
1171
1258
  const varName = `r${i}`;
1172
1259
  const produces = [];
1173
1260
  const seenNames = new Set();
1261
+ // Header/cookie-origin produces — walked first so a value that appears in
1262
+ // BOTH a Set-Cookie and the JSON body (unlikely, but not ruled out) prefers
1263
+ // the header binding, which is what the runtime actually threads.
1264
+ const rawSetCookie = Object.entries(capture.responseHeaders).find(([k]) => k.toLowerCase() === "set-cookie")?.[1];
1265
+ if (rawSetCookie !== undefined) {
1266
+ for (const { name: cookieName, value } of walkSetCookiePairs(rawSetCookie)) {
1267
+ if (!usedValues.has(value))
1268
+ continue;
1269
+ const sv = stateIndex.get(value);
1270
+ if (!sv || sv.originIndex !== index || !sv.headerOrigin)
1271
+ continue;
1272
+ const targetHeader = usedValueTargetHeader.get(value);
1273
+ if (!targetHeader)
1274
+ continue;
1275
+ let name = `${cookieName.replace(/[^A-Za-z0-9]/g, "")}Cookie`;
1276
+ if (!/^[A-Za-z_$]/.test(name))
1277
+ name = `_${name}`;
1278
+ let suffix = 1;
1279
+ while (seenNames.has(name)) {
1280
+ suffix++;
1281
+ name = `${cookieName.replace(/[^A-Za-z0-9]/g, "")}Cookie${suffix}`;
1282
+ }
1283
+ seenNames.add(name);
1284
+ produces.push({
1285
+ kind: "header",
1286
+ name,
1287
+ sourceHeader: sv.headerOrigin.sourceHeader,
1288
+ cookieName: sv.headerOrigin.cookieName,
1289
+ targetHeader,
1290
+ });
1291
+ }
1292
+ }
1174
1293
  if (capture.responseBody !== undefined && capture.responseBody !== null) {
1175
1294
  for (const { value, path } of walkStringLeaves(capture.responseBody)) {
1176
1295
  if (!usedValues.has(value))
@@ -1186,7 +1305,7 @@ function compileActionSteps(actions, stateIndex) {
1186
1305
  name = `${pathToVarName(path)}${suffix}`;
1187
1306
  }
1188
1307
  seenNames.add(name);
1189
- produces.push({ name, pathExpr: `${varName}${pathToAccessor(path)}`, path });
1308
+ produces.push({ kind: "body", name, pathExpr: `${varName}${pathToAccessor(path)}`, path });
1190
1309
  }
1191
1310
  }
1192
1311
  const ct = Object.entries(capture.requestHeaders).find(([k]) => k.toLowerCase() === "content-type");
@@ -1203,6 +1322,27 @@ function compileActionSteps(actions, stateIndex) {
1203
1322
  return { capture, varName, produces, isMultipart, isCrossDomain };
1204
1323
  });
1205
1324
  }
1325
+ /**
1326
+ * Collects every header/cookie-origin produce across an action sequence, in
1327
+ * step order — this is what `emitContractTs` renders as `createHttpClient`'s
1328
+ * `bind` option so the generated `executeHttp` actually forwards a value like
1329
+ * disneycruise's `Set-Cookie: __pa=<jwt>` mint to the stateful call that 401s
1330
+ * without it. Deduped by `targetHeader`: `HttpResponseBinding` (http-client.ts)
1331
+ * is one binding per target header, so if two steps somehow produced the same
1332
+ * target the earliest wins.
1333
+ */
1334
+ function collectHeaderBindings(actionSteps) {
1335
+ const byTarget = new Map();
1336
+ for (const step of actionSteps) {
1337
+ for (const p of step.produces) {
1338
+ if (p.kind !== "header")
1339
+ continue;
1340
+ if (!byTarget.has(p.targetHeader))
1341
+ byTarget.set(p.targetHeader, p);
1342
+ }
1343
+ }
1344
+ return [...byTarget.values()];
1345
+ }
1206
1346
  /**
1207
1347
  * Replaces occurrences of state values in `template` with `${varName}`
1208
1348
  * interpolations. Returns a JS template-literal string fragment (no backticks).
@@ -1217,6 +1357,12 @@ function interpolateStateValues(template, priorSteps, payloadAccessorByValue = n
1217
1357
  const varNameByValue = new Map();
1218
1358
  for (const step of priorSteps) {
1219
1359
  for (const p of step.produces) {
1360
+ // Header/cookie-origin produces have no body path — their value never
1361
+ // appears as a literal in a URL/body template (http-client's `bind`
1362
+ // forwards it directly as a request header), so there's nothing to
1363
+ // interpolate here.
1364
+ if (p.kind === "header")
1365
+ continue;
1220
1366
  let cursor = step.capture.responseBody;
1221
1367
  for (const segment of p.path) {
1222
1368
  if (cursor !== null &&
@@ -1810,6 +1956,11 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
1810
1956
  }
1811
1957
  }
1812
1958
  for (const p of step.produces) {
1959
+ // Header/cookie-origin produces never surface as a JS accessor —
1960
+ // createHttpClient's `bind` option (rendered once, above the steps)
1961
+ // captures and forwards the value internally.
1962
+ if (p.kind === "header")
1963
+ continue;
1813
1964
  if (declaredNames.has(p.name))
1814
1965
  continue;
1815
1966
  if (!referencedNames.has(p.name))
@@ -1835,11 +1986,30 @@ function summariseResponseShape(value) {
1835
1986
  }
1836
1987
  return obj;
1837
1988
  }
1989
+ /**
1990
+ * Renders `headerBindings` as a trailing `, bind: [...]` fragment for
1991
+ * `createHttpClient`'s options object literal — empty string when there are
1992
+ * none, so a plugin with no header/cookie-origin state keeps the exact output
1993
+ * this emitter already produced. Structurally matches `HttpResponseBinding`
1994
+ * (http-client.ts) without importing the type: the object literal typechecks
1995
+ * against `HttpClientOptions.bind` on its own shape.
1996
+ */
1997
+ function bindOptionLiteral(headerBindings) {
1998
+ if (headerBindings.length === 0)
1999
+ return "";
2000
+ const entries = headerBindings
2001
+ .map((b) => {
2002
+ const cookieNameField = b.cookieName !== undefined ? ` cookieName: ${JSON.stringify(b.cookieName)},` : "";
2003
+ return `{ sourceHeader: ${JSON.stringify(b.sourceHeader)},${cookieNameField} targetHeader: ${JSON.stringify(b.targetHeader)} }`;
2004
+ })
2005
+ .join(", ");
2006
+ return `, bind: [${entries}]`;
2007
+ }
1838
2008
  // ── code emitters ─────────────────────────────────────────────────────────────
1839
2009
  /** Generates a complete contract.ts source string for a plugin — exported so
1840
2010
  * unit tests can drive the emitter directly without spawning the CLI. */
1841
2011
  function emitContractTs(opts) {
1842
- const { siteId, pascal, baseUrl, baseHeaders, minTime, safeRps, responseBody, gql, gqlQuery, endpointPath, auxFiles, multiStepBody, inputBody, hasMultipartStep = false, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, payloadFieldNames, base64ContentHelper = "", } = opts;
2012
+ const { siteId, pascal, baseUrl, baseHeaders, minTime, safeRps, responseBody, gql, gqlQuery, endpointPath, auxFiles, multiStepBody, inputBody, hasMultipartStep = false, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, payloadFieldNames, base64ContentHelper = "", headerBindings = [], } = opts;
1843
2013
  // Multi-step plugins thread responses through many different shapes that a
1844
2014
  // single Zod schema can't cover — use z.unknown() so each per-step access
1845
2015
  // compiles cleanly. Single-endpoint plugins keep the inferred schema.
@@ -1997,7 +2167,7 @@ function getGql(baseUrl: string): GqlFn {
1997
2167
  }
1998
2168
  `
1999
2169
  : `
2000
- const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS });
2170
+ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)} });
2001
2171
  `;
2002
2172
  const executeHttpBody = multiStepBody
2003
2173
  ? multiStepBody
@@ -2019,6 +2189,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
2019
2189
  const queryChecklistLine = gql
2020
2190
  ? `\n * [ ] Trim UI-only fields from ${pascal.toUpperCase()}_QUERY (keep only fields you need)`
2021
2191
  : "";
2192
+ const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2022
2193
  return `/**
2023
2194
  * Generated by recon-generate.ts — review before shipping.
2024
2195
  *
@@ -2026,6 +2197,9 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
2026
2197
  * [ ] Narrow ${pascal}ResponseSchema to match the real response shape
2027
2198
  * [ ] Adjust ${pascal}PayloadSchema to your actual request parameters
2028
2199
  * [ ] Verify BASE_HEADERS — remove any that aren't load-bearing
2200
+ * [ ] Out-of-tree: \`pnpm add bottleneck zod\` — this file imports both
2201
+ * directly, and a strict node_modules layout (pnpm) won't resolve
2202
+ * them as transitive deps of @enricai/barnacle alone
2029
2203
  */
2030
2204
 
2031
2205
  import Bottleneck from "bottleneck";
@@ -2057,13 +2231,14 @@ ${queryConst}${gqlCacheBlock}${fixtureComments}
2057
2231
  * Plugin for ${siteId}. Tries the direct-HTTP hot path first; falls back to
2058
2232
  * Stagehand automatically on schema drift or bot challenge.
2059
2233
  */
2060
- export const ${siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
2234
+ export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
2061
2235
  meta: {
2062
2236
  siteId: ${JSON.stringify(siteId)},
2063
2237
  displayName: ${JSON.stringify(pascal.replace(/([A-Z])/g, " $1").trim())},
2064
2238
  bodySchema: ${pascal}PayloadSchema,
2065
2239
  responseSchema: ${pascal}ResponseSchema,
2066
- defaultBaseUrl: ${JSON.stringify(baseUrl)},${hasMultipartStep ? "\n multipart: true," : ""}
2240
+ defaultBaseUrl: ${JSON.stringify(baseUrl)},
2241
+ apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${hasMultipartStep ? "\n multipart: true," : ""}
2067
2242
  },
2068
2243
 
2069
2244
  /** Hot path: direct HTTP — no browser, no LLM tokens. */
@@ -2084,6 +2259,11 @@ ${executeHttpBody}
2084
2259
  return { data: raw as ${pascal}Response };
2085
2260
  },
2086
2261
  };
2262
+
2263
+ // Out-of-tree loader resolves \`m.plugin ?? m.default ?? m\` — this named
2264
+ // alias is what BARNACLE_PLUGINS finds; without it the loader would fall
2265
+ // through to \`m.default\` (the response schema above) and 404 at runtime.
2266
+ export { ${camel}Plugin as plugin };
2087
2267
  `;
2088
2268
  }
2089
2269
  /**
@@ -2162,12 +2342,12 @@ function buildManifestInstruction(instruction, field) {
2162
2342
  * omitted; a site that needs it keeps the `.ts` path or wires `spec.httpModule`.
2163
2343
  */
2164
2344
  function emitConfigManifest(opts) {
2165
- const { siteId, displayName, baseUrl, flowSteps } = opts;
2345
+ const { siteId, displayName, baseUrl, flowSteps, vocabulary } = opts;
2166
2346
  const payloadFieldNames = new Set();
2167
2347
  const steps = flowSteps.map((step) => {
2168
2348
  const isObj = typeof step !== "string";
2169
2349
  const instruction = isObj ? step.step : step;
2170
- const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined);
2350
+ const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary);
2171
2351
  if (field !== null)
2172
2352
  payloadFieldNames.add(field);
2173
2353
  const rewritten = buildManifestInstruction(instruction, field);
@@ -2211,13 +2391,13 @@ function emitConfigManifest(opts) {
2211
2391
  * payload schema (both are driven by this same set).
2212
2392
  */
2213
2393
  function emitBrowserFlowTs(opts) {
2214
- const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false } = opts;
2394
+ const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false, vocabulary, } = opts;
2215
2395
  const payloadFieldNames = new Set();
2216
2396
  const hasUploadStep = flowSteps.some((s) => typeof s !== "string" && s.upload === true);
2217
2397
  const stepLiterals = flowSteps.map((step) => {
2218
2398
  const isObj = typeof step !== "string";
2219
2399
  const instruction = isObj ? step.step : step;
2220
- const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined);
2400
+ const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary);
2221
2401
  if (field !== null)
2222
2402
  payloadFieldNames.add(field);
2223
2403
  const instructionExpr = buildStepInstructionExpr(instruction, field);
@@ -2322,30 +2502,71 @@ ${flowStepsBlock}
2322
2502
  `;
2323
2503
  return { code, payloadFieldNames };
2324
2504
  }
2505
+ /** Generates the site's index.ts barrel — exported so the out-of-tree e2e
2506
+ * test can drive the emitter directly without spawning the CLI. */
2325
2507
  function emitIndexTs(opts) {
2326
2508
  const { siteId } = opts;
2327
2509
  const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2328
2510
  return `/**
2329
2511
  * Generated by recon-generate.ts.
2330
- * Register this built-in plugin by adding it to \`BUILTIN_SITE_PLUGINS\` in
2331
- * src/plugins/discover.ts:
2512
+ * Build this package, then point BARNACLE_PLUGINS at the compiled module —
2513
+ * no core edits required:
2514
+ *
2515
+ * BARNACLE_PLUGINS=./dist/sites/${siteId}/index.js pnpm start
2332
2516
  *
2333
- * import { ${camel}Plugin } from "@/sites/${siteId}";
2334
- * BUILTIN_SITE_PLUGINS.push(${camel}Plugin as SitePlugin<unknown, unknown>);
2517
+ * The loader resolves \`m.plugin ?? m.default ?? m\` — the \`plugin\` alias
2518
+ * below is what it finds.
2335
2519
  */
2336
2520
 
2337
- export { ${camel}Plugin } from "@/sites/${siteId}/contract";
2521
+ export { ${camel}Plugin, ${camel}Plugin as plugin } from "@/sites/${siteId}/contract";
2338
2522
  `;
2339
2523
  }
2340
2524
  // ── main ──────────────────────────────────────────────────────────────────────
2525
+ /**
2526
+ * Resolves the vocabulary for this run and reports which one is in play.
2527
+ *
2528
+ * The deprecation warning is the whole point of the 1.x window: removing a
2529
+ * built-in default that consumers silently depend on is only safe if the
2530
+ * transition is loud. It fires only when the built-in would actually change the
2531
+ * output — a flow with nothing to splice gets no nag it can't act on.
2532
+ */
2533
+ async function resolveVocabulary(specifier, flowSteps) {
2534
+ if (specifier) {
2535
+ const vocabulary = await (0, load_vocabulary_1.loadReconVocabulary)(specifier, process.cwd());
2536
+ logger.info(`vocabulary: ${specifier === load_vocabulary_1.VOCABULARY_NONE ? "none (splicing disabled)" : `${vocabulary.table.length} row(s) from ${specifier}`}`);
2537
+ return vocabulary;
2538
+ }
2539
+ // Warn only when the built-in table itself changes the outcome. A step with an
2540
+ // explicit payloadField resolves the same under any vocabulary, so nagging
2541
+ // about it would send consumers to fix something that isn't broken.
2542
+ const builtinChangesOutcome = flowSteps.some((step) => {
2543
+ const isObj = typeof step !== "string";
2544
+ const instruction = isObj ? step.step : step;
2545
+ const explicit = isObj ? step.payloadField : undefined;
2546
+ const forceNone = isObj ? step.payloadFieldNone : undefined;
2547
+ const withBuiltin = resolveStepPayloadField(instruction, explicit, forceNone, DEPRECATED_BUILTIN_ATS_VOCABULARY);
2548
+ const withNothing = resolveStepPayloadField(instruction, explicit, forceNone, vocabulary_1.EMPTY_VOCABULARY);
2549
+ return withBuiltin !== withNothing;
2550
+ });
2551
+ if (builtinChangesOutcome) {
2552
+ logger.warn(`DeprecationWarning: no --vocabulary given, falling back to the built-in recruiting table. ` +
2553
+ `It is removed in 2.0.0, after which this is an error. Fix: create a vocabulary module ` +
2554
+ `exporting a ReconVocabulary (see @enricai/barnacle/recon/vocabulary) and pass ` +
2555
+ `--vocabulary ./src/recon/<name>.ts, or --vocabulary none if this site splices no caller data.`);
2556
+ }
2557
+ return DEPRECATED_BUILTIN_ATS_VOCABULARY;
2558
+ }
2341
2559
  async function main() {
2342
2560
  const args = process.argv.slice(2);
2343
2561
  let siteId = "";
2344
2562
  let force = false;
2345
2563
  let emit = "ts";
2564
+ let vocabularySpecifier = "";
2346
2565
  for (let i = 0; i < args.length; i++) {
2347
2566
  if (args[i] === "--site-id" && args[i + 1])
2348
2567
  siteId = args[++i];
2568
+ else if (args[i] === "--vocabulary" && args[i + 1])
2569
+ vocabularySpecifier = args[++i];
2349
2570
  else if (args[i] === "--force")
2350
2571
  force = true;
2351
2572
  else if (args[i] === "--emit" && args[i + 1]) {
@@ -2412,6 +2633,10 @@ async function main() {
2412
2633
  return [];
2413
2634
  }
2414
2635
  })();
2636
+ // Resolved once and threaded down, never captured into a module const: a
2637
+ // module-level const would freeze at import time, which is the bug that makes
2638
+ // RECON_QUESTION_KEYWORDS silently inert for anyone setting it after load.
2639
+ const vocabulary = await resolveVocabulary(vocabularySpecifier, flowSteps);
2415
2640
  const pascal = toPascalCase(siteId);
2416
2641
  const baseUrl = deriveBaseUrl(captures);
2417
2642
  const baseHeaders = deriveRequestHeaders(captures, replays, baseUrl);
@@ -2562,6 +2787,7 @@ async function main() {
2562
2787
  s.capture.url.includes("recruitingCEJobApplicationDrafts"));
2563
2788
  if (draftPostStep && !draftPostStep.produces.some((p) => p.name === "draftId")) {
2564
2789
  draftPostStep.produces.push({
2790
+ kind: "body",
2565
2791
  name: "draftId",
2566
2792
  pathExpr: `${draftPostStep.varName}.APPDraftId`,
2567
2793
  path: ["APPDraftId"],
@@ -2570,6 +2796,7 @@ async function main() {
2570
2796
  const attachPostStep = actionSteps.find((s) => s.capture.method === "POST" && s.capture.url.includes("/attachments"));
2571
2797
  if (attachPostStep && !attachPostStep.produces.some((p) => p.name === "attachmentId")) {
2572
2798
  attachPostStep.produces.push({
2799
+ kind: "body",
2573
2800
  name: "attachmentId",
2574
2801
  pathExpr: `${attachPostStep.varName}.Id`,
2575
2802
  path: ["Id"],
@@ -2618,6 +2845,7 @@ async function main() {
2618
2845
  ? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, base64PatchOverride)
2619
2846
  : multiStepBody;
2620
2847
  const hasMultipartStep = actionSteps.some((s) => s.isMultipart);
2848
+ const headerBindings = collectHeaderBindings(actionSteps);
2621
2849
  // For submission flows the final action's response body is the most useful
2622
2850
  // shape inference target (it's the terminal success signal). Fall back to
2623
2851
  // the replay body for single-endpoint sites.
@@ -2632,6 +2860,7 @@ async function main() {
2632
2860
  displayName: pascal,
2633
2861
  baseUrl,
2634
2862
  flowSteps,
2863
+ vocabulary,
2635
2864
  }));
2636
2865
  logger.info(`wrote ${manifestPath}`);
2637
2866
  logger.info(`done — review ${manifestPath}, fill in response/extract schemas, then load via BARNACLE_PLUGINS or BARNACLE_PLUGINS_CONFIG_DIR (no compile step)`);
@@ -2648,6 +2877,7 @@ async function main() {
2648
2877
  flowSteps,
2649
2878
  isSubmissionFlow,
2650
2879
  hasMultipartStep,
2880
+ vocabulary,
2651
2881
  });
2652
2882
  (0, node_fs_1.writeFileSync)(`${outDir}/contract.ts`, emitContractTs({
2653
2883
  siteId,
@@ -2673,6 +2903,7 @@ async function main() {
2673
2903
  discoveredRawOptionFields,
2674
2904
  discoveredAdditionalBodyKeys,
2675
2905
  payloadFieldNames: browserFlow.payloadFieldNames,
2906
+ headerBindings,
2676
2907
  }));
2677
2908
  logger.info(`wrote ${outDir}/contract.ts`);
2678
2909
  (0, node_fs_1.writeFileSync)(`${outDir}/flows/browser-flow.ts`, browserFlow.code);