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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/AUTHORING.md +33 -16
  2. package/CHANGELOG.md +8 -0
  3. package/bin/apifuse-dev.ts +7 -8
  4. package/bin/apifuse-pack-types.ts +22 -0
  5. package/bin/apifuse-perf.ts +45 -10
  6. package/bin/apifuse-record.ts +6 -8
  7. package/bin/apifuse-submit-check.ts +2 -2
  8. package/dist/config/loader.js +13 -12
  9. package/dist/define.d.ts +3 -2
  10. package/dist/define.js +1 -1
  11. package/dist/dev.js +3 -1
  12. package/dist/engine.d.ts +7 -6
  13. package/dist/engine.js +6 -6
  14. package/dist/error-resolution.js +21 -4
  15. package/dist/errors.d.ts +3 -0
  16. package/dist/errors.js +9 -0
  17. package/dist/handle-meta.d.ts +39 -0
  18. package/dist/handle-meta.js +29 -0
  19. package/dist/handle.d.ts +187 -0
  20. package/dist/handle.js +254 -0
  21. package/dist/i18n/catalog.js +21 -1
  22. package/dist/index.d.ts +6 -4
  23. package/dist/index.js +4 -3
  24. package/dist/lint.d.ts +3 -0
  25. package/dist/lint.js +388 -62
  26. package/dist/provider.d.ts +4 -4
  27. package/dist/provider.js +3 -3
  28. package/dist/runtime/browser.js +2 -1
  29. package/dist/runtime/cache.js +3 -2
  30. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  31. package/dist/runtime/diagnostic-env-values.js +57 -0
  32. package/dist/runtime/diagnostic-env.d.ts +11 -0
  33. package/dist/runtime/diagnostic-env.js +35 -0
  34. package/dist/runtime/diagnostic-redactor.d.ts +49 -0
  35. package/dist/runtime/diagnostic-redactor.js +569 -0
  36. package/dist/runtime/egress-lease.d.ts +2 -0
  37. package/dist/runtime/egress-lease.js +4 -1
  38. package/dist/runtime/env.js +2 -1
  39. package/dist/runtime/handle-wordlist.d.ts +43 -0
  40. package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
  41. package/dist/runtime/handle.d.ts +71 -0
  42. package/dist/runtime/handle.js +895 -0
  43. package/dist/runtime/instrumentation.js +3 -0
  44. package/dist/runtime/native-network.d.ts +10 -1
  45. package/dist/runtime/native-network.js +230 -29
  46. package/dist/runtime/native-telemetry.d.ts +142 -0
  47. package/dist/runtime/native-telemetry.js +231 -0
  48. package/dist/runtime/ocr.js +2 -1
  49. package/dist/runtime/otlp.js +5 -4
  50. package/dist/runtime/proxy-nodemaven.js +3 -2
  51. package/dist/runtime/request-options.d.ts +7 -0
  52. package/dist/runtime/request-options.js +62 -22
  53. package/dist/runtime/resolver-vendors/browser.js +0 -1
  54. package/dist/runtime/resolver.js +3 -2
  55. package/dist/runtime/state.d.ts +4 -1
  56. package/dist/runtime/state.js +32 -17
  57. package/dist/runtime/stt.js +3 -2
  58. package/dist/runtime/trace.d.ts +5 -0
  59. package/dist/runtime/trace.js +115 -20
  60. package/dist/schema.d.ts +11 -0
  61. package/dist/schema.js +56 -2
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1 -0
  64. package/dist/server/self-test-redaction.js +8 -33
  65. package/dist/server/sensitive-values.d.ts +26 -0
  66. package/dist/server/sensitive-values.js +128 -0
  67. package/dist/server/serve-implementation.d.ts +4 -11
  68. package/dist/server/serve-implementation.js +305 -96
  69. package/dist/server/trace-output.d.ts +2 -1
  70. package/dist/server/trace-output.js +46 -14
  71. package/dist/testing/run.js +10 -7
  72. package/dist/trace-sanitization.d.ts +4 -3
  73. package/dist/trace-sanitization.js +45 -22
  74. package/dist/types.d.ts +4 -107
  75. package/package.json +2 -1
  76. package/src/config/loader.ts +13 -12
  77. package/src/define.ts +4 -3
  78. package/src/dev.ts +3 -1
  79. package/src/engine.ts +8 -8
  80. package/src/error-resolution.ts +21 -4
  81. package/src/errors.ts +13 -0
  82. package/src/handle-meta.ts +58 -0
  83. package/src/handle.ts +529 -0
  84. package/src/i18n/catalog.ts +23 -1
  85. package/src/index.ts +28 -13
  86. package/src/lint.ts +456 -71
  87. package/src/provider.ts +10 -20
  88. package/src/runtime/browser.ts +2 -1
  89. package/src/runtime/cache.ts +3 -2
  90. package/src/runtime/diagnostic-env-values.ts +56 -0
  91. package/src/runtime/diagnostic-env.ts +55 -0
  92. package/src/runtime/diagnostic-redactor.ts +662 -0
  93. package/src/runtime/egress-lease.ts +6 -1
  94. package/src/runtime/env.ts +2 -1
  95. package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
  96. package/src/runtime/handle.ts +1162 -0
  97. package/src/runtime/instrumentation.ts +3 -0
  98. package/src/runtime/native-network.ts +281 -33
  99. package/src/runtime/native-telemetry.ts +366 -0
  100. package/src/runtime/ocr.ts +2 -1
  101. package/src/runtime/otlp.ts +5 -4
  102. package/src/runtime/proxy-nodemaven.ts +3 -2
  103. package/src/runtime/request-options.ts +76 -28
  104. package/src/runtime/resolver-vendors/browser.ts +0 -1
  105. package/src/runtime/resolver.ts +3 -2
  106. package/src/runtime/state.ts +44 -16
  107. package/src/runtime/stt.ts +3 -2
  108. package/src/runtime/trace.ts +168 -26
  109. package/src/schema.ts +63 -2
  110. package/src/server/index.ts +17 -0
  111. package/src/server/self-test-redaction.ts +10 -40
  112. package/src/server/sensitive-values.ts +197 -0
  113. package/src/server/serve-implementation.ts +388 -128
  114. package/src/server/trace-output.ts +77 -17
  115. package/src/testing/run.ts +11 -7
  116. package/src/trace-sanitization.ts +70 -23
  117. package/src/types.ts +4 -137
  118. package/dist/choice-token.d.ts +0 -24
  119. package/dist/choice-token.js +0 -74
  120. package/dist/runtime/choice-wordlist.d.ts +0 -9
  121. package/dist/runtime/choice.d.ts +0 -27
  122. package/dist/runtime/choice.js +0 -801
  123. package/src/choice-token.ts +0 -165
  124. package/src/runtime/choice.ts +0 -1197
@@ -0,0 +1,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
+ }
@@ -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[key]?.trim();
59
+ const value = readDiagnosticEnv(key, env)?.trim();
59
60
  return value ? value : undefined;
60
61
  }
61
62
  export function createOcrClientFromEnv(config, env = process.env) {
@@ -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: env[OTEL_EXPORTER_OTLP_TRACES_ENDPOINT],
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: env[OTEL_EXPORTER_OTLP_ENDPOINT],
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[OTEL_EXPORTER_OTLP_TRACES_HEADERS]);
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[OTEL_EXPORTER_OTLP_HEADERS]));
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 process.env[NODEMAVEN_USERNAME_ENV]?.trim() || undefined;
29
+ return readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)?.trim() || undefined;
29
30
  }
30
31
  function readNodemavenPassword() {
31
- return process.env[NODEMAVEN_PASSWORD_ENV]?.trim() || undefined;
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 = 4;
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
- return [...new Set([value, componentEncoded, formEncoded].map(normalizePercentEscapes))];
129
- }
130
- function allSensitiveValueVariants(sensitiveValues) {
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 pattern = variant.value.replace(/%([\da-f]{2})|[.*+?^${}()|[\]\\]/gi, (match, hex) => {
151
- if (hex) {
152
- return `%${[...String(hex)]
153
- .map((character) => /[a-f]/i.test(character)
154
- ? `[${character.toLowerCase()}${character.toUpperCase()}]`
155
- : character)
156
- .join("")}`;
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(source[offset - 1]) || isTokenCharacter(source[end]))) {
164
- return match;
165
- }
166
- return REDACTED_QUERY_VALUE;
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
  })
@@ -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[key]?.trim();
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[name]?.trim();
138
+ const raw = readDiagnosticEnv(name, env)?.trim();
138
139
  if (!raw)
139
140
  return undefined;
140
141
  if (!/^[1-9]\d*$/.test(raw)) {
@@ -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(): ProviderRuntimeState;
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 {};
@@ -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
- local next_decoded = cjson.decode(envelope)
64
- next_decoded.createdAt = current_decoded.createdAt
65
- envelope = cjson.encode(next_decoded)
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
- return { value, version, expiresAt, createdAt, updatedAt };
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 = Date.now()) {
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 = { namespaces: new Map() }, scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR) {
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(Date.now() + 3_600_000).toISOString();
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(Date.now() + amount * multiplier).toISOString();
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();
@@ -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[key]?.trim();
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.APIFUSE__STT__CLOUDFLARE_API_TOKEN?.trim();
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
  }
@@ -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 {};