@enricai/barnacle 1.12.10 → 1.12.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/random.d.ts +8 -0
- package/dist/lib/random.d.ts.map +1 -1
- package/dist/lib/random.js +12 -0
- package/dist/lib/random.js.map +1 -1
- package/dist/scraper/session-teardown.d.ts +1 -1
- package/dist/scraper/session-teardown.js +2 -2
- package/dist/scraper/session-teardown.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +29 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +404 -137
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +5 -1
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.resolveStepPayloadField = resolveStepPayloadField;
|
|
25
|
+
exports.buildKnownFieldValues = buildKnownFieldValues;
|
|
25
26
|
exports.extractStepPersonaValue = extractStepPersonaValue;
|
|
26
27
|
exports.deriveFillLabelField = deriveFillLabelField;
|
|
27
28
|
exports.harvestPersonaBindings = harvestPersonaBindings;
|
|
@@ -75,6 +76,109 @@ const logger = (0, logging_1.getScriptLogger)("recon-generate");
|
|
|
75
76
|
*/
|
|
76
77
|
const ENGINE_PKG = "@enricai/barnacle";
|
|
77
78
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* A reserved recon env token, e.g. `${RECON_EMAIL}` or `${RECON_PHONE}` — the
|
|
81
|
+
* `RECON_` prefix is what marks a `${UPPER_SNAKE}` token as a recon-owned
|
|
82
|
+
* splice site rather than an unrelated caller-authored env reference.
|
|
83
|
+
*/
|
|
84
|
+
const RESERVED_ENV_TOKEN = /\$\{RECON_[A-Z0-9_]*\}/;
|
|
85
|
+
/**
|
|
86
|
+
* The reserved `${RECON_PASSWORD}` token. Unlike every other `RESERVED_ENV_TOKEN`
|
|
87
|
+
* (RECON_EMAIL, RECON_PHONE, ...), which name a piece of the caller's real
|
|
88
|
+
* applicant identity and so splice to a `payload.<field>` accessor, this one
|
|
89
|
+
* names a credential the recon capture needed to authenticate but that has no
|
|
90
|
+
* caller-supplied counterpart on the applicant payload — there is no "Password"
|
|
91
|
+
* field to route it through. It gets its own reserved-tooling handling ahead of
|
|
92
|
+
* (never through) vocabulary/payload-field resolution.
|
|
93
|
+
*/
|
|
94
|
+
const RECON_PASSWORD_TOKEN = `$${"{RECON_PASSWORD}"}`;
|
|
95
|
+
/**
|
|
96
|
+
* Masks the apostrophe in a possessive `'s` (e.g. "the candidate's name") with
|
|
97
|
+
* a non-quote placeholder of the same length, so a naive `'...'` quote scan
|
|
98
|
+
* doesn't mistake the possessive apostrophe for an opening quote delimiter.
|
|
99
|
+
* Length-preserving so callers that need positions in the ORIGINAL instruction
|
|
100
|
+
* can reuse the indices matched against the masked string unchanged.
|
|
101
|
+
*/
|
|
102
|
+
function maskPossessiveApostrophes(instruction) {
|
|
103
|
+
return instruction.replace(/(\w)'(s\b)/g, "$1 $2");
|
|
104
|
+
}
|
|
105
|
+
function findQuoteSpans(instruction) {
|
|
106
|
+
const masked = maskPossessiveApostrophes(instruction);
|
|
107
|
+
return [...masked.matchAll(/'([^']*)'/g)].map((m) => ({
|
|
108
|
+
index: m.index,
|
|
109
|
+
length: m[0].length,
|
|
110
|
+
value: instruction.slice(m.index + 1, m.index + m[0].length - 1),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Picks which quoted span in an instruction is the persona VALUE, per the
|
|
115
|
+
* grammar recon-flow steps use: a `Select`/`Choose`/`Pick` step names the
|
|
116
|
+
* ANSWER first, then the question, so the value is the FIRST quoted span; a
|
|
117
|
+
* `Fill`/`Enter`/`Type` step names the field label first and the value last,
|
|
118
|
+
* so it is the LAST quoted span.
|
|
119
|
+
*/
|
|
120
|
+
function pickValueSpan(instruction, spans) {
|
|
121
|
+
return /^\s*(select|choose|pick)\b/i.test(instruction) ? spans[0] : spans[spans.length - 1];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Locates the splice site in a flow-step instruction as `{ before, matched,
|
|
125
|
+
* after }` slices of the ORIGINAL instruction — a reserved `${RECON_*}` env
|
|
126
|
+
* token when present (preferred, since it names its own site unambiguously),
|
|
127
|
+
* otherwise the quoted VALUE span per {@link pickValueSpan}'s verb-class rule.
|
|
128
|
+
* This is the position-aware counterpart to {@link extractStepPersonaValue}:
|
|
129
|
+
* that function only needs the extracted string, this one needs the actual
|
|
130
|
+
* before/after slices so a template literal can be rebuilt around the site.
|
|
131
|
+
*
|
|
132
|
+
* @returns null when the instruction carries no spliceable site
|
|
133
|
+
*/
|
|
134
|
+
function locateSpliceSite(instruction) {
|
|
135
|
+
const envToken = RESERVED_ENV_TOKEN.exec(instruction);
|
|
136
|
+
if (envToken) {
|
|
137
|
+
return {
|
|
138
|
+
before: instruction.slice(0, envToken.index),
|
|
139
|
+
matched: envToken[0],
|
|
140
|
+
after: instruction.slice(envToken.index + envToken[0].length),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const spans = findQuoteSpans(instruction);
|
|
144
|
+
if (spans.length === 0)
|
|
145
|
+
return null;
|
|
146
|
+
const span = pickValueSpan(instruction, spans);
|
|
147
|
+
return {
|
|
148
|
+
before: instruction.slice(0, span.index),
|
|
149
|
+
matched: instruction.slice(span.index, span.index + span.length),
|
|
150
|
+
after: instruction.slice(span.index + span.length),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A quoted span immediately followed by "button"/"link"/"tab" NAMES a control,
|
|
155
|
+
* not a fill/select VALUE — `click the 'Sign in with email' button` carries no
|
|
156
|
+
* applicant datum even though the quoted text happens to contain a field-label
|
|
157
|
+
* word ("email"). Checked ahead of vocabulary matching so a control's own name
|
|
158
|
+
* can never be mistaken for the data it merely mentions.
|
|
159
|
+
*/
|
|
160
|
+
function namesAControl(instruction) {
|
|
161
|
+
return findQuoteSpans(instruction).some((span) => /^\s*(button|link|tab)\b/i.test(instruction.slice(span.index + span.length)));
|
|
162
|
+
}
|
|
163
|
+
/** The quoted VALUE a Select/Choose/Fill step carries, per {@link pickValueSpan}'s
|
|
164
|
+
* grammar rule — used to validate a vocabulary match's ANSWER, not just its label. */
|
|
165
|
+
function pickedQuotedValue(instruction) {
|
|
166
|
+
const spans = findQuoteSpans(instruction);
|
|
167
|
+
if (spans.length === 0)
|
|
168
|
+
return null;
|
|
169
|
+
const value = pickValueSpan(instruction, spans).value;
|
|
170
|
+
return value.length > 0 ? value : null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Closed-enum answers (Yes/No, decline-to-answer, ...) a Select/Choose step
|
|
174
|
+
* commonly carries. None of these is ever an applicant's own datum — they are
|
|
175
|
+
* the RESPONSE to a screening question, not the fact a name/state/phone field
|
|
176
|
+
* asks for — so a vocabulary label match must never bind one to a payload field.
|
|
177
|
+
* Generic English grammar, not a domain vocabulary concern, so it belongs here
|
|
178
|
+
* rather than in any consumer's `--vocabulary`.
|
|
179
|
+
*/
|
|
180
|
+
const OPERATIONAL_ANSWER = /^(yes|no|true|false|n\/a|none|decline(?:\s+to\s+(?:answer|self-identify))?|prefer not to (?:answer|say))$/i;
|
|
181
|
+
const EMPTY_KNOWN_FIELD_VALUES = new Map();
|
|
78
182
|
function toPascalCase(siteId) {
|
|
79
183
|
return siteId
|
|
80
184
|
.split(/[-_]/)
|
|
@@ -95,16 +199,26 @@ function toPascalCase(siteId) {
|
|
|
95
199
|
* @param forceNone when true, force a literal step (the `payloadFieldNone` opt-out)
|
|
96
200
|
* @param vocabulary the consumer's domain vocabulary; defaults to {@link EMPTY_VOCABULARY}
|
|
97
201
|
* (no splicing) when the caller passes none
|
|
202
|
+
* @param knownFieldValues field→value pairs the flow already established
|
|
203
|
+
* unambiguously via a Fill/Enter/Type step ({@link buildKnownFieldValues}). A
|
|
204
|
+
* Select/Choose step's ANSWER must equal the known value for the field a
|
|
205
|
+
* label match names, or it is not that field's datum (e.g. a device-type
|
|
206
|
+
* dropdown's `'Mobile'` looks like a phone-number field by label alone, but
|
|
207
|
+
* is nothing like the number the Fill step already bound).
|
|
98
208
|
* @returns the PascalCase payload field name to splice, or null to keep literal
|
|
99
209
|
*/
|
|
100
|
-
function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary = vocabulary_1.EMPTY_VOCABULARY) {
|
|
210
|
+
function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary = vocabulary_1.EMPTY_VOCABULARY, knownFieldValues = EMPTY_KNOWN_FIELD_VALUES) {
|
|
101
211
|
if (forceNone)
|
|
102
212
|
return null;
|
|
103
213
|
if (explicit)
|
|
104
214
|
return explicit;
|
|
105
|
-
// A
|
|
106
|
-
//
|
|
107
|
-
|
|
215
|
+
// A control's own name (a button/link/tab label) is never a fill/select VALUE,
|
|
216
|
+
// even when it happens to contain a word a vocabulary row also matches.
|
|
217
|
+
if (namesAControl(instruction))
|
|
218
|
+
return null;
|
|
219
|
+
// A quoted literal or a reserved ${RECON_*} env token IS the recon constant
|
|
220
|
+
// this step would replace, so it is spliceable on its own.
|
|
221
|
+
const hasQuotedConstant = /'[^']*'/.test(instruction) || RESERVED_ENV_TOKEN.test(instruction);
|
|
108
222
|
// A dropdown step carries no constant to replace, so a label match alone can't
|
|
109
223
|
// tell "select the test candidate's state" (the caller's data) from "select the
|
|
110
224
|
// neighborhood from the Country dropdown" (a facet that merely says Country).
|
|
@@ -115,12 +229,53 @@ function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary =
|
|
|
115
229
|
return null;
|
|
116
230
|
if (vocabulary.exclusions.some((rx) => rx.test(instruction)))
|
|
117
231
|
return null;
|
|
232
|
+
// A Select/Choose/Pick step names an ANSWER, not a Fill step's self-evident
|
|
233
|
+
// value — a label match alone can't tell the applicant's own datum from an
|
|
234
|
+
// operational choice the widget merely offers, so the answer itself is
|
|
235
|
+
// validated below.
|
|
236
|
+
const isSelectStep = /\b(select|choose|pick)\b/i.test(instruction);
|
|
118
237
|
for (const [rx, field] of vocabulary.table) {
|
|
119
|
-
if (rx.test(instruction))
|
|
238
|
+
if (!rx.test(instruction))
|
|
239
|
+
continue;
|
|
240
|
+
if (!isSelectStep)
|
|
120
241
|
return field;
|
|
242
|
+
const answer = pickedQuotedValue(instruction);
|
|
243
|
+
if (answer !== null && OPERATIONAL_ANSWER.test(answer.trim()))
|
|
244
|
+
return null;
|
|
245
|
+
const known = knownFieldValues.get(field);
|
|
246
|
+
if (known !== undefined && answer !== known)
|
|
247
|
+
return null;
|
|
248
|
+
return field;
|
|
121
249
|
}
|
|
122
250
|
return null;
|
|
123
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Field→value pairs the flow establishes unambiguously via a Fill/Enter/Type
|
|
254
|
+
* step — the applicant's own datum, per {@link deriveFillLabelField}'s same
|
|
255
|
+
* self-describing grammar. {@link resolveStepPayloadField} uses this to verify
|
|
256
|
+
* a Select/Choose step's ANSWER is that SAME datum before binding it to the
|
|
257
|
+
* field a label match names, catching the mismatch a label alone cannot see
|
|
258
|
+
* (a field whose label mentions "phone" but whose select answer is a device
|
|
259
|
+
* type, not a number).
|
|
260
|
+
*/
|
|
261
|
+
function buildKnownFieldValues(flowSteps, vocabulary, env) {
|
|
262
|
+
const known = new Map();
|
|
263
|
+
for (const step of flowSteps) {
|
|
264
|
+
const isObj = typeof step !== "string";
|
|
265
|
+
const instruction = isObj ? step.step : step;
|
|
266
|
+
if (!/^\s*(?:fill(?:\s+in)?|enter|type)\b/i.test(instruction))
|
|
267
|
+
continue;
|
|
268
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary) ?? deriveFillLabelField(instruction);
|
|
269
|
+
if (field === null)
|
|
270
|
+
continue;
|
|
271
|
+
const value = extractStepPersonaValue(instruction, env);
|
|
272
|
+
if (value === null)
|
|
273
|
+
continue;
|
|
274
|
+
if (!known.has(field))
|
|
275
|
+
known.set(field, value);
|
|
276
|
+
}
|
|
277
|
+
return known;
|
|
278
|
+
}
|
|
124
279
|
/**
|
|
125
280
|
* Extracts the concrete persona VALUE a flow step fills — the recon-supplied
|
|
126
281
|
* constant that appears verbatim in the captured request body — so the body
|
|
@@ -159,14 +314,10 @@ function extractStepPersonaValue(instruction, env) {
|
|
|
159
314
|
if (resolved)
|
|
160
315
|
return resolved;
|
|
161
316
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const quotes = [...cleaned.matchAll(/'([^']*)'/g)].map((m) => m[1]);
|
|
165
|
-
if (quotes.length === 0)
|
|
317
|
+
const spans = findQuoteSpans(instruction);
|
|
318
|
+
if (spans.length === 0)
|
|
166
319
|
return null;
|
|
167
|
-
const value =
|
|
168
|
-
? quotes[0]
|
|
169
|
-
: quotes[quotes.length - 1];
|
|
320
|
+
const value = pickValueSpan(instruction, spans).value;
|
|
170
321
|
return value.length > 0 ? value : null;
|
|
171
322
|
}
|
|
172
323
|
/**
|
|
@@ -214,10 +365,11 @@ function deriveFillLabelField(instruction) {
|
|
|
214
365
|
*/
|
|
215
366
|
function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
216
367
|
const bindings = new Map();
|
|
368
|
+
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary, env);
|
|
217
369
|
for (const step of flowSteps) {
|
|
218
370
|
const isObj = typeof step !== "string";
|
|
219
371
|
const instruction = isObj ? step.step : step;
|
|
220
|
-
const vocabField = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary);
|
|
372
|
+
const vocabField = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
221
373
|
// Vocabulary wins outright. Only on a miss do we fall back to deriving the
|
|
222
374
|
// field from the instruction's own label — and never when the author opted
|
|
223
375
|
// the step out (`payloadFieldNone`) or the vocabulary explicitly excluded it.
|
|
@@ -569,16 +721,22 @@ function firstGraphQLQuery(captures) {
|
|
|
569
721
|
* flow's own `payloadField` facets appearing in the candidate's query/variables,
|
|
570
722
|
* a non-landing capture phase, and how often the same operation re-fires are
|
|
571
723
|
* combined into one composite score.
|
|
724
|
+
*
|
|
725
|
+
* The `payloadField` facets are drawn from {@link resolveStepPayloadField} with
|
|
726
|
+
* {@link buildKnownFieldValues}'s known-value guard applied, so an operational
|
|
727
|
+
* Select answer (e.g. a device-type dropdown) never contributes a spurious
|
|
728
|
+
* facet match to the ranking.
|
|
572
729
|
*/
|
|
573
|
-
function selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary) {
|
|
730
|
+
function selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, env = process.env) {
|
|
574
731
|
const candidates = captures.filter((c) => c.status >= 200 && c.status < 300 && c.query !== null && !/^\s*mutation\b/.test(c.query));
|
|
575
732
|
if (candidates.length === 0)
|
|
576
733
|
return null;
|
|
734
|
+
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary, env);
|
|
577
735
|
const payloadFields = new Set();
|
|
578
736
|
for (const step of flowSteps) {
|
|
579
737
|
const isObj = typeof step !== "string";
|
|
580
738
|
const instruction = isObj ? step.step : step;
|
|
581
|
-
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary);
|
|
739
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
582
740
|
if (field !== null)
|
|
583
741
|
payloadFields.add(field);
|
|
584
742
|
}
|
|
@@ -1547,6 +1705,7 @@ function buildSelectOptionResolutions(flowSteps, captures, vocabulary, env) {
|
|
|
1547
1705
|
const resolutions = [];
|
|
1548
1706
|
const rawCodeFields = new Map();
|
|
1549
1707
|
const seenWireKeys = new Set();
|
|
1708
|
+
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary, env);
|
|
1550
1709
|
for (const step of flowSteps) {
|
|
1551
1710
|
const instruction = typeof step === "string" ? step : step.step;
|
|
1552
1711
|
if (!/^\s*(select|choose|pick)\b/i.test(instruction) && !/\bselect\b/i.test(instruction)) {
|
|
@@ -1604,7 +1763,7 @@ function buildSelectOptionResolutions(flowSteps, captures, vocabulary, env) {
|
|
|
1604
1763
|
}
|
|
1605
1764
|
// Fallback: no id= — resolve the wire key from the vocabulary persona field
|
|
1606
1765
|
// (lowercased) and the code from the global {label,value} map by label.
|
|
1607
|
-
const field = resolveStepPayloadField(instruction, typeof step === "string" ? undefined : step.payloadField, typeof step === "string" ? undefined : step.payloadFieldNone, vocabulary);
|
|
1766
|
+
const field = resolveStepPayloadField(instruction, typeof step === "string" ? undefined : step.payloadField, typeof step === "string" ? undefined : step.payloadFieldNone, vocabulary, knownFieldValues);
|
|
1608
1767
|
if (field === null)
|
|
1609
1768
|
continue;
|
|
1610
1769
|
const code = labelValue.get(label);
|
|
@@ -3243,12 +3402,54 @@ function bindOptionLiteral(headerBindings) {
|
|
|
3243
3402
|
// ── code emitters ─────────────────────────────────────────────────────────────
|
|
3244
3403
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
3245
3404
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
3405
|
+
/**
|
|
3406
|
+
* Splices payload fields into a string variable that packs filter facets as
|
|
3407
|
+
* delimited `key:value` segments (e.g. a product-catalog `filters` variable
|
|
3408
|
+
* shaped like `category:widgets|priceRange:10~50`) — the shape GraphQL search
|
|
3409
|
+
* endpoints commonly use instead of exposing each facet as its own top-level
|
|
3410
|
+
* variable, which is otherwise invisible to the key-name-equality strategy in
|
|
3411
|
+
* {@link renderGqlVariablesExpr}. Every facet segment whose key correlates
|
|
3412
|
+
* (case-insensitively) with one of `fields` gets its value slot spliced with
|
|
3413
|
+
* `payload.<Field>`; segments that don't correlate are left as their literal
|
|
3414
|
+
* `key:value` piece. Returns `null` for non-string values, strings with no
|
|
3415
|
+
* delimiter-separated `key:value` segment, and strings whose facet keys
|
|
3416
|
+
* correlate with none of `fields`, so opaque tokens, JSON blobs, and plain
|
|
3417
|
+
* literals fall through to the existing JSON.stringify path unchanged.
|
|
3418
|
+
*/
|
|
3419
|
+
function spliceFacetsIntoStringVariable(value, fields) {
|
|
3420
|
+
if (typeof value !== "string")
|
|
3421
|
+
return null;
|
|
3422
|
+
const segments = value.split(/([|,;])/);
|
|
3423
|
+
const hasFacetShape = segments.some((segment, index) => index % 2 === 0 && segment.includes(":"));
|
|
3424
|
+
if (!hasFacetShape)
|
|
3425
|
+
return null;
|
|
3426
|
+
let hasMatch = false;
|
|
3427
|
+
const body = segments
|
|
3428
|
+
.map((segment, index) => {
|
|
3429
|
+
if (index % 2 !== 0 || !segment.includes(":"))
|
|
3430
|
+
return escapeForTemplateLiteral(segment);
|
|
3431
|
+
const colonIndex = segment.indexOf(":");
|
|
3432
|
+
const facetKey = segment.slice(0, colonIndex);
|
|
3433
|
+
const matchedField = fields.find((field) => field.toLowerCase() === facetKey.toLowerCase());
|
|
3434
|
+
if (!matchedField)
|
|
3435
|
+
return escapeForTemplateLiteral(segment);
|
|
3436
|
+
hasMatch = true;
|
|
3437
|
+
return `${escapeForTemplateLiteral(`${facetKey}:`)}\${payload.${matchedField}}`;
|
|
3438
|
+
})
|
|
3439
|
+
.join("");
|
|
3440
|
+
if (!hasMatch)
|
|
3441
|
+
return null;
|
|
3442
|
+
return `\`${body}\``;
|
|
3443
|
+
}
|
|
3246
3444
|
/**
|
|
3247
3445
|
* Renders the variables literal for the primary-operation getGql() call —
|
|
3248
3446
|
* each key from the selected capture's own recorded variables is bound to
|
|
3249
3447
|
* `payload.<Field>` when it correlates (case-insensitively) with one of the
|
|
3250
|
-
* flow's payloadFieldNames
|
|
3251
|
-
*
|
|
3448
|
+
* flow's payloadFieldNames. When no top-level key correlates and the value is
|
|
3449
|
+
* a string packing facets in a delimited `key:value` grammar (see
|
|
3450
|
+
* {@link spliceFacetsIntoStringVariable}), a correlated facet's value slot is
|
|
3451
|
+
* spliced with `payload.<Field>` instead of freezing the whole string; any
|
|
3452
|
+
* other value is emitted verbatim via JSON.stringify.
|
|
3252
3453
|
*/
|
|
3253
3454
|
function renderGqlVariablesExpr(variables, payloadFieldNames) {
|
|
3254
3455
|
if (variables === null || typeof variables !== "object" || Array.isArray(variables))
|
|
@@ -3256,7 +3457,10 @@ function renderGqlVariablesExpr(variables, payloadFieldNames) {
|
|
|
3256
3457
|
const fields = payloadFieldNames ? [...payloadFieldNames] : [];
|
|
3257
3458
|
const entries = Object.entries(variables).map(([key, value]) => {
|
|
3258
3459
|
const matchedField = fields.find((field) => field.toLowerCase() === key.toLowerCase());
|
|
3259
|
-
const
|
|
3460
|
+
const facetSpliceExpr = matchedField ? null : spliceFacetsIntoStringVariable(value, fields);
|
|
3461
|
+
const valueExpr = matchedField
|
|
3462
|
+
? `payload.${matchedField}`
|
|
3463
|
+
: (facetSpliceExpr ?? JSON.stringify(value));
|
|
3260
3464
|
return `${key}: ${valueExpr}`;
|
|
3261
3465
|
});
|
|
3262
3466
|
return entries.length > 0 ? `{ ${entries.join(", ")} }` : "{}";
|
|
@@ -3308,37 +3512,82 @@ function emitContractTs(opts) {
|
|
|
3308
3512
|
// longer drives the public schema. A missing inputBody means this is a
|
|
3309
3513
|
// non-submission (query-type) flow, which keeps its own contract untouched.
|
|
3310
3514
|
const basePayloadSchemaExpr = inputBody
|
|
3311
|
-
? `ApplicantContactSchema
|
|
3515
|
+
? `ApplicantContactSchema`
|
|
3312
3516
|
: `z.object({\n query: z.string().min(1),\n})`;
|
|
3517
|
+
// Every field source below (the base extend's own keys, form-schema
|
|
3518
|
+
// discovery, browser-flow splicing, option/raw-option enums, additional
|
|
3519
|
+
// body keys, and structured keys) is merged into a SINGLE `.extend({...})`
|
|
3520
|
+
// object literal, keyed by field name, rather than each becoming its own
|
|
3521
|
+
// chained `.extend()` call. A name that recurs across sources collapses to
|
|
3522
|
+
// one declaration — the later source in this list wins, mirroring the
|
|
3523
|
+
// override semantics a chain of `.extend()` calls used to have (each
|
|
3524
|
+
// subsequent `.extend` replaced an earlier field of the same name).
|
|
3525
|
+
const extendFields = new Map();
|
|
3526
|
+
const addExtendField = (name, line) => {
|
|
3527
|
+
extendFields.set(name, line);
|
|
3528
|
+
};
|
|
3529
|
+
// The base extend's own keys — submission flows only.
|
|
3530
|
+
if (inputBody) {
|
|
3531
|
+
addExtendField("Email", " Email: z.email(),");
|
|
3532
|
+
addExtendField("ClickUrl", " ClickUrl: z.string().min(1),");
|
|
3533
|
+
addExtendField("Answers", " Answers: multipartJsonObject(z.record(z.string(), z.unknown())),");
|
|
3534
|
+
}
|
|
3535
|
+
// ApplicantContactSchema's own merged identity/address/resume field names
|
|
3536
|
+
// (see src/lib/application-identity.ts, application-address.ts,
|
|
3537
|
+
// application-resume.ts, applicant-payload.ts) — reserved so no discovered/
|
|
3538
|
+
// spliced source can redeclare (and silently shadow) a field the base
|
|
3539
|
+
// ApplicantContactSchema already supplies. Only relevant for submission
|
|
3540
|
+
// flows, where basePayloadSchemaExpr actually is ApplicantContactSchema.
|
|
3541
|
+
const applicantContactFieldNames = new Set([
|
|
3542
|
+
"FirstName",
|
|
3543
|
+
"LastName",
|
|
3544
|
+
"Phone",
|
|
3545
|
+
"AddressLine",
|
|
3546
|
+
"City",
|
|
3547
|
+
"State",
|
|
3548
|
+
"PostalCode",
|
|
3549
|
+
"Country",
|
|
3550
|
+
"County",
|
|
3551
|
+
"Resume",
|
|
3552
|
+
"ResumeContentType",
|
|
3553
|
+
"ResumeFilename",
|
|
3554
|
+
"ResumeBase64",
|
|
3555
|
+
]);
|
|
3556
|
+
const isReservedByApplicantContactSchema = (name) => Boolean(inputBody) && applicantContactFieldNames.has(name);
|
|
3557
|
+
// Multi-step flows that include a multipart upload need the binary asset
|
|
3558
|
+
// on the payload. A query-type flow (no ApplicantContactSchema base) still
|
|
3559
|
+
// needs these fields spelled out explicitly.
|
|
3560
|
+
if (hasMultipartStep && !inputBody) {
|
|
3561
|
+
addExtendField("Resume", " Resume: z.instanceof(Buffer),");
|
|
3562
|
+
addExtendField("ResumeContentType", " ResumeContentType: z.string(),");
|
|
3563
|
+
addExtendField("ResumeFilename", " ResumeFilename: z.string(),");
|
|
3564
|
+
}
|
|
3313
3565
|
// Form-schema-discovered fields (e.g. AddressLine1, UserSsn, Reference1FirstName)
|
|
3314
3566
|
// are added to the payload as required strings. Site-agnostic: the set is
|
|
3315
3567
|
// populated by applyFormSchemaSubstitutions when the recon includes a
|
|
3316
3568
|
// detectable form schema; empty for sites without one.
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
.
|
|
3322
|
-
|
|
3569
|
+
if (discoveredFormFields) {
|
|
3570
|
+
for (const name of [...discoveredFormFields].sort()) {
|
|
3571
|
+
if (isReservedByApplicantContactSchema(name))
|
|
3572
|
+
continue;
|
|
3573
|
+
addExtendField(name, ` ${name}: z.string(),`);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3323
3576
|
// Candidate-PII fields the browser flow splices as `payload.<field>`. Emitted
|
|
3324
3577
|
// as required strings (z.email() for Email per the repo's z.string().email()→
|
|
3325
3578
|
// z.email() migration) so those references typecheck in the generated flow.
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
.map((name) => ` ${name}: ${name === "Email" ? "z.email()" : "z.string()"},`)
|
|
3334
|
-
.join("\n")}\n})`
|
|
3335
|
-
: "";
|
|
3579
|
+
if (payloadFieldNames) {
|
|
3580
|
+
for (const name of [...payloadFieldNames].sort()) {
|
|
3581
|
+
if (isReservedByApplicantContactSchema(name))
|
|
3582
|
+
continue;
|
|
3583
|
+
addExtendField(name, ` ${name}: ${name === "Email" ? "z.email()" : "z.string()"},`);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3336
3586
|
// Build per-field OPT_<Name> constant declarations + payload-schema enum
|
|
3337
3587
|
// entries from the form schema's options. Only fields whose option-id
|
|
3338
3588
|
// slots were actually rewritten in the body (i.e. that appear in
|
|
3339
3589
|
// discoveredOptionFields) get emitted; the rest leave their schema entries
|
|
3340
|
-
// unused.
|
|
3341
|
-
// available for the final schema concat.
|
|
3590
|
+
// unused.
|
|
3342
3591
|
const emittedOptionMappings = [];
|
|
3343
3592
|
if (fieldOptionsMap && discoveredOptionFields && discoveredOptionFields.size > 0) {
|
|
3344
3593
|
for (const mapping of fieldOptionsMap.values()) {
|
|
@@ -3356,11 +3605,11 @@ function emitContractTs(opts) {
|
|
|
3356
3605
|
return `\nconst OPT_${mapping.semanticName} = {\n${entries}\n} as const;\n`;
|
|
3357
3606
|
})
|
|
3358
3607
|
.join("");
|
|
3359
|
-
const
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3608
|
+
for (const mapping of emittedOptionMappings) {
|
|
3609
|
+
if (isReservedByApplicantContactSchema(mapping.semanticName))
|
|
3610
|
+
continue;
|
|
3611
|
+
addExtendField(mapping.semanticName, ` ${mapping.semanticName}: z.enum([${mapping.options.map((o) => JSON.stringify(o.value)).join(", ")}]),`);
|
|
3612
|
+
}
|
|
3364
3613
|
// Phase E raw-option payload fields: options whose label strings are empty in
|
|
3365
3614
|
// the schema — no semantic enum is possible, so the caller supplies the
|
|
3366
3615
|
// option-id UUID directly. The recon-observed UUID is documented in a TSDoc
|
|
@@ -3368,11 +3617,11 @@ function emitContractTs(opts) {
|
|
|
3368
3617
|
const sortedRawOptionEntries = discoveredRawOptionFields
|
|
3369
3618
|
? [...discoveredRawOptionFields.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
3370
3619
|
: [];
|
|
3371
|
-
const
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3620
|
+
for (const [name, reconUuid] of sortedRawOptionEntries) {
|
|
3621
|
+
if (isReservedByApplicantContactSchema(name))
|
|
3622
|
+
continue;
|
|
3623
|
+
addExtendField(name, ` /** Recon-observed: ${reconUuid}. Caller supplies the option-id UUID for this field. */\n ${name}: z.string(),`);
|
|
3624
|
+
}
|
|
3376
3625
|
// A non-scalar (Mechanism B) field forces multipart wire encoding just like
|
|
3377
3626
|
// an upload step does: the multipart body encodes arrays/objects as
|
|
3378
3627
|
// JSON-stringified strings, so those fields need the same
|
|
@@ -3383,25 +3632,23 @@ function emitContractTs(opts) {
|
|
|
3383
3632
|
const sortedAdditionalKeys = discoveredAdditionalBodyKeys
|
|
3384
3633
|
? [...discoveredAdditionalBodyKeys.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
3385
3634
|
: [];
|
|
3386
|
-
const
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
.join("\n")}\n})`
|
|
3404
|
-
: "";
|
|
3635
|
+
for (const [name, kind] of sortedAdditionalKeys) {
|
|
3636
|
+
if (isReservedByApplicantContactSchema(name))
|
|
3637
|
+
continue;
|
|
3638
|
+
// Use multipartBoolean() for booleans when multipart is in play, so
|
|
3639
|
+
// multipart string-encoded "true"/"false" round-trip to native booleans
|
|
3640
|
+
// (matches the inputBody boolean handling for parity).
|
|
3641
|
+
const zod = kind === "string"
|
|
3642
|
+
? "z.string()"
|
|
3643
|
+
: kind === "number"
|
|
3644
|
+
? payloadNeedsMultipart
|
|
3645
|
+
? "z.coerce.number()"
|
|
3646
|
+
: "z.number()"
|
|
3647
|
+
: payloadNeedsMultipart
|
|
3648
|
+
? "multipartBoolean()"
|
|
3649
|
+
: "z.boolean()";
|
|
3650
|
+
addExtendField(name, ` ${name}: ${zod},`);
|
|
3651
|
+
}
|
|
3405
3652
|
// Mechanism B: nested caller structures become payload fields carrying their
|
|
3406
3653
|
// inferred schema. Emitted as an object body so multi-line z.array(z.object(
|
|
3407
3654
|
// …)) expressions indent cleanly; a leading TSDoc flags eventData's opaque
|
|
@@ -3409,15 +3656,13 @@ function emitContractTs(opts) {
|
|
|
3409
3656
|
const sortedStructuredEntries = discoveredStructuredKeys
|
|
3410
3657
|
? [...discoveredStructuredKeys.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
3411
3658
|
: [];
|
|
3412
|
-
const
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
.join("\n")}\n})`
|
|
3420
|
-
: "";
|
|
3659
|
+
for (const [name, schema] of sortedStructuredEntries) {
|
|
3660
|
+
if (isReservedByApplicantContactSchema(name))
|
|
3661
|
+
continue;
|
|
3662
|
+
const key = isValidJsIdentifier(name) ? name : JSON.stringify(name);
|
|
3663
|
+
const value = payloadNeedsMultipart ? `multipartJsonObject(${schema})` : schema;
|
|
3664
|
+
addExtendField(name, ` ${key}: ${value},`);
|
|
3665
|
+
}
|
|
3421
3666
|
// The structural walk over the captured request body that used to BE the
|
|
3422
3667
|
// public payload schema (see basePayloadSchemaExpr above) is still the
|
|
3423
3668
|
// right starting point for the plugin author's internal builder — it's
|
|
@@ -3430,18 +3675,18 @@ function emitContractTs(opts) {
|
|
|
3430
3675
|
const internalRequestReferenceExpr = inputBody
|
|
3431
3676
|
? inferZodSchema(inputBody, 0, "", { multipartCoerce: hasMultipartStep })
|
|
3432
3677
|
: null;
|
|
3433
|
-
//
|
|
3434
|
-
//
|
|
3435
|
-
//
|
|
3436
|
-
//
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
//
|
|
3442
|
-
//
|
|
3443
|
-
//
|
|
3444
|
-
//
|
|
3678
|
+
// All field sources above are merged into a SINGLE `.extend({...})` object
|
|
3679
|
+
// literal, keyed by field name — a name that recurs across sources (or
|
|
3680
|
+
// that collides with the base extend's own Email/ClickUrl/Answers) collapses
|
|
3681
|
+
// to its last-declared line, rather than becoming a second, dupe-prone
|
|
3682
|
+
// `.extend()` call chained onto the schema.
|
|
3683
|
+
const mergedExtension = extendFields.size > 0 ? `.extend({\n${[...extendFields.values()].join("\n")}\n})` : "";
|
|
3684
|
+
const payloadSchemaExpr = `${basePayloadSchemaExpr}${mergedExtension}`;
|
|
3685
|
+
// basePayloadSchemaExpr's own Answers field always wraps in
|
|
3686
|
+
// multipartJsonObject() for submission flows (inputBody set);
|
|
3687
|
+
// multipartBoolean() and the structured-keys wrapping above are needed
|
|
3688
|
+
// whenever payloadNeedsMultipart is true (an upload step OR a non-scalar
|
|
3689
|
+
// discoveredStructuredKeys field).
|
|
3445
3690
|
// Named imports from the same module are combined into one import statement.
|
|
3446
3691
|
const zodMultipartNamedImports = [
|
|
3447
3692
|
...(payloadNeedsMultipart ? ["multipartBoolean"] : []),
|
|
@@ -3585,7 +3830,7 @@ const limiter = new Bottleneck({ minTime: ${minTime} });
|
|
|
3585
3830
|
: `
|
|
3586
3831
|
/** Hot path: direct HTTP — no browser, no LLM tokens. */
|
|
3587
3832
|
async executeHttp(
|
|
3588
|
-
payload: ${pascal}Payload,
|
|
3833
|
+
${executeHttpBody.includes("payload.") ? "payload" : "_payload"}: ${pascal}Payload,
|
|
3589
3834
|
${executeHttpBody.includes("context.") ? "context" : "_context"}: SitePluginContext
|
|
3590
3835
|
): Promise<SitePluginResult<${pascal}Response>> {
|
|
3591
3836
|
${executeHttpBody}
|
|
@@ -3680,59 +3925,50 @@ function escapeForTemplateLiteral(segment) {
|
|
|
3680
3925
|
* Build the emitted instruction expression for one step: a plain double-quoted
|
|
3681
3926
|
* literal when nothing splices, or a backtick template literal with the recon
|
|
3682
3927
|
* constant replaced by `${payload.<field>}` when the resolver picks a field.
|
|
3683
|
-
* The
|
|
3684
|
-
*
|
|
3928
|
+
* The splice site is located by {@link locateSpliceSite} — a reserved
|
|
3929
|
+
* `${RECON_*}` env token when present, otherwise the quoted VALUE span (never
|
|
3930
|
+
* a selector's or label's quoted span), matching {@link extractStepPersonaValue}'s
|
|
3931
|
+
* own choice so the browser-flow and HTTP-body emitters never disagree on
|
|
3932
|
+
* which quoted span is the persona value.
|
|
3685
3933
|
*/
|
|
3686
3934
|
function buildStepInstructionExpr(instruction, field) {
|
|
3687
3935
|
if (field === null)
|
|
3688
3936
|
return JSON.stringify(instruction);
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
const emailToken = `$${"{RECON_EMAIL}"}`;
|
|
3692
|
-
const emailIdx = instruction.indexOf(emailToken);
|
|
3693
|
-
const [before, matched, after] = emailIdx >= 0
|
|
3694
|
-
? [
|
|
3695
|
-
instruction.slice(0, emailIdx),
|
|
3696
|
-
emailToken,
|
|
3697
|
-
instruction.slice(emailIdx + emailToken.length),
|
|
3698
|
-
]
|
|
3699
|
-
: (() => {
|
|
3700
|
-
const m = /'[^']*'/.exec(instruction);
|
|
3701
|
-
if (m === null)
|
|
3702
|
-
return [instruction, "", ""];
|
|
3703
|
-
return [
|
|
3704
|
-
instruction.slice(0, m.index),
|
|
3705
|
-
m[0],
|
|
3706
|
-
instruction.slice(m.index + m[0].length),
|
|
3707
|
-
];
|
|
3708
|
-
})();
|
|
3709
|
-
if (matched === "")
|
|
3937
|
+
const site = locateSpliceSite(instruction);
|
|
3938
|
+
if (site === null)
|
|
3710
3939
|
return JSON.stringify(instruction);
|
|
3711
|
-
return `\`${escapeForTemplateLiteral(before)}\${payload.${field}}${escapeForTemplateLiteral(after)}\``;
|
|
3940
|
+
return `\`${escapeForTemplateLiteral(site.before)}\${payload.${field}}${escapeForTemplateLiteral(site.after)}\``;
|
|
3712
3941
|
}
|
|
3713
3942
|
/**
|
|
3714
|
-
*
|
|
3715
|
-
*
|
|
3716
|
-
*
|
|
3717
|
-
*
|
|
3718
|
-
*
|
|
3943
|
+
* Build the emitted instruction expression for a step whose splice site is the
|
|
3944
|
+
* reserved `${RECON_PASSWORD}` token: a backtick template literal with the
|
|
3945
|
+
* token replaced by `${throwawayPassword}`, the per-run credential minted by
|
|
3946
|
+
* {@link generateThrowawayPassword} — never the recon capture's literal
|
|
3947
|
+
* password, and never routed through `payload.<field>` since no caller-
|
|
3948
|
+
* supplied Password field exists on the applicant payload.
|
|
3949
|
+
*/
|
|
3950
|
+
function buildPasswordInstructionExpr(instruction) {
|
|
3951
|
+
const site = locateSpliceSite(instruction);
|
|
3952
|
+
if (site === null)
|
|
3953
|
+
return JSON.stringify(instruction);
|
|
3954
|
+
return `\`${escapeForTemplateLiteral(site.before)}\${throwawayPassword}${escapeForTemplateLiteral(site.after)}\``;
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* Rewrites one step instruction into the config-manifest templating form: the
|
|
3958
|
+
* splice site located by {@link locateSpliceSite} becomes `{{ .request.<field> }}`.
|
|
3959
|
+
* Unlike {@link buildStepInstructionExpr} this yields a plain manifest string,
|
|
3960
|
+
* not a TS expression — the runtime config-plugin resolver, not the code
|
|
3961
|
+
* generator, performs the splice. Reuses {@link locateSpliceSite} so this
|
|
3962
|
+
* emitter never lands the splice on a selector's or label's quoted span, the
|
|
3963
|
+
* same guarantee {@link buildStepInstructionExpr} makes.
|
|
3719
3964
|
*/
|
|
3720
3965
|
function buildManifestInstruction(instruction, field) {
|
|
3721
3966
|
if (field === null)
|
|
3722
3967
|
return instruction;
|
|
3723
|
-
const
|
|
3724
|
-
|
|
3725
|
-
if (emailIdx >= 0) {
|
|
3726
|
-
return (instruction.slice(0, emailIdx) +
|
|
3727
|
-
`{{ .request.${field} }}` +
|
|
3728
|
-
instruction.slice(emailIdx + emailToken.length));
|
|
3729
|
-
}
|
|
3730
|
-
const m = /'[^']*'/.exec(instruction);
|
|
3731
|
-
if (m === null)
|
|
3968
|
+
const site = locateSpliceSite(instruction);
|
|
3969
|
+
if (site === null)
|
|
3732
3970
|
return instruction;
|
|
3733
|
-
return
|
|
3734
|
-
`{{ .request.${field} }}` +
|
|
3735
|
-
instruction.slice(m.index + m[0].length));
|
|
3971
|
+
return `${site.before}{{ .request.${field} }}${site.after}`;
|
|
3736
3972
|
}
|
|
3737
3973
|
/**
|
|
3738
3974
|
* The JSON Schema `type` keyword for a sample value. Just the keyword, not a
|
|
@@ -3774,12 +4010,28 @@ function jsonSchemaTypeOf(value) {
|
|
|
3774
4010
|
* browser `flow` is the only execution path, and the field is omitted.
|
|
3775
4011
|
*/
|
|
3776
4012
|
function emitConfigManifest(opts) {
|
|
3777
|
-
const { siteId, displayName, baseUrl, flowSteps, vocabulary, inputBody, recoveredFields, httpModulePath, } = opts;
|
|
4013
|
+
const { siteId, displayName, baseUrl, flowSteps, vocabulary, inputBody, recoveredFields, httpModulePath, env = process.env, } = opts;
|
|
3778
4014
|
const payloadFieldNames = new Set();
|
|
4015
|
+
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary ?? vocabulary_1.EMPTY_VOCABULARY, env);
|
|
3779
4016
|
const steps = flowSteps.map((step) => {
|
|
3780
4017
|
const isObj = typeof step !== "string";
|
|
3781
4018
|
const instruction = isObj ? step.step : step;
|
|
3782
|
-
|
|
4019
|
+
// A config-only manifest has no compiled code to mint a throwaway credential
|
|
4020
|
+
// at runtime (unlike emitBrowserFlowTs's generateThrowawayPassword() splice),
|
|
4021
|
+
// so ${RECON_PASSWORD} is routed to an explicit "Password" request field
|
|
4022
|
+
// instead — the operator supplies it at call time. Either way the literal
|
|
4023
|
+
// token must never survive into the manifest.
|
|
4024
|
+
if (instruction.includes(RECON_PASSWORD_TOKEN)) {
|
|
4025
|
+
payloadFieldNames.add("Password");
|
|
4026
|
+
const rewritten = buildManifestInstruction(instruction, "Password");
|
|
4027
|
+
const optional = isObj ? step.optional === true : false;
|
|
4028
|
+
const upload = isObj ? step.upload === true : false;
|
|
4029
|
+
const submitStep = isObj ? step.submitStep === true : false;
|
|
4030
|
+
if (!optional && !upload && !submitStep)
|
|
4031
|
+
return rewritten;
|
|
4032
|
+
return { step: rewritten, optional, upload, submitStep };
|
|
4033
|
+
}
|
|
4034
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
3783
4035
|
if (field !== null)
|
|
3784
4036
|
payloadFieldNames.add(field);
|
|
3785
4037
|
const rewritten = buildManifestInstruction(instruction, field);
|
|
@@ -3840,13 +4092,28 @@ function emitConfigManifest(opts) {
|
|
|
3840
4092
|
* payload schema (both are driven by this same set).
|
|
3841
4093
|
*/
|
|
3842
4094
|
function emitBrowserFlowTs(opts) {
|
|
3843
|
-
const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false, vocabulary, frameSelector, } = opts;
|
|
4095
|
+
const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false, vocabulary, frameSelector, env = process.env, } = opts;
|
|
3844
4096
|
const payloadFieldNames = new Set();
|
|
3845
4097
|
const hasUploadStep = flowSteps.some((s) => typeof s !== "string" && s.upload === true);
|
|
4098
|
+
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary ?? vocabulary_1.EMPTY_VOCABULARY, env);
|
|
4099
|
+
let usesThrowawayPassword = false;
|
|
3846
4100
|
const stepLiterals = flowSteps.map((step) => {
|
|
3847
4101
|
const isObj = typeof step !== "string";
|
|
3848
4102
|
const instruction = isObj ? step.step : step;
|
|
3849
|
-
|
|
4103
|
+
// ${RECON_PASSWORD} is reserved-tooling, not a domain-vocabulary concern —
|
|
4104
|
+
// it names a credential the recon capture needed to authenticate, not a
|
|
4105
|
+
// piece of the caller's applicant identity, so it never reaches
|
|
4106
|
+
// resolveStepPayloadField/vocabulary and never routes through
|
|
4107
|
+
// payload.<field>. It gets a generated throwaway credential instead.
|
|
4108
|
+
if (instruction.includes(RECON_PASSWORD_TOKEN)) {
|
|
4109
|
+
usesThrowawayPassword = true;
|
|
4110
|
+
const instructionExpr = buildPasswordInstructionExpr(instruction);
|
|
4111
|
+
const optional = isObj ? step.optional === true : false;
|
|
4112
|
+
const upload = isObj ? step.upload === true : false;
|
|
4113
|
+
const submitStep = isObj ? step.submitStep === true : false;
|
|
4114
|
+
return ` { instruction: ${instructionExpr}, optional: ${optional}, upload: ${upload}, submitStep: ${submitStep} },`;
|
|
4115
|
+
}
|
|
4116
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
3850
4117
|
if (field !== null)
|
|
3851
4118
|
payloadFieldNames.add(field);
|
|
3852
4119
|
const instructionExpr = buildStepInstructionExpr(instruction, field);
|
|
@@ -3869,7 +4136,7 @@ function emitBrowserFlowTs(opts) {
|
|
|
3869
4136
|
// the multipart contract fields and wires the fixture during hand-finish.
|
|
3870
4137
|
const uploadFixtureExpr = hasUploadStep && hasMultipartStep
|
|
3871
4138
|
? `{
|
|
3872
|
-
buffer: Buffer.from(payload.Resume
|
|
4139
|
+
buffer: Buffer.from(payload.Resume),
|
|
3873
4140
|
name: payload.ResumeFilename ?? "resume.pdf",
|
|
3874
4141
|
mimeType: payload.ResumeContentType ?? "application/pdf",
|
|
3875
4142
|
}`
|
|
@@ -3892,7 +4159,7 @@ import type { Stagehand } from "@browserbasehq/stagehand";
|
|
|
3892
4159
|
import { z } from "zod/v4";
|
|
3893
4160
|
|
|
3894
4161
|
import { buildAnthropicClient, buildRephraseModel } from "${ENGINE_PKG}/lib/llm/anthropic-client";
|
|
3895
|
-
import { getLogger } from "${ENGINE_PKG}/lib/logging"
|
|
4162
|
+
import { getLogger } from "${ENGINE_PKG}/lib/logging";${usesThrowawayPassword ? `\nimport { generateThrowawayPassword } from "${ENGINE_PKG}/lib/random";` : ""}
|
|
3896
4163
|
import { type HealingFlowStep, runHealingFlow, waitForSpaReady } from "${ENGINE_PKG}/scraper/flow-runner";
|
|
3897
4164
|
import { guardedExtract } from "${ENGINE_PKG}/scraper/stagehand-guard";
|
|
3898
4165
|
import type { ${pascal}Payload, ${pascal}Response } from "@/sites/${siteId}/contract";
|
|
@@ -3920,7 +4187,7 @@ export async function run${pascal}BrowserFlow(
|
|
|
3920
4187
|
// networkidle can resolve before a Cloudflare-fronted SPA hydrates; wait for
|
|
3921
4188
|
// the real DOM so the first steps don't probe an empty shell page and skip.
|
|
3922
4189
|
await waitForSpaReady(page, logger);
|
|
3923
|
-
|
|
4190
|
+
${usesThrowawayPassword ? "\n // Minted once per run — the flow needs a credential to authenticate, but\n // there is no caller-supplied Password field on the payload to splice.\n const throwawayPassword = generateThrowawayPassword();\n" : ""}
|
|
3924
4191
|
const FLOW_STEPS: HealingFlowStep[] = [
|
|
3925
4192
|
${flowStepsBlock}
|
|
3926
4193
|
];
|