@apifuse/provider-sdk 2.2.0-beta.45 → 2.2.0-beta.47
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/CHANGELOG.md +8 -0
- package/bin/apifuse-submit-check.ts +239 -9
- package/dist/runtime/resolver-shared.d.ts +12 -1
- package/dist/runtime/resolver.js +2 -2
- package/dist/runtime/stealth.d.ts +2 -0
- package/dist/runtime/stealth.js +173 -17
- package/dist/server/serve-implementation.js +7 -3
- package/package.json +1 -1
- package/src/runtime/resolver-shared.ts +17 -1
- package/src/runtime/resolver.ts +7 -2
- package/src/runtime/stealth.ts +235 -21
- package/src/server/serve-implementation.ts +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.47
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 0ff0c6c90153af3aab75978eb73040c9adaa2609.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.46
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 0340fbc51b4ad71689bb1ec61b9b818dedda4587.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.45
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit 5d337d9fc1c77cd814ec7a4e1aa44ba0f8b2a5a9.
|
|
@@ -4312,6 +4312,7 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
|
|
|
4312
4312
|
const operations = Object.entries(provider.operations);
|
|
4313
4313
|
const missing: string[] = [];
|
|
4314
4314
|
const vacuous: string[] = [];
|
|
4315
|
+
const evidenceFreeScenario: string[] = [];
|
|
4315
4316
|
const placeholder: string[] = [];
|
|
4316
4317
|
const unsupported: string[] = [];
|
|
4317
4318
|
const generatedStarter: string[] = [];
|
|
@@ -4323,8 +4324,13 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
|
|
|
4323
4324
|
missing.push(operationId);
|
|
4324
4325
|
continue;
|
|
4325
4326
|
}
|
|
4326
|
-
if (hasCheck && !hasUnsupported
|
|
4327
|
-
|
|
4327
|
+
if (hasCheck && !hasUnsupported) {
|
|
4328
|
+
const vacuousClass = vacuousHealthCaseClass(operation.healthCheck);
|
|
4329
|
+
if (vacuousClass === "empty-assertions") {
|
|
4330
|
+
vacuous.push(operationId);
|
|
4331
|
+
} else if (vacuousClass === "evidence-free-scenario") {
|
|
4332
|
+
evidenceFreeScenario.push(operationId);
|
|
4333
|
+
}
|
|
4328
4334
|
}
|
|
4329
4335
|
if (hasUnsupported) {
|
|
4330
4336
|
const reason = operation.healthCheckUnsupported?.reason ?? "";
|
|
@@ -4351,14 +4357,36 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
|
|
|
4351
4357
|
);
|
|
4352
4358
|
}
|
|
4353
4359
|
|
|
4354
|
-
if (vacuous.length > 0) {
|
|
4360
|
+
if (vacuous.length > 0 || evidenceFreeScenario.length > 0) {
|
|
4361
|
+
const remediationParts: string[] = [];
|
|
4362
|
+
if (vacuous.length > 0) {
|
|
4363
|
+
remediationParts.push(
|
|
4364
|
+
`healthCheck.assertions for ${vacuous.join(", ")} is empty — assert on status and response shape (e.g. throw or return {status:'degraded'} when the upstream contract breaks), or declare healthCheckUnsupported with a specific reason if the operation genuinely cannot be probed.`,
|
|
4365
|
+
);
|
|
4366
|
+
}
|
|
4367
|
+
if (evidenceFreeScenario.length > 0) {
|
|
4368
|
+
remediationParts.push(
|
|
4369
|
+
`healthCheck scenario for ${evidenceFreeScenario.join(", ")} asserts nothing beyond transport — add an assert step whose expression reads the probe response (a field under data, an extracted value, or a quantifier over response rows), not just status_2xx or a bare response-envelope check.`,
|
|
4370
|
+
);
|
|
4371
|
+
}
|
|
4372
|
+
const message =
|
|
4373
|
+
evidenceFreeScenario.length === 0
|
|
4374
|
+
? "One or more operations have healthCheck cases with empty assertions."
|
|
4375
|
+
: vacuous.length === 0
|
|
4376
|
+
? "One or more operations have healthCheck scenarios without response evidence."
|
|
4377
|
+
: "One or more operations have healthCheck cases with empty assertions or evidence-free scenarios.";
|
|
4355
4378
|
return blocker(
|
|
4356
4379
|
"health-coverage",
|
|
4357
4380
|
"health",
|
|
4358
|
-
|
|
4359
|
-
|
|
4381
|
+
message,
|
|
4382
|
+
remediationParts.join(" "),
|
|
4360
4383
|
CATEGORY_MAX_POINTS.health,
|
|
4361
|
-
|
|
4384
|
+
[
|
|
4385
|
+
...vacuous.map((operationId) => `${operationId}: empty healthCheck.assertions`),
|
|
4386
|
+
...evidenceFreeScenario.map(
|
|
4387
|
+
(operationId) => `${operationId}: healthCheck scenario asserts nothing beyond transport`,
|
|
4388
|
+
),
|
|
4389
|
+
],
|
|
4362
4390
|
);
|
|
4363
4391
|
}
|
|
4364
4392
|
|
|
@@ -4413,14 +4441,216 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
|
|
|
4413
4441
|
);
|
|
4414
4442
|
}
|
|
4415
4443
|
|
|
4416
|
-
|
|
4444
|
+
type VacuousHealthCaseClass = "empty-assertions" | "evidence-free-scenario";
|
|
4445
|
+
|
|
4446
|
+
/**
|
|
4447
|
+
* Classify an operation whose every health case is vacuous. Returns undefined
|
|
4448
|
+
* when at least one case carries real coverage: an imperative case with a
|
|
4449
|
+
* non-vacuous assertions function, or a declarative case whose scenario reads
|
|
4450
|
+
* the probe response. When all cases are vacuous, the class picks the blocker
|
|
4451
|
+
* wording — "empty-assertions" whenever any vacuous case is imperative (the
|
|
4452
|
+
* pre-scenario behavior, kept byte-identical), "evidence-free-scenario" when
|
|
4453
|
+
* the suite is scenario-only.
|
|
4454
|
+
*/
|
|
4455
|
+
function vacuousHealthCaseClass(
|
|
4417
4456
|
healthCheck: ProviderDefinition["operations"][string]["healthCheck"],
|
|
4418
|
-
):
|
|
4457
|
+
): VacuousHealthCaseClass | undefined {
|
|
4419
4458
|
const cases = healthCheck?.cases;
|
|
4420
4459
|
if (!Array.isArray(cases) || cases.length === 0) {
|
|
4460
|
+
return "empty-assertions";
|
|
4461
|
+
}
|
|
4462
|
+
let sawImperativeCase = false;
|
|
4463
|
+
for (const healthCase of cases) {
|
|
4464
|
+
if (healthCase?.scenario !== undefined) {
|
|
4465
|
+
if (scenarioCarriesResponseEvidence(healthCase.scenario)) {
|
|
4466
|
+
return undefined;
|
|
4467
|
+
}
|
|
4468
|
+
continue;
|
|
4469
|
+
}
|
|
4470
|
+
sawImperativeCase = true;
|
|
4471
|
+
if (!isVacuousAssertionFunction(healthCase?.assertions)) {
|
|
4472
|
+
return undefined;
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
return sawImperativeCase ? "empty-assertions" : "evidence-free-scenario";
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
/**
|
|
4479
|
+
* A declarative case (`scenario`) carries real coverage when at least one
|
|
4480
|
+
* `assert` step reads the probe response beyond the transport envelope. The
|
|
4481
|
+
* SDK's own definitions drive this judgement:
|
|
4482
|
+
*
|
|
4483
|
+
* - The assert step is the scenario's only verdict-bearing construct
|
|
4484
|
+
* (`AssertResult.passed`). A guard can only attribute
|
|
4485
|
+
* degraded/expected_absence and stop the run (an escape hatch, not a health
|
|
4486
|
+
* verdict), and an operation step does not intrinsically fail on a non-2xx
|
|
4487
|
+
* response — that is what the explicit `status_2xx` predicate exists for. So
|
|
4488
|
+
* a scenario without a semantic assert step proves nothing beyond the
|
|
4489
|
+
* operation merely executing, which is the same emptiness the imperative
|
|
4490
|
+
* vacuity check flags.
|
|
4491
|
+
* - Leaves that cannot observe upstream content carry no evidence: predicates
|
|
4492
|
+
* whose operands read nothing produced by a step (pure literals,
|
|
4493
|
+
* attempt/credential metadata, and the `status_code`/`request_id`/`kind`
|
|
4494
|
+
* envelope fields of a step result — this is what makes the legacy transport
|
|
4495
|
+
* pin `status_2xx` on `["status_code"]` non-evidence), predicates comparing
|
|
4496
|
+
* a reference against an identical copy of itself (`equals(x, x)` holds for
|
|
4497
|
+
* any response), and positive-polarity presence/type pins (`exists`,
|
|
4498
|
+
* `type_is`, `not(not_exists(...))`, zero-bound `array_length_gte`) on the
|
|
4499
|
+
* bare `data` root — the declarative spellings of the legacy transport-only
|
|
4500
|
+
* assertion (`status === 200` + `typeof data === "object"`), satisfied by
|
|
4501
|
+
* any well-formed response body. Polarity matters: `not(exists(data))`
|
|
4502
|
+
* ("this probe must return no body") is falsified by ordinary responses, so
|
|
4503
|
+
* the carve-out only applies where the pin appears un-negated.
|
|
4504
|
+
* - Everything else counts: paths under `data`, extract-step results,
|
|
4505
|
+
* `duration_ms` (parity with imperative `ctx.durationMs` reads),
|
|
4506
|
+
* `status_2xx` applied to a body path (envelope-style upstreams answer HTTP
|
|
4507
|
+
* 200 and carry the real code in the body), and quantifiers, which iterate
|
|
4508
|
+
* response rows by construction.
|
|
4509
|
+
*
|
|
4510
|
+
* Fail-open contract (same as isVacuousAssertionFunction): the provider module
|
|
4511
|
+
* already validated this scenario against HealthScenarioSchema in its own SDK
|
|
4512
|
+
* at import time, so any shape this walk does not recognize (unknown step
|
|
4513
|
+
* kind, expression kind, operator, or reference namespace) indicates a newer
|
|
4514
|
+
* grammar, not an empty probe — treat it as evidence rather than flag it.
|
|
4515
|
+
* The inverse direction is accepted and documented: a determined author can
|
|
4516
|
+
* still fabricate a read that never fails (a decorative deep path inside a
|
|
4517
|
+
* disjunction, a degenerate bound). That effort is comparable to writing the
|
|
4518
|
+
* real one-line assertion — the same accepted limitation as the imperative
|
|
4519
|
+
* check's decorative-ctx-read — and the live `--smoke` probe remains the
|
|
4520
|
+
* runtime defense.
|
|
4521
|
+
*/
|
|
4522
|
+
export function scenarioCarriesResponseEvidence(scenario: unknown): boolean {
|
|
4523
|
+
if (!isRecord(scenario) || !Array.isArray(scenario.steps)) {
|
|
4524
|
+
return true;
|
|
4525
|
+
}
|
|
4526
|
+
let sawUnknownStepKind = false;
|
|
4527
|
+
for (const step of scenario.steps) {
|
|
4528
|
+
if (!isRecord(step)) {
|
|
4529
|
+
sawUnknownStepKind = true;
|
|
4530
|
+
continue;
|
|
4531
|
+
}
|
|
4532
|
+
if (step.kind === "assert") {
|
|
4533
|
+
if (expressionReadsResponse(step.expression)) {
|
|
4534
|
+
return true;
|
|
4535
|
+
}
|
|
4536
|
+
continue;
|
|
4537
|
+
}
|
|
4538
|
+
if (step.kind !== "operation" && step.kind !== "extract" && step.kind !== "guard") {
|
|
4539
|
+
sawUnknownStepKind = true;
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
return sawUnknownStepKind;
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
/**
|
|
4546
|
+
* Predicate operators of the current scenario grammar. An operator outside
|
|
4547
|
+
* this set indicates a newer grammar and fails open (counts as evidence),
|
|
4548
|
+
* because a future operator may source the response through fields this walk
|
|
4549
|
+
* does not know about.
|
|
4550
|
+
*/
|
|
4551
|
+
const KNOWN_SCENARIO_PREDICATE_OPERATORS: ReadonlySet<string> = new Set([
|
|
4552
|
+
"exists",
|
|
4553
|
+
"not_exists",
|
|
4554
|
+
"non_empty",
|
|
4555
|
+
"is_true",
|
|
4556
|
+
"equals",
|
|
4557
|
+
"not_equals",
|
|
4558
|
+
"contains",
|
|
4559
|
+
"matches",
|
|
4560
|
+
"number_gt",
|
|
4561
|
+
"number_gte",
|
|
4562
|
+
"number_lt",
|
|
4563
|
+
"number_lte",
|
|
4564
|
+
"array_length_eq",
|
|
4565
|
+
"array_length_gte",
|
|
4566
|
+
"array_length_lte",
|
|
4567
|
+
"status_2xx",
|
|
4568
|
+
"type_is",
|
|
4569
|
+
]);
|
|
4570
|
+
|
|
4571
|
+
function expressionReadsResponse(expression: unknown, negationDepth = 0): boolean {
|
|
4572
|
+
if (!isRecord(expression)) {
|
|
4421
4573
|
return true;
|
|
4422
4574
|
}
|
|
4423
|
-
|
|
4575
|
+
if (expression.kind === "all" || expression.kind === "any") {
|
|
4576
|
+
return Array.isArray(expression.clauses)
|
|
4577
|
+
? expression.clauses.some((clause) => expressionReadsResponse(clause, negationDepth))
|
|
4578
|
+
: true;
|
|
4579
|
+
}
|
|
4580
|
+
if (expression.kind === "not") {
|
|
4581
|
+
return expressionReadsResponse(expression.clause, negationDepth + 1);
|
|
4582
|
+
}
|
|
4583
|
+
if (expression.kind === "quantifier") {
|
|
4584
|
+
return true;
|
|
4585
|
+
}
|
|
4586
|
+
if (expression.kind !== "predicate" || typeof expression.operator !== "string") {
|
|
4587
|
+
return true;
|
|
4588
|
+
}
|
|
4589
|
+
const operator = expression.operator;
|
|
4590
|
+
if (!KNOWN_SCENARIO_PREDICATE_OPERATORS.has(operator)) {
|
|
4591
|
+
return true;
|
|
4592
|
+
}
|
|
4593
|
+
const actualReads = operandReadsStepOutput(expression.actual);
|
|
4594
|
+
const expectedReads = operandReadsStepOutput(expression.expected);
|
|
4595
|
+
if (!actualReads && !expectedReads) {
|
|
4596
|
+
return false;
|
|
4597
|
+
}
|
|
4598
|
+
// A predicate comparing a reference against an identical copy of itself
|
|
4599
|
+
// (equals(x, x) and friends) holds or fails identically for every response,
|
|
4600
|
+
// so the reads discriminate nothing.
|
|
4601
|
+
if (
|
|
4602
|
+
actualReads &&
|
|
4603
|
+
expectedReads &&
|
|
4604
|
+
JSON.stringify(expression.actual) === JSON.stringify(expression.expected)
|
|
4605
|
+
) {
|
|
4606
|
+
return false;
|
|
4607
|
+
}
|
|
4608
|
+
// Presence/type pins on the whole `data` root are satisfied by any
|
|
4609
|
+
// well-formed JSON body — but only in positive polarity. Under an odd
|
|
4610
|
+
// number of `not` wrappers the same pin means "this probe must return no
|
|
4611
|
+
// body / a different shape", which ordinary responses falsify, so it is a
|
|
4612
|
+
// real (if unusual) constraint and stays evidence.
|
|
4613
|
+
const positivePolarity = negationDepth % 2 === 0;
|
|
4614
|
+
const bareEnvelopePin = positivePolarity
|
|
4615
|
+
? operator === "exists" ||
|
|
4616
|
+
operator === "type_is" ||
|
|
4617
|
+
(operator === "array_length_gte" && expression.expected === 0)
|
|
4618
|
+
: operator === "not_exists";
|
|
4619
|
+
if (bareEnvelopePin && !expectedReads && isBareResponseEnvelope(expression.actual)) {
|
|
4620
|
+
return false;
|
|
4621
|
+
}
|
|
4622
|
+
return true;
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
function operandReadsStepOutput(operand: unknown): boolean {
|
|
4626
|
+
if (!isRecord(operand) || !isRecord(operand.ref)) {
|
|
4627
|
+
return false;
|
|
4628
|
+
}
|
|
4629
|
+
const reference = operand.ref;
|
|
4630
|
+
if (reference.namespace === "attempt" || reference.namespace === "credentials") {
|
|
4631
|
+
return false;
|
|
4632
|
+
}
|
|
4633
|
+
if (reference.namespace !== "steps") {
|
|
4634
|
+
return true;
|
|
4635
|
+
}
|
|
4636
|
+
if (!Array.isArray(reference.path) || reference.path.length === 0) {
|
|
4637
|
+
return true;
|
|
4638
|
+
}
|
|
4639
|
+
const head = reference.path[0];
|
|
4640
|
+
return head !== "status_code" && head !== "request_id" && head !== "kind";
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
function isBareResponseEnvelope(operand: unknown): boolean {
|
|
4644
|
+
if (!isRecord(operand) || !isRecord(operand.ref)) {
|
|
4645
|
+
return false;
|
|
4646
|
+
}
|
|
4647
|
+
const reference = operand.ref;
|
|
4648
|
+
return (
|
|
4649
|
+
reference.namespace === "steps" &&
|
|
4650
|
+
Array.isArray(reference.path) &&
|
|
4651
|
+
reference.path.length === 1 &&
|
|
4652
|
+
reference.path[0] === "data"
|
|
4653
|
+
);
|
|
4424
4654
|
}
|
|
4425
4655
|
|
|
4426
4656
|
function isVacuousAssertionFunction(assertions: unknown): boolean {
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
import type { ResolverContext } from "../types.js";
|
|
1
|
+
import type { ChallengeSolution, ProviderChallenge, ResolverContext } from "../types.js";
|
|
2
|
+
import type { TraceRecorder } from "./trace.js";
|
|
2
3
|
export declare const RESOLVER_INSTRUMENTATION_METADATA: unique symbol;
|
|
4
|
+
/**
|
|
5
|
+
* Internal solve surface. The instrumentation layer threads its recorder as a
|
|
6
|
+
* third argument so vendor-level spans (`resolver.vendor.*`) attach to the
|
|
7
|
+
* active trace. Every wrapper that re-exposes `solve` must forward the extra
|
|
8
|
+
* arguments; a wrapper typed against the public two-argument `ResolverContext`
|
|
9
|
+
* silently drops the recorder and vendor spans vanish.
|
|
10
|
+
*/
|
|
11
|
+
export type ResolverSolveWithRecorder = {
|
|
12
|
+
solve(challenge: ProviderChallenge, signal?: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
|
|
13
|
+
};
|
|
3
14
|
export declare function createUnsupportedResolverClient(reason?: string): ResolverContext;
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -682,8 +682,8 @@ export function bindResolverSignal(resolver, defaultSignal) {
|
|
|
682
682
|
if (!defaultSignal)
|
|
683
683
|
return resolver;
|
|
684
684
|
const boundResolver = {
|
|
685
|
-
solve(challenge, signal = defaultSignal) {
|
|
686
|
-
return resolver.solve(challenge, signal);
|
|
685
|
+
solve(challenge, signal = defaultSignal, traceRecorder) {
|
|
686
|
+
return resolver.solve(challenge, signal, traceRecorder);
|
|
687
687
|
},
|
|
688
688
|
};
|
|
689
689
|
if (resolverCaches.has(resolver)) {
|
|
@@ -4,6 +4,8 @@ import type { StealthClient, StealthResponse } from "../types.js";
|
|
|
4
4
|
export declare const DEFAULT_PROFILE = "chrome-desktop";
|
|
5
5
|
export type StealthClientOptions = ProxyResolutionOptions & {
|
|
6
6
|
warn?: (message: string) => void;
|
|
7
|
+
/** Abort all requests issued by this client. */
|
|
8
|
+
signal?: AbortSignal;
|
|
7
9
|
/**
|
|
8
10
|
* Proxy-only stealth transport overrides. Use only for upstream proxy products
|
|
9
11
|
* that terminate CONNECT with a private CA instead of tunneling the origin
|
package/dist/runtime/stealth.js
CHANGED
|
@@ -206,11 +206,14 @@ function splitCombinedSetCookieHeader(headerValue) {
|
|
|
206
206
|
return cookieStrings;
|
|
207
207
|
}
|
|
208
208
|
export async function normalizeResponse(response, requestUrl, maxBodyBytes) {
|
|
209
|
+
return normalizeResponseWithSignal(response, requestUrl, maxBodyBytes);
|
|
210
|
+
}
|
|
211
|
+
async function normalizeResponseWithSignal(response, requestUrl, maxBodyBytes, signal) {
|
|
209
212
|
const headers = Object.fromEntries(response.headers.entries());
|
|
210
213
|
const cookies = new StealthCookieJar(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
|
|
211
214
|
const bodyBytes = maxBodyBytes === undefined
|
|
212
|
-
? await response
|
|
213
|
-
: await readResponseBodyWithLimit(response, maxBodyBytes);
|
|
215
|
+
? await readResponseArrayBuffer(response, signal)
|
|
216
|
+
: await readResponseBodyWithLimit(response, maxBodyBytes, signal);
|
|
214
217
|
const body = new TextDecoder().decode(bodyBytes);
|
|
215
218
|
return {
|
|
216
219
|
status: response.status,
|
|
@@ -236,6 +239,40 @@ export async function normalizeResponse(response, requestUrl, maxBodyBytes) {
|
|
|
236
239
|
},
|
|
237
240
|
};
|
|
238
241
|
}
|
|
242
|
+
async function readResponseArrayBuffer(response, signal) {
|
|
243
|
+
if (!signal)
|
|
244
|
+
return response.arrayBuffer();
|
|
245
|
+
throwIfAmbientAborted(signal);
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
let settled = false;
|
|
248
|
+
const settle = (operation) => {
|
|
249
|
+
if (settled)
|
|
250
|
+
return;
|
|
251
|
+
settled = true;
|
|
252
|
+
signal.removeEventListener("abort", onAbort);
|
|
253
|
+
operation();
|
|
254
|
+
};
|
|
255
|
+
const onAbort = () => {
|
|
256
|
+
const error = toAmbientCancellationError(signal);
|
|
257
|
+
try {
|
|
258
|
+
void response.body?.cancel().catch(() => undefined);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Preserve the cancellation error if accessing or cancelling the body fails.
|
|
262
|
+
}
|
|
263
|
+
settle(() => reject(error));
|
|
264
|
+
};
|
|
265
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
266
|
+
try {
|
|
267
|
+
void response.arrayBuffer().then((body) => settle(() => resolve(body)), (error) => settle(() => reject(error)));
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
settle(() => reject(error));
|
|
271
|
+
}
|
|
272
|
+
if (signal.aborted)
|
|
273
|
+
onAbort();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
239
276
|
function responseTooLargeError(maxBodyBytes, observedBytes) {
|
|
240
277
|
return new TransportError(`Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`, {
|
|
241
278
|
code: "response_too_large",
|
|
@@ -251,7 +288,8 @@ function declaredContentLength(headers) {
|
|
|
251
288
|
const parsed = Number(contentLength);
|
|
252
289
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
253
290
|
}
|
|
254
|
-
async function readResponseBodyWithLimit(response, maxBodyBytes) {
|
|
291
|
+
async function readResponseBodyWithLimit(response, maxBodyBytes, signal) {
|
|
292
|
+
throwIfAmbientAborted(signal);
|
|
255
293
|
const contentLength = declaredContentLength(response.headers);
|
|
256
294
|
if (contentLength !== undefined && contentLength > maxBodyBytes) {
|
|
257
295
|
await response.body?.cancel().catch(() => undefined);
|
|
@@ -269,7 +307,7 @@ async function readResponseBodyWithLimit(response, maxBodyBytes) {
|
|
|
269
307
|
let receivedBytes = 0;
|
|
270
308
|
try {
|
|
271
309
|
while (true) {
|
|
272
|
-
const { done, value } = await reader
|
|
310
|
+
const { done, value } = await readResponseBodyChunk(reader, signal);
|
|
273
311
|
if (done)
|
|
274
312
|
break;
|
|
275
313
|
if (!value)
|
|
@@ -285,6 +323,9 @@ async function readResponseBodyWithLimit(response, maxBodyBytes) {
|
|
|
285
323
|
finally {
|
|
286
324
|
reader.releaseLock();
|
|
287
325
|
}
|
|
326
|
+
return concatenateResponseBodyChunks(chunks, receivedBytes);
|
|
327
|
+
}
|
|
328
|
+
function concatenateResponseBodyChunks(chunks, receivedBytes) {
|
|
288
329
|
const bodyBytes = new Uint8Array(receivedBytes);
|
|
289
330
|
let offset = 0;
|
|
290
331
|
for (const chunk of chunks) {
|
|
@@ -293,6 +334,30 @@ async function readResponseBodyWithLimit(response, maxBodyBytes) {
|
|
|
293
334
|
}
|
|
294
335
|
return bodyBytes.buffer;
|
|
295
336
|
}
|
|
337
|
+
function readResponseBodyChunk(reader, signal) {
|
|
338
|
+
if (!signal)
|
|
339
|
+
return reader.read();
|
|
340
|
+
throwIfAmbientAborted(signal);
|
|
341
|
+
return new Promise((resolve, reject) => {
|
|
342
|
+
let settled = false;
|
|
343
|
+
const settle = (operation) => {
|
|
344
|
+
if (settled)
|
|
345
|
+
return;
|
|
346
|
+
settled = true;
|
|
347
|
+
signal.removeEventListener("abort", onAbort);
|
|
348
|
+
operation();
|
|
349
|
+
};
|
|
350
|
+
const onAbort = () => {
|
|
351
|
+
const error = toAmbientCancellationError(signal);
|
|
352
|
+
void reader.cancel().catch(() => undefined);
|
|
353
|
+
settle(() => reject(error));
|
|
354
|
+
};
|
|
355
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
356
|
+
void reader.read().then((chunk) => settle(() => resolve(chunk)), (error) => settle(() => reject(error)));
|
|
357
|
+
if (signal.aborted)
|
|
358
|
+
onAbort();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
296
361
|
function normalizeBody(body) {
|
|
297
362
|
if (body === undefined) {
|
|
298
363
|
return "";
|
|
@@ -425,8 +490,38 @@ function normalizeStealthTransportError(error) {
|
|
|
425
490
|
cause: error instanceof Error ? error : undefined,
|
|
426
491
|
});
|
|
427
492
|
}
|
|
428
|
-
function
|
|
429
|
-
|
|
493
|
+
function toAmbientCancellationError(signal, error = signal.reason) {
|
|
494
|
+
if (error instanceof TransportError && error.code === "transport_cancelled") {
|
|
495
|
+
return error;
|
|
496
|
+
}
|
|
497
|
+
return new TransportError("Request cancelled", {
|
|
498
|
+
code: "transport_cancelled",
|
|
499
|
+
status: 0,
|
|
500
|
+
retryable: false,
|
|
501
|
+
...(error !== undefined
|
|
502
|
+
? { cause: error instanceof Error ? error : new Error(String(error)) }
|
|
503
|
+
: {}),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function throwIfAmbientAborted(signal) {
|
|
507
|
+
if (signal?.aborted)
|
|
508
|
+
throw toAmbientCancellationError(signal);
|
|
509
|
+
}
|
|
510
|
+
function sleep(ms, signal) {
|
|
511
|
+
if (!signal)
|
|
512
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
513
|
+
throwIfAmbientAborted(signal);
|
|
514
|
+
return new Promise((resolve, reject) => {
|
|
515
|
+
const onAbort = () => {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
reject(toAmbientCancellationError(signal));
|
|
518
|
+
};
|
|
519
|
+
const timer = setTimeout(() => {
|
|
520
|
+
signal.removeEventListener("abort", onAbort);
|
|
521
|
+
resolve();
|
|
522
|
+
}, ms);
|
|
523
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
524
|
+
});
|
|
430
525
|
}
|
|
431
526
|
function normalizeMethod(method) {
|
|
432
527
|
switch (method.toUpperCase()) {
|
|
@@ -480,7 +575,7 @@ function discardStealthRedirectBody(response) {
|
|
|
480
575
|
// failure must not replace or delay that decision.
|
|
481
576
|
}
|
|
482
577
|
}
|
|
483
|
-
async function fetchStealthRedirectChain(transport, cookieJar, requestUrl, method, options) {
|
|
578
|
+
async function fetchStealthRedirectChain(transport, cookieJar, requestUrl, method, options, signal) {
|
|
484
579
|
let currentUrl = requestUrl;
|
|
485
580
|
let currentMethod = method;
|
|
486
581
|
let currentBody = options.body === undefined ? undefined : normalizeBody(options.body);
|
|
@@ -489,6 +584,7 @@ async function fetchStealthRedirectChain(transport, cookieJar, requestUrl, metho
|
|
|
489
584
|
let response;
|
|
490
585
|
const deadline = options.timeout ? performance.now() + options.timeout : undefined;
|
|
491
586
|
while (true) {
|
|
587
|
+
throwIfAmbientAborted(signal);
|
|
492
588
|
const headers = { ...currentHeaders };
|
|
493
589
|
if (!hasHeader(headers, "Cookie")) {
|
|
494
590
|
const cookieHeader = cookieJar.toHeader(currentUrl);
|
|
@@ -499,10 +595,12 @@ async function fetchStealthRedirectChain(transport, cookieJar, requestUrl, metho
|
|
|
499
595
|
headers: normalizeHeaders(headers),
|
|
500
596
|
method: currentMethod,
|
|
501
597
|
redirect: "manual",
|
|
598
|
+
...(signal ? { signal } : {}),
|
|
502
599
|
};
|
|
503
600
|
if (currentBody !== undefined)
|
|
504
601
|
requestInit.body = currentBody;
|
|
505
602
|
await transport.clearCookies();
|
|
603
|
+
throwIfAmbientAborted(signal);
|
|
506
604
|
const remainingTimeout = deadline === undefined ? undefined : Math.ceil(deadline - performance.now());
|
|
507
605
|
if (remainingTimeout !== undefined && remainingTimeout <= 0) {
|
|
508
606
|
throw new TransportError("Request timed out", {
|
|
@@ -513,6 +611,10 @@ async function fetchStealthRedirectChain(transport, cookieJar, requestUrl, metho
|
|
|
513
611
|
if (remainingTimeout !== undefined)
|
|
514
612
|
requestInit.timeout = remainingTimeout;
|
|
515
613
|
response = await transport.fetch(currentUrl, requestInit);
|
|
614
|
+
if (signal?.aborted) {
|
|
615
|
+
discardStealthRedirectBody(response);
|
|
616
|
+
throw toAmbientCancellationError(signal);
|
|
617
|
+
}
|
|
516
618
|
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers), response.url ?? currentUrl);
|
|
517
619
|
if (!isRedirectStatus(response.status) || options.redirect === "manual")
|
|
518
620
|
break;
|
|
@@ -541,7 +643,7 @@ async function fetchStealthRedirectChain(transport, cookieJar, requestUrl, metho
|
|
|
541
643
|
currentUrl = nextUrl;
|
|
542
644
|
followedHops += 1;
|
|
543
645
|
}
|
|
544
|
-
const normalized = await
|
|
646
|
+
const normalized = await normalizeResponseWithSignal(response, currentUrl, options.maxBodyBytes, signal);
|
|
545
647
|
if (followedHops > 0)
|
|
546
648
|
normalized.redirected = true;
|
|
547
649
|
return { normalized, response };
|
|
@@ -579,20 +681,56 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
579
681
|
}
|
|
580
682
|
return entry;
|
|
581
683
|
}
|
|
582
|
-
async function withClient(profileName, proxyUrl, ignoreTlsErrors, operation) {
|
|
684
|
+
async function withClient(profileName, proxyUrl, ignoreTlsErrors, operation, signal) {
|
|
685
|
+
throwIfAmbientAborted(signal);
|
|
583
686
|
const entry = await getClientEntry(profileName, proxyUrl, ignoreTlsErrors);
|
|
687
|
+
throwIfAmbientAborted(signal);
|
|
584
688
|
const previous = entry.tail;
|
|
585
689
|
let release;
|
|
586
690
|
entry.tail = new Promise((resolve) => {
|
|
587
691
|
release = resolve;
|
|
588
692
|
});
|
|
589
|
-
|
|
693
|
+
let acquired = false;
|
|
590
694
|
try {
|
|
591
|
-
|
|
695
|
+
await waitForClientTurn(previous, signal);
|
|
696
|
+
acquired = true;
|
|
697
|
+
throwIfAmbientAborted(signal);
|
|
698
|
+
const client = await entry.session;
|
|
699
|
+
throwIfAmbientAborted(signal);
|
|
700
|
+
const result = await operation(client);
|
|
701
|
+
throwIfAmbientAborted(signal);
|
|
702
|
+
return result;
|
|
592
703
|
}
|
|
593
704
|
finally {
|
|
594
|
-
|
|
705
|
+
if (acquired) {
|
|
706
|
+
release();
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
void previous.then(release, release);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async function waitForClientTurn(previous, signal) {
|
|
714
|
+
if (!signal) {
|
|
715
|
+
await previous;
|
|
716
|
+
return;
|
|
595
717
|
}
|
|
718
|
+
throwIfAmbientAborted(signal);
|
|
719
|
+
await new Promise((resolve, reject) => {
|
|
720
|
+
let settled = false;
|
|
721
|
+
const settle = (operation) => {
|
|
722
|
+
if (settled)
|
|
723
|
+
return;
|
|
724
|
+
settled = true;
|
|
725
|
+
signal.removeEventListener("abort", onAbort);
|
|
726
|
+
operation();
|
|
727
|
+
};
|
|
728
|
+
const onAbort = () => settle(() => reject(toAmbientCancellationError(signal)));
|
|
729
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
730
|
+
void previous.then(() => settle(resolve), (error) => settle(() => reject(error)));
|
|
731
|
+
if (signal.aborted)
|
|
732
|
+
onAbort();
|
|
733
|
+
});
|
|
596
734
|
}
|
|
597
735
|
async function resolveRequestProxy(options, proxyAttempt, refreshEpoch) {
|
|
598
736
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
@@ -646,6 +784,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
646
784
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
647
785
|
}
|
|
648
786
|
})();
|
|
787
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
649
788
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
650
789
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
651
790
|
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
@@ -675,6 +814,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
675
814
|
let stalePoolDiagnosticProxy;
|
|
676
815
|
const attemptedProxies = new Set();
|
|
677
816
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
817
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
678
818
|
let proxy;
|
|
679
819
|
let attemptProxy;
|
|
680
820
|
// Reuse the exact serialization used by this outbound attempt in its catch path.
|
|
@@ -702,6 +842,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
702
842
|
});
|
|
703
843
|
};
|
|
704
844
|
try {
|
|
845
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
705
846
|
const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
|
|
706
847
|
const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
|
|
707
848
|
fallbackSensitiveValues = [
|
|
@@ -714,6 +855,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
714
855
|
fallbackRedactedUrl = structural.redactedUrl;
|
|
715
856
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
716
857
|
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
858
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
717
859
|
proxy = attemptProxy.url;
|
|
718
860
|
if (proxy && dedupeAllocatorEndpoints) {
|
|
719
861
|
// An under-filled allocation repeats endpoints (via the modulo
|
|
@@ -731,7 +873,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
731
873
|
const profileName = options.profile ?? defaultProfile;
|
|
732
874
|
serializedUrl = serializeRequestUrl(resolveUrl(baseUrl, url), options.params, sensitiveParams);
|
|
733
875
|
const { requestUrl } = serializedUrl;
|
|
734
|
-
const { normalized, response } = await withClient(profileName, proxy, ignoreTlsErrors, (transport) => fetchStealthRedirectChain(transport, cookieJar, requestUrl, method, options));
|
|
876
|
+
const { normalized, response } = await withClient(profileName, proxy, ignoreTlsErrors, (transport) => fetchStealthRedirectChain(transport, cookieJar, requestUrl, method, options, clientOptions.signal), clientOptions.signal);
|
|
735
877
|
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
736
878
|
throw createProxyConnectFailureError(normalized.body);
|
|
737
879
|
}
|
|
@@ -766,10 +908,16 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
766
908
|
const sensitiveValues = serializedUrl?.sensitiveValues ?? fallbackSensitiveValues;
|
|
767
909
|
let normalizedError;
|
|
768
910
|
try {
|
|
911
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
769
912
|
normalizedError = normalizeStealthTransportError(error);
|
|
770
913
|
}
|
|
771
914
|
catch (normalizationError) {
|
|
772
|
-
|
|
915
|
+
const redactedNormalizationError = redactSensitiveError(normalizationError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
|
|
916
|
+
if (normalizationError instanceof TransportError &&
|
|
917
|
+
normalizationError.code === "transport_cancelled") {
|
|
918
|
+
recordProxyAttempt("error", proxyAttemptErrorCode(normalizationError), proxyAttemptStatus(normalizationError));
|
|
919
|
+
}
|
|
920
|
+
throw redactedNormalizationError;
|
|
773
921
|
}
|
|
774
922
|
const retryErrorCode = proxyAttemptErrorCode(normalizedError);
|
|
775
923
|
const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
|
|
@@ -816,8 +964,9 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
816
964
|
proxyUsed: Boolean(proxy),
|
|
817
965
|
})) {
|
|
818
966
|
if (stealthRetryOptions) {
|
|
819
|
-
await sleep(computeProxyTransportRetryDelayMs(stealthRetryOptions, attempt + 1));
|
|
967
|
+
await sleep(computeProxyTransportRetryDelayMs(stealthRetryOptions, attempt + 1), clientOptions.signal);
|
|
820
968
|
}
|
|
969
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
821
970
|
continue;
|
|
822
971
|
}
|
|
823
972
|
throw normalizedError;
|
|
@@ -826,11 +975,13 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
826
975
|
if (rotatesRegistryChain &&
|
|
827
976
|
stalePoolError &&
|
|
828
977
|
refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES) {
|
|
978
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
829
979
|
await invalidateProxyResolutionCacheAsync({
|
|
830
980
|
proxyPolicy: clientOptions.proxyPolicy,
|
|
831
981
|
upstream: clientOptions.upstream,
|
|
832
982
|
affinityKey: clientOptions.affinityKey,
|
|
833
983
|
});
|
|
984
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
834
985
|
continue;
|
|
835
986
|
}
|
|
836
987
|
const proxyAuthDiagnostic = stalePoolError && stalePoolDiagnosticProxy
|
|
@@ -851,6 +1002,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
851
1002
|
}
|
|
852
1003
|
break;
|
|
853
1004
|
}
|
|
1005
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
854
1006
|
throw normalizeStealthTransportError(lastError);
|
|
855
1007
|
},
|
|
856
1008
|
cookies: cookieJar,
|
|
@@ -1043,16 +1195,20 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
1043
1195
|
async function classifyProxyAuthDiagnostic(profileName, proxy) {
|
|
1044
1196
|
try {
|
|
1045
1197
|
return await withClient(profileName, proxy, false, async (client) => {
|
|
1198
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1046
1199
|
await client.clearCookies();
|
|
1200
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1047
1201
|
const response = await client.fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
|
|
1048
1202
|
method: "GET",
|
|
1049
1203
|
timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
|
|
1204
|
+
...(clientOptions.signal ? { signal: clientOptions.signal } : {}),
|
|
1050
1205
|
});
|
|
1051
|
-
const normalized = await
|
|
1206
|
+
const normalized = await normalizeResponseWithSignal(response, undefined, undefined, clientOptions.signal);
|
|
1052
1207
|
return classifyProxyAuthDiagnosticMessage(normalized.body);
|
|
1053
|
-
});
|
|
1208
|
+
}, clientOptions.signal);
|
|
1054
1209
|
}
|
|
1055
1210
|
catch (error) {
|
|
1211
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1056
1212
|
const message = error instanceof Error
|
|
1057
1213
|
? [error.message, error.cause instanceof Error ? error.cause.message : ""]
|
|
1058
1214
|
.filter(Boolean)
|
|
@@ -24,7 +24,7 @@ import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
|
24
24
|
import { createOcrClientFromEnv } from "../runtime/ocr.js";
|
|
25
25
|
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
|
|
26
26
|
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector, } from "../runtime/proxy-telemetry.js";
|
|
27
|
-
import { createUnsupportedResolverClient } from "../runtime/resolver-shared.js";
|
|
27
|
+
import { createUnsupportedResolverClient, } from "../runtime/resolver-shared.js";
|
|
28
28
|
import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
|
|
29
29
|
import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
|
|
30
30
|
import { StealthCookieJar } from "../runtime/stealth-cookies.js";
|
|
@@ -291,8 +291,10 @@ function bindResolverSignalWithoutRuntime(resolver, defaultSignal) {
|
|
|
291
291
|
if (!defaultSignal)
|
|
292
292
|
return resolver;
|
|
293
293
|
return {
|
|
294
|
-
solve(challenge, signal = defaultSignal) {
|
|
295
|
-
|
|
294
|
+
solve(challenge, signal = defaultSignal, traceRecorder) {
|
|
295
|
+
// Forward the instrumentation trace recorder so resolver.vendor.* spans
|
|
296
|
+
// survive this wrapper. See ResolverSolveWithRecorder in resolver-shared.ts.
|
|
297
|
+
return resolver.solve(challenge, signal, traceRecorder);
|
|
296
298
|
},
|
|
297
299
|
};
|
|
298
300
|
}
|
|
@@ -409,6 +411,7 @@ function createProviderContext(provider, request, operationId, options, state =
|
|
|
409
411
|
upstream: proxyClientOptions.upstream,
|
|
410
412
|
affinityKey: proxyClientOptions.affinityKey,
|
|
411
413
|
telemetry: proxyTelemetry,
|
|
414
|
+
...(signal ? { signal } : {}),
|
|
412
415
|
};
|
|
413
416
|
const { capabilityModules } = options;
|
|
414
417
|
const logStealthCleanupError = (error) => logProviderCleanupError(options.logger, provider, "operation", operationId, request.requestId, "stealth", error);
|
|
@@ -566,6 +569,7 @@ function createAuthFlowContext(provider, request, options, state, proxyTelemetry
|
|
|
566
569
|
upstream: proxyClientOptions.upstream,
|
|
567
570
|
affinityKey: proxyClientOptions.affinityKey,
|
|
568
571
|
telemetry: proxyTelemetry,
|
|
572
|
+
...(signal ? { signal } : {}),
|
|
569
573
|
};
|
|
570
574
|
const { capabilityModules } = options;
|
|
571
575
|
const logStealthCleanupError = (error) => logProviderCleanupError(options.logger, provider, "auth", "flow", request.requestId, "stealth", error);
|
package/package.json
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { ProviderError } from "../errors.js";
|
|
2
|
-
import type { ResolverContext } from "../types.js";
|
|
2
|
+
import type { ChallengeSolution, ProviderChallenge, ResolverContext } from "../types.js";
|
|
3
|
+
import type { TraceRecorder } from "./trace.js";
|
|
3
4
|
|
|
4
5
|
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
|
|
5
6
|
"@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
|
|
6
7
|
);
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Internal solve surface. The instrumentation layer threads its recorder as a
|
|
11
|
+
* third argument so vendor-level spans (`resolver.vendor.*`) attach to the
|
|
12
|
+
* active trace. Every wrapper that re-exposes `solve` must forward the extra
|
|
13
|
+
* arguments; a wrapper typed against the public two-argument `ResolverContext`
|
|
14
|
+
* silently drops the recorder and vendor spans vanish.
|
|
15
|
+
*/
|
|
16
|
+
export type ResolverSolveWithRecorder = {
|
|
17
|
+
solve(
|
|
18
|
+
challenge: ProviderChallenge,
|
|
19
|
+
signal?: AbortSignal,
|
|
20
|
+
traceRecorder?: TraceRecorder,
|
|
21
|
+
): Promise<ChallengeSolution>;
|
|
22
|
+
};
|
|
23
|
+
|
|
8
24
|
export function createUnsupportedResolverClient(reason?: string): ResolverContext {
|
|
9
25
|
return {
|
|
10
26
|
async solve() {
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
import {
|
|
42
42
|
createUnsupportedResolverClient,
|
|
43
43
|
RESOLVER_INSTRUMENTATION_METADATA,
|
|
44
|
+
type ResolverSolveWithRecorder,
|
|
44
45
|
} from "./resolver-shared.js";
|
|
45
46
|
import {
|
|
46
47
|
APIFUSE__CDP_POOL__URL,
|
|
@@ -1022,8 +1023,12 @@ export function bindResolverSignal(
|
|
|
1022
1023
|
): ResolverContext {
|
|
1023
1024
|
if (!defaultSignal) return resolver;
|
|
1024
1025
|
const boundResolver: ResolverContext = {
|
|
1025
|
-
solve(challenge, signal = defaultSignal) {
|
|
1026
|
-
return resolver.solve(
|
|
1026
|
+
solve(challenge, signal = defaultSignal, traceRecorder?: TraceRecorder) {
|
|
1027
|
+
return (resolver as Partial<ResolverSolveWithRecorder> & ResolverContext).solve(
|
|
1028
|
+
challenge,
|
|
1029
|
+
signal,
|
|
1030
|
+
traceRecorder,
|
|
1031
|
+
);
|
|
1027
1032
|
},
|
|
1028
1033
|
};
|
|
1029
1034
|
if (resolverCaches.has(resolver)) {
|
package/src/runtime/stealth.ts
CHANGED
|
@@ -101,6 +101,8 @@ function sensitiveQueryParamNames(url: string): string[] {
|
|
|
101
101
|
|
|
102
102
|
export type StealthClientOptions = ProxyResolutionOptions & {
|
|
103
103
|
warn?: (message: string) => void;
|
|
104
|
+
/** Abort all requests issued by this client. */
|
|
105
|
+
signal?: AbortSignal;
|
|
104
106
|
/**
|
|
105
107
|
* Proxy-only stealth transport overrides. Use only for upstream proxy products
|
|
106
108
|
* that terminate CONNECT with a private CA instead of tunneling the origin
|
|
@@ -360,6 +362,15 @@ export async function normalizeResponse(
|
|
|
360
362
|
response: StealthTransportResponse,
|
|
361
363
|
requestUrl?: string,
|
|
362
364
|
maxBodyBytes?: number,
|
|
365
|
+
): Promise<StealthResponse> {
|
|
366
|
+
return normalizeResponseWithSignal(response, requestUrl, maxBodyBytes);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function normalizeResponseWithSignal(
|
|
370
|
+
response: StealthTransportResponse,
|
|
371
|
+
requestUrl?: string,
|
|
372
|
+
maxBodyBytes?: number,
|
|
373
|
+
signal?: AbortSignal,
|
|
363
374
|
): Promise<StealthResponse> {
|
|
364
375
|
const headers = Object.fromEntries(response.headers.entries());
|
|
365
376
|
const cookies = new StealthCookieJar(
|
|
@@ -368,8 +379,8 @@ export async function normalizeResponse(
|
|
|
368
379
|
);
|
|
369
380
|
const bodyBytes =
|
|
370
381
|
maxBodyBytes === undefined
|
|
371
|
-
? await response
|
|
372
|
-
: await readResponseBodyWithLimit(response, maxBodyBytes);
|
|
382
|
+
? await readResponseArrayBuffer(response, signal)
|
|
383
|
+
: await readResponseBodyWithLimit(response, maxBodyBytes, signal);
|
|
373
384
|
const body = new TextDecoder().decode(bodyBytes);
|
|
374
385
|
|
|
375
386
|
return {
|
|
@@ -397,6 +408,42 @@ export async function normalizeResponse(
|
|
|
397
408
|
};
|
|
398
409
|
}
|
|
399
410
|
|
|
411
|
+
async function readResponseArrayBuffer(
|
|
412
|
+
response: StealthTransportResponse,
|
|
413
|
+
signal?: AbortSignal,
|
|
414
|
+
): Promise<ArrayBuffer> {
|
|
415
|
+
if (!signal) return response.arrayBuffer();
|
|
416
|
+
throwIfAmbientAborted(signal);
|
|
417
|
+
return new Promise((resolve, reject) => {
|
|
418
|
+
let settled = false;
|
|
419
|
+
const settle = (operation: () => void) => {
|
|
420
|
+
if (settled) return;
|
|
421
|
+
settled = true;
|
|
422
|
+
signal.removeEventListener("abort", onAbort);
|
|
423
|
+
operation();
|
|
424
|
+
};
|
|
425
|
+
const onAbort = () => {
|
|
426
|
+
const error = toAmbientCancellationError(signal);
|
|
427
|
+
try {
|
|
428
|
+
void response.body?.cancel().catch(() => undefined);
|
|
429
|
+
} catch {
|
|
430
|
+
// Preserve the cancellation error if accessing or cancelling the body fails.
|
|
431
|
+
}
|
|
432
|
+
settle(() => reject(error));
|
|
433
|
+
};
|
|
434
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
435
|
+
try {
|
|
436
|
+
void response.arrayBuffer().then(
|
|
437
|
+
(body) => settle(() => resolve(body)),
|
|
438
|
+
(error) => settle(() => reject(error)),
|
|
439
|
+
);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
settle(() => reject(error));
|
|
442
|
+
}
|
|
443
|
+
if (signal.aborted) onAbort();
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
400
447
|
function responseTooLargeError(maxBodyBytes: number, observedBytes: number): TransportError {
|
|
401
448
|
return new TransportError(
|
|
402
449
|
`Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`,
|
|
@@ -419,7 +466,9 @@ function declaredContentLength(headers: StealthTransportHeaders): number | undef
|
|
|
419
466
|
async function readResponseBodyWithLimit(
|
|
420
467
|
response: StealthTransportResponse,
|
|
421
468
|
maxBodyBytes: number,
|
|
469
|
+
signal?: AbortSignal,
|
|
422
470
|
): Promise<ArrayBuffer> {
|
|
471
|
+
throwIfAmbientAborted(signal);
|
|
423
472
|
const contentLength = declaredContentLength(response.headers);
|
|
424
473
|
if (contentLength !== undefined && contentLength > maxBodyBytes) {
|
|
425
474
|
await response.body?.cancel().catch(() => undefined);
|
|
@@ -439,7 +488,7 @@ async function readResponseBodyWithLimit(
|
|
|
439
488
|
let receivedBytes = 0;
|
|
440
489
|
try {
|
|
441
490
|
while (true) {
|
|
442
|
-
const { done, value } = await reader
|
|
491
|
+
const { done, value } = await readResponseBodyChunk(reader, signal);
|
|
443
492
|
if (done) break;
|
|
444
493
|
if (!value) continue;
|
|
445
494
|
receivedBytes += value.byteLength;
|
|
@@ -453,6 +502,13 @@ async function readResponseBodyWithLimit(
|
|
|
453
502
|
reader.releaseLock();
|
|
454
503
|
}
|
|
455
504
|
|
|
505
|
+
return concatenateResponseBodyChunks(chunks, receivedBytes);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function concatenateResponseBodyChunks(
|
|
509
|
+
chunks: readonly Uint8Array[],
|
|
510
|
+
receivedBytes: number,
|
|
511
|
+
): ArrayBuffer {
|
|
456
512
|
const bodyBytes = new Uint8Array(receivedBytes);
|
|
457
513
|
let offset = 0;
|
|
458
514
|
for (const chunk of chunks) {
|
|
@@ -462,6 +518,34 @@ async function readResponseBodyWithLimit(
|
|
|
462
518
|
return bodyBytes.buffer;
|
|
463
519
|
}
|
|
464
520
|
|
|
521
|
+
function readResponseBodyChunk(
|
|
522
|
+
reader: ReturnType<StealthTransportBody["getReader"]>,
|
|
523
|
+
signal?: AbortSignal,
|
|
524
|
+
): Promise<{ done: boolean; value?: Uint8Array }> {
|
|
525
|
+
if (!signal) return reader.read();
|
|
526
|
+
throwIfAmbientAborted(signal);
|
|
527
|
+
return new Promise((resolve, reject) => {
|
|
528
|
+
let settled = false;
|
|
529
|
+
const settle = (operation: () => void) => {
|
|
530
|
+
if (settled) return;
|
|
531
|
+
settled = true;
|
|
532
|
+
signal.removeEventListener("abort", onAbort);
|
|
533
|
+
operation();
|
|
534
|
+
};
|
|
535
|
+
const onAbort = () => {
|
|
536
|
+
const error = toAmbientCancellationError(signal);
|
|
537
|
+
void reader.cancel().catch(() => undefined);
|
|
538
|
+
settle(() => reject(error));
|
|
539
|
+
};
|
|
540
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
541
|
+
void reader.read().then(
|
|
542
|
+
(chunk) => settle(() => resolve(chunk)),
|
|
543
|
+
(error) => settle(() => reject(error)),
|
|
544
|
+
);
|
|
545
|
+
if (signal.aborted) onAbort();
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
465
549
|
function normalizeBody(body: StealthFetchOptions["body"]): string {
|
|
466
550
|
if (body === undefined) {
|
|
467
551
|
return "";
|
|
@@ -627,8 +711,41 @@ function normalizeStealthTransportError(error: unknown): TransportError {
|
|
|
627
711
|
});
|
|
628
712
|
}
|
|
629
713
|
|
|
630
|
-
function
|
|
631
|
-
|
|
714
|
+
function toAmbientCancellationError(
|
|
715
|
+
signal: AbortSignal,
|
|
716
|
+
error: unknown = signal.reason,
|
|
717
|
+
): TransportError {
|
|
718
|
+
if (error instanceof TransportError && error.code === "transport_cancelled") {
|
|
719
|
+
return error;
|
|
720
|
+
}
|
|
721
|
+
return new TransportError("Request cancelled", {
|
|
722
|
+
code: "transport_cancelled",
|
|
723
|
+
status: 0,
|
|
724
|
+
retryable: false,
|
|
725
|
+
...(error !== undefined
|
|
726
|
+
? { cause: error instanceof Error ? error : new Error(String(error)) }
|
|
727
|
+
: {}),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function throwIfAmbientAborted(signal: AbortSignal | undefined): void {
|
|
732
|
+
if (signal?.aborted) throw toAmbientCancellationError(signal);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
736
|
+
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
737
|
+
throwIfAmbientAborted(signal);
|
|
738
|
+
return new Promise((resolve, reject) => {
|
|
739
|
+
const onAbort = () => {
|
|
740
|
+
clearTimeout(timer);
|
|
741
|
+
reject(toAmbientCancellationError(signal));
|
|
742
|
+
};
|
|
743
|
+
const timer = setTimeout(() => {
|
|
744
|
+
signal.removeEventListener("abort", onAbort);
|
|
745
|
+
resolve();
|
|
746
|
+
}, ms);
|
|
747
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
748
|
+
});
|
|
632
749
|
}
|
|
633
750
|
|
|
634
751
|
function normalizeMethod(method: HttpMethod | string): StealthMethod {
|
|
@@ -693,6 +810,7 @@ async function fetchStealthRedirectChain(
|
|
|
693
810
|
requestUrl: string,
|
|
694
811
|
method: StealthMethod,
|
|
695
812
|
options: StealthFetchOptions,
|
|
813
|
+
signal?: AbortSignal,
|
|
696
814
|
): Promise<{ normalized: StealthResponse; response: StealthTransportResponse }> {
|
|
697
815
|
let currentUrl = requestUrl;
|
|
698
816
|
let currentMethod = method;
|
|
@@ -703,6 +821,7 @@ async function fetchStealthRedirectChain(
|
|
|
703
821
|
const deadline = options.timeout ? performance.now() + options.timeout : undefined;
|
|
704
822
|
|
|
705
823
|
while (true) {
|
|
824
|
+
throwIfAmbientAborted(signal);
|
|
706
825
|
const headers = { ...currentHeaders };
|
|
707
826
|
if (!hasHeader(headers, "Cookie")) {
|
|
708
827
|
const cookieHeader = cookieJar.toHeader(currentUrl);
|
|
@@ -712,10 +831,12 @@ async function fetchStealthRedirectChain(
|
|
|
712
831
|
headers: normalizeHeaders(headers),
|
|
713
832
|
method: currentMethod,
|
|
714
833
|
redirect: "manual",
|
|
834
|
+
...(signal ? { signal } : {}),
|
|
715
835
|
};
|
|
716
836
|
if (currentBody !== undefined) requestInit.body = currentBody;
|
|
717
837
|
|
|
718
838
|
await transport.clearCookies();
|
|
839
|
+
throwIfAmbientAborted(signal);
|
|
719
840
|
const remainingTimeout =
|
|
720
841
|
deadline === undefined ? undefined : Math.ceil(deadline - performance.now());
|
|
721
842
|
if (remainingTimeout !== undefined && remainingTimeout <= 0) {
|
|
@@ -726,6 +847,10 @@ async function fetchStealthRedirectChain(
|
|
|
726
847
|
}
|
|
727
848
|
if (remainingTimeout !== undefined) requestInit.timeout = remainingTimeout;
|
|
728
849
|
response = await transport.fetch(currentUrl, requestInit);
|
|
850
|
+
if (signal?.aborted) {
|
|
851
|
+
discardStealthRedirectBody(response);
|
|
852
|
+
throw toAmbientCancellationError(signal);
|
|
853
|
+
}
|
|
729
854
|
cookieJar.setFromCookieStrings(
|
|
730
855
|
setCookieHeadersFromResponse(response.headers),
|
|
731
856
|
response.url ?? currentUrl,
|
|
@@ -764,7 +889,12 @@ async function fetchStealthRedirectChain(
|
|
|
764
889
|
followedHops += 1;
|
|
765
890
|
}
|
|
766
891
|
|
|
767
|
-
const normalized = await
|
|
892
|
+
const normalized = await normalizeResponseWithSignal(
|
|
893
|
+
response,
|
|
894
|
+
currentUrl,
|
|
895
|
+
options.maxBodyBytes,
|
|
896
|
+
signal,
|
|
897
|
+
);
|
|
768
898
|
if (followedHops > 0) normalized.redirected = true;
|
|
769
899
|
return { normalized, response };
|
|
770
900
|
}
|
|
@@ -819,19 +949,57 @@ function createSessionFetcher(
|
|
|
819
949
|
proxyUrl: string | undefined,
|
|
820
950
|
ignoreTlsErrors: boolean,
|
|
821
951
|
operation: (client: WreqSession) => Promise<T>,
|
|
952
|
+
signal?: AbortSignal,
|
|
822
953
|
): Promise<T> {
|
|
954
|
+
throwIfAmbientAborted(signal);
|
|
823
955
|
const entry = await getClientEntry(profileName, proxyUrl, ignoreTlsErrors);
|
|
956
|
+
throwIfAmbientAborted(signal);
|
|
824
957
|
const previous = entry.tail;
|
|
825
958
|
let release!: () => void;
|
|
826
959
|
entry.tail = new Promise<void>((resolve) => {
|
|
827
960
|
release = resolve;
|
|
828
961
|
});
|
|
829
|
-
|
|
962
|
+
let acquired = false;
|
|
830
963
|
try {
|
|
831
|
-
|
|
964
|
+
await waitForClientTurn(previous, signal);
|
|
965
|
+
acquired = true;
|
|
966
|
+
throwIfAmbientAborted(signal);
|
|
967
|
+
const client = await entry.session;
|
|
968
|
+
throwIfAmbientAborted(signal);
|
|
969
|
+
const result = await operation(client);
|
|
970
|
+
throwIfAmbientAborted(signal);
|
|
971
|
+
return result;
|
|
832
972
|
} finally {
|
|
833
|
-
|
|
973
|
+
if (acquired) {
|
|
974
|
+
release();
|
|
975
|
+
} else {
|
|
976
|
+
void previous.then(release, release);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async function waitForClientTurn(previous: Promise<void>, signal?: AbortSignal): Promise<void> {
|
|
982
|
+
if (!signal) {
|
|
983
|
+
await previous;
|
|
984
|
+
return;
|
|
834
985
|
}
|
|
986
|
+
throwIfAmbientAborted(signal);
|
|
987
|
+
await new Promise<void>((resolve, reject) => {
|
|
988
|
+
let settled = false;
|
|
989
|
+
const settle = (operation: () => void) => {
|
|
990
|
+
if (settled) return;
|
|
991
|
+
settled = true;
|
|
992
|
+
signal.removeEventListener("abort", onAbort);
|
|
993
|
+
operation();
|
|
994
|
+
};
|
|
995
|
+
const onAbort = () => settle(() => reject(toAmbientCancellationError(signal)));
|
|
996
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
997
|
+
void previous.then(
|
|
998
|
+
() => settle(resolve),
|
|
999
|
+
(error) => settle(() => reject(error)),
|
|
1000
|
+
);
|
|
1001
|
+
if (signal.aborted) onAbort();
|
|
1002
|
+
});
|
|
835
1003
|
}
|
|
836
1004
|
|
|
837
1005
|
async function resolveRequestProxy(
|
|
@@ -893,6 +1061,7 @@ function createSessionFetcher(
|
|
|
893
1061
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
894
1062
|
}
|
|
895
1063
|
})();
|
|
1064
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
896
1065
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
897
1066
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
898
1067
|
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
@@ -933,6 +1102,7 @@ function createSessionFetcher(
|
|
|
933
1102
|
const attemptedProxies = new Set<string>();
|
|
934
1103
|
|
|
935
1104
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1105
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
936
1106
|
let proxy: string | undefined;
|
|
937
1107
|
let attemptProxy: ResolvedAttemptProxy | undefined;
|
|
938
1108
|
// Reuse the exact serialization used by this outbound attempt in its catch path.
|
|
@@ -963,6 +1133,7 @@ function createSessionFetcher(
|
|
|
963
1133
|
});
|
|
964
1134
|
};
|
|
965
1135
|
try {
|
|
1136
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
966
1137
|
const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
|
|
967
1138
|
const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
|
|
968
1139
|
fallbackSensitiveValues = [
|
|
@@ -975,6 +1146,7 @@ function createSessionFetcher(
|
|
|
975
1146
|
fallbackRedactedUrl = structural.redactedUrl;
|
|
976
1147
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
977
1148
|
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
1149
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
978
1150
|
proxy = attemptProxy.url;
|
|
979
1151
|
if (proxy && dedupeAllocatorEndpoints) {
|
|
980
1152
|
// An under-filled allocation repeats endpoints (via the modulo
|
|
@@ -1003,7 +1175,15 @@ function createSessionFetcher(
|
|
|
1003
1175
|
proxy,
|
|
1004
1176
|
ignoreTlsErrors,
|
|
1005
1177
|
(transport) =>
|
|
1006
|
-
fetchStealthRedirectChain(
|
|
1178
|
+
fetchStealthRedirectChain(
|
|
1179
|
+
transport,
|
|
1180
|
+
cookieJar,
|
|
1181
|
+
requestUrl,
|
|
1182
|
+
method,
|
|
1183
|
+
options,
|
|
1184
|
+
clientOptions.signal,
|
|
1185
|
+
),
|
|
1186
|
+
clientOptions.signal,
|
|
1007
1187
|
);
|
|
1008
1188
|
|
|
1009
1189
|
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
@@ -1051,14 +1231,26 @@ function createSessionFetcher(
|
|
|
1051
1231
|
const sensitiveValues = serializedUrl?.sensitiveValues ?? fallbackSensitiveValues;
|
|
1052
1232
|
let normalizedError: TransportError;
|
|
1053
1233
|
try {
|
|
1234
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1054
1235
|
normalizedError = normalizeStealthTransportError(error);
|
|
1055
1236
|
} catch (normalizationError) {
|
|
1056
|
-
|
|
1237
|
+
const redactedNormalizationError = redactSensitiveError(
|
|
1057
1238
|
normalizationError,
|
|
1058
1239
|
sensitiveValues,
|
|
1059
1240
|
serializedUrl?.requestUrl ?? fallbackRequestUrl,
|
|
1060
1241
|
serializedUrl?.redactedUrl ?? fallbackRedactedUrl,
|
|
1061
1242
|
);
|
|
1243
|
+
if (
|
|
1244
|
+
normalizationError instanceof TransportError &&
|
|
1245
|
+
normalizationError.code === "transport_cancelled"
|
|
1246
|
+
) {
|
|
1247
|
+
recordProxyAttempt(
|
|
1248
|
+
"error",
|
|
1249
|
+
proxyAttemptErrorCode(normalizationError),
|
|
1250
|
+
proxyAttemptStatus(normalizationError),
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
throw redactedNormalizationError;
|
|
1062
1254
|
}
|
|
1063
1255
|
const retryErrorCode = proxyAttemptErrorCode(normalizedError);
|
|
1064
1256
|
const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
|
|
@@ -1116,8 +1308,12 @@ function createSessionFetcher(
|
|
|
1116
1308
|
})
|
|
1117
1309
|
) {
|
|
1118
1310
|
if (stealthRetryOptions) {
|
|
1119
|
-
await sleep(
|
|
1311
|
+
await sleep(
|
|
1312
|
+
computeProxyTransportRetryDelayMs(stealthRetryOptions, attempt + 1),
|
|
1313
|
+
clientOptions.signal,
|
|
1314
|
+
);
|
|
1120
1315
|
}
|
|
1316
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1121
1317
|
continue;
|
|
1122
1318
|
}
|
|
1123
1319
|
throw normalizedError;
|
|
@@ -1129,11 +1325,13 @@ function createSessionFetcher(
|
|
|
1129
1325
|
stalePoolError &&
|
|
1130
1326
|
refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES
|
|
1131
1327
|
) {
|
|
1328
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1132
1329
|
await invalidateProxyResolutionCacheAsync({
|
|
1133
1330
|
proxyPolicy: clientOptions.proxyPolicy,
|
|
1134
1331
|
upstream: clientOptions.upstream,
|
|
1135
1332
|
affinityKey: clientOptions.affinityKey,
|
|
1136
1333
|
});
|
|
1334
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1137
1335
|
continue;
|
|
1138
1336
|
}
|
|
1139
1337
|
|
|
@@ -1169,6 +1367,7 @@ function createSessionFetcher(
|
|
|
1169
1367
|
break;
|
|
1170
1368
|
}
|
|
1171
1369
|
|
|
1370
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1172
1371
|
throw normalizeStealthTransportError(lastError);
|
|
1173
1372
|
},
|
|
1174
1373
|
cookies: cookieJar,
|
|
@@ -1392,16 +1591,31 @@ function createSessionFetcher(
|
|
|
1392
1591
|
proxy: string,
|
|
1393
1592
|
): Promise<"source_ip_denied" | "edge_auth_rejected" | undefined> {
|
|
1394
1593
|
try {
|
|
1395
|
-
return await withClient(
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1594
|
+
return await withClient(
|
|
1595
|
+
profileName,
|
|
1596
|
+
proxy,
|
|
1597
|
+
false,
|
|
1598
|
+
async (client) => {
|
|
1599
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1600
|
+
await client.clearCookies();
|
|
1601
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1602
|
+
const response = await client.fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
|
|
1603
|
+
method: "GET",
|
|
1604
|
+
timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
|
|
1605
|
+
...(clientOptions.signal ? { signal: clientOptions.signal } : {}),
|
|
1606
|
+
});
|
|
1607
|
+
const normalized = await normalizeResponseWithSignal(
|
|
1608
|
+
response,
|
|
1609
|
+
undefined,
|
|
1610
|
+
undefined,
|
|
1611
|
+
clientOptions.signal,
|
|
1612
|
+
);
|
|
1613
|
+
return classifyProxyAuthDiagnosticMessage(normalized.body);
|
|
1614
|
+
},
|
|
1615
|
+
clientOptions.signal,
|
|
1616
|
+
);
|
|
1404
1617
|
} catch (error) {
|
|
1618
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1405
1619
|
const message =
|
|
1406
1620
|
error instanceof Error
|
|
1407
1621
|
? [error.message, error.cause instanceof Error ? error.cause.message : ""]
|
|
@@ -64,7 +64,10 @@ import {
|
|
|
64
64
|
type ProxyTelemetryLogPayload,
|
|
65
65
|
} from "../runtime/proxy-telemetry.js";
|
|
66
66
|
import type * as ResolverRuntimeModule from "../runtime/resolver.js";
|
|
67
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
createUnsupportedResolverClient,
|
|
69
|
+
type ResolverSolveWithRecorder,
|
|
70
|
+
} from "../runtime/resolver-shared.js";
|
|
68
71
|
import {
|
|
69
72
|
assertRequiredSecretsPresent,
|
|
70
73
|
listMissingRequiredSecrets,
|
|
@@ -77,7 +80,7 @@ import {
|
|
|
77
80
|
import { StealthCookieJar } from "../runtime/stealth-cookies.js";
|
|
78
81
|
import type * as StealthRuntimeModule from "../runtime/stealth.js";
|
|
79
82
|
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
80
|
-
import { createTraceContext } from "../runtime/trace.js";
|
|
83
|
+
import { createTraceContext, type TraceRecorder } from "../runtime/trace.js";
|
|
81
84
|
import { resolveTraceConfigFromEnv } from "../runtime/trace-config.js";
|
|
82
85
|
import { parseSchema } from "../schema.js";
|
|
83
86
|
import {
|
|
@@ -493,8 +496,14 @@ function bindResolverSignalWithoutRuntime(
|
|
|
493
496
|
): ResolverContext {
|
|
494
497
|
if (!defaultSignal) return resolver;
|
|
495
498
|
return {
|
|
496
|
-
solve(challenge, signal = defaultSignal) {
|
|
497
|
-
|
|
499
|
+
solve(challenge, signal = defaultSignal, traceRecorder?: TraceRecorder) {
|
|
500
|
+
// Forward the instrumentation trace recorder so resolver.vendor.* spans
|
|
501
|
+
// survive this wrapper. See ResolverSolveWithRecorder in resolver-shared.ts.
|
|
502
|
+
return (resolver as Partial<ResolverSolveWithRecorder> & ResolverContext).solve(
|
|
503
|
+
challenge,
|
|
504
|
+
signal,
|
|
505
|
+
traceRecorder,
|
|
506
|
+
);
|
|
498
507
|
},
|
|
499
508
|
};
|
|
500
509
|
}
|
|
@@ -667,6 +676,7 @@ function createProviderContext(
|
|
|
667
676
|
upstream: proxyClientOptions.upstream,
|
|
668
677
|
affinityKey: proxyClientOptions.affinityKey,
|
|
669
678
|
telemetry: proxyTelemetry,
|
|
679
|
+
...(signal ? { signal } : {}),
|
|
670
680
|
};
|
|
671
681
|
const { capabilityModules } = options;
|
|
672
682
|
const logStealthCleanupError = (error: unknown) =>
|
|
@@ -891,6 +901,7 @@ function createAuthFlowContext(
|
|
|
891
901
|
upstream: proxyClientOptions.upstream,
|
|
892
902
|
affinityKey: proxyClientOptions.affinityKey,
|
|
893
903
|
telemetry: proxyTelemetry,
|
|
904
|
+
...(signal ? { signal } : {}),
|
|
894
905
|
};
|
|
895
906
|
const { capabilityModules } = options;
|
|
896
907
|
const logStealthCleanupError = (error: unknown) =>
|