@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
@@ -0,0 +1,231 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { closedEnum } from "./request-telemetry.js";
3
+ const MAX_SAMPLES = 24;
4
+ function integer(value) {
5
+ return Math.max(0, Math.floor(Number.isFinite(value) ? value : 0));
6
+ }
7
+ function saturatingAdd(a, b) {
8
+ if (!Number.isFinite(a) || !Number.isFinite(b))
9
+ return Number.MAX_SAFE_INTEGER;
10
+ return Math.min(Number.MAX_SAFE_INTEGER, integer(a) + integer(b));
11
+ }
12
+ function boundedText(value, redact) {
13
+ let result;
14
+ try {
15
+ result = redact ? redact(value) : value;
16
+ if (typeof result !== "string")
17
+ return "[REDACTION_FAILED]";
18
+ }
19
+ catch {
20
+ return "[REDACTION_FAILED]";
21
+ }
22
+ // Do not slice caller text: a substring may retain the entire vendor response.
23
+ const length = Math.min(result.length, 300);
24
+ const units = Buffer.alloc(length * 2);
25
+ for (let index = 0; index < length; index++)
26
+ units.writeUInt16LE(result.charCodeAt(index), index * 2);
27
+ return units.toString("utf16le");
28
+ }
29
+ function diagnostics(value, redact) {
30
+ const result = {};
31
+ for (const key of [
32
+ "host",
33
+ "serverName",
34
+ "protocol",
35
+ "sessionId",
36
+ "expiresAt",
37
+ "errorName",
38
+ "errorMessage",
39
+ "causeName",
40
+ "causeMessage",
41
+ "systemCode",
42
+ ]) {
43
+ if (value[key] !== undefined)
44
+ result[key] = boundedText(value[key], redact);
45
+ }
46
+ for (const key of ["port", "timeoutMs", "idleTimeoutMs", "status", "socksReplyCode"]) {
47
+ if (value[key] !== undefined)
48
+ result[key] = integer(value[key]);
49
+ }
50
+ if (value.sticky !== undefined)
51
+ result.sticky = value.sticky;
52
+ if (value.missingFields)
53
+ result.missingFields = value.missingFields
54
+ .slice(0, MAX_SAMPLES)
55
+ .map((field) => boundedText(field, redact));
56
+ return result;
57
+ }
58
+ function copyDiagnostics(value) {
59
+ return { ...value, ...(value.missingFields ? { missingFields: [...value.missingFields] } : {}) };
60
+ }
61
+ export class NativeTelemetryCollector {
62
+ key = "native";
63
+ #redact;
64
+ #recorded = false;
65
+ #attempts = 0;
66
+ #connectMs = 0;
67
+ #tunnelMs = 0;
68
+ #vendorSkips = 0;
69
+ #vendorSkipReasons = new Map();
70
+ #lifecycle = {
71
+ drain: 0,
72
+ drain_acknowledged: 0,
73
+ drain_error: 0,
74
+ drain_missing_handler: 0,
75
+ idle: 0,
76
+ expiry: 0,
77
+ };
78
+ #bytesIn = 0;
79
+ #bytesOut = 0;
80
+ #lastErrorCode;
81
+ #lastErrorDiagnostics;
82
+ #attemptSamples = [];
83
+ #attemptSamplesDropped = 0;
84
+ #vendorSkipSamples = [];
85
+ #vendorSkipSamplesDropped = 0;
86
+ constructor(options = {}) {
87
+ this.#redact = options.redact;
88
+ }
89
+ recordConnect(event) {
90
+ this.#recorded = true;
91
+ const ms = saturatingAdd(0, event.ms);
92
+ const tunnelMs = saturatingAdd(0, event.tunnelMs);
93
+ this.#attempts = saturatingAdd(this.#attempts, 1);
94
+ this.#connectMs = saturatingAdd(this.#connectMs, ms);
95
+ this.#tunnelMs = saturatingAdd(this.#tunnelMs, tunnelMs);
96
+ if (event.errorCode)
97
+ this.recordError({ errorCode: event.errorCode, diagnostics: event.diagnostics });
98
+ if (this.#attemptSamples.length < MAX_SAMPLES) {
99
+ this.#attemptSamples.push({
100
+ n: this.#attempts,
101
+ kind: event.kind,
102
+ outcome: event.outcome,
103
+ ms,
104
+ tunnelMs,
105
+ proxyUsed: event.proxyUsed,
106
+ ...(event.vendor ? { vendor: event.vendor } : {}),
107
+ ...(event.errorCode ? { errorCode: event.errorCode } : {}),
108
+ ...(event.diagnostics ? { diagnostics: diagnostics(event.diagnostics, this.#redact) } : {}),
109
+ });
110
+ }
111
+ else
112
+ this.#attemptSamplesDropped = saturatingAdd(this.#attemptSamplesDropped, 1);
113
+ }
114
+ recordVendorSkip(event) {
115
+ this.#recorded = true;
116
+ this.#vendorSkips = saturatingAdd(this.#vendorSkips, 1);
117
+ this.#vendorSkipReasons.set(event.reason, saturatingAdd(this.#vendorSkipReasons.get(event.reason) ?? 0, 1));
118
+ if (this.#vendorSkipSamples.length < MAX_SAMPLES) {
119
+ this.#vendorSkipSamples.push({
120
+ vendor: event.vendor,
121
+ reason: event.reason,
122
+ ...(event.diagnostics ? { diagnostics: diagnostics(event.diagnostics, this.#redact) } : {}),
123
+ });
124
+ }
125
+ else
126
+ this.#vendorSkipSamplesDropped = saturatingAdd(this.#vendorSkipSamplesDropped, 1);
127
+ }
128
+ recordLifecycle(event) {
129
+ this.#recorded = true;
130
+ this.#lifecycle[event.kind] = saturatingAdd(this.#lifecycle[event.kind], 1);
131
+ if (event.errorCode)
132
+ this.recordError({ errorCode: event.errorCode, diagnostics: event.diagnostics });
133
+ }
134
+ recordBytes(event) {
135
+ this.#recorded = true;
136
+ if (event.direction === "in")
137
+ this.#bytesIn = saturatingAdd(this.#bytesIn, event.bytes);
138
+ else
139
+ this.#bytesOut = saturatingAdd(this.#bytesOut, event.bytes);
140
+ }
141
+ recordError(event) {
142
+ this.#recorded = true;
143
+ this.#lastErrorCode = event.errorCode;
144
+ this.#lastErrorDiagnostics = event.diagnostics
145
+ ? diagnostics(event.diagnostics, this.#redact)
146
+ : undefined;
147
+ }
148
+ toLogPayload() {
149
+ if (!this.#recorded)
150
+ return undefined;
151
+ return {
152
+ attempts: this.#attempts,
153
+ connectMs: this.#connectMs,
154
+ tunnelMs: this.#tunnelMs,
155
+ vendorSkips: this.#vendorSkips,
156
+ vendorSkipReasons: [...this.#vendorSkipReasons].map(([reason, count]) => ({ reason, count })),
157
+ drain: this.#lifecycle.drain,
158
+ drainAcknowledged: this.#lifecycle.drain_acknowledged,
159
+ drainErrors: this.#lifecycle.drain_error,
160
+ drainMissingHandler: this.#lifecycle.drain_missing_handler,
161
+ idle: this.#lifecycle.idle,
162
+ expiry: this.#lifecycle.expiry,
163
+ bytesIn: this.#bytesIn,
164
+ bytesOut: this.#bytesOut,
165
+ ...(this.#lastErrorCode ? { lastErrorCode: this.#lastErrorCode } : {}),
166
+ ...(this.#lastErrorDiagnostics
167
+ ? { lastErrorDiagnostics: copyDiagnostics(this.#lastErrorDiagnostics) }
168
+ : {}),
169
+ ...(this.#attemptSamples.length
170
+ ? {
171
+ attemptSamples: this.#attemptSamples.map((sample) => ({
172
+ ...sample,
173
+ ...(sample.diagnostics ? { diagnostics: copyDiagnostics(sample.diagnostics) } : {}),
174
+ })),
175
+ }
176
+ : {}),
177
+ ...(this.#attemptSamplesDropped
178
+ ? { attemptSamplesDropped: this.#attemptSamplesDropped }
179
+ : {}),
180
+ ...(this.#vendorSkipSamples.length
181
+ ? {
182
+ vendorSkipSamples: this.#vendorSkipSamples.map((sample) => ({
183
+ ...sample,
184
+ ...(sample.diagnostics ? { diagnostics: copyDiagnostics(sample.diagnostics) } : {}),
185
+ })),
186
+ }
187
+ : {}),
188
+ ...(this.#vendorSkipSamplesDropped
189
+ ? { vendorSkipSamplesDropped: this.#vendorSkipSamplesDropped }
190
+ : {}),
191
+ };
192
+ }
193
+ toHeaderPayload(log) {
194
+ return {
195
+ attempts: log.attempts,
196
+ connectMs: log.connectMs,
197
+ tunnelMs: log.tunnelMs,
198
+ vendorSkips: log.vendorSkips,
199
+ vendorSkipReasons: log.vendorSkipReasons.map(({ reason, count }) => ({
200
+ reason: closedEnum(reason),
201
+ count,
202
+ })),
203
+ drain: log.drain,
204
+ drainAcknowledged: log.drainAcknowledged,
205
+ drainErrors: log.drainErrors,
206
+ drainMissingHandler: log.drainMissingHandler,
207
+ idle: log.idle,
208
+ expiry: log.expiry,
209
+ bytesIn: log.bytesIn,
210
+ bytesOut: log.bytesOut,
211
+ ...(log.lastErrorCode ? { lastErrorCode: closedEnum(log.lastErrorCode) } : {}),
212
+ ...(log.attemptSamples
213
+ ? {
214
+ attemptSamples: log.attemptSamples.map((sample) => ({
215
+ n: sample.n,
216
+ kind: closedEnum(sample.kind),
217
+ outcome: closedEnum(sample.outcome),
218
+ ms: sample.ms,
219
+ tunnelMs: sample.tunnelMs,
220
+ proxyUsed: sample.proxyUsed,
221
+ ...(sample.errorCode ? { errorCode: closedEnum(sample.errorCode) } : {}),
222
+ })),
223
+ }
224
+ : {}),
225
+ ...(log.attemptSamplesDropped ? { attemptSamplesDropped: log.attemptSamplesDropped } : {}),
226
+ ...(log.vendorSkipSamplesDropped
227
+ ? { vendorSkipSamplesDropped: log.vendorSkipSamplesDropped }
228
+ : {}),
229
+ };
230
+ }
231
+ }
@@ -15,6 +15,9 @@ export declare function createProviderRuntimeStateFromEnv(options?: {
15
15
  readonly providerId?: string;
16
16
  readonly allowMemoryFallback?: boolean;
17
17
  }): ProviderRuntimeState;
18
- export declare function createMemoryProviderRuntimeState(): ProviderRuntimeState;
18
+ export declare function createMemoryProviderRuntimeState(options?: {
19
+ /** Clock for expiry and timestamps; defaults to `Date.now`. */
20
+ readonly now?: () => number;
21
+ }): ProviderRuntimeState;
19
22
  export declare function createUnsupportedProviderRuntimeState(): ProviderRuntimeState;
20
23
  export {};
@@ -60,9 +60,12 @@ if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
60
60
  end
61
61
 
62
62
  if current_decoded and type(current_decoded.createdAt) == "string" then
63
- local next_decoded = cjson.decode(envelope)
64
- next_decoded.createdAt = current_decoded.createdAt
65
- envelope = cjson.encode(next_decoded)
63
+ -- Preserve the original createdAt by patching the encoded envelope in place.
64
+ -- The envelope puts its metadata before "value", so the first match is the
65
+ -- envelope's own key, never a provider value; re-encoding with cjson would
66
+ -- silently turn [] inside the value into {}.
67
+ local preserved = string.gsub(current_decoded.createdAt, "%%", "%%%%")
68
+ envelope = (string.gsub(envelope, '"createdAt":"[^"]*"', '"createdAt":"' .. preserved .. '"', 1))
66
69
  end
67
70
  redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
68
71
  redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
@@ -197,7 +200,11 @@ function isStateTombstone(raw) {
197
200
  }
198
201
  function redisEnvelope(value, version, createdAt, expiresAt) {
199
202
  const updatedAt = new Date().toISOString();
200
- return { value, version, expiresAt, createdAt, updatedAt };
203
+ // Metadata precedes `value` on purpose: the compare-and-set Lua script patches
204
+ // `createdAt` in the encoded string (first occurrence) instead of decoding and
205
+ // re-encoding with cjson, which would turn empty arrays inside `value` into
206
+ // objects. Keep the key order stable.
207
+ return { version, expiresAt, createdAt, updatedAt, value };
201
208
  }
202
209
  class RedisProviderStateNamespace {
203
210
  backend;
@@ -404,10 +411,12 @@ class UnsupportedProviderRuntimeState {
404
411
  }
405
412
  class MemoryProviderStateNamespace {
406
413
  options;
414
+ now;
407
415
  // biome-ignore lint/suspicious/noExplicitAny: in-memory state stores heterogeneous generic values by key.
408
416
  values = new Map();
409
- constructor(options) {
417
+ constructor(options, now = Date.now) {
410
418
  this.options = options;
419
+ this.now = now;
411
420
  }
412
421
  enforceValueSize(value) {
413
422
  const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
@@ -426,7 +435,7 @@ class MemoryProviderStateNamespace {
426
435
  throw new UnsupportedProviderStateError(`Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`);
427
436
  }
428
437
  }
429
- pruneExpired(nowMs = Date.now()) {
438
+ pruneExpired(nowMs = this.now()) {
430
439
  for (const [key, row] of this.values.entries()) {
431
440
  if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
432
441
  this.values.delete(key);
@@ -445,9 +454,9 @@ class MemoryProviderStateNamespace {
445
454
  async set(key, value, options) {
446
455
  this.pruneExpired();
447
456
  this.enforceWritePolicy(key, value, options?.ttl);
448
- const now = new Date().toISOString();
457
+ const now = new Date(this.now()).toISOString();
449
458
  const current = this.values.get(key);
450
- const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
459
+ const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl, this.now());
451
460
  const row = {
452
461
  key,
453
462
  value,
@@ -469,12 +478,12 @@ class MemoryProviderStateNamespace {
469
478
  return { ok: false, current: current ?? null };
470
479
  }
471
480
  this.enforceWritePolicy(key, value, options?.ttl);
472
- const now = new Date().toISOString();
481
+ const now = new Date(this.now()).toISOString();
473
482
  const stored = {
474
483
  key,
475
484
  value,
476
485
  version: expectedVersion + 1,
477
- expiresAt: resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl),
486
+ expiresAt: resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl, this.now()),
478
487
  createdAt: current?.createdAt ?? now,
479
488
  updatedAt: now,
480
489
  };
@@ -491,7 +500,10 @@ class MemoryProviderStateNamespace {
491
500
  class MemoryProviderRuntimeState {
492
501
  backend;
493
502
  scopeDiscriminator;
494
- constructor(backend = { namespaces: new Map() }, scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR) {
503
+ constructor(backend = {
504
+ namespaces: new Map(),
505
+ now: Date.now,
506
+ }, scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR) {
495
507
  this.backend = backend;
496
508
  this.scopeDiscriminator = scopeDiscriminator;
497
509
  }
@@ -504,15 +516,15 @@ class MemoryProviderRuntimeState {
504
516
  const existing = this.backend.namespaces.get(namespaceIdentity);
505
517
  if (existing)
506
518
  return existing;
507
- const created = new MemoryProviderStateNamespace(options);
519
+ const created = new MemoryProviderStateNamespace(options, this.backend.now);
508
520
  this.backend.namespaces.set(namespaceIdentity, created);
509
521
  return created;
510
522
  }
511
523
  }
512
- function resolveMemoryStateExpiresAt(ttl) {
524
+ function resolveMemoryStateExpiresAt(ttl, nowMs = Date.now()) {
513
525
  const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
514
526
  if (!match)
515
- return new Date(Date.now() + 3_600_000).toISOString();
527
+ return new Date(nowMs + 3_600_000).toISOString();
516
528
  const amount = Number(match[1]);
517
529
  const unit = match[2];
518
530
  const multiplier = unit === "ms"
@@ -524,7 +536,7 @@ function resolveMemoryStateExpiresAt(ttl) {
524
536
  : unit === "h"
525
537
  ? 3_600_000
526
538
  : 86_400_000;
527
- return new Date(Date.now() + amount * multiplier).toISOString();
539
+ return new Date(nowMs + amount * multiplier).toISOString();
528
540
  }
529
541
  export function createRedisProviderRuntimeState(options) {
530
542
  return new RedisProviderRuntimeState(options);
@@ -542,8 +554,11 @@ export function createProviderRuntimeStateFromEnv(options = {}) {
542
554
  }
543
555
  return createUnsupportedProviderRuntimeState();
544
556
  }
545
- export function createMemoryProviderRuntimeState() {
546
- return new MemoryProviderRuntimeState();
557
+ export function createMemoryProviderRuntimeState(options) {
558
+ return new MemoryProviderRuntimeState({
559
+ namespaces: new Map(),
560
+ now: options?.now ?? Date.now,
561
+ });
547
562
  }
548
563
  export function createUnsupportedProviderRuntimeState() {
549
564
  return new UnsupportedProviderRuntimeState();
@@ -12,3 +12,4 @@ export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_
12
12
  export { DEFAULT_SELF_TEST_PORT, deriveSelfTestToken, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV, PROVIDER_RUNTIME_SELF_TEST_PORT_ENV, resolveSelfTestMasterSecrets, type SelfTestMasterSecrets, verifySelfTestAuthorization, } from "./self-test-token.js";
13
13
  export type { AuthFlowRequest, AuthFlowResponse, AuthFlowSuccessResponse, ConnectionMode, OperationConnection, OperationErrorResponse, OperationRequest, OperationResponse, OperationSuccessResponse, } from "./types.js";
14
14
  export { AuthFlowRequestSchema, AuthFlowSuccessResponseSchema, ConnectionModeSchema, ErrorEnvelopeSchema, OperationConnectionSchema, OperationErrorResponseSchema, OperationRequestSchema, OperationSuccessResponseSchema, } from "./types.js";
15
+ export { NativeTelemetryCollector, type NativeAttemptSample, type NativeConnectTelemetryEvent, type NativeLifecycleTelemetryEvent, type NativeTelemetryDiagnostics, type NativeTelemetryErrorCode, type NativeTelemetryHeaderPayload, type NativeTelemetryKind, type NativeTelemetryLifecycleKind, type NativeTelemetryLogPayload, type NativeTelemetryOutcome, type NativeTelemetrySink, type NativeTelemetryVendorSkipReason, type NativeVendorSkipTelemetryEvent, } from "../runtime/native-telemetry.js";
@@ -6,3 +6,4 @@ export { resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js"
6
6
  export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction.js";
7
7
  export { DEFAULT_SELF_TEST_PORT, deriveSelfTestToken, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV, PROVIDER_RUNTIME_SELF_TEST_PORT_ENV, resolveSelfTestMasterSecrets, verifySelfTestAuthorization, } from "./self-test-token.js";
8
8
  export { AuthFlowRequestSchema, AuthFlowSuccessResponseSchema, ConnectionModeSchema, ErrorEnvelopeSchema, OperationConnectionSchema, OperationErrorResponseSchema, OperationRequestSchema, OperationSuccessResponseSchema, } from "./types.js";
9
+ export { NativeTelemetryCollector, } from "../runtime/native-telemetry.js";
@@ -1,7 +1,7 @@
1
1
  import type { ProviderDefinition } from "../types.js";
2
2
  export declare const SELF_TEST_SENSITIVE_SOURCE_KINDS: readonly ["selfTestMasterSecrets", "providerSecrets", "healthProbeSecrets", "requestCredentials"];
3
3
  /** Explicit source allowlist: no environment-name or payload-shape secret sweep. */
4
- export declare const DIAGNOSTIC_SENSITIVE_SOURCE_KINDS: readonly ["selfTestMasterSecrets", "providerSecrets", "healthProbeSecrets", "requestCredentials", "engineProxyCredentials", "engineSolverKeys", "engineCeremonyLeaseCredentials", "choiceMasterSecret", "ocrCredentials", "sttCredentials", "cdpCredentials", "statefulForwardingSecret", "redisCredentials", "otlpHeaders", "cacheKeyPepper", "sensitiveParams"];
4
+ export declare const DIAGNOSTIC_SENSITIVE_SOURCE_KINDS: readonly ["selfTestMasterSecrets", "providerSecrets", "healthProbeSecrets", "requestCredentials", "engineProxyCredentials", "engineSolverKeys", "engineCeremonyLeaseCredentials", "ocrCredentials", "sttCredentials", "cdpCredentials", "statefulForwardingSecret", "redisCredentials", "otlpHeaders", "cacheKeyPepper", "sensitiveParams"];
5
5
  export type DiagnosticSensitiveSourceKind = (typeof DIAGNOSTIC_SENSITIVE_SOURCE_KINDS)[number];
6
6
  /**
7
7
  * Startup inventory; request-scoped readers additionally register live values on access.
@@ -2,7 +2,6 @@ import { resolvedStringVariants, urlCredentialComponents, otlpHeaderCredentialVa
2
2
  import { PROVIDER_CACHE_REDIS_URL_ENV, PROVIDER_STATE_REDIS_URL_ENV, REDIS_URL_ENV, } from "../config/loader.js";
3
3
  import { ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES, ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES, } from "../engine.js";
4
4
  import { APIFUSE__CACHE__KEY_PEPPER_ENV } from "../runtime/cache.js";
5
- import { PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV } from "../runtime/choice.js";
6
5
  import { APIFUSE__ENGINE__CEREMONY_LEASE_KEY } from "../runtime/egress-lease.js";
7
6
  import { APIFUSE__OCR__API_KEY_ENV, APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV, } from "../runtime/ocr.js";
8
7
  import { OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, } from "../runtime/otlp.js";
@@ -21,7 +20,6 @@ export const DIAGNOSTIC_SENSITIVE_SOURCE_KINDS = [
21
20
  "engineProxyCredentials",
22
21
  "engineSolverKeys",
23
22
  "engineCeremonyLeaseCredentials",
24
- "choiceMasterSecret",
25
23
  "ocrCredentials",
26
24
  "sttCredentials",
27
25
  "cdpCredentials",
@@ -68,7 +66,6 @@ export function collectDiagnosticSensitiveValues(provider, options = {}) {
68
66
  // These are the resolver-config API keys after #251's engine credential projection.
69
67
  engineSolverKeys: () => resolvedEnvValues(ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES),
70
68
  engineCeremonyLeaseCredentials: () => resolvedEnvValues([APIFUSE__ENGINE__CEREMONY_LEASE_KEY]),
71
- choiceMasterSecret: () => resolvedEnvValues([PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV]),
72
69
  ocrCredentials: () => resolvedEnvValues([APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__OCR__API_KEY_ENV]),
73
70
  sttCredentials: () => resolvedEnvValues([APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV]),
74
71
  cdpCredentials: () => resolvedStringVariants(env[APIFUSE__CDP_POOL__URL]).flatMap((url) => [
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { type ProviderEngine } from "../engine.js";
4
4
  import { type ProviderErrorObservability } from "../errors.js";
5
5
  import { type ProviderErrorCategory } from "../observability.js";
6
+ import type { HandleTelemetryEvent } from "../handle.js";
6
7
  import { type ProxyTelemetryLogPayload } from "../runtime/proxy-telemetry.js";
7
8
  import { type ResolverTelemetryLogPayload } from "../runtime/resolver-telemetry.js";
8
9
  import type { OcrContext, ProviderContext, ProviderDefinition, ProviderFilesContext, ProviderRuntimeState, ResolverContext, SttContext } from "../types.js";
@@ -115,18 +116,10 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
115
116
  event: "provider_secrets_missing";
116
117
  providerId: string;
117
118
  missingSecrets: string[];
118
- } | {
119
+ } | ({
119
120
  level: "info";
120
- event: "provider_choice_token";
121
- providerId: string;
122
- purpose: string;
123
- operation: "parse" | "consume";
124
- format: "word" | "legacy";
125
- outcome: "success" | "not-found" | "invalid" | "unsupported" | "error";
126
- consumeMode: "never" | "on-parse" | "explicit";
127
- consumed: boolean;
128
- replay: boolean;
129
- } | {
121
+ event: "provider_handle";
122
+ } & HandleTelemetryEvent) | {
130
123
  level: "warn";
131
124
  event: "provider_cleanup_failed";
132
125
  providerId: string;
@@ -1,3 +1,4 @@
1
+ import { NativeTelemetryCollector } from "../runtime/native-telemetry.js";
1
2
  import { STATUS_CODES } from "node:http";
2
3
  import { readDiagnosticEnv, withDiagnosticEnv } from "../runtime/diagnostic-env.js";
3
4
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -18,7 +19,7 @@ import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.j
18
19
  import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, sourceForCategory, } from "../observability.js";
19
20
  import { createScratchpad } from "../runtime/auth-flow.js";
20
21
  import { createProviderCache } from "../runtime/cache.js";
21
- import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
22
+ import { createHandleContext } from "../runtime/handle.js";
22
23
  import { createCredentialContext } from "../runtime/credential.js";
23
24
  import { bindDiagnosticSensitiveRegistry, compileProcessDiagnosticSensitiveValues, createDiagnosticRedactor, REDACTION_FAILED, isDiagnosticRedactionSuppressed, bindCriticalDiagnosticRedactor, redactCriticalDiagnosticText, redactDiagnosticText, registerDiagnosticSensitiveValues, } from "../runtime/diagnostic-redactor.js";
24
25
  import { createCeremonyEgressLeaseRuntime, ENGINE_CEREMONY_EGRESS_LEASE, } from "../runtime/egress-lease.js";
@@ -525,10 +526,7 @@ function createProviderContext(provider, request, operationId, options, state =
525
526
  : {}),
526
527
  };
527
528
  const logStealthCleanupError = (error) => logProviderCleanupError(options.logger, provider, "operation", operationId, request.requestId, "stealth", error, scope.redact);
528
- const env = createEnvContext([
529
- ...providerSecretNames(provider),
530
- PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
531
- ]);
529
+ const env = createEnvContext([...providerSecretNames(provider)]);
532
530
  const credential = createCredentialContext({
533
531
  allowedKeys: provider.credential?.keys,
534
532
  mode: request.connection?.mode,
@@ -575,6 +573,8 @@ function createProviderContext(provider, request, operationId, options, state =
575
573
  ? {
576
574
  native: {
577
575
  network: capabilityModules.nativeNetwork.createNativeNetworkClient({
576
+ telemetry: scope.telemetry.proxy,
577
+ nativeTelemetry: scope.nativeTelemetry,
578
578
  egress: provider.native.network,
579
579
  proxyPolicy: resolveNativeProxyPolicy(provider),
580
580
  affinityKey: proxyClientOptions.affinityKey,
@@ -596,15 +596,13 @@ function createProviderContext(provider, request, operationId, options, state =
596
596
  ? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
597
597
  : undefined) ??
598
598
  createUnsupportedResolverClient("Provider does not declare resolver capability"), signal),
599
- choice: createProviderChoiceContext({
599
+ handle: createHandleContext({
600
600
  providerId: provider.id,
601
- env,
602
601
  request: requestContext,
603
- credential,
604
602
  state: requestState,
605
603
  onTelemetry: (event) => (options.logger ?? defaultProviderServerLogger)({
606
604
  level: "info",
607
- event: "provider_choice_token",
605
+ event: "provider_handle",
608
606
  ...event,
609
607
  }),
610
608
  }),
@@ -732,6 +730,8 @@ function createAuthFlowContext(provider, request, route, options, state, scope,
732
730
  ? {
733
731
  native: {
734
732
  network: capabilityModules.nativeNetwork.createNativeNetworkClient({
733
+ telemetry: scope.telemetry.proxy,
734
+ nativeTelemetry: scope.nativeTelemetry,
735
735
  egress: provider.native.network,
736
736
  proxyPolicy: resolveNativeProxyPolicy(provider),
737
737
  affinityKey: proxyClientOptions.affinityKey,
@@ -884,6 +884,52 @@ function toErrorResponse(error, requestId, observabilityDetails, redact) {
884
884
  // narrowing from a ProviderError-typed value would collapse the negative branch
885
885
  // to `never`. Narrowing from unknown avoids that while still recognizing errors
886
886
  // from a duplicate SDK module instance.
887
+ function ownErrorCode(error) {
888
+ if (error === null || (typeof error !== "object" && typeof error !== "function")) {
889
+ return undefined;
890
+ }
891
+ try {
892
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
893
+ return descriptor && Object.hasOwn(descriptor, "value") && typeof descriptor.value === "string"
894
+ ? descriptor.value
895
+ : undefined;
896
+ }
897
+ catch {
898
+ return undefined;
899
+ }
900
+ }
901
+ function errorChainHasCode(error, expected) {
902
+ const seen = new Set();
903
+ let current = error;
904
+ for (let depth = 0; depth < 5; depth++) {
905
+ if (current === null || (typeof current !== "object" && typeof current !== "function")) {
906
+ return false;
907
+ }
908
+ if (seen.has(current))
909
+ return false;
910
+ seen.add(current);
911
+ let code;
912
+ try {
913
+ code = providerErrorCode(current) ?? ownErrorCode(current);
914
+ }
915
+ catch {
916
+ code = ownErrorCode(current);
917
+ }
918
+ if (code === expected)
919
+ return true;
920
+ let cause;
921
+ try {
922
+ cause = Object.getOwnPropertyDescriptor(current, "cause");
923
+ }
924
+ catch {
925
+ return false;
926
+ }
927
+ if (!cause || !Object.hasOwn(cause, "value"))
928
+ return false;
929
+ current = cause.value;
930
+ }
931
+ return false;
932
+ }
887
933
  function providerObservabilityDetails(error, declaredErrorCode) {
888
934
  const declaredRetryable = sdkOwnsErrorResolution(error)
889
935
  ? undefined
@@ -909,24 +955,43 @@ function providerObservabilityDetails(error, declaredErrorCode) {
909
955
  retryable: providerErrorOption(error, "retryable") ?? declaredRetryable ?? false,
910
956
  };
911
957
  }
958
+ // Native proxy resolution throws ProxyResolutionError directly, while HTTP
959
+ // normalizes it beneath a transport_network_error cause and stealth retains
960
+ // the original code. Preserve proxy attribution across all three forms.
961
+ if (errorChainHasCode(error, "PROXY_REQUIRED")) {
962
+ return {
963
+ category: "proxy_pool",
964
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
965
+ retryable: true,
966
+ };
967
+ }
912
968
  if (!isTransportError(error)) {
913
969
  return undefined;
914
970
  }
915
- const isProxyPoolCode = providerErrorCode(error) === PROXY_POOL_EXHAUSTED_CODE ||
916
- providerErrorCode(error) === PROXY_EDGE_AUTH_REJECTED_CODE ||
917
- providerErrorCode(error) === "PROXY_ALLOCATION_FAILED";
971
+ const errorCode = providerErrorCode(error);
972
+ const isProxyPoolCode = errorCode === PROXY_POOL_EXHAUSTED_CODE ||
973
+ errorCode === PROXY_EDGE_AUTH_REJECTED_CODE ||
974
+ errorCode === "PROXY_ALLOCATION_FAILED" ||
975
+ errorCode === "native_proxy_expired" ||
976
+ errorCode === "native_proxy_invalid";
918
977
  const category = providerErrorOption(error, "category") ??
919
978
  (isProxyPoolCode
920
979
  ? "proxy_pool"
921
- : providerErrorCode(error) === PROXY_AUTH_IP_DENIED_CODE
980
+ : errorCode === PROXY_AUTH_IP_DENIED_CODE
922
981
  ? "anti_bot_blocked"
923
- : providerErrorCode(error) === "transport_timeout"
982
+ : errorCode === "transport_timeout" ||
983
+ errorCode === "native_connection_timeout" ||
984
+ errorCode === "native_connection_idle_timeout"
924
985
  ? "timeout"
925
- : providerErrorCode(error) === "transport_network_error"
986
+ : errorCode === "transport_network_error" ||
987
+ errorCode === "native_connection_failed" ||
988
+ errorCode === "native_connection_closed"
926
989
  ? "network"
927
- : error.upstreamStatus
928
- ? categoryForStatus(error.upstreamStatus)
929
- : "upstream_http");
990
+ : errorCode === "native_connection_aborted"
991
+ ? "client_cancelled"
992
+ : error.upstreamStatus
993
+ ? categoryForStatus(error.upstreamStatus)
994
+ : "upstream_http");
930
995
  return {
931
996
  category,
932
997
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
@@ -1415,9 +1480,11 @@ function createRequestScope(input) {
1415
1480
  bindDiagnosticSensitiveRegistry(trace, sensitiveRegistry);
1416
1481
  const proxyCollector = new ProxyTelemetryCollector();
1417
1482
  const resolverCollector = new ResolverTelemetryCollector({ redact });
1483
+ const nativeCollector = new NativeTelemetryCollector({ redact });
1418
1484
  const telemetry = new RequestTelemetry(trace);
1419
1485
  telemetry.register(proxyCollector);
1420
1486
  telemetry.register(resolverCollector);
1487
+ telemetry.register(nativeCollector);
1421
1488
  let rootRunner = (fn) => fn();
1422
1489
  let resolveRoot;
1423
1490
  const rootTerminal = new Promise((resolve) => {
@@ -1494,6 +1561,7 @@ function createRequestScope(input) {
1494
1561
  trace,
1495
1562
  telemetry,
1496
1563
  resolverTelemetry: resolverCollector,
1564
+ nativeTelemetry: nativeCollector,
1497
1565
  seedCredentials(rawBody, kind) {
1498
1566
  const harvested = rawRequestCredentials(rawBody, kind);
1499
1567
  sensitiveRegistry.add(harvested.values);