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

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 (72) hide show
  1. package/AUTHORING.md +16 -0
  2. package/CHANGELOG.md +4 -0
  3. package/bin/apifuse-dev.ts +2 -1
  4. package/bin/apifuse-perf.ts +41 -4
  5. package/bin/apifuse-record.ts +3 -2
  6. package/dist/config/loader.js +13 -12
  7. package/dist/engine.js +3 -2
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.js +1 -1
  10. package/dist/lint.js +36 -15
  11. package/dist/provider.d.ts +1 -1
  12. package/dist/provider.js +1 -1
  13. package/dist/runtime/browser.js +2 -1
  14. package/dist/runtime/cache.js +3 -2
  15. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  16. package/dist/runtime/diagnostic-env-values.js +57 -0
  17. package/dist/runtime/diagnostic-env.d.ts +11 -0
  18. package/dist/runtime/diagnostic-env.js +35 -0
  19. package/dist/runtime/diagnostic-redactor.d.ts +49 -0
  20. package/dist/runtime/diagnostic-redactor.js +569 -0
  21. package/dist/runtime/egress-lease.d.ts +2 -0
  22. package/dist/runtime/egress-lease.js +4 -1
  23. package/dist/runtime/env.js +2 -1
  24. package/dist/runtime/instrumentation.js +3 -0
  25. package/dist/runtime/ocr.js +2 -1
  26. package/dist/runtime/otlp.js +5 -4
  27. package/dist/runtime/proxy-nodemaven.js +3 -2
  28. package/dist/runtime/request-options.d.ts +7 -0
  29. package/dist/runtime/request-options.js +62 -22
  30. package/dist/runtime/resolver-vendors/browser.js +0 -1
  31. package/dist/runtime/resolver.js +3 -2
  32. package/dist/runtime/stt.js +3 -2
  33. package/dist/runtime/trace.d.ts +5 -0
  34. package/dist/runtime/trace.js +115 -20
  35. package/dist/schema.d.ts +11 -0
  36. package/dist/schema.js +56 -2
  37. package/dist/server/self-test-redaction.js +8 -33
  38. package/dist/server/sensitive-values.d.ts +26 -0
  39. package/dist/server/sensitive-values.js +131 -0
  40. package/dist/server/serve-implementation.js +219 -78
  41. package/dist/server/trace-output.d.ts +2 -1
  42. package/dist/server/trace-output.js +46 -14
  43. package/dist/trace-sanitization.d.ts +4 -3
  44. package/dist/trace-sanitization.js +45 -22
  45. package/package.json +2 -1
  46. package/src/config/loader.ts +13 -12
  47. package/src/engine.ts +3 -2
  48. package/src/index.ts +2 -0
  49. package/src/lint.ts +42 -16
  50. package/src/provider.ts +1 -0
  51. package/src/runtime/browser.ts +2 -1
  52. package/src/runtime/cache.ts +3 -2
  53. package/src/runtime/diagnostic-env-values.ts +56 -0
  54. package/src/runtime/diagnostic-env.ts +55 -0
  55. package/src/runtime/diagnostic-redactor.ts +662 -0
  56. package/src/runtime/egress-lease.ts +6 -1
  57. package/src/runtime/env.ts +2 -1
  58. package/src/runtime/instrumentation.ts +3 -0
  59. package/src/runtime/ocr.ts +2 -1
  60. package/src/runtime/otlp.ts +5 -4
  61. package/src/runtime/proxy-nodemaven.ts +3 -2
  62. package/src/runtime/request-options.ts +76 -28
  63. package/src/runtime/resolver-vendors/browser.ts +0 -1
  64. package/src/runtime/resolver.ts +3 -2
  65. package/src/runtime/stt.ts +3 -2
  66. package/src/runtime/trace.ts +168 -26
  67. package/src/schema.ts +63 -2
  68. package/src/server/self-test-redaction.ts +10 -40
  69. package/src/server/sensitive-values.ts +200 -0
  70. package/src/server/serve-implementation.ts +301 -96
  71. package/src/server/trace-output.ts +77 -17
  72. package/src/trace-sanitization.ts +70 -23
package/src/schema.ts CHANGED
@@ -105,6 +105,8 @@ export type SensitiveFieldKind =
105
105
  export interface SensitiveFieldOptions {
106
106
  /**
107
107
  * Mark this schema as sensitive. Defaults to true for the helper presets.
108
+ * An explicit `false` emits `x-apifuse-sensitive: false` (reviewed public
109
+ * data, never redacted) and drops any sensitivity kind.
108
110
  */
109
111
  sensitive?: boolean;
110
112
  /**
@@ -208,10 +210,27 @@ export function field<TSchema extends ZodType>(
208
210
  ? schema.describe(options.description)
209
211
  : schema;
210
212
  const metadata = described.meta() ?? {};
213
+ const isSensitive = options.sensitive ?? true;
214
+ // A public declaration only rewrites the meta of the node it receives;
215
+ // z.toJSONSchema folds that over the inner leaf, so `false` on a wrapper
216
+ // around a sensitive() leaf would publish `false` next to the leaf's kind
217
+ // while collectSensitivePaths() still redacts. Refuse the contradiction.
218
+ if (!isSensitive && wrapsSensitiveDeclaration(described)) {
219
+ throw new TypeError(
220
+ "publicField()/field({ sensitive: false }) wraps a sensitive() declaration; apply it to the leaf, before .optional(), .nullable(), .default(), .pipe(), z.lazy() or similar wrappers.",
221
+ );
222
+ }
223
+ // Zod merges meta with the parent chain, so an inherited kind can only be
224
+ // cleared by writing undefined over it (dropped from JSON Schema output).
225
+ const kind = isSensitive
226
+ ? (options.kind ?? Reflect.get(metadata, APIFUSE_SENSITIVE_KIND_META_KEY))
227
+ : undefined;
211
228
  return described.meta({
212
229
  ...metadata,
213
- ...((options.sensitive ?? true) ? { [APIFUSE_SENSITIVE_META_KEY]: true } : {}),
214
- ...(options.kind ? { [APIFUSE_SENSITIVE_KIND_META_KEY]: options.kind } : {}),
230
+ [APIFUSE_SENSITIVE_META_KEY]: isSensitive,
231
+ ...(kind !== undefined || APIFUSE_SENSITIVE_KIND_META_KEY in metadata
232
+ ? { [APIFUSE_SENSITIVE_KIND_META_KEY]: kind }
233
+ : {}),
215
234
  });
216
235
  }
217
236
 
@@ -222,6 +241,18 @@ export function sensitive<TSchema extends ZodType>(
222
241
  return field(schema, { sensitive: true, kind });
223
242
  }
224
243
 
244
+ /**
245
+ * Declare a field as reviewed public data (business, facility, product, or
246
+ * place names and contacts) so a key-based privacy classifier hit counts as
247
+ * declared instead of undeclared. Emits `x-apifuse-sensitive: false` on the
248
+ * JSON Schema leaf; nothing is redacted. Use `sensitive()` for anything that
249
+ * identifies a person. Apply it to the leaf, before `.optional()` or
250
+ * `.nullable()`; wrapping a `sensitive()` declaration throws at definition time.
251
+ */
252
+ export function publicField<TSchema extends ZodType>(schema: TSchema): TSchema {
253
+ return field(schema, { sensitive: false });
254
+ }
255
+
225
256
  function sensitiveString(
226
257
  kind: SensitiveFieldKind,
227
258
  description: string,
@@ -382,6 +413,36 @@ function readZodMetadata(schema: unknown): object | undefined {
382
413
  return metadata && typeof metadata === "object" ? metadata : undefined;
383
414
  }
384
415
 
416
+ // Same-path wrappers (optional, nullable, default, catch, readonly, pipe,
417
+ // lazy, ...) share the JSON Schema leaf with the schema they wrap.
418
+ function wrapsSensitiveDeclaration(schema: unknown, seen = new Set<unknown>()): boolean {
419
+ const def = readZodDef(schema);
420
+ if (!def || seen.has(schema)) return false;
421
+ seen.add(schema);
422
+ return sameLeafChildren(def).some(
423
+ (child) => isSensitiveSchema(child) || wrapsSensitiveDeclaration(child, seen),
424
+ );
425
+ }
426
+
427
+ function sameLeafChildren(def: object): unknown[] {
428
+ switch (Reflect.get(def, "type")) {
429
+ case "pipe":
430
+ return [Reflect.get(def, "in"), Reflect.get(def, "out")];
431
+ case "lazy": {
432
+ // A forward-reference getter can throw before its target is
433
+ // initialised; treat that as no declaration rather than failing.
434
+ const getter = Reflect.get(def, "getter");
435
+ try {
436
+ return typeof getter === "function" ? [getter()] : [];
437
+ } catch {
438
+ return [];
439
+ }
440
+ }
441
+ default:
442
+ return [Reflect.get(def, "innerType")];
443
+ }
444
+ }
445
+
385
446
  function readZodDef(schema: unknown): object | undefined {
386
447
  if (!schema || typeof schema !== "object") return undefined;
387
448
  const def = Reflect.get(schema, "def") ?? Reflect.get(schema, "_def");
@@ -1,8 +1,8 @@
1
1
  import type { ProviderDefinition } from "../types.js";
2
2
  import {
3
- PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV,
4
- PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV,
5
- } from "./self-test-token.js";
3
+ collectDiagnosticSensitiveValues,
4
+ SELF_TEST_SENSITIVE_SOURCE_KINDS,
5
+ } from "./sensitive-values.js";
6
6
 
7
7
  export const SELF_TEST_REDACTED_PLACEHOLDER = "[REDACTED]";
8
8
 
@@ -31,39 +31,12 @@ export function collectSelfTestSensitiveValues(
31
31
  credentialInputs?: Readonly<Record<string, string>>;
32
32
  } = {},
33
33
  ): string[] {
34
- const env = options.env ?? process.env;
35
- const envNames = new Set<string>([
36
- PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV,
37
- PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV,
38
- ]);
39
- for (const secret of provider.secrets ?? []) {
40
- envNames.add(secret.name);
41
- }
42
- const healthProbe = provider.healthProbe ?? provider.healthMonitor;
43
- for (const name of healthProbe?.requiredSecrets ?? []) {
44
- envNames.add(name);
45
- }
46
- for (const name of Object.values(healthProbe?.credentialInputs ?? {})) {
47
- envNames.add(name);
48
- }
49
-
50
- const values = new Set<string>();
51
- for (const name of envNames) {
52
- const value = env[name];
53
- if (typeof value === "string" && value.length >= MIN_SENSITIVE_VALUE_LENGTH) {
54
- values.add(value);
55
- }
56
- }
57
- for (const value of Object.values(options.credentialInputs ?? {})) {
58
- if (value.length >= MIN_SENSITIVE_VALUE_LENGTH) {
59
- values.add(value);
60
- }
61
- }
62
- return [...values];
63
- }
64
-
65
- function escapeRegExp(value: string): string {
66
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34
+ return collectDiagnosticSensitiveValues(provider, {
35
+ ...options,
36
+ sourceKinds: SELF_TEST_SENSITIVE_SOURCE_KINDS,
37
+ minimumLength: MIN_SENSITIVE_VALUE_LENGTH,
38
+ includeResolvedEnvVariants: false,
39
+ });
67
40
  }
68
41
 
69
42
  /**
@@ -79,10 +52,7 @@ export function redactSelfTestText(
79
52
  let redacted = text;
80
53
  for (const value of sensitiveValues) {
81
54
  if (value.length < MIN_SENSITIVE_VALUE_LENGTH) continue;
82
- redacted = redacted.replace(
83
- new RegExp(escapeRegExp(value), "g"),
84
- SELF_TEST_REDACTED_PLACEHOLDER,
85
- );
55
+ redacted = redacted.replaceAll(value, SELF_TEST_REDACTED_PLACEHOLDER);
86
56
  }
87
57
  for (const pattern of HEADER_SHAPED_SECRETS) {
88
58
  redacted = redacted.replace(
@@ -0,0 +1,200 @@
1
+ import {
2
+ resolvedStringVariants,
3
+ urlCredentialComponents,
4
+ otlpHeaderCredentialValues,
5
+ } from "../runtime/diagnostic-env-values.js";
6
+ import {
7
+ PROVIDER_CACHE_REDIS_URL_ENV,
8
+ PROVIDER_STATE_REDIS_URL_ENV,
9
+ REDIS_URL_ENV,
10
+ } from "../config/loader.js";
11
+ import {
12
+ ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES,
13
+ ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES,
14
+ } from "../engine.js";
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
+ import { APIFUSE__ENGINE__CEREMONY_LEASE_KEY } from "../runtime/egress-lease.js";
18
+ import {
19
+ APIFUSE__OCR__API_KEY_ENV,
20
+ APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV,
21
+ } from "../runtime/ocr.js";
22
+ import {
23
+ OTEL_EXPORTER_OTLP_HEADERS,
24
+ OTEL_EXPORTER_OTLP_TRACES_HEADERS,
25
+ OTEL_EXPORTER_OTLP_ENDPOINT,
26
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
27
+ } from "../runtime/otlp.js";
28
+ import { APIFUSE__CDP_POOL__URL } from "../runtime/resolver-config.js";
29
+ import { APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV } from "../runtime/stt.js";
30
+ import type { ProviderDefinition } from "../types.js";
31
+ import {
32
+ PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV,
33
+ PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV,
34
+ } from "./self-test-token.js";
35
+
36
+ export const SELF_TEST_SENSITIVE_SOURCE_KINDS = [
37
+ "selfTestMasterSecrets",
38
+ "providerSecrets",
39
+ "healthProbeSecrets",
40
+ "requestCredentials",
41
+ ] as const;
42
+
43
+ /** Explicit source allowlist: no environment-name or payload-shape secret sweep. */
44
+ export const DIAGNOSTIC_SENSITIVE_SOURCE_KINDS = [
45
+ ...SELF_TEST_SENSITIVE_SOURCE_KINDS,
46
+ "engineProxyCredentials",
47
+ "engineSolverKeys",
48
+ "engineCeremonyLeaseCredentials",
49
+ "choiceMasterSecret",
50
+ "ocrCredentials",
51
+ "sttCredentials",
52
+ "cdpCredentials",
53
+ "statefulForwardingSecret",
54
+ "redisCredentials",
55
+ "otlpHeaders",
56
+ "cacheKeyPepper",
57
+ "sensitiveParams",
58
+ ] as const;
59
+ export type DiagnosticSensitiveSourceKind = (typeof DIAGNOSTIC_SENSITIVE_SOURCE_KINDS)[number];
60
+
61
+ /**
62
+ * Startup inventory; request-scoped readers additionally register live values on access.
63
+ */
64
+ export function collectStaticDiagnosticSensitiveValues(
65
+ provider: ProviderDefinition,
66
+ options: {
67
+ env?: Readonly<Record<string, string | undefined>>;
68
+ statefulForwardingSecret?: string;
69
+ } = {},
70
+ ): string[] {
71
+ return collectDiagnosticSensitiveValues(provider, {
72
+ ...options,
73
+ sourceKinds: DIAGNOSTIC_SENSITIVE_SOURCE_KINDS.filter(
74
+ (kind) => kind !== "requestCredentials" && kind !== "sensitiveParams",
75
+ ),
76
+ });
77
+ }
78
+
79
+ /** Generalised self-test collector, also used to seed and enrich ordinary request scopes. */
80
+ export function collectDiagnosticSensitiveValues(
81
+ provider: ProviderDefinition,
82
+ options: {
83
+ env?: Readonly<Record<string, string | undefined>>;
84
+ credentialInputs?: Readonly<Record<string, unknown>>;
85
+ connectionSecrets?: Readonly<Record<string, string | undefined>>;
86
+ sensitiveParams?: readonly string[];
87
+ statefulForwardingSecret?: string;
88
+ sourceKinds?: readonly DiagnosticSensitiveSourceKind[];
89
+ minimumLength?: number;
90
+ /** Preserve the legacy self-test response collector when false. */
91
+ includeResolvedEnvVariants?: boolean;
92
+ } = {},
93
+ ): string[] {
94
+ const env = options.env ?? process.env;
95
+ const envValues = (names: readonly string[]) => names.map((name) => env[name]);
96
+ const resolvedEnvValues = (names: readonly string[]) =>
97
+ envValues(names).flatMap((value) =>
98
+ value === undefined
99
+ ? []
100
+ : options.includeResolvedEnvVariants === false
101
+ ? [value]
102
+ : [value, value.trim()],
103
+ );
104
+ const healthProbe = provider.healthProbe ?? provider.healthMonitor;
105
+ const sources: Record<DiagnosticSensitiveSourceKind, () => readonly unknown[]> = {
106
+ selfTestMasterSecrets: () =>
107
+ resolvedEnvValues([
108
+ PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV,
109
+ PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV,
110
+ ]),
111
+ providerSecrets: () => resolvedEnvValues((provider.secrets ?? []).map((secret) => secret.name)),
112
+ healthProbeSecrets: () =>
113
+ resolvedEnvValues([
114
+ ...(healthProbe?.requiredSecrets ?? []),
115
+ ...Object.values(healthProbe?.credentialInputs ?? {}),
116
+ ]),
117
+ requestCredentials: () => [
118
+ ...Object.values(options.credentialInputs ?? {}),
119
+ ...Object.values(options.connectionSecrets ?? {}),
120
+ ],
121
+ engineProxyCredentials: () => resolvedEnvValues(ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES),
122
+ // These are the resolver-config API keys after #251's engine credential projection.
123
+ engineSolverKeys: () => resolvedEnvValues(ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES),
124
+ engineCeremonyLeaseCredentials: () => resolvedEnvValues([APIFUSE__ENGINE__CEREMONY_LEASE_KEY]),
125
+ choiceMasterSecret: () => resolvedEnvValues([PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV]),
126
+ ocrCredentials: () =>
127
+ resolvedEnvValues([APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__OCR__API_KEY_ENV]),
128
+ sttCredentials: () => resolvedEnvValues([APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV]),
129
+ cdpCredentials: () =>
130
+ resolvedStringVariants(env[APIFUSE__CDP_POOL__URL]).flatMap((url) => [
131
+ url,
132
+ ...urlCredentialComponents(url, { includePathAndQuery: true }),
133
+ ]),
134
+ statefulForwardingSecret: () => resolvedStringVariants(options.statefulForwardingSecret),
135
+ redisCredentials: () =>
136
+ [PROVIDER_CACHE_REDIS_URL_ENV, PROVIDER_STATE_REDIS_URL_ENV, REDIS_URL_ENV]
137
+ .flatMap((name) => resolvedStringVariants(env[name]))
138
+ .flatMap((url) => urlCredentialComponents(url, { includePathAndQuery: false })),
139
+ otlpHeaders: () => [
140
+ ...[OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_HEADERS].flatMap((name) =>
141
+ env[name] === undefined ? [] : otlpHeaderCredentialValues(env[name]),
142
+ ),
143
+ ...[OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT].flatMap((name) =>
144
+ resolvedStringVariants(env[name]).flatMap((url) =>
145
+ urlCredentialComponents(url, { includePathAndQuery: false }),
146
+ ),
147
+ ),
148
+ ],
149
+ cacheKeyPepper: () => resolvedEnvValues([APIFUSE__CACHE__KEY_PEPPER_ENV]),
150
+ sensitiveParams: () => options.sensitiveParams ?? [],
151
+ };
152
+ const values = new Set<string>();
153
+ for (const kind of options.sourceKinds ?? DIAGNOSTIC_SENSITIVE_SOURCE_KINDS) {
154
+ for (const value of sources[kind]()) {
155
+ if (typeof value === "string" && value.length >= (options.minimumLength ?? 1))
156
+ values.add(value);
157
+ }
158
+ }
159
+ return [...values];
160
+ }
161
+
162
+ const diagnosticEnvNames = new WeakMap<ProviderDefinition, Set<string>>();
163
+
164
+ /** Derive the live-read allowlist from the same collector as the startup inventory. */
165
+ export function createDiagnosticEnvObserver(
166
+ provider: ProviderDefinition,
167
+ register: (values: readonly string[]) => void,
168
+ has?: (value: string) => boolean,
169
+ ): (name: string, value: string) => void {
170
+ let names = diagnosticEnvNames.get(provider);
171
+ if (!names) {
172
+ names = new Set<string>();
173
+ const captured = names;
174
+ collectStaticDiagnosticSensitiveValues(provider, {
175
+ env: new Proxy(
176
+ {},
177
+ {
178
+ get: (_target, key) => {
179
+ if (typeof key === "string") captured.add(key);
180
+ return undefined;
181
+ },
182
+ },
183
+ ),
184
+ });
185
+ diagnosticEnvNames.set(provider, names);
186
+ }
187
+ const seen = new Map<string, string>();
188
+ return (name, value) => {
189
+ if (!names.has(name) || seen.get(name) === value) return;
190
+ if (has?.(value) && has(value.trim())) return;
191
+ // Includes URL userinfo/CDP components and OTLP auth values, using the same rules.
192
+ register([
193
+ ...resolvedStringVariants(value),
194
+ ...collectStaticDiagnosticSensitiveValues(provider, {
195
+ env: { [name]: value },
196
+ }),
197
+ ]);
198
+ seen.set(name, value);
199
+ };
200
+ }