@enricai/barnacle 1.12.40 → 1.12.42

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 (120) hide show
  1. package/dist/api/errors.d.ts +1 -1
  2. package/dist/api/errors.js +1 -1
  3. package/dist/api/helpers/reply.d.ts +1 -1
  4. package/dist/api/helpers/reply.js +1 -1
  5. package/dist/api/plugins/auth.js +1 -1
  6. package/dist/api/plugins/error-handler.js +3 -3
  7. package/dist/api/routes/submissions.js +2 -2
  8. package/dist/api/schemas/submissions.js +1 -1
  9. package/dist/lib/applicant-payload.js +3 -3
  10. package/dist/lib/datadog.js +2 -2
  11. package/dist/lib/dd-metrics.js +1 -1
  12. package/dist/lib/llm/anthropic-client.js +1 -1
  13. package/dist/lib/llm/judge.d.ts +1 -1
  14. package/dist/lib/llm/judge.js +2 -2
  15. package/dist/lib/llm/judges/error-messages.d.ts +2 -2
  16. package/dist/lib/llm/judges/error-messages.js +3 -3
  17. package/dist/lib/llm/judges/invalid-fields.d.ts +2 -2
  18. package/dist/lib/llm/judges/invalid-fields.js +3 -3
  19. package/dist/lib/llm/judges/modal-priority.d.ts +2 -2
  20. package/dist/lib/llm/judges/modal-priority.js +3 -3
  21. package/dist/lib/llm/judges/select-option.d.ts +2 -2
  22. package/dist/lib/llm/judges/select-option.js +3 -3
  23. package/dist/lib/llm/judges/verify-submit.d.ts +2 -2
  24. package/dist/lib/llm/judges/verify-submit.js +3 -3
  25. package/dist/lib/logging.js +2 -2
  26. package/dist/lib/option-matcher.js +2 -2
  27. package/dist/lib/statsd.js +3 -3
  28. package/dist/lib/telemetry/beacon-capture.d.ts +1 -1
  29. package/dist/lib/telemetry/beacon-capture.js +2 -2
  30. package/dist/lib/telemetry/call-capture.js +2 -2
  31. package/dist/lib/telemetry/reconciliation-source.d.ts +2 -2
  32. package/dist/lib/telemetry/reconciliation-source.js +5 -5
  33. package/dist/lib/telemetry/s3-sink.js +2 -2
  34. package/dist/lib/telemetry/submission-capture.js +3 -3
  35. package/dist/lib/telemetry/submission-query.d.ts +2 -2
  36. package/dist/lib/telemetry/submission-reader.d.ts +1 -1
  37. package/dist/lib/telemetry/submission-reader.js +2 -2
  38. package/dist/lib/telemetry/submissions-s3-objects.js +1 -1
  39. package/dist/lib/telemetry/submissions-s3-reader.d.ts +1 -1
  40. package/dist/lib/telemetry/submissions-s3-reader.js +3 -3
  41. package/dist/lib/tracking-click.js +4 -4
  42. package/dist/plugins/config-plugin.js +3 -3
  43. package/dist/plugins/discover.js +2 -2
  44. package/dist/recon/load-form-schema.d.ts +1 -1
  45. package/dist/recon/load-vocabulary.d.ts +1 -1
  46. package/dist/recon/load-vocabulary.js +1 -1
  47. package/dist/scraper/behavioral-signals.d.ts +1 -1
  48. package/dist/scraper/captcha-solver.d.ts +1 -1
  49. package/dist/scraper/captcha-solver.js +2 -2
  50. package/dist/scraper/cookie-jar.js +1 -1
  51. package/dist/scraper/deep-locator-actuate.d.ts +1 -1
  52. package/dist/scraper/deep-locator-actuate.js +3 -3
  53. package/dist/scraper/deep-locator-candidates.d.ts +1 -1
  54. package/dist/scraper/deep-locator-candidates.js +3 -3
  55. package/dist/scraper/deep-locator-click.d.ts +1 -1
  56. package/dist/scraper/deep-locator-click.js +1 -1
  57. package/dist/scraper/deep-locator-fake.d.ts +1 -1
  58. package/dist/scraper/deep-locator-scan.js +1 -1
  59. package/dist/scraper/deep-query.js +1 -1
  60. package/dist/scraper/flow-runner.d.ts +47 -4
  61. package/dist/scraper/flow-runner.d.ts.map +1 -1
  62. package/dist/scraper/flow-runner.js +200 -79
  63. package/dist/scraper/flow-runner.js.map +1 -1
  64. package/dist/scraper/frame-target.d.ts +16 -0
  65. package/dist/scraper/frame-target.d.ts.map +1 -1
  66. package/dist/scraper/frame-target.js +25 -1
  67. package/dist/scraper/frame-target.js.map +1 -1
  68. package/dist/scraper/graphql-client.js +1 -1
  69. package/dist/scraper/http-client.d.ts +1 -1
  70. package/dist/scraper/http-client.js +2 -2
  71. package/dist/scraper/http-status-classifier.js +1 -1
  72. package/dist/scraper/parse-json-response.d.ts +1 -1
  73. package/dist/scraper/parse-json-response.js +1 -1
  74. package/dist/scraper/pool.d.ts +3 -3
  75. package/dist/scraper/pool.js +3 -3
  76. package/dist/scraper/rate-limited-json-client.d.ts +1 -1
  77. package/dist/scraper/rate-limited-json-client.js +1 -1
  78. package/dist/scraper/raw-fetch.js +2 -2
  79. package/dist/scraper/require-response-field.js +1 -1
  80. package/dist/scraper/retry.d.ts +1 -1
  81. package/dist/scraper/retry.js +1 -1
  82. package/dist/scraper/session-browserbase.d.ts +1 -1
  83. package/dist/scraper/session-browserbase.js +6 -6
  84. package/dist/scraper/session-create-limiter.js +1 -1
  85. package/dist/scraper/session-ip.js +1 -1
  86. package/dist/scraper/session-shared.d.ts +1 -1
  87. package/dist/scraper/session-steel.d.ts +2 -2
  88. package/dist/scraper/session-steel.js +5 -5
  89. package/dist/scraper/session-teardown.d.ts +1 -1
  90. package/dist/scraper/session-teardown.js +1 -1
  91. package/dist/scraper/session-warmup.d.ts +1 -1
  92. package/dist/scraper/session.d.ts +2 -2
  93. package/dist/scraper/session.js +4 -4
  94. package/dist/scraper/spa-readiness.js +1 -1
  95. package/dist/scraper/stagehand-guard.d.ts +1 -1
  96. package/dist/scraper/stagehand-guard.js +2 -2
  97. package/dist/scraper/submit-control.js +1 -1
  98. package/dist/scripts/llm-heal.d.ts +2 -2
  99. package/dist/scripts/llm-heal.js +2 -2
  100. package/dist/scripts/recon-browser.d.ts +15 -0
  101. package/dist/scripts/recon-browser.d.ts.map +1 -1
  102. package/dist/scripts/recon-browser.js +61 -3
  103. package/dist/scripts/recon-browser.js.map +1 -1
  104. package/dist/scripts/recon-generate-multicall-fixture.d.ts +111 -2
  105. package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
  106. package/dist/scripts/recon-generate-multicall-fixture.js +311 -0
  107. package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
  108. package/dist/scripts/recon-generate-multiendpoint-fixture.d.ts +1 -1
  109. package/dist/scripts/recon-generate-repeated-section-fixture.d.ts +1 -1
  110. package/dist/scripts/recon-generate-repeated-section-fixture.js +1 -1
  111. package/dist/scripts/recon-generate.d.ts +1 -1
  112. package/dist/scripts/recon-generate.d.ts.map +1 -1
  113. package/dist/scripts/recon-generate.js +203 -28
  114. package/dist/scripts/recon-generate.js.map +1 -1
  115. package/dist/scripts/recon-http.js +1 -1
  116. package/dist/scripts/recon-replay-jobs.js +2 -2
  117. package/dist/scripts/recon-summarize.js +1 -1
  118. package/dist/testing/replay-integration-suite.mjs +1 -1
  119. package/dist/testmail/client.js +1 -1
  120. package/package.json +2 -2
@@ -17,8 +17,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.TRAILING_GRACE_WINDOW = exports.STEP_WATCHDOG_MS = exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR = exports.GOTO_TIMEOUT_MS = exports.waitForSpaReady = void 0;
18
18
  exports.mergeResponseHeaders = mergeResponseHeaders;
19
19
  exports.wireSignalCapture = wireSignalCapture;
20
+ exports.shouldRetryCaptchaRegistry = shouldRetryCaptchaRegistry;
20
21
  exports.prepareFailureDumpBody = prepareFailureDumpBody;
21
22
  exports.snapshotPage = snapshotPage;
23
+ exports.resolveDumpPageIdentity = resolveDumpPageIdentity;
22
24
  exports.findRecentBackendError = findRecentBackendError;
23
25
  exports.findRecentPageTransition = findRecentPageTransition;
24
26
  exports.isWizardExitAction = isWizardExitAction;
@@ -73,6 +75,7 @@ exports.parseWidgetOptionClickStep = parseWidgetOptionClickStep;
73
75
  exports.parseCheckStep = parseCheckStep;
74
76
  exports.pollEnumerate = pollEnumerate;
75
77
  exports.waitForTransitionBody = waitForTransitionBody;
78
+ exports.hasOriginOrPathChanged = hasOriginOrPathChanged;
76
79
  exports.injectCaptchaTokenAndSubmit = injectCaptchaTokenAndSubmit;
77
80
  exports.submitCaptchaGatedForm = submitCaptchaGatedForm;
78
81
  exports.trySelectPrimitive = trySelectPrimitive;
@@ -110,24 +113,24 @@ const logging_1 = require("../lib/logging");
110
113
  const call_capture_1 = require("../lib/telemetry/call-capture");
111
114
  const call_types_1 = require("../lib/telemetry/call-types");
112
115
  const capture_filters_1 = require("../recon/capture-filters");
113
- const browser_click_expr_1 = require("../scraper/browser-click-expr");
114
- const captcha_callback_capture_1 = require("../scraper/captcha-callback-capture");
115
- const captcha_solver_1 = require("../scraper/captcha-solver");
116
- const deep_locator_actuate_1 = require("../scraper/deep-locator-actuate");
117
- const deep_locator_candidates_1 = require("../scraper/deep-locator-candidates");
118
- const deep_locator_click_1 = require("../scraper/deep-locator-click");
119
- const deep_locator_scan_1 = require("../scraper/deep-locator-scan");
120
- const errors_2 = require("../scraper/errors");
121
- const frame_target_1 = require("../scraper/frame-target");
122
- const phantom_click_1 = require("../scraper/phantom-click");
123
- const session_teardown_1 = require("../scraper/session-teardown");
124
- const spa_readiness_1 = require("../scraper/spa-readiness");
125
- const stagehand_guard_1 = require("../scraper/stagehand-guard");
126
- const submit_control_1 = require("../scraper/submit-control");
127
- const watchdog_1 = require("../scraper/watchdog");
116
+ const browser_click_expr_1 = require("./browser-click-expr");
117
+ const captcha_callback_capture_1 = require("./captcha-callback-capture");
118
+ const captcha_solver_1 = require("./captcha-solver");
119
+ const deep_locator_actuate_1 = require("./deep-locator-actuate");
120
+ const deep_locator_candidates_1 = require("./deep-locator-candidates");
121
+ const deep_locator_click_1 = require("./deep-locator-click");
122
+ const deep_locator_scan_1 = require("./deep-locator-scan");
123
+ const errors_2 = require("./errors");
124
+ const frame_target_1 = require("./frame-target");
125
+ const phantom_click_1 = require("./phantom-click");
126
+ const session_teardown_1 = require("./session-teardown");
127
+ const spa_readiness_1 = require("./spa-readiness");
128
+ const stagehand_guard_1 = require("./stagehand-guard");
129
+ const submit_control_1 = require("./submit-control");
130
+ const watchdog_1 = require("./watchdog");
128
131
  const recon_shared_1 = require("../scripts/recon-shared");
129
132
  const client_1 = require("../testmail/client");
130
- var spa_readiness_2 = require("../scraper/spa-readiness");
133
+ var spa_readiness_2 = require("./spa-readiness");
131
134
  Object.defineProperty(exports, "waitForSpaReady", { enumerable: true, get: function () { return spa_readiness_2.waitForSpaReady; } });
132
135
  const logger = (0, logging_1.getLogger)({ name: "scraper/flow-runner" });
133
136
  /** Cap on the rolling capture-filename window held in memory for failure dumps. */
@@ -716,6 +719,31 @@ const ADVANCE_TRANSITION_POLL_INTERVAL_MS = 350;
716
719
  * {@link ADVANCE_TRANSITION_POLL_MS} unchanged.
717
720
  */
718
721
  const CAPTCHA_TRANSITION_POLL_MS = 45_000;
722
+ /**
723
+ * Bounded retry budget for the captchaGated solve+inject+registryState-check
724
+ * sequence. The registry-empty/absent condition (the hCaptcha render callback
725
+ * hasn't attached to `HCAPTCHA_CALLBACK_REGISTRY_GLOBAL` yet when the solve
726
+ * finishes) is an intermittent attach-timing race, not a deterministic
727
+ * failure, so a single attempt is too eager to give up. Mirrors the
728
+ * MAX_STEP_ATTEMPTS/PRIMITIVE_ENUMERATE_ATTEMPTS=5 naming precedent, but a
729
+ * captcha solve is expensive (10-30s+ round trip via a real solver service),
730
+ * so the budget is kept small.
731
+ */
732
+ const CAPTCHA_REGISTRY_RETRY_ATTEMPTS = 3;
733
+ /**
734
+ * Pure decision gate for the captchaGated registry-retry loop: isolates the
735
+ * retry-vs-give-up call from the solve+inject+poll I/O so the attach-timing
736
+ * race (registry empty/absent, or a callback that fired without a confirmed
737
+ * transition) can be pinned with a unit test independent of any browser
738
+ * evaluate() call.
739
+ */
740
+ function shouldRetryCaptchaRegistry(attemptNumber, maxAttempts, registryState, callbackDiscovered, confirmed) {
741
+ if (confirmed)
742
+ return false;
743
+ const registryRace = registryState === "empty" || registryState === "absent";
744
+ const callbackFiredButNoNav = callbackDiscovered && !confirmed;
745
+ return (registryRace || callbackFiredButNoNav) && attemptNumber < maxAttempts;
746
+ }
719
747
  /**
720
748
  * Attempts to decode opaque request parameters: tries JSON parse, then
721
749
  * URL-decode, then base64. Returns the decoded value or null if none worked.
@@ -939,10 +967,11 @@ async function snapshotPage(target, signalCounter, page, captureSelectionState =
939
967
  // A resolved child FrameTarget's url() reads location.href off the CDP
940
968
  // frame session (frame-target.ts's childFrameTarget), which rejects (or
941
969
  // trips its watchdog) once the OOPIF detaches — most commonly right after
942
- // a submit click tears down the wizard iframe. Falling back to page.url()
943
- // lets the post-submit navigation still register as a urlChanged signal
944
- // instead of throwing the whole step out of the cascade.
945
- const url = page ? await target.url().catch(() => page.url()) : await target.url();
970
+ // a submit click tears down the wizard iframe. readCurrentFrameUrl
971
+ // re-resolves against the declared frame selector before ever falling
972
+ // back to page.url(), so a same-origin in-frame navigation still reports
973
+ // its own scope's URL instead of masquerading as the top wrapper page.
974
+ const url = page ? await (0, frame_target_1.readCurrentFrameUrl)(page, target) : await target.url();
946
975
  return {
947
976
  networkCount: signalCounter.n,
948
977
  url,
@@ -980,7 +1009,7 @@ async function snapshotPage(target, signalCounter, page, captureSelectionState =
980
1009
  */
981
1010
  async function resolveDumpPageIdentity(page, frameTarget) {
982
1011
  const pageTitle = await (frameTarget ?? (0, frame_target_1.mainFrameTarget)(page)).title().catch(() => "");
983
- const pageUrl = await (frameTarget ? frameTarget.url() : Promise.resolve(page.url())).catch(() => page.url());
1012
+ const pageUrl = frameTarget ? await (0, frame_target_1.readCurrentFrameUrl)(page, frameTarget) : page.url();
984
1013
  return { pageTitle, pageUrl };
985
1014
  }
986
1015
  /**
@@ -4454,6 +4483,52 @@ async function waitForTransitionBody(params) {
4454
4483
  }
4455
4484
  return false;
4456
4485
  }
4486
+ /**
4487
+ * Reduces a URL to origin+pathname (query/hash ignored) so a captcha
4488
+ * target's post-submit `url()` can be compared to its pre-submit baseline
4489
+ * without a same-page query-string change (e.g. a step counter) registering
4490
+ * as a false advance. Unparseable URLs compare equal only to themselves.
4491
+ */
4492
+ function originAndPathOf(url) {
4493
+ try {
4494
+ const parsed = new URL(url);
4495
+ return `${parsed.origin}${parsed.pathname}`;
4496
+ }
4497
+ catch {
4498
+ return url;
4499
+ }
4500
+ }
4501
+ /**
4502
+ * Credits a captchaGated advance from an observed navigation: true only
4503
+ * when origin or pathname actually changed, so a same-page query-string
4504
+ * mutation (e.g. a step counter) never counts as an advance.
4505
+ */
4506
+ function hasOriginOrPathChanged(preUrl, postUrl) {
4507
+ return originAndPathOf(preUrl) !== originAndPathOf(postUrl);
4508
+ }
4509
+ /**
4510
+ * Polls the captcha target's URL for an origin/path change after a
4511
+ * multipart form submit, which navigates the page without producing any
4512
+ * matchable XHR body for {@link waitForTransitionBody} to catch — the
4513
+ * regex-based poll alone silently misses a full-page submit advance. Runs
4514
+ * independently of whether `advanceTransitionBodyPattern` is configured.
4515
+ */
4516
+ async function waitForCaptchaNavigation(params) {
4517
+ const { page, captchaTarget, baselineUrl, timeoutMs, intervalMs } = params;
4518
+ const check = async () => {
4519
+ const currentUrl = await (0, frame_target_1.readCurrentFrameUrl)(page, captchaTarget);
4520
+ return hasOriginOrPathChanged(baselineUrl, currentUrl);
4521
+ };
4522
+ if (await check())
4523
+ return true;
4524
+ const deadline = performance.now() + timeoutMs;
4525
+ while (performance.now() < deadline) {
4526
+ await page.waitForTimeout(intervalMs);
4527
+ if (await check())
4528
+ return true;
4529
+ }
4530
+ return false;
4531
+ }
4457
4532
  /**
4458
4533
  * Shared in-page discovery of the widget's callback, used identically by
4459
4534
  * `injectCaptchaTokenAndSubmit`'s precheck (to decide the branch) and its
@@ -7333,7 +7408,7 @@ async function executeStepWithHealing(params) {
7333
7408
  });
7334
7409
  if ((cfg.extract ?? "link") === "link") {
7335
7410
  const emailStepTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
7336
- const currentPageUrl = await emailStepTarget.url();
7411
+ const currentPageUrl = await (0, frame_target_1.readCurrentFrameUrl)(page, emailStepTarget);
7337
7412
  const url = extractLinkFromMessage(msg, cfg.linkPattern, currentPageUrl);
7338
7413
  if (!url) {
7339
7414
  throw new errors_2.EmailStepExtractError("no link matched in the verification email");
@@ -7629,52 +7704,51 @@ async function executeStepWithHealing(params) {
7629
7704
  }
7630
7705
  else {
7631
7706
  logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: sitekey found, requesting solve`);
7632
- const pageUrl = await captchaTarget.url();
7707
+ const pageUrl = await (0, frame_target_1.readCurrentFrameUrl)(page, captchaTarget);
7633
7708
  const uaRaw = await page.evaluate("navigator.userAgent").catch(() => null);
7634
7709
  const userAgent = typeof uaRaw === "string" ? uaRaw : undefined;
7635
- const solved = await (0, captcha_solver_1.solveCaptcha)({
7636
- type: "hcaptcha",
7637
- siteKey,
7638
- pageUrl,
7639
- isInvisible,
7640
- userAgent,
7641
- }).catch((err) => {
7642
- logger.error(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: solve failed (${(0, errors_1.toErrorMessage)(err)}); failing the step rather than silently proceeding`);
7643
- throw err;
7644
- });
7645
- const preCaptchaCaptureIdx = latestCaptureIndex(recentCaptures);
7646
- const injectResult = await injectCaptchaTokenAndSubmit(captchaTarget, solved.token);
7647
- const registryState = await captchaTarget.evaluate(`(() => {
7710
+ // Bounded retry around the solve+inject+registryState-check+poll unit:
7711
+ // the registry-empty/absent condition is an intermittent attach-timing
7712
+ // race (the render callback hasn't registered by the time the solve
7713
+ // finishes), not a deterministic failure, so a single attempt is too
7714
+ // eager to give up and route straight to a global replan. Also retries
7715
+ // when the widget's own callback fired (callbackDiscovered) but neither
7716
+ // transition poll below confirmed an advance — the callback-fired-but-
7717
+ // no-nav case is the same underlying race from the other side.
7718
+ for (let captchaAttempt = 1; captchaAttempt <= CAPTCHA_REGISTRY_RETRY_ATTEMPTS; captchaAttempt++) {
7719
+ if (captchaAttempt > 1) {
7720
+ // Re-assert the capture install in the target frame before
7721
+ // retrying: a late-attaching registry may not exist yet on the
7722
+ // first attempt, and re-running this idempotent install gives a
7723
+ // widget that renders on demand another chance to be caught.
7724
+ await captchaTarget.evaluate((0, captcha_callback_capture_1.buildHcaptchaCallbackCaptureScript)()).catch(() => undefined);
7725
+ }
7726
+ const solved = await (0, captcha_solver_1.solveCaptcha)({
7727
+ type: "hcaptcha",
7728
+ siteKey,
7729
+ pageUrl,
7730
+ isInvisible,
7731
+ userAgent,
7732
+ }).catch((err) => {
7733
+ logger.error(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: solve failed (${(0, errors_1.toErrorMessage)(err)}); failing the step rather than silently proceeding`);
7734
+ throw err;
7735
+ });
7736
+ const preCaptchaCaptureIdx = latestCaptureIndex(recentCaptures);
7737
+ const injectResult = await injectCaptchaTokenAndSubmit(captchaTarget, solved.token);
7738
+ const registryState = await captchaTarget.evaluate(`(() => {
7648
7739
  const registry = window[${JSON.stringify(captcha_callback_capture_1.HCAPTCHA_CALLBACK_REGISTRY_GLOBAL)}];
7649
7740
  if (!registry) return "absent";
7650
7741
  return Object.keys(registry).length === 0 ? "empty" : "populated";
7651
7742
  })()`);
7652
- logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: token injected=${injectResult.injected} hasForm=${injectResult.hasForm} callbackDiscovered=${injectResult.callbackDiscovered} registryState=${registryState}`);
7653
- // Reuse the EXISTING waitForTransitionBody poll (no new captcha-specific
7654
- // poll loop), just with a widened budget so the solve+submit round trip
7655
- // doesn't eat into the plain ADVANCE_TRANSITION_POLL_MS window. A
7656
- // pattern-confirmed transition here is treated as verified the same way
7657
- // the probe-absent branch below treats a detected transition — the
7658
- // normal cascade/verifier still runs when the pattern doesn't confirm
7659
- // (or none is configured), so this never replaces that verifier.
7660
- let confirmed = false;
7661
- if (advanceTransitionBodyPattern) {
7662
- confirmed = await waitForTransitionBody({
7663
- page,
7664
- preIdx: preCaptchaCaptureIdx,
7665
- advanceTransitionBodyPattern,
7666
- timeoutMs: CAPTCHA_TRANSITION_POLL_MS,
7667
- intervalMs: ADVANCE_TRANSITION_POLL_INTERVAL_MS,
7668
- });
7669
- logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: post-submit transition poll confirmed=${confirmed}`);
7670
- }
7671
- // Some widgets' own render callback already submitted the form as a
7672
- // side effect of the inject (the poll above would have confirmed it);
7673
- // only issue an explicit submit when no transition was observed AND a
7674
- // form is known to exist to submit — otherwise this would double-submit
7675
- // a form the widget's own callback already advanced.
7676
- if (!confirmed && injectResult.hasForm) {
7677
- await submitCaptchaGatedForm(captchaTarget);
7743
+ logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: attempt=${captchaAttempt}/${CAPTCHA_REGISTRY_RETRY_ATTEMPTS} token injected=${injectResult.injected} hasForm=${injectResult.hasForm} callbackDiscovered=${injectResult.callbackDiscovered} registryState=${registryState}`);
7744
+ // Reuse the EXISTING waitForTransitionBody poll (no new captcha-specific
7745
+ // poll loop), just with a widened budget so the solve+submit round trip
7746
+ // doesn't eat into the plain ADVANCE_TRANSITION_POLL_MS window. A
7747
+ // pattern-confirmed transition here is treated as verified the same way
7748
+ // the probe-absent branch below treats a detected transition — the
7749
+ // normal cascade/verifier still runs when the pattern doesn't confirm
7750
+ // (or none is configured), so this never replaces that verifier.
7751
+ let confirmed = false;
7678
7752
  if (advanceTransitionBodyPattern) {
7679
7753
  confirmed = await waitForTransitionBody({
7680
7754
  page,
@@ -7683,24 +7757,71 @@ async function executeStepWithHealing(params) {
7683
7757
  timeoutMs: CAPTCHA_TRANSITION_POLL_MS,
7684
7758
  intervalMs: ADVANCE_TRANSITION_POLL_INTERVAL_MS,
7685
7759
  });
7686
- logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: post-fallback-submit transition poll confirmed=${confirmed}`);
7760
+ logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: post-submit transition poll confirmed=${confirmed}`);
7687
7761
  }
7688
- }
7689
- if (confirmed) {
7690
- trajectory?.push({ stepIndex, verifiedBy: "network" });
7691
- return "completed";
7692
- }
7693
- // No render-config callback was discoverable AND neither the inject's
7694
- // own submit path nor the explicit fallback submit produced a
7695
- // confirmed transition: the token was never actually delivered
7696
- // through the site's own submit machinery. Fail loudly instead of
7697
- // falling through to the normal cascade as if the solve had worked —
7698
- // that's exactly the misleading "token injected=true" silent no-op
7699
- // this hook must not produce. Only applies when a pattern is actually
7700
- // configured to poll for — with none configured, no poll ever ran, so
7701
- // there's nothing here to contradict the normal cascade/verifier.
7702
- if (advanceTransitionBodyPattern && !injectResult.callbackDiscovered) {
7703
- throw new errors_2.CaptchaError(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: no render-config callback could be found and delivered, and no transition was confirmed after the solve`);
7762
+ // Some widgets' own render callback already submitted the form as a
7763
+ // side effect of the inject (the poll above would have confirmed it);
7764
+ // only issue an explicit submit when no transition was observed AND a
7765
+ // form is known to exist to submit — otherwise this would double-submit
7766
+ // a form the widget's own callback already advanced.
7767
+ if (!confirmed && injectResult.hasForm) {
7768
+ await submitCaptchaGatedForm(captchaTarget);
7769
+ if (advanceTransitionBodyPattern) {
7770
+ confirmed = await waitForTransitionBody({
7771
+ page,
7772
+ preIdx: preCaptchaCaptureIdx,
7773
+ advanceTransitionBodyPattern,
7774
+ timeoutMs: CAPTCHA_TRANSITION_POLL_MS,
7775
+ intervalMs: ADVANCE_TRANSITION_POLL_INTERVAL_MS,
7776
+ });
7777
+ logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: post-fallback-submit transition poll confirmed=${confirmed}`);
7778
+ }
7779
+ }
7780
+ if (confirmed) {
7781
+ trajectory?.push({ stepIndex, verifiedBy: "network" });
7782
+ return "completed";
7783
+ }
7784
+ // A full-page multipart form submit (no matching XHR body, or no
7785
+ // pattern configured at all) still navigates the page — that's a
7786
+ // real advance the requestPostData-based poll above can never see.
7787
+ // This poll runs regardless of whether advanceTransitionBodyPattern
7788
+ // is set: it's an additional signal, not a replacement for the
7789
+ // pattern-based one.
7790
+ const navigationConfirmed = await waitForCaptchaNavigation({
7791
+ page,
7792
+ captchaTarget,
7793
+ baselineUrl: pageUrl,
7794
+ timeoutMs: CAPTCHA_TRANSITION_POLL_MS,
7795
+ intervalMs: ADVANCE_TRANSITION_POLL_INTERVAL_MS,
7796
+ });
7797
+ if (navigationConfirmed) {
7798
+ logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: post-submit navigation to a new origin/path confirmed the advance`);
7799
+ trajectory?.push({ stepIndex, verifiedBy: "url" });
7800
+ return "completed";
7801
+ }
7802
+ // The intermittent race this loop exists for: a still-empty/absent
7803
+ // registry means the callback attach hadn't landed when this attempt
7804
+ // solved+injected, and a discovered-but-unconfirmed callback means it
7805
+ // fired without a poll seeing the resulting transition. Both are
7806
+ // worth another attempt within budget rather than an immediate throw.
7807
+ if (shouldRetryCaptchaRegistry(captchaAttempt, CAPTCHA_REGISTRY_RETRY_ATTEMPTS, registryState, injectResult.callbackDiscovered, confirmed)) {
7808
+ logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: registryState=${registryState} callbackDiscovered=${injectResult.callbackDiscovered} with no confirmed transition on attempt ${captchaAttempt}; retrying`);
7809
+ continue;
7810
+ }
7811
+ // No render-config callback was discoverable AND neither the inject's
7812
+ // own submit path nor the explicit fallback submit produced a
7813
+ // confirmed transition, network or navigation: the token was never
7814
+ // actually delivered through the site's own submit machinery. Fail
7815
+ // loudly instead of falling through to the normal cascade as if the
7816
+ // solve had worked — that's exactly the misleading "token
7817
+ // injected=true" silent no-op this hook must not produce. Only
7818
+ // applies when a pattern is actually configured to poll for — with
7819
+ // none configured, no network poll ever ran, so there's nothing here
7820
+ // to contradict the normal cascade/verifier.
7821
+ if (advanceTransitionBodyPattern && !injectResult.callbackDiscovered) {
7822
+ throw new errors_2.CaptchaError(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: no render-config callback could be found and delivered, and no transition was confirmed after the solve`);
7823
+ }
7824
+ break;
7704
7825
  }
7705
7826
  }
7706
7827
  }