@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
@@ -1,4 +1,11 @@
1
1
  import { isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE } from "../fixture-sanitization.js";
2
+ import {
3
+ type DiagnosticRedactor,
4
+ redactedKeyAllocator,
5
+ isRedactedKey,
6
+ REDACTION_FAILED,
7
+ redactDiagnosticText,
8
+ } from "../runtime/diagnostic-redactor.js";
2
9
  import {
3
10
  OTEL_EXPORTER_OTLP_ENDPOINT,
4
11
  OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
@@ -22,6 +29,16 @@ import type { TraceConfig } from "../types.js";
22
29
 
23
30
  type EnvLike = Record<string, string | undefined>;
24
31
 
32
+ const TRACE_RESOURCE_ATTRIBUTE_SANITIZER = Symbol.for(
33
+ "@apifuse/provider-sdk/runtime/trace-resource-attribute-sanitizer",
34
+ );
35
+
36
+ type ResourceAttributesWithSanitizer = Record<string, string> & {
37
+ [TRACE_RESOURCE_ATTRIBUTE_SANITIZER]?: (
38
+ attributes: Record<string, string>,
39
+ ) => Record<string, string>;
40
+ };
41
+
25
42
  // Warn once per environment object: process.env in production, each injected env in tests.
26
43
  const warnedEnvironments = new WeakSet<EnvLike>();
27
44
  const warnedResourceEnvironments = new WeakSet<EnvLike>();
@@ -73,22 +90,52 @@ function resolveServerOTLPExportOptions(
73
90
  function resolveExportResourceAttributes(
74
91
  requestAttributes: Record<string, string>,
75
92
  env: EnvLike,
76
- ): Record<string, string> {
93
+ redact?: DiagnosticRedactor,
94
+ ): {
95
+ attributes: Record<string, string>;
96
+ sanitize: (attributes: Record<string, string>) => Record<string, string>;
97
+ } {
77
98
  const resolution = resolveOTLPResourceAttributes({}, env);
78
99
  warnDiscardedResourceAttributes(env, resolution);
79
- const operatorAttributes = Object.fromEntries(
80
- Object.entries(resolution.attributes).map(([key, value]) => [
81
- sanitizeSpanNameForOutput(key),
82
- isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeSpanNameForOutput(value),
83
- ]),
84
- );
85
- const sanitizedRequestAttributes = Object.fromEntries(
86
- Object.entries(sanitizeTraceAttributes(requestAttributes)).map(([key, value]) => [
87
- key,
88
- String(value),
89
- ]),
90
- );
91
- return { ...operatorAttributes, ...sanitizedRequestAttributes };
100
+ const operatorAttributes = resolution.attributes;
101
+ const requestKeys = new Set(Object.keys(requestAttributes));
102
+ return {
103
+ // Keep the raw snapshot private until export. A credential registered after
104
+ // trace creation must be matched before any key or value is bounded.
105
+ attributes: { ...operatorAttributes, ...requestAttributes },
106
+ sanitize(attributes) {
107
+ const nextKey = redactedKeyAllocator(Object.keys(attributes));
108
+ const sanitizedOperatorAttributes: Record<string, string> = {};
109
+ const rawRequestAttributes: Record<string, string> = {};
110
+ for (const [key, value] of Object.entries(attributes)) {
111
+ const unchangedOperatorValue = !requestKeys.has(key) && operatorAttributes[key] === value;
112
+ if (unchangedOperatorValue) {
113
+ const redactedKey = isRedactedKey(key) ? key : redactDiagnosticText(key, redact);
114
+ const keyChanged = redactedKey !== key;
115
+ const sanitizedKey = keyChanged ? nextKey() : sanitizeSpanNameForOutput(key);
116
+ sanitizedOperatorAttributes[sanitizedKey] = keyChanged
117
+ ? redactedKey === REDACTION_FAILED
118
+ ? REDACTION_FAILED
119
+ : REDACTED_FIXTURE_VALUE
120
+ : isSensitiveFixtureKey(key)
121
+ ? REDACTED_FIXTURE_VALUE
122
+ : sanitizeSpanNameForOutput(value, redact);
123
+ } else {
124
+ rawRequestAttributes[key] = value;
125
+ }
126
+ }
127
+ const sanitizedRequestAttributes = Object.fromEntries(
128
+ Object.entries(
129
+ sanitizeTraceAttributes(
130
+ rawRequestAttributes,
131
+ redact,
132
+ Object.keys(sanitizedOperatorAttributes),
133
+ ),
134
+ ).map(([key, value]) => [key, String(value)]),
135
+ );
136
+ return { ...sanitizedOperatorAttributes, ...sanitizedRequestAttributes };
137
+ },
138
+ };
92
139
  }
93
140
 
94
141
  /** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
@@ -96,12 +143,14 @@ export function resolveServerTraceContextOptions(
96
143
  config: TraceConfig,
97
144
  resourceAttributes: Record<string, string>,
98
145
  env: EnvLike = process.env,
146
+ redact?: DiagnosticRedactor,
99
147
  ): CreateTraceContextOptions {
100
148
  const resolved = resolveTraceContextOptions(config);
101
149
  const outputEnabled = config.enabled !== false && config.exporter !== "none";
102
150
  const consoleHook =
103
151
  outputEnabled && (config.exporter === "console" || config.exporter === "json")
104
- ? (span: Span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
152
+ ? (span: Span) =>
153
+ console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes, redact)))
105
154
  : undefined;
106
155
  const onSpan =
107
156
  consoleHook && resolved.onSpan
@@ -114,15 +163,26 @@ export function resolveServerTraceContextOptions(
114
163
  outputEnabled && config.exporter === "otlp"
115
164
  ? resolveServerOTLPExportOptions(config, env)
116
165
  : undefined;
166
+ const exportResources = exportOptions
167
+ ? resolveExportResourceAttributes(resourceAttributes, env, redact)
168
+ : undefined;
169
+ if (exportResources) {
170
+ Object.defineProperty(
171
+ exportResources.attributes as ResourceAttributesWithSanitizer,
172
+ TRACE_RESOURCE_ATTRIBUTE_SANITIZER,
173
+ { value: exportResources.sanitize },
174
+ );
175
+ }
117
176
 
118
177
  return {
119
178
  maxSpans: resolved.maxSpans,
120
179
  onSpan,
180
+ ...(redact ? { redact } : {}),
121
181
  ...(exportOptions
122
182
  ? {
123
183
  exportOptions,
124
- resourceAttributes: resolveExportResourceAttributes(resourceAttributes, env),
125
- sanitizeSpanForExport: (span: Span) => sanitizeSpanForOutput(span),
184
+ resourceAttributes: exportResources?.attributes,
185
+ sanitizeSpanForExport: (span: Span) => sanitizeSpanForOutput(span, undefined, redact),
126
186
  }
127
187
  : {}),
128
188
  };
@@ -4,20 +4,33 @@ import {
4
4
  REDACTED_FIXTURE_VALUE,
5
5
  sanitizeDiagnosticText,
6
6
  } from "./fixture-sanitization.js";
7
+ import {
8
+ type DiagnosticRedactor,
9
+ diagnosticStructuredRedactor,
10
+ redactDiagnosticAttributeKey,
11
+ copyDiagnosticAttributeTypes,
12
+ isDiagnosticBigintAttribute,
13
+ redactedKeyAllocator,
14
+ isRedactedKey,
15
+ redactCriticalDiagnosticText,
16
+ diagnosticAttributeRedactor,
17
+ REDACTION_FAILED,
18
+ redactDiagnosticText,
19
+ } from "./runtime/diagnostic-redactor.js";
7
20
  import type { TraceSpan } from "./types.js";
8
21
 
9
22
  const MAX_TRACE_TEXT_LENGTH = 300;
10
23
 
11
- function sanitizeTraceText(value: string): string {
12
- const encoded = sanitizeDiagnosticText(value);
24
+ function sanitizeTraceText(value: string, redact?: DiagnosticRedactor): string {
25
+ const encoded = sanitizeDiagnosticText(redactDiagnosticText(value, redact));
13
26
  return encoded.length > MAX_TRACE_TEXT_LENGTH
14
27
  ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
15
28
  : encoded;
16
29
  }
17
30
 
18
31
  /** Span names are SDK-authored identifiers; retain them while preventing log injection. */
19
- export function sanitizeSpanNameForOutput(value: string): string {
20
- const encoded = encodeDiagnosticControls(value);
32
+ export function sanitizeSpanNameForOutput(value: string, redact?: DiagnosticRedactor): string {
33
+ const encoded = encodeDiagnosticControls(redactDiagnosticText(value, redact));
21
34
  return encoded.length > MAX_TRACE_TEXT_LENGTH
22
35
  ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
23
36
  : encoded;
@@ -25,39 +38,73 @@ export function sanitizeSpanNameForOutput(value: string): string {
25
38
 
26
39
  export function sanitizeTraceAttributes(
27
40
  attributes: Record<string, unknown>,
41
+ redact?: DiagnosticRedactor,
42
+ reservedKeys: Iterable<string> = [],
28
43
  ): Record<string, string | number | boolean> {
29
- return Object.fromEntries(
30
- Object.entries(attributes).map(([key, value]) => [
31
- sanitizeTraceText(key),
32
- isSensitiveFixtureKey(key)
33
- ? REDACTED_FIXTURE_VALUE
34
- : typeof value === "string"
35
- ? sanitizeTraceText(value)
36
- : typeof value === "number" || typeof value === "boolean"
37
- ? value
38
- : sanitizeTraceText(String(value)),
39
- ]),
44
+ const nextKey = redactedKeyAllocator([...Object.keys(attributes), ...reservedKeys]);
45
+ return copyDiagnosticAttributeTypes(
46
+ attributes,
47
+ Object.fromEntries(
48
+ Object.entries(attributes).map(([key, value]) => {
49
+ const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
50
+ const keyChanged = redactedKey !== key;
51
+ const sanitizedKey = keyChanged ? nextKey() : sanitizeTraceText(key);
52
+ const valueRedactor =
53
+ typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
54
+ ? diagnosticStructuredRedactor(redact)
55
+ : diagnosticAttributeRedactor(key, redact);
56
+ const sanitizedValue = keyChanged
57
+ ? redactedKey === REDACTION_FAILED
58
+ ? REDACTION_FAILED
59
+ : REDACTED_FIXTURE_VALUE
60
+ : isSensitiveFixtureKey(key)
61
+ ? REDACTED_FIXTURE_VALUE
62
+ : typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
63
+ ? String(value).length < 8
64
+ ? String(value)
65
+ : sanitizeTraceText(String(value), valueRedactor)
66
+ : typeof value === "string"
67
+ ? sanitizeTraceText(value, valueRedactor)
68
+ : typeof value === "number" || typeof value === "boolean"
69
+ ? (() => {
70
+ if (String(value).length < 8) return value;
71
+ const redacted = redactDiagnosticText(
72
+ String(value),
73
+ diagnosticStructuredRedactor(valueRedactor),
74
+ );
75
+ return redacted === String(value) ? value : sanitizeTraceText(redacted);
76
+ })()
77
+ : sanitizeTraceText(String(value), valueRedactor);
78
+ return [sanitizedKey, sanitizedValue];
79
+ }),
80
+ ),
40
81
  );
41
82
  }
42
83
 
43
84
  export function sanitizeSpanForOutput(
44
85
  span: TraceSpan,
45
86
  additionalAttributes?: Record<string, string>,
87
+ redact?: DiagnosticRedactor,
46
88
  ): TraceSpan {
47
89
  // Keep this schema explicit so future fields are not silently added to a
48
90
  // process output path before their trust boundary has been reviewed.
49
91
  return {
50
- id: span.id,
51
- name: sanitizeSpanNameForOutput(span.name),
92
+ id: redactCriticalDiagnosticText(span.id, redact),
93
+ name: sanitizeSpanNameForOutput(redactCriticalDiagnosticText(span.name, redact)),
52
94
  startedAt: span.startedAt,
53
95
  endedAt: span.endedAt,
54
96
  duration_ms: span.duration_ms,
55
97
  status: span.status,
56
- attributes: sanitizeTraceAttributes({
57
- ...span.attributes,
58
- ...(additionalAttributes ?? {}),
59
- }),
60
- ...(span.error !== undefined ? { error: sanitizeTraceText(span.error) } : {}),
61
- ...(span.parentId !== undefined ? { parentId: span.parentId } : {}),
98
+ attributes: sanitizeTraceAttributes(
99
+ copyDiagnosticAttributeTypes(span.attributes, {
100
+ ...span.attributes,
101
+ ...(additionalAttributes ?? {}),
102
+ }),
103
+ redact,
104
+ ),
105
+ ...(span.error !== undefined ? { error: sanitizeTraceText(span.error, redact) } : {}),
106
+ ...(span.parentId !== undefined
107
+ ? { parentId: redactCriticalDiagnosticText(span.parentId, redact) }
108
+ : {}),
62
109
  };
63
110
  }