@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.
- 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 +15 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +3 -1
- 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 +13 -3
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +94 -31
- 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;
|
|
@@ -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
|
|
@@ -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,
|
|
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
|
|
8589
|
+
const unfilteredHeuristicActionCaptures = gql
|
|
8527
8590
|
? dedupRedundantSameOperationCaptures(graphqlActionSequence, primaryGraphQLOperation)
|
|
8528
|
-
: collapseRedundantPatches(extractActionSequence(captures,
|
|
8591
|
+
: collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec, ownBackendHostnames, fallbackDomain));
|
|
8529
8592
|
// A flow-declared submitEndpointPattern is authoritative: it may match only
|
|
8530
|
-
//
|
|
8531
|
-
// the wizard") even though the
|
|
8532
|
-
//
|
|
8533
|
-
//
|
|
8534
|
-
// unfiltered
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
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
|