@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
package/src/contract.ts CHANGED
@@ -7,11 +7,7 @@ import {
7
7
  type JsonValue,
8
8
  toJsonValue,
9
9
  } from "./contract-json.js";
10
- import {
11
- describeSchema,
12
- OutputTextTrustProjectionError,
13
- serializeSmsMatcher,
14
- } from "./contract-serialization.js";
10
+ import { describeSchema, serializeSmsMatcher } from "./contract-serialization.js";
15
11
  import {
16
12
  PROVIDER_CONTRACT_SCHEMA_VERSION,
17
13
  type ProviderContractOperation,
@@ -34,7 +30,6 @@ export {
34
30
  type ProviderContractOperation,
35
31
  type ProviderContractSnapshot,
36
32
  };
37
- export { OutputTextTrustProjectionError };
38
33
 
39
34
  export function extractProviderContract(provider: ProviderDefinition): ProviderContractSnapshot {
40
35
  const auth = extractAuth(provider.auth);
@@ -99,7 +94,7 @@ function extractOperation(
99
94
  const relatedOperations = toJsonValue(operation.relatedOperations);
100
95
  const toolRouter = toJsonValue(operation.toolRouter);
101
96
  const observability = toJsonValue(operation.observability);
102
- const transport = extractTransport(operation.transport, operationId);
97
+ const transport = extractTransport(operation.transport);
103
98
  const fixtures = toJsonValue(operation.fixtures);
104
99
  const upstream = toJsonValue(operation.upstream);
105
100
  const hints = toJsonValue(operation.hints);
@@ -109,7 +104,7 @@ function extractOperation(
109
104
  return {
110
105
  id: operationId,
111
106
  inputSchema: describeSchema(operation.input),
112
- outputSchema: describeSchema(operation.output, { operationId, outputTextTrust: true }),
107
+ outputSchema: describeSchema(operation.output),
113
108
  ...(descriptionKey === undefined ? {} : { descriptionKey }),
114
109
  ...(docs === undefined ? {} : { docs }),
115
110
  ...(whenToUseKeys === undefined ? {} : { whenToUseKeys }),
@@ -146,10 +141,7 @@ function extractAuth(value: ProviderDefinition["auth"]): JsonValue | undefined {
146
141
  });
147
142
  }
148
143
 
149
- function extractTransport(
150
- value: OperationTransport | undefined,
151
- operationId: string,
152
- ): JsonValue | undefined {
144
+ function extractTransport(value: OperationTransport | undefined): JsonValue | undefined {
153
145
  if (!value) return undefined;
154
146
  if (value.kind !== "sse") return toJsonValue(value);
155
147
  return compactObject({
@@ -157,10 +149,7 @@ function extractTransport(
157
149
  events: Object.fromEntries(
158
150
  Object.entries(value.events)
159
151
  .sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
160
- .map(([eventName, schema]) => [
161
- eventName,
162
- describeSchema(schema, { eventName, operationId, outputTextTrust: true }),
163
- ]),
152
+ .map(([eventName, schema]) => [eventName, describeSchema(schema)]),
164
153
  ),
165
154
  });
166
155
  }
package/src/index.ts CHANGED
@@ -20,7 +20,6 @@ export {
20
20
  extractProviderContract,
21
21
  type JsonPrimitive,
22
22
  type JsonValue,
23
- OutputTextTrustProjectionError,
24
23
  PROVIDER_CONTRACT_SCHEMA_VERSION,
25
24
  type ProviderContractOperation,
26
25
  type ProviderContractSnapshot,
@@ -51,7 +50,6 @@ export * from "./recipes/gov-api.js";
51
50
  export * from "./recipes/rest-api.js";
52
51
  export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
53
52
  export type { BrowserClientOptions } from "./runtime/browser.js";
54
- export { BrowserClient, createBrowserClient } from "./runtime/browser.js";
55
53
  export {
56
54
  APIFUSE__CACHE__KEY_PEPPER_ENV,
57
55
  createBypassProviderCache,
@@ -73,25 +71,23 @@ export { createEnvContext } from "./runtime/env.js";
73
71
  export { executeOperation } from "./runtime/executor.js";
74
72
  export { createHttpClient } from "./runtime/http.js";
75
73
  export {
76
- createNativeNetworkClient,
77
- createEnvVendorCredentialResolver,
78
- deriveNativeCredentialAffinityKey,
79
74
  NativeEgressGrantExpiredError,
80
75
  NativeEgressNotDeclaredError,
81
76
  NativeIdleTimeoutError,
82
77
  NativeNetworkError,
83
78
  NativeProxyExpiredError,
84
- resolveNativeGatewayProxy,
85
- type NativeGatewayProxy,
86
- type NativeGatewayProxyResolutionInput,
87
- type NativeGatewayProxySkipReason,
88
- type NativeGatewayProxySynthesizer,
89
- type NativeGatewayProxySynthesisResult,
90
- type NativeGatewayProxySynthesisInput,
91
- type NativeNetworkClientOptions,
92
- type NativeNetworkErrorCode,
93
- type VendorCredentialLookup,
94
- type VendorCredentialResolver,
79
+ } from "./runtime/native-network-errors.js";
80
+ export type {
81
+ NativeGatewayProxy,
82
+ NativeGatewayProxyResolutionInput,
83
+ NativeGatewayProxySkipReason,
84
+ NativeGatewayProxySynthesizer,
85
+ NativeGatewayProxySynthesisResult,
86
+ NativeGatewayProxySynthesisInput,
87
+ NativeNetworkClientOptions,
88
+ NativeNetworkErrorCode,
89
+ VendorCredentialLookup,
90
+ VendorCredentialResolver,
95
91
  } from "./runtime/native-network.js";
96
92
  export type { Insight, InsightSeverity } from "./runtime/insights.js";
97
93
  export { generateInsights } from "./runtime/insights.js";
@@ -100,7 +96,7 @@ export {
100
96
  type InstrumentedProviderContext,
101
97
  wrapWithInstrumentation,
102
98
  } from "./runtime/instrumentation.js";
103
- export { type PrevalidateResult, prevalidate } from "./runtime/prevalidate.js";
99
+ export type { PrevalidateResult } from "./runtime/prevalidate.js";
104
100
  export { getProviderBaseUrl } from "./runtime/provider.js";
105
101
  export {
106
102
  APIFUSE__CDP_POOL__URL,
@@ -108,12 +104,10 @@ export {
108
104
  APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
109
105
  APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
110
106
  APIFUSE__RESOLVER__TIMEOUT_MS,
111
- createResolverClientFromEnv,
112
- createUnsupportedResolverClient,
113
107
  DEFAULT_RESOLVER_TIMEOUT_MS,
114
- invalidateResolverSolution,
115
- type ResolverRuntimeOptions,
116
- } from "./runtime/resolver.js";
108
+ } from "./runtime/resolver-config.js";
109
+ export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
110
+ export type { ResolverRuntimeOptions } from "./runtime/resolver.js";
117
111
  export type { ResolverVendorTransport } from "./runtime/resolver-vendors/types.js";
118
112
  export {
119
113
  assertRequiredSecretsPresent,
@@ -124,7 +118,6 @@ export {
124
118
  createUnsupportedProviderRuntimeState,
125
119
  UnsupportedProviderStateError,
126
120
  } from "./runtime/state.js";
127
- export { createStealthClient } from "./runtime/stealth.js";
128
121
  export {
129
122
  APIFUSE__OCR__API_KEY_ENV,
130
123
  APIFUSE__OCR__BACKEND_ENV,
@@ -157,38 +150,29 @@ export {
157
150
  type TraceContext,
158
151
  } from "./runtime/trace.js";
159
152
  export {
160
- APIFUSE_TEXT_TRUST_META_KEY,
161
153
  APIFUSE_DESCRIPTION_KEY_META_KEY,
162
154
  APIFUSE_REDACTION_MARKER,
163
155
  APIFUSE_SENSITIVE_KIND_META_KEY,
164
156
  APIFUSE_SENSITIVE_META_KEY,
165
- AUTO_TRUSTED_ZOD_STRING_FORMATS,
166
- collectOutputTextTrust,
167
157
  collectSensitivePaths,
168
158
  describeKey,
169
159
  field,
170
160
  fields,
171
- findUnclassifiedOutputTextPaths,
172
161
  isSensitiveSchema,
173
- OutputTextTrustCollectionError,
174
- OutputTextTrustSchemaError,
175
162
  redactPayload,
176
- type OutputTextTrustMap,
177
163
  type SensitiveFieldKind,
178
164
  type SensitiveFieldOptions,
179
165
  type SensitivePath,
180
166
  sensitive,
181
- textTrust,
182
- type TextTrust,
183
- type TextTrustMetadata,
184
167
  z,
185
168
  } from "./schema.js";
186
169
  export {
187
170
  createServerApp,
171
+ createServerAppAsync,
188
172
  ERROR_OBSERVABILITY_HEADER,
189
173
  type ServeOptions,
190
174
  serve,
191
- } from "./server/index.js";
175
+ } from "./server/serve.js";
192
176
  export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
193
177
  export * from "./stream.js";
194
178
  export type {
package/src/provider.ts CHANGED
@@ -58,30 +58,20 @@ export {
58
58
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
59
59
  } from "./runtime/choice.js";
60
60
  export {
61
- APIFUSE_TEXT_TRUST_META_KEY,
62
61
  APIFUSE_DESCRIPTION_KEY_META_KEY,
63
62
  APIFUSE_REDACTION_MARKER,
64
63
  APIFUSE_SENSITIVE_KIND_META_KEY,
65
64
  APIFUSE_SENSITIVE_META_KEY,
66
- AUTO_TRUSTED_ZOD_STRING_FORMATS,
67
- collectOutputTextTrust,
68
65
  collectSensitivePaths,
69
66
  describeKey,
70
67
  field,
71
68
  fields,
72
- findUnclassifiedOutputTextPaths,
73
69
  isSensitiveSchema,
74
- OutputTextTrustCollectionError,
75
- OutputTextTrustSchemaError,
76
70
  redactPayload,
77
- type OutputTextTrustMap,
78
71
  type SensitiveFieldKind,
79
72
  type SensitiveFieldOptions,
80
73
  type SensitivePath,
81
74
  sensitive,
82
- textTrust,
83
- type TextTrust,
84
- type TextTrustMetadata,
85
75
  z,
86
76
  } from "./schema.js";
87
77
  export type {
@@ -177,25 +167,23 @@ export type {
177
167
  StateWriteOptions,
178
168
  } from "./types.js";
179
169
  export {
180
- createNativeNetworkClient,
181
- createEnvVendorCredentialResolver,
182
- deriveNativeCredentialAffinityKey,
183
170
  NativeEgressGrantExpiredError,
184
171
  NativeEgressNotDeclaredError,
185
172
  NativeIdleTimeoutError,
186
173
  NativeNetworkError,
187
174
  NativeProxyExpiredError,
188
- resolveNativeGatewayProxy,
189
- type NativeGatewayProxy,
190
- type NativeGatewayProxyResolutionInput,
191
- type NativeGatewayProxySkipReason,
192
- type NativeGatewayProxySynthesizer,
193
- type NativeGatewayProxySynthesisResult,
194
- type NativeGatewayProxySynthesisInput,
195
- type NativeNetworkClientOptions,
196
- type NativeNetworkErrorCode,
197
- type VendorCredentialLookup,
198
- type VendorCredentialResolver,
175
+ } from "./runtime/native-network-errors.js";
176
+ export type {
177
+ NativeGatewayProxy,
178
+ NativeGatewayProxyResolutionInput,
179
+ NativeGatewayProxySkipReason,
180
+ NativeGatewayProxySynthesizer,
181
+ NativeGatewayProxySynthesisResult,
182
+ NativeGatewayProxySynthesisInput,
183
+ NativeNetworkClientOptions,
184
+ NativeNetworkErrorCode,
185
+ VendorCredentialLookup,
186
+ VendorCredentialResolver,
199
187
  } from "./runtime/native-network.js";
200
188
  export {
201
189
  HttpRetryAfterPolicy,
@@ -10,7 +10,7 @@ import type {
10
10
  SttContext,
11
11
  } from "../types.js";
12
12
  import { createUnsupportedOcrClient } from "./ocr.js";
13
- import { createUnsupportedResolverClient } from "./resolver.js";
13
+ import { createUnsupportedResolverClient } from "./resolver-shared.js";
14
14
  import { createUnsupportedSttClient } from "./stt.js";
15
15
 
16
16
  function normalizeAllowedKeys(allowedKeys: string[]): Set<string> {
@@ -44,6 +44,7 @@ export type HttpClientOptions = ProxyResolutionOptions & {
44
44
  warn?: (message: string) => void;
45
45
  userAgent?: string;
46
46
  onRetrySummary?: (summary: HttpRetrySummary) => void;
47
+ signal?: AbortSignal;
47
48
  };
48
49
 
49
50
  type HttpStatusOutcome = {
@@ -76,9 +77,45 @@ function isDedupeSkipOutcome(outcome: NativeHttpAttemptOutcome): outcome is Nati
76
77
  return "kind" in outcome && outcome.kind === "dedupe-skip";
77
78
  }
78
79
 
79
- async function sleep(ms: number): Promise<void> {
80
+ function toAmbientCancellationError(
81
+ signal: AbortSignal,
82
+ error: unknown = signal.reason,
83
+ ): TransportError {
84
+ if (error instanceof TransportError && error.code === "transport_cancelled") {
85
+ return error;
86
+ }
87
+ return new TransportError("Request cancelled", {
88
+ code: "transport_cancelled",
89
+ status: 0,
90
+ retryable: false,
91
+ ...(error !== undefined
92
+ ? { cause: error instanceof Error ? error : new Error(String(error)) }
93
+ : {}),
94
+ });
95
+ }
96
+
97
+ function throwIfAmbientAborted(signal: AbortSignal | undefined): void {
98
+ if (signal?.aborted) throw toAmbientCancellationError(signal);
99
+ }
100
+
101
+ async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
102
+ throwIfAmbientAborted(signal);
80
103
  if (ms <= 0) return;
81
- await new Promise((resolve) => setTimeout(resolve, ms));
104
+ if (!signal) {
105
+ await new Promise((resolve) => setTimeout(resolve, ms));
106
+ return;
107
+ }
108
+ await new Promise<void>((resolve, reject) => {
109
+ const onAbort = () => {
110
+ clearTimeout(timer);
111
+ reject(toAmbientCancellationError(signal));
112
+ };
113
+ const timer = setTimeout(() => {
114
+ signal.removeEventListener("abort", onAbort);
115
+ resolve();
116
+ }, ms);
117
+ signal.addEventListener("abort", onAbort, { once: true });
118
+ });
82
119
  }
83
120
 
84
121
  function toUpstreamHttpError(status: number): TransportError {
@@ -132,7 +169,21 @@ function isTimeoutMessage(message: string): boolean {
132
169
  return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
133
170
  }
134
171
 
135
- function toHttpTransportError(error: unknown): TransportError {
172
+ function toHttpTransportError(
173
+ error: unknown,
174
+ ambientSignal?: AbortSignal,
175
+ timeoutSignal?: AbortSignal,
176
+ ): TransportError {
177
+ if (ambientSignal?.aborted) {
178
+ return toAmbientCancellationError(ambientSignal, error);
179
+ }
180
+ if (timeoutSignal?.aborted) {
181
+ return new TransportError("Request timed out", {
182
+ code: "transport_timeout",
183
+ status: 0,
184
+ ...(error instanceof Error ? { cause: error } : {}),
185
+ });
186
+ }
136
187
  if (error instanceof TransportError) {
137
188
  if (error.code) {
138
189
  return error;
@@ -216,6 +267,78 @@ function requireNativeResponseBody(response: Response): ReadableStream<Uint8Arra
216
267
  return response.body;
217
268
  }
218
269
 
270
+ function mergeAbortSignals(
271
+ ...signals: Array<AbortSignal | null | undefined>
272
+ ): AbortSignal | undefined {
273
+ const activeSignals = signals.filter((signal): signal is AbortSignal => signal != null);
274
+ if (activeSignals.length === 0) return undefined;
275
+ if (activeSignals.length === 1) return activeSignals[0];
276
+ return AbortSignal.any(activeSignals);
277
+ }
278
+
279
+ function cancelStreamOnAbort(
280
+ body: ReadableStream<Uint8Array>,
281
+ signal: AbortSignal | undefined,
282
+ ): ReadableStream<Uint8Array> {
283
+ if (!signal) return body;
284
+
285
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
286
+ let finished = false;
287
+ let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
288
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
289
+ const onAbort = () => {
290
+ if (finished) return;
291
+ finished = true;
292
+ cleanup();
293
+ const reason = toAmbientCancellationError(signal);
294
+ streamController?.error(reason);
295
+ const cancellation = reader ? reader.cancel(reason) : body.cancel(reason);
296
+ void cancellation.catch(() => undefined).finally(() => reader?.releaseLock());
297
+ };
298
+
299
+ return new ReadableStream<Uint8Array>(
300
+ {
301
+ start(controller) {
302
+ streamController = controller;
303
+ signal.addEventListener("abort", onAbort, { once: true });
304
+ if (signal.aborted) onAbort();
305
+ },
306
+ async pull(controller) {
307
+ try {
308
+ reader ??= body.getReader();
309
+ const chunk = await reader.read();
310
+ if (finished) return;
311
+ if (chunk.done) {
312
+ finished = true;
313
+ cleanup();
314
+ controller.close();
315
+ reader.releaseLock();
316
+ return;
317
+ }
318
+ controller.enqueue(chunk.value);
319
+ } catch (error) {
320
+ if (finished) return;
321
+ finished = true;
322
+ cleanup();
323
+ controller.error(error);
324
+ reader?.releaseLock();
325
+ }
326
+ },
327
+ async cancel(reason) {
328
+ if (finished) return;
329
+ finished = true;
330
+ cleanup();
331
+ try {
332
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
333
+ } finally {
334
+ reader?.releaseLock();
335
+ }
336
+ },
337
+ },
338
+ { highWaterMark: 0 },
339
+ );
340
+ }
341
+
219
342
  function sanitizeStreamErrors(
220
343
  body: ReadableStream<Uint8Array>,
221
344
  serializedUrl: SerializedRequestUrl,
@@ -265,9 +388,13 @@ function sanitizeStreamErrors(
265
388
  function toNativeHttpStreamResponse(
266
389
  response: Response,
267
390
  serializedUrl: SerializedRequestUrl,
391
+ signal?: AbortSignal,
268
392
  ): HttpStreamResponse {
269
393
  const headers = Object.fromEntries(response.headers.entries());
270
- const body = sanitizeStreamErrors(requireNativeResponseBody(response), serializedUrl);
394
+ const body = sanitizeStreamErrors(
395
+ cancelStreamOnAbort(requireNativeResponseBody(response), signal),
396
+ serializedUrl,
397
+ );
271
398
  return {
272
399
  body,
273
400
  headers,
@@ -585,16 +712,19 @@ async function fetchNativeHttp(
585
712
  const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
586
713
  const { requestUrl } = serializedUrl;
587
714
  const controller = options.timeout ? new AbortController() : undefined;
715
+ const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
588
716
  const timeoutHandle = options.timeout
589
717
  ? setTimeout(() => controller?.abort(), options.timeout)
590
718
  : undefined;
591
719
 
592
720
  let proxy: string | undefined;
593
721
  try {
722
+ throwIfAmbientAborted(clientOptions.signal);
594
723
  // Resolve inside the try (and after the timeout is armed) so allocator
595
724
  // failures are branded as TransportErrors and count against the request
596
725
  // deadline, exactly as an inline resolve would.
597
726
  proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
727
+ throwIfAmbientAborted(clientOptions.signal);
598
728
  // For a registry allocator chain, skip an endpoint a prior attempt already
599
729
  // tried rather than re-issuing the same request. Returning the sentinel
600
730
  // (instead of breaking) lets the loop keep advancing the flat offset until
@@ -609,7 +739,7 @@ async function fetchNativeHttp(
609
739
  headers: options.headers,
610
740
  method,
611
741
  ...(proxy ? { proxy } : {}),
612
- signal: controller?.signal,
742
+ ...(signal ? { signal } : {}),
613
743
  };
614
744
  if (options.body !== undefined) {
615
745
  requestInit.body = normalizeNativeFetchBody(options.body);
@@ -651,7 +781,7 @@ async function fetchNativeHttp(
651
781
  );
652
782
  }
653
783
  const transportError: NativeHttpAttemptError = redactSensitiveError(
654
- toHttpTransportError(error),
784
+ toHttpTransportError(error, clientOptions.signal, controller?.signal),
655
785
  serializedUrl.sensitiveValues,
656
786
  serializedUrl.requestUrl,
657
787
  serializedUrl.redactedUrl,
@@ -674,17 +804,20 @@ async function fetchNativeHttpStream(
674
804
  const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
675
805
  const { requestUrl } = serializedUrl;
676
806
  const controller = options.timeout ? new AbortController() : undefined;
807
+ const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
677
808
  const timeoutHandle = options.timeout
678
809
  ? setTimeout(() => controller?.abort(), options.timeout)
679
810
  : undefined;
680
811
 
681
812
  try {
813
+ throwIfAmbientAborted(clientOptions.signal);
682
814
  const proxy = await resolveNativeProxy(options, clientOptions, warn);
815
+ throwIfAmbientAborted(clientOptions.signal);
683
816
  const requestInit: NativeFetchInit = {
684
817
  headers: options.headers,
685
818
  method,
686
819
  ...(proxy ? { proxy } : {}),
687
- signal: controller?.signal,
820
+ ...(signal ? { signal } : {}),
688
821
  };
689
822
  if (options.body !== undefined) {
690
823
  requestInit.body = normalizeNativeFetchBody(options.body);
@@ -703,7 +836,9 @@ async function fetchNativeHttpStream(
703
836
  });
704
837
  }
705
838
 
706
- return toNativeHttpStreamResponse(response, serializedUrl);
839
+ // Per-call timeout remains header-scoped, while the ambient request signal
840
+ // stays attached to the response body for its full consumption lifetime.
841
+ return toNativeHttpStreamResponse(response, serializedUrl, clientOptions.signal);
707
842
  } catch (error) {
708
843
  if (error instanceof SyntaxError) {
709
844
  throw redactSensitiveError(
@@ -714,7 +849,7 @@ async function fetchNativeHttpStream(
714
849
  );
715
850
  }
716
851
  throw redactSensitiveError(
717
- toHttpTransportError(error),
852
+ toHttpTransportError(error, clientOptions.signal, controller?.signal),
718
853
  serializedUrl.sensitiveValues,
719
854
  serializedUrl.requestUrl,
720
855
  serializedUrl.redactedUrl,
@@ -850,6 +985,7 @@ export function createHttpClient(
850
985
  );
851
986
 
852
987
  if (!retryEnabled || !retryOptions) {
988
+ throwIfAmbientAborted(clientOptions.signal);
853
989
  const outcome = await executeOnce();
854
990
  if (isDedupeSkipOutcome(outcome)) {
855
991
  // Single-shot path never de-duplicates (dedupeContext is undefined),
@@ -874,6 +1010,7 @@ export function createHttpClient(
874
1010
  // allocations skip offsets.
875
1011
  let issued = 0;
876
1012
  for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
1013
+ throwIfAmbientAborted(clientOptions.signal);
877
1014
  // Whether this offset actually issued a request (vs. a skipped duplicate),
878
1015
  // so the catch counts a thrown *transport* failure once without
879
1016
  // double-counting a status outcome that already incremented before it
@@ -892,7 +1029,10 @@ export function createHttpClient(
892
1029
  if (isHttpStatusOutcome(outcome)) {
893
1030
  lastStatus = outcome.status;
894
1031
  if (outcome.retryable && issued < retryOptions.attempts) {
895
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
1032
+ await sleep(
1033
+ computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers),
1034
+ clientOptions.signal,
1035
+ );
896
1036
  continue;
897
1037
  }
898
1038
  throw toUpstreamHttpError(outcome.status);
@@ -916,6 +1056,7 @@ export function createHttpClient(
916
1056
  }
917
1057
  return response;
918
1058
  } catch (error) {
1059
+ throwIfAmbientAborted(clientOptions.signal);
919
1060
  if (!issuedThisAttempt) issued += 1;
920
1061
  lastError = error;
921
1062
  lastErrorCode = proxyTransportRetryErrorCode(error);
@@ -931,7 +1072,10 @@ export function createHttpClient(
931
1072
  proxyUsed,
932
1073
  })
933
1074
  ) {
934
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
1075
+ await sleep(
1076
+ computeProxyTransportRetryDelayMs(retryOptions, attempt),
1077
+ clientOptions.signal,
1078
+ );
935
1079
  continue;
936
1080
  }
937
1081
  throw error;
@@ -23,7 +23,7 @@ import {
23
23
  getTraceRecorder,
24
24
  type TraceContext,
25
25
  } from "./trace.js";
26
- import { RESOLVER_INSTRUMENTATION_METADATA } from "./resolver.js";
26
+ import { RESOLVER_INSTRUMENTATION_METADATA } from "./resolver-shared.js";
27
27
 
28
28
  export interface InstrumentationOptions extends CreateTraceContextOptions {}
29
29
 
@@ -0,0 +1,99 @@
1
+ import { TransportError } from "../errors.js";
2
+ import { canonicalizeEgressHost } from "../native-address.js";
3
+
4
+ export type NativeNetworkErrorCode =
5
+ | "native_connection_aborted"
6
+ | "native_connection_closed"
7
+ | "native_connection_failed"
8
+ | "native_connection_idle_timeout"
9
+ | "native_connection_timeout"
10
+ | "native_egress_authorization_failed"
11
+ | "native_egress_grant_expired"
12
+ | "native_egress_grant_invalid"
13
+ | "native_egress_grant_limit_exceeded"
14
+ | "native_egress_input_invalid"
15
+ | "native_egress_not_declared"
16
+ | "native_egress_policy_invalid"
17
+ | "native_dynamic_egress_unsupported"
18
+ | "native_proxy_expired"
19
+ | "native_proxy_invalid";
20
+
21
+ export class NativeNetworkError extends TransportError {
22
+ constructor(message: string, code: NativeNetworkErrorCode, cause?: Error) {
23
+ const isEgressPolicyFailure =
24
+ code.startsWith("native_egress_") || code === "native_dynamic_egress_unsupported";
25
+ super(message, {
26
+ code,
27
+ status: 0,
28
+ ...(cause ? { cause } : {}),
29
+ ...(isEgressPolicyFailure ? { category: "provider_error" as const, retryable: false } : {}),
30
+ });
31
+ this.name = "NativeNetworkError";
32
+ }
33
+
34
+ override get code(): NativeNetworkErrorCode {
35
+ return super.code as NativeNetworkErrorCode;
36
+ }
37
+ }
38
+
39
+ export function safeDiagnosticEgressHost(value: unknown): string {
40
+ const canonical = canonicalizeEgressHost(value);
41
+ return canonical.ok ? canonical.host : `<invalid-host:${canonical.reason}>`;
42
+ }
43
+
44
+ export class NativeProxyExpiredError extends NativeNetworkError {
45
+ constructor(readonly expiresAt: string) {
46
+ super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
47
+ this.name = "NativeProxyExpiredError";
48
+ }
49
+ }
50
+
51
+ /** Raised before transport setup when a native destination is not authorized. */
52
+ export class NativeEgressNotDeclaredError extends NativeNetworkError {
53
+ readonly host: string;
54
+
55
+ constructor(
56
+ host: string,
57
+ readonly port: number,
58
+ readonly tls: "required" | "disabled",
59
+ ) {
60
+ const diagnosticHost = safeDiagnosticEgressHost(host);
61
+ super(
62
+ `Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${diagnosticHost}:${port}`,
63
+ "native_egress_not_declared",
64
+ );
65
+ this.host = diagnosticHost;
66
+ this.name = "NativeEgressNotDeclaredError";
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Raised when the destination was authorized by a grant whose TTL elapsed and
72
+ * its expiry remains in the client's bounded recent-expiry evidence window.
73
+ */
74
+ export class NativeEgressGrantExpiredError extends NativeNetworkError {
75
+ readonly host: string;
76
+
77
+ constructor(
78
+ host: string,
79
+ readonly port: number,
80
+ readonly tls: "required" | "disabled",
81
+ readonly expiresAt: string,
82
+ ) {
83
+ const diagnosticHost = safeDiagnosticEgressHost(host);
84
+ super(
85
+ `Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${diagnosticHost}:${port}`,
86
+ "native_egress_grant_expired",
87
+ );
88
+ this.host = diagnosticHost;
89
+ this.name = "NativeEgressGrantExpiredError";
90
+ }
91
+ }
92
+
93
+ /** Raised when an established connection exceeds its opt-in read-idle window. */
94
+ export class NativeIdleTimeoutError extends NativeNetworkError {
95
+ constructor() {
96
+ super("Native network socket timed out while reading.", "native_connection_idle_timeout");
97
+ this.name = "NativeIdleTimeoutError";
98
+ }
99
+ }