@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,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
|
+
}
|
package/dist/runtime/ocr.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./diagnostic-env.js";
|
|
1
2
|
import { ProviderError, TransportError } from "../errors.js";
|
|
2
3
|
import { CLOUDFLARE_ACCOUNT_ID_ENV } from "./stt.js";
|
|
3
4
|
import { createTimeoutController, isTimeoutLikeError } from "./timeout.js";
|
|
@@ -55,7 +56,7 @@ export function createUnsupportedOcrClient(reason) {
|
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
function normalizedEnvValue(env, key) {
|
|
58
|
-
const value = env
|
|
59
|
+
const value = readDiagnosticEnv(key, env)?.trim();
|
|
59
60
|
return value ? value : undefined;
|
|
60
61
|
}
|
|
61
62
|
export function createOcrClientFromEnv(config, env = process.env) {
|
package/dist/runtime/otlp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./diagnostic-env.js";
|
|
1
2
|
import { AsyncResource } from "node:async_hooks";
|
|
2
3
|
export const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
|
|
3
4
|
export const OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT";
|
|
@@ -118,12 +119,12 @@ export function resolveOTLPExportOptions(explicit, env = process.env) {
|
|
|
118
119
|
{ source: EXPLICIT_ENDPOINT_SOURCE, value: explicit.endpoint, appendPath: false },
|
|
119
120
|
{
|
|
120
121
|
source: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
|
|
121
|
-
value:
|
|
122
|
+
value: readDiagnosticEnv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, env),
|
|
122
123
|
appendPath: false,
|
|
123
124
|
},
|
|
124
125
|
{
|
|
125
126
|
source: OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
126
|
-
value:
|
|
127
|
+
value: readDiagnosticEnv(OTEL_EXPORTER_OTLP_ENDPOINT, env),
|
|
127
128
|
appendPath: true,
|
|
128
129
|
},
|
|
129
130
|
];
|
|
@@ -137,9 +138,9 @@ export function resolveOTLPExportOptions(explicit, env = process.env) {
|
|
|
137
138
|
return { status: "invalid", source: candidate.source, reason: parsed.reason };
|
|
138
139
|
}
|
|
139
140
|
const endpoint = candidate.appendPath ? appendTracesPath(parsed.url) : candidate.value;
|
|
140
|
-
const tracesHeaders = presentValue(env
|
|
141
|
+
const tracesHeaders = presentValue(readDiagnosticEnv(OTEL_EXPORTER_OTLP_TRACES_HEADERS, env));
|
|
141
142
|
const headersSource = tracesHeaders !== undefined ? OTEL_EXPORTER_OTLP_TRACES_HEADERS : OTEL_EXPORTER_OTLP_HEADERS;
|
|
142
|
-
const envHeaders = parseHeaderList(tracesHeaders ?? presentValue(env
|
|
143
|
+
const envHeaders = parseHeaderList(tracesHeaders ?? presentValue(readDiagnosticEnv(OTEL_EXPORTER_OTLP_HEADERS, env)));
|
|
143
144
|
if (!headersAreSendable(envHeaders)) {
|
|
144
145
|
return { status: "invalid", source: headersSource, reason: INVALID_HEADERS };
|
|
145
146
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./diagnostic-env.js";
|
|
1
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
3
|
export const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
3
4
|
export const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
@@ -25,10 +26,10 @@ export function hasNodemavenCredentials() {
|
|
|
25
26
|
return Boolean(readNodemavenUsername() && readNodemavenPassword());
|
|
26
27
|
}
|
|
27
28
|
function readNodemavenUsername() {
|
|
28
|
-
return
|
|
29
|
+
return readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)?.trim() || undefined;
|
|
29
30
|
}
|
|
30
31
|
function readNodemavenPassword() {
|
|
31
|
-
return
|
|
32
|
+
return readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)?.trim() || undefined;
|
|
32
33
|
}
|
|
33
34
|
function resolveNodemavenFilter(value) {
|
|
34
35
|
const raw = value?.trim().toLowerCase();
|
|
@@ -51,6 +51,13 @@ export declare function redactUrlQueryParams(url: string, sensitiveParamNames: r
|
|
|
51
51
|
* separate, structurally redacted URL for diagnostics and observability.
|
|
52
52
|
*/
|
|
53
53
|
export declare function serializeRequestUrl(url: string, params?: RequestParams, sensitiveParams?: Record<string, string>): SerializedRequestUrl;
|
|
54
|
+
export declare function normalizePercentEscapes(value: string): string;
|
|
55
|
+
export type SensitiveValueVariant = {
|
|
56
|
+
value: string;
|
|
57
|
+
requiresTokenBoundary: boolean;
|
|
58
|
+
caseInsensitive: boolean;
|
|
59
|
+
};
|
|
60
|
+
export declare function allSensitiveValueVariants(sensitiveValues: readonly string[]): SensitiveValueVariant[];
|
|
54
61
|
/** Scrubs raw and URL-encoded secret values from diagnostic text. */
|
|
55
62
|
export declare function redactSensitiveText(text: string, sensitiveValues: readonly string[], requestUrl?: string, redactedUrl?: string): string;
|
|
56
63
|
/**
|
|
@@ -108,13 +108,33 @@ export function serializeRequestUrl(url, params, sensitiveParams) {
|
|
|
108
108
|
sensitiveValues: [...sensitiveValues],
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
|
-
const MIN_UNSCOPED_SENSITIVE_VALUE_LENGTH =
|
|
112
|
-
function normalizePercentEscapes(value) {
|
|
111
|
+
const MIN_UNSCOPED_SENSITIVE_VALUE_LENGTH = 8;
|
|
112
|
+
export function normalizePercentEscapes(value) {
|
|
113
113
|
return value.replace(/%[\da-f]{2}/gi, (percentEscape) => percentEscape.toUpperCase());
|
|
114
114
|
}
|
|
115
115
|
function sensitiveValueVariants(value) {
|
|
116
116
|
if (!value || value === REDACTED_QUERY_VALUE)
|
|
117
117
|
return [];
|
|
118
|
+
// Owner-visible policy: short credentials are literal, case-sensitive tokens only.
|
|
119
|
+
// Encoded short needles collide with benign cursors and ordinary diagnostic text.
|
|
120
|
+
if (value.length < 8)
|
|
121
|
+
return [value];
|
|
122
|
+
// These ASCII characters are identical under NFC, JSON, URI-component and
|
|
123
|
+
// form encoding. Avoid constructing those identical variants for common tokens.
|
|
124
|
+
if (/^[A-Za-z0-9._*-]+$/.test(value)) {
|
|
125
|
+
const base64 = Buffer.from(value, "utf8").toString("base64");
|
|
126
|
+
const base64Url = base64.replaceAll("+", "-").replaceAll("/", "_");
|
|
127
|
+
return [
|
|
128
|
+
...new Set([
|
|
129
|
+
value,
|
|
130
|
+
base64,
|
|
131
|
+
base64.replace(/=+$/, ""),
|
|
132
|
+
base64Url,
|
|
133
|
+
base64Url.replace(/=+$/, ""),
|
|
134
|
+
]),
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
const normalized = value.normalize("NFC");
|
|
118
138
|
const formEncoded = new URLSearchParams({ value }).toString().slice("value=".length);
|
|
119
139
|
let componentEncoded;
|
|
120
140
|
try {
|
|
@@ -125,12 +145,31 @@ function sensitiveValueVariants(value) {
|
|
|
125
145
|
// still safe to scrub, and URLSearchParams supplies its well-formed form.
|
|
126
146
|
componentEncoded = value;
|
|
127
147
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
const base64 = Buffer.from(value, "utf8").toString("base64");
|
|
149
|
+
const base64Url = base64.replaceAll("+", "-").replaceAll("/", "_");
|
|
150
|
+
const jsonEscaped = JSON.stringify(value).slice(1, -1);
|
|
151
|
+
return [
|
|
152
|
+
...new Set([
|
|
153
|
+
value,
|
|
154
|
+
normalized,
|
|
155
|
+
componentEncoded,
|
|
156
|
+
formEncoded,
|
|
157
|
+
jsonEscaped,
|
|
158
|
+
base64,
|
|
159
|
+
base64.replace(/=+$/, ""),
|
|
160
|
+
base64Url,
|
|
161
|
+
base64Url.replace(/=+$/, ""),
|
|
162
|
+
].map(normalizePercentEscapes)),
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
export function allSensitiveValueVariants(sensitiveValues) {
|
|
131
166
|
const variants = new Map();
|
|
132
167
|
for (const sensitiveValue of sensitiveValues) {
|
|
133
168
|
const requiresTokenBoundary = sensitiveValue.length < MIN_UNSCOPED_SENSITIVE_VALUE_LENGTH;
|
|
169
|
+
// Long values are unlikely to be ordinary prose, so case folding is worth
|
|
170
|
+
// the extra over-redaction protection. Values of 4-7 characters remain
|
|
171
|
+
// case-sensitive because they commonly occur as words and identifiers.
|
|
172
|
+
const caseInsensitive = sensitiveValue.length >= 8;
|
|
134
173
|
for (const value of sensitiveValueVariants(sensitiveValue)) {
|
|
135
174
|
const existing = variants.get(value);
|
|
136
175
|
variants.set(value, {
|
|
@@ -138,6 +177,7 @@ function allSensitiveValueVariants(sensitiveValues) {
|
|
|
138
177
|
requiresTokenBoundary: existing
|
|
139
178
|
? existing.requiresTokenBoundary && requiresTokenBoundary
|
|
140
179
|
: requiresTokenBoundary,
|
|
180
|
+
caseInsensitive: existing ? existing.caseInsensitive || caseInsensitive : caseInsensitive,
|
|
141
181
|
});
|
|
142
182
|
}
|
|
143
183
|
}
|
|
@@ -147,24 +187,24 @@ function isTokenCharacter(value) {
|
|
|
147
187
|
return value !== undefined && /[\p{L}\p{N}]/u.test(value);
|
|
148
188
|
}
|
|
149
189
|
function replaceSensitiveVariant(text, variant) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
return `\\${match}`;
|
|
159
|
-
});
|
|
160
|
-
return text.replace(new RegExp(pattern, "gu"), (match, offset, source) => {
|
|
161
|
-
const end = offset + match.length;
|
|
190
|
+
const normalized = normalizePercentEscapes(text);
|
|
191
|
+
const comparable = variant.caseInsensitive ? normalized.toLowerCase() : text;
|
|
192
|
+
const needle = variant.caseInsensitive ? variant.value.toLowerCase() : variant.value;
|
|
193
|
+
let cursor = 0;
|
|
194
|
+
let output = "";
|
|
195
|
+
for (let index = comparable.indexOf(needle); index !== -1; index = comparable.indexOf(needle, index + 1)) {
|
|
196
|
+
const end = index + needle.length;
|
|
162
197
|
if (variant.requiresTokenBoundary &&
|
|
163
|
-
(isTokenCharacter(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
(isTokenCharacter(comparable[index - 1]) || isTokenCharacter(comparable[end])))
|
|
199
|
+
continue;
|
|
200
|
+
// Unicode folding can change offsets; suppress the field instead of slicing a secret.
|
|
201
|
+
if (comparable.length !== text.length)
|
|
202
|
+
return REDACTED_QUERY_VALUE;
|
|
203
|
+
output += text.slice(cursor, index) + REDACTED_QUERY_VALUE;
|
|
204
|
+
cursor = end;
|
|
205
|
+
index = end - 1;
|
|
206
|
+
}
|
|
207
|
+
return cursor ? output + text.slice(cursor) : text;
|
|
168
208
|
}
|
|
169
209
|
/** Scrubs raw and URL-encoded secret values from diagnostic text. */
|
|
170
210
|
export function redactSensitiveText(text, sensitiveValues, requestUrl, redactedUrl) {
|
|
@@ -111,7 +111,6 @@ async function runBoundedCleanup(cleanup, timeoutMs, challengeKind, operation, t
|
|
|
111
111
|
onError(error) {
|
|
112
112
|
return {
|
|
113
113
|
error_message: error instanceof Error ? error.message : String(error),
|
|
114
|
-
...(error instanceof Error && error.stack ? { error_stack: error.stack } : {}),
|
|
115
114
|
};
|
|
116
115
|
},
|
|
117
116
|
})
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./diagnostic-env.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { resolveProxyConfigAsync, } from "../config/loader.js";
|
|
3
4
|
import { ProviderError } from "../errors.js";
|
|
@@ -130,11 +131,11 @@ const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS = new Set([
|
|
|
130
131
|
"custom",
|
|
131
132
|
]);
|
|
132
133
|
function normalizedEnvValue(env, key) {
|
|
133
|
-
const value = env
|
|
134
|
+
const value = readDiagnosticEnv(key, env)?.trim();
|
|
134
135
|
return value ? value : undefined;
|
|
135
136
|
}
|
|
136
137
|
function readPositiveIntegerEnv(env, name) {
|
|
137
|
-
const raw = env
|
|
138
|
+
const raw = readDiagnosticEnv(name, env)?.trim();
|
|
138
139
|
if (!raw)
|
|
139
140
|
return undefined;
|
|
140
141
|
if (!/^[1-9]\d*$/.test(raw)) {
|
package/dist/runtime/state.d.ts
CHANGED
|
@@ -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(
|
|
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 {};
|
package/dist/runtime/state.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
envelope
|
|
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
|
-
|
|
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 =
|
|
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 = {
|
|
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(
|
|
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(
|
|
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();
|
package/dist/runtime/stt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./diagnostic-env.js";
|
|
1
2
|
import { ProviderError, TransportError, ValidationError } from "../errors.js";
|
|
2
3
|
import { createTimeoutController, isTimeoutLikeError } from "./timeout.js";
|
|
3
4
|
export const APIFUSE__STT__BACKEND_ENV = "APIFUSE__STT__BACKEND";
|
|
@@ -32,7 +33,7 @@ export function createUnsupportedSttClient(reason) {
|
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
function normalizedEnvValue(env, key) {
|
|
35
|
-
const value = env
|
|
36
|
+
const value = readDiagnosticEnv(key, env)?.trim();
|
|
36
37
|
return value ? value : undefined;
|
|
37
38
|
}
|
|
38
39
|
export function createSttClientFromEnv(config, env = process.env) {
|
|
@@ -53,7 +54,7 @@ export function createSttClientFromEnv(config, env = process.env) {
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
const accountId = env.APIFUSE__CLOUDFLARE__ACCOUNT_ID?.trim();
|
|
56
|
-
const apiToken = env
|
|
57
|
+
const apiToken = readDiagnosticEnv(APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, env)?.trim();
|
|
57
58
|
if (!accountId || !apiToken) {
|
|
58
59
|
return createUnsupportedSttClient(`STT backend ${backend} requires ${CLOUDFLARE_ACCOUNT_ID_ENV} and ${APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV}`);
|
|
59
60
|
}
|
package/dist/runtime/trace.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TraceContext as BaseTraceContext, TraceAttributeValue, TraceConfig, TraceSpan } from "../types.js";
|
|
2
|
+
import { type DiagnosticRedactor } from "./diagnostic-redactor.js";
|
|
2
3
|
import { type OTLPExportOptions } from "./otlp.js";
|
|
3
4
|
export type SpanAttributeValue = TraceAttributeValue;
|
|
4
5
|
export type Span = TraceSpan;
|
|
@@ -8,6 +9,8 @@ export interface TraceContext extends BaseTraceContext {
|
|
|
8
9
|
export interface CreateTraceContextOptions {
|
|
9
10
|
maxSpans?: number;
|
|
10
11
|
onSpan?: (span: Span) => void;
|
|
12
|
+
/** Redacts diagnostic attribute values and errors before recording or calling onSpan. */
|
|
13
|
+
redact?: DiagnosticRedactor;
|
|
11
14
|
exportOptions?: OTLPExportOptions;
|
|
12
15
|
resourceAttributes?: Record<string, string>;
|
|
13
16
|
/** W3C-compatible 32-character lowercase hexadecimal trace id used for export. */
|
|
@@ -34,5 +37,7 @@ export declare function updateTraceContextExportMetadata(trace: BaseTraceContext
|
|
|
34
37
|
}): void;
|
|
35
38
|
export declare function resolveTraceContextOptions(config?: TraceConfig): CreateTraceContextOptions;
|
|
36
39
|
export declare function getTraceRecorder(trace: BaseTraceContext): TraceRecorder | null;
|
|
40
|
+
/** Internal, read-only process counters; no reset can re-arm a warning. */
|
|
41
|
+
export declare function getDroppedOTLPSpanCount(reason: string): number;
|
|
37
42
|
export declare function createTraceContext(options?: CreateTraceContextOptions): TraceContext;
|
|
38
43
|
export {};
|