@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
@@ -1,5 +1,9 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { sanitizeSpanNameForOutput } from "../trace-sanitization.js";
3
+ import { diagnosticStructuredRedactor, redactDiagnosticAttributeKey, copyDiagnosticAttributeTypes, redactedKeyAllocator, isRedactedKey, redactCriticalDiagnosticText, diagnosticAttributeRedactor, REDACTION_FAILED, redactDiagnosticText, } from "./diagnostic-redactor.js";
2
4
  import { exportSpansOTLP } from "./otlp.js";
5
+ import { REDACTED_QUERY_VALUE } from "./request-options.js";
6
+ const TRACE_RESOURCE_ATTRIBUTE_SANITIZER = Symbol.for("@apifuse/provider-sdk/runtime/trace-resource-attribute-sanitizer");
3
7
  export const TRACE_RECORDER = Symbol.for("@apifuse/provider-sdk/runtime/trace-recorder");
4
8
  const TRACE_EXPORT_METADATA = Symbol.for("@apifuse/provider-sdk/runtime/trace-export-metadata");
5
9
  function assertValidTraceId(traceId) {
@@ -32,23 +36,76 @@ export function resolveTraceContextOptions(config) {
32
36
  exportOptions: buildOTLPExportOptions(config),
33
37
  };
34
38
  }
35
- function normalizeAttributeValue(value) {
36
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
37
- return value;
39
+ function normalizeAttributeValue(value, redact, retainBigints = false) {
40
+ if (typeof value === "number" || typeof value === "boolean") {
41
+ // Typed columns are contracts: unlike string tokens, number/boolean/bigint
42
+ // values only consult the registry at >=8 characters in string form.
43
+ const text = String(value);
44
+ if (text.length < 8)
45
+ return value;
46
+ const redacted = redactDiagnosticText(text, diagnosticStructuredRedactor(redact));
47
+ return redacted === text ? value : redacted;
48
+ }
49
+ if (typeof value === "bigint") {
50
+ // Bigints are not valid OTLP attribute primitives, so retain their prior
51
+ // string normalization while still checking their diagnostic form.
52
+ const text = String(value);
53
+ if (text.length < 8)
54
+ return retainBigints ? value : text;
55
+ const redacted = redactDiagnosticText(text, diagnosticStructuredRedactor(redact));
56
+ return retainBigints && redacted === text ? value : redacted;
38
57
  }
39
58
  if (value === null || value === undefined) {
40
59
  return undefined;
41
60
  }
42
- return String(value);
61
+ return redactDiagnosticText(String(value), redact);
43
62
  }
44
- function normalizeAttributes(attributes) {
63
+ function normalizeAttributes(attributes, redact, retainBigints = false) {
45
64
  if (!attributes) {
46
65
  return {};
47
66
  }
67
+ const nextKey = redactedKeyAllocator(Object.keys(attributes));
48
68
  const normalizedEntries = Object.entries(attributes)
49
- .map(([key, value]) => [key, normalizeAttributeValue(value)])
69
+ .map(([key, value]) => {
70
+ const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
71
+ const keyChanged = redactedKey !== key;
72
+ const normalizedKey = keyChanged ? nextKey() : key;
73
+ // Once a key changes, its original sensitivity classification is no
74
+ // longer available to later output sanitizers. Suppress the value here.
75
+ const normalizedValue = keyChanged
76
+ ? redactedKey === REDACTION_FAILED
77
+ ? REDACTION_FAILED
78
+ : REDACTED_QUERY_VALUE
79
+ : normalizeAttributeValue(value, diagnosticAttributeRedactor(key, redact), retainBigints);
80
+ return [normalizedKey, normalizedValue];
81
+ })
50
82
  .filter((entry) => entry[1] !== undefined);
51
- return Object.fromEntries(normalizedEntries);
83
+ return copyDiagnosticAttributeTypes(attributes, Object.fromEntries(normalizedEntries));
84
+ }
85
+ function prepareResourceAttributesForExport(attributes, redact, sanitize) {
86
+ if (!attributes)
87
+ return undefined;
88
+ if (sanitize) {
89
+ const sanitized = sanitize({ ...attributes });
90
+ if (!sanitized || typeof sanitized !== "object") {
91
+ throw new Error("resource attribute sanitizer returned no attributes");
92
+ }
93
+ return sanitized;
94
+ }
95
+ const nextKey = redactedKeyAllocator(Object.keys(attributes));
96
+ return Object.fromEntries(Object.entries(attributes).map(([key, value]) => {
97
+ const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
98
+ const keyChanged = redactedKey !== key;
99
+ const sanitizedKey = keyChanged ? nextKey() : sanitizeSpanNameForOutput(key);
100
+ return [
101
+ sanitizedKey,
102
+ keyChanged
103
+ ? redactedKey === REDACTION_FAILED
104
+ ? REDACTION_FAILED
105
+ : REDACTED_QUERY_VALUE
106
+ : sanitizeSpanNameForOutput(value, diagnosticAttributeRedactor(key, redact)),
107
+ ];
108
+ }));
52
109
  }
53
110
  function insertCompletedSpan(completed, entry, maxSpans) {
54
111
  const insertAt = completed.findIndex((existingEntry) => existingEntry.sequence > entry.sequence);
@@ -64,7 +121,10 @@ function insertCompletedSpan(completed, entry, maxSpans) {
64
121
  }
65
122
  /** Hands the hook a copy whose attributes are detached from the recorded span; a hook that returns nothing fails closed. */
66
123
  function prepareSpanForExport(span, sanitize) {
67
- const copy = { ...span, attributes: { ...span.attributes } };
124
+ const copy = {
125
+ ...span,
126
+ attributes: copyDiagnosticAttributeTypes(span.attributes, { ...span.attributes }),
127
+ };
68
128
  if (!sanitize)
69
129
  return copy;
70
130
  const sanitized = sanitize(copy);
@@ -76,6 +136,24 @@ function prepareSpanForExport(span, sanitize) {
76
136
  export function getTraceRecorder(trace) {
77
137
  return trace[TRACE_RECORDER] ?? null;
78
138
  }
139
+ const droppedOTLPSpans = new Map();
140
+ /** Internal, read-only process counters; no reset can re-arm a warning. */
141
+ export function getDroppedOTLPSpanCount(reason) {
142
+ return droppedOTLPSpans.get(reason) ?? 0;
143
+ }
144
+ function warnDroppedOTLPSpans(reason, count) {
145
+ const previous = getDroppedOTLPSpanCount(reason);
146
+ const total = previous + count;
147
+ droppedOTLPSpans.set(reason, total);
148
+ if (previous !== 0)
149
+ return;
150
+ try {
151
+ console.warn(`[apifuse] OTLP export skipped; reason=${reason}; dropped_spans=${total}`);
152
+ }
153
+ catch {
154
+ // Warning sinks are best-effort. The counter/latch survives a failed sink.
155
+ }
156
+ }
79
157
  export function createTraceContext(options = {}) {
80
158
  if (options.traceId !== undefined)
81
159
  assertValidTraceId(options.traceId);
@@ -86,9 +164,11 @@ export function createTraceContext(options = {}) {
86
164
  // Export configuration (which can carry collector credentials) stays in this closure;
87
165
  // the context object handed to provider code never exposes it.
88
166
  const exportOptions = options.exportOptions;
89
- const exportResourceAttributes = options.resourceAttributes
90
- ? { ...options.resourceAttributes }
167
+ const resourceAttributeOptions = options.resourceAttributes;
168
+ const exportResourceAttributes = resourceAttributeOptions
169
+ ? { ...resourceAttributeOptions }
91
170
  : undefined;
171
+ const exportResourceAttributeSanitizer = resourceAttributeOptions?.[TRACE_RESOURCE_ATTRIBUTE_SANITIZER];
92
172
  // One trace id per context so every export batch of this request shares it and
93
173
  // two processes can never mint the same id.
94
174
  let exportTraceId = options.traceId ?? crypto.randomUUID().replace(/-/g, "");
@@ -109,46 +189,61 @@ export function createTraceContext(options = {}) {
109
189
  return;
110
190
  // Sanitization runs off the request path; a faulty sanitizer drops the batch, never the request.
111
191
  let spans;
192
+ let resourceAttributes;
112
193
  try {
113
194
  spans = pending.map((entry) => prepareSpanForExport(entry.span, options.sanitizeSpanForExport));
195
+ resourceAttributes = prepareResourceAttributesForExport(exportResourceAttributes, options.redact, exportResourceAttributeSanitizer);
114
196
  }
115
197
  catch {
116
- console.warn("[apifuse] OTLP export skipped; span sanitization failed.");
198
+ try {
199
+ console.warn("[apifuse] OTLP export skipped; span sanitization failed.");
200
+ }
201
+ catch {
202
+ // A broken diagnostic sink must not escape this detached callback.
203
+ }
204
+ return;
205
+ }
206
+ // A matching trace ID cannot be replaced with non-hex text on the wire. Emit less.
207
+ if (redactCriticalDiagnosticText(exportTraceId, options.redact) !== exportTraceId) {
208
+ warnDroppedOTLPSpans("unverifiable_trace_id", pending.length);
117
209
  return;
118
210
  }
119
- void exportSpansOTLP(spans, exportOptions, exportResourceAttributes, exportTraceId);
211
+ void exportSpansOTLP(spans, exportOptions, resourceAttributes, exportTraceId);
120
212
  });
121
213
  };
122
214
  const recorder = {
123
215
  async runSpan(name, fn, spanOptions = {}) {
124
216
  const pendingSpan = {
125
217
  id: crypto.randomUUID(),
126
- name,
218
+ name: redactCriticalDiagnosticText(name, options.redact),
127
219
  startedAt: Date.now(),
128
220
  parentId: activeSpanStorage.getStore()?.id,
129
221
  sequence: sequence++,
130
- attributes: normalizeAttributes(spanOptions.attributes),
222
+ attributes: normalizeAttributes(spanOptions.attributes, options.redact, true),
131
223
  };
132
224
  const finalize = (status, extraAttributes, error) => {
133
225
  const endedAt = Date.now();
134
226
  const duration = endedAt - pendingSpan.startedAt;
135
227
  const attributes = {
136
228
  ...pendingSpan.attributes,
137
- ...normalizeAttributes(extraAttributes),
229
+ ...extraAttributes,
138
230
  };
139
231
  if (attributes.duration_ms === undefined) {
140
232
  attributes.duration_ms = duration;
141
233
  }
142
234
  const span = {
143
- id: pendingSpan.id,
144
- name: pendingSpan.name,
235
+ id: redactCriticalDiagnosticText(pendingSpan.id, options.redact),
236
+ // Recheck at recording: harvest and live env reads may follow span start.
237
+ name: redactCriticalDiagnosticText(pendingSpan.name, options.redact),
145
238
  startedAt: pendingSpan.startedAt,
146
239
  endedAt,
147
240
  duration_ms: duration,
148
241
  status,
149
- attributes,
150
- ...(error ? { error } : {}),
151
- ...(pendingSpan.parentId ? { parentId: pendingSpan.parentId } : {}),
242
+ attributes: normalizeAttributes(attributes, options.redact),
243
+ ...(error ? { error: redactDiagnosticText(error, options.redact) } : {}),
244
+ ...(pendingSpan.parentId
245
+ ? { parentId: redactCriticalDiagnosticText(pendingSpan.parentId, options.redact) }
246
+ : {}),
152
247
  };
153
248
  insertCompletedSpan(completed, { sequence: pendingSpan.sequence, span, exported: false }, maxSpans);
154
249
  options.onSpan?.(span);
package/dist/schema.d.ts CHANGED
@@ -22,6 +22,8 @@ export type SensitiveFieldKind = "api_key" | "authorization" | "cookie" | "crede
22
22
  export interface SensitiveFieldOptions {
23
23
  /**
24
24
  * Mark this schema as sensitive. Defaults to true for the helper presets.
25
+ * An explicit `false` emits `x-apifuse-sensitive: false` (reviewed public
26
+ * data, never redacted) and drops any sensitivity kind.
25
27
  */
26
28
  sensitive?: boolean;
27
29
  /**
@@ -48,6 +50,15 @@ declare module "zod" {
48
50
  }
49
51
  export declare function field<TSchema extends ZodType>(schema: TSchema, options?: SensitiveFieldOptions): TSchema;
50
52
  export declare function sensitive<TSchema extends ZodType>(schema: TSchema, kind?: SensitiveFieldKind): TSchema;
53
+ /**
54
+ * Declare a field as reviewed public data (business, facility, product, or
55
+ * place names and contacts) so a key-based privacy classifier hit counts as
56
+ * declared instead of undeclared. Emits `x-apifuse-sensitive: false` on the
57
+ * JSON Schema leaf; nothing is redacted. Use `sensitive()` for anything that
58
+ * identifies a person. Apply it to the leaf, before `.optional()` or
59
+ * `.nullable()`; wrapping a `sensitive()` declaration throws at definition time.
60
+ */
61
+ export declare function publicField<TSchema extends ZodType>(schema: TSchema): TSchema;
51
62
  export declare const fields: {
52
63
  readonly apiKey: (options?: {
53
64
  description?: string;
package/dist/schema.js CHANGED
@@ -113,15 +113,41 @@ export function field(schema, options = {}) {
113
113
  ? schema.describe(options.description)
114
114
  : schema;
115
115
  const metadata = described.meta() ?? {};
116
+ const isSensitive = options.sensitive ?? true;
117
+ // A public declaration only rewrites the meta of the node it receives;
118
+ // z.toJSONSchema folds that over the inner leaf, so `false` on a wrapper
119
+ // around a sensitive() leaf would publish `false` next to the leaf's kind
120
+ // while collectSensitivePaths() still redacts. Refuse the contradiction.
121
+ if (!isSensitive && wrapsSensitiveDeclaration(described)) {
122
+ throw new TypeError("publicField()/field({ sensitive: false }) wraps a sensitive() declaration; apply it to the leaf, before .optional(), .nullable(), .default(), .pipe(), z.lazy() or similar wrappers.");
123
+ }
124
+ // Zod merges meta with the parent chain, so an inherited kind can only be
125
+ // cleared by writing undefined over it (dropped from JSON Schema output).
126
+ const kind = isSensitive
127
+ ? (options.kind ?? Reflect.get(metadata, APIFUSE_SENSITIVE_KIND_META_KEY))
128
+ : undefined;
116
129
  return described.meta({
117
130
  ...metadata,
118
- ...((options.sensitive ?? true) ? { [APIFUSE_SENSITIVE_META_KEY]: true } : {}),
119
- ...(options.kind ? { [APIFUSE_SENSITIVE_KIND_META_KEY]: options.kind } : {}),
131
+ [APIFUSE_SENSITIVE_META_KEY]: isSensitive,
132
+ ...(kind !== undefined || APIFUSE_SENSITIVE_KIND_META_KEY in metadata
133
+ ? { [APIFUSE_SENSITIVE_KIND_META_KEY]: kind }
134
+ : {}),
120
135
  });
121
136
  }
122
137
  export function sensitive(schema, kind) {
123
138
  return field(schema, { sensitive: true, kind });
124
139
  }
140
+ /**
141
+ * Declare a field as reviewed public data (business, facility, product, or
142
+ * place names and contacts) so a key-based privacy classifier hit counts as
143
+ * declared instead of undeclared. Emits `x-apifuse-sensitive: false` on the
144
+ * JSON Schema leaf; nothing is redacted. Use `sensitive()` for anything that
145
+ * identifies a person. Apply it to the leaf, before `.optional()` or
146
+ * `.nullable()`; wrapping a `sensitive()` declaration throws at definition time.
147
+ */
148
+ export function publicField(schema) {
149
+ return field(schema, { sensitive: false });
150
+ }
125
151
  function sensitiveString(kind, description, options = {}) {
126
152
  const schema = options.minLength === undefined ? z.string() : z.string().min(options.minLength);
127
153
  return field(schema, {
@@ -238,6 +264,34 @@ function readZodMetadata(schema) {
238
264
  const metadata = maybeMeta.call(schema);
239
265
  return metadata && typeof metadata === "object" ? metadata : undefined;
240
266
  }
267
+ // Same-path wrappers (optional, nullable, default, catch, readonly, pipe,
268
+ // lazy, ...) share the JSON Schema leaf with the schema they wrap.
269
+ function wrapsSensitiveDeclaration(schema, seen = new Set()) {
270
+ const def = readZodDef(schema);
271
+ if (!def || seen.has(schema))
272
+ return false;
273
+ seen.add(schema);
274
+ return sameLeafChildren(def).some((child) => isSensitiveSchema(child) || wrapsSensitiveDeclaration(child, seen));
275
+ }
276
+ function sameLeafChildren(def) {
277
+ switch (Reflect.get(def, "type")) {
278
+ case "pipe":
279
+ return [Reflect.get(def, "in"), Reflect.get(def, "out")];
280
+ case "lazy": {
281
+ // A forward-reference getter can throw before its target is
282
+ // initialised; treat that as no declaration rather than failing.
283
+ const getter = Reflect.get(def, "getter");
284
+ try {
285
+ return typeof getter === "function" ? [getter()] : [];
286
+ }
287
+ catch {
288
+ return [];
289
+ }
290
+ }
291
+ default:
292
+ return [Reflect.get(def, "innerType")];
293
+ }
294
+ }
241
295
  function readZodDef(schema) {
242
296
  if (!schema || typeof schema !== "object")
243
297
  return undefined;
@@ -12,3 +12,4 @@ export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_
12
12
  export { DEFAULT_SELF_TEST_PORT, deriveSelfTestToken, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV, PROVIDER_RUNTIME_SELF_TEST_PORT_ENV, resolveSelfTestMasterSecrets, type SelfTestMasterSecrets, verifySelfTestAuthorization, } from "./self-test-token.js";
13
13
  export type { AuthFlowRequest, AuthFlowResponse, AuthFlowSuccessResponse, ConnectionMode, OperationConnection, OperationErrorResponse, OperationRequest, OperationResponse, OperationSuccessResponse, } from "./types.js";
14
14
  export { AuthFlowRequestSchema, AuthFlowSuccessResponseSchema, ConnectionModeSchema, ErrorEnvelopeSchema, OperationConnectionSchema, OperationErrorResponseSchema, OperationRequestSchema, OperationSuccessResponseSchema, } from "./types.js";
15
+ export { NativeTelemetryCollector, type NativeAttemptSample, type NativeConnectTelemetryEvent, type NativeLifecycleTelemetryEvent, type NativeTelemetryDiagnostics, type NativeTelemetryErrorCode, type NativeTelemetryHeaderPayload, type NativeTelemetryKind, type NativeTelemetryLifecycleKind, type NativeTelemetryLogPayload, type NativeTelemetryOutcome, type NativeTelemetrySink, type NativeTelemetryVendorSkipReason, type NativeVendorSkipTelemetryEvent, } from "../runtime/native-telemetry.js";
@@ -6,3 +6,4 @@ export { resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js"
6
6
  export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction.js";
7
7
  export { DEFAULT_SELF_TEST_PORT, deriveSelfTestToken, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV, PROVIDER_RUNTIME_SELF_TEST_PORT_ENV, resolveSelfTestMasterSecrets, verifySelfTestAuthorization, } from "./self-test-token.js";
8
8
  export { AuthFlowRequestSchema, AuthFlowSuccessResponseSchema, ConnectionModeSchema, ErrorEnvelopeSchema, OperationConnectionSchema, OperationErrorResponseSchema, OperationRequestSchema, OperationSuccessResponseSchema, } from "./types.js";
9
+ export { NativeTelemetryCollector, } from "../runtime/native-telemetry.js";
@@ -1,4 +1,4 @@
1
- import { PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV, } from "./self-test-token.js";
1
+ import { collectDiagnosticSensitiveValues, SELF_TEST_SENSITIVE_SOURCE_KINDS, } from "./sensitive-values.js";
2
2
  export const SELF_TEST_REDACTED_PLACEHOLDER = "[REDACTED]";
3
3
  /** Maximum length of any string echoed in a self-test response. */
4
4
  export const SELF_TEST_MAX_TEXT_LENGTH = 300;
@@ -16,37 +16,12 @@ const HEADER_SHAPED_SECRETS = [
16
16
  * credential inputs.
17
17
  */
18
18
  export function collectSelfTestSensitiveValues(provider, options = {}) {
19
- const env = options.env ?? process.env;
20
- const envNames = new Set([
21
- PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV,
22
- PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV,
23
- ]);
24
- for (const secret of provider.secrets ?? []) {
25
- envNames.add(secret.name);
26
- }
27
- const healthProbe = provider.healthProbe ?? provider.healthMonitor;
28
- for (const name of healthProbe?.requiredSecrets ?? []) {
29
- envNames.add(name);
30
- }
31
- for (const name of Object.values(healthProbe?.credentialInputs ?? {})) {
32
- envNames.add(name);
33
- }
34
- const values = new Set();
35
- for (const name of envNames) {
36
- const value = env[name];
37
- if (typeof value === "string" && value.length >= MIN_SENSITIVE_VALUE_LENGTH) {
38
- values.add(value);
39
- }
40
- }
41
- for (const value of Object.values(options.credentialInputs ?? {})) {
42
- if (value.length >= MIN_SENSITIVE_VALUE_LENGTH) {
43
- values.add(value);
44
- }
45
- }
46
- return [...values];
47
- }
48
- function escapeRegExp(value) {
49
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ return collectDiagnosticSensitiveValues(provider, {
20
+ ...options,
21
+ sourceKinds: SELF_TEST_SENSITIVE_SOURCE_KINDS,
22
+ minimumLength: MIN_SENSITIVE_VALUE_LENGTH,
23
+ includeResolvedEnvVariants: false,
24
+ });
50
25
  }
51
26
  /**
52
27
  * Scrubs known secret values and header-shaped credentials from a string and
@@ -58,7 +33,7 @@ export function redactSelfTestText(text, sensitiveValues, maxLength = SELF_TEST_
58
33
  for (const value of sensitiveValues) {
59
34
  if (value.length < MIN_SENSITIVE_VALUE_LENGTH)
60
35
  continue;
61
- redacted = redacted.replace(new RegExp(escapeRegExp(value), "g"), SELF_TEST_REDACTED_PLACEHOLDER);
36
+ redacted = redacted.replaceAll(value, SELF_TEST_REDACTED_PLACEHOLDER);
62
37
  }
63
38
  for (const pattern of HEADER_SHAPED_SECRETS) {
64
39
  redacted = redacted.replace(pattern, (_match, prefix) => `${prefix}${SELF_TEST_REDACTED_PLACEHOLDER}`);
@@ -0,0 +1,26 @@
1
+ import type { ProviderDefinition } from "../types.js";
2
+ export declare const SELF_TEST_SENSITIVE_SOURCE_KINDS: readonly ["selfTestMasterSecrets", "providerSecrets", "healthProbeSecrets", "requestCredentials"];
3
+ /** Explicit source allowlist: no environment-name or payload-shape secret sweep. */
4
+ export declare const DIAGNOSTIC_SENSITIVE_SOURCE_KINDS: readonly ["selfTestMasterSecrets", "providerSecrets", "healthProbeSecrets", "requestCredentials", "engineProxyCredentials", "engineSolverKeys", "engineCeremonyLeaseCredentials", "ocrCredentials", "sttCredentials", "cdpCredentials", "statefulForwardingSecret", "redisCredentials", "otlpHeaders", "cacheKeyPepper", "sensitiveParams"];
5
+ export type DiagnosticSensitiveSourceKind = (typeof DIAGNOSTIC_SENSITIVE_SOURCE_KINDS)[number];
6
+ /**
7
+ * Startup inventory; request-scoped readers additionally register live values on access.
8
+ */
9
+ export declare function collectStaticDiagnosticSensitiveValues(provider: ProviderDefinition, options?: {
10
+ env?: Readonly<Record<string, string | undefined>>;
11
+ statefulForwardingSecret?: string;
12
+ }): string[];
13
+ /** Generalised self-test collector, also used to seed and enrich ordinary request scopes. */
14
+ export declare function collectDiagnosticSensitiveValues(provider: ProviderDefinition, options?: {
15
+ env?: Readonly<Record<string, string | undefined>>;
16
+ credentialInputs?: Readonly<Record<string, unknown>>;
17
+ connectionSecrets?: Readonly<Record<string, string | undefined>>;
18
+ sensitiveParams?: readonly string[];
19
+ statefulForwardingSecret?: string;
20
+ sourceKinds?: readonly DiagnosticSensitiveSourceKind[];
21
+ minimumLength?: number;
22
+ /** Preserve the legacy self-test response collector when false. */
23
+ includeResolvedEnvVariants?: boolean;
24
+ }): string[];
25
+ /** Derive the live-read allowlist from the same collector as the startup inventory. */
26
+ export declare function createDiagnosticEnvObserver(provider: ProviderDefinition, register: (values: readonly string[]) => void, has?: (value: string) => boolean): (name: string, value: string) => void;
@@ -0,0 +1,128 @@
1
+ import { resolvedStringVariants, urlCredentialComponents, otlpHeaderCredentialValues, } from "../runtime/diagnostic-env-values.js";
2
+ import { PROVIDER_CACHE_REDIS_URL_ENV, PROVIDER_STATE_REDIS_URL_ENV, REDIS_URL_ENV, } from "../config/loader.js";
3
+ import { ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES, ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES, } from "../engine.js";
4
+ import { APIFUSE__CACHE__KEY_PEPPER_ENV } from "../runtime/cache.js";
5
+ import { APIFUSE__ENGINE__CEREMONY_LEASE_KEY } from "../runtime/egress-lease.js";
6
+ import { APIFUSE__OCR__API_KEY_ENV, APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV, } from "../runtime/ocr.js";
7
+ import { OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, } from "../runtime/otlp.js";
8
+ import { APIFUSE__CDP_POOL__URL } from "../runtime/resolver-config.js";
9
+ import { APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV } from "../runtime/stt.js";
10
+ import { PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV, } from "./self-test-token.js";
11
+ export const SELF_TEST_SENSITIVE_SOURCE_KINDS = [
12
+ "selfTestMasterSecrets",
13
+ "providerSecrets",
14
+ "healthProbeSecrets",
15
+ "requestCredentials",
16
+ ];
17
+ /** Explicit source allowlist: no environment-name or payload-shape secret sweep. */
18
+ export const DIAGNOSTIC_SENSITIVE_SOURCE_KINDS = [
19
+ ...SELF_TEST_SENSITIVE_SOURCE_KINDS,
20
+ "engineProxyCredentials",
21
+ "engineSolverKeys",
22
+ "engineCeremonyLeaseCredentials",
23
+ "ocrCredentials",
24
+ "sttCredentials",
25
+ "cdpCredentials",
26
+ "statefulForwardingSecret",
27
+ "redisCredentials",
28
+ "otlpHeaders",
29
+ "cacheKeyPepper",
30
+ "sensitiveParams",
31
+ ];
32
+ /**
33
+ * Startup inventory; request-scoped readers additionally register live values on access.
34
+ */
35
+ export function collectStaticDiagnosticSensitiveValues(provider, options = {}) {
36
+ return collectDiagnosticSensitiveValues(provider, {
37
+ ...options,
38
+ sourceKinds: DIAGNOSTIC_SENSITIVE_SOURCE_KINDS.filter((kind) => kind !== "requestCredentials" && kind !== "sensitiveParams"),
39
+ });
40
+ }
41
+ /** Generalised self-test collector, also used to seed and enrich ordinary request scopes. */
42
+ export function collectDiagnosticSensitiveValues(provider, options = {}) {
43
+ const env = options.env ?? process.env;
44
+ const envValues = (names) => names.map((name) => env[name]);
45
+ const resolvedEnvValues = (names) => envValues(names).flatMap((value) => value === undefined
46
+ ? []
47
+ : options.includeResolvedEnvVariants === false
48
+ ? [value]
49
+ : [value, value.trim()]);
50
+ const healthProbe = provider.healthProbe ?? provider.healthMonitor;
51
+ const sources = {
52
+ selfTestMasterSecrets: () => resolvedEnvValues([
53
+ PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV,
54
+ PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV,
55
+ ]),
56
+ providerSecrets: () => resolvedEnvValues((provider.secrets ?? []).map((secret) => secret.name)),
57
+ healthProbeSecrets: () => resolvedEnvValues([
58
+ ...(healthProbe?.requiredSecrets ?? []),
59
+ ...Object.values(healthProbe?.credentialInputs ?? {}),
60
+ ]),
61
+ requestCredentials: () => [
62
+ ...Object.values(options.credentialInputs ?? {}),
63
+ ...Object.values(options.connectionSecrets ?? {}),
64
+ ],
65
+ engineProxyCredentials: () => resolvedEnvValues(ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES),
66
+ // These are the resolver-config API keys after #251's engine credential projection.
67
+ engineSolverKeys: () => resolvedEnvValues(ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES),
68
+ engineCeremonyLeaseCredentials: () => resolvedEnvValues([APIFUSE__ENGINE__CEREMONY_LEASE_KEY]),
69
+ ocrCredentials: () => resolvedEnvValues([APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__OCR__API_KEY_ENV]),
70
+ sttCredentials: () => resolvedEnvValues([APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV]),
71
+ cdpCredentials: () => resolvedStringVariants(env[APIFUSE__CDP_POOL__URL]).flatMap((url) => [
72
+ url,
73
+ ...urlCredentialComponents(url, { includePathAndQuery: true }),
74
+ ]),
75
+ statefulForwardingSecret: () => resolvedStringVariants(options.statefulForwardingSecret),
76
+ redisCredentials: () => [PROVIDER_CACHE_REDIS_URL_ENV, PROVIDER_STATE_REDIS_URL_ENV, REDIS_URL_ENV]
77
+ .flatMap((name) => resolvedStringVariants(env[name]))
78
+ .flatMap((url) => urlCredentialComponents(url, { includePathAndQuery: false })),
79
+ otlpHeaders: () => [
80
+ ...[OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_HEADERS].flatMap((name) => env[name] === undefined ? [] : otlpHeaderCredentialValues(env[name])),
81
+ ...[OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT].flatMap((name) => resolvedStringVariants(env[name]).flatMap((url) => urlCredentialComponents(url, { includePathAndQuery: false }))),
82
+ ],
83
+ cacheKeyPepper: () => resolvedEnvValues([APIFUSE__CACHE__KEY_PEPPER_ENV]),
84
+ sensitiveParams: () => options.sensitiveParams ?? [],
85
+ };
86
+ const values = new Set();
87
+ for (const kind of options.sourceKinds ?? DIAGNOSTIC_SENSITIVE_SOURCE_KINDS) {
88
+ for (const value of sources[kind]()) {
89
+ if (typeof value === "string" && value.length >= (options.minimumLength ?? 1))
90
+ values.add(value);
91
+ }
92
+ }
93
+ return [...values];
94
+ }
95
+ const diagnosticEnvNames = new WeakMap();
96
+ /** Derive the live-read allowlist from the same collector as the startup inventory. */
97
+ export function createDiagnosticEnvObserver(provider, register, has) {
98
+ let names = diagnosticEnvNames.get(provider);
99
+ if (!names) {
100
+ names = new Set();
101
+ const captured = names;
102
+ collectStaticDiagnosticSensitiveValues(provider, {
103
+ env: new Proxy({}, {
104
+ get: (_target, key) => {
105
+ if (typeof key === "string")
106
+ captured.add(key);
107
+ return undefined;
108
+ },
109
+ }),
110
+ });
111
+ diagnosticEnvNames.set(provider, names);
112
+ }
113
+ const seen = new Map();
114
+ return (name, value) => {
115
+ if (!names.has(name) || seen.get(name) === value)
116
+ return;
117
+ if (has?.(value) && has(value.trim()))
118
+ return;
119
+ // Includes URL userinfo/CDP components and OTLP auth values, using the same rules.
120
+ register([
121
+ ...resolvedStringVariants(value),
122
+ ...collectStaticDiagnosticSensitiveValues(provider, {
123
+ env: { [name]: value },
124
+ }),
125
+ ]);
126
+ seen.set(name, value);
127
+ };
128
+ }
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { type ProviderEngine } from "../engine.js";
4
4
  import { type ProviderErrorObservability } from "../errors.js";
5
5
  import { type ProviderErrorCategory } from "../observability.js";
6
+ import type { HandleTelemetryEvent } from "../handle.js";
6
7
  import { type ProxyTelemetryLogPayload } from "../runtime/proxy-telemetry.js";
7
8
  import { type ResolverTelemetryLogPayload } from "../runtime/resolver-telemetry.js";
8
9
  import type { OcrContext, ProviderContext, ProviderDefinition, ProviderFilesContext, ProviderRuntimeState, ResolverContext, SttContext } from "../types.js";
@@ -115,18 +116,10 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
115
116
  event: "provider_secrets_missing";
116
117
  providerId: string;
117
118
  missingSecrets: string[];
118
- } | {
119
+ } | ({
119
120
  level: "info";
120
- event: "provider_choice_token";
121
- providerId: string;
122
- purpose: string;
123
- operation: "parse" | "consume";
124
- format: "word" | "legacy";
125
- outcome: "success" | "not-found" | "invalid" | "unsupported" | "error";
126
- consumeMode: "never" | "on-parse" | "explicit";
127
- consumed: boolean;
128
- replay: boolean;
129
- } | {
121
+ event: "provider_handle";
122
+ } & HandleTelemetryEvent) | {
130
123
  level: "warn";
131
124
  event: "provider_cleanup_failed";
132
125
  providerId: string;