@enricai/barnacle 1.12.43 → 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 +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.d.ts +13 -3
  40. package/dist/scripts/recon-generate.d.ts.map +1 -1
  41. package/dist/scripts/recon-generate.js +94 -31
  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;
@@ -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
@@ -7734,7 +7792,7 @@ ${executeHttpMethodBlock}
7734
7792
  session: BrowserSession,
7735
7793
  context: SitePluginContext
7736
7794
  ): Promise<SitePluginResult<${pascal}Response>> {
7737
- 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);
7738
7796
  return { data: raw as ${pascal}Response };
7739
7797
  },
7740
7798
  };
@@ -8120,6 +8178,7 @@ import { getLogger } from "${ENGINE_PKG}/lib/logging";${usesThrowawayPassword ?
8120
8178
  import { type HealingFlowStep, runHealingFlow } from "${ENGINE_PKG}/scraper/flow-runner";
8121
8179
  import { waitForSpaReady } from "${ENGINE_PKG}/scraper/spa-readiness";
8122
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";
8123
8182
  import ${isSubmissionFlow
8124
8183
  ? `type { ${pascal}Payload, ${pascal}Response }`
8125
8184
  : `{ type ${pascal}Payload, type ${pascal}Response, ${pascal}ResponseSchema }`} from "@/sites/${siteId}/contract";
@@ -8142,7 +8201,8 @@ const ${pascal}BrowserSchema = z.object({
8142
8201
  export async function run${pascal}BrowserFlow(
8143
8202
  stagehand: Stagehand,
8144
8203
  ${isSubmissionFlow ? "entryUrl" : "baseUrl"}: string,
8145
- payload: ${pascal}Payload
8204
+ payload: ${pascal}Payload,
8205
+ sessionProxy: SessionProxyTuple | null
8146
8206
  ): Promise<${pascal}Response> {
8147
8207
  const page = await stagehand.context.awaitActivePage();
8148
8208
 
@@ -8167,6 +8227,7 @@ ${flowStepsBlock}
8167
8227
  anthropic: buildAnthropicClient(),
8168
8228
  rephraseModel: buildRephraseModel(),
8169
8229
  uploadFixture: ${uploadFixtureExpr},${frameSelector !== undefined ? `\n frameSelector: ${JSON.stringify(frameSelector)},` : ""}${usesEmailStep ? "\n allocatedInbox: allocatedInbox," : ""}
8230
+ sessionProxy,
8170
8231
  });
8171
8232
 
8172
8233
  // Schema-enforced extract via guardedExtract: Stagehand 3.4.0 accepts
@@ -8464,9 +8525,11 @@ async function main() {
8464
8525
  const gql = isGraphQL(captures);
8465
8526
  // Hoisted so both the primary-operation gate below and rawActionCaptures
8466
8527
  // (further down) read the same computed sequence instead of calling the
8467
- // 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.
8468
8531
  const graphqlActionSequence = gql
8469
- ? extractGraphQLActionSequence(captures, submitPatterns, foldReturnSpec, ownBackendHostnames, fallbackDomain)
8532
+ ? extractGraphQLActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain)
8470
8533
  : [];
8471
8534
  // A foldReturn-admitted read/drill capture (see extractGraphQLActionSequence's
8472
8535
  // doc comment) can put 2+ entries in graphqlActionSequence with none of them
@@ -8476,7 +8539,7 @@ async function main() {
8476
8539
  // out primaryGraphQLOperation below or flip isSubmissionFlow further down.
8477
8540
  const graphqlActionSequenceHasMutation = graphqlActionSequence.some((a) => a.capture.query !== null && /^\s*mutation\b/.test(a.capture.query));
8478
8541
  const primaryGraphQLOperation = gql && !graphqlActionSequenceHasMutation
8479
- ? selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, process.env, ownBackendHostnames, fallbackDomain)
8542
+ ? selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, process.env, ownBackendHostnames, fallbackDomain, submitPatterns)
8480
8543
  : null;
8481
8544
  if (primaryGraphQLOperation && primaryGraphQLOperation.unpopulatedDeclaredVariables.length > 0) {
8482
8545
  const operationLabel = primaryGraphQLOperation.capture.operationName ?? "(anonymous)";
@@ -8489,20 +8552,20 @@ async function main() {
8489
8552
  // capture could otherwise win the endpoint/body fallback while an
8490
8553
  // unrelated capture supplies the query text.
8491
8554
  const fallbackGraphQLCapture = gql
8492
- ? firstGraphQLCapture(captures, ownBackendHostnames, fallbackDomain)
8555
+ ? firstGraphQLCapture(captures, ownBackendHostnames, fallbackDomain, submitPatterns)
8493
8556
  : null;
8494
8557
  const gqlQuery = primaryGraphQLOperation?.capture.query ?? fallbackGraphQLCapture?.query ?? null;
8495
8558
  const endpointPath = primaryGraphQLOperation?.endpointPath ??
8496
8559
  (fallbackGraphQLCapture
8497
8560
  ? safeUrlPathname(fallbackGraphQLCapture.url)
8498
- : firstEndpointPath(captures, ownBackendHostnames, fallbackDomain));
8561
+ : firstEndpointPath(captures, ownBackendHostnames, fallbackDomain, submitPatterns));
8499
8562
  // Derived from the primary operation's own Phase-1 capture, never from
8500
8563
  // replay array order -- a replay's body reflects whichever endpoint fired
8501
8564
  // first, not necessarily the primary operation, and only exists once
8502
8565
  // recon:http has run.
8503
8566
  const winningCapture = primaryGraphQLOperation?.capture ??
8504
8567
  fallbackGraphQLCapture ??
8505
- firstEndpointCapture(captures, ownBackendHostnames, fallbackDomain);
8568
+ firstEndpointCapture(captures, ownBackendHostnames, fallbackDomain, submitPatterns);
8506
8569
  const responseBody = winningCapture?.responseBody ?? null;
8507
8570
  // Every 2xx capture sharing the winning capture's operation identity, not
8508
8571
  // just the one that happened to win selection -- a paginated/re-filtered
@@ -8523,20 +8586,20 @@ async function main() {
8523
8586
  // a wizard whose every section saves independently, so it is only trusted
8524
8587
  // when it isn't a strict undercount of what the same captures' own
8525
8588
  // heuristic extraction finds.
8526
- const patternedHeuristicActionCaptures = gql
8589
+ const unfilteredHeuristicActionCaptures = gql
8527
8590
  ? dedupRedundantSameOperationCaptures(graphqlActionSequence, primaryGraphQLOperation)
8528
- : collapseRedundantPatches(extractActionSequence(captures, submitPatterns, foldReturnSpec, ownBackendHostnames, fallbackDomain));
8591
+ : collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain));
8529
8592
  // 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));
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);
8540
8603
  const patternUndercounts = patternedHeuristicActionCaptures.length < unfilteredHeuristicActionCaptures.length;
8541
8604
  if (patternUndercounts && requireSubmitEndpointMatch) {
8542
8605
  // Distinct wording from the non-required case below: this pattern is never