@enricai/barnacle 1.12.42 → 1.12.44

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 (47) 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 +22 -6
  15. package/dist/scraper/flow-runner.d.ts.map +1 -1
  16. package/dist/scraper/flow-runner.js +46 -39
  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.d.ts +24 -12
  40. package/dist/scripts/recon-generate.d.ts.map +1 -1
  41. package/dist/scripts/recon-generate.js +142 -78
  42. package/dist/scripts/recon-generate.js.map +1 -1
  43. package/dist/types/session-proxy.d.ts +11 -0
  44. package/dist/types/session-proxy.d.ts.map +1 -0
  45. package/dist/types/session-proxy.js +3 -0
  46. package/dist/types/session-proxy.js.map +1 -0
  47. 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;
@@ -1050,7 +1051,7 @@ const IGNORE_REQUEST_HEADERS = new Set([
1050
1051
  * headers (a `X-CSRF-Token`, a `Job-Boards-API-Token`, an `API-ShortName`,
1051
1052
  * etc.) without the generator needing to know about any particular site.
1052
1053
  */
1053
- function deriveRequestHeaders(captures, replays, baseUrl, submitPatterns = null) {
1054
+ function deriveRequestHeaders(captures, replays, baseUrl, submitPatterns = null, ownBackendHostnames = [], fallbackDomain = null) {
1054
1055
  const successfulUrls = new Set(replays.filter((r) => r.success).map((r) => endpointKey(r.url)));
1055
1056
  // Prefer ACTION captures (non-GET 2xx, non-noise) as the authoritative
1056
1057
  // header source. Replay-matched static-asset GETs lack the API-specific
@@ -1059,7 +1060,7 @@ function deriveRequestHeaders(captures, replays, baseUrl, submitPatterns = null)
1059
1060
  // (multi-step submission flows), use them. For sites where the flow is a
1060
1061
  // single REST call (no detectable action sequence), fall back to the
1061
1062
  // replay-matched captures.
1062
- const actionCaptures = extractActionSequence(captures, submitPatterns).map((a) => a.capture);
1063
+ const actionCaptures = extractActionSequence(captures, submitPatterns, null, ownBackendHostnames, fallbackDomain).map((a) => a.capture);
1063
1064
  const replayMatchedCaptures = captures.filter((c) => successfulUrls.has(endpointKey(c.url)));
1064
1065
  const relevantCaptures = actionCaptures.length > 0 ? actionCaptures : replayMatchedCaptures;
1065
1066
  const counts = new Map();
@@ -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
@@ -1426,11 +1484,11 @@ function resolveManifestActionSequence(runRoot, captures) {
1426
1484
  /**
1427
1485
  * Extracts the ordered sequence of meaningful POSTs that represent the
1428
1486
  * transactional flow: non-noise 2xx POSTs, minus telemetry and error-reporting
1429
- * sinks. Assets need no filter of their own — they arrive as GETs. Host is
1430
- * NOT a filter criterion — a multi-step submission routinely bounces to a
1431
- * different host mid-flow (an account-creation redirect, a tenant API
1432
- * subdomain distinct from the landing page's host), so `isNoiseUrl` alone
1433
- * decides what counts as site traffic vs. third-party noise.
1487
+ * sinks. Assets need no filter of their own — they arrive as GETs. When the
1488
+ * caller has host-provenance data (`ownBackendHostnames`/`fallbackDomain`),
1489
+ * a capture whose host fails {@link isAllowedFixtureHost} is dropped too —
1490
+ * `isNoiseUrl` alone lets a third-party telemetry/beacon POST masquerade as
1491
+ * a submission step, since it can look identical in shape to a real one.
1434
1492
  *
1435
1493
  * When the flow declares submit patterns, only POSTs matching them survive —
1436
1494
  * this isolates the submission from same-origin page chrome (bootstrap, chatbot,
@@ -1448,9 +1506,15 @@ function resolveManifestActionSequence(runRoot, captures) {
1448
1506
  * at a live site, and it is the only gate between a browser's incidental
1449
1507
  * chatter and the emitted hot path.
1450
1508
  */
1451
- function extractActionSequence(captures, submitPatterns = null, foldReturnSpec = null) {
1509
+ function extractActionSequence(captures, submitPatterns = null, foldReturnSpec = null, ownBackendHostnames = [], fallbackDomain = null) {
1452
1510
  const matchesSubmit = compileSubmitMatcher(submitPatterns);
1453
1511
  const matchesFoldReturn = compileFoldReturnEndpointMatcher(foldReturnSpec);
1512
+ // Callers with no host-provenance data (the exported function's unit
1513
+ // tests) pass neither ownBackendHostnames nor fallbackDomain — in that
1514
+ // case isAllowedFixtureHost would reject every candidate, so the gate
1515
+ // only applies once the caller has actually resolved a notion of "own
1516
+ // backend" to check against.
1517
+ const hasHostProvenance = ownBackendHostnames.length > 0 || fallbackDomain !== null;
1454
1518
  return captures
1455
1519
  .map((capture, index) => ({ capture, index }))
1456
1520
  .filter(({ capture }) => {
@@ -1462,6 +1526,9 @@ function extractActionSequence(captures, submitPatterns = null, foldReturnSpec =
1462
1526
  return false;
1463
1527
  if (!matchesSubmit(capture))
1464
1528
  return false;
1529
+ if (hasHostProvenance &&
1530
+ !(0, capture_filters_1.isAllowedFixtureHost)(captureHostname(capture.url), ownBackendHostnames, fallbackDomain))
1531
+ return false;
1465
1532
  return true;
1466
1533
  });
1467
1534
  }
@@ -1473,8 +1540,10 @@ function extractActionSequence(captures, submitPatterns = null, foldReturnSpec =
1473
1540
  * make up a transactional flow (`UpsertSavedApplication`, `SubmitForm`, ...)
1474
1541
  * — and drops `query` operations, which are read/bootstrap calls (e.g. a
1475
1542
  * page-load `ListForms`) that carry no state-threading value and are exactly
1476
- * what let a chronologically-first fallback pick an unrelated query. Host is
1477
- * NOT a filter criterion, matching {@link extractActionSequence}.
1543
+ * what let a chronologically-first fallback pick an unrelated query. When
1544
+ * the caller has host-provenance data (`ownBackendHostnames`/`fallbackDomain`),
1545
+ * a capture whose host fails {@link isAllowedFixtureHost} is dropped too,
1546
+ * matching {@link extractActionSequence}.
1478
1547
  *
1479
1548
  * When the flow declares a `foldReturnSpec`, a non-mutation capture whose
1480
1549
  * URL matches its `endpointPattern` is admitted despite the query drop
@@ -1489,10 +1558,16 @@ function extractActionSequence(captures, submitPatterns = null, foldReturnSpec =
1489
1558
  * Exported for tests: this predicate decides what a generated GraphQL plugin
1490
1559
  * will send at a live site.
1491
1560
  */
1492
- function extractGraphQLActionSequence(captures, submitPatterns = null, foldReturnSpec = null) {
1561
+ function extractGraphQLActionSequence(captures, submitPatterns = null, foldReturnSpec = null, ownBackendHostnames = [], fallbackDomain = null) {
1493
1562
  const matchesSubmit = compileSubmitMatcher(submitPatterns);
1494
1563
  const matchesFoldReturn = compileFoldReturnEndpointMatcher(foldReturnSpec);
1495
1564
  const matchesFoldReturnResults = compileFoldReturnResultsMatcher(foldReturnSpec);
1565
+ // Callers with no host-provenance data (the exported function's unit
1566
+ // tests) pass neither ownBackendHostnames nor fallbackDomain — in that
1567
+ // case isAllowedFixtureHost would reject every candidate, so the gate
1568
+ // only applies once the caller has actually resolved a notion of "own
1569
+ // backend" to check against.
1570
+ const hasHostProvenance = ownBackendHostnames.length > 0 || fallbackDomain !== null;
1496
1571
  return captures
1497
1572
  .map((capture, index) => ({ capture, index }))
1498
1573
  .filter(({ capture }) => {
@@ -1502,6 +1577,9 @@ function extractGraphQLActionSequence(captures, submitPatterns = null, foldRetur
1502
1577
  return false;
1503
1578
  if (!matchesSubmit(capture))
1504
1579
  return false;
1580
+ if (hasHostProvenance &&
1581
+ !(0, capture_filters_1.isAllowedFixtureHost)(captureHostname(capture.url), ownBackendHostnames, fallbackDomain))
1582
+ return false;
1505
1583
  if (capture.query !== null && /^\s*mutation\b/.test(capture.query))
1506
1584
  return true;
1507
1585
  return matchesFoldReturn(capture) || matchesFoldReturnResults(capture);
@@ -7714,7 +7792,7 @@ ${executeHttpMethodBlock}
7714
7792
  session: BrowserSession,
7715
7793
  context: SitePluginContext
7716
7794
  ): Promise<SitePluginResult<${pascal}Response>> {
7717
- const raw = await run${pascal}BrowserFlow(session.stagehand, ${inputBody ? "payload.ClickUrl" : "context.baseUrl"}, payload);
7795
+ const raw = await run${pascal}BrowserFlow(session.stagehand, ${inputBody ? "payload.ClickUrl" : "context.baseUrl"}, payload, session.sessionProxy ?? null);
7718
7796
  return { data: raw as ${pascal}Response };
7719
7797
  },
7720
7798
  };
@@ -8100,6 +8178,7 @@ import { getLogger } from "${ENGINE_PKG}/lib/logging";${usesThrowawayPassword ?
8100
8178
  import { type HealingFlowStep, runHealingFlow } from "${ENGINE_PKG}/scraper/flow-runner";
8101
8179
  import { waitForSpaReady } from "${ENGINE_PKG}/scraper/spa-readiness";
8102
8180
  import { guardedExtract } from "${ENGINE_PKG}/scraper/stagehand-guard";${usesEmailStep ? `\nimport { testmailInboxFromAddress } from "${ENGINE_PKG}/testmail/client";` : ""}
8181
+ import type { SessionProxyTuple } from "${ENGINE_PKG}/types/session-proxy";
8103
8182
  import ${isSubmissionFlow
8104
8183
  ? `type { ${pascal}Payload, ${pascal}Response }`
8105
8184
  : `{ type ${pascal}Payload, type ${pascal}Response, ${pascal}ResponseSchema }`} from "@/sites/${siteId}/contract";
@@ -8122,7 +8201,8 @@ const ${pascal}BrowserSchema = z.object({
8122
8201
  export async function run${pascal}BrowserFlow(
8123
8202
  stagehand: Stagehand,
8124
8203
  ${isSubmissionFlow ? "entryUrl" : "baseUrl"}: string,
8125
- payload: ${pascal}Payload
8204
+ payload: ${pascal}Payload,
8205
+ sessionProxy: SessionProxyTuple | null
8126
8206
  ): Promise<${pascal}Response> {
8127
8207
  const page = await stagehand.context.awaitActivePage();
8128
8208
 
@@ -8147,6 +8227,7 @@ ${flowStepsBlock}
8147
8227
  anthropic: buildAnthropicClient(),
8148
8228
  rephraseModel: buildRephraseModel(),
8149
8229
  uploadFixture: ${uploadFixtureExpr},${frameSelector !== undefined ? `\n frameSelector: ${JSON.stringify(frameSelector)},` : ""}${usesEmailStep ? "\n allocatedInbox: allocatedInbox," : ""}
8230
+ sessionProxy,
8150
8231
  });
8151
8232
 
8152
8233
  // Schema-enforced extract via guardedExtract: Stagehand 3.4.0 accepts
@@ -8318,7 +8399,7 @@ async function main() {
8318
8399
  return [];
8319
8400
  }
8320
8401
  })();
8321
- const { flowSteps, frameSelector, submitEndpointPattern, submitBodyPattern, displayName, foldReturnSpec, } = (() => {
8402
+ const { flowSteps, frameSelector, submitEndpointPattern, submitBodyPattern, requireSubmitEndpointMatch, displayName, foldReturnSpec, } = (() => {
8322
8403
  const flowFileContents = (() => {
8323
8404
  try {
8324
8405
  return (0, node_fs_1.readFileSync)(flowFile, "utf8");
@@ -8339,6 +8420,7 @@ async function main() {
8339
8420
  frameSelector: undefined,
8340
8421
  submitEndpointPattern: null,
8341
8422
  submitBodyPattern: null,
8423
+ requireSubmitEndpointMatch: false,
8342
8424
  displayName: undefined,
8343
8425
  foldReturnSpec,
8344
8426
  };
@@ -8352,6 +8434,7 @@ async function main() {
8352
8434
  frameSelector: obj.frameSelector,
8353
8435
  submitEndpointPattern: obj.submitEndpointPattern ?? null,
8354
8436
  submitBodyPattern: obj.submitBodyPattern ?? null,
8437
+ requireSubmitEndpointMatch: obj.requireSubmitEndpointMatch ?? false,
8355
8438
  displayName: obj.displayName,
8356
8439
  foldReturnSpec,
8357
8440
  };
@@ -8361,6 +8444,7 @@ async function main() {
8361
8444
  frameSelector: undefined,
8362
8445
  submitEndpointPattern: null,
8363
8446
  submitBodyPattern: null,
8447
+ requireSubmitEndpointMatch: false,
8364
8448
  displayName: undefined,
8365
8449
  foldReturnSpec,
8366
8450
  };
@@ -8371,6 +8455,7 @@ async function main() {
8371
8455
  frameSelector: undefined,
8372
8456
  submitEndpointPattern: null,
8373
8457
  submitBodyPattern: null,
8458
+ requireSubmitEndpointMatch: false,
8374
8459
  displayName: undefined,
8375
8460
  foldReturnSpec,
8376
8461
  };
@@ -8433,16 +8518,18 @@ async function main() {
8433
8518
  for (const f of unmanifestedFiles) {
8434
8519
  logger.warn(`excluding aux fixture '${f}' — no aux-manifest.json entry, provenance unverifiable`);
8435
8520
  }
8436
- const baseHeaders = deriveRequestHeaders(captures, replays, baseUrl, submitPatterns);
8521
+ const baseHeaders = deriveRequestHeaders(captures, replays, baseUrl, submitPatterns, ownBackendHostnames, fallbackDomain);
8437
8522
  const minTime = deriveMinTime(rateLimits);
8438
8523
  const hasRateLimitProbeData = rateLimits.some((f) => f.safeRps !== null);
8439
8524
  const safeRps = rateLimits.find((f) => f.safeRps !== null)?.safeRps ?? Math.floor(1000 / minTime);
8440
8525
  const gql = isGraphQL(captures);
8441
8526
  // Hoisted so both the primary-operation gate below and rawActionCaptures
8442
8527
  // (further down) read the same computed sequence instead of calling the
8443
- // extractor twice.
8528
+ // extractor twice. Computed unfiltered (submitPatterns: null) — a
8529
+ // flow-declared submit pattern must truncate this sequence, not filter
8530
+ // it, so every gate reading it sees the full host-gated chain.
8444
8531
  const graphqlActionSequence = gql
8445
- ? extractGraphQLActionSequence(captures, submitPatterns, foldReturnSpec)
8532
+ ? extractGraphQLActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain)
8446
8533
  : [];
8447
8534
  // A foldReturn-admitted read/drill capture (see extractGraphQLActionSequence's
8448
8535
  // doc comment) can put 2+ entries in graphqlActionSequence with none of them
@@ -8452,7 +8539,7 @@ async function main() {
8452
8539
  // out primaryGraphQLOperation below or flip isSubmissionFlow further down.
8453
8540
  const graphqlActionSequenceHasMutation = graphqlActionSequence.some((a) => a.capture.query !== null && /^\s*mutation\b/.test(a.capture.query));
8454
8541
  const primaryGraphQLOperation = gql && !graphqlActionSequenceHasMutation
8455
- ? selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, process.env, ownBackendHostnames, fallbackDomain)
8542
+ ? selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, process.env, ownBackendHostnames, fallbackDomain, submitPatterns)
8456
8543
  : null;
8457
8544
  if (primaryGraphQLOperation && primaryGraphQLOperation.unpopulatedDeclaredVariables.length > 0) {
8458
8545
  const operationLabel = primaryGraphQLOperation.capture.operationName ?? "(anonymous)";
@@ -8465,20 +8552,20 @@ async function main() {
8465
8552
  // capture could otherwise win the endpoint/body fallback while an
8466
8553
  // unrelated capture supplies the query text.
8467
8554
  const fallbackGraphQLCapture = gql
8468
- ? firstGraphQLCapture(captures, ownBackendHostnames, fallbackDomain)
8555
+ ? firstGraphQLCapture(captures, ownBackendHostnames, fallbackDomain, submitPatterns)
8469
8556
  : null;
8470
8557
  const gqlQuery = primaryGraphQLOperation?.capture.query ?? fallbackGraphQLCapture?.query ?? null;
8471
8558
  const endpointPath = primaryGraphQLOperation?.endpointPath ??
8472
8559
  (fallbackGraphQLCapture
8473
8560
  ? safeUrlPathname(fallbackGraphQLCapture.url)
8474
- : firstEndpointPath(captures, ownBackendHostnames, fallbackDomain));
8561
+ : firstEndpointPath(captures, ownBackendHostnames, fallbackDomain, submitPatterns));
8475
8562
  // Derived from the primary operation's own Phase-1 capture, never from
8476
8563
  // replay array order -- a replay's body reflects whichever endpoint fired
8477
8564
  // first, not necessarily the primary operation, and only exists once
8478
8565
  // recon:http has run.
8479
8566
  const winningCapture = primaryGraphQLOperation?.capture ??
8480
8567
  fallbackGraphQLCapture ??
8481
- firstEndpointCapture(captures, ownBackendHostnames, fallbackDomain);
8568
+ firstEndpointCapture(captures, ownBackendHostnames, fallbackDomain, submitPatterns);
8482
8569
  const responseBody = winningCapture?.responseBody ?? null;
8483
8570
  // Every 2xx capture sharing the winning capture's operation identity, not
8484
8571
  // just the one that happened to win selection -- a paginated/re-filtered
@@ -8499,30 +8586,33 @@ async function main() {
8499
8586
  // a wizard whose every section saves independently, so it is only trusted
8500
8587
  // when it isn't a strict undercount of what the same captures' own
8501
8588
  // heuristic extraction finds.
8502
- const patternedHeuristicActionCaptures = gql
8589
+ const unfilteredHeuristicActionCaptures = gql
8503
8590
  ? dedupRedundantSameOperationCaptures(graphqlActionSequence, primaryGraphQLOperation)
8504
- : collapseRedundantPatches(extractActionSequence(captures, submitPatterns, foldReturnSpec));
8505
- // The same undercount hazard applies one layer below the manifest: a
8506
- // flow-declared submitEndpointPattern that matches only one section's URL
8507
- // (the natural way to describe "the button that finishes the wizard")
8508
- // filters the heuristic sequence down to that one capture even though the
8509
- // same captures, read without the pattern, show every section saving
8510
- // independently. A pattern that undercounts what the unfiltered heuristic
8511
- // finds is therefore not trusted either — it falls back to the richer,
8512
- // unfiltered sequence instead of collapsing a real multi-call flow into
8513
- // the generic single-endpoint fallback.
8514
- const unfilteredHeuristicActionCaptures = submitPatterns.endpoint === null && submitPatterns.body === null
8515
- ? patternedHeuristicActionCaptures
8516
- : gql
8517
- ? dedupRedundantSameOperationCaptures(extractGraphQLActionSequence(captures, null, foldReturnSpec), primaryGraphQLOperation)
8518
- : collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec));
8591
+ : collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain));
8592
+ // A flow-declared submitEndpointPattern is authoritative: it may match only
8593
+ // the final step's URL (the natural way to describe "the button that
8594
+ // finishes the wizard") even though the earlier steps of the same chain
8595
+ // (auth mint, paged listing, ...) are what state-threading depends on.
8596
+ // Truncating at the last match — instead of filtering to matches only —
8597
+ // keeps that whole chain; the gap between this and the unfiltered
8598
+ // sequence is logged below for visibility, but the declared pattern is
8599
+ // never overridden by the richer unfiltered sequence.
8600
+ const patternedHeuristicActionCaptures = submitPatterns.endpoint === null && submitPatterns.body === null
8601
+ ? unfilteredHeuristicActionCaptures
8602
+ : truncateActionSequenceAtSubmitPattern(unfilteredHeuristicActionCaptures, submitPatterns);
8519
8603
  const patternUndercounts = patternedHeuristicActionCaptures.length < unfilteredHeuristicActionCaptures.length;
8520
- if (patternUndercounts) {
8604
+ if (patternUndercounts && requireSubmitEndpointMatch) {
8605
+ // Distinct wording from the non-required case below: this pattern is never
8606
+ // discarded, so a message that says "ignoring"/"undercount" would misstate
8607
+ // what happened. The disagreement is still worth a warn-level surface —
8608
+ // the flow author should know the declared pattern covers fewer captures
8609
+ // than the unfiltered heuristic sequence finds.
8610
+ logger.warn(`submission selection: declared submitEndpointPattern/submitBodyPattern (${patternedHeuristicActionCaptures.length} capture(s)) disagrees with the unfiltered heuristic action sequence (${unfilteredHeuristicActionCaptures.length} capture(s)); using the declared pattern because requireSubmitEndpointMatch is set`);
8611
+ }
8612
+ else if (patternUndercounts) {
8521
8613
  logger.info(`submission selection: ignoring submitEndpointPattern/submitBodyPattern (${patternedHeuristicActionCaptures.length} capture(s)) as an undercount of the unfiltered heuristic action sequence (${unfilteredHeuristicActionCaptures.length} capture(s))`);
8522
8614
  }
8523
- const heuristicActionCaptures = patternUndercounts
8524
- ? unfilteredHeuristicActionCaptures
8525
- : patternedHeuristicActionCaptures;
8615
+ const heuristicActionCaptures = patternedHeuristicActionCaptures;
8526
8616
  const manifestActionCaptures = resolveManifestActionSequence(runRoot, captures);
8527
8617
  const manifestUndercounts = manifestActionCaptures !== null &&
8528
8618
  manifestActionCaptures.length < heuristicActionCaptures.length;
@@ -8535,19 +8625,6 @@ async function main() {
8535
8625
  const rawActionCaptures = manifestActionCaptures !== null && !manifestUndercounts
8536
8626
  ? manifestActionCaptures
8537
8627
  : heuristicActionCaptures;
8538
- // The declared submitEndpointPattern's own under-match: unlike manifestUndercounts
8539
- // (which compares an authoritative submit-manifest.json against the heuristic
8540
- // sequence), this compares the pattern-filtered heuristic sequence against the SAME
8541
- // captures with no submitPatterns filter at all — the true raw non-GET 2xx non-noise
8542
- // action set. A pattern that matches conspicuously fewer calls than that raw set is a
8543
- // detection failure (the pattern is too narrow), not evidence the flow is read-only,
8544
- // and per the no-silent-fallback rule must not be allowed to quietly collapse into the
8545
- // generic single-endpoint {query} template below.
8546
- const rawUnfilteredActionCaptures = submitEndpointPattern === null
8547
- ? null
8548
- : gql
8549
- ? dedupRedundantSameOperationCaptures(extractGraphQLActionSequence(captures, null, foldReturnSpec), primaryGraphQLOperation)
8550
- : collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec));
8551
8628
  // Form-schema detection runs BEFORE state-indexing so the field-id/option-id
8552
8629
  // UUIDs can be shielded from indexing — those UUIDs are stable schema
8553
8630
  // anchors that T2/T3 substitution depends on remaining literal in body
@@ -8608,19 +8685,6 @@ async function main() {
8608
8685
  : new Map();
8609
8686
  const actionSteps = actionCaptures.length > 1 ? compileActionSteps(actionCaptures, stateIndex) : [];
8610
8687
  const isSubmissionFlow = actionSteps.length > 1 && (!gql || graphqlActionSequenceHasMutation);
8611
- // Loud failure for a submitEndpointPattern that under-matches the raw traffic badly
8612
- // enough to collapse the flow to the single-endpoint fallback: heuristicActionCaptures
8613
- // is the pattern-filtered sequence (used both directly and as rawActionCaptures' floor
8614
- // via manifestUndercounts above), so if it's this thin ONLY because the pattern itself
8615
- // excluded real action captures the unfiltered raw set still has, emitting the generic
8616
- // {query} template would misrepresent a genuine multi-call flow as read-only. Exit
8617
- // rather than degrade quietly, per the no-defensive/no-silent-fallback rule.
8618
- if (rawUnfilteredActionCaptures !== null &&
8619
- !isSubmissionFlow &&
8620
- rawUnfilteredActionCaptures.length > heuristicActionCaptures.length) {
8621
- logger.error(`ERROR declared submitEndpointPattern ${JSON.stringify(submitEndpointPattern)} matched only ${heuristicActionCaptures.length} of ${rawUnfilteredActionCaptures.length} raw non-GET 2xx non-noise action capture(s) — this under-match looks like a detection failure, not a read-only flow; refusing to fall back to the generic single-endpoint {query} template. Fix submitEndpointPattern in recon-flow.json to cover the real submission calls.`);
8622
- process.exit(1);
8623
- }
8624
8688
  // Diagnostic for the FAILURE-3 shape (a flowless recon capture): a "submission flow"
8625
8689
  // whose every action capture is landing-phase is almost certainly page-chrome
8626
8690
  // bootstrap (e.g. page-chrome `POST /widgets`) misread as an apply flow, not a walked