@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.
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/tracking-click.js +2 -2
- package/dist/lib/tracking-click.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +1 -0
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/scraper/captcha-solver.d.ts +7 -0
- package/dist/scraper/captcha-solver.d.ts.map +1 -1
- package/dist/scraper/captcha-solver.js +11 -2
- package/dist/scraper/captcha-solver.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +22 -6
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +46 -39
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts +6 -3
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +52 -5
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scraper/session-proxy.d.ts +18 -0
- package/dist/scraper/session-proxy.d.ts.map +1 -0
- package/dist/scraper/session-proxy.js +19 -0
- package/dist/scraper/session-proxy.js.map +1 -0
- package/dist/scraper/session-shared.d.ts +29 -0
- package/dist/scraper/session-shared.d.ts.map +1 -1
- package/dist/scraper/session-shared.js.map +1 -1
- package/dist/scraper/session-steel.d.ts.map +1 -1
- package/dist/scraper/session-steel.js +11 -0
- package/dist/scraper/session-steel.js.map +1 -1
- package/dist/scraper/session-teardown.d.ts +19 -0
- package/dist/scraper/session-teardown.d.ts.map +1 -1
- package/dist/scraper/session-teardown.js +22 -0
- package/dist/scraper/session-teardown.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +12 -0
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +24 -12
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +142 -78
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/types/session-proxy.d.ts +11 -0
- package/dist/types/session-proxy.d.ts.map +1 -0
- package/dist/types/session-proxy.js +3 -0
- package/dist/types/session-proxy.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
1121
|
+
const ownBackendCandidates = captures.filter((c) => c.query &&
|
|
1109
1122
|
(!hasHostProvenance ||
|
|
1110
|
-
(0, capture_filters_1.isAllowedFixtureHost)(captureHostname(c.url), ownBackendHostnames, fallbackDomain)))
|
|
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
|
|
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.
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
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.
|
|
1477
|
-
*
|
|
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,
|
|
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
|
|
8589
|
+
const unfilteredHeuristicActionCaptures = gql
|
|
8503
8590
|
? dedupRedundantSameOperationCaptures(graphqlActionSequence, primaryGraphQLOperation)
|
|
8504
|
-
: collapseRedundantPatches(extractActionSequence(captures,
|
|
8505
|
-
//
|
|
8506
|
-
//
|
|
8507
|
-
//
|
|
8508
|
-
//
|
|
8509
|
-
//
|
|
8510
|
-
//
|
|
8511
|
-
//
|
|
8512
|
-
//
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
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 =
|
|
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
|