@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.
- package/AUTHORING.md +33 -16
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +7 -8
- package/bin/apifuse-pack-types.ts +22 -0
- package/bin/apifuse-perf.ts +45 -10
- package/bin/apifuse-record.ts +6 -8
- package/bin/apifuse-submit-check.ts +2 -2
- package/dist/config/loader.js +13 -12
- package/dist/define.d.ts +3 -2
- package/dist/define.js +1 -1
- package/dist/dev.js +3 -1
- package/dist/engine.d.ts +7 -6
- package/dist/engine.js +6 -6
- package/dist/error-resolution.js +21 -4
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +9 -0
- package/dist/handle-meta.d.ts +39 -0
- package/dist/handle-meta.js +29 -0
- package/dist/handle.d.ts +187 -0
- package/dist/handle.js +254 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/lint.d.ts +3 -0
- package/dist/lint.js +388 -62
- package/dist/provider.d.ts +4 -4
- package/dist/provider.js +3 -3
- package/dist/runtime/browser.js +2 -1
- package/dist/runtime/cache.js +3 -2
- package/dist/runtime/diagnostic-env-values.d.ts +5 -0
- package/dist/runtime/diagnostic-env-values.js +57 -0
- package/dist/runtime/diagnostic-env.d.ts +11 -0
- package/dist/runtime/diagnostic-env.js +35 -0
- package/dist/runtime/diagnostic-redactor.d.ts +49 -0
- package/dist/runtime/diagnostic-redactor.js +569 -0
- package/dist/runtime/egress-lease.d.ts +2 -0
- package/dist/runtime/egress-lease.js +4 -1
- package/dist/runtime/env.js +2 -1
- package/dist/runtime/handle-wordlist.d.ts +43 -0
- package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +895 -0
- package/dist/runtime/instrumentation.js +3 -0
- package/dist/runtime/native-network.d.ts +10 -1
- package/dist/runtime/native-network.js +230 -29
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -0
- package/dist/runtime/ocr.js +2 -1
- package/dist/runtime/otlp.js +5 -4
- package/dist/runtime/proxy-nodemaven.js +3 -2
- package/dist/runtime/request-options.d.ts +7 -0
- package/dist/runtime/request-options.js +62 -22
- package/dist/runtime/resolver-vendors/browser.js +0 -1
- package/dist/runtime/resolver.js +3 -2
- package/dist/runtime/state.d.ts +4 -1
- package/dist/runtime/state.js +32 -17
- package/dist/runtime/stt.js +3 -2
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +115 -20
- package/dist/schema.d.ts +11 -0
- package/dist/schema.js +56 -2
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +128 -0
- package/dist/server/serve-implementation.d.ts +4 -11
- package/dist/server/serve-implementation.js +305 -96
- package/dist/server/trace-output.d.ts +2 -1
- package/dist/server/trace-output.js +46 -14
- package/dist/testing/run.js +10 -7
- package/dist/trace-sanitization.d.ts +4 -3
- package/dist/trace-sanitization.js +45 -22
- package/dist/types.d.ts +4 -107
- package/package.json +2 -1
- package/src/config/loader.ts +13 -12
- package/src/define.ts +4 -3
- package/src/dev.ts +3 -1
- package/src/engine.ts +8 -8
- package/src/error-resolution.ts +21 -4
- package/src/errors.ts +13 -0
- package/src/handle-meta.ts +58 -0
- package/src/handle.ts +529 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/index.ts +28 -13
- package/src/lint.ts +456 -71
- package/src/provider.ts +10 -20
- package/src/runtime/browser.ts +2 -1
- package/src/runtime/cache.ts +3 -2
- package/src/runtime/diagnostic-env-values.ts +56 -0
- package/src/runtime/diagnostic-env.ts +55 -0
- package/src/runtime/diagnostic-redactor.ts +662 -0
- package/src/runtime/egress-lease.ts +6 -1
- package/src/runtime/env.ts +2 -1
- package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
- package/src/runtime/handle.ts +1162 -0
- package/src/runtime/instrumentation.ts +3 -0
- package/src/runtime/native-network.ts +281 -33
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/ocr.ts +2 -1
- package/src/runtime/otlp.ts +5 -4
- package/src/runtime/proxy-nodemaven.ts +3 -2
- package/src/runtime/request-options.ts +76 -28
- package/src/runtime/resolver-vendors/browser.ts +0 -1
- package/src/runtime/resolver.ts +3 -2
- package/src/runtime/state.ts +44 -16
- package/src/runtime/stt.ts +3 -2
- package/src/runtime/trace.ts +168 -26
- package/src/schema.ts +63 -2
- package/src/server/index.ts +17 -0
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/sensitive-values.ts +197 -0
- package/src/server/serve-implementation.ts +388 -128
- package/src/server/trace-output.ts +77 -17
- package/src/testing/run.ts +11 -7
- package/src/trace-sanitization.ts +70 -23
- package/src/types.ts +4 -137
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice-wordlist.d.ts +0 -9
- package/dist/runtime/choice.d.ts +0 -27
- package/dist/runtime/choice.js +0 -801
- package/src/choice-token.ts +0 -165
- 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
|
+
}
|
package/src/runtime/ocr.ts
CHANGED
|
@@ -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
|
|
100
|
+
const value = readDiagnosticEnv(key, env)?.trim();
|
|
100
101
|
return value ? value : undefined;
|
|
101
102
|
}
|
|
102
103
|
|
package/src/runtime/otlp.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
47
|
+
return readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)?.trim() || undefined;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function readNodemavenPassword(): string | undefined {
|
|
50
|
-
return
|
|
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 =
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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(
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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(
|
|
229
|
-
)
|
|
230
|
-
|
|
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. */
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -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
|
|
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
|
|
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`);
|