@apifuse/provider-sdk 2.2.0-beta.46 → 2.2.0-beta.48
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-public.d.ts +1 -1
- package/dist/runtime/resolver-public.js +1 -1
- package/dist/runtime/resolver-shared.d.ts +12 -1
- package/dist/runtime/resolver-vendors/capsolver.js +6 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +1 -0
- package/dist/runtime/resolver.d.ts +12 -0
- package/dist/runtime/resolver.js +44 -10
- package/dist/server/serve-implementation.js +5 -3
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/runtime/resolver-public.ts +3 -0
- package/src/runtime/resolver-shared.ts +17 -1
- package/src/runtime/resolver-vendors/capsolver.ts +8 -1
- package/src/runtime/resolver-vendors/twocaptcha.ts +1 -0
- package/src/runtime/resolver.ts +72 -18
- package/src/server/serve-implementation.ts +13 -4
- package/src/types.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.48
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit ba6636fa8dd53a35af3091ae6efd797f5ef3d836.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.47
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 0ff0c6c90153af3aab75978eb73040c9adaa2609.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.46
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit 0340fbc51b4ad71689bb1ec61b9b818dedda4587.
|
|
@@ -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 +1 @@
|
|
|
1
|
-
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_VENDOR_PREFERENCE, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, resolveProviderResolverVendors, type ResolverAdapterFactory, type ResolverInstrumentationMetadata, type ResolverRuntimeOptions, } from "./resolver.js";
|
|
1
|
+
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_VENDOR_PREFERENCE, DEFAULT_RESOLVER_TIMEOUT_MS, getResolverSolutionSource, invalidateCachedResolverSolution, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, resolveProviderResolverVendors, type ResolverAdapterFactory, type ResolverInstrumentationMetadata, type ResolverRuntimeOptions, type ResolverSolutionSource, } from "./resolver.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_VENDOR_PREFERENCE, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, resolveProviderResolverVendors, } from "./resolver.js";
|
|
1
|
+
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_VENDOR_PREFERENCE, DEFAULT_RESOLVER_TIMEOUT_MS, getResolverSolutionSource, invalidateCachedResolverSolution, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, resolveProviderResolverVendors, } from "./resolver.js";
|
|
@@ -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;
|
|
@@ -7,6 +7,11 @@ const CAPSOLVER_VENDOR_ID = "capsolver";
|
|
|
7
7
|
const DEFAULT_CAPSOLVER_BASE_URL = "https://api.capsolver.com";
|
|
8
8
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
10
|
+
const SDK_ESTIMATED_COOKIE_TTL_MS_BY_CHALLENGE_KIND = {
|
|
11
|
+
// CapSolver omits expiry, so use one conservative hour despite measured
|
|
12
|
+
// AWS WAF lifetimes of days.
|
|
13
|
+
aws_waf: 60 * 60 * 1_000,
|
|
14
|
+
};
|
|
10
15
|
class CapsolverSolveTimeoutError extends Error {
|
|
11
16
|
constructor() {
|
|
12
17
|
super("Capsolver resolver solve budget elapsed");
|
|
@@ -483,6 +488,7 @@ export function createCapsolverResolverVendorAdapter(options) {
|
|
|
483
488
|
form: "cookies",
|
|
484
489
|
cookies: { "aws-waf-token": solutionValue },
|
|
485
490
|
userAgent: identity?.userAgent ?? getStealthProfile(DEFAULT_PROFILE).userAgent,
|
|
491
|
+
sdkEstimatedExpires: (now() + SDK_ESTIMATED_COOKIE_TTL_MS_BY_CHALLENGE_KIND.aws_waf) / 1_000,
|
|
486
492
|
}
|
|
487
493
|
: { form: "token", token: solutionValue };
|
|
488
494
|
}
|
|
@@ -365,6 +365,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
365
365
|
if (!token?.trim()) {
|
|
366
366
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
|
|
367
367
|
}
|
|
368
|
+
// AWS WAF remains a token solution here, so resolver cookie caching does not apply.
|
|
368
369
|
return { form: "token", token };
|
|
369
370
|
}
|
|
370
371
|
};
|
|
@@ -31,6 +31,7 @@ export interface ResolverRuntimeOptions {
|
|
|
31
31
|
readonly identityScope?: string;
|
|
32
32
|
}) => ResolverVendorTransport;
|
|
33
33
|
}
|
|
34
|
+
export type ResolverSolutionSource = "cache" | "vendor";
|
|
34
35
|
export type ResolverInstrumentationMetadata = {
|
|
35
36
|
readonly target: ResolverContext;
|
|
36
37
|
readonly traceRecorder: TraceRecorder;
|
|
@@ -40,8 +41,19 @@ export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<Provider
|
|
|
40
41
|
export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
|
|
41
42
|
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
42
43
|
export declare function swapResolverDefaultUserAgentForTests(resolver: (() => string | undefined) | undefined): () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Identify whether this exact SDK-returned solution object came from the cache
|
|
46
|
+
* or a vendor solve. Returns undefined for copied or caller-created objects.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getResolverSolutionSource(solution: ChallengeSolution): ResolverSolutionSource | undefined;
|
|
43
49
|
/** Remove the cached entry for the exact solution object returned by this resolver. */
|
|
44
50
|
export declare function invalidateResolverSolution(resolver: ResolverContext, challenge: ProviderChallenge, solution: ChallengeSolution): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Remove a solution only when it was returned from the resolver cache. Providers
|
|
53
|
+
* should call this when an upstream serves the same challenge after a cached
|
|
54
|
+
* solution was applied, so the next `solve()` mints a fresh solution.
|
|
55
|
+
*/
|
|
56
|
+
export declare function invalidateCachedResolverSolution(resolver: ResolverContext, challenge: ProviderChallenge, solution: ChallengeSolution): Promise<boolean>;
|
|
45
57
|
export declare function createResolverClient(options: {
|
|
46
58
|
readonly kinds: readonly ProviderChallengeKind[];
|
|
47
59
|
readonly adapters: readonly ResolverVendorAdapter[];
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -19,6 +19,7 @@ const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
|
|
|
19
19
|
const MIN_RESOLVER_CACHE_TTL_MS = 1_000;
|
|
20
20
|
const resolverCaches = new WeakMap();
|
|
21
21
|
const solutionIssuerDigests = new WeakMap();
|
|
22
|
+
const resolverSolutionSources = new WeakMap();
|
|
22
23
|
const SAFE_CAUSE_MESSAGE_WORDS = new Set([
|
|
23
24
|
"abort",
|
|
24
25
|
"aborted",
|
|
@@ -376,7 +377,7 @@ function isResolverCacheIndex(value) {
|
|
|
376
377
|
function solutionExpiryMs(solution) {
|
|
377
378
|
if (solution.form !== "cookies")
|
|
378
379
|
return undefined;
|
|
379
|
-
const expires = solution.expires;
|
|
380
|
+
const expires = solution.expires ?? solution.sdkEstimatedExpires;
|
|
380
381
|
if (typeof expires !== "number" || !Number.isFinite(expires))
|
|
381
382
|
return undefined;
|
|
382
383
|
return expires * 1_000;
|
|
@@ -386,14 +387,33 @@ function rememberSolutionIssuer(solution, issuerDigest) {
|
|
|
386
387
|
solutionIssuerDigests.set(solution, issuerDigest);
|
|
387
388
|
}
|
|
388
389
|
}
|
|
390
|
+
function rememberSolutionSource(solution, source) {
|
|
391
|
+
if (typeof solution === "object" && solution !== null) {
|
|
392
|
+
resolverSolutionSources.set(solution, source);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Identify whether this exact SDK-returned solution object came from the cache
|
|
397
|
+
* or a vendor solve. Returns undefined for copied or caller-created objects.
|
|
398
|
+
*/
|
|
399
|
+
export function getResolverSolutionSource(solution) {
|
|
400
|
+
return resolverSolutionSources.get(solution);
|
|
401
|
+
}
|
|
389
402
|
async function readCachedSolution(cache, challenge, issuerDigest, now) {
|
|
390
403
|
const cached = await cache.get(resolverSolutionCacheKey(cache, challenge, issuerDigest));
|
|
391
404
|
if (!cached || !isCachedResolverSolution(cached.value))
|
|
392
405
|
return undefined;
|
|
393
406
|
if (cached.value.issuerDigest !== issuerDigest || cached.value.expiresAtMs <= now)
|
|
394
407
|
return undefined;
|
|
395
|
-
|
|
396
|
-
|
|
408
|
+
if (cached.value.solution.form !== "cookies")
|
|
409
|
+
return undefined;
|
|
410
|
+
const solution = {
|
|
411
|
+
...cached.value.solution,
|
|
412
|
+
cookies: { ...cached.value.solution.cookies },
|
|
413
|
+
};
|
|
414
|
+
rememberSolutionIssuer(solution, issuerDigest);
|
|
415
|
+
rememberSolutionSource(solution, "cache");
|
|
416
|
+
return solution;
|
|
397
417
|
}
|
|
398
418
|
async function findCachedSolution(cache, challenge, identity, identityScope) {
|
|
399
419
|
const now = Date.now();
|
|
@@ -457,8 +477,7 @@ async function cacheResolverSolution(cache, challenge, solution, identity, ident
|
|
|
457
477
|
{ direct: true, expiresAtMs, issuerDigest },
|
|
458
478
|
], now);
|
|
459
479
|
}
|
|
460
|
-
|
|
461
|
-
export async function invalidateResolverSolution(resolver, challenge, solution) {
|
|
480
|
+
async function invalidateResolverSolutionWithOutcome(resolver, challenge, solution) {
|
|
462
481
|
const metadata = resolver[RESOLVER_INSTRUMENTATION_METADATA];
|
|
463
482
|
const cacheOwner = metadata?.target ?? resolver;
|
|
464
483
|
const invalidate = async () => {
|
|
@@ -484,14 +503,28 @@ export async function invalidateResolverSolution(resolver, challenge, solution)
|
|
|
484
503
|
return "index_entry_deleted";
|
|
485
504
|
};
|
|
486
505
|
if (!metadata) {
|
|
487
|
-
await invalidate();
|
|
488
|
-
return;
|
|
506
|
+
return await invalidate();
|
|
489
507
|
}
|
|
490
|
-
await metadata.traceRecorder.runSpan("resolver.cache.invalidate", invalidate, {
|
|
508
|
+
return await metadata.traceRecorder.runSpan("resolver.cache.invalidate", invalidate, {
|
|
491
509
|
attributes: { challenge_kind: challenge.kind },
|
|
492
510
|
onSuccess: (outcome) => ({ outcome }),
|
|
493
511
|
});
|
|
494
512
|
}
|
|
513
|
+
/** Remove the cached entry for the exact solution object returned by this resolver. */
|
|
514
|
+
export async function invalidateResolverSolution(resolver, challenge, solution) {
|
|
515
|
+
await invalidateResolverSolutionWithOutcome(resolver, challenge, solution);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Remove a solution only when it was returned from the resolver cache. Providers
|
|
519
|
+
* should call this when an upstream serves the same challenge after a cached
|
|
520
|
+
* solution was applied, so the next `solve()` mints a fresh solution.
|
|
521
|
+
*/
|
|
522
|
+
export async function invalidateCachedResolverSolution(resolver, challenge, solution) {
|
|
523
|
+
if (getResolverSolutionSource(solution) !== "cache")
|
|
524
|
+
return false;
|
|
525
|
+
const outcome = await invalidateResolverSolutionWithOutcome(resolver, challenge, solution);
|
|
526
|
+
return outcome === "entry_deleted" || outcome === "index_entry_deleted";
|
|
527
|
+
}
|
|
495
528
|
async function resolveResolverIdentity(proxyIntent) {
|
|
496
529
|
const userAgentSource = proxyIntent.userAgent ? "declared" : "defaulted";
|
|
497
530
|
let proxyUrl;
|
|
@@ -618,6 +651,7 @@ function createResolverChainClient(options) {
|
|
|
618
651
|
await cacheResolverSolution(options.cache, challenge, solution, issuingIdentity, options.identityScope);
|
|
619
652
|
}
|
|
620
653
|
}
|
|
654
|
+
rememberSolutionSource(solution, "vendor");
|
|
621
655
|
return solution;
|
|
622
656
|
}
|
|
623
657
|
catch (error) {
|
|
@@ -682,8 +716,8 @@ export function bindResolverSignal(resolver, defaultSignal) {
|
|
|
682
716
|
if (!defaultSignal)
|
|
683
717
|
return resolver;
|
|
684
718
|
const boundResolver = {
|
|
685
|
-
solve(challenge, signal = defaultSignal) {
|
|
686
|
-
return resolver.solve(challenge, signal);
|
|
719
|
+
solve(challenge, signal = defaultSignal, traceRecorder) {
|
|
720
|
+
return resolver.solve(challenge, signal, traceRecorder);
|
|
687
721
|
},
|
|
688
722
|
};
|
|
689
723
|
if (resolverCaches.has(resolver)) {
|
|
@@ -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
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -338,6 +338,11 @@ export type ChallengeSolution = {
|
|
|
338
338
|
readonly userAgent: string;
|
|
339
339
|
/** Epoch seconds copied from the upstream cookie's own expiry attribute; never a constant. */
|
|
340
340
|
readonly expires?: number;
|
|
341
|
+
/**
|
|
342
|
+
* Epoch seconds conservatively estimated by the SDK when a resolver vendor omits
|
|
343
|
+
* the upstream cookie's expiry. `expires` takes precedence when both are present.
|
|
344
|
+
*/
|
|
345
|
+
readonly sdkEstimatedExpires?: number;
|
|
341
346
|
};
|
|
342
347
|
export interface ProviderResolverConfig {
|
|
343
348
|
/** Optional ordered override for the SDK-owned vendor fallback chain. */
|
package/package.json
CHANGED
|
@@ -10,6 +10,8 @@ export {
|
|
|
10
10
|
createUnsupportedResolverClient,
|
|
11
11
|
DEFAULT_RESOLVER_VENDOR_PREFERENCE,
|
|
12
12
|
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
13
|
+
getResolverSolutionSource,
|
|
14
|
+
invalidateCachedResolverSolution,
|
|
13
15
|
invalidateResolverSolution,
|
|
14
16
|
RESOLVER_ADAPTER_REGISTRY,
|
|
15
17
|
RESOLVER_INSTRUMENTATION_METADATA,
|
|
@@ -17,4 +19,5 @@ export {
|
|
|
17
19
|
type ResolverAdapterFactory,
|
|
18
20
|
type ResolverInstrumentationMetadata,
|
|
19
21
|
type ResolverRuntimeOptions,
|
|
22
|
+
type ResolverSolutionSource,
|
|
20
23
|
} from "./resolver.js";
|
|
@@ -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() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getStealthProfile } from "../../stealth/profiles.js";
|
|
2
|
-
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
|
+
import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind } from "../../types.js";
|
|
3
3
|
import { redactSensitiveText } from "../request-options.js";
|
|
4
4
|
import { DEFAULT_PROFILE } from "../stealth.js";
|
|
5
5
|
import type { TraceRecorder } from "../trace.js";
|
|
@@ -17,6 +17,11 @@ const CAPSOLVER_VENDOR_ID = "capsolver" as const;
|
|
|
17
17
|
const DEFAULT_CAPSOLVER_BASE_URL = "https://api.capsolver.com";
|
|
18
18
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
19
19
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
20
|
+
const SDK_ESTIMATED_COOKIE_TTL_MS_BY_CHALLENGE_KIND = {
|
|
21
|
+
// CapSolver omits expiry, so use one conservative hour despite measured
|
|
22
|
+
// AWS WAF lifetimes of days.
|
|
23
|
+
aws_waf: 60 * 60 * 1_000,
|
|
24
|
+
} satisfies Partial<Record<ProviderChallengeKind, number>>;
|
|
20
25
|
|
|
21
26
|
type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
|
|
22
27
|
type CapsolverOperationPhase = "create_task" | "poll_result";
|
|
@@ -657,6 +662,8 @@ export function createCapsolverResolverVendorAdapter(
|
|
|
657
662
|
form: "cookies" as const,
|
|
658
663
|
cookies: { "aws-waf-token": solutionValue },
|
|
659
664
|
userAgent: identity?.userAgent ?? getStealthProfile(DEFAULT_PROFILE).userAgent,
|
|
665
|
+
sdkEstimatedExpires:
|
|
666
|
+
(now() + SDK_ESTIMATED_COOKIE_TTL_MS_BY_CHALLENGE_KIND.aws_waf) / 1_000,
|
|
660
667
|
}
|
|
661
668
|
: { form: "token" as const, token: solutionValue };
|
|
662
669
|
}
|
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,
|
|
@@ -142,11 +143,21 @@ type ResolverCacheIndex = {
|
|
|
142
143
|
}[];
|
|
143
144
|
};
|
|
144
145
|
|
|
146
|
+
export type ResolverSolutionSource = "cache" | "vendor";
|
|
147
|
+
|
|
148
|
+
type ResolverSolutionInvalidationOutcome =
|
|
149
|
+
| "cache_disabled"
|
|
150
|
+
| "entry_deleted"
|
|
151
|
+
| "index_entry_deleted"
|
|
152
|
+
| "not_cookie_solution"
|
|
153
|
+
| "solution_not_cached";
|
|
154
|
+
|
|
145
155
|
const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
|
|
146
156
|
const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
|
|
147
157
|
const MIN_RESOLVER_CACHE_TTL_MS = 1_000;
|
|
148
158
|
const resolverCaches = new WeakMap<object, ProviderCache | null>();
|
|
149
159
|
const solutionIssuerDigests = new WeakMap<object, string>();
|
|
160
|
+
const resolverSolutionSources = new WeakMap<object, ResolverSolutionSource>();
|
|
150
161
|
const SAFE_CAUSE_MESSAGE_WORDS: ReadonlySet<string> = new Set([
|
|
151
162
|
"abort",
|
|
152
163
|
"aborted",
|
|
@@ -596,7 +607,7 @@ function isResolverCacheIndex(value: unknown): value is ResolverCacheIndex {
|
|
|
596
607
|
|
|
597
608
|
function solutionExpiryMs(solution: ChallengeSolution): number | undefined {
|
|
598
609
|
if (solution.form !== "cookies") return undefined;
|
|
599
|
-
const expires = solution.expires;
|
|
610
|
+
const expires = solution.expires ?? solution.sdkEstimatedExpires;
|
|
600
611
|
if (typeof expires !== "number" || !Number.isFinite(expires)) return undefined;
|
|
601
612
|
return expires * 1_000;
|
|
602
613
|
}
|
|
@@ -607,6 +618,22 @@ function rememberSolutionIssuer(solution: ChallengeSolution, issuerDigest: strin
|
|
|
607
618
|
}
|
|
608
619
|
}
|
|
609
620
|
|
|
621
|
+
function rememberSolutionSource(solution: ChallengeSolution, source: ResolverSolutionSource): void {
|
|
622
|
+
if (typeof solution === "object" && solution !== null) {
|
|
623
|
+
resolverSolutionSources.set(solution, source);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Identify whether this exact SDK-returned solution object came from the cache
|
|
629
|
+
* or a vendor solve. Returns undefined for copied or caller-created objects.
|
|
630
|
+
*/
|
|
631
|
+
export function getResolverSolutionSource(
|
|
632
|
+
solution: ChallengeSolution,
|
|
633
|
+
): ResolverSolutionSource | undefined {
|
|
634
|
+
return resolverSolutionSources.get(solution);
|
|
635
|
+
}
|
|
636
|
+
|
|
610
637
|
async function readCachedSolution(
|
|
611
638
|
cache: ProviderCache,
|
|
612
639
|
challenge: ProviderChallenge,
|
|
@@ -617,8 +644,14 @@ async function readCachedSolution(
|
|
|
617
644
|
if (!cached || !isCachedResolverSolution(cached.value)) return undefined;
|
|
618
645
|
if (cached.value.issuerDigest !== issuerDigest || cached.value.expiresAtMs <= now)
|
|
619
646
|
return undefined;
|
|
620
|
-
|
|
621
|
-
|
|
647
|
+
if (cached.value.solution.form !== "cookies") return undefined;
|
|
648
|
+
const solution = {
|
|
649
|
+
...cached.value.solution,
|
|
650
|
+
cookies: { ...cached.value.solution.cookies },
|
|
651
|
+
};
|
|
652
|
+
rememberSolutionIssuer(solution, issuerDigest);
|
|
653
|
+
rememberSolutionSource(solution, "cache");
|
|
654
|
+
return solution;
|
|
622
655
|
}
|
|
623
656
|
|
|
624
657
|
async function findCachedSolution(
|
|
@@ -719,25 +752,18 @@ async function cacheResolverSolution(
|
|
|
719
752
|
);
|
|
720
753
|
}
|
|
721
754
|
|
|
722
|
-
|
|
723
|
-
export async function invalidateResolverSolution(
|
|
755
|
+
async function invalidateResolverSolutionWithOutcome(
|
|
724
756
|
resolver: ResolverContext,
|
|
725
757
|
challenge: ProviderChallenge,
|
|
726
758
|
solution: ChallengeSolution,
|
|
727
|
-
): Promise<
|
|
759
|
+
): Promise<ResolverSolutionInvalidationOutcome> {
|
|
728
760
|
const metadata = (
|
|
729
761
|
resolver as ResolverContext & {
|
|
730
762
|
readonly [RESOLVER_INSTRUMENTATION_METADATA]?: ResolverInstrumentationMetadata;
|
|
731
763
|
}
|
|
732
764
|
)[RESOLVER_INSTRUMENTATION_METADATA];
|
|
733
765
|
const cacheOwner = metadata?.target ?? resolver;
|
|
734
|
-
const invalidate = async (): Promise<
|
|
735
|
-
| "cache_disabled"
|
|
736
|
-
| "entry_deleted"
|
|
737
|
-
| "index_entry_deleted"
|
|
738
|
-
| "not_cookie_solution"
|
|
739
|
-
| "solution_not_cached"
|
|
740
|
-
> => {
|
|
766
|
+
const invalidate = async (): Promise<ResolverSolutionInvalidationOutcome> => {
|
|
741
767
|
if (solution.form !== "cookies") return "not_cookie_solution";
|
|
742
768
|
if (!resolverCaches.has(cacheOwner)) {
|
|
743
769
|
throw new Error("Resolver cache registration lookup failed during solution invalidation");
|
|
@@ -763,15 +789,38 @@ export async function invalidateResolverSolution(
|
|
|
763
789
|
};
|
|
764
790
|
|
|
765
791
|
if (!metadata) {
|
|
766
|
-
await invalidate();
|
|
767
|
-
return;
|
|
792
|
+
return await invalidate();
|
|
768
793
|
}
|
|
769
|
-
await metadata.traceRecorder.runSpan("resolver.cache.invalidate", invalidate, {
|
|
794
|
+
return await metadata.traceRecorder.runSpan("resolver.cache.invalidate", invalidate, {
|
|
770
795
|
attributes: { challenge_kind: challenge.kind },
|
|
771
796
|
onSuccess: (outcome) => ({ outcome }),
|
|
772
797
|
});
|
|
773
798
|
}
|
|
774
799
|
|
|
800
|
+
/** Remove the cached entry for the exact solution object returned by this resolver. */
|
|
801
|
+
export async function invalidateResolverSolution(
|
|
802
|
+
resolver: ResolverContext,
|
|
803
|
+
challenge: ProviderChallenge,
|
|
804
|
+
solution: ChallengeSolution,
|
|
805
|
+
): Promise<void> {
|
|
806
|
+
await invalidateResolverSolutionWithOutcome(resolver, challenge, solution);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Remove a solution only when it was returned from the resolver cache. Providers
|
|
811
|
+
* should call this when an upstream serves the same challenge after a cached
|
|
812
|
+
* solution was applied, so the next `solve()` mints a fresh solution.
|
|
813
|
+
*/
|
|
814
|
+
export async function invalidateCachedResolverSolution(
|
|
815
|
+
resolver: ResolverContext,
|
|
816
|
+
challenge: ProviderChallenge,
|
|
817
|
+
solution: ChallengeSolution,
|
|
818
|
+
): Promise<boolean> {
|
|
819
|
+
if (getResolverSolutionSource(solution) !== "cache") return false;
|
|
820
|
+
const outcome = await invalidateResolverSolutionWithOutcome(resolver, challenge, solution);
|
|
821
|
+
return outcome === "entry_deleted" || outcome === "index_entry_deleted";
|
|
822
|
+
}
|
|
823
|
+
|
|
775
824
|
async function resolveResolverIdentity(
|
|
776
825
|
proxyIntent: NonNullable<ResolverRuntimeOptions["proxyIntent"]>,
|
|
777
826
|
): Promise<{
|
|
@@ -937,6 +986,7 @@ function createResolverChainClient(options: {
|
|
|
937
986
|
);
|
|
938
987
|
}
|
|
939
988
|
}
|
|
989
|
+
rememberSolutionSource(solution, "vendor");
|
|
940
990
|
return solution;
|
|
941
991
|
} catch (error) {
|
|
942
992
|
signal.throwIfAborted();
|
|
@@ -1022,8 +1072,12 @@ export function bindResolverSignal(
|
|
|
1022
1072
|
): ResolverContext {
|
|
1023
1073
|
if (!defaultSignal) return resolver;
|
|
1024
1074
|
const boundResolver: ResolverContext = {
|
|
1025
|
-
solve(challenge, signal = defaultSignal) {
|
|
1026
|
-
return resolver.solve(
|
|
1075
|
+
solve(challenge, signal = defaultSignal, traceRecorder?: TraceRecorder) {
|
|
1076
|
+
return (resolver as Partial<ResolverSolveWithRecorder> & ResolverContext).solve(
|
|
1077
|
+
challenge,
|
|
1078
|
+
signal,
|
|
1079
|
+
traceRecorder,
|
|
1080
|
+
);
|
|
1027
1081
|
},
|
|
1028
1082
|
};
|
|
1029
1083
|
if (resolverCaches.has(resolver)) {
|
|
@@ -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
|
}
|
package/src/types.ts
CHANGED
|
@@ -409,6 +409,11 @@ export type ChallengeSolution =
|
|
|
409
409
|
readonly userAgent: string;
|
|
410
410
|
/** Epoch seconds copied from the upstream cookie's own expiry attribute; never a constant. */
|
|
411
411
|
readonly expires?: number;
|
|
412
|
+
/**
|
|
413
|
+
* Epoch seconds conservatively estimated by the SDK when a resolver vendor omits
|
|
414
|
+
* the upstream cookie's expiry. `expires` takes precedence when both are present.
|
|
415
|
+
*/
|
|
416
|
+
readonly sdkEstimatedExpires?: number;
|
|
412
417
|
};
|
|
413
418
|
|
|
414
419
|
export interface ProviderResolverConfig {
|