@apifuse/provider-sdk 2.2.0-beta.59 → 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 (124) hide show
  1. package/AUTHORING.md +33 -16
  2. package/CHANGELOG.md +8 -0
  3. package/bin/apifuse-dev.ts +7 -8
  4. package/bin/apifuse-pack-types.ts +22 -0
  5. package/bin/apifuse-perf.ts +45 -10
  6. package/bin/apifuse-record.ts +6 -8
  7. package/bin/apifuse-submit-check.ts +2 -2
  8. package/dist/config/loader.js +13 -12
  9. package/dist/define.d.ts +3 -2
  10. package/dist/define.js +1 -1
  11. package/dist/dev.js +3 -1
  12. package/dist/engine.d.ts +7 -6
  13. package/dist/engine.js +6 -6
  14. package/dist/error-resolution.js +21 -4
  15. package/dist/errors.d.ts +3 -0
  16. package/dist/errors.js +9 -0
  17. package/dist/handle-meta.d.ts +39 -0
  18. package/dist/handle-meta.js +29 -0
  19. package/dist/handle.d.ts +187 -0
  20. package/dist/handle.js +254 -0
  21. package/dist/i18n/catalog.js +21 -1
  22. package/dist/index.d.ts +6 -4
  23. package/dist/index.js +4 -3
  24. package/dist/lint.d.ts +3 -0
  25. package/dist/lint.js +388 -62
  26. package/dist/provider.d.ts +4 -4
  27. package/dist/provider.js +3 -3
  28. package/dist/runtime/browser.js +2 -1
  29. package/dist/runtime/cache.js +3 -2
  30. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  31. package/dist/runtime/diagnostic-env-values.js +57 -0
  32. package/dist/runtime/diagnostic-env.d.ts +11 -0
  33. package/dist/runtime/diagnostic-env.js +35 -0
  34. package/dist/runtime/diagnostic-redactor.d.ts +49 -0
  35. package/dist/runtime/diagnostic-redactor.js +569 -0
  36. package/dist/runtime/egress-lease.d.ts +2 -0
  37. package/dist/runtime/egress-lease.js +4 -1
  38. package/dist/runtime/env.js +2 -1
  39. package/dist/runtime/handle-wordlist.d.ts +43 -0
  40. package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
  41. package/dist/runtime/handle.d.ts +71 -0
  42. package/dist/runtime/handle.js +895 -0
  43. package/dist/runtime/instrumentation.js +3 -0
  44. package/dist/runtime/native-network.d.ts +10 -1
  45. package/dist/runtime/native-network.js +230 -29
  46. package/dist/runtime/native-telemetry.d.ts +142 -0
  47. package/dist/runtime/native-telemetry.js +231 -0
  48. package/dist/runtime/ocr.js +2 -1
  49. package/dist/runtime/otlp.js +5 -4
  50. package/dist/runtime/proxy-nodemaven.js +3 -2
  51. package/dist/runtime/request-options.d.ts +7 -0
  52. package/dist/runtime/request-options.js +62 -22
  53. package/dist/runtime/resolver-vendors/browser.js +0 -1
  54. package/dist/runtime/resolver.js +3 -2
  55. package/dist/runtime/state.d.ts +4 -1
  56. package/dist/runtime/state.js +32 -17
  57. package/dist/runtime/stt.js +3 -2
  58. package/dist/runtime/trace.d.ts +5 -0
  59. package/dist/runtime/trace.js +115 -20
  60. package/dist/schema.d.ts +11 -0
  61. package/dist/schema.js +56 -2
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1 -0
  64. package/dist/server/self-test-redaction.js +8 -33
  65. package/dist/server/sensitive-values.d.ts +26 -0
  66. package/dist/server/sensitive-values.js +128 -0
  67. package/dist/server/serve-implementation.d.ts +4 -11
  68. package/dist/server/serve-implementation.js +305 -96
  69. package/dist/server/trace-output.d.ts +2 -1
  70. package/dist/server/trace-output.js +46 -14
  71. package/dist/testing/run.js +10 -7
  72. package/dist/trace-sanitization.d.ts +4 -3
  73. package/dist/trace-sanitization.js +45 -22
  74. package/dist/types.d.ts +4 -107
  75. package/package.json +2 -1
  76. package/src/config/loader.ts +13 -12
  77. package/src/define.ts +4 -3
  78. package/src/dev.ts +3 -1
  79. package/src/engine.ts +8 -8
  80. package/src/error-resolution.ts +21 -4
  81. package/src/errors.ts +13 -0
  82. package/src/handle-meta.ts +58 -0
  83. package/src/handle.ts +529 -0
  84. package/src/i18n/catalog.ts +23 -1
  85. package/src/index.ts +28 -13
  86. package/src/lint.ts +456 -71
  87. package/src/provider.ts +10 -20
  88. package/src/runtime/browser.ts +2 -1
  89. package/src/runtime/cache.ts +3 -2
  90. package/src/runtime/diagnostic-env-values.ts +56 -0
  91. package/src/runtime/diagnostic-env.ts +55 -0
  92. package/src/runtime/diagnostic-redactor.ts +662 -0
  93. package/src/runtime/egress-lease.ts +6 -1
  94. package/src/runtime/env.ts +2 -1
  95. package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
  96. package/src/runtime/handle.ts +1162 -0
  97. package/src/runtime/instrumentation.ts +3 -0
  98. package/src/runtime/native-network.ts +281 -33
  99. package/src/runtime/native-telemetry.ts +366 -0
  100. package/src/runtime/ocr.ts +2 -1
  101. package/src/runtime/otlp.ts +5 -4
  102. package/src/runtime/proxy-nodemaven.ts +3 -2
  103. package/src/runtime/request-options.ts +76 -28
  104. package/src/runtime/resolver-vendors/browser.ts +0 -1
  105. package/src/runtime/resolver.ts +3 -2
  106. package/src/runtime/state.ts +44 -16
  107. package/src/runtime/stt.ts +3 -2
  108. package/src/runtime/trace.ts +168 -26
  109. package/src/schema.ts +63 -2
  110. package/src/server/index.ts +17 -0
  111. package/src/server/self-test-redaction.ts +10 -40
  112. package/src/server/sensitive-values.ts +197 -0
  113. package/src/server/serve-implementation.ts +388 -128
  114. package/src/server/trace-output.ts +77 -17
  115. package/src/testing/run.ts +11 -7
  116. package/src/trace-sanitization.ts +70 -23
  117. package/src/types.ts +4 -137
  118. package/dist/choice-token.d.ts +0 -24
  119. package/dist/choice-token.js +0 -74
  120. package/dist/runtime/choice-wordlist.d.ts +0 -9
  121. package/dist/runtime/choice.d.ts +0 -27
  122. package/dist/runtime/choice.js +0 -801
  123. package/src/choice-token.ts +0 -165
  124. package/src/runtime/choice.ts +0 -1197
@@ -0,0 +1,366 @@
1
+ import { Buffer } from "node:buffer";
2
+ import type { ProviderProxyProvider } from "../types.js";
3
+ import type { NativeNetworkErrorCode } from "./native-network-errors.js";
4
+ import { closedEnum, type ClosedEnum, type TelemetryContributor } from "./request-telemetry.js";
5
+
6
+ export type NativeTelemetryErrorCode =
7
+ | NativeNetworkErrorCode
8
+ | "PROXY_REQUIRED"
9
+ | "PROXY_ALLOCATION_FAILED"
10
+ | "other";
11
+ export type NativeTelemetryKind = "tcp" | "tls";
12
+ export type NativeTelemetryOutcome = "ok" | "error";
13
+ export type NativeTelemetryVendorSkipReason =
14
+ | "credentials_absent"
15
+ | "protocol_unsupported"
16
+ | "allocation_failed"
17
+ | "credential_lookup_failed"
18
+ | "adapter_unavailable";
19
+ export type NativeTelemetryLifecycleKind =
20
+ | "drain"
21
+ | "drain_acknowledged"
22
+ | "drain_error"
23
+ | "drain_missing_handler"
24
+ | "idle"
25
+ | "expiry";
26
+
27
+ /** Operator-only text is redacted before it is detached and bounded to 300 code units. */
28
+ export type NativeTelemetryDiagnostics = {
29
+ host?: string;
30
+ serverName?: string;
31
+ protocol?: string;
32
+ sessionId?: string;
33
+ expiresAt?: string;
34
+ errorName?: string;
35
+ errorMessage?: string;
36
+ causeName?: string;
37
+ causeMessage?: string;
38
+ systemCode?: string;
39
+ missingFields?: readonly string[];
40
+ port?: number;
41
+ timeoutMs?: number;
42
+ idleTimeoutMs?: number;
43
+ status?: number;
44
+ socksReplyCode?: number;
45
+ sticky?: boolean;
46
+ };
47
+
48
+ export type NativeConnectTelemetryEvent = {
49
+ kind: NativeTelemetryKind;
50
+ outcome: NativeTelemetryOutcome;
51
+ /** Entire establishment, including lease resolution, tunnel and TLS handshake. */
52
+ ms: number;
53
+ /** Tunnel establishment only; included in ms, never added to it. */
54
+ tunnelMs: number;
55
+ proxyUsed: boolean;
56
+ /** Selected vendor for this connection; operator log only. */
57
+ vendor?: ProviderProxyProvider;
58
+ errorCode?: NativeTelemetryErrorCode;
59
+ diagnostics?: NativeTelemetryDiagnostics;
60
+ };
61
+ export type NativeVendorSkipTelemetryEvent = {
62
+ vendor: ProviderProxyProvider;
63
+ reason: NativeTelemetryVendorSkipReason;
64
+ diagnostics?: NativeTelemetryDiagnostics;
65
+ };
66
+ export type NativeLifecycleTelemetryEvent = {
67
+ kind: NativeTelemetryLifecycleKind;
68
+ errorCode?: NativeTelemetryErrorCode;
69
+ diagnostics?: NativeTelemetryDiagnostics;
70
+ };
71
+
72
+ export interface NativeTelemetrySink {
73
+ recordConnect(event: NativeConnectTelemetryEvent): void;
74
+ recordVendorSkip(event: NativeVendorSkipTelemetryEvent): void;
75
+ recordLifecycle(event: NativeLifecycleTelemetryEvent): void;
76
+ /** Application bytes returned by read or accepted by a successful write. Excludes tunnel/TLS framing. */
77
+ recordBytes(event: { direction: "in" | "out"; bytes: number }): void;
78
+ recordError(event: {
79
+ errorCode: NativeTelemetryErrorCode;
80
+ diagnostics?: NativeTelemetryDiagnostics;
81
+ }): void;
82
+ }
83
+
84
+ export type NativeAttemptSample = NativeConnectTelemetryEvent & { n: number };
85
+ export type NativeTelemetryLogPayload = {
86
+ attempts: number;
87
+ connectMs: number;
88
+ tunnelMs: number;
89
+ vendorSkips: number;
90
+ vendorSkipReasons: { reason: NativeTelemetryVendorSkipReason; count: number }[];
91
+ drain: number;
92
+ drainAcknowledged: number;
93
+ drainErrors: number;
94
+ drainMissingHandler: number;
95
+ idle: number;
96
+ expiry: number;
97
+ bytesIn: number;
98
+ bytesOut: number;
99
+ lastErrorCode?: NativeTelemetryErrorCode;
100
+ lastErrorDiagnostics?: NativeTelemetryDiagnostics;
101
+ attemptSamples?: NativeAttemptSample[];
102
+ attemptSamplesDropped?: number;
103
+ vendorSkipSamples?: NativeVendorSkipTelemetryEvent[];
104
+ vendorSkipSamplesDropped?: number;
105
+ };
106
+
107
+ export type NativeTelemetryHeaderPayload = {
108
+ attempts: number;
109
+ connectMs: number;
110
+ tunnelMs: number;
111
+ vendorSkips: number;
112
+ vendorSkipReasons: { reason: ClosedEnum<NativeTelemetryVendorSkipReason>; count: number }[];
113
+ drain: number;
114
+ drainAcknowledged: number;
115
+ drainErrors: number;
116
+ drainMissingHandler: number;
117
+ idle: number;
118
+ expiry: number;
119
+ bytesIn: number;
120
+ bytesOut: number;
121
+ lastErrorCode?: ClosedEnum<NativeTelemetryErrorCode>;
122
+ attemptSamples?: {
123
+ n: number;
124
+ kind: ClosedEnum<NativeTelemetryKind>;
125
+ outcome: ClosedEnum<NativeTelemetryOutcome>;
126
+ ms: number;
127
+ tunnelMs: number;
128
+ proxyUsed: boolean;
129
+ errorCode?: ClosedEnum<NativeTelemetryErrorCode>;
130
+ }[];
131
+ attemptSamplesDropped?: number;
132
+ vendorSkipSamplesDropped?: number;
133
+ };
134
+
135
+ const MAX_SAMPLES = 24;
136
+ function integer(value: number): number {
137
+ return Math.max(0, Math.floor(Number.isFinite(value) ? value : 0));
138
+ }
139
+ function saturatingAdd(a: number, b: number): number {
140
+ if (!Number.isFinite(a) || !Number.isFinite(b)) return Number.MAX_SAFE_INTEGER;
141
+ return Math.min(Number.MAX_SAFE_INTEGER, integer(a) + integer(b));
142
+ }
143
+ function boundedText(value: string, redact?: (text: string) => string): string {
144
+ let result: string;
145
+ try {
146
+ result = redact ? redact(value) : value;
147
+ if (typeof result !== "string") return "[REDACTION_FAILED]";
148
+ } catch {
149
+ return "[REDACTION_FAILED]";
150
+ }
151
+ // Do not slice caller text: a substring may retain the entire vendor response.
152
+ const length = Math.min(result.length, 300);
153
+ const units = Buffer.alloc(length * 2);
154
+ for (let index = 0; index < length; index++)
155
+ units.writeUInt16LE(result.charCodeAt(index), index * 2);
156
+ return units.toString("utf16le");
157
+ }
158
+ function diagnostics(
159
+ value: NativeTelemetryDiagnostics,
160
+ redact?: (text: string) => string,
161
+ ): NativeTelemetryDiagnostics {
162
+ const result: NativeTelemetryDiagnostics = {};
163
+ for (const key of [
164
+ "host",
165
+ "serverName",
166
+ "protocol",
167
+ "sessionId",
168
+ "expiresAt",
169
+ "errorName",
170
+ "errorMessage",
171
+ "causeName",
172
+ "causeMessage",
173
+ "systemCode",
174
+ ] as const) {
175
+ if (value[key] !== undefined) result[key] = boundedText(value[key], redact);
176
+ }
177
+ for (const key of ["port", "timeoutMs", "idleTimeoutMs", "status", "socksReplyCode"] as const) {
178
+ if (value[key] !== undefined) result[key] = integer(value[key]);
179
+ }
180
+ if (value.sticky !== undefined) result.sticky = value.sticky;
181
+ if (value.missingFields)
182
+ result.missingFields = value.missingFields
183
+ .slice(0, MAX_SAMPLES)
184
+ .map((field) => boundedText(field, redact));
185
+ return result;
186
+ }
187
+ function copyDiagnostics(value: NativeTelemetryDiagnostics): NativeTelemetryDiagnostics {
188
+ return { ...value, ...(value.missingFields ? { missingFields: [...value.missingFields] } : {}) };
189
+ }
190
+
191
+ export class NativeTelemetryCollector
192
+ implements
193
+ NativeTelemetrySink,
194
+ TelemetryContributor<NativeTelemetryLogPayload, NativeTelemetryHeaderPayload>
195
+ {
196
+ readonly key = "native" as const;
197
+ readonly #redact: ((text: string) => string) | undefined;
198
+ #recorded = false;
199
+ #attempts = 0;
200
+ #connectMs = 0;
201
+ #tunnelMs = 0;
202
+ #vendorSkips = 0;
203
+ #vendorSkipReasons = new Map<NativeTelemetryVendorSkipReason, number>();
204
+ #lifecycle = {
205
+ drain: 0,
206
+ drain_acknowledged: 0,
207
+ drain_error: 0,
208
+ drain_missing_handler: 0,
209
+ idle: 0,
210
+ expiry: 0,
211
+ };
212
+ #bytesIn = 0;
213
+ #bytesOut = 0;
214
+ #lastErrorCode: NativeTelemetryErrorCode | undefined;
215
+ #lastErrorDiagnostics: NativeTelemetryDiagnostics | undefined;
216
+ #attemptSamples: NativeAttemptSample[] = [];
217
+ #attemptSamplesDropped = 0;
218
+ #vendorSkipSamples: NativeVendorSkipTelemetryEvent[] = [];
219
+ #vendorSkipSamplesDropped = 0;
220
+
221
+ constructor(options: { redact?: (text: string) => string } = {}) {
222
+ this.#redact = options.redact;
223
+ }
224
+
225
+ recordConnect(event: NativeConnectTelemetryEvent): void {
226
+ this.#recorded = true;
227
+ const ms = saturatingAdd(0, event.ms);
228
+ const tunnelMs = saturatingAdd(0, event.tunnelMs);
229
+ this.#attempts = saturatingAdd(this.#attempts, 1);
230
+ this.#connectMs = saturatingAdd(this.#connectMs, ms);
231
+ this.#tunnelMs = saturatingAdd(this.#tunnelMs, tunnelMs);
232
+ if (event.errorCode)
233
+ this.recordError({ errorCode: event.errorCode, diagnostics: event.diagnostics });
234
+ if (this.#attemptSamples.length < MAX_SAMPLES) {
235
+ this.#attemptSamples.push({
236
+ n: this.#attempts,
237
+ kind: event.kind,
238
+ outcome: event.outcome,
239
+ ms,
240
+ tunnelMs,
241
+ proxyUsed: event.proxyUsed,
242
+ ...(event.vendor ? { vendor: event.vendor } : {}),
243
+ ...(event.errorCode ? { errorCode: event.errorCode } : {}),
244
+ ...(event.diagnostics ? { diagnostics: diagnostics(event.diagnostics, this.#redact) } : {}),
245
+ });
246
+ } else this.#attemptSamplesDropped = saturatingAdd(this.#attemptSamplesDropped, 1);
247
+ }
248
+ recordVendorSkip(event: NativeVendorSkipTelemetryEvent): void {
249
+ this.#recorded = true;
250
+ this.#vendorSkips = saturatingAdd(this.#vendorSkips, 1);
251
+ this.#vendorSkipReasons.set(
252
+ event.reason,
253
+ saturatingAdd(this.#vendorSkipReasons.get(event.reason) ?? 0, 1),
254
+ );
255
+ if (this.#vendorSkipSamples.length < MAX_SAMPLES) {
256
+ this.#vendorSkipSamples.push({
257
+ vendor: event.vendor,
258
+ reason: event.reason,
259
+ ...(event.diagnostics ? { diagnostics: diagnostics(event.diagnostics, this.#redact) } : {}),
260
+ });
261
+ } else this.#vendorSkipSamplesDropped = saturatingAdd(this.#vendorSkipSamplesDropped, 1);
262
+ }
263
+ recordLifecycle(event: NativeLifecycleTelemetryEvent): void {
264
+ this.#recorded = true;
265
+ this.#lifecycle[event.kind] = saturatingAdd(this.#lifecycle[event.kind], 1);
266
+ if (event.errorCode)
267
+ this.recordError({ errorCode: event.errorCode, diagnostics: event.diagnostics });
268
+ }
269
+ recordBytes(event: { direction: "in" | "out"; bytes: number }): void {
270
+ this.#recorded = true;
271
+ if (event.direction === "in") this.#bytesIn = saturatingAdd(this.#bytesIn, event.bytes);
272
+ else this.#bytesOut = saturatingAdd(this.#bytesOut, event.bytes);
273
+ }
274
+ recordError(event: {
275
+ errorCode: NativeTelemetryErrorCode;
276
+ diagnostics?: NativeTelemetryDiagnostics;
277
+ }): void {
278
+ this.#recorded = true;
279
+ this.#lastErrorCode = event.errorCode;
280
+ this.#lastErrorDiagnostics = event.diagnostics
281
+ ? diagnostics(event.diagnostics, this.#redact)
282
+ : undefined;
283
+ }
284
+ toLogPayload(): NativeTelemetryLogPayload | undefined {
285
+ if (!this.#recorded) return undefined;
286
+ return {
287
+ attempts: this.#attempts,
288
+ connectMs: this.#connectMs,
289
+ tunnelMs: this.#tunnelMs,
290
+ vendorSkips: this.#vendorSkips,
291
+ vendorSkipReasons: [...this.#vendorSkipReasons].map(([reason, count]) => ({ reason, count })),
292
+ drain: this.#lifecycle.drain,
293
+ drainAcknowledged: this.#lifecycle.drain_acknowledged,
294
+ drainErrors: this.#lifecycle.drain_error,
295
+ drainMissingHandler: this.#lifecycle.drain_missing_handler,
296
+ idle: this.#lifecycle.idle,
297
+ expiry: this.#lifecycle.expiry,
298
+ bytesIn: this.#bytesIn,
299
+ bytesOut: this.#bytesOut,
300
+ ...(this.#lastErrorCode ? { lastErrorCode: this.#lastErrorCode } : {}),
301
+ ...(this.#lastErrorDiagnostics
302
+ ? { lastErrorDiagnostics: copyDiagnostics(this.#lastErrorDiagnostics) }
303
+ : {}),
304
+ ...(this.#attemptSamples.length
305
+ ? {
306
+ attemptSamples: this.#attemptSamples.map((sample) => ({
307
+ ...sample,
308
+ ...(sample.diagnostics ? { diagnostics: copyDiagnostics(sample.diagnostics) } : {}),
309
+ })),
310
+ }
311
+ : {}),
312
+ ...(this.#attemptSamplesDropped
313
+ ? { attemptSamplesDropped: this.#attemptSamplesDropped }
314
+ : {}),
315
+ ...(this.#vendorSkipSamples.length
316
+ ? {
317
+ vendorSkipSamples: this.#vendorSkipSamples.map((sample) => ({
318
+ ...sample,
319
+ ...(sample.diagnostics ? { diagnostics: copyDiagnostics(sample.diagnostics) } : {}),
320
+ })),
321
+ }
322
+ : {}),
323
+ ...(this.#vendorSkipSamplesDropped
324
+ ? { vendorSkipSamplesDropped: this.#vendorSkipSamplesDropped }
325
+ : {}),
326
+ };
327
+ }
328
+ toHeaderPayload(log: NativeTelemetryLogPayload): NativeTelemetryHeaderPayload {
329
+ return {
330
+ attempts: log.attempts,
331
+ connectMs: log.connectMs,
332
+ tunnelMs: log.tunnelMs,
333
+ vendorSkips: log.vendorSkips,
334
+ vendorSkipReasons: log.vendorSkipReasons.map(({ reason, count }) => ({
335
+ reason: closedEnum(reason),
336
+ count,
337
+ })),
338
+ drain: log.drain,
339
+ drainAcknowledged: log.drainAcknowledged,
340
+ drainErrors: log.drainErrors,
341
+ drainMissingHandler: log.drainMissingHandler,
342
+ idle: log.idle,
343
+ expiry: log.expiry,
344
+ bytesIn: log.bytesIn,
345
+ bytesOut: log.bytesOut,
346
+ ...(log.lastErrorCode ? { lastErrorCode: closedEnum(log.lastErrorCode) } : {}),
347
+ ...(log.attemptSamples
348
+ ? {
349
+ attemptSamples: log.attemptSamples.map((sample) => ({
350
+ n: sample.n,
351
+ kind: closedEnum(sample.kind),
352
+ outcome: closedEnum(sample.outcome),
353
+ ms: sample.ms,
354
+ tunnelMs: sample.tunnelMs,
355
+ proxyUsed: sample.proxyUsed,
356
+ ...(sample.errorCode ? { errorCode: closedEnum(sample.errorCode) } : {}),
357
+ })),
358
+ }
359
+ : {}),
360
+ ...(log.attemptSamplesDropped ? { attemptSamplesDropped: log.attemptSamplesDropped } : {}),
361
+ ...(log.vendorSkipSamplesDropped
362
+ ? { vendorSkipSamplesDropped: log.vendorSkipSamplesDropped }
363
+ : {}),
364
+ };
365
+ }
366
+ }
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "./diagnostic-env.js";
1
2
  import { ProviderError, TransportError } from "../errors.js";
2
3
  import type {
3
4
  OcrCaptchaCandidate,
@@ -96,7 +97,7 @@ export function createUnsupportedOcrClient(reason?: string): OcrContext {
96
97
  }
97
98
 
98
99
  function normalizedEnvValue(env: EnvLike, key: string): string | undefined {
99
- const value = env[key]?.trim();
100
+ const value = readDiagnosticEnv(key, env)?.trim();
100
101
  return value ? value : undefined;
101
102
  }
102
103
 
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "./diagnostic-env.js";
1
2
  import { AsyncResource } from "node:async_hooks";
2
3
 
3
4
  import type { TraceSpan } from "../types.js";
@@ -153,12 +154,12 @@ export function resolveOTLPExportOptions(
153
154
  { source: EXPLICIT_ENDPOINT_SOURCE, value: explicit.endpoint, appendPath: false },
154
155
  {
155
156
  source: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
156
- value: env[OTEL_EXPORTER_OTLP_TRACES_ENDPOINT],
157
+ value: readDiagnosticEnv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, env),
157
158
  appendPath: false,
158
159
  },
159
160
  {
160
161
  source: OTEL_EXPORTER_OTLP_ENDPOINT,
161
- value: env[OTEL_EXPORTER_OTLP_ENDPOINT],
162
+ value: readDiagnosticEnv(OTEL_EXPORTER_OTLP_ENDPOINT, env),
162
163
  appendPath: true,
163
164
  },
164
165
  ];
@@ -173,11 +174,11 @@ export function resolveOTLPExportOptions(
173
174
  }
174
175
  const endpoint = candidate.appendPath ? appendTracesPath(parsed.url) : candidate.value;
175
176
 
176
- const tracesHeaders = presentValue(env[OTEL_EXPORTER_OTLP_TRACES_HEADERS]);
177
+ const tracesHeaders = presentValue(readDiagnosticEnv(OTEL_EXPORTER_OTLP_TRACES_HEADERS, env));
177
178
  const headersSource =
178
179
  tracesHeaders !== undefined ? OTEL_EXPORTER_OTLP_TRACES_HEADERS : OTEL_EXPORTER_OTLP_HEADERS;
179
180
  const envHeaders = parseHeaderList(
180
- tracesHeaders ?? presentValue(env[OTEL_EXPORTER_OTLP_HEADERS]),
181
+ tracesHeaders ?? presentValue(readDiagnosticEnv(OTEL_EXPORTER_OTLP_HEADERS, env)),
181
182
  );
182
183
  if (!headersAreSendable(envHeaders)) {
183
184
  return { status: "invalid", source: headersSource, reason: INVALID_HEADERS };
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "./diagnostic-env.js";
1
2
  import { createHash, randomBytes } from "node:crypto";
2
3
 
3
4
  import type { ProviderProxyPolicy } from "../types.js";
@@ -43,11 +44,11 @@ export function hasNodemavenCredentials(): boolean {
43
44
  }
44
45
 
45
46
  function readNodemavenUsername(): string | undefined {
46
- return process.env[NODEMAVEN_USERNAME_ENV]?.trim() || undefined;
47
+ return readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)?.trim() || undefined;
47
48
  }
48
49
 
49
50
  function readNodemavenPassword(): string | undefined {
50
- return process.env[NODEMAVEN_PASSWORD_ENV]?.trim() || undefined;
51
+ return readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)?.trim() || undefined;
51
52
  }
52
53
 
53
54
  function resolveNodemavenFilter(value: string | undefined): string {
@@ -1,3 +1,4 @@
1
+ import { isProviderError } from "../errors.js";
1
2
  import type {
2
3
  HttpClient,
3
4
  RequestOptions,
@@ -5,7 +6,6 @@ import type {
5
6
  RequestParams,
6
7
  RequestParamValue,
7
8
  } from "../types.js";
8
- import { isProviderError } from "../errors.js";
9
9
 
10
10
  export const REDACTED_QUERY_VALUE = "[REDACTED]";
11
11
 
@@ -161,15 +161,34 @@ export function serializeRequestUrl(
161
161
  };
162
162
  }
163
163
 
164
- const MIN_UNSCOPED_SENSITIVE_VALUE_LENGTH = 4;
164
+ const MIN_UNSCOPED_SENSITIVE_VALUE_LENGTH = 8;
165
165
 
166
- function normalizePercentEscapes(value: string): string {
166
+ export function normalizePercentEscapes(value: string): string {
167
167
  return value.replace(/%[\da-f]{2}/gi, (percentEscape) => percentEscape.toUpperCase());
168
168
  }
169
169
 
170
170
  function sensitiveValueVariants(value: string): string[] {
171
171
  if (!value || value === REDACTED_QUERY_VALUE) return [];
172
172
 
173
+ // Owner-visible policy: short credentials are literal, case-sensitive tokens only.
174
+ // Encoded short needles collide with benign cursors and ordinary diagnostic text.
175
+ if (value.length < 8) return [value];
176
+ // These ASCII characters are identical under NFC, JSON, URI-component and
177
+ // form encoding. Avoid constructing those identical variants for common tokens.
178
+ if (/^[A-Za-z0-9._*-]+$/.test(value)) {
179
+ const base64 = Buffer.from(value, "utf8").toString("base64");
180
+ const base64Url = base64.replaceAll("+", "-").replaceAll("/", "_");
181
+ return [
182
+ ...new Set([
183
+ value,
184
+ base64,
185
+ base64.replace(/=+$/, ""),
186
+ base64Url,
187
+ base64Url.replace(/=+$/, ""),
188
+ ]),
189
+ ];
190
+ }
191
+ const normalized = value.normalize("NFC");
173
192
  const formEncoded = new URLSearchParams({ value }).toString().slice("value=".length);
174
193
  let componentEncoded: string;
175
194
  try {
@@ -179,15 +198,42 @@ function sensitiveValueVariants(value: string): string[] {
179
198
  // still safe to scrub, and URLSearchParams supplies its well-formed form.
180
199
  componentEncoded = value;
181
200
  }
182
- return [...new Set([value, componentEncoded, formEncoded].map(normalizePercentEscapes))];
183
- }
184
-
185
- type SensitiveValueVariant = { value: string; requiresTokenBoundary: boolean };
201
+ const base64 = Buffer.from(value, "utf8").toString("base64");
202
+ const base64Url = base64.replaceAll("+", "-").replaceAll("/", "_");
203
+ const jsonEscaped = JSON.stringify(value).slice(1, -1);
204
+ return [
205
+ ...new Set(
206
+ [
207
+ value,
208
+ normalized,
209
+ componentEncoded,
210
+ formEncoded,
211
+ jsonEscaped,
212
+ base64,
213
+ base64.replace(/=+$/, ""),
214
+ base64Url,
215
+ base64Url.replace(/=+$/, ""),
216
+ ].map(normalizePercentEscapes),
217
+ ),
218
+ ];
219
+ }
220
+
221
+ export type SensitiveValueVariant = {
222
+ value: string;
223
+ requiresTokenBoundary: boolean;
224
+ caseInsensitive: boolean;
225
+ };
186
226
 
187
- function allSensitiveValueVariants(sensitiveValues: readonly string[]): SensitiveValueVariant[] {
227
+ export function allSensitiveValueVariants(
228
+ sensitiveValues: readonly string[],
229
+ ): SensitiveValueVariant[] {
188
230
  const variants = new Map<string, SensitiveValueVariant>();
189
231
  for (const sensitiveValue of sensitiveValues) {
190
232
  const requiresTokenBoundary = sensitiveValue.length < MIN_UNSCOPED_SENSITIVE_VALUE_LENGTH;
233
+ // Long values are unlikely to be ordinary prose, so case folding is worth
234
+ // the extra over-redaction protection. Values of 4-7 characters remain
235
+ // case-sensitive because they commonly occur as words and identifiers.
236
+ const caseInsensitive = sensitiveValue.length >= 8;
191
237
  for (const value of sensitiveValueVariants(sensitiveValue)) {
192
238
  const existing = variants.get(value);
193
239
  variants.set(value, {
@@ -195,6 +241,7 @@ function allSensitiveValueVariants(sensitiveValues: readonly string[]): Sensitiv
195
241
  requiresTokenBoundary: existing
196
242
  ? existing.requiresTokenBoundary && requiresTokenBoundary
197
243
  : requiresTokenBoundary,
244
+ caseInsensitive: existing ? existing.caseInsensitive || caseInsensitive : caseInsensitive,
198
245
  });
199
246
  }
200
247
  }
@@ -209,28 +256,29 @@ function isTokenCharacter(value: string | undefined): boolean {
209
256
  }
210
257
 
211
258
  function replaceSensitiveVariant(text: string, variant: SensitiveValueVariant): string {
212
- const pattern = variant.value.replace(/%([\da-f]{2})|[.*+?^${}()|[\]\\]/gi, (match, hex) => {
213
- if (hex) {
214
- return `%${[...String(hex)]
215
- .map((character) =>
216
- /[a-f]/i.test(character)
217
- ? `[${character.toLowerCase()}${character.toUpperCase()}]`
218
- : character,
219
- )
220
- .join("")}`;
221
- }
222
- return `\\${match}`;
223
- });
224
- return text.replace(new RegExp(pattern, "gu"), (match, offset: number, source: string) => {
225
- const end = offset + match.length;
259
+ const normalized = normalizePercentEscapes(text);
260
+ const comparable = variant.caseInsensitive ? normalized.toLowerCase() : text;
261
+ const needle = variant.caseInsensitive ? variant.value.toLowerCase() : variant.value;
262
+ let cursor = 0;
263
+ let output = "";
264
+ for (
265
+ let index = comparable.indexOf(needle);
266
+ index !== -1;
267
+ index = comparable.indexOf(needle, index + 1)
268
+ ) {
269
+ const end = index + needle.length;
226
270
  if (
227
271
  variant.requiresTokenBoundary &&
228
- (isTokenCharacter(source[offset - 1]) || isTokenCharacter(source[end]))
229
- ) {
230
- return match;
231
- }
232
- return REDACTED_QUERY_VALUE;
233
- });
272
+ (isTokenCharacter(comparable[index - 1]) || isTokenCharacter(comparable[end]))
273
+ )
274
+ continue;
275
+ // Unicode folding can change offsets; suppress the field instead of slicing a secret.
276
+ if (comparable.length !== text.length) return REDACTED_QUERY_VALUE;
277
+ output += text.slice(cursor, index) + REDACTED_QUERY_VALUE;
278
+ cursor = end;
279
+ index = end - 1;
280
+ }
281
+ return cursor ? output + text.slice(cursor) : text;
234
282
  }
235
283
 
236
284
  /** Scrubs raw and URL-encoded secret values from diagnostic text. */
@@ -173,7 +173,6 @@ async function runBoundedCleanup(
173
173
  onError(error) {
174
174
  return {
175
175
  error_message: error instanceof Error ? error.message : String(error),
176
- ...(error instanceof Error && error.stack ? { error_stack: error.stack } : {}),
177
176
  };
178
177
  },
179
178
  })
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "./diagnostic-env.js";
1
2
  import { createHash } from "node:crypto";
2
3
 
3
4
  import {
@@ -309,12 +310,12 @@ type ResolverChainEntry = {
309
310
  };
310
311
 
311
312
  function normalizedEnvValue(env: EnvLike, key: string): string | undefined {
312
- const value = env[key]?.trim();
313
+ const value = readDiagnosticEnv(key, env)?.trim();
313
314
  return value ? value : undefined;
314
315
  }
315
316
 
316
317
  function readPositiveIntegerEnv(env: EnvLike, name: string): string | undefined {
317
- const raw = env[name]?.trim();
318
+ const raw = readDiagnosticEnv(name, env)?.trim();
318
319
  if (!raw) return undefined;
319
320
  if (!/^[1-9]\d*$/.test(raw)) {
320
321
  throw new Error(`${name} must be a positive integer`);