@enricai/barnacle 1.9.7 → 1.9.9

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 (91) hide show
  1. package/README.md +6 -6
  2. package/dist/api/errors.d.ts +1 -1
  3. package/dist/api/errors.d.ts.map +1 -1
  4. package/dist/api/errors.js +3 -3
  5. package/dist/api/errors.js.map +1 -1
  6. package/dist/api/plugins/error-handler.js +1 -1
  7. package/dist/api/plugins/error-handler.js.map +1 -1
  8. package/dist/api/routes/submissions.d.ts +1 -1
  9. package/dist/api/schemas/telemetry.d.ts +1 -1
  10. package/dist/api/schemas/telemetry.js +1 -1
  11. package/dist/cache/response-cache.d.ts +1 -1
  12. package/dist/cache/response-cache.js +2 -2
  13. package/dist/config.d.ts +4 -4
  14. package/dist/lib/llm/judges/invalid-fields.d.ts +1 -1
  15. package/dist/lib/llm/judges/invalid-fields.d.ts.map +1 -1
  16. package/dist/lib/llm/judges/invalid-fields.js +1 -1
  17. package/dist/lib/llm/judges/verify-submit.js +1 -1
  18. package/dist/lib/logging.d.ts +1 -1
  19. package/dist/lib/logging.js +1 -1
  20. package/dist/lib/telemetry/beacon-capture.d.ts +1 -1
  21. package/dist/lib/telemetry/beacon-capture.js +1 -1
  22. package/dist/lib/telemetry/call-capture.d.ts +1 -1
  23. package/dist/lib/telemetry/call-capture.js +1 -1
  24. package/dist/lib/telemetry/reconciliation-source.d.ts +4 -4
  25. package/dist/lib/telemetry/reconciliation-source.js +5 -5
  26. package/dist/lib/telemetry/run-telemetry.d.ts +1 -1
  27. package/dist/lib/telemetry/run-telemetry.js +1 -1
  28. package/dist/lib/telemetry/s3-sink.d.ts +1 -1
  29. package/dist/lib/telemetry/s3-sink.js +1 -1
  30. package/dist/lib/telemetry/submissions-s3-objects.d.ts +1 -1
  31. package/dist/lib/telemetry/submissions-s3-objects.js +1 -1
  32. package/dist/lib/telemetry/submissions-s3-reader.d.ts +1 -1
  33. package/dist/lib/telemetry/submissions-s3-reader.js +1 -1
  34. package/dist/plugins/config-plugin.js +2 -2
  35. package/dist/recon/load-form-schema.d.ts +1 -1
  36. package/dist/recon/load-form-schema.js +1 -1
  37. package/dist/recon/load-vocabulary.d.ts +1 -1
  38. package/dist/recon/load-vocabulary.js +2 -2
  39. package/dist/scraper/deep-locator-actuate.d.ts +1 -1
  40. package/dist/scraper/deep-locator-actuate.js +3 -3
  41. package/dist/scraper/deep-locator-candidates.d.ts +2 -2
  42. package/dist/scraper/deep-locator-candidates.js +3 -3
  43. package/dist/scraper/deep-locator-fake.d.ts +8 -8
  44. package/dist/scraper/deep-locator-fake.d.ts.map +1 -1
  45. package/dist/scraper/deep-locator-fake.js +5 -5
  46. package/dist/scraper/deep-locator-fake.js.map +1 -1
  47. package/dist/scraper/deep-locator-scan.d.ts +5 -5
  48. package/dist/scraper/deep-locator-scan.js +7 -7
  49. package/dist/scraper/deep-query.js +1 -1
  50. package/dist/scraper/errors.d.ts +2 -2
  51. package/dist/scraper/errors.js +2 -2
  52. package/dist/scraper/flow-runner.d.ts +6 -6
  53. package/dist/scraper/flow-runner.js +18 -18
  54. package/dist/scraper/flow-runner.js.map +1 -1
  55. package/dist/scraper/frame-target.js +1 -1
  56. package/dist/scraper/http-client.js +1 -1
  57. package/dist/scraper/metrics.d.ts +1 -1
  58. package/dist/scraper/metrics.js +1 -1
  59. package/dist/scraper/phantom-click.d.ts +1 -1
  60. package/dist/scraper/phantom-click.d.ts.map +1 -1
  61. package/dist/scraper/retry.js +1 -1
  62. package/dist/scraper/retry.js.map +1 -1
  63. package/dist/scraper/session-browserbase.d.ts +3 -3
  64. package/dist/scraper/session-browserbase.js +4 -4
  65. package/dist/scraper/session-browserbase.js.map +1 -1
  66. package/dist/scraper/session-shared.d.ts +2 -2
  67. package/dist/scraper/stagehand-guard.d.ts +4 -4
  68. package/dist/scraper/stagehand-guard.js +5 -5
  69. package/dist/scraper/submit-control.js +1 -1
  70. package/dist/scripts/judge-llm-batch.d.ts +1 -1
  71. package/dist/scripts/llm-heal.d.ts +1 -1
  72. package/dist/scripts/recon-browser.d.ts +1 -1
  73. package/dist/scripts/recon-browser.js +2 -2
  74. package/dist/scripts/recon-generate-multicall-fixture.d.ts +1 -1
  75. package/dist/scripts/recon-generate.d.ts +11 -1
  76. package/dist/scripts/recon-generate.d.ts.map +1 -1
  77. package/dist/scripts/recon-generate.js +182 -22
  78. package/dist/scripts/recon-generate.js.map +1 -1
  79. package/dist/scripts/recon-heal.js +1 -1
  80. package/dist/scripts/recon-heal.js.map +1 -1
  81. package/dist/scripts/recon-http.d.ts +1 -1
  82. package/dist/scripts/recon-http.js +1 -1
  83. package/dist/scripts/recon-shared.d.ts +1 -1
  84. package/dist/server.js +1 -1
  85. package/dist/site-plugin.d.ts +1 -1
  86. package/dist/testing/contract-parity-suite.d.mts +1 -1
  87. package/dist/testing/contract-parity-suite.mjs +1 -1
  88. package/dist/testing/coverage-guard-suite.d.mts +1 -1
  89. package/dist/testing/coverage-guard-suite.mjs +1 -1
  90. package/dist/testing/replay-integration-suite.d.mts +1 -1
  91. package/package.json +2 -2
@@ -1657,10 +1657,10 @@ function locateFormEnvelopePath(parsedBody) {
1657
1657
  if (candidates.length === 0)
1658
1658
  return [];
1659
1659
  const maxP = Math.max(...candidates.map((c) => c.primitives));
1660
- // The analytics blob (`eventData`) mirrors the form, so the object with the
1660
+ // The analytics blob (`eventData`) parallels the form, so the object with the
1661
1661
  // MOST primitives can be a deep descendant of the true envelope. Pick the
1662
1662
  // SHALLOWEST primitive-rich object (≥ half the max) instead — that is the
1663
- // form envelope itself, whose analytics mirror sits below it. Tie-break on a
1663
+ // form envelope itself, whose analytics copy sits below it. Tie-break on a
1664
1664
  // higher primitive count. Threshold is relative, not a magic key name.
1665
1665
  const rich = candidates.filter((c) => c.primitives >= Math.max(1, maxP / 2));
1666
1666
  // No object carries a scalar field (maxP === 0): the body root is the only
@@ -2147,20 +2147,7 @@ function resolveResponsePathValue(responseBody, path) {
2147
2147
  * 36-char UUID).
2148
2148
  */
2149
2149
  function interpolateStateValues(template, priorSteps, payloadAccessorByValue = new Map()) {
2150
- const varNameByValue = new Map();
2151
- for (const step of priorSteps) {
2152
- for (const p of step.produces) {
2153
- // Header/cookie-origin produces have no body path — their value never
2154
- // appears as a literal in a URL/body template (http-client's `bind`
2155
- // forwards it directly as a request header), so there's nothing to
2156
- // interpolate here.
2157
- if (p.kind === "header")
2158
- continue;
2159
- const value = resolveResponsePathValue(step.capture.responseBody, p.path);
2160
- if (value !== null)
2161
- varNameByValue.set(value, p.name);
2162
- }
2163
- }
2150
+ const varNameByValue = deriveStateVarByValue(priorSteps);
2164
2151
  let result = template;
2165
2152
  // Pass 1: substitute state values (length-descending to avoid prefix
2166
2153
  // conflicts). `\$` is a literal dollar sign (NOT an interpolation);
@@ -2213,6 +2200,16 @@ function interpolateStateValues(template, priorSteps, payloadAccessorByValue = n
2213
2200
  * coordinate — and only WHOLE request-body leaves qualify, so every returned
2214
2201
  * value is one the whole-value pass will bind (and whose field must be declared).
2215
2202
  *
2203
+ * A field is NOT a caller coordinate when it is a per-step POSITION CONSTANT — the
2204
+ * step's own identifier that the harness varies as it walks the wizard (like
2205
+ * `stepIndex`). Such a field is produced-and-re-sent on every step, so it looks
2206
+ * like a coordinate, but each producer step echoes a DIFFERENT value; a genuine
2207
+ * coordinate (a jobId) is echoed with the SAME value on every step and the
2208
+ * per-value dedupe below leaves it a single entry. So any field bound to ≥2
2209
+ * distinct values across its producer steps is dropped wholesale: each step keeps
2210
+ * its own captured literal and the field is never declared as caller input.
2211
+ * Site-agnostic — the signal is value cardinality per field, not any field name.
2212
+ *
2216
2213
  * @param actions the compiled action steps (carry produces[] + request bodies)
2217
2214
  * @param alreadyBound capture value → existing accessor; a `payload.*` accessor
2218
2215
  * is reused, a non-payload one (e.g. `txnId`) vetoes the value
@@ -2273,6 +2270,20 @@ function deriveProducerBoundaryBindings(actions, alreadyBound) {
2273
2270
  bindings.set(value, { accessor: `payload.${field}`, field, producerIndex: i });
2274
2271
  }
2275
2272
  }
2273
+ // Drop per-step position constants: a field bound to ≥2 distinct values across
2274
+ // its producer steps is the step's own identifier the harness walked, not a
2275
+ // caller coordinate (which is echoed with one stable value). Removing every
2276
+ // entry leaves each step's captured literal in place and undeclared.
2277
+ const distinctValuesByField = new Map();
2278
+ for (const [value, binding] of bindings) {
2279
+ const values = distinctValuesByField.get(binding.field) ?? new Set();
2280
+ values.add(value);
2281
+ distinctValuesByField.set(binding.field, values);
2282
+ }
2283
+ for (const [value, binding] of [...bindings]) {
2284
+ if ((distinctValuesByField.get(binding.field)?.size ?? 0) >= 2)
2285
+ bindings.delete(value);
2286
+ }
2276
2287
  return bindings;
2277
2288
  }
2278
2289
  /**
@@ -2319,6 +2330,132 @@ function applyWholeValuePayloadSubstitutions(template, parsedBody, producerScope
2319
2330
  }
2320
2331
  return result;
2321
2332
  }
2333
+ /** Maximum number of nested URL-encodings a query-param value is probed for
2334
+ * before giving up. Real captures observed a doubly-encoded value (`%2520`); the
2335
+ * extra headroom costs one cheap `decodeURIComponent` per level and stops runaway. */
2336
+ const MAX_URL_PARAM_DECODE_DEPTH = 3;
2337
+ /**
2338
+ * Maps each response-produced value to the `${var}` name later steps thread it as.
2339
+ * Shared by {@link interpolateStateValues} (the body/URL substitution) and the
2340
+ * URL-param pass, so a threaded coordinate (e.g. a jobId a prior step produced)
2341
+ * resolves to the same var in both — one source of truth, they can never diverge.
2342
+ *
2343
+ * Header/cookie-origin produces are skipped: they have no body path and their
2344
+ * value never appears as a literal in a URL/body template (http-client's `bind`
2345
+ * forwards it directly as a request header), so there is nothing to interpolate.
2346
+ */
2347
+ function deriveStateVarByValue(priorSteps) {
2348
+ const varNameByValue = new Map();
2349
+ for (const step of priorSteps) {
2350
+ for (const p of step.produces) {
2351
+ if (p.kind === "header")
2352
+ continue;
2353
+ const value = resolveResponsePathValue(step.capture.responseBody, p.path);
2354
+ if (value !== null)
2355
+ varNameByValue.set(value, p.name);
2356
+ }
2357
+ }
2358
+ return varNameByValue;
2359
+ }
2360
+ /** One `decodeURIComponent`, or null when the input is not validly percent-encoded
2361
+ * (a stray `%` throws) — lets the progressive-decode loop stop instead of crash. */
2362
+ function safeDecodeOnce(value) {
2363
+ try {
2364
+ return decodeURIComponent(value);
2365
+ }
2366
+ catch {
2367
+ return null;
2368
+ }
2369
+ }
2370
+ /** True when a JSON string leaf is itself an http(s) URL, the only leaves whose
2371
+ * query string this pass rewrites. Site-agnostic: a structural test, not a key name. */
2372
+ function isHttpUrlLeaf(value) {
2373
+ if (!value.includes("://"))
2374
+ return false;
2375
+ try {
2376
+ const u = new URL(value);
2377
+ return u.protocol === "http:" || u.protocol === "https:";
2378
+ }
2379
+ catch {
2380
+ return false;
2381
+ }
2382
+ }
2383
+ /** Wraps an accessor expression in `encodeURIComponent(...)` `depth` times, so a
2384
+ * value the capture stored URL-encoded is re-encoded to the same nesting at call
2385
+ * time (a caller value with `&`/`=`/spaces then survives inside the query string). */
2386
+ function wrapEncode(expr, depth) {
2387
+ let out = expr;
2388
+ for (let d = 0; d < depth; d++)
2389
+ out = `encodeURIComponent(${out})`;
2390
+ return out;
2391
+ }
2392
+ /**
2393
+ * Binds caller coordinates that were copied into a URL-valued body field's query
2394
+ * string. Every other substitution pass matches whole leaf values, so a job
2395
+ * coordinate re-encoded inside a redirect/thank-you URL stays frozen at the recon
2396
+ * persona's value. This runs on the PRISTINE parsed body leaf — before state
2397
+ * threading fragments a composite — and rewrites each query-param value that
2398
+ * matches the unified binding table (after bounded progressive URL-decoding, so a
2399
+ * double-encoded copy is caught) into a `${encodeURIComponent(<accessor>)}`
2400
+ * fragment nested to the matched encode depth. Unmatched params (and the delimiters
2401
+ * around them) stay byte-for-byte. Site-agnostic: keys off "leaf parses as an
2402
+ * http(s) URL", never a field name.
2403
+ */
2404
+ function applyUrlParamPayloadSubstitutions(template, parsedBody, bindings) {
2405
+ if (bindings.size === 0)
2406
+ return template;
2407
+ let result = template;
2408
+ for (const { value, path } of walkStringLeaves(parsedBody)) {
2409
+ if (!isHttpUrlLeaf(value))
2410
+ continue;
2411
+ const qIdx = value.indexOf("?");
2412
+ if (qIdx < 0)
2413
+ continue;
2414
+ const prefix = value.slice(0, qIdx + 1);
2415
+ const rawQuery = value.slice(qIdx + 1);
2416
+ let changed = false;
2417
+ const newParams = rawQuery.split("&").map((pair) => {
2418
+ const eq = pair.indexOf("=");
2419
+ if (eq < 0)
2420
+ return pair;
2421
+ const name = pair.slice(0, eq);
2422
+ const rawVal = pair.slice(eq + 1);
2423
+ if (rawVal.length < MIN_STATE_VALUE_LENGTH)
2424
+ return pair;
2425
+ if (CACHE_BUSTER_QUERY_KEYS.has(name))
2426
+ return pair;
2427
+ let candidate = rawVal;
2428
+ let matched = null;
2429
+ let depth = 0;
2430
+ for (let d = 0; d <= MAX_URL_PARAM_DECODE_DEPTH; d++) {
2431
+ const accessor = bindings.get(candidate);
2432
+ if (accessor !== undefined) {
2433
+ matched = accessor;
2434
+ depth = d;
2435
+ break;
2436
+ }
2437
+ const next = safeDecodeOnce(candidate);
2438
+ if (next === null || next === candidate)
2439
+ break;
2440
+ candidate = next;
2441
+ }
2442
+ if (matched === null)
2443
+ return pair;
2444
+ changed = true;
2445
+ return `${name}=\${${wrapEncode(matched, depth)}}`;
2446
+ });
2447
+ if (!changed)
2448
+ continue;
2449
+ const newUrl = prefix + newParams.join("&");
2450
+ const key = path[path.length - 1] ?? "";
2451
+ if (key.length === 0)
2452
+ continue;
2453
+ const target = `"${key}":${JSON.stringify(value)}`;
2454
+ const replacement = `"${key}":"${newUrl}"`;
2455
+ result = result.split(target).join(replacement);
2456
+ }
2457
+ return result;
2458
+ }
2322
2459
  /**
2323
2460
  * Substitutes literal JSON key/value pairs in a body template with payload
2324
2461
  * interpolations. Catches short strings (e.g. Culture: "en"), booleans
@@ -2405,7 +2542,7 @@ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies
2405
2542
  * Documented closed set of JSON-key-name fragments (matched case-insensitively)
2406
2543
  * that mark a value as a per-request TIMESTAMP the plugin must generate fresh at
2407
2544
  * call time, not replay from the capture. Closed set per the no-regex-on-open-
2408
- * sets feedback, mirroring {@link CACHE_BUSTER_QUERY_KEYS}'s posture. A frozen
2545
+ * sets feedback, matching {@link CACHE_BUSTER_QUERY_KEYS}'s posture. A frozen
2409
2546
  * capture timestamp would make every submission claim the recon instant.
2410
2547
  */
2411
2548
  const VOLATILE_TIMESTAMP_KEY_FRAGMENTS = ["timestamp", "esign", "signeddate", "signedat"];
@@ -2766,8 +2903,28 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
2766
2903
  const rawBodyWithProducerBoundary = parsedBody !== null
2767
2904
  ? applyWholeValuePayloadSubstitutions(rawBodyWithStructuredSubs, parsedBody, producerBoundaryBindings, entryUrlParams, i)
2768
2905
  : rawBodyWithStructuredSubs;
2769
- const bodyAfterStateAndKv = rawBodyWithProducerBoundary
2770
- ? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithProducerBoundary, prior, payloadAccessorByValue), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys)
2906
+ // Coordinates copied into a URL-valued leaf's query string bind here, BEFORE
2907
+ // interpolateStateValues same discipline as the whole-value pass above. A
2908
+ // composite (a jobSeqNo whose prefix a prior step produces as a state var)
2909
+ // would otherwise be fragmented mid-URL by Pass 1's global split, stranding
2910
+ // the tail frozen. The unified table is highest-priority-last: a per-step
2911
+ // state var (jobId2) wins over the caller payload accessor for that same
2912
+ // coordinate on steps that thread it, matching the top-level slot's behaviour.
2913
+ const urlParamBindings = new Map(payloadAccessorByValue);
2914
+ for (const [value, accessor] of entryUrlParams)
2915
+ urlParamBindings.set(value, accessor);
2916
+ for (const [value, binding] of producerBoundaryBindings) {
2917
+ if (binding.producerIndex === i)
2918
+ urlParamBindings.set(value, binding.accessor);
2919
+ }
2920
+ for (const [value, varName] of deriveStateVarByValue(prior)) {
2921
+ urlParamBindings.set(value, varName);
2922
+ }
2923
+ const rawBodyWithUrlParams = parsedBody !== null
2924
+ ? applyUrlParamPayloadSubstitutions(rawBodyWithProducerBoundary, parsedBody, urlParamBindings)
2925
+ : rawBodyWithProducerBoundary;
2926
+ const bodyAfterStateAndKv = rawBodyWithUrlParams
2927
+ ? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, payloadAccessorByValue), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys)
2771
2928
  : "";
2772
2929
  // Mechanism A — generic (plain-JSON, wire-key-anchored) dropdown label→code
2773
2930
  // rewrite. Runs AFTER interpolateStateValues + the payload-KV pass, not
@@ -2833,7 +2990,10 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
2833
2990
  }
2834
2991
  // Identifier scan against the rendered text — captures `${foo}`, `${foo.bar}`,
2835
2992
  // etc. The first segment (anchored at `${`) is the binding's name. Closed
2836
- // grammar (template-literal syntax we generated ourselves).
2993
+ // grammar (template-literal syntax we generated ourselves). The optional
2994
+ // `encodeURIComponent(` prefixes let the URL-param pass wrap a bound accessor
2995
+ // (`${encodeURIComponent(jobTitle2)}`) without hiding the inner state var from
2996
+ // this scan — otherwise its `const` would be pruned and the reference dangle.
2837
2997
  // For multipart steps the bodyArg isn't emitted (the body is a FormData
2838
2998
  // built inline), but the URL and headers ARE in executable code — scan
2839
2999
  // only those two haystacks for multipart.
@@ -2844,7 +3004,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
2844
3004
  ? [r.url, r.headersExpr]
2845
3005
  : [r.url, r.headersExpr, r.bodyArg];
2846
3006
  for (const haystack of haystacks) {
2847
- for (const match of haystack.matchAll(/\$\{([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
3007
+ for (const match of haystack.matchAll(/\$\{(?:encodeURIComponent\()*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
2848
3008
  referencedNames.add(match[1]);
2849
3009
  }
2850
3010
  }
@@ -3163,7 +3323,7 @@ function emitContractTs(opts) {
3163
3323
  : "";
3164
3324
  // optionSchemaExtension is appended LAST so option enums show up at the
3165
3325
  // end of the payload type — the section ordering (base, multipart fields,
3166
- // form-schema fields, option enums, raw-option fields) mirrors the body
3326
+ // form-schema fields, option enums, raw-option fields) matches the body
3167
3327
  // emit order and keeps the generated payload type readable.
3168
3328
  const payloadSchemaExpr = hasMultipartStep
3169
3329
  ? `${basePayloadSchemaExpr}.extend({\n Resume: z.instanceof(Buffer),\n ResumeContentType: z.string(),\n ResumeFilename: z.string(),\n})${formFieldsExtension}${splicedFieldsExtension}${optionSchemaExtension}${rawOptionSchemaExtension}${additionalBodyKeysExtension}${structuredKeysExtension}`