@enricai/barnacle 1.12.43 → 1.12.45

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 (51) hide show
  1. package/dist/config.d.ts +19 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +18 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/lib/tracking-click.js +2 -2
  6. package/dist/lib/tracking-click.js.map +1 -1
  7. package/dist/plugins/config-plugin.d.ts.map +1 -1
  8. package/dist/plugins/config-plugin.js +1 -0
  9. package/dist/plugins/config-plugin.js.map +1 -1
  10. package/dist/scraper/captcha-solver.d.ts +7 -0
  11. package/dist/scraper/captcha-solver.d.ts.map +1 -1
  12. package/dist/scraper/captcha-solver.js +11 -2
  13. package/dist/scraper/captcha-solver.js.map +1 -1
  14. package/dist/scraper/flow-runner.d.ts +15 -0
  15. package/dist/scraper/flow-runner.d.ts.map +1 -1
  16. package/dist/scraper/flow-runner.js +3 -1
  17. package/dist/scraper/flow-runner.js.map +1 -1
  18. package/dist/scraper/session-browserbase.d.ts +6 -3
  19. package/dist/scraper/session-browserbase.d.ts.map +1 -1
  20. package/dist/scraper/session-browserbase.js +52 -5
  21. package/dist/scraper/session-browserbase.js.map +1 -1
  22. package/dist/scraper/session-proxy.d.ts +18 -0
  23. package/dist/scraper/session-proxy.d.ts.map +1 -0
  24. package/dist/scraper/session-proxy.js +19 -0
  25. package/dist/scraper/session-proxy.js.map +1 -0
  26. package/dist/scraper/session-shared.d.ts +29 -0
  27. package/dist/scraper/session-shared.d.ts.map +1 -1
  28. package/dist/scraper/session-shared.js.map +1 -1
  29. package/dist/scraper/session-steel.d.ts.map +1 -1
  30. package/dist/scraper/session-steel.js +11 -0
  31. package/dist/scraper/session-steel.js.map +1 -1
  32. package/dist/scraper/session-teardown.d.ts +19 -0
  33. package/dist/scraper/session-teardown.d.ts.map +1 -1
  34. package/dist/scraper/session-teardown.js +22 -0
  35. package/dist/scraper/session-teardown.js.map +1 -1
  36. package/dist/scripts/recon-browser.d.ts.map +1 -1
  37. package/dist/scripts/recon-browser.js +12 -0
  38. package/dist/scripts/recon-browser.js.map +1 -1
  39. package/dist/scripts/recon-generate-multicall-fixture.d.ts +14 -0
  40. package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
  41. package/dist/scripts/recon-generate-multicall-fixture.js +72 -0
  42. package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
  43. package/dist/scripts/recon-generate.d.ts +13 -3
  44. package/dist/scripts/recon-generate.d.ts.map +1 -1
  45. package/dist/scripts/recon-generate.js +159 -89
  46. package/dist/scripts/recon-generate.js.map +1 -1
  47. package/dist/types/session-proxy.d.ts +11 -0
  48. package/dist/types/session-proxy.d.ts.map +1 -0
  49. package/dist/types/session-proxy.js +3 -0
  50. package/dist/types/session-proxy.js.map +1 -0
  51. package/package.json +5 -1
@@ -38,6 +38,7 @@ exports.gatherResponseBodySamples = gatherResponseBodySamples;
38
38
  exports.selectPrimaryGraphQLOperation = selectPrimaryGraphQLOperation;
39
39
  exports.firstEndpointCapture = firstEndpointCapture;
40
40
  exports.firstEndpointPath = firstEndpointPath;
41
+ exports.truncateActionSequenceAtSubmitPattern = truncateActionSequenceAtSubmitPattern;
41
42
  exports.resolveManifestActionSequence = resolveManifestActionSequence;
42
43
  exports.extractActionSequence = extractActionSequence;
43
44
  exports.extractGraphQLActionSequence = extractGraphQLActionSequence;
@@ -1102,12 +1103,31 @@ function isGraphQL(captures) {
1102
1103
  * `selectPrimaryGraphQLOperation` winner) resolves its query, endpoint, and
1103
1104
  * operationName from — all three MUST trace back to this same capture, not
1104
1105
  * three independent array scans that could each land on a different one.
1105
- */
1106
- function firstGraphQLCapture(captures, ownBackendHostnames = [], fallbackDomain = null) {
1106
+ *
1107
+ * `submitPatterns` (declared after this function so the types can appear
1108
+ * before the interface they use) restricts the search to non-mutation
1109
+ * (query) captures matching the pattern when the flow declares one AND at
1110
+ * least one own-backend query capture matches it — mirroring
1111
+ * {@link truncateActionSequenceAtSubmitPattern}'s never-hard-fail fallback
1112
+ * to the unfiltered pool. The restriction only ever narrows within the
1113
+ * query captures, never promotes a mutation over them: this fallback exists
1114
+ * specifically for the read op a bypassed {@link selectPrimaryGraphQLOperation}
1115
+ * would have picked, and a declared pattern that happens to match the
1116
+ * flow's own mutation capture (its real submission step, resolved
1117
+ * elsewhere) must not hijack this read-op resolution.
1118
+ */
1119
+ function firstGraphQLCapture(captures, ownBackendHostnames = [], fallbackDomain = null, submitPatterns = null) {
1107
1120
  const hasHostProvenance = ownBackendHostnames.length > 0 || fallbackDomain !== null;
1108
- return (captures.find((c) => c.query &&
1121
+ const ownBackendCandidates = captures.filter((c) => c.query &&
1109
1122
  (!hasHostProvenance ||
1110
- (0, capture_filters_1.isAllowedFixtureHost)(captureHostname(c.url), ownBackendHostnames, fallbackDomain))) ?? null);
1123
+ (0, capture_filters_1.isAllowedFixtureHost)(captureHostname(c.url), ownBackendHostnames, fallbackDomain)));
1124
+ if (submitPatterns === null ||
1125
+ (submitPatterns.endpoint === null && submitPatterns.body === null)) {
1126
+ return ownBackendCandidates[0] ?? null;
1127
+ }
1128
+ const nonMutationCandidates = ownBackendCandidates.filter((c) => !/^\s*mutation\b/.test(c.query ?? ""));
1129
+ const pool = nonMutationCandidates.length > 0 ? nonMutationCandidates : ownBackendCandidates;
1130
+ return restrictToSubmitPattern(pool, submitPatterns)[0] ?? null;
1111
1131
  }
1112
1132
  /**
1113
1133
  * Strips leading blank lines and `#`-comment lines (GraphQL comments run
@@ -1216,19 +1236,24 @@ function isPopulatedVariableValue(value) {
1216
1236
  * Select answer (e.g. a device-type dropdown) never contributes a spurious
1217
1237
  * facet match to the ranking.
1218
1238
  */
1219
- function selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, env = process.env, ownBackendHostnames = [], fallbackDomain = null) {
1239
+ function selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, env = process.env, ownBackendHostnames = [], fallbackDomain = null, submitPatterns = null) {
1220
1240
  // Callers with no host-provenance data (the exported function's unit
1221
1241
  // tests) pass neither ownBackendHostnames nor fallbackDomain — in that
1222
1242
  // case isAllowedFixtureHost would reject every candidate, so the gate
1223
1243
  // only applies once the caller has actually resolved a notion of "own
1224
1244
  // backend" to check against.
1225
1245
  const hasHostProvenance = ownBackendHostnames.length > 0 || fallbackDomain !== null;
1226
- const candidates = captures.filter((c) => c.status >= 200 &&
1246
+ const ownBackendCandidates = captures.filter((c) => c.status >= 200 &&
1227
1247
  c.status < 300 &&
1228
1248
  c.query !== null &&
1229
1249
  !/^\s*mutation\b/.test(c.query) &&
1230
1250
  (!hasHostProvenance ||
1231
1251
  (0, capture_filters_1.isAllowedFixtureHost)(captureHostname(c.url), ownBackendHostnames, fallbackDomain)));
1252
+ // A declared submitEndpointPattern is authoritative for single-endpoint
1253
+ // primary selection too: it must not be silently ignored just because
1254
+ // this scoring path (not the multi-step submission path) is the one that
1255
+ // ends up resolving the flow's single action.
1256
+ const candidates = restrictToSubmitPattern(ownBackendCandidates, submitPatterns);
1232
1257
  if (candidates.length === 0)
1233
1258
  return null;
1234
1259
  const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary, env);
@@ -1322,7 +1347,7 @@ function selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, env = pr
1322
1347
  * derives a path string from, so non-GraphQL flows can also read that
1323
1348
  * capture's own `.responseBody` instead of an array-order-first replay.
1324
1349
  */
1325
- function firstEndpointCapture(captures, ownBackendHostnames = [], fallbackDomain = null) {
1350
+ function firstEndpointCapture(captures, ownBackendHostnames = [], fallbackDomain = null, submitPatterns = null) {
1326
1351
  // Callers with no host-provenance data (unit tests exercising the
1327
1352
  // chronological-first fallback in isolation) pass neither argument -- in
1328
1353
  // that case isAllowedFixtureHost would reject every candidate, so the
@@ -1331,7 +1356,7 @@ function firstEndpointCapture(captures, ownBackendHostnames = [], fallbackDomain
1331
1356
  const hasHostProvenance = ownBackendHostnames.length > 0 || fallbackDomain !== null;
1332
1357
  const allowed = (c) => !hasHostProvenance ||
1333
1358
  (0, capture_filters_1.isAllowedFixtureHost)(captureHostname(c.url), ownBackendHostnames, fallbackDomain);
1334
- const nonGetCaptures = captures.filter((c) => c.method !== "GET" && allowed(c));
1359
+ const nonGetCaptures = restrictToSubmitPattern(captures.filter((c) => c.method !== "GET" && allowed(c)), submitPatterns);
1335
1360
  for (const c of nonGetCaptures) {
1336
1361
  try {
1337
1362
  new URL(c.url);
@@ -1341,7 +1366,7 @@ function firstEndpointCapture(captures, ownBackendHostnames = [], fallbackDomain
1341
1366
  // skip
1342
1367
  }
1343
1368
  }
1344
- for (const c of captures.filter(allowed)) {
1369
+ for (const c of restrictToSubmitPattern(captures.filter(allowed), submitPatterns)) {
1345
1370
  try {
1346
1371
  new URL(c.url);
1347
1372
  return c;
@@ -1360,9 +1385,9 @@ function safeUrlPathname(url) {
1360
1385
  return "/api/search";
1361
1386
  }
1362
1387
  }
1363
- function firstEndpointPath(captures, ownBackendHostnames = [], fallbackDomain = null) {
1388
+ function firstEndpointPath(captures, ownBackendHostnames = [], fallbackDomain = null, submitPatterns = null) {
1364
1389
  try {
1365
- const capture = firstEndpointCapture(captures, ownBackendHostnames, fallbackDomain);
1390
+ const capture = firstEndpointCapture(captures, ownBackendHostnames, fallbackDomain, submitPatterns);
1366
1391
  return capture ? new URL(capture.url).pathname : "/api/search";
1367
1392
  }
1368
1393
  catch {
@@ -1390,6 +1415,39 @@ function compileSubmitMatcher(patterns) {
1390
1415
  return true;
1391
1416
  };
1392
1417
  }
1418
+ /**
1419
+ * Restricts a single-endpoint primary-capture candidate pool to captures
1420
+ * matching the flow's declared submit pattern, when one is declared AND at
1421
+ * least one candidate in the pool matches it. A pattern that matches nothing
1422
+ * among the candidates falls back to the unfiltered pool — mirroring
1423
+ * {@link truncateActionSequenceAtSubmitPattern}'s never-hard-fail philosophy
1424
+ * — so `firstEndpointCapture`/`firstEndpointPath`/`firstGraphQLCapture`/
1425
+ * `selectPrimaryGraphQLOperation` never lose a winner over a pattern that
1426
+ * simply doesn't apply to this candidate pool.
1427
+ */
1428
+ function restrictToSubmitPattern(pool, submitPatterns) {
1429
+ const matchesSubmit = compileSubmitMatcher(submitPatterns);
1430
+ const matching = pool.filter((c) => matchesSubmit(c));
1431
+ return matching.length > 0 ? matching : pool;
1432
+ }
1433
+ /**
1434
+ * Truncates a host-gated action sequence at the LAST capture matching the
1435
+ * flow's declared submit pattern, keeping every capture up to and including
1436
+ * it. A pattern that names only the wizard's final step must not collapse a
1437
+ * multi-step chain (auth mint, paged listing, terminal submit) down to the
1438
+ * bare matching capture(s) — a hard filter would drop the earlier steps the
1439
+ * fold plan and state-threading depend on. When nothing matches, the result
1440
+ * is empty, matching what a hard filter would have produced.
1441
+ */
1442
+ function truncateActionSequenceAtSubmitPattern(sequence, submitPatterns) {
1443
+ const matchesSubmit = compileSubmitMatcher(submitPatterns);
1444
+ let lastMatchIndex = -1;
1445
+ for (let i = 0; i < sequence.length; i++) {
1446
+ if (matchesSubmit(sequence[i].capture))
1447
+ lastMatchIndex = i;
1448
+ }
1449
+ return sequence.slice(0, lastMatchIndex + 1);
1450
+ }
1393
1451
  /**
1394
1452
  * Reads `submit-manifest.json` (written by recon-browser) and resolves it to the
1395
1453
  * authoritative submission action sequence. This is the deepest submit-selection
@@ -7020,19 +7078,46 @@ function emitContractTs(opts) {
7020
7078
  //
7021
7079
  // The captured request body (inputBody) is the SITE's internal request
7022
7080
  // shape (a vendor's ddoKey/formData, a GraphQL worklet's variables, …) — not
7023
- // what the real caller sends. The plugin's buildBarnacleFormData posts
7024
- // the standard candidate payload (ApplicantContactSchema's identity/
7025
- // address/resume fields + Email + job-targeting + a JSON Answers block) to
7026
- // every plugin's /run, so that — not a structural inference over
7027
- // inputBody — is the public contract every submission-flow plugin must
7028
- // declare, unconditionally (see recon-generate-payload-schema-mismatch.md
7029
- // fix option (a)). inputBody remains available to the plugin author as the
7030
- // internal request shape the site's own call needs to be built from; it no
7031
- // longer drives the public schema. A missing inputBody means this is a
7032
- // non-submission (query-type) flow, which keeps its own contract untouched.
7033
- const basePayloadSchemaExpr = inputBody
7081
+ // necessarily the real caller's shape. ApplicantContactSchema (the job-
7082
+ // application template: identity/address/resume fields + Email + a JSON
7083
+ // Answers block) only belongs on flows that are actually job/benefits
7084
+ // applications — selecting it purely because SOME body was captured picks
7085
+ // it for unrelated submission flows too. usesApplicantContactSchema is the
7086
+ // real-evidence gate: it requires the captured inputBody to itself carry
7087
+ // one of ApplicantContactSchema's own field names (case-insensitively,
7088
+ // checked at any depth via walkAllPrimitiveLeaves) before the template
7089
+ // applies (see recon-generate-payload-schema-mismatch.md).
7090
+ const applicantContactFieldNames = new Set([
7091
+ "FirstName",
7092
+ "LastName",
7093
+ "Phone",
7094
+ "AddressLine",
7095
+ "City",
7096
+ "State",
7097
+ "PostalCode",
7098
+ "Country",
7099
+ "County",
7100
+ "Resume",
7101
+ "ResumeContentType",
7102
+ "ResumeFilename",
7103
+ "ResumeBase64",
7104
+ ]);
7105
+ const baseContractFieldNames = new Set(["Email", "ClickUrl", "Answers"]);
7106
+ const applicantContactEvidenceFieldNamesLower = new Set([...applicantContactFieldNames, ...baseContractFieldNames].map((name) => name.toLowerCase()));
7107
+ const usesApplicantContactSchema = inputBody != null &&
7108
+ [...walkAllPrimitiveLeaves(inputBody)].some(({ path }) => path.some((segment) => applicantContactEvidenceFieldNamesLower.has(segment.toLowerCase())));
7109
+ const isReservedByApplicantContactSchema = (name) => usesApplicantContactSchema && applicantContactFieldNames.has(name);
7110
+ // A missing inputBody, or one with no ApplicantContactSchema evidence,
7111
+ // means this is not a job-application submission flow. The former keeps
7112
+ // its own read-flow `{ query }` contract untouched; the latter (a body WAS
7113
+ // captured, it just isn't an application) gets a plain empty base instead
7114
+ // — extend() below still layers on every genuinely-discovered field from
7115
+ // the other sources unchanged.
7116
+ const basePayloadSchemaExpr = usesApplicantContactSchema
7034
7117
  ? `ApplicantContactSchema`
7035
- : `z.object({\n query: z.string().min(1),\n})`;
7118
+ : inputBody
7119
+ ? `z.object({})`
7120
+ : `z.object({\n query: z.string().min(1),\n})`;
7036
7121
  // Only the single-endpoint GraphQL read path (a real primary operation, no
7037
7122
  // multi-step flow) is a candidate for a paging signal — multiStepBody
7038
7123
  // already owns its own per-call semantics.
@@ -7095,34 +7180,12 @@ function emitContractTs(opts) {
7095
7180
  if (paginationSignal) {
7096
7181
  addExtendField("maxPages", " maxPages: z.number().int().positive().optional(),");
7097
7182
  }
7098
- // The base extend's own keys — submission flows only.
7099
- if (inputBody) {
7183
+ // The base extend's own keys — job-application submission flows only.
7184
+ if (usesApplicantContactSchema) {
7100
7185
  addExtendField("Email", " Email: z.email(),");
7101
7186
  addExtendField("ClickUrl", " ClickUrl: z.string().min(1),");
7102
7187
  addExtendField("Answers", " Answers: multipartJsonObject(z.record(z.string(), z.unknown())),");
7103
7188
  }
7104
- // ApplicantContactSchema's own merged identity/address/resume field names
7105
- // (see src/lib/application-identity.ts, application-address.ts,
7106
- // application-resume.ts, applicant-payload.ts) — reserved so no discovered/
7107
- // spliced source can redeclare (and silently shadow) a field the base
7108
- // ApplicantContactSchema already supplies. Only relevant for submission
7109
- // flows, where basePayloadSchemaExpr actually is ApplicantContactSchema.
7110
- const applicantContactFieldNames = new Set([
7111
- "FirstName",
7112
- "LastName",
7113
- "Phone",
7114
- "AddressLine",
7115
- "City",
7116
- "State",
7117
- "PostalCode",
7118
- "Country",
7119
- "County",
7120
- "Resume",
7121
- "ResumeContentType",
7122
- "ResumeFilename",
7123
- "ResumeBase64",
7124
- ]);
7125
- const isReservedByApplicantContactSchema = (name) => Boolean(inputBody) && applicantContactFieldNames.has(name);
7126
7189
  // A declared foldReturn.drillParamBindings names drill query params that
7127
7190
  // are caller-driven instead of frozen literals (see
7128
7191
  // recon-generate-foldreturn-cannot-bind-drill-query-param-to-caller-payload.md)
@@ -7143,9 +7206,10 @@ function emitContractTs(opts) {
7143
7206
  addExtendField(binding.payloadField, ` ${binding.payloadField}: ${zod},`);
7144
7207
  }
7145
7208
  // Multi-step flows that include a multipart upload need the binary asset
7146
- // on the payload. A query-type flow (no ApplicantContactSchema base) still
7147
- // needs these fields spelled out explicitly.
7148
- if (hasMultipartStep && !inputBody) {
7209
+ // on the payload. A non-applicant flow (no ApplicantContactSchema base,
7210
+ // whether or not a body was captured) still needs these fields spelled
7211
+ // out explicitly.
7212
+ if (hasMultipartStep && !usesApplicantContactSchema) {
7149
7213
  addExtendField("Resume", " Resume: z.instanceof(Buffer),");
7150
7214
  addExtendField("ResumeContentType", " ResumeContentType: z.string(),");
7151
7215
  addExtendField("ResumeFilename", " ResumeFilename: z.string(),");
@@ -7287,9 +7351,8 @@ function emitContractTs(opts) {
7287
7351
  // comment above) — a GraphQL mutation that happens to declare an
7288
7352
  // unpopulated variable with a matching name (e.g. `$email`) must not
7289
7353
  // downgrade that required base field.
7290
- const baseContractFieldNames = new Set(["Email", "ClickUrl", "Answers"]);
7291
7354
  for (const [fieldName, line] of extendFields) {
7292
- if (inputBody && baseContractFieldNames.has(fieldName))
7355
+ if (usesApplicantContactSchema && baseContractFieldNames.has(fieldName))
7293
7356
  continue;
7294
7357
  if (unpopulatedDeclaredVariables.some((name) => name.toLowerCase() === fieldName.toLowerCase())) {
7295
7358
  extendFields.set(fieldName, line.replace(/,\s*$/, ".optional(),"));
@@ -7298,23 +7361,25 @@ function emitContractTs(opts) {
7298
7361
  const mergedExtension = extendFields.size > 0 ? `.extend({\n${[...extendFields.values()].join("\n")}\n})` : "";
7299
7362
  const payloadSchemaExpr = `${basePayloadSchemaExpr}${mergedExtension}`;
7300
7363
  // basePayloadSchemaExpr's own Answers field always wraps in
7301
- // multipartJsonObject() for submission flows (inputBody set);
7302
- // multipartBoolean() is only imported when a boolean field was actually
7303
- // wrapped in it above (an additional-body-key or an inputBody field under
7304
- // multipartCoerce) — payloadNeedsMultipart alone doesn't imply that.
7364
+ // multipartJsonObject() for job-application submission flows
7365
+ // (usesApplicantContactSchema); multipartBoolean() is only imported when a
7366
+ // boolean field was actually wrapped in it above (an additional-body-key
7367
+ // or an inputBody field under multipartCoerce) — payloadNeedsMultipart
7368
+ // alone doesn't imply that.
7305
7369
  // Named imports from the same module are combined into one import statement.
7306
7370
  const zodMultipartNamedImports = [
7307
7371
  ...(usesMultipartBoolean ? ["multipartBoolean"] : []),
7308
- ...(inputBody || (payloadNeedsMultipart && sortedStructuredEntries.length > 0)
7372
+ ...(usesApplicantContactSchema || (payloadNeedsMultipart && sortedStructuredEntries.length > 0)
7309
7373
  ? ["multipartJsonObject"]
7310
7374
  : []),
7311
7375
  ];
7312
7376
  const multipartBoolImport = zodMultipartNamedImports.length > 0
7313
7377
  ? `import { ${zodMultipartNamedImports.join(", ")} } from "${ENGINE_PKG}/lib/zod-multipart";\n`
7314
7378
  : "";
7315
- // ApplicantContactSchema backs the default submission-flow payload schema
7316
- // (see basePayloadSchemaExpr above); only referenced when inputBody is set.
7317
- const applicantContactImport = inputBody
7379
+ // ApplicantContactSchema backs the job-application submission-flow payload
7380
+ // schema (see basePayloadSchemaExpr above); only referenced when
7381
+ // usesApplicantContactSchema is true.
7382
+ const applicantContactImport = usesApplicantContactSchema
7318
7383
  ? `import { ApplicantContactSchema } from "${ENGINE_PKG}/lib/applicant-payload";\n`
7319
7384
  : "";
7320
7385
  // Content-Type must be absent from multipart fetch calls so FormData can inject the boundary.
@@ -7715,17 +7780,17 @@ export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
7715
7780
  bodySchema: ${pascal}PayloadSchema,
7716
7781
  responseSchema: ${pascal}ResponseSchema,
7717
7782
  defaultBaseUrl: ${JSON.stringify(baseUrl)},
7718
- ${payloadNeedsMultipart || inputBody
7783
+ ${payloadNeedsMultipart || usesApplicantContactSchema || hasMultipartStep
7719
7784
  ? `// multipart is required whenever the flow itself uploads a file
7720
- // (hasMultipartStep), OR this is a submission flow (inputBody set) since
7721
- // basePayloadSchemaExpr always requires a real Resume Buffer via
7722
- // ApplicantContactSchema regardless of whether the recorded browser flow
7723
- // contained an upload step, OR the payload has a non-scalar
7724
- // discoveredStructuredKeys field (payloadNeedsMultipart), since the
7725
- // multipart wire format is what makes that field's JSON-stringified
7726
- // encoding parseable.
7785
+ // (hasMultipartStep), OR this is a job-application submission flow
7786
+ // (usesApplicantContactSchema) since basePayloadSchemaExpr requires a
7787
+ // real Resume Buffer via ApplicantContactSchema regardless of whether
7788
+ // the recorded browser flow contained an upload step, OR the payload
7789
+ // has a non-scalar discoveredStructuredKeys field (payloadNeedsMultipart),
7790
+ // since the multipart wire format is what makes that field's
7791
+ // JSON-stringified encoding parseable.
7727
7792
  `
7728
- : ""}apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || inputBody ? "\n multipart: true," : ""}
7793
+ : ""}apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || usesApplicantContactSchema || hasMultipartStep ? "\n multipart: true," : ""}
7729
7794
  },
7730
7795
  ${executeHttpMethodBlock}
7731
7796
  /** Browser fallback: Stagehand + Steel — invoked only when hot path fails. */
@@ -7734,7 +7799,7 @@ ${executeHttpMethodBlock}
7734
7799
  session: BrowserSession,
7735
7800
  context: SitePluginContext
7736
7801
  ): Promise<SitePluginResult<${pascal}Response>> {
7737
- const raw = await run${pascal}BrowserFlow(session.stagehand, ${inputBody ? "payload.ClickUrl" : "context.baseUrl"}, payload);
7802
+ const raw = await run${pascal}BrowserFlow(session.stagehand, ${usesApplicantContactSchema ? "payload.ClickUrl" : "context.baseUrl"}, payload, session.sessionProxy ?? null);
7738
7803
  return { data: raw as ${pascal}Response };
7739
7804
  },
7740
7805
  };
@@ -8120,6 +8185,7 @@ import { getLogger } from "${ENGINE_PKG}/lib/logging";${usesThrowawayPassword ?
8120
8185
  import { type HealingFlowStep, runHealingFlow } from "${ENGINE_PKG}/scraper/flow-runner";
8121
8186
  import { waitForSpaReady } from "${ENGINE_PKG}/scraper/spa-readiness";
8122
8187
  import { guardedExtract } from "${ENGINE_PKG}/scraper/stagehand-guard";${usesEmailStep ? `\nimport { testmailInboxFromAddress } from "${ENGINE_PKG}/testmail/client";` : ""}
8188
+ import type { SessionProxyTuple } from "${ENGINE_PKG}/types/session-proxy";
8123
8189
  import ${isSubmissionFlow
8124
8190
  ? `type { ${pascal}Payload, ${pascal}Response }`
8125
8191
  : `{ type ${pascal}Payload, type ${pascal}Response, ${pascal}ResponseSchema }`} from "@/sites/${siteId}/contract";
@@ -8142,7 +8208,8 @@ const ${pascal}BrowserSchema = z.object({
8142
8208
  export async function run${pascal}BrowserFlow(
8143
8209
  stagehand: Stagehand,
8144
8210
  ${isSubmissionFlow ? "entryUrl" : "baseUrl"}: string,
8145
- payload: ${pascal}Payload
8211
+ payload: ${pascal}Payload,
8212
+ sessionProxy: SessionProxyTuple | null
8146
8213
  ): Promise<${pascal}Response> {
8147
8214
  const page = await stagehand.context.awaitActivePage();
8148
8215
 
@@ -8167,6 +8234,7 @@ ${flowStepsBlock}
8167
8234
  anthropic: buildAnthropicClient(),
8168
8235
  rephraseModel: buildRephraseModel(),
8169
8236
  uploadFixture: ${uploadFixtureExpr},${frameSelector !== undefined ? `\n frameSelector: ${JSON.stringify(frameSelector)},` : ""}${usesEmailStep ? "\n allocatedInbox: allocatedInbox," : ""}
8237
+ sessionProxy,
8170
8238
  });
8171
8239
 
8172
8240
  // Schema-enforced extract via guardedExtract: Stagehand 3.4.0 accepts
@@ -8464,9 +8532,11 @@ async function main() {
8464
8532
  const gql = isGraphQL(captures);
8465
8533
  // Hoisted so both the primary-operation gate below and rawActionCaptures
8466
8534
  // (further down) read the same computed sequence instead of calling the
8467
- // extractor twice.
8535
+ // extractor twice. Computed unfiltered (submitPatterns: null) — a
8536
+ // flow-declared submit pattern must truncate this sequence, not filter
8537
+ // it, so every gate reading it sees the full host-gated chain.
8468
8538
  const graphqlActionSequence = gql
8469
- ? extractGraphQLActionSequence(captures, submitPatterns, foldReturnSpec, ownBackendHostnames, fallbackDomain)
8539
+ ? extractGraphQLActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain)
8470
8540
  : [];
8471
8541
  // A foldReturn-admitted read/drill capture (see extractGraphQLActionSequence's
8472
8542
  // doc comment) can put 2+ entries in graphqlActionSequence with none of them
@@ -8476,7 +8546,7 @@ async function main() {
8476
8546
  // out primaryGraphQLOperation below or flip isSubmissionFlow further down.
8477
8547
  const graphqlActionSequenceHasMutation = graphqlActionSequence.some((a) => a.capture.query !== null && /^\s*mutation\b/.test(a.capture.query));
8478
8548
  const primaryGraphQLOperation = gql && !graphqlActionSequenceHasMutation
8479
- ? selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, process.env, ownBackendHostnames, fallbackDomain)
8549
+ ? selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, process.env, ownBackendHostnames, fallbackDomain, submitPatterns)
8480
8550
  : null;
8481
8551
  if (primaryGraphQLOperation && primaryGraphQLOperation.unpopulatedDeclaredVariables.length > 0) {
8482
8552
  const operationLabel = primaryGraphQLOperation.capture.operationName ?? "(anonymous)";
@@ -8489,20 +8559,20 @@ async function main() {
8489
8559
  // capture could otherwise win the endpoint/body fallback while an
8490
8560
  // unrelated capture supplies the query text.
8491
8561
  const fallbackGraphQLCapture = gql
8492
- ? firstGraphQLCapture(captures, ownBackendHostnames, fallbackDomain)
8562
+ ? firstGraphQLCapture(captures, ownBackendHostnames, fallbackDomain, submitPatterns)
8493
8563
  : null;
8494
8564
  const gqlQuery = primaryGraphQLOperation?.capture.query ?? fallbackGraphQLCapture?.query ?? null;
8495
8565
  const endpointPath = primaryGraphQLOperation?.endpointPath ??
8496
8566
  (fallbackGraphQLCapture
8497
8567
  ? safeUrlPathname(fallbackGraphQLCapture.url)
8498
- : firstEndpointPath(captures, ownBackendHostnames, fallbackDomain));
8568
+ : firstEndpointPath(captures, ownBackendHostnames, fallbackDomain, submitPatterns));
8499
8569
  // Derived from the primary operation's own Phase-1 capture, never from
8500
8570
  // replay array order -- a replay's body reflects whichever endpoint fired
8501
8571
  // first, not necessarily the primary operation, and only exists once
8502
8572
  // recon:http has run.
8503
8573
  const winningCapture = primaryGraphQLOperation?.capture ??
8504
8574
  fallbackGraphQLCapture ??
8505
- firstEndpointCapture(captures, ownBackendHostnames, fallbackDomain);
8575
+ firstEndpointCapture(captures, ownBackendHostnames, fallbackDomain, submitPatterns);
8506
8576
  const responseBody = winningCapture?.responseBody ?? null;
8507
8577
  // Every 2xx capture sharing the winning capture's operation identity, not
8508
8578
  // just the one that happened to win selection -- a paginated/re-filtered
@@ -8523,20 +8593,20 @@ async function main() {
8523
8593
  // a wizard whose every section saves independently, so it is only trusted
8524
8594
  // when it isn't a strict undercount of what the same captures' own
8525
8595
  // heuristic extraction finds.
8526
- const patternedHeuristicActionCaptures = gql
8596
+ const unfilteredHeuristicActionCaptures = gql
8527
8597
  ? dedupRedundantSameOperationCaptures(graphqlActionSequence, primaryGraphQLOperation)
8528
- : collapseRedundantPatches(extractActionSequence(captures, submitPatterns, foldReturnSpec, ownBackendHostnames, fallbackDomain));
8598
+ : collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain));
8529
8599
  // A flow-declared submitEndpointPattern is authoritative: it may match only
8530
- // one section's URL (the natural way to describe "the button that finishes
8531
- // the wizard") even though the same captures, read without the pattern,
8532
- // show every section saving independently. That gap is logged below for
8533
- // visibility, but the declared pattern is never overridden by the richer
8534
- // unfiltered sequence.
8535
- const unfilteredHeuristicActionCaptures = submitPatterns.endpoint === null && submitPatterns.body === null
8536
- ? patternedHeuristicActionCaptures
8537
- : gql
8538
- ? dedupRedundantSameOperationCaptures(extractGraphQLActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain), primaryGraphQLOperation)
8539
- : collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain));
8600
+ // the final step's URL (the natural way to describe "the button that
8601
+ // finishes the wizard") even though the earlier steps of the same chain
8602
+ // (auth mint, paged listing, ...) are what state-threading depends on.
8603
+ // Truncating at the last match — instead of filtering to matches only —
8604
+ // keeps that whole chain; the gap between this and the unfiltered
8605
+ // sequence is logged below for visibility, but the declared pattern is
8606
+ // never overridden by the richer unfiltered sequence.
8607
+ const patternedHeuristicActionCaptures = submitPatterns.endpoint === null && submitPatterns.body === null
8608
+ ? unfilteredHeuristicActionCaptures
8609
+ : truncateActionSequenceAtSubmitPattern(unfilteredHeuristicActionCaptures, submitPatterns);
8540
8610
  const patternUndercounts = patternedHeuristicActionCaptures.length < unfilteredHeuristicActionCaptures.length;
8541
8611
  if (patternUndercounts && requireSubmitEndpointMatch) {
8542
8612
  // Distinct wording from the non-required case below: this pattern is never