@apifuse/provider-sdk 2.2.0-beta.60 → 2.2.0-beta.61

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 (73) hide show
  1. package/AUTHORING.md +17 -16
  2. package/CHANGELOG.md +4 -0
  3. package/bin/apifuse-dev.ts +5 -7
  4. package/bin/apifuse-pack-types.ts +22 -0
  5. package/bin/apifuse-perf.ts +4 -6
  6. package/bin/apifuse-record.ts +4 -7
  7. package/bin/apifuse-submit-check.ts +2 -2
  8. package/dist/define.d.ts +3 -2
  9. package/dist/define.js +1 -1
  10. package/dist/dev.js +3 -1
  11. package/dist/engine.d.ts +7 -6
  12. package/dist/engine.js +3 -4
  13. package/dist/error-resolution.js +21 -4
  14. package/dist/errors.d.ts +3 -0
  15. package/dist/errors.js +9 -0
  16. package/dist/handle-meta.d.ts +39 -0
  17. package/dist/handle-meta.js +29 -0
  18. package/dist/handle.d.ts +187 -0
  19. package/dist/handle.js +254 -0
  20. package/dist/i18n/catalog.js +21 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +3 -2
  23. package/dist/lint.d.ts +3 -0
  24. package/dist/lint.js +352 -47
  25. package/dist/provider.d.ts +3 -3
  26. package/dist/provider.js +2 -2
  27. package/dist/runtime/handle-wordlist.d.ts +43 -0
  28. package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
  29. package/dist/runtime/handle.d.ts +71 -0
  30. package/dist/runtime/handle.js +895 -0
  31. package/dist/runtime/native-network.d.ts +10 -1
  32. package/dist/runtime/native-network.js +230 -29
  33. package/dist/runtime/native-telemetry.d.ts +142 -0
  34. package/dist/runtime/native-telemetry.js +231 -0
  35. package/dist/runtime/state.d.ts +4 -1
  36. package/dist/runtime/state.js +32 -17
  37. package/dist/server/index.d.ts +1 -0
  38. package/dist/server/index.js +1 -0
  39. package/dist/server/sensitive-values.d.ts +1 -1
  40. package/dist/server/sensitive-values.js +0 -3
  41. package/dist/server/serve-implementation.d.ts +4 -11
  42. package/dist/server/serve-implementation.js +86 -18
  43. package/dist/testing/run.js +10 -7
  44. package/dist/types.d.ts +4 -107
  45. package/package.json +1 -1
  46. package/src/define.ts +4 -3
  47. package/src/dev.ts +3 -1
  48. package/src/engine.ts +5 -6
  49. package/src/error-resolution.ts +21 -4
  50. package/src/errors.ts +13 -0
  51. package/src/handle-meta.ts +58 -0
  52. package/src/handle.ts +529 -0
  53. package/src/i18n/catalog.ts +23 -1
  54. package/src/index.ts +26 -13
  55. package/src/lint.ts +414 -55
  56. package/src/provider.ts +9 -20
  57. package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
  58. package/src/runtime/handle.ts +1162 -0
  59. package/src/runtime/native-network.ts +281 -33
  60. package/src/runtime/native-telemetry.ts +366 -0
  61. package/src/runtime/state.ts +44 -16
  62. package/src/server/index.ts +17 -0
  63. package/src/server/sensitive-values.ts +0 -3
  64. package/src/server/serve-implementation.ts +87 -32
  65. package/src/testing/run.ts +11 -7
  66. package/src/types.ts +4 -137
  67. package/dist/choice-token.d.ts +0 -24
  68. package/dist/choice-token.js +0 -74
  69. package/dist/runtime/choice-wordlist.d.ts +0 -9
  70. package/dist/runtime/choice.d.ts +0 -27
  71. package/dist/runtime/choice.js +0 -801
  72. package/src/choice-token.ts +0 -165
  73. package/src/runtime/choice.ts +0 -1197
@@ -1,8 +1,9 @@
1
1
  import { Socket } from "node:net";
2
2
  import { type TLSSocket } from "node:tls";
3
- import { type ProxyProtocol } from "../config/loader.js";
3
+ import { type ProxyProtocol, type ProxyTelemetrySink } from "../config/loader.js";
4
4
  import { type DynamicEgressRuleSnapshot } from "../native-egress-policy.js";
5
5
  import type { NativeNetworkClient, NativeNetworkConnectInput, NativeNetworkConnection, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProxyEgressInfo, ProviderProxyPolicy, ProviderProxyProvider, EnvContext } from "../types.js";
6
+ import type { NativeTelemetrySink } from "./native-telemetry.js";
6
7
  export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./native-network-errors.js";
7
8
  export type { NativeNetworkErrorCode } from "./native-network-errors.js";
8
9
  export type NativeGatewayProxy = NativeProxyEgressInfo & {
@@ -15,6 +16,8 @@ export type NativeGatewayProxySynthesisInput = {
15
16
  readonly now: number;
16
17
  readonly protocol: ProxyProtocol;
17
18
  readonly credentials: VendorCredentialResolver;
19
+ /** Existing proxy lease telemetry; adapters may report detailed allocation facts. */
20
+ readonly telemetry?: ProxyTelemetrySink;
18
21
  };
19
22
  export type VendorCredentialLookup = {
20
23
  readonly kind: "present";
@@ -50,8 +53,14 @@ export type NativeGatewayProxyResolutionInput = {
50
53
  readonly protocol?: ProxyProtocol;
51
54
  readonly credentials?: VendorCredentialResolver;
52
55
  readonly gatewaySynthesizers?: readonly NativeGatewayProxySynthesizer[];
56
+ readonly telemetry?: ProxyTelemetrySink;
57
+ readonly nativeTelemetry?: NativeTelemetrySink;
53
58
  };
54
59
  export type NativeNetworkClientOptions = {
60
+ /** Existing proxy lease contributor, shared with HTTP and stealth. */
61
+ readonly telemetry?: ProxyTelemetrySink;
62
+ /** Request-scoped connection, byte and lifecycle observations. */
63
+ readonly nativeTelemetry?: NativeTelemetrySink;
55
64
  readonly proxyPolicy?: ProviderProxyPolicy;
56
65
  readonly affinityKey?: string;
57
66
  /** Stable credential/account identity; hashed before vendor synthesis. */
@@ -101,6 +101,7 @@ async function synthesizeSmartproxyGateway(input) {
101
101
  proxyPolicy: input.policy,
102
102
  affinityKey: input.affinityKey,
103
103
  protocol: input.protocol,
104
+ telemetry: input.telemetry,
104
105
  }, {
105
106
  protocol: input.protocol,
106
107
  poolIndex: 0,
@@ -119,6 +120,9 @@ async function synthesizeSmartproxyGateway(input) {
119
120
  };
120
121
  }
121
122
  catch (error) {
123
+ if (error instanceof ProxyResolutionError && error.telemetry) {
124
+ emitTelemetry(() => input.telemetry?.recordProxyResolution(error.telemetry));
125
+ }
122
126
  const cause = error instanceof Error ? error : new Error(String(error));
123
127
  return skipped({
124
128
  kind: "allocation_failed",
@@ -184,6 +188,59 @@ function defaultVendorProtocol(vendor) {
184
188
  ? VENDOR_DEFAULT_PROTOCOL[vendor]
185
189
  : "http";
186
190
  }
191
+ // Instrumentation must not change the transport's exceptions or vendor ordering.
192
+ function emitTelemetry(emit) {
193
+ try {
194
+ emit();
195
+ }
196
+ catch {
197
+ /* An observer cannot fail an underlying operation. */
198
+ }
199
+ }
200
+ function nativeErrorCode(error) {
201
+ const code = error instanceof NativeNetworkError || error instanceof ProxyResolutionError
202
+ ? error.code
203
+ : undefined;
204
+ switch (code) {
205
+ case "native_connection_aborted":
206
+ case "native_connection_closed":
207
+ case "native_connection_failed":
208
+ case "native_connection_idle_timeout":
209
+ case "native_connection_timeout":
210
+ case "native_egress_authorization_failed":
211
+ case "native_egress_grant_expired":
212
+ case "native_egress_grant_invalid":
213
+ case "native_egress_grant_limit_exceeded":
214
+ case "native_egress_input_invalid":
215
+ case "native_egress_not_declared":
216
+ case "native_egress_policy_invalid":
217
+ case "native_dynamic_egress_unsupported":
218
+ case "native_proxy_expired":
219
+ case "native_proxy_invalid":
220
+ case "PROXY_REQUIRED":
221
+ case "PROXY_ALLOCATION_FAILED":
222
+ return code;
223
+ default:
224
+ return "other";
225
+ }
226
+ }
227
+ function nativeErrorDiagnostics(error) {
228
+ if (!(error instanceof Error))
229
+ return {};
230
+ const cause = error.cause instanceof Error ? error.cause : undefined;
231
+ const detail = cause ?? error;
232
+ const systemCode = Reflect.get(detail, "code");
233
+ const status = Reflect.get(detail, "connectStatusCode");
234
+ const socksReplyCode = Reflect.get(detail, "socks5ReplyCode");
235
+ return {
236
+ errorName: error.name,
237
+ errorMessage: error.message,
238
+ ...(cause ? { causeName: cause.name, causeMessage: cause.message } : {}),
239
+ ...(typeof systemCode === "string" ? { systemCode } : {}),
240
+ ...(typeof status === "number" ? { status } : {}),
241
+ ...(typeof socksReplyCode === "number" ? { socksReplyCode } : {}),
242
+ };
243
+ }
187
244
  async function resolveNativeGatewayProxyDetailed(input) {
188
245
  if (input.policy.mode === "disabled")
189
246
  return { skips: [] };
@@ -191,14 +248,61 @@ async function resolveNativeGatewayProxyDetailed(input) {
191
248
  const credentials = input.credentials ?? createEnvVendorCredentialResolver();
192
249
  const now = input.now ?? Date.now();
193
250
  const skips = [];
194
- for (const vendor of resolveNativeVendorChain(input.policy)) {
251
+ const chain = resolveNativeVendorChain(input.policy);
252
+ const recordSkip = (skip) => {
253
+ skips.push(skip);
254
+ emitTelemetry(() => input.nativeTelemetry?.recordVendorSkip({
255
+ vendor: skip.vendor,
256
+ reason: skip.reason.kind,
257
+ diagnostics: skip.reason.kind === "credentials_absent"
258
+ ? { missingFields: skip.reason.missing }
259
+ : skip.reason.kind === "protocol_unsupported"
260
+ ? { protocol: skip.reason.protocol }
261
+ : skip.reason.kind === "allocation_failed" ||
262
+ skip.reason.kind === "credential_lookup_failed"
263
+ ? nativeErrorDiagnostics(skip.reason.cause)
264
+ : {},
265
+ }));
266
+ if (skip.vendor === "smartproxy" || skip.vendor === "nodemaven") {
267
+ const vendor = skip.vendor;
268
+ const next = chain[chain.indexOf(vendor) + 1];
269
+ emitTelemetry(() => input.telemetry?.recordProxyVendorFailover?.({
270
+ vendor,
271
+ ...(next === "smartproxy" || next === "nodemaven" ? { nextVendor: next } : {}),
272
+ phase: "resolution",
273
+ reason: skip.reason.kind === "credentials_absent"
274
+ ? "no_credentials"
275
+ : skip.reason.kind === "protocol_unsupported"
276
+ ? "protocol_unsupported"
277
+ : "allocation_failed",
278
+ }));
279
+ }
280
+ };
281
+ for (const vendor of chain) {
195
282
  const protocol = input.protocol ?? defaultVendorProtocol(vendor);
196
283
  if (!isProxyProtocol(protocol)) {
197
- skips.push({ vendor, reason: { kind: "protocol_unsupported", protocol: String(protocol) } });
284
+ recordSkip({ vendor, reason: { kind: "protocol_unsupported", protocol: String(protocol) } });
198
285
  continue;
199
286
  }
287
+ let resolutionRecorded = false;
288
+ const telemetry = input.telemetry
289
+ ? {
290
+ recordProxyResolution(event) {
291
+ resolutionRecorded = true;
292
+ emitTelemetry(() => input.telemetry?.recordProxyResolution(event));
293
+ },
294
+ recordProxyAttempt(event) {
295
+ emitTelemetry(() => input.telemetry?.recordProxyAttempt?.(event));
296
+ },
297
+ recordProxyVendorFailover(event) {
298
+ emitTelemetry(() => input.telemetry?.recordProxyVendorFailover?.(event));
299
+ },
300
+ }
301
+ : undefined;
200
302
  let vendorSkip;
201
303
  for (const synthesize of synthesizers) {
304
+ const startedAt = Date.now();
305
+ resolutionRecorded = false;
202
306
  try {
203
307
  const resolved = await synthesize({
204
308
  vendor,
@@ -207,6 +311,7 @@ async function resolveNativeGatewayProxyDetailed(input) {
207
311
  now,
208
312
  protocol,
209
313
  credentials,
314
+ telemetry,
210
315
  });
211
316
  if (!resolved)
212
317
  continue;
@@ -216,6 +321,17 @@ async function resolveNativeGatewayProxyDetailed(input) {
216
321
  }
217
322
  if (resolved.vendor === vendor) {
218
323
  assertTunnelingScheme(resolved.url);
324
+ if (!resolutionRecorded && (vendor === "smartproxy" || vendor === "nodemaven")) {
325
+ const resolvedProtocol = new URL(resolved.url).protocol === "http:" ? "http" : "socks5";
326
+ emitTelemetry(() => telemetry?.recordProxyResolution({
327
+ provider: vendor,
328
+ protocol: resolvedProtocol,
329
+ cacheStatus: "disabled",
330
+ cacheHit: false,
331
+ resolutionMs: Math.max(0, Date.now() - startedAt),
332
+ attempts: 1,
333
+ }));
334
+ }
219
335
  return { proxy: resolved, skips };
220
336
  }
221
337
  }
@@ -226,7 +342,7 @@ async function resolveNativeGatewayProxyDetailed(input) {
226
342
  };
227
343
  }
228
344
  }
229
- skips.push({ vendor, reason: vendorSkip ?? { kind: "adapter_unavailable" } });
345
+ recordSkip({ vendor, reason: vendorSkip ?? { kind: "adapter_unavailable" } });
230
346
  }
231
347
  return { skips };
232
348
  }
@@ -651,6 +767,10 @@ export function createNativeNetworkConnection(socket, proxy, options, idleTimeou
651
767
  if (socket.readableEnded || socket.destroyed)
652
768
  return;
653
769
  closeReason = new NativeIdleTimeoutError();
770
+ emitTelemetry(() => options.nativeTelemetry?.recordLifecycle({
771
+ kind: "idle",
772
+ errorCode: "native_connection_idle_timeout",
773
+ }));
654
774
  socket.destroy(closeReason);
655
775
  }, Math.max(0, idleTimeoutMs));
656
776
  idleTimer.unref?.();
@@ -668,6 +788,11 @@ export function createNativeNetworkConnection(socket, proxy, options, idleTimeou
668
788
  };
669
789
  socket.on("error", (error) => {
670
790
  terminalError = error;
791
+ if (!closeReason)
792
+ emitTelemetry(() => options.nativeTelemetry?.recordError({
793
+ errorCode: "native_connection_failed",
794
+ diagnostics: nativeErrorDiagnostics(error),
795
+ }));
671
796
  });
672
797
  socket.once("end", clearIdleTimer);
673
798
  socket.once("close", clearLifecycle);
@@ -689,16 +814,32 @@ export function createNativeNetworkConnection(socket, proxy, options, idleTimeou
689
814
  const leadDelay = Math.max(0, remaining - leadSeconds * 1_000);
690
815
  expiringTimer = setTimeout(() => {
691
816
  expiringTimer = undefined;
817
+ emitTelemetry(() => options.nativeTelemetry?.recordLifecycle({ kind: "drain" }));
692
818
  const handler = drainHandler;
693
819
  if (!handler) {
820
+ emitTelemetry(() => options.nativeTelemetry?.recordLifecycle({ kind: "drain_missing_handler" }));
694
821
  warn("Native sticky proxy is expiring without a drain handler");
695
822
  return;
696
823
  }
697
824
  void (async () => {
698
825
  try {
699
- await Promise.race([Promise.resolve(handler(event)), lifecycleCutoff]);
826
+ await Promise.race([
827
+ Promise.resolve(handler(event)).then(() => {
828
+ if (socket.destroyed || lifecycleSettled)
829
+ return;
830
+ emitTelemetry(() => options.nativeTelemetry?.recordLifecycle({ kind: "drain_acknowledged" }));
831
+ }),
832
+ lifecycleCutoff,
833
+ ]);
700
834
  }
701
- catch {
835
+ catch (error) {
836
+ if (socket.destroyed || lifecycleSettled)
837
+ return;
838
+ emitTelemetry(() => options.nativeTelemetry?.recordLifecycle({
839
+ kind: "drain_error",
840
+ errorCode: "other",
841
+ diagnostics: nativeErrorDiagnostics(error),
842
+ }));
702
843
  // Drain failures do not bypass the hard-expiry fail-safe.
703
844
  }
704
845
  })();
@@ -709,6 +850,11 @@ export function createNativeNetworkConnection(socket, proxy, options, idleTimeou
709
850
  if (socket.destroyed)
710
851
  return;
711
852
  closeReason = new NativeProxyExpiredError(expiresAt);
853
+ emitTelemetry(() => options.nativeTelemetry?.recordLifecycle({
854
+ kind: "expiry",
855
+ errorCode: "native_proxy_expired",
856
+ diagnostics: { expiresAt },
857
+ }));
712
858
  settleLifecycle();
713
859
  socket.destroy();
714
860
  }, remaining);
@@ -722,6 +868,7 @@ export function createNativeNetworkConnection(socket, proxy, options, idleTimeou
722
868
  const chunk = socket.read();
723
869
  if (chunk) {
724
870
  resetIdleTimer();
871
+ emitTelemetry(() => options.nativeTelemetry?.recordBytes({ direction: "in", bytes: chunk.byteLength }));
725
872
  return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
726
873
  }
727
874
  if (socket.readableEnded || socket.destroyed)
@@ -770,6 +917,7 @@ export function createNativeNetworkConnection(socket, proxy, options, idleTimeou
770
917
  if (closeReason)
771
918
  throw closeReason;
772
919
  if (socket.destroyed) {
920
+ emitTelemetry(() => options.nativeTelemetry?.recordError({ errorCode: "native_connection_closed" }));
773
921
  throw new NativeNetworkError("Native connection is closed", "native_connection_closed");
774
922
  }
775
923
  await new Promise((resolve, reject) => {
@@ -780,6 +928,7 @@ export function createNativeNetworkConnection(socket, proxy, options, idleTimeou
780
928
  resolve();
781
929
  });
782
930
  });
931
+ emitTelemetry(() => options.nativeTelemetry?.recordBytes({ direction: "out", bytes: data.byteLength }));
783
932
  },
784
933
  close: async () => {
785
934
  if (socket.closed || socket.destroyed) {
@@ -808,6 +957,8 @@ async function resolveConnectionProxy(options, input, deadline) {
808
957
  protocol: options.proxyProtocol,
809
958
  credentials: options.credentials,
810
959
  gatewaySynthesizers: options.gatewaySynthesizers,
960
+ telemetry: options.telemetry,
961
+ nativeTelemetry: options.nativeTelemetry,
811
962
  }), input.signal, deadline);
812
963
  if (!resolution.proxy && policy.mode === "required") {
813
964
  throw proxyRequiredError(policy, resolution.skips);
@@ -1201,32 +1352,82 @@ export function createNativeEgressAuthorization(options) {
1201
1352
  /** Create the SDK byte-stream runtime with provider-declared egress enforcement. */
1202
1353
  export function createNativeNetworkClient(options = {}) {
1203
1354
  const egress = createNativeEgressAuthorization(options);
1204
- return {
1205
- connectTcp: async (input) => {
1206
- const request = snapshotNativeConnectInput(input);
1207
- egress.assertConnect(request, "disabled");
1208
- const deadline = deadlineFrom(request.timeoutMs);
1209
- assertCanStart(request.signal, deadline);
1210
- const proxy = await resolveConnectionProxy(options, request, deadline);
1211
- egress.assertConnect(request, "disabled");
1212
- const socket = proxy
1213
- ? await connectProxyTunnel(proxy, request, deadline, () => egress.assertConnect(request, "disabled"))
1214
- : await connectPlainSocket(request.host, request.port, request.signal, deadline);
1215
- return createNativeNetworkConnection(socket, proxy, options, request.idleTimeoutMs);
1216
- },
1217
- connectTls: async (input) => {
1218
- const request = snapshotNativeConnectInput(input);
1219
- egress.assertConnect(request, "required");
1355
+ const connect = async (input, kind) => {
1356
+ const startedAt = Date.now();
1357
+ let proxy;
1358
+ let request;
1359
+ let tunnelMs = 0;
1360
+ let outcome = "error";
1361
+ let failure;
1362
+ try {
1363
+ request = snapshotNativeConnectInput(input);
1364
+ const tls = kind === "tls" ? "required" : "disabled";
1365
+ egress.assertConnect(request, tls);
1220
1366
  const deadline = deadlineFrom(request.timeoutMs);
1221
1367
  assertCanStart(request.signal, deadline);
1222
- const proxy = await resolveConnectionProxy(options, request, deadline);
1223
- egress.assertConnect(request, "required");
1224
- const tunnel = proxy
1225
- ? await connectProxyTunnel(proxy, request, deadline, () => egress.assertConnect(request, "required"))
1226
- : undefined;
1227
- const socket = await upgradeTls(tunnel, request, deadline);
1228
- return createNativeNetworkConnection(socket, proxy, options, request.idleTimeoutMs);
1229
- },
1368
+ proxy = await resolveConnectionProxy(options, request, deadline);
1369
+ egress.assertConnect(request, tls);
1370
+ let tunnel;
1371
+ if (proxy) {
1372
+ const tunnelStartedAt = Date.now();
1373
+ try {
1374
+ const destination = request;
1375
+ tunnel = await connectProxyTunnel(proxy, request, deadline, () => egress.assertConnect(destination, tls));
1376
+ }
1377
+ finally {
1378
+ tunnelMs = Math.max(0, Date.now() - tunnelStartedAt);
1379
+ }
1380
+ }
1381
+ const socket = kind === "tls"
1382
+ ? await upgradeTls(tunnel, request, deadline)
1383
+ : (tunnel ??
1384
+ (await connectPlainSocket(request.host, request.port, request.signal, deadline)));
1385
+ const connection = createNativeNetworkConnection(socket, proxy, options, request.idleTimeoutMs);
1386
+ outcome = "ok";
1387
+ return connection;
1388
+ }
1389
+ catch (error) {
1390
+ failure = error;
1391
+ throw error;
1392
+ }
1393
+ finally {
1394
+ // One terminal sample for this connect call, including preflight and rethrows.
1395
+ emitTelemetry(() => options.nativeTelemetry?.recordConnect({
1396
+ kind,
1397
+ outcome,
1398
+ ms: Math.max(0, Date.now() - startedAt),
1399
+ tunnelMs,
1400
+ proxyUsed: proxy !== undefined,
1401
+ ...(proxy ? { vendor: proxy.vendor } : {}),
1402
+ ...(outcome === "error" ? { errorCode: nativeErrorCode(failure) } : {}),
1403
+ diagnostics: {
1404
+ ...(request
1405
+ ? {
1406
+ host: request.host,
1407
+ port: request.port,
1408
+ ...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
1409
+ ...(request.idleTimeoutMs === undefined
1410
+ ? {}
1411
+ : { idleTimeoutMs: request.idleTimeoutMs }),
1412
+ ...(request.serverName ? { serverName: request.serverName } : {}),
1413
+ }
1414
+ : {}),
1415
+ ...(proxy
1416
+ ? {
1417
+ protocol: new URL(proxy.url).protocol,
1418
+ sticky: proxy.sticky,
1419
+ ...(proxy.sessionId ? { sessionId: proxy.sessionId } : {}),
1420
+ ...(proxy.expiresAt ? { expiresAt: proxy.expiresAt } : {}),
1421
+ }
1422
+ : {}),
1423
+ ...nativeErrorDiagnostics(failure),
1424
+ },
1425
+ }));
1426
+ }
1427
+ };
1428
+ return {
1429
+ connectTcp: (input) => connect(input, "tcp"),
1430
+ connectTls: (input) => connect(input, "tls"),
1230
1431
  grantTcpEgress: (input) => egress.grant(snapshotNativeGrantInput(input)),
1231
1432
  };
1232
1433
  }
@@ -0,0 +1,142 @@
1
+ import type { ProviderProxyProvider } from "../types.js";
2
+ import type { NativeNetworkErrorCode } from "./native-network-errors.js";
3
+ import { type ClosedEnum, type TelemetryContributor } from "./request-telemetry.js";
4
+ export type NativeTelemetryErrorCode = NativeNetworkErrorCode | "PROXY_REQUIRED" | "PROXY_ALLOCATION_FAILED" | "other";
5
+ export type NativeTelemetryKind = "tcp" | "tls";
6
+ export type NativeTelemetryOutcome = "ok" | "error";
7
+ export type NativeTelemetryVendorSkipReason = "credentials_absent" | "protocol_unsupported" | "allocation_failed" | "credential_lookup_failed" | "adapter_unavailable";
8
+ export type NativeTelemetryLifecycleKind = "drain" | "drain_acknowledged" | "drain_error" | "drain_missing_handler" | "idle" | "expiry";
9
+ /** Operator-only text is redacted before it is detached and bounded to 300 code units. */
10
+ export type NativeTelemetryDiagnostics = {
11
+ host?: string;
12
+ serverName?: string;
13
+ protocol?: string;
14
+ sessionId?: string;
15
+ expiresAt?: string;
16
+ errorName?: string;
17
+ errorMessage?: string;
18
+ causeName?: string;
19
+ causeMessage?: string;
20
+ systemCode?: string;
21
+ missingFields?: readonly string[];
22
+ port?: number;
23
+ timeoutMs?: number;
24
+ idleTimeoutMs?: number;
25
+ status?: number;
26
+ socksReplyCode?: number;
27
+ sticky?: boolean;
28
+ };
29
+ export type NativeConnectTelemetryEvent = {
30
+ kind: NativeTelemetryKind;
31
+ outcome: NativeTelemetryOutcome;
32
+ /** Entire establishment, including lease resolution, tunnel and TLS handshake. */
33
+ ms: number;
34
+ /** Tunnel establishment only; included in ms, never added to it. */
35
+ tunnelMs: number;
36
+ proxyUsed: boolean;
37
+ /** Selected vendor for this connection; operator log only. */
38
+ vendor?: ProviderProxyProvider;
39
+ errorCode?: NativeTelemetryErrorCode;
40
+ diagnostics?: NativeTelemetryDiagnostics;
41
+ };
42
+ export type NativeVendorSkipTelemetryEvent = {
43
+ vendor: ProviderProxyProvider;
44
+ reason: NativeTelemetryVendorSkipReason;
45
+ diagnostics?: NativeTelemetryDiagnostics;
46
+ };
47
+ export type NativeLifecycleTelemetryEvent = {
48
+ kind: NativeTelemetryLifecycleKind;
49
+ errorCode?: NativeTelemetryErrorCode;
50
+ diagnostics?: NativeTelemetryDiagnostics;
51
+ };
52
+ export interface NativeTelemetrySink {
53
+ recordConnect(event: NativeConnectTelemetryEvent): void;
54
+ recordVendorSkip(event: NativeVendorSkipTelemetryEvent): void;
55
+ recordLifecycle(event: NativeLifecycleTelemetryEvent): void;
56
+ /** Application bytes returned by read or accepted by a successful write. Excludes tunnel/TLS framing. */
57
+ recordBytes(event: {
58
+ direction: "in" | "out";
59
+ bytes: number;
60
+ }): void;
61
+ recordError(event: {
62
+ errorCode: NativeTelemetryErrorCode;
63
+ diagnostics?: NativeTelemetryDiagnostics;
64
+ }): void;
65
+ }
66
+ export type NativeAttemptSample = NativeConnectTelemetryEvent & {
67
+ n: number;
68
+ };
69
+ export type NativeTelemetryLogPayload = {
70
+ attempts: number;
71
+ connectMs: number;
72
+ tunnelMs: number;
73
+ vendorSkips: number;
74
+ vendorSkipReasons: {
75
+ reason: NativeTelemetryVendorSkipReason;
76
+ count: number;
77
+ }[];
78
+ drain: number;
79
+ drainAcknowledged: number;
80
+ drainErrors: number;
81
+ drainMissingHandler: number;
82
+ idle: number;
83
+ expiry: number;
84
+ bytesIn: number;
85
+ bytesOut: number;
86
+ lastErrorCode?: NativeTelemetryErrorCode;
87
+ lastErrorDiagnostics?: NativeTelemetryDiagnostics;
88
+ attemptSamples?: NativeAttemptSample[];
89
+ attemptSamplesDropped?: number;
90
+ vendorSkipSamples?: NativeVendorSkipTelemetryEvent[];
91
+ vendorSkipSamplesDropped?: number;
92
+ };
93
+ export type NativeTelemetryHeaderPayload = {
94
+ attempts: number;
95
+ connectMs: number;
96
+ tunnelMs: number;
97
+ vendorSkips: number;
98
+ vendorSkipReasons: {
99
+ reason: ClosedEnum<NativeTelemetryVendorSkipReason>;
100
+ count: number;
101
+ }[];
102
+ drain: number;
103
+ drainAcknowledged: number;
104
+ drainErrors: number;
105
+ drainMissingHandler: number;
106
+ idle: number;
107
+ expiry: number;
108
+ bytesIn: number;
109
+ bytesOut: number;
110
+ lastErrorCode?: ClosedEnum<NativeTelemetryErrorCode>;
111
+ attemptSamples?: {
112
+ n: number;
113
+ kind: ClosedEnum<NativeTelemetryKind>;
114
+ outcome: ClosedEnum<NativeTelemetryOutcome>;
115
+ ms: number;
116
+ tunnelMs: number;
117
+ proxyUsed: boolean;
118
+ errorCode?: ClosedEnum<NativeTelemetryErrorCode>;
119
+ }[];
120
+ attemptSamplesDropped?: number;
121
+ vendorSkipSamplesDropped?: number;
122
+ };
123
+ export declare class NativeTelemetryCollector implements NativeTelemetrySink, TelemetryContributor<NativeTelemetryLogPayload, NativeTelemetryHeaderPayload> {
124
+ #private;
125
+ readonly key: "native";
126
+ constructor(options?: {
127
+ redact?: (text: string) => string;
128
+ });
129
+ recordConnect(event: NativeConnectTelemetryEvent): void;
130
+ recordVendorSkip(event: NativeVendorSkipTelemetryEvent): void;
131
+ recordLifecycle(event: NativeLifecycleTelemetryEvent): void;
132
+ recordBytes(event: {
133
+ direction: "in" | "out";
134
+ bytes: number;
135
+ }): void;
136
+ recordError(event: {
137
+ errorCode: NativeTelemetryErrorCode;
138
+ diagnostics?: NativeTelemetryDiagnostics;
139
+ }): void;
140
+ toLogPayload(): NativeTelemetryLogPayload | undefined;
141
+ toHeaderPayload(log: NativeTelemetryLogPayload): NativeTelemetryHeaderPayload;
142
+ }