@apifuse/provider-sdk 2.2.0-beta.60 → 2.2.0-beta.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +17 -16
- package/CHANGELOG.md +4 -0
- package/bin/apifuse-dev.ts +5 -7
- package/bin/apifuse-pack-types.ts +22 -0
- package/bin/apifuse-perf.ts +4 -6
- package/bin/apifuse-record.ts +4 -7
- package/bin/apifuse-submit-check.ts +2 -2
- 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 +3 -4
- 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 +4 -3
- package/dist/index.js +3 -2
- package/dist/lint.d.ts +3 -0
- package/dist/lint.js +352 -47
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- 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/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/state.d.ts +4 -1
- package/dist/runtime/state.js +32 -17
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +0 -3
- package/dist/server/serve-implementation.d.ts +4 -11
- package/dist/server/serve-implementation.js +86 -18
- package/dist/testing/run.js +10 -7
- package/dist/types.d.ts +4 -107
- package/package.json +1 -1
- package/src/define.ts +4 -3
- package/src/dev.ts +3 -1
- package/src/engine.ts +5 -6
- 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 +26 -13
- package/src/lint.ts +414 -55
- package/src/provider.ts +9 -20
- package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
- package/src/runtime/handle.ts +1162 -0
- package/src/runtime/native-network.ts +281 -33
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/state.ts +44 -16
- package/src/server/index.ts +17 -0
- package/src/server/sensitive-values.ts +0 -3
- package/src/server/serve-implementation.ts +87 -32
- package/src/testing/run.ts +11 -7
- 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/state.ts
CHANGED
|
@@ -75,9 +75,12 @@ if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
if current_decoded and type(current_decoded.createdAt) == "string" then
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
envelope
|
|
78
|
+
-- Preserve the original createdAt by patching the encoded envelope in place.
|
|
79
|
+
-- The envelope puts its metadata before "value", so the first match is the
|
|
80
|
+
-- envelope's own key, never a provider value; re-encoding with cjson would
|
|
81
|
+
-- silently turn [] inside the value into {}.
|
|
82
|
+
local preserved = string.gsub(current_decoded.createdAt, "%%", "%%%%")
|
|
83
|
+
envelope = (string.gsub(envelope, '"createdAt":"[^"]*"', '"createdAt":"' .. preserved .. '"', 1))
|
|
81
84
|
end
|
|
82
85
|
redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
|
|
83
86
|
redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
|
|
@@ -267,7 +270,11 @@ function redisEnvelope(
|
|
|
267
270
|
expiresAt: string,
|
|
268
271
|
): RedisStateEnvelope {
|
|
269
272
|
const updatedAt = new Date().toISOString();
|
|
270
|
-
|
|
273
|
+
// Metadata precedes `value` on purpose: the compare-and-set Lua script patches
|
|
274
|
+
// `createdAt` in the encoded string (first occurrence) instead of decoding and
|
|
275
|
+
// re-encoding with cjson, which would turn empty arrays inside `value` into
|
|
276
|
+
// objects. Keep the key order stable.
|
|
277
|
+
return { version, expiresAt, createdAt, updatedAt, value };
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
@@ -604,7 +611,10 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
604
611
|
// biome-ignore lint/suspicious/noExplicitAny: in-memory state stores heterogeneous generic values by key.
|
|
605
612
|
readonly values = new Map<string, StateValue<any>>();
|
|
606
613
|
|
|
607
|
-
constructor(
|
|
614
|
+
constructor(
|
|
615
|
+
private readonly options: StateNamespaceOptions,
|
|
616
|
+
private readonly now: () => number = Date.now,
|
|
617
|
+
) {}
|
|
608
618
|
|
|
609
619
|
private enforceValueSize(value: unknown): void {
|
|
610
620
|
const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
@@ -631,7 +641,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
631
641
|
}
|
|
632
642
|
}
|
|
633
643
|
|
|
634
|
-
private pruneExpired(nowMs =
|
|
644
|
+
private pruneExpired(nowMs = this.now()): void {
|
|
635
645
|
for (const [key, row] of this.values.entries()) {
|
|
636
646
|
if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
|
|
637
647
|
this.values.delete(key);
|
|
@@ -655,9 +665,12 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
655
665
|
async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
656
666
|
this.pruneExpired();
|
|
657
667
|
this.enforceWritePolicy(key, value, options?.ttl);
|
|
658
|
-
const now = new Date().toISOString();
|
|
668
|
+
const now = new Date(this.now()).toISOString();
|
|
659
669
|
const current = this.values.get(key);
|
|
660
|
-
const expiresAt = resolveMemoryStateExpiresAt(
|
|
670
|
+
const expiresAt = resolveMemoryStateExpiresAt(
|
|
671
|
+
options?.ttl ?? this.options.defaultTtl,
|
|
672
|
+
this.now(),
|
|
673
|
+
);
|
|
661
674
|
const row = {
|
|
662
675
|
key,
|
|
663
676
|
value,
|
|
@@ -692,13 +705,14 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
692
705
|
return { ok: false, current: current ?? null };
|
|
693
706
|
}
|
|
694
707
|
this.enforceWritePolicy(key, value, options?.ttl);
|
|
695
|
-
const now = new Date().toISOString();
|
|
708
|
+
const now = new Date(this.now()).toISOString();
|
|
696
709
|
const stored = {
|
|
697
710
|
key,
|
|
698
711
|
value,
|
|
699
712
|
version: expectedVersion + 1,
|
|
700
713
|
expiresAt: resolveMemoryStateExpiresAt(
|
|
701
714
|
options?.ttl ?? this.options.defaultTtl,
|
|
715
|
+
this.now(),
|
|
702
716
|
),
|
|
703
717
|
createdAt: current?.createdAt ?? now,
|
|
704
718
|
updatedAt: now,
|
|
@@ -725,11 +739,16 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
725
739
|
|
|
726
740
|
type MemoryProviderStateBackend = {
|
|
727
741
|
readonly namespaces: Map<string, MemoryProviderStateNamespace>;
|
|
742
|
+
/** Clock used for expiry; tests inject a fake clock so ttl behaviour matches production. */
|
|
743
|
+
readonly now: () => number;
|
|
728
744
|
};
|
|
729
745
|
|
|
730
746
|
class MemoryProviderRuntimeState implements ProviderRuntimeState {
|
|
731
747
|
constructor(
|
|
732
|
-
private readonly backend: MemoryProviderStateBackend = {
|
|
748
|
+
private readonly backend: MemoryProviderStateBackend = {
|
|
749
|
+
namespaces: new Map(),
|
|
750
|
+
now: Date.now,
|
|
751
|
+
},
|
|
733
752
|
private readonly scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR,
|
|
734
753
|
) {}
|
|
735
754
|
|
|
@@ -746,15 +765,18 @@ class MemoryProviderRuntimeState implements ProviderRuntimeState {
|
|
|
746
765
|
const namespaceIdentity = `${scopeDiscriminator}\0${name}`;
|
|
747
766
|
const existing = this.backend.namespaces.get(namespaceIdentity);
|
|
748
767
|
if (existing) return existing;
|
|
749
|
-
const created = new MemoryProviderStateNamespace(options);
|
|
768
|
+
const created = new MemoryProviderStateNamespace(options, this.backend.now);
|
|
750
769
|
this.backend.namespaces.set(namespaceIdentity, created);
|
|
751
770
|
return created;
|
|
752
771
|
}
|
|
753
772
|
}
|
|
754
773
|
|
|
755
|
-
function resolveMemoryStateExpiresAt(
|
|
774
|
+
function resolveMemoryStateExpiresAt(
|
|
775
|
+
ttl: StateWriteOptions["ttl"],
|
|
776
|
+
nowMs = Date.now(),
|
|
777
|
+
): string {
|
|
756
778
|
const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
|
|
757
|
-
if (!match) return new Date(
|
|
779
|
+
if (!match) return new Date(nowMs + 3_600_000).toISOString();
|
|
758
780
|
const amount = Number(match[1]);
|
|
759
781
|
const unit = match[2];
|
|
760
782
|
const multiplier =
|
|
@@ -767,7 +789,7 @@ function resolveMemoryStateExpiresAt(ttl: StateWriteOptions["ttl"]): string {
|
|
|
767
789
|
: unit === "h"
|
|
768
790
|
? 3_600_000
|
|
769
791
|
: 86_400_000;
|
|
770
|
-
return new Date(
|
|
792
|
+
return new Date(nowMs + amount * multiplier).toISOString();
|
|
771
793
|
}
|
|
772
794
|
|
|
773
795
|
export function createRedisProviderRuntimeState(
|
|
@@ -792,8 +814,14 @@ export function createProviderRuntimeStateFromEnv(
|
|
|
792
814
|
return createUnsupportedProviderRuntimeState();
|
|
793
815
|
}
|
|
794
816
|
|
|
795
|
-
export function createMemoryProviderRuntimeState(
|
|
796
|
-
|
|
817
|
+
export function createMemoryProviderRuntimeState(options?: {
|
|
818
|
+
/** Clock for expiry and timestamps; defaults to `Date.now`. */
|
|
819
|
+
readonly now?: () => number;
|
|
820
|
+
}): ProviderRuntimeState {
|
|
821
|
+
return new MemoryProviderRuntimeState({
|
|
822
|
+
namespaces: new Map(),
|
|
823
|
+
now: options?.now ?? Date.now,
|
|
824
|
+
});
|
|
797
825
|
}
|
|
798
826
|
|
|
799
827
|
export function createUnsupportedProviderRuntimeState(): ProviderRuntimeState {
|
package/src/server/index.ts
CHANGED
|
@@ -128,3 +128,20 @@ export {
|
|
|
128
128
|
OperationRequestSchema,
|
|
129
129
|
OperationSuccessResponseSchema,
|
|
130
130
|
} from "./types.js";
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
NativeTelemetryCollector,
|
|
134
|
+
type NativeAttemptSample,
|
|
135
|
+
type NativeConnectTelemetryEvent,
|
|
136
|
+
type NativeLifecycleTelemetryEvent,
|
|
137
|
+
type NativeTelemetryDiagnostics,
|
|
138
|
+
type NativeTelemetryErrorCode,
|
|
139
|
+
type NativeTelemetryHeaderPayload,
|
|
140
|
+
type NativeTelemetryKind,
|
|
141
|
+
type NativeTelemetryLifecycleKind,
|
|
142
|
+
type NativeTelemetryLogPayload,
|
|
143
|
+
type NativeTelemetryOutcome,
|
|
144
|
+
type NativeTelemetrySink,
|
|
145
|
+
type NativeTelemetryVendorSkipReason,
|
|
146
|
+
type NativeVendorSkipTelemetryEvent,
|
|
147
|
+
} from "../runtime/native-telemetry.js";
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES,
|
|
14
14
|
} from "../engine.js";
|
|
15
15
|
import { APIFUSE__CACHE__KEY_PEPPER_ENV } from "../runtime/cache.js";
|
|
16
|
-
import { PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV } from "../runtime/choice.js";
|
|
17
16
|
import { APIFUSE__ENGINE__CEREMONY_LEASE_KEY } from "../runtime/egress-lease.js";
|
|
18
17
|
import {
|
|
19
18
|
APIFUSE__OCR__API_KEY_ENV,
|
|
@@ -46,7 +45,6 @@ export const DIAGNOSTIC_SENSITIVE_SOURCE_KINDS = [
|
|
|
46
45
|
"engineProxyCredentials",
|
|
47
46
|
"engineSolverKeys",
|
|
48
47
|
"engineCeremonyLeaseCredentials",
|
|
49
|
-
"choiceMasterSecret",
|
|
50
48
|
"ocrCredentials",
|
|
51
49
|
"sttCredentials",
|
|
52
50
|
"cdpCredentials",
|
|
@@ -122,7 +120,6 @@ export function collectDiagnosticSensitiveValues(
|
|
|
122
120
|
// These are the resolver-config API keys after #251's engine credential projection.
|
|
123
121
|
engineSolverKeys: () => resolvedEnvValues(ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES),
|
|
124
122
|
engineCeremonyLeaseCredentials: () => resolvedEnvValues([APIFUSE__ENGINE__CEREMONY_LEASE_KEY]),
|
|
125
|
-
choiceMasterSecret: () => resolvedEnvValues([PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV]),
|
|
126
123
|
ocrCredentials: () =>
|
|
127
124
|
resolvedEnvValues([APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__OCR__API_KEY_ENV]),
|
|
128
125
|
sttCredentials: () => resolvedEnvValues([APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV]),
|