@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.
Files changed (45) hide show
  1. package/dist/plugins/config-plugin.d.ts.map +1 -1
  2. package/dist/plugins/config-plugin.js +2 -1
  3. package/dist/plugins/config-plugin.js.map +1 -1
  4. package/dist/scraper/cdp-heartbeat.d.ts +34 -0
  5. package/dist/scraper/cdp-heartbeat.d.ts.map +1 -0
  6. package/dist/scraper/cdp-heartbeat.js +30 -0
  7. package/dist/scraper/cdp-heartbeat.js.map +1 -0
  8. package/dist/scraper/errors.d.ts +15 -1
  9. package/dist/scraper/errors.d.ts.map +1 -1
  10. package/dist/scraper/errors.js +24 -2
  11. package/dist/scraper/errors.js.map +1 -1
  12. package/dist/scraper/flow-runner.d.ts +10 -0
  13. package/dist/scraper/flow-runner.d.ts.map +1 -1
  14. package/dist/scraper/flow-runner.js +5 -1
  15. package/dist/scraper/flow-runner.js.map +1 -1
  16. package/dist/scraper/session-browserbase.d.ts.map +1 -1
  17. package/dist/scraper/session-browserbase.js +27 -1
  18. package/dist/scraper/session-browserbase.js.map +1 -1
  19. package/dist/scraper/session-shared.d.ts +18 -0
  20. package/dist/scraper/session-shared.d.ts.map +1 -1
  21. package/dist/scraper/session-shared.js.map +1 -1
  22. package/dist/scraper/session-steel.d.ts +9 -0
  23. package/dist/scraper/session-steel.d.ts.map +1 -1
  24. package/dist/scraper/session-steel.js +53 -0
  25. package/dist/scraper/session-steel.js.map +1 -1
  26. package/dist/scraper/session-teardown.d.ts +22 -0
  27. package/dist/scraper/session-teardown.d.ts.map +1 -0
  28. package/dist/scraper/session-teardown.js +54 -0
  29. package/dist/scraper/session-teardown.js.map +1 -0
  30. package/dist/scripts/recon-browser.d.ts.map +1 -1
  31. package/dist/scripts/recon-browser.js +23 -1
  32. package/dist/scripts/recon-browser.js.map +1 -1
  33. package/dist/scripts/recon-generate-multicall-fixture.d.ts +26 -0
  34. package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
  35. package/dist/scripts/recon-generate-multicall-fixture.js +47 -0
  36. package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
  37. package/dist/scripts/recon-generate-multiendpoint-fixture.d.ts +12 -0
  38. package/dist/scripts/recon-generate-multiendpoint-fixture.d.ts.map +1 -0
  39. package/dist/scripts/recon-generate-multiendpoint-fixture.js +108 -0
  40. package/dist/scripts/recon-generate-multiendpoint-fixture.js.map +1 -0
  41. package/dist/scripts/recon-generate.d.ts +8 -0
  42. package/dist/scripts/recon-generate.d.ts.map +1 -1
  43. package/dist/scripts/recon-generate.js +178 -55
  44. package/dist/scripts/recon-generate.js.map +1 -1
  45. 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
- const u = new URL(c.url);
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
- const responseSchemaExpr = multiStepBody ? `z.unknown()` : inferZodSchema(responseBody);
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
- const clientImport = gql
3489
- ? `import { createGraphqlClient } from "${ENGINE_PKG}/scraper/graphql-client";`
3490
- : `import { createHttpClient } from "${ENGINE_PKG}/scraper/http-client";`;
3491
- const queryConst = gql && gqlQuery
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 = gql
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
- const narrowSchemaChecklistLine = multiStepBody
3569
- ? `\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)`
3570
- : `\n * [ ] Narrow ${pascal}ResponseSchema to match the real response shape`;
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 Bottleneck from "bottleneck";
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
- if (manifestActionCaptures !== null) {
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
- (gql
4160
- ? graphqlActionSequence
4161
- : collapseRedundantPatches(extractActionSequence(captures, baseUrl, submitPatterns)));
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
- const multiStepBody = isSubmissionFlow
4291
- ? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, formSchema, personaBindings, entryUrlParams, shieldedUuids, selectResolutions, discoveredStructuredKeys, rawCodeFields)
4292
- : undefined;
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
- httpModulePath: isSubmissionFlow ? `./${siteId}.http.js` : undefined,
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,