@apifuse/provider-sdk 2.2.0-beta.26 → 2.2.0-beta.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-dev.ts +2 -2
  3. package/bin/apifuse-pack-smoke.ts +1 -1
  4. package/bin/apifuse-pack-types.ts +2 -1
  5. package/bin/apifuse-perf.ts +2 -4
  6. package/bin/apifuse-record.ts +1 -1
  7. package/dist/auth-turn/index.d.ts +1 -1
  8. package/dist/auth-turn/index.js +1 -1
  9. package/dist/ceremonies/index.js +52 -11
  10. package/dist/config/loader.d.ts +1 -1
  11. package/dist/config/loader.js +4 -2
  12. package/dist/contract-serialization.d.ts +1 -20
  13. package/dist/contract-serialization.js +8 -587
  14. package/dist/contract.d.ts +0 -2
  15. package/dist/contract.js +5 -9
  16. package/dist/index.d.ts +9 -8
  17. package/dist/index.js +6 -8
  18. package/dist/provider.d.ts +3 -2
  19. package/dist/provider.js +2 -2
  20. package/dist/runtime/auth-flow.js +1 -1
  21. package/dist/runtime/http.d.ts +1 -0
  22. package/dist/runtime/http.js +135 -12
  23. package/dist/runtime/instrumentation.js +1 -1
  24. package/dist/runtime/native-network-errors.d.ts +33 -0
  25. package/dist/runtime/native-network-errors.js +69 -0
  26. package/dist/runtime/native-network.d.ts +2 -33
  27. package/dist/runtime/native-network.js +2 -68
  28. package/dist/runtime/redis.d.ts +1 -1
  29. package/dist/runtime/redis.js +4 -2
  30. package/dist/runtime/resolver-config.d.ts +6 -0
  31. package/dist/runtime/resolver-config.js +6 -0
  32. package/dist/runtime/resolver-shared.d.ts +3 -0
  33. package/dist/runtime/resolver-shared.js +12 -0
  34. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
  35. package/dist/runtime/resolver-vendors/twocaptcha.js +80 -43
  36. package/dist/runtime/resolver-vendors/types.d.ts +1 -1
  37. package/dist/runtime/resolver.d.ts +6 -10
  38. package/dist/runtime/resolver.js +19 -18
  39. package/dist/runtime/state.js +5 -115
  40. package/dist/runtime/stealth-cookies.d.ts +20 -0
  41. package/dist/runtime/stealth-cookies.js +111 -0
  42. package/dist/runtime/stealth.js +7 -130
  43. package/dist/schema.d.ts +0 -63
  44. package/dist/schema.js +8 -808
  45. package/dist/serve.d.ts +1 -1
  46. package/dist/serve.js +1 -1
  47. package/dist/server/index.d.ts +1 -1
  48. package/dist/server/index.js +1 -1
  49. package/dist/server/self-test.d.ts +13 -0
  50. package/dist/server/self-test.js +124 -46
  51. package/dist/server/serve-implementation.d.ts +199 -0
  52. package/dist/server/serve-implementation.js +2054 -0
  53. package/dist/server/serve.d.ts +1 -187
  54. package/dist/server/serve.js +1 -1827
  55. package/dist/stateful/errors.d.ts +5 -0
  56. package/dist/stateful/errors.js +10 -0
  57. package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
  58. package/dist/stateful/stateful-provider-session-routing.js +2 -10
  59. package/dist/stream.js +7 -1
  60. package/package.json +27 -2
  61. package/src/auth-turn/index.ts +1 -1
  62. package/src/ceremonies/index.ts +68 -18
  63. package/src/config/loader.ts +5 -2
  64. package/src/contract-serialization.ts +8 -859
  65. package/src/contract.ts +5 -16
  66. package/src/index.ts +18 -34
  67. package/src/provider.ts +12 -24
  68. package/src/runtime/auth-flow.ts +1 -1
  69. package/src/runtime/http.ts +155 -11
  70. package/src/runtime/instrumentation.ts +1 -1
  71. package/src/runtime/native-network-errors.ts +99 -0
  72. package/src/runtime/native-network.ts +16 -97
  73. package/src/runtime/redis.ts +7 -2
  74. package/src/runtime/resolver-config.ts +6 -0
  75. package/src/runtime/resolver-shared.ts +17 -0
  76. package/src/runtime/resolver-vendors/twocaptcha.ts +100 -49
  77. package/src/runtime/resolver-vendors/types.ts +1 -0
  78. package/src/runtime/resolver.ts +47 -22
  79. package/src/runtime/state.ts +5 -144
  80. package/src/runtime/stealth-cookies.ts +132 -0
  81. package/src/runtime/stealth.ts +14 -157
  82. package/src/schema.ts +9 -1060
  83. package/src/serve.ts +6 -1
  84. package/src/server/index.ts +1 -0
  85. package/src/server/self-test.ts +184 -59
  86. package/src/server/serve-implementation.ts +3024 -0
  87. package/src/server/serve.ts +1 -2661
  88. package/src/stateful/errors.ts +12 -0
  89. package/src/stateful/stateful-provider-session-routing.ts +2 -11
  90. package/src/stream.ts +8 -1
@@ -12,7 +12,6 @@ import {
12
12
  VENDOR_DEFAULT_PROTOCOL,
13
13
  resolveWithVendor,
14
14
  } from "../config/loader.js";
15
- import { TransportError } from "../errors.js";
16
15
  import {
17
16
  type DynamicEgressRuleSnapshot,
18
17
  NativeEgressPolicyValidationError,
@@ -46,6 +45,14 @@ import type {
46
45
  EnvContext,
47
46
  } from "../types.js";
48
47
  import { createEnvContext } from "./env.js";
48
+ import {
49
+ NativeEgressGrantExpiredError,
50
+ NativeEgressNotDeclaredError,
51
+ NativeIdleTimeoutError,
52
+ NativeNetworkError,
53
+ NativeProxyExpiredError,
54
+ safeDiagnosticEgressHost,
55
+ } from "./native-network-errors.js";
49
56
  import {
50
57
  NODEMAVEN_FILTER_ENV,
51
58
  NODEMAVEN_PASSWORD_ENV,
@@ -55,102 +62,14 @@ import {
55
62
  } from "./proxy-nodemaven.js";
56
63
  import { redactSensitiveError } from "./request-options.js";
57
64
 
58
- export type NativeNetworkErrorCode =
59
- | "native_connection_aborted"
60
- | "native_connection_closed"
61
- | "native_connection_failed"
62
- | "native_connection_idle_timeout"
63
- | "native_connection_timeout"
64
- | "native_egress_authorization_failed"
65
- | "native_egress_grant_expired"
66
- | "native_egress_grant_invalid"
67
- | "native_egress_grant_limit_exceeded"
68
- | "native_egress_input_invalid"
69
- | "native_egress_not_declared"
70
- | "native_egress_policy_invalid"
71
- | "native_dynamic_egress_unsupported"
72
- | "native_proxy_expired"
73
- | "native_proxy_invalid";
74
-
75
- export class NativeNetworkError extends TransportError {
76
- constructor(message: string, code: NativeNetworkErrorCode, cause?: Error) {
77
- const isEgressPolicyFailure =
78
- code.startsWith("native_egress_") || code === "native_dynamic_egress_unsupported";
79
- super(message, {
80
- code,
81
- status: 0,
82
- ...(cause ? { cause } : {}),
83
- ...(isEgressPolicyFailure ? { category: "provider_error" as const, retryable: false } : {}),
84
- });
85
- this.name = "NativeNetworkError";
86
- }
87
-
88
- override get code(): NativeNetworkErrorCode {
89
- return super.code as NativeNetworkErrorCode;
90
- }
91
- }
92
-
93
- function safeDiagnosticEgressHost(value: unknown): string {
94
- const canonical = canonicalizeEgressHost(value);
95
- return canonical.ok ? canonical.host : `<invalid-host:${canonical.reason}>`;
96
- }
97
-
98
- export class NativeProxyExpiredError extends NativeNetworkError {
99
- constructor(readonly expiresAt: string) {
100
- super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
101
- this.name = "NativeProxyExpiredError";
102
- }
103
- }
104
-
105
- /** Raised before transport setup when a native destination is not authorized. */
106
- export class NativeEgressNotDeclaredError extends NativeNetworkError {
107
- readonly host: string;
108
-
109
- constructor(
110
- host: string,
111
- readonly port: number,
112
- readonly tls: "required" | "disabled",
113
- ) {
114
- const diagnosticHost = safeDiagnosticEgressHost(host);
115
- super(
116
- `Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${diagnosticHost}:${port}`,
117
- "native_egress_not_declared",
118
- );
119
- this.host = diagnosticHost;
120
- this.name = "NativeEgressNotDeclaredError";
121
- }
122
- }
123
-
124
- /**
125
- * Raised when the destination was authorized by a grant whose TTL elapsed and
126
- * its expiry remains in the client's bounded recent-expiry evidence window.
127
- */
128
- export class NativeEgressGrantExpiredError extends NativeNetworkError {
129
- readonly host: string;
130
-
131
- constructor(
132
- host: string,
133
- readonly port: number,
134
- readonly tls: "required" | "disabled",
135
- readonly expiresAt: string,
136
- ) {
137
- const diagnosticHost = safeDiagnosticEgressHost(host);
138
- super(
139
- `Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${diagnosticHost}:${port}`,
140
- "native_egress_grant_expired",
141
- );
142
- this.host = diagnosticHost;
143
- this.name = "NativeEgressGrantExpiredError";
144
- }
145
- }
146
-
147
- /** Raised when an established connection exceeds its opt-in read-idle window. */
148
- export class NativeIdleTimeoutError extends NativeNetworkError {
149
- constructor() {
150
- super("Native network socket timed out while reading.", "native_connection_idle_timeout");
151
- this.name = "NativeIdleTimeoutError";
152
- }
153
- }
65
+ export {
66
+ NativeEgressGrantExpiredError,
67
+ NativeEgressNotDeclaredError,
68
+ NativeIdleTimeoutError,
69
+ NativeNetworkError,
70
+ NativeProxyExpiredError,
71
+ } from "./native-network-errors.js";
72
+ export type { NativeNetworkErrorCode } from "./native-network-errors.js";
154
73
 
155
74
  export type NativeGatewayProxy = NativeProxyEgressInfo & {
156
75
  readonly url: string;
@@ -1,4 +1,8 @@
1
- import { Redis } from "ioredis";
1
+ import { createRequire } from "node:module";
2
+
3
+ import type { Redis } from "ioredis";
4
+
5
+ const require = createRequire(import.meta.url);
2
6
 
3
7
  export type ProviderRedisClient = Redis;
4
8
 
@@ -17,7 +21,8 @@ type RedisTimeoutOptions<T> = {
17
21
  export function createProviderRedisClient(
18
22
  options: ProviderRedisClientOptions,
19
23
  ): ProviderRedisClient {
20
- const redis = new Redis(options.redisUrl, {
24
+ const { Redis: RedisClient } = require("ioredis") as typeof import("ioredis");
25
+ const redis = new RedisClient(options.redisUrl, {
21
26
  connectTimeout: options.timeoutMs,
22
27
  enableOfflineQueue: false,
23
28
  lazyConnect: true,
@@ -0,0 +1,6 @@
1
+ export const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
2
+ export const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
3
+ export const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
4
+ export const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
5
+ export const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
6
+ export const DEFAULT_RESOLVER_TIMEOUT_MS = 180_000;
@@ -0,0 +1,17 @@
1
+ import { ProviderError } from "../errors.js";
2
+ import type { ResolverContext } from "../types.js";
3
+
4
+ export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
5
+ "@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
6
+ );
7
+
8
+ export function createUnsupportedResolverClient(reason?: string): ResolverContext {
9
+ return {
10
+ async solve() {
11
+ throw new ProviderError(reason ?? "Resolver runtime is not configured", {
12
+ code: "RESOLVER_UNAVAILABLE",
13
+ fix: "Declare resolver on the provider definition and configure vendor credentials.",
14
+ });
15
+ },
16
+ };
17
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
2
+ import type { TraceRecorder } from "../trace.js";
2
3
  import { assertResolverHostAllowed } from "./hosts.js";
3
4
  import {
4
5
  type ResolverIdentity,
@@ -34,6 +35,7 @@ export interface TwoCaptchaResolverVendorAdapter extends ResolverVendorAdapter {
34
35
  challenge: ProviderChallenge,
35
36
  identity: ResolverIdentity | undefined,
36
37
  signal: AbortSignal,
38
+ traceRecorder?: TraceRecorder,
37
39
  ): Promise<Extract<ChallengeSolution, { readonly form: "token" }>>;
38
40
  }
39
41
 
@@ -231,6 +233,28 @@ function endpoint(baseUrl: string, path: string): string {
231
233
  return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
232
234
  }
233
235
 
236
+ function spanErrorAttributes(
237
+ error: unknown,
238
+ phase: TwoCaptchaOperationPhase,
239
+ ): Record<string, unknown> | undefined {
240
+ if (error instanceof ResolverVendorUnavailableError) {
241
+ return {
242
+ unavailability_reason: error.reason,
243
+ transport_phase: error.phase,
244
+ };
245
+ }
246
+ // The solve budget raises TwoCaptchaSolveTimeoutError inside the spanned closure and
247
+ // the outer catch converts it only after the span has been finalized, so the span has
248
+ // to recognize it here or slow solves lose exactly the attribution these spans add.
249
+ if (error instanceof TwoCaptchaSolveTimeoutError) {
250
+ return {
251
+ unavailability_reason: "timeout",
252
+ transport_phase: phase,
253
+ };
254
+ }
255
+ return undefined;
256
+ }
257
+
234
258
  export function createTwoCaptchaResolverVendorAdapter(
235
259
  options: TwoCaptchaResolverVendorOptions,
236
260
  ): TwoCaptchaResolverVendorAdapter {
@@ -248,7 +272,7 @@ export function createTwoCaptchaResolverVendorAdapter(
248
272
  return resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, kind);
249
273
  },
250
274
 
251
- async solve(challenge, identity, callerSignal) {
275
+ async solve(challenge, identity, callerSignal, traceRecorder) {
252
276
  const apiKey = options.apiKey?.trim();
253
277
  if (!apiKey) {
254
278
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_credentials", {
@@ -285,62 +309,89 @@ export function createTwoCaptchaResolverVendorAdapter(
285
309
  let phase: TwoCaptchaOperationPhase = "create_task";
286
310
 
287
311
  try {
288
- const createResult = await postJson(
289
- fetchImpl,
290
- endpoint(baseUrl, "createTask"),
291
- {
292
- clientKey: apiKey,
293
- task: {
294
- type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
295
- websiteURL: challenge.pageUrl,
296
- websiteKey: challenge.siteKey,
297
- isInvisible: false,
298
- ...(identity ? { userAgent: identity.userAgent } : {}),
299
- ...(proxy ?? {}),
300
- },
301
- },
302
- solveController.signal,
303
- phase,
304
- );
305
- const taskId = taskIdFrom(createResult.payload);
306
- if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
307
- throw unavailableForPayload(createResult.payload, phase);
308
- }
309
-
310
- phase = "poll_result";
311
- while (true) {
312
- callerSignal.throwIfAborted();
313
- const remainingMs = timeoutMs - (now() - startedAt);
314
- if (remainingMs <= 0) throw new TwoCaptchaSolveTimeoutError();
315
- await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
316
- callerSignal.throwIfAborted();
317
- if (now() - startedAt >= timeoutMs) throw new TwoCaptchaSolveTimeoutError();
318
-
319
- const pollResult = await postJson(
312
+ const createTask = async () => {
313
+ const createResult = await postJson(
320
314
  fetchImpl,
321
- endpoint(baseUrl, "getTaskResult"),
322
- { clientKey: apiKey, taskId },
315
+ endpoint(baseUrl, "createTask"),
316
+ {
317
+ clientKey: apiKey,
318
+ task: {
319
+ type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
320
+ websiteURL: challenge.pageUrl,
321
+ websiteKey: challenge.siteKey,
322
+ isInvisible: false,
323
+ ...(identity ? { userAgent: identity.userAgent } : {}),
324
+ ...(proxy ?? {}),
325
+ },
326
+ },
323
327
  solveController.signal,
324
328
  phase,
325
329
  );
326
- if (!pollResult.ok || pollResult.payload.errorId !== 0) {
327
- throw unavailableForPayload(pollResult.payload, phase);
328
- }
329
- if (pollResult.payload.status === "processing") continue;
330
- if (pollResult.payload.status !== "ready") {
331
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
332
- phase,
333
- });
330
+ const taskId = taskIdFrom(createResult.payload);
331
+ if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
332
+ throw unavailableForPayload(createResult.payload, phase);
334
333
  }
334
+ return taskId;
335
+ };
336
+ const taskId = traceRecorder
337
+ ? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
338
+ attributes: {
339
+ vendor: TWOCAPTCHA_VENDOR_ID,
340
+ challenge_kind: challenge.kind,
341
+ },
342
+ onError: (error) => spanErrorAttributes(error, "create_task"),
343
+ })
344
+ : await createTask();
335
345
 
336
- const token = tokenFrom(pollResult.payload);
337
- if (!token?.trim()) {
338
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
346
+ phase = "poll_result";
347
+ const pollResult = async () => {
348
+ while (true) {
349
+ callerSignal.throwIfAborted();
350
+ const remainingMs = timeoutMs - (now() - startedAt);
351
+ if (remainingMs <= 0) throw new TwoCaptchaSolveTimeoutError();
352
+ await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
353
+ callerSignal.throwIfAborted();
354
+ if (now() - startedAt >= timeoutMs) throw new TwoCaptchaSolveTimeoutError();
355
+
356
+ const pollResult = await postJson(
357
+ fetchImpl,
358
+ endpoint(baseUrl, "getTaskResult"),
359
+ { clientKey: apiKey, taskId },
360
+ solveController.signal,
339
361
  phase,
340
- });
362
+ );
363
+ if (!pollResult.ok || pollResult.payload.errorId !== 0) {
364
+ throw unavailableForPayload(pollResult.payload, phase);
365
+ }
366
+ if (pollResult.payload.status === "processing") continue;
367
+ if (pollResult.payload.status !== "ready") {
368
+ throw new ResolverVendorUnavailableError(
369
+ TWOCAPTCHA_VENDOR_ID,
370
+ "transport_failure",
371
+ { phase },
372
+ );
373
+ }
374
+
375
+ const token = tokenFrom(pollResult.payload);
376
+ if (!token?.trim()) {
377
+ throw new ResolverVendorUnavailableError(
378
+ TWOCAPTCHA_VENDOR_ID,
379
+ "transport_failure",
380
+ { phase },
381
+ );
382
+ }
383
+ return { form: "token" as const, token };
341
384
  }
342
- return { form: "token", token };
343
- }
385
+ };
386
+ return traceRecorder
387
+ ? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
388
+ attributes: {
389
+ vendor: TWOCAPTCHA_VENDOR_ID,
390
+ challenge_kind: challenge.kind,
391
+ },
392
+ onError: (error) => spanErrorAttributes(error, "poll_result"),
393
+ })
394
+ : await pollResult();
344
395
  } catch (error) {
345
396
  if (callerSignal.aborted) throw abortReason(callerSignal);
346
397
  if (
@@ -112,6 +112,7 @@ export interface ResolverVendorAdapter {
112
112
 
113
113
  export type ResolverVendorUnavailableReason =
114
114
  | "missing_credentials"
115
+ | "missing_proxy_identity"
115
116
  | "missing_transport"
116
117
  | "allocation_exhausted"
117
118
  | "transport_failure"
@@ -6,6 +6,7 @@ import type {
6
6
  ProviderCache,
7
7
  ProviderChallenge,
8
8
  ProviderChallengeKind,
9
+ ProviderProxyMode,
9
10
  ProviderResolverConfig,
10
11
  ProviderResolverVendor,
11
12
  ResolverContext,
@@ -29,14 +30,32 @@ import {
29
30
  type ResolverVendorUnavailableReason,
30
31
  resolverVendorSupports,
31
32
  } from "./resolver-vendors/types.js";
33
+ import {
34
+ createUnsupportedResolverClient,
35
+ RESOLVER_INSTRUMENTATION_METADATA,
36
+ } from "./resolver-shared.js";
37
+ import {
38
+ APIFUSE__CDP_POOL__URL,
39
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
40
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
41
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
42
+ APIFUSE__RESOLVER__TIMEOUT_MS,
43
+ DEFAULT_RESOLVER_TIMEOUT_MS,
44
+ } from "./resolver-config.js";
32
45
  import type { TraceRecorder } from "./trace.js";
33
46
 
34
- export const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
35
- export const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
36
- export const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
37
- export const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
38
- export const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
39
- export const DEFAULT_RESOLVER_TIMEOUT_MS = 180_000;
47
+ export {
48
+ createUnsupportedResolverClient,
49
+ RESOLVER_INSTRUMENTATION_METADATA,
50
+ } from "./resolver-shared.js";
51
+ export {
52
+ APIFUSE__CDP_POOL__URL,
53
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
54
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
55
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
56
+ APIFUSE__RESOLVER__TIMEOUT_MS,
57
+ DEFAULT_RESOLVER_TIMEOUT_MS,
58
+ } from "./resolver-config.js";
40
59
 
41
60
  type EnvLike = Record<string, string | undefined>;
42
61
 
@@ -75,6 +94,8 @@ type ResolverChainClient = ResolverContext & {
75
94
  export interface ResolverRuntimeOptions {
76
95
  readonly allowedHosts?: readonly string[];
77
96
  readonly cache?: ProviderCache;
97
+ /** Provider-declared proxy intent. The SDK never accepts a caller-built identity. */
98
+ readonly proxyMode?: ProviderProxyMode;
78
99
  /** Server-owned context/proxy scope used only for identity-bound cache entries. */
79
100
  readonly identityScope?: string;
80
101
  /** SDK-owned transport already bound to the resolved proxy lease and client profile. */
@@ -143,10 +164,6 @@ const SAFE_CAUSE_MESSAGE_WORDS: ReadonlySet<string> = new Set([
143
164
  "writing",
144
165
  ]);
145
166
 
146
- export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
147
- "@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
148
- );
149
-
150
167
  export type ResolverInstrumentationMetadata = {
151
168
  readonly target: ResolverContext;
152
169
  readonly traceRecorder: TraceRecorder;
@@ -340,7 +357,9 @@ function sanitizeCauseMessage(message: string): string {
340
357
  .map((token) => {
341
358
  if (token === "[REDACTED]" || /^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token)) return token;
342
359
  const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
343
- return word.length > 0 && word.length <= 32 && SAFE_CAUSE_MESSAGE_WORDS.has(word.toLowerCase())
360
+ return word.length > 0 &&
361
+ word.length <= 32 &&
362
+ SAFE_CAUSE_MESSAGE_WORDS.has(word.toLowerCase())
344
363
  ? token
345
364
  : "[REDACTED]";
346
365
  });
@@ -691,6 +710,7 @@ function createResolverChainClient(options: {
691
710
  readonly unavailableReason?: string;
692
711
  readonly cache?: ProviderCache;
693
712
  readonly identity?: ResolverIdentity;
713
+ readonly proxyMode?: ProviderProxyMode;
694
714
  readonly identityScope?: string;
695
715
  readonly transport?: ResolverVendorTransport;
696
716
  readonly createTransport?: ResolverRuntimeOptions["createTransport"];
@@ -715,6 +735,19 @@ function createResolverChainClient(options: {
715
735
  const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
716
736
  if (supportingEntries.length === 0) throwUnsupportedKind(challenge.kind);
717
737
  signal.throwIfAborted();
738
+ // A required proxy policy is checked before the cache. Solutions minted under a
739
+ // previous release are shared and long-lived, so consulting the cache first would
740
+ // keep reporting success without a proxy identity until every old entry expired.
741
+ const requiredProxyIdentityMissing =
742
+ options.proxyMode === "required" && options.identity === undefined;
743
+ if (requiredProxyIdentityMissing) {
744
+ throwExhausted(
745
+ supportingEntries.map((entry) => ({
746
+ vendor: entry.id,
747
+ reason: "missing_proxy_identity" as const,
748
+ })),
749
+ );
750
+ }
718
751
  if (options.cache && resolverChallengeIsCacheable(challenge)) {
719
752
  const cached = await findCachedSolution(
720
753
  options.cache,
@@ -804,6 +837,7 @@ export function createResolverClient(options: {
804
837
  readonly unavailableReason?: string;
805
838
  readonly cache?: ProviderCache;
806
839
  readonly identity?: ResolverIdentity;
840
+ readonly proxyMode?: ProviderProxyMode;
807
841
  readonly transport?: ResolverVendorTransport;
808
842
  readonly createTransport?: ResolverRuntimeOptions["createTransport"];
809
843
  readonly clientProfile?: string;
@@ -819,6 +853,7 @@ export function createResolverClient(options: {
819
853
  unavailableReason: options.unavailableReason,
820
854
  cache: options.cache,
821
855
  identity: options.identity,
856
+ proxyMode: options.proxyMode,
822
857
  transport: options.transport,
823
858
  createTransport: options.createTransport,
824
859
  clientProfile: options.clientProfile,
@@ -853,17 +888,6 @@ function resolveVendorAvailability(
853
888
  : { vendor, available: false, reason: "missing_credentials" };
854
889
  }
855
890
 
856
- export function createUnsupportedResolverClient(reason?: string): ResolverContext {
857
- return {
858
- async solve() {
859
- throw new ProviderError(reason ?? "Resolver runtime is not configured", {
860
- code: "RESOLVER_UNAVAILABLE",
861
- fix: "Declare resolver on the provider definition and configure vendor credentials.",
862
- });
863
- },
864
- };
865
- }
866
-
867
891
  export function bindResolverSignal(
868
892
  resolver: ResolverContext,
869
893
  defaultSignal: AbortSignal | undefined,
@@ -921,6 +945,7 @@ function createResolverClientFromEnvInternal(
921
945
  };
922
946
  }),
923
947
  cache: options.cache,
948
+ proxyMode: options.proxyMode,
924
949
  identityScope: options.identityScope,
925
950
  transport: options.transport,
926
951
  createTransport: options.createTransport,