@enricai/barnacle 1.12.7 → 1.12.8
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/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +2 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/scraper/cdp-heartbeat.d.ts +34 -0
- package/dist/scraper/cdp-heartbeat.d.ts.map +1 -0
- package/dist/scraper/cdp-heartbeat.js +30 -0
- package/dist/scraper/cdp-heartbeat.js.map +1 -0
- package/dist/scraper/errors.d.ts +15 -1
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +24 -2
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +10 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +5 -1
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +27 -1
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scraper/session-shared.d.ts +18 -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 +9 -0
- package/dist/scraper/session-steel.d.ts.map +1 -1
- package/dist/scraper/session-steel.js +53 -0
- package/dist/scraper/session-steel.js.map +1 -1
- package/dist/scraper/session-teardown.d.ts +22 -0
- package/dist/scraper/session-teardown.d.ts.map +1 -0
- package/dist/scraper/session-teardown.js +54 -0
- package/dist/scraper/session-teardown.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +23 -1
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.d.ts +26 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +47 -0
- package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
- package/dist/scripts/recon-generate-multiendpoint-fixture.d.ts +12 -0
- package/dist/scripts/recon-generate-multiendpoint-fixture.d.ts.map +1 -0
- package/dist/scripts/recon-generate-multiendpoint-fixture.js +108 -0
- package/dist/scripts/recon-generate-multiendpoint-fixture.js.map +1 -0
- package/dist/scripts/recon-generate.d.ts +8 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +178 -55
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -31,6 +31,7 @@ exports.selectReturnAction = selectReturnAction;
|
|
|
31
31
|
exports.selectEffectiveResponseBody = selectEffectiveResponseBody;
|
|
32
32
|
exports.extractEntryUrlParams = extractEntryUrlParams;
|
|
33
33
|
exports.selectPrimaryGraphQLOperation = selectPrimaryGraphQLOperation;
|
|
34
|
+
exports.firstEndpointPath = firstEndpointPath;
|
|
34
35
|
exports.resolveManifestActionSequence = resolveManifestActionSequence;
|
|
35
36
|
exports.extractActionSequence = extractActionSequence;
|
|
36
37
|
exports.extractGraphQLActionSequence = extractGraphQLActionSequence;
|
|
@@ -631,10 +632,18 @@ function selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary) {
|
|
|
631
632
|
return { capture: winner.capture, endpointPath };
|
|
632
633
|
}
|
|
633
634
|
function firstEndpointPath(captures) {
|
|
635
|
+
const nonGetCaptures = captures.filter((c) => c.method !== "GET");
|
|
636
|
+
for (const c of nonGetCaptures) {
|
|
637
|
+
try {
|
|
638
|
+
return new URL(c.url).pathname;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
// skip
|
|
642
|
+
}
|
|
643
|
+
}
|
|
634
644
|
for (const c of captures) {
|
|
635
645
|
try {
|
|
636
|
-
|
|
637
|
-
return u.pathname;
|
|
646
|
+
return new URL(c.url).pathname;
|
|
638
647
|
}
|
|
639
648
|
catch {
|
|
640
649
|
// skip
|
|
@@ -3280,7 +3289,7 @@ function renderGqlVariablesExpr(variables, payloadFieldNames) {
|
|
|
3280
3289
|
return entries.length > 0 ? `{ ${entries.join(", ")} }` : "{}";
|
|
3281
3290
|
}
|
|
3282
3291
|
function emitContractTs(opts) {
|
|
3283
|
-
const { siteId, pascal, baseUrl, baseHeaders, minTime, safeRps, responseBody, gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, auxFiles, multiStepBody, inputBody, hasMultipartStep = false, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, payloadFieldNames, headerBindings = [], } = opts;
|
|
3292
|
+
const { siteId, pascal, baseUrl, baseHeaders, minTime, safeRps, responseBody, gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, auxFiles, multiStepBody, omitExecuteHttp = false, inputBody, hasMultipartStep = false, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, payloadFieldNames, headerBindings = [], } = opts;
|
|
3284
3293
|
// This is the CLIENT-level schema — createHttpClient's default, and the
|
|
3285
3294
|
// plugin's caller-facing contract (what executeHttp's return value promises
|
|
3286
3295
|
// its own caller). It does NOT validate any individual call in a multi-step
|
|
@@ -3300,7 +3309,9 @@ function emitContractTs(opts) {
|
|
|
3300
3309
|
// without affecting per-call validation. Single-endpoint plugins keep the
|
|
3301
3310
|
// inferred schema, since there both roles (client default and sole call)
|
|
3302
3311
|
// coincide.
|
|
3303
|
-
|
|
3312
|
+
// Browser-flow-only plugins have no HTTP call to infer a shape from either
|
|
3313
|
+
// — same z.unknown() treatment as the multi-step case, for the same reason.
|
|
3314
|
+
const responseSchemaExpr = multiStepBody || omitExecuteHttp ? `z.unknown()` : inferZodSchema(responseBody);
|
|
3304
3315
|
// Multi-step flows that include a multipart upload need the binary asset
|
|
3305
3316
|
// on the payload. ApplicantContactSchema (via ApplicantResumeSchema) already
|
|
3306
3317
|
// declares Resume/ResumeContentType/ResumeFilename, so submission flows
|
|
@@ -3485,14 +3496,22 @@ function emitContractTs(opts) {
|
|
|
3485
3496
|
.map(([k, v]) => ` ${isValidJsIdentifier(k) ? k : JSON.stringify(k)}: ${JSON.stringify(v)}`)
|
|
3486
3497
|
.join(",\n");
|
|
3487
3498
|
const fixtureImport = auxFiles.length > 0 ? `// import { loadFixture } from "${ENGINE_PKG}/scraper/fixtures";\n` : "";
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3499
|
+
// Browser-flow-only plugins have no direct-HTTP hot path, so none of the
|
|
3500
|
+
// client-construction machinery below (import, rate limiter, cache/client
|
|
3501
|
+
// const) is emitted — it would sit unreferenced and trip Biome's
|
|
3502
|
+
// `noUnusedVariables`.
|
|
3503
|
+
const clientImport = omitExecuteHttp
|
|
3504
|
+
? ""
|
|
3505
|
+
: gql
|
|
3506
|
+
? `import { createGraphqlClient } from "${ENGINE_PKG}/scraper/graphql-client";`
|
|
3507
|
+
: `import { createHttpClient } from "${ENGINE_PKG}/scraper/http-client";`;
|
|
3508
|
+
const queryConst = !omitExecuteHttp && gql && gqlQuery
|
|
3492
3509
|
? `\n// Lifted verbatim from recon capture — trim UI-only fields before shipping.\nconst ${pascal.toUpperCase()}_QUERY = \`${gqlQuery.trim()}\`;\n`
|
|
3493
3510
|
: "";
|
|
3494
|
-
const gqlCacheBlock =
|
|
3495
|
-
?
|
|
3511
|
+
const gqlCacheBlock = omitExecuteHttp
|
|
3512
|
+
? ""
|
|
3513
|
+
: gql
|
|
3514
|
+
? `
|
|
3496
3515
|
type GqlFn = (operationName: string, query: string, variables: Record<string, unknown>) => Promise<${pascal}Response>;
|
|
3497
3516
|
|
|
3498
3517
|
const gqlCache = new Map<string, GqlFn>();
|
|
@@ -3511,7 +3530,7 @@ function getGql(baseUrl: string): GqlFn {
|
|
|
3511
3530
|
return client;
|
|
3512
3531
|
}
|
|
3513
3532
|
`
|
|
3514
|
-
|
|
3533
|
+
: `
|
|
3515
3534
|
const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)} });
|
|
3516
3535
|
`;
|
|
3517
3536
|
const gqlOperationNameExpr = gqlOperationName
|
|
@@ -3556,7 +3575,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
3556
3575
|
export const ${pascal}InternalRequestReference = ${internalRequestReferenceExpr};
|
|
3557
3576
|
`
|
|
3558
3577
|
: "";
|
|
3559
|
-
const queryChecklistLine = gql
|
|
3578
|
+
const queryChecklistLine = !omitExecuteHttp && gql
|
|
3560
3579
|
? `\n * [ ] Trim UI-only fields from ${pascal.toUpperCase()}_QUERY (keep only fields you need)`
|
|
3561
3580
|
: "";
|
|
3562
3581
|
// Multi-step flows validate each call against its own per-call inferred
|
|
@@ -3564,37 +3583,73 @@ export const ${pascal}InternalRequestReference = ${internalRequestReferenceExpr}
|
|
|
3564
3583
|
// what executeHttp promises ITS OWN caller, never a per-call validator, so
|
|
3565
3584
|
// the checklist item must say that explicitly. Single-endpoint plugins have
|
|
3566
3585
|
// exactly one call, so the client schema and that call's validator are the
|
|
3567
|
-
// same schema and the shorter wording stays accurate.
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3586
|
+
// same schema and the shorter wording stays accurate. Browser-flow-only
|
|
3587
|
+
// plugins have no executeHttp at all, so ResponseSchema is only ever the
|
|
3588
|
+
// browser flow's own return-value contract.
|
|
3589
|
+
const narrowSchemaChecklistLine = omitExecuteHttp
|
|
3590
|
+
? `\n * [ ] Narrow ${pascal}ResponseSchema to match what the browser flow should promise ITS CALLER — this flow could not synthesize a trustworthy executeHttp (a required value from the captured sequence never resolved), so it ships browser-only`
|
|
3591
|
+
: multiStepBody
|
|
3592
|
+
? `\n * [ ] Narrow ${pascal}ResponseSchema to match what executeHttp should promise ITS CALLER — this is the plugin's own return-value contract, not a per-call validator (each call in the flow is already checked against its own inferred schema)`
|
|
3593
|
+
: `\n * [ ] Narrow ${pascal}ResponseSchema to match the real response shape`;
|
|
3571
3594
|
const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
3595
|
+
// Browser-flow-only plugins need neither Bottleneck (no rate-limited HTTP
|
|
3596
|
+
// client) nor BASE_HEADERS (no per-call headers to bake in) — both would
|
|
3597
|
+
// sit unreferenced and trip Biome's `noUnusedVariables`.
|
|
3598
|
+
const bottleneckImport = omitExecuteHttp ? "" : `import Bottleneck from "bottleneck";\n`;
|
|
3599
|
+
const baseHeadersBlock = omitExecuteHttp
|
|
3600
|
+
? ""
|
|
3601
|
+
: `
|
|
3602
|
+
const BASE_HEADERS: Record<string, string> = {
|
|
3603
|
+
${headersLiteral},
|
|
3604
|
+
};
|
|
3605
|
+
`;
|
|
3606
|
+
const limiterBlock = omitExecuteHttp
|
|
3607
|
+
? ""
|
|
3608
|
+
: `
|
|
3609
|
+
// Safe ceiling: ${safeRps} rps — from recon rate-limit probe.
|
|
3610
|
+
const limiter = new Bottleneck({ minTime: ${minTime} });
|
|
3611
|
+
`;
|
|
3612
|
+
const executeHttpMethodBlock = omitExecuteHttp
|
|
3613
|
+
? ""
|
|
3614
|
+
: `
|
|
3615
|
+
/** Hot path: direct HTTP — no browser, no LLM tokens. */
|
|
3616
|
+
async executeHttp(
|
|
3617
|
+
payload: ${pascal}Payload,
|
|
3618
|
+
${executeHttpBody.includes("context.") ? "context" : "_context"}: SitePluginContext
|
|
3619
|
+
): Promise<SitePluginResult<${pascal}Response>> {
|
|
3620
|
+
${executeHttpBody}
|
|
3621
|
+
},
|
|
3622
|
+
`;
|
|
3623
|
+
const pluginDocComment = omitExecuteHttp
|
|
3624
|
+
? `/**
|
|
3625
|
+
* Plugin for ${siteId}. Browser-flow-only: the captured multi-step submission
|
|
3626
|
+
* sequence could not be synthesized into a trustworthy direct-HTTP hot path
|
|
3627
|
+
* (see the checklist above), so this always runs via Stagehand.
|
|
3628
|
+
*/`
|
|
3629
|
+
: `/**
|
|
3630
|
+
* Plugin for ${siteId}. Tries the direct-HTTP hot path first; falls back to
|
|
3631
|
+
* Stagehand automatically on schema drift or bot challenge.
|
|
3632
|
+
*/`;
|
|
3633
|
+
const baseHeadersChecklistLine = omitExecuteHttp
|
|
3634
|
+
? ""
|
|
3635
|
+
: `\n * [ ] Verify BASE_HEADERS — remove any that aren't load-bearing`;
|
|
3636
|
+
const outOfTreeChecklistLine = omitExecuteHttp
|
|
3637
|
+
? `\n * [ ] Out-of-tree: \`pnpm add zod\` — this file imports it directly, and a\n * strict node_modules layout (pnpm) won't resolve it as a transitive\n * dep of @enricai/barnacle alone`
|
|
3638
|
+
: `\n * [ ] Out-of-tree: \`pnpm add bottleneck zod\` — this file imports both\n * directly, and a strict node_modules layout (pnpm) won't resolve\n * them as transitive deps of @enricai/barnacle alone`;
|
|
3572
3639
|
return `/**
|
|
3573
3640
|
* Generated by recon-generate.ts — review before shipping.
|
|
3574
3641
|
*
|
|
3575
3642
|
* Checklist:${queryChecklistLine}${narrowSchemaChecklistLine}
|
|
3576
|
-
* [ ] Adjust ${pascal}PayloadSchema to your actual request parameters
|
|
3577
|
-
* [ ] Verify BASE_HEADERS — remove any that aren't load-bearing
|
|
3578
|
-
* [ ] Out-of-tree: \`pnpm add bottleneck zod\` — this file imports both
|
|
3579
|
-
* directly, and a strict node_modules layout (pnpm) won't resolve
|
|
3580
|
-
* them as transitive deps of @enricai/barnacle alone
|
|
3643
|
+
* [ ] Adjust ${pascal}PayloadSchema to your actual request parameters${baseHeadersChecklistLine}${outOfTreeChecklistLine}
|
|
3581
3644
|
*/
|
|
3582
3645
|
|
|
3583
|
-
import
|
|
3584
|
-
import { z } from "zod/v4";
|
|
3646
|
+
${bottleneckImport}import { z } from "zod/v4";
|
|
3585
3647
|
|
|
3586
3648
|
${fixtureImport}${applicantContactImport}${caseInsensitiveHeadersImport}${multipartBoolImport}${clientImport}
|
|
3587
3649
|
import type { BrowserSession } from "${ENGINE_PKG}/scraper/session";
|
|
3588
3650
|
import type { SitePlugin, SitePluginContext, SitePluginResult } from "${ENGINE_PKG}/site-plugin";
|
|
3589
3651
|
import { run${pascal}BrowserFlow } from "@/sites/${siteId}/flows/browser-flow";
|
|
3590
|
-
|
|
3591
|
-
const BASE_HEADERS: Record<string, string> = {
|
|
3592
|
-
${headersLiteral},
|
|
3593
|
-
};
|
|
3594
|
-
|
|
3595
|
-
// Safe ceiling: ${safeRps} rps — from recon rate-limit probe.
|
|
3596
|
-
const limiter = new Bottleneck({ minTime: ${minTime} });
|
|
3597
|
-
|
|
3652
|
+
${baseHeadersBlock}${limiterBlock}
|
|
3598
3653
|
const ${pascal}ResponseSchema = ${responseSchemaExpr};
|
|
3599
3654
|
|
|
3600
3655
|
export type ${pascal}Response = z.infer<typeof ${pascal}ResponseSchema>;
|
|
@@ -3605,10 +3660,7 @@ const ${pascal}PayloadSchema = ${payloadSchemaExpr};
|
|
|
3605
3660
|
|
|
3606
3661
|
export type ${pascal}Payload = z.infer<typeof ${pascal}PayloadSchema>;
|
|
3607
3662
|
${internalRequestReferenceBlock}${queryConst}${gqlCacheBlock}${fixtureComments}
|
|
3608
|
-
|
|
3609
|
-
* Plugin for ${siteId}. Tries the direct-HTTP hot path first; falls back to
|
|
3610
|
-
* Stagehand automatically on schema drift or bot challenge.
|
|
3611
|
-
*/
|
|
3663
|
+
${pluginDocComment}
|
|
3612
3664
|
export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
|
|
3613
3665
|
meta: {
|
|
3614
3666
|
siteId: ${JSON.stringify(siteId)},
|
|
@@ -3626,15 +3678,7 @@ export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
|
|
|
3626
3678
|
// encoding parseable.
|
|
3627
3679
|
apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || inputBody ? "\n multipart: true," : ""}
|
|
3628
3680
|
},
|
|
3629
|
-
|
|
3630
|
-
/** Hot path: direct HTTP — no browser, no LLM tokens. */
|
|
3631
|
-
async executeHttp(
|
|
3632
|
-
payload: ${pascal}Payload,
|
|
3633
|
-
${executeHttpBody.includes("context.") ? "context" : "_context"}: SitePluginContext
|
|
3634
|
-
): Promise<SitePluginResult<${pascal}Response>> {
|
|
3635
|
-
${executeHttpBody}
|
|
3636
|
-
},
|
|
3637
|
-
|
|
3681
|
+
${executeHttpMethodBlock}
|
|
3638
3682
|
/** Browser fallback: Stagehand + Steel — invoked only when hot path fails. */
|
|
3639
3683
|
async execute(
|
|
3640
3684
|
payload: ${pascal}Payload,
|
|
@@ -4150,15 +4194,60 @@ async function main() {
|
|
|
4150
4194
|
// Selection precedence: (A) the authoritative submit-manifest recon-browser
|
|
4151
4195
|
// wrote from the verified submission; else (B/C) pattern/heuristic extraction.
|
|
4152
4196
|
// The manifest is the only signal that separates a submission POST from a
|
|
4153
|
-
// page-chrome POST sharing its URL, so it wins when present
|
|
4197
|
+
// page-chrome POST sharing its URL, so it normally wins when present — but
|
|
4198
|
+
// a manifest built from a single flow-declared submit step cannot represent
|
|
4199
|
+
// a wizard whose every section saves independently, so it is only trusted
|
|
4200
|
+
// when it isn't a strict undercount of what the same captures' own
|
|
4201
|
+
// heuristic extraction finds.
|
|
4202
|
+
const patternedHeuristicActionCaptures = gql
|
|
4203
|
+
? graphqlActionSequence
|
|
4204
|
+
: collapseRedundantPatches(extractActionSequence(captures, baseUrl, submitPatterns));
|
|
4205
|
+
// The same undercount hazard applies one layer below the manifest: a
|
|
4206
|
+
// flow-declared submitEndpointPattern that matches only one section's URL
|
|
4207
|
+
// (the natural way to describe "the button that finishes the wizard")
|
|
4208
|
+
// filters the heuristic sequence down to that one capture even though the
|
|
4209
|
+
// same captures, read without the pattern, show every section saving
|
|
4210
|
+
// independently. A pattern that undercounts what the unfiltered heuristic
|
|
4211
|
+
// finds is therefore not trusted either — it falls back to the richer,
|
|
4212
|
+
// unfiltered sequence instead of collapsing a real multi-call flow into
|
|
4213
|
+
// the generic single-endpoint fallback.
|
|
4214
|
+
const unfilteredHeuristicActionCaptures = submitPatterns.endpoint === null && submitPatterns.body === null
|
|
4215
|
+
? patternedHeuristicActionCaptures
|
|
4216
|
+
: gql
|
|
4217
|
+
? extractGraphQLActionSequence(captures, baseUrl, null)
|
|
4218
|
+
: collapseRedundantPatches(extractActionSequence(captures, baseUrl, null));
|
|
4219
|
+
const patternUndercounts = patternedHeuristicActionCaptures.length < unfilteredHeuristicActionCaptures.length;
|
|
4220
|
+
if (patternUndercounts) {
|
|
4221
|
+
logger.info(`submission selection: ignoring submitEndpointPattern/submitBodyPattern (${patternedHeuristicActionCaptures.length} capture(s)) as an undercount of the unfiltered heuristic action sequence (${unfilteredHeuristicActionCaptures.length} capture(s))`);
|
|
4222
|
+
}
|
|
4223
|
+
const heuristicActionCaptures = patternUndercounts
|
|
4224
|
+
? unfilteredHeuristicActionCaptures
|
|
4225
|
+
: patternedHeuristicActionCaptures;
|
|
4154
4226
|
const manifestActionCaptures = resolveManifestActionSequence(runRoot, captures);
|
|
4155
|
-
|
|
4227
|
+
const manifestUndercounts = manifestActionCaptures !== null &&
|
|
4228
|
+
manifestActionCaptures.length < heuristicActionCaptures.length;
|
|
4229
|
+
if (manifestActionCaptures !== null && manifestUndercounts) {
|
|
4230
|
+
logger.info(`submission selection: ignoring submit-manifest.json (${manifestActionCaptures.length} capture(s)) as an undercount of the heuristic action sequence (${heuristicActionCaptures.length} capture(s))`);
|
|
4231
|
+
}
|
|
4232
|
+
else if (manifestActionCaptures !== null) {
|
|
4156
4233
|
logger.info(`submission selection: using submit-manifest.json (${manifestActionCaptures.length} authoritative capture(s))`);
|
|
4157
4234
|
}
|
|
4158
|
-
const rawActionCaptures = manifestActionCaptures
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4235
|
+
const rawActionCaptures = manifestActionCaptures !== null && !manifestUndercounts
|
|
4236
|
+
? manifestActionCaptures
|
|
4237
|
+
: heuristicActionCaptures;
|
|
4238
|
+
// The declared submitEndpointPattern's own under-match: unlike manifestUndercounts
|
|
4239
|
+
// (which compares an authoritative submit-manifest.json against the heuristic
|
|
4240
|
+
// sequence), this compares the pattern-filtered heuristic sequence against the SAME
|
|
4241
|
+
// captures with no submitPatterns filter at all — the true raw same-host non-GET 2xx
|
|
4242
|
+
// action set. A pattern that matches conspicuously fewer calls than that raw set is a
|
|
4243
|
+
// detection failure (the pattern is too narrow), not evidence the flow is read-only,
|
|
4244
|
+
// and per the no-silent-fallback rule must not be allowed to quietly collapse into the
|
|
4245
|
+
// generic single-endpoint {query} template below.
|
|
4246
|
+
const rawSameHostActionCaptures = submitEndpointPattern === null
|
|
4247
|
+
? null
|
|
4248
|
+
: gql
|
|
4249
|
+
? extractGraphQLActionSequence(captures, baseUrl, null)
|
|
4250
|
+
: collapseRedundantPatches(extractActionSequence(captures, baseUrl, null));
|
|
4162
4251
|
// Form-schema detection runs BEFORE state-indexing so the field-id/option-id
|
|
4163
4252
|
// UUIDs can be shielded from indexing — those UUIDs are stable schema
|
|
4164
4253
|
// anchors that T2/T3 substitution depends on remaining literal in body
|
|
@@ -4211,6 +4300,19 @@ async function main() {
|
|
|
4211
4300
|
: new Map();
|
|
4212
4301
|
const actionSteps = actionCaptures.length > 1 ? compileActionSteps(actionCaptures, stateIndex) : [];
|
|
4213
4302
|
const isSubmissionFlow = actionSteps.length > 1;
|
|
4303
|
+
// Loud failure for a submitEndpointPattern that under-matches the raw traffic badly
|
|
4304
|
+
// enough to collapse the flow to the single-endpoint fallback: heuristicActionCaptures
|
|
4305
|
+
// is the pattern-filtered sequence (used both directly and as rawActionCaptures' floor
|
|
4306
|
+
// via manifestUndercounts above), so if it's this thin ONLY because the pattern itself
|
|
4307
|
+
// excluded real action captures the unfiltered raw set still has, emitting the generic
|
|
4308
|
+
// {query} template would misrepresent a genuine multi-call flow as read-only. Exit
|
|
4309
|
+
// rather than degrade quietly, per the no-defensive/no-silent-fallback rule.
|
|
4310
|
+
if (rawSameHostActionCaptures !== null &&
|
|
4311
|
+
!isSubmissionFlow &&
|
|
4312
|
+
rawSameHostActionCaptures.length > heuristicActionCaptures.length) {
|
|
4313
|
+
logger.error(`ERROR declared submitEndpointPattern ${JSON.stringify(submitEndpointPattern)} matched only ${heuristicActionCaptures.length} of ${rawSameHostActionCaptures.length} raw same-host non-GET 2xx 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.`);
|
|
4314
|
+
process.exit(1);
|
|
4315
|
+
}
|
|
4214
4316
|
// Diagnostic for the FAILURE-3 shape (a flowless recon capture): a "submission flow"
|
|
4215
4317
|
// whose every action capture is landing-phase is almost certainly page-chrome
|
|
4216
4318
|
// bootstrap (e.g. page-chrome `POST /widgets`) misread as an apply flow, not a walked
|
|
@@ -4287,15 +4389,33 @@ async function main() {
|
|
|
4287
4389
|
staticBaseHeaders[k] = v;
|
|
4288
4390
|
}
|
|
4289
4391
|
}
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4392
|
+
// Third branch (browser-flow-only): a multi-action flow (isSubmissionFlow)
|
|
4393
|
+
// that crosses hosts mid-sequence (compileActionSteps' isCrossDomain — a
|
|
4394
|
+
// captured redirect off the original domain, e.g. an auth bounce or a
|
|
4395
|
+
// vendor-hosted submission step). A bare `fetch`-based executeHttp can't
|
|
4396
|
+
// reliably replay that: cookies/CSRF/session state minted for one origin
|
|
4397
|
+
// don't automatically carry to the next the way a real browser's redirect
|
|
4398
|
+
// handling does, so synthesizing a same-shape HTTP sequence would silently
|
|
4399
|
+
// drop the session boundary the recon actually walked. Per-step, this
|
|
4400
|
+
// already surfaces as the "cross-domain redirect detected ... likely needs
|
|
4401
|
+
// browser fallback for this step" TODO (see emitMultiStepExecuteHttp); at
|
|
4402
|
+
// the whole-flow level the honest emit is no executeHttp at all — never a
|
|
4403
|
+
// same-host multi-step body that quietly drops the hop, and never a
|
|
4404
|
+
// downgrade to the single-endpoint `{query}` branch either, since that's a
|
|
4405
|
+
// fabrication of its own kind for a flow that isn't a single-action
|
|
4406
|
+
// query/search to begin with.
|
|
4407
|
+
const browserFlowOnly = isSubmissionFlow && actionSteps.some((s) => s.isCrossDomain);
|
|
4408
|
+
const multiStepBody = browserFlowOnly
|
|
4409
|
+
? undefined
|
|
4410
|
+
: isSubmissionFlow
|
|
4411
|
+
? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, formSchema, personaBindings, entryUrlParams, shieldedUuids, selectResolutions, discoveredStructuredKeys, rawCodeFields)
|
|
4412
|
+
: undefined;
|
|
4293
4413
|
const hasMultipartStep = actionSteps.some((s) => s.isMultipart);
|
|
4294
4414
|
const headerBindings = collectHeaderBindings(actionSteps);
|
|
4295
4415
|
// Shape inference targets the SAME call executeHttp returns — see
|
|
4296
4416
|
// selectEffectiveResponseBody — so the two surfaces can't describe different calls.
|
|
4297
4417
|
const effectiveResponseBody = selectEffectiveResponseBody(isSubmissionFlow, actionSteps, responseBody);
|
|
4298
|
-
logger.info(`generating plugin for ${siteId} (${gql ? "GraphQL" : isSubmissionFlow ? `submission flow, ${actionSteps.length} steps` : "single-endpoint REST"}, baseUrl: ${baseUrl})`);
|
|
4418
|
+
logger.info(`generating plugin for ${siteId} (${gql ? "GraphQL" : browserFlowOnly ? `submission flow, ${actionSteps.length} steps, browser-flow-only (cross-domain hop detected)` : isSubmissionFlow ? `submission flow, ${actionSteps.length} steps` : "single-endpoint REST"}, baseUrl: ${baseUrl})`);
|
|
4299
4419
|
if (emit === "config") {
|
|
4300
4420
|
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
4301
4421
|
(0, node_fs_1.writeFileSync)(manifestPath, emitConfigManifest({
|
|
@@ -4309,7 +4429,9 @@ async function main() {
|
|
|
4309
4429
|
// A submission flow is the case where the `.ts` emit carries an
|
|
4310
4430
|
// executeHttp hot path; point the manifest at where the operator drops
|
|
4311
4431
|
// the compiled module rather than silently dropping the direct path.
|
|
4312
|
-
|
|
4432
|
+
// Not set for the browser-flow-only branch — there is no hot path to
|
|
4433
|
+
// compile a module for.
|
|
4434
|
+
httpModulePath: isSubmissionFlow && !browserFlowOnly ? `./${siteId}.http.js` : undefined,
|
|
4313
4435
|
}));
|
|
4314
4436
|
logger.info(`wrote ${manifestPath}`);
|
|
4315
4437
|
logger.info(`done — review ${manifestPath}, fill in response/extract schemas, then load via BARNACLE_PLUGINS or BARNACLE_PLUGINS_CONFIG_DIR (no compile step)`);
|
|
@@ -4346,6 +4468,7 @@ async function main() {
|
|
|
4346
4468
|
gqlVariables: primaryGraphQLOperation?.capture.variables ?? null,
|
|
4347
4469
|
auxFiles,
|
|
4348
4470
|
multiStepBody,
|
|
4471
|
+
omitExecuteHttp: browserFlowOnly,
|
|
4349
4472
|
inputBody,
|
|
4350
4473
|
hasMultipartStep,
|
|
4351
4474
|
discoveredFormFields,
|