@apifuse/provider-sdk 2.2.0-beta.60 → 2.2.0-beta.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AUTHORING.md +17 -16
  2. package/CHANGELOG.md +4 -0
  3. package/bin/apifuse-dev.ts +5 -7
  4. package/bin/apifuse-pack-types.ts +22 -0
  5. package/bin/apifuse-perf.ts +4 -6
  6. package/bin/apifuse-record.ts +4 -7
  7. package/bin/apifuse-submit-check.ts +2 -2
  8. package/dist/define.d.ts +3 -2
  9. package/dist/define.js +1 -1
  10. package/dist/dev.js +3 -1
  11. package/dist/engine.d.ts +7 -6
  12. package/dist/engine.js +3 -4
  13. package/dist/error-resolution.js +21 -4
  14. package/dist/errors.d.ts +3 -0
  15. package/dist/errors.js +9 -0
  16. package/dist/handle-meta.d.ts +39 -0
  17. package/dist/handle-meta.js +29 -0
  18. package/dist/handle.d.ts +187 -0
  19. package/dist/handle.js +254 -0
  20. package/dist/i18n/catalog.js +21 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +3 -2
  23. package/dist/lint.d.ts +3 -0
  24. package/dist/lint.js +352 -47
  25. package/dist/provider.d.ts +3 -3
  26. package/dist/provider.js +2 -2
  27. package/dist/runtime/handle-wordlist.d.ts +43 -0
  28. package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
  29. package/dist/runtime/handle.d.ts +71 -0
  30. package/dist/runtime/handle.js +895 -0
  31. package/dist/runtime/native-network.d.ts +10 -1
  32. package/dist/runtime/native-network.js +230 -29
  33. package/dist/runtime/native-telemetry.d.ts +142 -0
  34. package/dist/runtime/native-telemetry.js +231 -0
  35. package/dist/runtime/state.d.ts +4 -1
  36. package/dist/runtime/state.js +32 -17
  37. package/dist/server/index.d.ts +1 -0
  38. package/dist/server/index.js +1 -0
  39. package/dist/server/sensitive-values.d.ts +1 -1
  40. package/dist/server/sensitive-values.js +0 -3
  41. package/dist/server/serve-implementation.d.ts +4 -11
  42. package/dist/server/serve-implementation.js +86 -18
  43. package/dist/testing/run.js +10 -7
  44. package/dist/types.d.ts +4 -107
  45. package/package.json +1 -1
  46. package/src/define.ts +4 -3
  47. package/src/dev.ts +3 -1
  48. package/src/engine.ts +5 -6
  49. package/src/error-resolution.ts +21 -4
  50. package/src/errors.ts +13 -0
  51. package/src/handle-meta.ts +58 -0
  52. package/src/handle.ts +529 -0
  53. package/src/i18n/catalog.ts +23 -1
  54. package/src/index.ts +26 -13
  55. package/src/lint.ts +414 -55
  56. package/src/provider.ts +9 -20
  57. package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
  58. package/src/runtime/handle.ts +1162 -0
  59. package/src/runtime/native-network.ts +281 -33
  60. package/src/runtime/native-telemetry.ts +366 -0
  61. package/src/runtime/state.ts +44 -16
  62. package/src/server/index.ts +17 -0
  63. package/src/server/sensitive-values.ts +0 -3
  64. package/src/server/serve-implementation.ts +87 -32
  65. package/src/testing/run.ts +11 -7
  66. package/src/types.ts +4 -137
  67. package/dist/choice-token.d.ts +0 -24
  68. package/dist/choice-token.js +0 -74
  69. package/dist/runtime/choice-wordlist.d.ts +0 -9
  70. package/dist/runtime/choice.d.ts +0 -27
  71. package/dist/runtime/choice.js +0 -801
  72. package/src/choice-token.ts +0 -165
  73. package/src/runtime/choice.ts +0 -1197
@@ -0,0 +1,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
+ }
@@ -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
- local next_decoded = cjson.decode(envelope)
79
- next_decoded.createdAt = current_decoded.createdAt
80
- envelope = cjson.encode(next_decoded)
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
- return { value, version, expiresAt, createdAt, updatedAt };
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(private readonly options: StateNamespaceOptions) {}
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 = Date.now()): void {
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(options?.ttl ?? this.options.defaultTtl);
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 = { namespaces: new Map() },
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(ttl: StateWriteOptions["ttl"]): string {
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(Date.now() + 3_600_000).toISOString();
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(Date.now() + amount * multiplier).toISOString();
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(): ProviderRuntimeState {
796
- return new MemoryProviderRuntimeState();
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 {
@@ -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]),