@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.
- package/AUTHORING.md +16 -0
- package/CHANGELOG.md +4 -0
- package/bin/apifuse-dev.ts +2 -1
- package/bin/apifuse-perf.ts +41 -4
- package/bin/apifuse-record.ts +3 -2
- package/dist/config/loader.js +13 -12
- package/dist/engine.js +3 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/lint.js +36 -15
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/browser.js +2 -1
- package/dist/runtime/cache.js +3 -2
- package/dist/runtime/diagnostic-env-values.d.ts +5 -0
- package/dist/runtime/diagnostic-env-values.js +57 -0
- package/dist/runtime/diagnostic-env.d.ts +11 -0
- package/dist/runtime/diagnostic-env.js +35 -0
- package/dist/runtime/diagnostic-redactor.d.ts +49 -0
- package/dist/runtime/diagnostic-redactor.js +569 -0
- package/dist/runtime/egress-lease.d.ts +2 -0
- package/dist/runtime/egress-lease.js +4 -1
- package/dist/runtime/env.js +2 -1
- package/dist/runtime/instrumentation.js +3 -0
- package/dist/runtime/ocr.js +2 -1
- package/dist/runtime/otlp.js +5 -4
- package/dist/runtime/proxy-nodemaven.js +3 -2
- package/dist/runtime/request-options.d.ts +7 -0
- package/dist/runtime/request-options.js +62 -22
- package/dist/runtime/resolver-vendors/browser.js +0 -1
- package/dist/runtime/resolver.js +3 -2
- package/dist/runtime/stt.js +3 -2
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +115 -20
- package/dist/schema.d.ts +11 -0
- package/dist/schema.js +56 -2
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +131 -0
- package/dist/server/serve-implementation.js +219 -78
- package/dist/server/trace-output.d.ts +2 -1
- package/dist/server/trace-output.js +46 -14
- package/dist/trace-sanitization.d.ts +4 -3
- package/dist/trace-sanitization.js +45 -22
- package/package.json +2 -1
- package/src/config/loader.ts +13 -12
- package/src/engine.ts +3 -2
- package/src/index.ts +2 -0
- package/src/lint.ts +42 -16
- package/src/provider.ts +1 -0
- package/src/runtime/browser.ts +2 -1
- package/src/runtime/cache.ts +3 -2
- package/src/runtime/diagnostic-env-values.ts +56 -0
- package/src/runtime/diagnostic-env.ts +55 -0
- package/src/runtime/diagnostic-redactor.ts +662 -0
- package/src/runtime/egress-lease.ts +6 -1
- package/src/runtime/env.ts +2 -1
- package/src/runtime/instrumentation.ts +3 -0
- package/src/runtime/ocr.ts +2 -1
- package/src/runtime/otlp.ts +5 -4
- package/src/runtime/proxy-nodemaven.ts +3 -2
- package/src/runtime/request-options.ts +76 -28
- package/src/runtime/resolver-vendors/browser.ts +0 -1
- package/src/runtime/resolver.ts +3 -2
- package/src/runtime/stt.ts +3 -2
- package/src/runtime/trace.ts +168 -26
- package/src/schema.ts +63 -2
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/sensitive-values.ts +200 -0
- package/src/server/serve-implementation.ts +301 -96
- package/src/server/trace-output.ts +77 -17
- package/src/trace-sanitization.ts +70 -23
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type DiagnosticRedactor } from "../runtime/diagnostic-redactor.js";
|
|
1
2
|
import { type CreateTraceContextOptions } from "../runtime/trace.js";
|
|
2
3
|
import type { TraceConfig } from "../types.js";
|
|
3
4
|
type EnvLike = Record<string, string | undefined>;
|
|
4
5
|
/** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
|
|
5
|
-
export declare function resolveServerTraceContextOptions(config: TraceConfig, resourceAttributes: Record<string, string>, env?: EnvLike): CreateTraceContextOptions;
|
|
6
|
+
export declare function resolveServerTraceContextOptions(config: TraceConfig, resourceAttributes: Record<string, string>, env?: EnvLike, redact?: DiagnosticRedactor): CreateTraceContextOptions;
|
|
6
7
|
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE } from "../fixture-sanitization.js";
|
|
2
|
+
import { redactedKeyAllocator, isRedactedKey, REDACTION_FAILED, redactDiagnosticText, } from "../runtime/diagnostic-redactor.js";
|
|
2
3
|
import { OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, resolveOTLPExportOptions, resolveOTLPResourceAttributes, } from "../runtime/otlp.js";
|
|
3
4
|
import { resolveTraceContextOptions, } from "../runtime/trace.js";
|
|
4
5
|
import { sanitizeSpanForOutput, sanitizeSpanNameForOutput, sanitizeTraceAttributes, } from "../trace-sanitization.js";
|
|
6
|
+
const TRACE_RESOURCE_ATTRIBUTE_SANITIZER = Symbol.for("@apifuse/provider-sdk/runtime/trace-resource-attribute-sanitizer");
|
|
5
7
|
// Warn once per environment object: process.env in production, each injected env in tests.
|
|
6
8
|
const warnedEnvironments = new WeakSet();
|
|
7
9
|
const warnedResourceEnvironments = new WeakSet();
|
|
@@ -36,25 +38,48 @@ function resolveServerOTLPExportOptions(config, env) {
|
|
|
36
38
|
* environment are identifiers rather than request data: their values are kept, with control
|
|
37
39
|
* characters neutralized, length bounded, and secret-named keys redacted.
|
|
38
40
|
*/
|
|
39
|
-
function resolveExportResourceAttributes(requestAttributes, env) {
|
|
41
|
+
function resolveExportResourceAttributes(requestAttributes, env, redact) {
|
|
40
42
|
const resolution = resolveOTLPResourceAttributes({}, env);
|
|
41
43
|
warnDiscardedResourceAttributes(env, resolution);
|
|
42
|
-
const operatorAttributes =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
const operatorAttributes = resolution.attributes;
|
|
45
|
+
const requestKeys = new Set(Object.keys(requestAttributes));
|
|
46
|
+
return {
|
|
47
|
+
// Keep the raw snapshot private until export. A credential registered after
|
|
48
|
+
// trace creation must be matched before any key or value is bounded.
|
|
49
|
+
attributes: { ...operatorAttributes, ...requestAttributes },
|
|
50
|
+
sanitize(attributes) {
|
|
51
|
+
const nextKey = redactedKeyAllocator(Object.keys(attributes));
|
|
52
|
+
const sanitizedOperatorAttributes = {};
|
|
53
|
+
const rawRequestAttributes = {};
|
|
54
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
55
|
+
const unchangedOperatorValue = !requestKeys.has(key) && operatorAttributes[key] === value;
|
|
56
|
+
if (unchangedOperatorValue) {
|
|
57
|
+
const redactedKey = isRedactedKey(key) ? key : redactDiagnosticText(key, redact);
|
|
58
|
+
const keyChanged = redactedKey !== key;
|
|
59
|
+
const sanitizedKey = keyChanged ? nextKey() : sanitizeSpanNameForOutput(key);
|
|
60
|
+
sanitizedOperatorAttributes[sanitizedKey] = keyChanged
|
|
61
|
+
? redactedKey === REDACTION_FAILED
|
|
62
|
+
? REDACTION_FAILED
|
|
63
|
+
: REDACTED_FIXTURE_VALUE
|
|
64
|
+
: isSensitiveFixtureKey(key)
|
|
65
|
+
? REDACTED_FIXTURE_VALUE
|
|
66
|
+
: sanitizeSpanNameForOutput(value, redact);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
rawRequestAttributes[key] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const sanitizedRequestAttributes = Object.fromEntries(Object.entries(sanitizeTraceAttributes(rawRequestAttributes, redact, Object.keys(sanitizedOperatorAttributes))).map(([key, value]) => [key, String(value)]));
|
|
73
|
+
return { ...sanitizedOperatorAttributes, ...sanitizedRequestAttributes };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
51
76
|
}
|
|
52
77
|
/** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
|
|
53
|
-
export function resolveServerTraceContextOptions(config, resourceAttributes, env = process.env) {
|
|
78
|
+
export function resolveServerTraceContextOptions(config, resourceAttributes, env = process.env, redact) {
|
|
54
79
|
const resolved = resolveTraceContextOptions(config);
|
|
55
80
|
const outputEnabled = config.enabled !== false && config.exporter !== "none";
|
|
56
81
|
const consoleHook = outputEnabled && (config.exporter === "console" || config.exporter === "json")
|
|
57
|
-
? (span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
|
|
82
|
+
? (span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes, redact)))
|
|
58
83
|
: undefined;
|
|
59
84
|
const onSpan = consoleHook && resolved.onSpan
|
|
60
85
|
? (span) => {
|
|
@@ -65,14 +90,21 @@ export function resolveServerTraceContextOptions(config, resourceAttributes, env
|
|
|
65
90
|
const exportOptions = outputEnabled && config.exporter === "otlp"
|
|
66
91
|
? resolveServerOTLPExportOptions(config, env)
|
|
67
92
|
: undefined;
|
|
93
|
+
const exportResources = exportOptions
|
|
94
|
+
? resolveExportResourceAttributes(resourceAttributes, env, redact)
|
|
95
|
+
: undefined;
|
|
96
|
+
if (exportResources) {
|
|
97
|
+
Object.defineProperty(exportResources.attributes, TRACE_RESOURCE_ATTRIBUTE_SANITIZER, { value: exportResources.sanitize });
|
|
98
|
+
}
|
|
68
99
|
return {
|
|
69
100
|
maxSpans: resolved.maxSpans,
|
|
70
101
|
onSpan,
|
|
102
|
+
...(redact ? { redact } : {}),
|
|
71
103
|
...(exportOptions
|
|
72
104
|
? {
|
|
73
105
|
exportOptions,
|
|
74
|
-
resourceAttributes:
|
|
75
|
-
sanitizeSpanForExport: (span) => sanitizeSpanForOutput(span),
|
|
106
|
+
resourceAttributes: exportResources?.attributes,
|
|
107
|
+
sanitizeSpanForExport: (span) => sanitizeSpanForOutput(span, undefined, redact),
|
|
76
108
|
}
|
|
77
109
|
: {}),
|
|
78
110
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type DiagnosticRedactor } from "./runtime/diagnostic-redactor.js";
|
|
1
2
|
import type { TraceSpan } from "./types.js";
|
|
2
3
|
/** Span names are SDK-authored identifiers; retain them while preventing log injection. */
|
|
3
|
-
export declare function sanitizeSpanNameForOutput(value: string): string;
|
|
4
|
-
export declare function sanitizeTraceAttributes(attributes: Record<string, unknown>): Record<string, string | number | boolean>;
|
|
5
|
-
export declare function sanitizeSpanForOutput(span: TraceSpan, additionalAttributes?: Record<string, string
|
|
4
|
+
export declare function sanitizeSpanNameForOutput(value: string, redact?: DiagnosticRedactor): string;
|
|
5
|
+
export declare function sanitizeTraceAttributes(attributes: Record<string, unknown>, redact?: DiagnosticRedactor, reservedKeys?: Iterable<string>): Record<string, string | number | boolean>;
|
|
6
|
+
export declare function sanitizeSpanForOutput(span: TraceSpan, additionalAttributes?: Record<string, string>, redact?: DiagnosticRedactor): TraceSpan;
|
|
@@ -1,45 +1,68 @@
|
|
|
1
1
|
import { encodeDiagnosticControls, isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE, sanitizeDiagnosticText, } from "./fixture-sanitization.js";
|
|
2
|
+
import { diagnosticStructuredRedactor, redactDiagnosticAttributeKey, copyDiagnosticAttributeTypes, isDiagnosticBigintAttribute, redactedKeyAllocator, isRedactedKey, redactCriticalDiagnosticText, diagnosticAttributeRedactor, REDACTION_FAILED, redactDiagnosticText, } from "./runtime/diagnostic-redactor.js";
|
|
2
3
|
const MAX_TRACE_TEXT_LENGTH = 300;
|
|
3
|
-
function sanitizeTraceText(value) {
|
|
4
|
-
const encoded = sanitizeDiagnosticText(value);
|
|
4
|
+
function sanitizeTraceText(value, redact) {
|
|
5
|
+
const encoded = sanitizeDiagnosticText(redactDiagnosticText(value, redact));
|
|
5
6
|
return encoded.length > MAX_TRACE_TEXT_LENGTH
|
|
6
7
|
? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
|
|
7
8
|
: encoded;
|
|
8
9
|
}
|
|
9
10
|
/** Span names are SDK-authored identifiers; retain them while preventing log injection. */
|
|
10
|
-
export function sanitizeSpanNameForOutput(value) {
|
|
11
|
-
const encoded = encodeDiagnosticControls(value);
|
|
11
|
+
export function sanitizeSpanNameForOutput(value, redact) {
|
|
12
|
+
const encoded = encodeDiagnosticControls(redactDiagnosticText(value, redact));
|
|
12
13
|
return encoded.length > MAX_TRACE_TEXT_LENGTH
|
|
13
14
|
? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
|
|
14
15
|
: encoded;
|
|
15
16
|
}
|
|
16
|
-
export function sanitizeTraceAttributes(attributes) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
export function sanitizeTraceAttributes(attributes, redact, reservedKeys = []) {
|
|
18
|
+
const nextKey = redactedKeyAllocator([...Object.keys(attributes), ...reservedKeys]);
|
|
19
|
+
return copyDiagnosticAttributeTypes(attributes, Object.fromEntries(Object.entries(attributes).map(([key, value]) => {
|
|
20
|
+
const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
|
|
21
|
+
const keyChanged = redactedKey !== key;
|
|
22
|
+
const sanitizedKey = keyChanged ? nextKey() : sanitizeTraceText(key);
|
|
23
|
+
const valueRedactor = typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
|
|
24
|
+
? diagnosticStructuredRedactor(redact)
|
|
25
|
+
: diagnosticAttributeRedactor(key, redact);
|
|
26
|
+
const sanitizedValue = keyChanged
|
|
27
|
+
? redactedKey === REDACTION_FAILED
|
|
28
|
+
? REDACTION_FAILED
|
|
29
|
+
: REDACTED_FIXTURE_VALUE
|
|
30
|
+
: isSensitiveFixtureKey(key)
|
|
31
|
+
? REDACTED_FIXTURE_VALUE
|
|
32
|
+
: typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
|
|
33
|
+
? String(value).length < 8
|
|
34
|
+
? String(value)
|
|
35
|
+
: sanitizeTraceText(String(value), valueRedactor)
|
|
36
|
+
: typeof value === "string"
|
|
37
|
+
? sanitizeTraceText(value, valueRedactor)
|
|
38
|
+
: typeof value === "number" || typeof value === "boolean"
|
|
39
|
+
? (() => {
|
|
40
|
+
if (String(value).length < 8)
|
|
41
|
+
return value;
|
|
42
|
+
const redacted = redactDiagnosticText(String(value), diagnosticStructuredRedactor(valueRedactor));
|
|
43
|
+
return redacted === String(value) ? value : sanitizeTraceText(redacted);
|
|
44
|
+
})()
|
|
45
|
+
: sanitizeTraceText(String(value), valueRedactor);
|
|
46
|
+
return [sanitizedKey, sanitizedValue];
|
|
47
|
+
})));
|
|
27
48
|
}
|
|
28
|
-
export function sanitizeSpanForOutput(span, additionalAttributes) {
|
|
49
|
+
export function sanitizeSpanForOutput(span, additionalAttributes, redact) {
|
|
29
50
|
// Keep this schema explicit so future fields are not silently added to a
|
|
30
51
|
// process output path before their trust boundary has been reviewed.
|
|
31
52
|
return {
|
|
32
|
-
id: span.id,
|
|
33
|
-
name: sanitizeSpanNameForOutput(span.name),
|
|
53
|
+
id: redactCriticalDiagnosticText(span.id, redact),
|
|
54
|
+
name: sanitizeSpanNameForOutput(redactCriticalDiagnosticText(span.name, redact)),
|
|
34
55
|
startedAt: span.startedAt,
|
|
35
56
|
endedAt: span.endedAt,
|
|
36
57
|
duration_ms: span.duration_ms,
|
|
37
58
|
status: span.status,
|
|
38
|
-
attributes: sanitizeTraceAttributes({
|
|
59
|
+
attributes: sanitizeTraceAttributes(copyDiagnosticAttributeTypes(span.attributes, {
|
|
39
60
|
...span.attributes,
|
|
40
61
|
...(additionalAttributes ?? {}),
|
|
41
|
-
}),
|
|
42
|
-
...(span.error !== undefined ? { error: sanitizeTraceText(span.error) } : {}),
|
|
43
|
-
...(span.parentId !== undefined
|
|
62
|
+
}), redact),
|
|
63
|
+
...(span.error !== undefined ? { error: sanitizeTraceText(span.error, redact) } : {}),
|
|
64
|
+
...(span.parentId !== undefined
|
|
65
|
+
? { parentId: redactCriticalDiagnosticText(span.parentId, redact) }
|
|
66
|
+
: {}),
|
|
44
67
|
};
|
|
45
68
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.60",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
"format": "biome format --write",
|
|
111
111
|
"type-check": "tsc --noEmit",
|
|
112
112
|
"test": "bun test",
|
|
113
|
+
"bench:redaction": "bun scripts/benchmark-request-redaction.ts",
|
|
113
114
|
"check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run lint:test-typesafety && bun run build",
|
|
114
115
|
"api:update": "bun run build && bun scripts/api-reports.ts update",
|
|
115
116
|
"api:check": "bun run build && bun scripts/api-reports.ts check",
|
package/src/config/loader.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "../runtime/diagnostic-env.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
@@ -256,8 +257,8 @@ const SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS = 10_000;
|
|
|
256
257
|
|
|
257
258
|
function redisUrlFromEnv(): string | undefined {
|
|
258
259
|
return (
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
|
|
261
|
+
readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
|
|
261
262
|
undefined
|
|
262
263
|
);
|
|
263
264
|
}
|
|
@@ -268,9 +269,9 @@ export function providerCacheRedisUrlFromEnv(): string | undefined {
|
|
|
268
269
|
|
|
269
270
|
export function providerStateRedisUrlFromEnv(): string | undefined {
|
|
270
271
|
return (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
readDiagnosticEnv(PROVIDER_STATE_REDIS_URL_ENV)?.trim() ||
|
|
273
|
+
readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
|
|
274
|
+
readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
|
|
274
275
|
undefined
|
|
275
276
|
);
|
|
276
277
|
}
|
|
@@ -693,12 +694,12 @@ export async function resolveWithVendor(
|
|
|
693
694
|
const startedAt = Date.now();
|
|
694
695
|
const username = (
|
|
695
696
|
context.credentials === undefined
|
|
696
|
-
?
|
|
697
|
+
? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
|
|
697
698
|
: context.credentials[NODEMAVEN_USERNAME_ENV]
|
|
698
699
|
)?.trim();
|
|
699
700
|
const password = (
|
|
700
701
|
context.credentials === undefined
|
|
701
|
-
?
|
|
702
|
+
? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
|
|
702
703
|
: context.credentials[NODEMAVEN_PASSWORD_ENV]
|
|
703
704
|
)?.trim();
|
|
704
705
|
const filter =
|
|
@@ -749,7 +750,7 @@ export async function resolveWithVendor(
|
|
|
749
750
|
// smartproxy allocation-style vendor.
|
|
750
751
|
const appKey = (
|
|
751
752
|
context.credentials === undefined
|
|
752
|
-
?
|
|
753
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
753
754
|
: context.credentials[SMARTPROXY_APP_KEY_ENV]
|
|
754
755
|
)?.trim();
|
|
755
756
|
if (!appKey) {
|
|
@@ -838,18 +839,18 @@ function vendorHasCredentials(
|
|
|
838
839
|
if (vendor === "nodemaven") {
|
|
839
840
|
return Boolean(
|
|
840
841
|
(credentials === undefined
|
|
841
|
-
?
|
|
842
|
+
? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
|
|
842
843
|
: credentials[NODEMAVEN_USERNAME_ENV]
|
|
843
844
|
)?.trim() &&
|
|
844
845
|
(credentials === undefined
|
|
845
|
-
?
|
|
846
|
+
? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
|
|
846
847
|
: credentials[NODEMAVEN_PASSWORD_ENV]
|
|
847
848
|
)?.trim(),
|
|
848
849
|
);
|
|
849
850
|
}
|
|
850
851
|
return Boolean(
|
|
851
852
|
(credentials === undefined
|
|
852
|
-
?
|
|
853
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
853
854
|
: credentials[SMARTPROXY_APP_KEY_ENV]
|
|
854
855
|
)?.trim(),
|
|
855
856
|
);
|
|
@@ -1757,7 +1758,7 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
|
|
|
1757
1758
|
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
1758
1759
|
const appKey = (
|
|
1759
1760
|
options.engineCredentials === undefined
|
|
1760
|
-
?
|
|
1761
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
1761
1762
|
: options.engineCredentials[SMARTPROXY_APP_KEY_ENV]
|
|
1762
1763
|
)?.trim();
|
|
1763
1764
|
if (!appKey) return undefined;
|
package/src/engine.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./runtime/diagnostic-env.js";
|
|
1
2
|
import { ProviderError } from "./errors.js";
|
|
2
3
|
import {
|
|
3
4
|
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
@@ -160,7 +161,7 @@ export function readEngineProxyCredentials(
|
|
|
160
161
|
): Readonly<Record<string, string>> {
|
|
161
162
|
return Object.fromEntries(
|
|
162
163
|
ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES.flatMap((name) => {
|
|
163
|
-
const value = environment
|
|
164
|
+
const value = readDiagnosticEnv(name, environment)?.trim();
|
|
164
165
|
return value ? [[name, value] as const] : [];
|
|
165
166
|
}),
|
|
166
167
|
);
|
|
@@ -174,7 +175,7 @@ export function createProviderEnvironment(
|
|
|
174
175
|
return Object.fromEntries(
|
|
175
176
|
declaredNames.flatMap((name) => {
|
|
176
177
|
if (isEngineOwnedEnvName(name)) return [];
|
|
177
|
-
const value = environment
|
|
178
|
+
const value = readDiagnosticEnv(name, environment);
|
|
178
179
|
return value === undefined ? [] : [[name, value] as const];
|
|
179
180
|
}),
|
|
180
181
|
);
|
package/src/index.ts
CHANGED
|
@@ -275,6 +275,7 @@ export {
|
|
|
275
275
|
type Span,
|
|
276
276
|
type TraceContext,
|
|
277
277
|
} from "./runtime/trace.js";
|
|
278
|
+
export type { DiagnosticRedactor } from "./runtime/diagnostic-redactor.js";
|
|
278
279
|
export {
|
|
279
280
|
APIFUSE_CONTENT_PROVENANCE_META_KEY,
|
|
280
281
|
APIFUSE_CONTENT_TRUST_META_KEY,
|
|
@@ -287,6 +288,7 @@ export {
|
|
|
287
288
|
field,
|
|
288
289
|
fields,
|
|
289
290
|
isSensitiveSchema,
|
|
291
|
+
publicField,
|
|
290
292
|
redactPayload,
|
|
291
293
|
type SensitiveFieldKind,
|
|
292
294
|
type SensitiveFieldOptions,
|
package/src/lint.ts
CHANGED
|
@@ -550,18 +550,29 @@ function uniqueFields(fields: string[]): string[] {
|
|
|
550
550
|
return Array.from(new Set(fields));
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
|
|
553
|
+
function readSensitivityDeclaration(
|
|
554
|
+
schema: unknown,
|
|
555
|
+
seen = new Set<unknown>(),
|
|
556
|
+
): boolean | undefined {
|
|
557
|
+
// Declarations sit on the leaf; same-path wrappers such as .optional(),
|
|
558
|
+
// .nullable(), .pipe() or z.lazy() carry no metadata of their own, so look
|
|
559
|
+
// through them.
|
|
560
|
+
if (!isSchema(schema) || seen.has(schema)) return undefined;
|
|
561
|
+
seen.add(schema);
|
|
562
|
+
const value = Reflect.get(getSchemaMetadata(schema), APIFUSE_SENSITIVE_META_KEY);
|
|
563
|
+
if (typeof value === "boolean") return value;
|
|
564
|
+
const def = getSchemaDef(schema);
|
|
565
|
+
const children =
|
|
566
|
+
def.type === "pipe"
|
|
567
|
+
? [def.in, def.out]
|
|
568
|
+
: def.type === "lazy" && typeof def.getter === "function"
|
|
569
|
+
? [def.getter()]
|
|
570
|
+
: [def.innerType];
|
|
571
|
+
for (const child of children) {
|
|
572
|
+
const declaration = readSensitivityDeclaration(child, seen);
|
|
573
|
+
if (declaration !== undefined) return declaration;
|
|
556
574
|
}
|
|
557
|
-
|
|
558
|
-
if (typeof meta !== "function") return false;
|
|
559
|
-
const metadata = meta.call(schema);
|
|
560
|
-
return (
|
|
561
|
-
!!metadata &&
|
|
562
|
-
typeof metadata === "object" &&
|
|
563
|
-
Reflect.get(metadata, APIFUSE_SENSITIVE_META_KEY) === true
|
|
564
|
-
);
|
|
575
|
+
return undefined;
|
|
565
576
|
}
|
|
566
577
|
|
|
567
578
|
function getSchemaMetadata(schema: SchemaLike): Record<string, unknown> {
|
|
@@ -591,9 +602,24 @@ const SENSITIVE_FIELD_NAMES = new Set([
|
|
|
591
602
|
"paymenturl",
|
|
592
603
|
]);
|
|
593
604
|
|
|
605
|
+
// Only phone-shaped keys can hold reviewed public data (facility phones); the
|
|
606
|
+
// rest are credentials, which a public declaration must not launder.
|
|
607
|
+
const PUBLIC_DECLARABLE_FIELD_NAMES = new Set(["phone", "phonenumber"]);
|
|
608
|
+
|
|
609
|
+
function normalizeFieldName(name: string): string {
|
|
610
|
+
return name.toLowerCase().replace(/[-_\s]/g, "");
|
|
611
|
+
}
|
|
612
|
+
|
|
594
613
|
function isSensitiveFieldName(name: string): boolean {
|
|
595
|
-
|
|
596
|
-
|
|
614
|
+
return SENSITIVE_FIELD_NAMES.has(normalizeFieldName(name));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function isDeclaredSensitiveField(key: string, schema: unknown): boolean {
|
|
618
|
+
const declaration = readSensitivityDeclaration(schema);
|
|
619
|
+
return (
|
|
620
|
+
declaration === true ||
|
|
621
|
+
(declaration === false && PUBLIC_DECLARABLE_FIELD_NAMES.has(normalizeFieldName(key)))
|
|
622
|
+
);
|
|
597
623
|
}
|
|
598
624
|
|
|
599
625
|
function collectUnmarkedSensitiveFields(
|
|
@@ -608,7 +634,7 @@ function collectUnmarkedSensitiveFields(
|
|
|
608
634
|
const out: string[] = [];
|
|
609
635
|
for (const [key, child] of Object.entries(getObjectShape(schema))) {
|
|
610
636
|
const childPath = basePath ? `${basePath}.${key}` : key;
|
|
611
|
-
if (isSensitiveFieldName(key) && !
|
|
637
|
+
if (isSensitiveFieldName(key) && !isDeclaredSensitiveField(key, child)) {
|
|
612
638
|
out.push(childPath);
|
|
613
639
|
}
|
|
614
640
|
out.push(...collectUnmarkedSensitiveFields(child, childPath, seen));
|
|
@@ -1401,7 +1427,7 @@ export function lintOperation(op: {
|
|
|
1401
1427
|
rule: "sensitive-field-unmarked",
|
|
1402
1428
|
level: "warn",
|
|
1403
1429
|
field,
|
|
1404
|
-
message: `Schema field "${field}" looks sensitive; mark it with fields.*()
|
|
1430
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*() or sensitive(...); publicField(...) is accepted only for reviewed public phone-shaped fields.`,
|
|
1405
1431
|
});
|
|
1406
1432
|
}
|
|
1407
1433
|
|
|
@@ -1410,7 +1436,7 @@ export function lintOperation(op: {
|
|
|
1410
1436
|
rule: "sensitive-field-unmarked",
|
|
1411
1437
|
level: "warn",
|
|
1412
1438
|
field,
|
|
1413
|
-
message: `Schema field "${field}" looks sensitive; mark it with fields.*()
|
|
1439
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*() or sensitive(...); publicField(...) is accepted only for reviewed public phone-shaped fields.`,
|
|
1414
1440
|
});
|
|
1415
1441
|
}
|
|
1416
1442
|
|
package/src/provider.ts
CHANGED
package/src/runtime/browser.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./diagnostic-env.js";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import type { CDPSession, Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
|
|
3
4
|
|
|
@@ -495,7 +496,7 @@ type SupportedBrowserClient = {
|
|
|
495
496
|
};
|
|
496
497
|
|
|
497
498
|
function getDefaultCdpPoolUrl(env = process.env): string | undefined {
|
|
498
|
-
return env
|
|
499
|
+
return readDiagnosticEnv("APIFUSE__CDP_POOL__URL", env);
|
|
499
500
|
}
|
|
500
501
|
|
|
501
502
|
async function importOptionalModule<T extends object>(moduleName: string): Promise<T> {
|
package/src/runtime/cache.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./diagnostic-env.js";
|
|
1
2
|
import { createHash, createHmac } from "node:crypto";
|
|
2
3
|
|
|
3
4
|
import { providerCacheRedisUrlFromEnv } from "../config/loader.js";
|
|
@@ -362,7 +363,7 @@ async function withRedisFallback<T>(operation: () => Promise<T>): Promise<T | un
|
|
|
362
363
|
|
|
363
364
|
export function createProviderCache(options: ProviderCacheOptions): ProviderCache {
|
|
364
365
|
const redisUrl = options.redisUrl ?? providerCacheRedisUrlFromEnv();
|
|
365
|
-
const configuredPepper =
|
|
366
|
+
const configuredPepper = readDiagnosticEnv(APIFUSE__CACHE__KEY_PEPPER_ENV);
|
|
366
367
|
const pepper = configuredPepper && configuredPepper.length > 0 ? configuredPepper : undefined;
|
|
367
368
|
const backend = getSharedBackend(redisUrl);
|
|
368
369
|
const memoryMaxEntries = Math.max(1, options.memoryMaxEntries ?? DEFAULT_MEMORY_MAX_ENTRIES);
|
|
@@ -599,7 +600,7 @@ export function isProviderCacheBypassed(cache: ProviderCache): boolean {
|
|
|
599
600
|
export function createBypassProviderCache(
|
|
600
601
|
options: Pick<ProviderCacheOptions, "providerId">,
|
|
601
602
|
): ProviderCache {
|
|
602
|
-
const configuredPepper =
|
|
603
|
+
const configuredPepper = readDiagnosticEnv(APIFUSE__CACHE__KEY_PEPPER_ENV);
|
|
603
604
|
const pepper = configuredPepper && configuredPepper.length > 0 ? configuredPepper : undefined;
|
|
604
605
|
const events: ProviderCacheLookupMeta[] = [];
|
|
605
606
|
const secretScopedKeys = new Set<string>();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function resolvedStringVariants(value: string | undefined): string[] {
|
|
2
|
+
if (value === undefined) return [];
|
|
3
|
+
return [...new Set([value, value.trim()])].filter((candidate) => candidate.length > 0);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function decodedComponent(value: string): string {
|
|
7
|
+
try {
|
|
8
|
+
return decodeURIComponent(value);
|
|
9
|
+
} catch {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function urlCredentialComponents(
|
|
15
|
+
value: string,
|
|
16
|
+
options: { includePathAndQuery: boolean },
|
|
17
|
+
): string[] {
|
|
18
|
+
let url: URL;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(value.trim());
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const username = decodedComponent(url.username);
|
|
25
|
+
const password = decodedComponent(url.password);
|
|
26
|
+
const components = [username, password, username && password ? `${username}:${password}` : ""];
|
|
27
|
+
if (options.includePathAndQuery) {
|
|
28
|
+
for (const segment of url.pathname.split("/")) {
|
|
29
|
+
if (segment) components.push(decodedComponent(segment));
|
|
30
|
+
}
|
|
31
|
+
for (const [key, entryValue] of url.searchParams) {
|
|
32
|
+
if (key) components.push(key);
|
|
33
|
+
if (entryValue) {
|
|
34
|
+
components.push(entryValue, `${key}=${entryValue}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [...new Set(components.flatMap((component) => resolvedStringVariants(component)))];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function otlpHeaderCredentialValues(value: string): string[] {
|
|
42
|
+
const variants = resolvedStringVariants(value);
|
|
43
|
+
for (const member of value.split(",")) {
|
|
44
|
+
const separator = member.indexOf("=");
|
|
45
|
+
if (separator <= 0) continue;
|
|
46
|
+
const headerValue = decodedComponent(member.slice(separator + 1).trim());
|
|
47
|
+
variants.push(...resolvedStringVariants(headerValue));
|
|
48
|
+
const headerName = decodedComponent(member.slice(0, separator).trim()).toLowerCase();
|
|
49
|
+
if (headerName === "authorization" || headerName === "proxy-authorization") {
|
|
50
|
+
// A transport may echo just the credential without its HTTP auth scheme.
|
|
51
|
+
const credential = /^(?:Bearer|Basic)\s+(.+)$/i.exec(headerValue)?.[1];
|
|
52
|
+
if (credential) variants.push(...resolvedStringVariants(credential));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...new Set(variants)];
|
|
56
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
resolvedStringVariants,
|
|
5
|
+
urlCredentialComponents,
|
|
6
|
+
otlpHeaderCredentialValues,
|
|
7
|
+
} from "./diagnostic-env-values.js";
|
|
8
|
+
import {
|
|
9
|
+
type CompiledDiagnosticSensitiveValues,
|
|
10
|
+
registerProcessDiagnosticValues,
|
|
11
|
+
suppressProcessDiagnosticValues,
|
|
12
|
+
withAppDiagnosticFallback,
|
|
13
|
+
} from "./diagnostic-redactor.js";
|
|
14
|
+
|
|
15
|
+
type EnvObserver = (name: string, value: string) => void;
|
|
16
|
+
export interface DiagnosticEnvScope {
|
|
17
|
+
observe: EnvObserver;
|
|
18
|
+
finished: boolean;
|
|
19
|
+
staticValues?: CompiledDiagnosticSensitiveValues;
|
|
20
|
+
}
|
|
21
|
+
const observers = new AsyncLocalStorage<DiagnosticEnvScope>();
|
|
22
|
+
|
|
23
|
+
/** Resolve and register atomically, before any consumer can use a live env credential. */
|
|
24
|
+
export function readDiagnosticEnv(
|
|
25
|
+
name: string,
|
|
26
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
const value = env[name];
|
|
29
|
+
if (value !== undefined) {
|
|
30
|
+
const scope = observers.getStore();
|
|
31
|
+
if (scope && !scope.finished) scope.observe(name, value);
|
|
32
|
+
else {
|
|
33
|
+
// ALS survives the response. Finished requests register into the app's
|
|
34
|
+
// inherited fallback lifetime, never a dead request's private inventory.
|
|
35
|
+
if (Buffer.byteLength(value) > 64 * 1024) suppressProcessDiagnosticValues();
|
|
36
|
+
else
|
|
37
|
+
registerProcessDiagnosticValues([
|
|
38
|
+
...resolvedStringVariants(value),
|
|
39
|
+
...urlCredentialComponents(value, {
|
|
40
|
+
includePathAndQuery: name === "APIFUSE__CDP_POOL__URL",
|
|
41
|
+
}),
|
|
42
|
+
...(name === "OTEL_EXPORTER_OTLP_HEADERS" || name === "OTEL_EXPORTER_OTLP_TRACES_HEADERS"
|
|
43
|
+
? otlpHeaderCredentialValues(value)
|
|
44
|
+
: []),
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function withDiagnosticEnv<T>(observer: EnvObserver | DiagnosticEnvScope, fn: () => T): T {
|
|
52
|
+
const scope = typeof observer === "function" ? { observe: observer, finished: false } : observer;
|
|
53
|
+
const run = () => observers.run(scope, fn);
|
|
54
|
+
return scope.staticValues ? withAppDiagnosticFallback(scope.staticValues, run) : run();
|
|
55
|
+
}
|