@enricai/barnacle 1.12.51 → 1.12.54
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/scraper/captcha-callback-capture.d.ts +5 -1
- package/dist/scraper/captcha-callback-capture.d.ts.map +1 -1
- package/dist/scraper/captcha-callback-capture.js +46 -1
- package/dist/scraper/captcha-callback-capture.js.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.d.ts +156 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +489 -0
- package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +6 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +246 -87
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/scripts/recon-summarize.d.ts +11 -1
- package/dist/scripts/recon-summarize.d.ts.map +1 -1
- package/dist/scripts/recon-summarize.js +51 -32
- package/dist/scripts/recon-summarize.js.map +1 -1
- package/dist/scripts/smoke-test.d.ts +20 -1
- package/dist/scripts/smoke-test.d.ts.map +1 -1
- package/dist/scripts/smoke-test.js +13 -4
- package/dist/scripts/smoke-test.js.map +1 -1
- package/package.json +1 -1
|
@@ -3803,19 +3803,20 @@ function unknownValueAccessor(varName, path) {
|
|
|
3803
3803
|
}
|
|
3804
3804
|
/**
|
|
3805
3805
|
* Builds a nested TypeScript assertion type matching a JSON path. e.g.
|
|
3806
|
-
* ["Auth","Token"] -> `{ Auth: { Token: string } }`
|
|
3807
|
-
* ["Sections","
|
|
3808
|
-
* The leaf is
|
|
3809
|
-
* string
|
|
3810
|
-
*
|
|
3811
|
-
*
|
|
3812
|
-
|
|
3813
|
-
|
|
3806
|
+
* ["Auth","Token"], "string" -> `{ Auth: { Token: string } }`
|
|
3807
|
+
* ["Sections","Complete","0"], "boolean" -> `{ Sections: { Complete: { "0": boolean } } }`
|
|
3808
|
+
* The leaf type is the produce's actual captured {@link BodyProduce.leafType}
|
|
3809
|
+
* (string/number/boolean), so this always agrees with the same field's
|
|
3810
|
+
* schema-inferred Zod type from {@link inferZodSchemaFromSamples}. Used to
|
|
3811
|
+
* keep emitted code free of `any` casts while still letting nested-path
|
|
3812
|
+
* access compile against `Record<string, unknown>`-typed response variables.
|
|
3813
|
+
*/
|
|
3814
|
+
function pathToAssertionType(path, leafType) {
|
|
3814
3815
|
if (path.length === 0)
|
|
3815
|
-
return
|
|
3816
|
+
return leafType;
|
|
3816
3817
|
const segment = path[0];
|
|
3817
3818
|
const key = isValidJsIdentifier(segment) ? segment : JSON.stringify(segment);
|
|
3818
|
-
return `{ ${key}: ${pathToAssertionType(path.slice(1))} }`;
|
|
3819
|
+
return `{ ${key}: ${pathToAssertionType(path.slice(1), leafType)} }`;
|
|
3819
3820
|
}
|
|
3820
3821
|
/**
|
|
3821
3822
|
* Same nesting as {@link pathToAssertionType} but the leaf types as
|
|
@@ -4112,7 +4113,18 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
4112
4113
|
name = `${pathToVarName(path)}${suffix}`;
|
|
4113
4114
|
}
|
|
4114
4115
|
seenNames.add(name);
|
|
4115
|
-
|
|
4116
|
+
const leafType = typeof rawValue === "number"
|
|
4117
|
+
? "number"
|
|
4118
|
+
: typeof rawValue === "boolean"
|
|
4119
|
+
? "boolean"
|
|
4120
|
+
: "string";
|
|
4121
|
+
produces.push({
|
|
4122
|
+
kind: "body",
|
|
4123
|
+
name,
|
|
4124
|
+
path,
|
|
4125
|
+
leafType,
|
|
4126
|
+
eligibleConsumers: sv.eligibleConsumers,
|
|
4127
|
+
});
|
|
4116
4128
|
}
|
|
4117
4129
|
}
|
|
4118
4130
|
const ct = Object.entries(capture.requestHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
@@ -4380,8 +4392,35 @@ function replaceGuardedAgainstExistingPlaceholders(text, pattern, bindingByValue
|
|
|
4380
4392
|
* bare-value splice into either is exactly the name-free URL-path-segment
|
|
4381
4393
|
* threading the eligibility gate already intends to allow, so callers
|
|
4382
4394
|
* rendering those pass `false` (the default).
|
|
4383
|
-
|
|
4384
|
-
|
|
4395
|
+
*
|
|
4396
|
+
* A value already bound to a `payload.<field>` accessor keeps that accessor
|
|
4397
|
+
* on EVERY call EXCEPT the value's own producing step, where
|
|
4398
|
+
* {@link deriveProducerBoundaryBindings} has deliberately scoped a binding to
|
|
4399
|
+
* THAT step (`producerIndex === stepIndex`, threaded in as `stepIndex` below):
|
|
4400
|
+
* the producer cannot thread its own not-yet-existent response, so its own
|
|
4401
|
+
* request body needs the ordinary produced-value handling elsewhere (see
|
|
4402
|
+
* {@link applyWholeValuePayloadSubstitutions}'s docstring) rather than a second,
|
|
4403
|
+
* redundant payload bind here. Every OTHER step — including every step AFTER
|
|
4404
|
+
* the producer that re-sends the same coordinate — keeps payload precedence:
|
|
4405
|
+
* a value legitimately sourced from `payload.<field>` on one call must resolve
|
|
4406
|
+
* to that same accessor on every call that re-sends it, never fall back to the
|
|
4407
|
+
* coincidentally-equal scraped `${var}` just because the value also happens to
|
|
4408
|
+
* qualify as a producer-boundary coordinate. A value with no producer-boundary
|
|
4409
|
+
* binding at all is a coincidental echo — never re-sent by the step whose
|
|
4410
|
+
* response produced it — so payload precedence is unconditional for it.
|
|
4411
|
+
*
|
|
4412
|
+
* `topLevelPayloadKvValues` extends that same precedence to values too SHORT
|
|
4413
|
+
* to ever enter `payloadAccessorByValue` (which only registers inputBody
|
|
4414
|
+
* string leaves >= MIN_STATE_VALUE_LENGTH): a short top-level scalar
|
|
4415
|
+
* (`{"currency":"usd"}`) is still unconditionally payload-ified by the later
|
|
4416
|
+
* `applyPayloadKeyValueSubstitutions` key/value pass, but only if its literal
|
|
4417
|
+
* survives THIS pass untouched. Without this set, a short value that also
|
|
4418
|
+
* clears the chain/force-include exemption as a coincidentally-equal
|
|
4419
|
+
* response-produced state var on an intervening call gets spliced here
|
|
4420
|
+
* first, and the literal is gone by the time the KV pass runs — masking
|
|
4421
|
+
* payload precedence on every call after the one that scraped it.
|
|
4422
|
+
*/
|
|
4423
|
+
function interpolateStateValues(template, priorSteps, targetCapture, payloadAccessorByValue = new Map(), isJsonBody = false, producerBoundaryBindings = new Map(), stepIndex = -1, topLevelPayloadKvValues = new Set()) {
|
|
4385
4424
|
const stateBindings = deriveStateVarByValue(priorSteps, targetCapture);
|
|
4386
4425
|
const bindingByValue = new Map();
|
|
4387
4426
|
for (const [value, accessor] of payloadAccessorByValue) {
|
|
@@ -4391,6 +4430,11 @@ function interpolateStateValues(template, priorSteps, targetCapture, payloadAcce
|
|
|
4391
4430
|
const unconditionalValues = new Set();
|
|
4392
4431
|
const sourceNameByValue = new Map();
|
|
4393
4432
|
for (const [value, binding] of stateBindings) {
|
|
4433
|
+
const isProducerStep = producerBoundaryBindings.get(value)?.producerIndex === stepIndex;
|
|
4434
|
+
if (payloadAccessorByValue.has(value) && !isProducerStep)
|
|
4435
|
+
continue;
|
|
4436
|
+
if (topLevelPayloadKvValues.has(value) && !isProducerStep)
|
|
4437
|
+
continue;
|
|
4394
4438
|
bindingByValue.set(value, `\${${binding.varName}}`);
|
|
4395
4439
|
sourceNameByValue.set(value, binding.sourceName);
|
|
4396
4440
|
if (binding.restricted)
|
|
@@ -4569,11 +4613,14 @@ function deriveProducerBoundaryBindings(actions, alreadyBound) {
|
|
|
4569
4613
|
* the exact `"<key>":` slot, so it only fires on a value's own JSON slot.
|
|
4570
4614
|
*
|
|
4571
4615
|
* `producerScoped` bindings fire only on their producing step
|
|
4572
|
-
* (`producerIndex === stepIndex`):
|
|
4573
|
-
*
|
|
4574
|
-
*
|
|
4575
|
-
*
|
|
4576
|
-
*
|
|
4616
|
+
* (`producerIndex === stepIndex`): only the producer, which cannot thread its
|
|
4617
|
+
* own not-yet-existent response, needs this whole-value bind. A later step
|
|
4618
|
+
* re-sending the same coordinate still resolves to `payload.<field>` — via
|
|
4619
|
+
* {@link interpolateStateValues}'s own producer-scoped exemption, which lets
|
|
4620
|
+
* the produced state var win ONLY on the producing step itself — never via a
|
|
4621
|
+
* second whole-value bind here. `entryUrlBindings` (a caller coordinate lifted
|
|
4622
|
+
* from the entry URL) fire on EVERY step — they are the caller's data on every
|
|
4623
|
+
* request, never a produced var.
|
|
4577
4624
|
*/
|
|
4578
4625
|
function applyWholeValuePayloadSubstitutions(template, parsedBody, producerScoped, entryUrlBindings, stepIndex) {
|
|
4579
4626
|
if (producerScoped.size === 0 && entryUrlBindings.size === 0)
|
|
@@ -5207,6 +5254,32 @@ pascalName = null) {
|
|
|
5207
5254
|
// skip non-JSON bodies (e.g. multipart raw bytes)
|
|
5208
5255
|
}
|
|
5209
5256
|
}
|
|
5257
|
+
// Every value `applyPayloadKeyValueSubstitutions` will unconditionally
|
|
5258
|
+
// payload-ify from the ENTRY payload's own top-level key/value pairs,
|
|
5259
|
+
// gathered here so {@link interpolateStateValues} can give it payload
|
|
5260
|
+
// precedence too, regardless of MIN_STATE_VALUE_LENGTH — see
|
|
5261
|
+
// {@link interpolateStateValues}'s `topLevelPayloadKvValues` docstring for
|
|
5262
|
+
// why this must run BEFORE that later pass, not just alongside it.
|
|
5263
|
+
// `inputBody` only, deliberately NOT `additionalBodies`: a later call's own
|
|
5264
|
+
// top-level field is exactly as likely to be a genuinely re-sent
|
|
5265
|
+
// response-produced state value (draftId minted by an earlier call and
|
|
5266
|
+
// resent as that later call's own `applicationDraftId`) as a caller-
|
|
5267
|
+
// supplied literal, so extending precedence to it would wrongly freeze
|
|
5268
|
+
// real cross-step state threading. Only the caller's OWN entry payload is
|
|
5269
|
+
// an unambiguous non-state origin.
|
|
5270
|
+
const topLevelPayloadKvValues = new Set();
|
|
5271
|
+
if (inputBody !== undefined && inputBody !== null && !Array.isArray(inputBody)) {
|
|
5272
|
+
for (const { value, path } of walkAllPrimitiveLeaves(inputBody)) {
|
|
5273
|
+
if (path.length !== 1)
|
|
5274
|
+
continue;
|
|
5275
|
+
const key = path[0];
|
|
5276
|
+
if (!isValidJsIdentifier(key))
|
|
5277
|
+
continue;
|
|
5278
|
+
if (value === null)
|
|
5279
|
+
continue;
|
|
5280
|
+
topLevelPayloadKvValues.add(String(value));
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5210
5283
|
// Detect the flow's THREADED transaction id: a single UUID the site mints
|
|
5211
5284
|
// once (on page load) and reuses across every submit body to correlate the
|
|
5212
5285
|
// multi-step wizard — observed on real ATS flows where one such id spans
|
|
@@ -5314,7 +5387,7 @@ pascalName = null) {
|
|
|
5314
5387
|
// length/chain-eligibility scoping doesn't happen to catch the coincidence.
|
|
5315
5388
|
const url = (0, capture_filters_1.isZeroVarianceRepeatCapture)(cap, actions.map((a) => a.capture))
|
|
5316
5389
|
? cap.url
|
|
5317
|
-
: interpolateStateValues(cap.url, prior, cap, payloadAccessorByValue);
|
|
5390
|
+
: interpolateStateValues(cap.url, prior, cap, payloadAccessorByValue, false, producerBoundaryBindings, i);
|
|
5318
5391
|
// Form-schema substitution runs first on the raw recon body so its
|
|
5319
5392
|
// field-id-anchored matches see the original JSON. State-threading and
|
|
5320
5393
|
// payload key-value passes then run on top. Option-id substitution runs
|
|
@@ -5384,7 +5457,7 @@ pascalName = null) {
|
|
|
5384
5457
|
? applyUrlParamPayloadSubstitutions(rawBodyWithProducerBoundary, parsedBody, urlParamBindings)
|
|
5385
5458
|
: rawBodyWithProducerBoundary;
|
|
5386
5459
|
const bodyAfterStateAndKv = rawBodyWithUrlParams
|
|
5387
|
-
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys)
|
|
5460
|
+
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys)
|
|
5388
5461
|
: "";
|
|
5389
5462
|
// Mechanism A — generic (plain-JSON, wire-key-anchored) dropdown label→code
|
|
5390
5463
|
// rewrite. Runs AFTER interpolateStateValues + the payload-KV pass, not
|
|
@@ -5412,8 +5485,22 @@ pascalName = null) {
|
|
|
5412
5485
|
const perCallHeaders = {};
|
|
5413
5486
|
for (const [k, v] of Object.entries(cap.requestHeaders)) {
|
|
5414
5487
|
const lower = k.toLowerCase();
|
|
5415
|
-
|
|
5416
|
-
|
|
5488
|
+
const interpolated = interpolateStateValues(v, prior, cap, payloadAccessorByValue, false, producerBoundaryBindings, i);
|
|
5489
|
+
// Authorization/Api-Token and a structurally-detected join-carrying
|
|
5490
|
+
// header are always emitted per-call (even when interpolation finds
|
|
5491
|
+
// nothing to thread, matching this gate's prior behavior exactly).
|
|
5492
|
+
// Any OTHER header name also gets a per-call entry once interpolation
|
|
5493
|
+
// actually recognizes its value as a prior step's produced state var
|
|
5494
|
+
// — interpolateStateValues itself is the source of truth for whether
|
|
5495
|
+
// a value is genuinely threadable; restricting that recognition to a
|
|
5496
|
+
// closed set of header names left every other header name frozen as
|
|
5497
|
+
// a literal BASE_HEADERS entry (or dropped per-call) even when its
|
|
5498
|
+
// captured value was a real, already-produced response field.
|
|
5499
|
+
if (lower === "api-token" ||
|
|
5500
|
+
lower === "authorization" ||
|
|
5501
|
+
joinCarryingHeaderNames?.has(k) ||
|
|
5502
|
+
interpolated !== v) {
|
|
5503
|
+
perCallHeaders[k] = interpolated;
|
|
5417
5504
|
}
|
|
5418
5505
|
}
|
|
5419
5506
|
// G1: emit baseUrl-derived headers (Origin, Referer) per-call from
|
|
@@ -5677,23 +5764,10 @@ pascalName = null) {
|
|
|
5677
5764
|
// find it. Rebinding by structure, not value, lets referencesItemVar
|
|
5678
5765
|
// (below) correctly stay false so the fetch hoists above the
|
|
5679
5766
|
// per-item loop instead of pinning to it.
|
|
5680
|
-
//
|
|
5681
|
-
//
|
|
5682
|
-
//
|
|
5683
|
-
|
|
5684
|
-
// literal search must still key off the item's own field (that's
|
|
5685
|
-
// the value textually present), while the emitted accessor points
|
|
5686
|
-
// at the ancestor's structurally-corresponding field instead —
|
|
5687
|
-
// searching for the ANCESTOR field's (different) value would never
|
|
5688
|
-
// match anything in `text` and silently freeze the literal.
|
|
5689
|
-
const threadedFieldPairs = isAncestorScoped
|
|
5690
|
-
? rawThreadedFields.map((tf) => ({
|
|
5691
|
-
valueField: tf,
|
|
5692
|
-
accessorField: tf.varName === itemVar
|
|
5693
|
-
? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
|
|
5694
|
-
: tf,
|
|
5695
|
-
}))
|
|
5696
|
-
: rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
|
|
5767
|
+
// See buildThreadedFieldPairs's doc for why the literal-value
|
|
5768
|
+
// search and the rendered accessor deliberately diverge for a
|
|
5769
|
+
// proven ancestor-scoped rebind.
|
|
5770
|
+
const threadedFieldPairs = buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes);
|
|
5697
5771
|
// Accessor swap first: rewrites an already-templated `${payload.X}`
|
|
5698
5772
|
// reference (from applyPayloadKeyValueSubstitutions) to this field's
|
|
5699
5773
|
// real accessor. Each target (`${payload.X}`) is a unique, fully
|
|
@@ -5701,6 +5775,7 @@ pascalName = null) {
|
|
|
5701
5775
|
// never re-shaped into `${payload.X}` form) can't re-match, so a
|
|
5702
5776
|
// sequential pass here carries none of the reentrancy risk the
|
|
5703
5777
|
// literal-value pass below has.
|
|
5778
|
+
let swapFiredForItemBoundField = false;
|
|
5704
5779
|
const swapped = threadedFieldPairs.reduce((acc, { valueField, accessorField }) => {
|
|
5705
5780
|
const replacement = `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`;
|
|
5706
5781
|
// applyPayloadKeyValueSubstitutions only ever names a payload
|
|
@@ -5714,10 +5789,16 @@ pascalName = null) {
|
|
|
5714
5789
|
// reference behind once the literal value itself has already
|
|
5715
5790
|
// been replaced by the payload-key-value pass.
|
|
5716
5791
|
const lastSegment = valueField.field.split(".").pop();
|
|
5792
|
+
const payloadFieldPlaceholder = `\${payload.${valueField.field}}`;
|
|
5793
|
+
const payloadLastSegmentPlaceholder = `\${payload.${lastSegment}}`;
|
|
5794
|
+
if (accessorField.varName === itemVar &&
|
|
5795
|
+
(acc.includes(payloadFieldPlaceholder) || acc.includes(payloadLastSegmentPlaceholder))) {
|
|
5796
|
+
swapFiredForItemBoundField = true;
|
|
5797
|
+
}
|
|
5717
5798
|
return acc
|
|
5718
|
-
.split(
|
|
5799
|
+
.split(payloadFieldPlaceholder)
|
|
5719
5800
|
.join(replacement)
|
|
5720
|
-
.split(
|
|
5801
|
+
.split(payloadLastSegmentPlaceholder)
|
|
5721
5802
|
.join(replacement);
|
|
5722
5803
|
}, text);
|
|
5723
5804
|
// Literal-value substitution: ONE guarded regex-alternation pass over
|
|
@@ -5740,6 +5821,7 @@ pascalName = null) {
|
|
|
5740
5821
|
: {
|
|
5741
5822
|
value: stringValue,
|
|
5742
5823
|
replacement: `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`,
|
|
5824
|
+
isItemBound: accessorField.varName === itemVar,
|
|
5743
5825
|
};
|
|
5744
5826
|
})
|
|
5745
5827
|
.filter((b) => b !== null);
|
|
@@ -5754,6 +5836,25 @@ pascalName = null) {
|
|
|
5754
5836
|
const filteredValueBindings = isProvenInvariant
|
|
5755
5837
|
? valueBindings.filter((b) => isGenuineVaryingQueryValue(b.value, chainCapture, allCaptures))
|
|
5756
5838
|
: valueBindings;
|
|
5839
|
+
// Ground truth for the hoist gate, computed per FIELD from what
|
|
5840
|
+
// this call actually spliced into the request, rather than
|
|
5841
|
+
// inferred after the fact by re-scanning the fully rendered text
|
|
5842
|
+
// for `itemVar`: a target's OTHER join fields can independently
|
|
5843
|
+
// prove isAncestorScoped and rebind cleanly while THIS field's own
|
|
5844
|
+
// rebind attempt still falls back to `?? tf` (no structurally-
|
|
5845
|
+
// corresponding ancestor field exists for it). Deriving
|
|
5846
|
+
// `referencesItemVar` straight from the same value/placeholder
|
|
5847
|
+
// bindings that feed the splice below — rather than relying
|
|
5848
|
+
// solely on the post-hoc itemVar word-boundary scan — closes the
|
|
5849
|
+
// gap where a fallback accessor's occurrence in the final text
|
|
5850
|
+
// could otherwise be missed. A binding whose value never survives
|
|
5851
|
+
// to `filteredValueBindings` (unresolved, or excluded by the
|
|
5852
|
+
// zero-variance guard) is correctly excluded here too — it never
|
|
5853
|
+
// reaches the rendered request, so it can't leak an out-of-scope
|
|
5854
|
+
// identifier into it.
|
|
5855
|
+
if (swapFiredForItemBoundField || filteredValueBindings.some((b) => b.isItemBound)) {
|
|
5856
|
+
referencesItemVar = true;
|
|
5857
|
+
}
|
|
5757
5858
|
const result = substituteThreadedValues(swapped, filteredValueBindings);
|
|
5758
5859
|
const withDrillParamBindings = applyDrillParamBindings(foldReturnSpec, chainCapture, result);
|
|
5759
5860
|
// The frozen-varying-param safety net exists to catch a
|
|
@@ -5785,7 +5886,15 @@ pascalName = null) {
|
|
|
5785
5886
|
// there is no ancestor loop to hoist into in the first place (a
|
|
5786
5887
|
// flat, single-level fold), so it is treated as item-scoped too —
|
|
5787
5888
|
// existing flat folds must keep emitting byte-identical code.
|
|
5788
|
-
|
|
5889
|
+
// Word-boundary match on the bare identifier — not anchored to
|
|
5890
|
+
// `${itemVar` — because `scopedAccessor`/`unknownValueAccessor`
|
|
5891
|
+
// wraps every non-leaf hop in a `(itemVar.field as Record<string,
|
|
5892
|
+
// unknown>)` cast for a nested field path, so the identifier can
|
|
5893
|
+
// appear as `${(itemVar...` rather than immediately after `${`. An
|
|
5894
|
+
// anchored pattern misses that shape entirely and lets a genuinely
|
|
5895
|
+
// item-scoped reference get hoisted above the item loop, where
|
|
5896
|
+
// `itemVar` is never declared.
|
|
5897
|
+
const itemVarRefPattern = new RegExp(`\\b${itemVar}\\b`);
|
|
5789
5898
|
let referencesItemVar = ancestorVars.length === 0;
|
|
5790
5899
|
for (const chainIndex of target.chain) {
|
|
5791
5900
|
const chainStep = actions[chainIndex];
|
|
@@ -5814,7 +5923,7 @@ pascalName = null) {
|
|
|
5814
5923
|
if (!referencedNames.has(p.name))
|
|
5815
5924
|
continue;
|
|
5816
5925
|
chainDeclared.add(p.name);
|
|
5817
|
-
const assertion = pathToAssertionType(p.path);
|
|
5926
|
+
const assertion = pathToAssertionType(p.path, p.leafType);
|
|
5818
5927
|
chainLines.push(` const ${p.name} = (${chainStep.varName} as ${assertion})${pathToAccessor(p.path, { assertNonNull: false })};`);
|
|
5819
5928
|
}
|
|
5820
5929
|
}
|
|
@@ -5856,7 +5965,7 @@ pascalName = null) {
|
|
|
5856
5965
|
if (!referencedNames.has(p.name))
|
|
5857
5966
|
continue;
|
|
5858
5967
|
declaredNames.add(p.name);
|
|
5859
|
-
const assertion = pathToAssertionType(p.path);
|
|
5968
|
+
const assertion = pathToAssertionType(p.path, p.leafType);
|
|
5860
5969
|
produceLines.push(` const ${p.name} = (${step.varName} as ${assertion})${pathToAccessor(p.path, { assertNonNull: false })};`);
|
|
5861
5970
|
}
|
|
5862
5971
|
const bindResponse = referencedNames.has(step.varName) || produceLines.length > 0;
|
|
@@ -5883,8 +5992,14 @@ pascalName = null) {
|
|
|
5883
5992
|
const perCallHeaderEntries = [];
|
|
5884
5993
|
for (const [k, v] of Object.entries(cap.requestHeaders)) {
|
|
5885
5994
|
const lower = k.toLowerCase();
|
|
5886
|
-
|
|
5887
|
-
|
|
5995
|
+
const interpolated = interpolateStateValues(v, actions.slice(0, i), cap, payloadAccessorByValue, false, producerBoundaryBindings, i);
|
|
5996
|
+
// Mirrors the non-multipart per-call header builder above: any
|
|
5997
|
+
// header name (not just Authorization/Api-Token) that interpolation
|
|
5998
|
+
// recognizes as a prior step's produced state var must thread
|
|
5999
|
+
// per-call too, or its custom name freezes into an invariant
|
|
6000
|
+
// BASE_HEADERS-equivalent literal.
|
|
6001
|
+
if (lower === "api-token" || lower === "authorization" || interpolated !== v) {
|
|
6002
|
+
perCallHeaderEntries.push(`${JSON.stringify(k)}: \`${interpolated}\``);
|
|
5888
6003
|
}
|
|
5889
6004
|
}
|
|
5890
6005
|
// G1+G2: include tenant-derived headers in the multipart fetch too.
|
|
@@ -6934,29 +7049,44 @@ function findStructurallyCorrespondingAncestorField(ancestorScopes, field) {
|
|
|
6934
7049
|
}
|
|
6935
7050
|
return null;
|
|
6936
7051
|
}
|
|
6937
|
-
/** Recursively searches `obj` for
|
|
6938
|
-
*
|
|
6939
|
-
*
|
|
6940
|
-
* `obj`'s root through the array's
|
|
6941
|
-
* `["entries", "
|
|
6942
|
-
* as `.entries["
|
|
6943
|
-
* a bracketed literal key or an array index.
|
|
7052
|
+
/** Recursively searches `obj` for an array-valued property holding an
|
|
7053
|
+
* element (itself a plain object) that contains `lastSegment` as one of its
|
|
7054
|
+
* own field paths' trailing segments, returning the full dot-path from
|
|
7055
|
+
* `obj`'s root through the array's index down to that field (e.g.
|
|
7056
|
+
* `["entries", "1", "code"]`) — an accessor {@link pathToAccessor} renders
|
|
7057
|
+
* as `.entries["1"].code`, valid whether the numeric segment is treated as
|
|
7058
|
+
* a bracketed literal key or an array index. Scans every element of a
|
|
7059
|
+
* candidate array, not only the first: a sparse first element (e.g. a
|
|
7060
|
+
* sold-out entry missing an optional nested object that every OTHER sibling
|
|
7061
|
+
* genuinely carries) must not silently defeat a structural correspondence
|
|
7062
|
+
* that a later element in the same array would prove. This applies equally
|
|
7063
|
+
* to the recursive descent into a NESTED array-within-array: it also walks
|
|
7064
|
+
* every sibling element rather than only element `0`, so a sparse first
|
|
7065
|
+
* element one level up can't hide a match that lives in a nested array on a
|
|
7066
|
+
* later sibling. */
|
|
6944
7067
|
function findFieldInFirstArrayElement(obj, lastSegment, path = []) {
|
|
6945
7068
|
for (const [key, value] of Object.entries(obj)) {
|
|
6946
7069
|
const childPath = [...path, key];
|
|
6947
7070
|
if (Array.isArray(value)) {
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
7071
|
+
for (let i = 0; i < value.length; i++) {
|
|
7072
|
+
const candidate = value[i];
|
|
7073
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
|
|
7074
|
+
continue;
|
|
7075
|
+
const candidateObj = candidate;
|
|
7076
|
+
for (const { path: subPath } of walkItemFieldPaths(candidateObj)) {
|
|
7077
|
+
if (subPath[subPath.length - 1] === lastSegment) {
|
|
7078
|
+
return [...childPath, String(i), ...subPath];
|
|
7079
|
+
}
|
|
6955
7080
|
}
|
|
6956
7081
|
}
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
7082
|
+
for (let i = 0; i < value.length; i++) {
|
|
7083
|
+
const candidate = value[i];
|
|
7084
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
|
|
7085
|
+
continue;
|
|
7086
|
+
const nested = findFieldInFirstArrayElement(candidate, lastSegment, [...childPath, String(i)]);
|
|
7087
|
+
if (nested)
|
|
7088
|
+
return nested;
|
|
7089
|
+
}
|
|
6960
7090
|
continue;
|
|
6961
7091
|
}
|
|
6962
7092
|
if (value !== null && typeof value === "object") {
|
|
@@ -6967,6 +7097,34 @@ function findFieldInFirstArrayElement(obj, lastSegment, path = []) {
|
|
|
6967
7097
|
}
|
|
6968
7098
|
return null;
|
|
6969
7099
|
}
|
|
7100
|
+
/** Pairs each raw threaded field (whose VALUE is what a drill request's
|
|
7101
|
+
* captured text actually contains — the only thing a literal-value search
|
|
7102
|
+
* can ever find) with the accessor it should render as, shared by
|
|
7103
|
+
* {@link emitMultiStepExecuteHttp} and {@link emitContractTs}'s identical
|
|
7104
|
+
* fold-hoist rebind so the two emitters can never drift on this logic (see
|
|
7105
|
+
* this module's fold-plan investigation notes on the two call sites having
|
|
7106
|
+
* previously diverged). For a proven-ancestor-scoped target these
|
|
7107
|
+
* deliberately diverge: the literal search must still key off the item's own
|
|
7108
|
+
* field (the value textually present in the captured request), while the
|
|
7109
|
+
* emitted accessor points at the ancestor's structurally-corresponding field
|
|
7110
|
+
* instead — searching for the ANCESTOR field's (different) value would never
|
|
7111
|
+
* match anything in the rendered text and silently freeze the literal. When
|
|
7112
|
+
* no structurally-corresponding ancestor field exists, the accessor stays
|
|
7113
|
+
* item-bound (`?? tf`) — safe only because every caller's own itemVar
|
|
7114
|
+
* word-boundary scan over the FINAL rendered text (after this pairing feeds
|
|
7115
|
+
* substitution) is what actually decides whether the target may hoist above
|
|
7116
|
+
* its item loop, not this function. */
|
|
7117
|
+
function buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes) {
|
|
7118
|
+
if (!isAncestorScoped) {
|
|
7119
|
+
return rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
|
|
7120
|
+
}
|
|
7121
|
+
return rawThreadedFields.map((tf) => ({
|
|
7122
|
+
valueField: tf,
|
|
7123
|
+
accessorField: tf.varName === itemVar
|
|
7124
|
+
? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
|
|
7125
|
+
: tf,
|
|
7126
|
+
}));
|
|
7127
|
+
}
|
|
6970
7128
|
/** Every string, numeric, and boolean leaf value present anywhere in a response —
|
|
6971
7129
|
* the set a chained drill-down step's request must overlap with for that
|
|
6972
7130
|
* step to count as depending on this response. Deliberately walks the WHOLE
|
|
@@ -9180,7 +9338,11 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9180
9338
|
// Every target's join/merge — plus any non-hoistable target's own
|
|
9181
9339
|
// chain fetch — goes here, spliced inside the item loop.
|
|
9182
9340
|
const itemScopedLines = [];
|
|
9183
|
-
|
|
9341
|
+
// Word-boundary match — see emitMultiStepExecuteHttp's identical
|
|
9342
|
+
// `itemVarRefPattern` for why an anchored `${itemVar` pattern misses a
|
|
9343
|
+
// nested field path's `${(itemVar.field as Record<string,
|
|
9344
|
+
// unknown>)...}` cast wrapper.
|
|
9345
|
+
const itemVarRefPattern = new RegExp(`\\b${itemVar}\\b`);
|
|
9184
9346
|
for (const [targetIndex, target] of foldPlan.targets.entries()) {
|
|
9185
9347
|
const matchedPrimaryItem = primaryItemsWithAncestors[target.primaryMatchedItemIndex];
|
|
9186
9348
|
if (!matchedPrimaryItem) {
|
|
@@ -9250,29 +9412,11 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9250
9412
|
...target.joinFields.map((field) => ({ varName: itemVar, field })),
|
|
9251
9413
|
...findThreadedJoinFields(threadingScopes, chainCapture, allCaptures),
|
|
9252
9414
|
]);
|
|
9253
|
-
// Mirrors emitMultiStepExecuteHttp's identical rebind
|
|
9254
|
-
//
|
|
9255
|
-
//
|
|
9256
|
-
//
|
|
9257
|
-
|
|
9258
|
-
// Pairs each raw threaded field (whose VALUE is what's actually
|
|
9259
|
-
// present in `rawUrl` — the only thing a literal-value search can
|
|
9260
|
-
// ever find) with the accessor it should render as. For an
|
|
9261
|
-
// ancestor-scoped rebind these deliberately diverge: the literal
|
|
9262
|
-
// search must still key off the item's own field (that's the
|
|
9263
|
-
// value textually present in the URL), while the emitted accessor
|
|
9264
|
-
// points at the ancestor's structurally-corresponding field
|
|
9265
|
-
// instead — searching for the ANCESTOR field's (different) value
|
|
9266
|
-
// would never match anything in the URL and silently freeze the
|
|
9267
|
-
// literal.
|
|
9268
|
-
const threadedFieldPairs = isAncestorScoped
|
|
9269
|
-
? rawThreadedFields.map((tf) => ({
|
|
9270
|
-
valueField: tf,
|
|
9271
|
-
accessorField: tf.varName === itemVar
|
|
9272
|
-
? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
|
|
9273
|
-
: tf,
|
|
9274
|
-
}))
|
|
9275
|
-
: rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
|
|
9415
|
+
// Mirrors emitMultiStepExecuteHttp's identical rebind — see
|
|
9416
|
+
// buildThreadedFieldPairs's doc for why the literal-value search
|
|
9417
|
+
// and the rendered accessor deliberately diverge for a proven
|
|
9418
|
+
// ancestor-scoped rebind.
|
|
9419
|
+
const threadedFieldPairs = buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes);
|
|
9276
9420
|
// ONE guarded regex-alternation pass over `withBase` for every
|
|
9277
9421
|
// threaded field's value, longest first — see substituteThreadedValues's
|
|
9278
9422
|
// doc for why a per-field sequential `.replace()` loop here (the bug
|
|
@@ -9294,6 +9438,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9294
9438
|
: {
|
|
9295
9439
|
value: stringValue,
|
|
9296
9440
|
replacement: `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`,
|
|
9441
|
+
isItemBound: accessorField.varName === itemVar,
|
|
9297
9442
|
};
|
|
9298
9443
|
})
|
|
9299
9444
|
.filter((b) => b !== null);
|
|
@@ -9306,6 +9451,20 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9306
9451
|
const filteredValueBindings = isProvenInvariant
|
|
9307
9452
|
? valueBindings.filter((b) => isGenuineVaryingQueryValue(b.value, chainCapture, allCaptures))
|
|
9308
9453
|
: valueBindings;
|
|
9454
|
+
// Mirrors emitMultiStepExecuteHttp's identical ground-truth hoist
|
|
9455
|
+
// signal: derive `referencesItemVar` straight from the same
|
|
9456
|
+
// value bindings that feed the splice below, rather than relying
|
|
9457
|
+
// solely on the post-hoc itemVar word-boundary scan — a target's
|
|
9458
|
+
// OTHER join fields can independently prove isAncestorScoped and
|
|
9459
|
+
// rebind cleanly while THIS field's own rebind attempt still
|
|
9460
|
+
// falls back to `?? tf` (no structurally-corresponding ancestor
|
|
9461
|
+
// field exists for it). A binding excluded from
|
|
9462
|
+
// `filteredValueBindings` (unresolved, or dropped by the
|
|
9463
|
+
// zero-variance guard) never reaches the rendered URL, so it's
|
|
9464
|
+
// correctly excluded here too.
|
|
9465
|
+
if (filteredValueBindings.some((b) => b.isItemBound)) {
|
|
9466
|
+
referencesItemVar = true;
|
|
9467
|
+
}
|
|
9309
9468
|
const result = substituteThreadedValues(withBase, filteredValueBindings);
|
|
9310
9469
|
const withDrillParamBindings = applyDrillParamBindings(foldReturnSpec, chainCapture, result);
|
|
9311
9470
|
// See emitMultiStepExecuteHttp's identical guard: the frozen-
|