@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.
- package/AUTHORING.md +33 -16
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +7 -8
- package/bin/apifuse-pack-types.ts +22 -0
- package/bin/apifuse-perf.ts +45 -10
- package/bin/apifuse-record.ts +6 -8
- package/bin/apifuse-submit-check.ts +2 -2
- package/dist/config/loader.js +13 -12
- package/dist/define.d.ts +3 -2
- package/dist/define.js +1 -1
- package/dist/dev.js +3 -1
- package/dist/engine.d.ts +7 -6
- package/dist/engine.js +6 -6
- package/dist/error-resolution.js +21 -4
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +9 -0
- package/dist/handle-meta.d.ts +39 -0
- package/dist/handle-meta.js +29 -0
- package/dist/handle.d.ts +187 -0
- package/dist/handle.js +254 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/lint.d.ts +3 -0
- package/dist/lint.js +388 -62
- package/dist/provider.d.ts +4 -4
- package/dist/provider.js +3 -3
- 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/handle-wordlist.d.ts +43 -0
- package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +895 -0
- package/dist/runtime/instrumentation.js +3 -0
- package/dist/runtime/native-network.d.ts +10 -1
- package/dist/runtime/native-network.js +230 -29
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -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/state.d.ts +4 -1
- package/dist/runtime/state.js +32 -17
- 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/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +128 -0
- package/dist/server/serve-implementation.d.ts +4 -11
- package/dist/server/serve-implementation.js +305 -96
- package/dist/server/trace-output.d.ts +2 -1
- package/dist/server/trace-output.js +46 -14
- package/dist/testing/run.js +10 -7
- package/dist/trace-sanitization.d.ts +4 -3
- package/dist/trace-sanitization.js +45 -22
- package/dist/types.d.ts +4 -107
- package/package.json +2 -1
- package/src/config/loader.ts +13 -12
- package/src/define.ts +4 -3
- package/src/dev.ts +3 -1
- package/src/engine.ts +8 -8
- package/src/error-resolution.ts +21 -4
- package/src/errors.ts +13 -0
- package/src/handle-meta.ts +58 -0
- package/src/handle.ts +529 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/index.ts +28 -13
- package/src/lint.ts +456 -71
- package/src/provider.ts +10 -20
- 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/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
- package/src/runtime/handle.ts +1162 -0
- package/src/runtime/instrumentation.ts +3 -0
- package/src/runtime/native-network.ts +281 -33
- package/src/runtime/native-telemetry.ts +366 -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/state.ts +44 -16
- package/src/runtime/stt.ts +3 -2
- package/src/runtime/trace.ts +168 -26
- package/src/schema.ts +63 -2
- package/src/server/index.ts +17 -0
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/sensitive-values.ts +197 -0
- package/src/server/serve-implementation.ts +388 -128
- package/src/server/trace-output.ts +77 -17
- package/src/testing/run.ts +11 -7
- package/src/trace-sanitization.ts +70 -23
- package/src/types.ts +4 -137
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice-wordlist.d.ts +0 -9
- package/dist/runtime/choice.d.ts +0 -27
- package/dist/runtime/choice.js +0 -801
- package/src/choice-token.ts +0 -165
- package/src/runtime/choice.ts +0 -1197
package/src/runtime/state.ts
CHANGED
|
@@ -75,9 +75,12 @@ if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
if current_decoded and type(current_decoded.createdAt) == "string" then
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
envelope
|
|
78
|
+
-- Preserve the original createdAt by patching the encoded envelope in place.
|
|
79
|
+
-- The envelope puts its metadata before "value", so the first match is the
|
|
80
|
+
-- envelope's own key, never a provider value; re-encoding with cjson would
|
|
81
|
+
-- silently turn [] inside the value into {}.
|
|
82
|
+
local preserved = string.gsub(current_decoded.createdAt, "%%", "%%%%")
|
|
83
|
+
envelope = (string.gsub(envelope, '"createdAt":"[^"]*"', '"createdAt":"' .. preserved .. '"', 1))
|
|
81
84
|
end
|
|
82
85
|
redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
|
|
83
86
|
redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
|
|
@@ -267,7 +270,11 @@ function redisEnvelope(
|
|
|
267
270
|
expiresAt: string,
|
|
268
271
|
): RedisStateEnvelope {
|
|
269
272
|
const updatedAt = new Date().toISOString();
|
|
270
|
-
|
|
273
|
+
// Metadata precedes `value` on purpose: the compare-and-set Lua script patches
|
|
274
|
+
// `createdAt` in the encoded string (first occurrence) instead of decoding and
|
|
275
|
+
// re-encoding with cjson, which would turn empty arrays inside `value` into
|
|
276
|
+
// objects. Keep the key order stable.
|
|
277
|
+
return { version, expiresAt, createdAt, updatedAt, value };
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
class RedisProviderStateNamespace implements ProviderStateNamespace {
|
|
@@ -604,7 +611,10 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
604
611
|
// biome-ignore lint/suspicious/noExplicitAny: in-memory state stores heterogeneous generic values by key.
|
|
605
612
|
readonly values = new Map<string, StateValue<any>>();
|
|
606
613
|
|
|
607
|
-
constructor(
|
|
614
|
+
constructor(
|
|
615
|
+
private readonly options: StateNamespaceOptions,
|
|
616
|
+
private readonly now: () => number = Date.now,
|
|
617
|
+
) {}
|
|
608
618
|
|
|
609
619
|
private enforceValueSize(value: unknown): void {
|
|
610
620
|
const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
@@ -631,7 +641,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
631
641
|
}
|
|
632
642
|
}
|
|
633
643
|
|
|
634
|
-
private pruneExpired(nowMs =
|
|
644
|
+
private pruneExpired(nowMs = this.now()): void {
|
|
635
645
|
for (const [key, row] of this.values.entries()) {
|
|
636
646
|
if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
|
|
637
647
|
this.values.delete(key);
|
|
@@ -655,9 +665,12 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
655
665
|
async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
|
|
656
666
|
this.pruneExpired();
|
|
657
667
|
this.enforceWritePolicy(key, value, options?.ttl);
|
|
658
|
-
const now = new Date().toISOString();
|
|
668
|
+
const now = new Date(this.now()).toISOString();
|
|
659
669
|
const current = this.values.get(key);
|
|
660
|
-
const expiresAt = resolveMemoryStateExpiresAt(
|
|
670
|
+
const expiresAt = resolveMemoryStateExpiresAt(
|
|
671
|
+
options?.ttl ?? this.options.defaultTtl,
|
|
672
|
+
this.now(),
|
|
673
|
+
);
|
|
661
674
|
const row = {
|
|
662
675
|
key,
|
|
663
676
|
value,
|
|
@@ -692,13 +705,14 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
692
705
|
return { ok: false, current: current ?? null };
|
|
693
706
|
}
|
|
694
707
|
this.enforceWritePolicy(key, value, options?.ttl);
|
|
695
|
-
const now = new Date().toISOString();
|
|
708
|
+
const now = new Date(this.now()).toISOString();
|
|
696
709
|
const stored = {
|
|
697
710
|
key,
|
|
698
711
|
value,
|
|
699
712
|
version: expectedVersion + 1,
|
|
700
713
|
expiresAt: resolveMemoryStateExpiresAt(
|
|
701
714
|
options?.ttl ?? this.options.defaultTtl,
|
|
715
|
+
this.now(),
|
|
702
716
|
),
|
|
703
717
|
createdAt: current?.createdAt ?? now,
|
|
704
718
|
updatedAt: now,
|
|
@@ -725,11 +739,16 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
|
|
|
725
739
|
|
|
726
740
|
type MemoryProviderStateBackend = {
|
|
727
741
|
readonly namespaces: Map<string, MemoryProviderStateNamespace>;
|
|
742
|
+
/** Clock used for expiry; tests inject a fake clock so ttl behaviour matches production. */
|
|
743
|
+
readonly now: () => number;
|
|
728
744
|
};
|
|
729
745
|
|
|
730
746
|
class MemoryProviderRuntimeState implements ProviderRuntimeState {
|
|
731
747
|
constructor(
|
|
732
|
-
private readonly backend: MemoryProviderStateBackend = {
|
|
748
|
+
private readonly backend: MemoryProviderStateBackend = {
|
|
749
|
+
namespaces: new Map(),
|
|
750
|
+
now: Date.now,
|
|
751
|
+
},
|
|
733
752
|
private readonly scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR,
|
|
734
753
|
) {}
|
|
735
754
|
|
|
@@ -746,15 +765,18 @@ class MemoryProviderRuntimeState implements ProviderRuntimeState {
|
|
|
746
765
|
const namespaceIdentity = `${scopeDiscriminator}\0${name}`;
|
|
747
766
|
const existing = this.backend.namespaces.get(namespaceIdentity);
|
|
748
767
|
if (existing) return existing;
|
|
749
|
-
const created = new MemoryProviderStateNamespace(options);
|
|
768
|
+
const created = new MemoryProviderStateNamespace(options, this.backend.now);
|
|
750
769
|
this.backend.namespaces.set(namespaceIdentity, created);
|
|
751
770
|
return created;
|
|
752
771
|
}
|
|
753
772
|
}
|
|
754
773
|
|
|
755
|
-
function resolveMemoryStateExpiresAt(
|
|
774
|
+
function resolveMemoryStateExpiresAt(
|
|
775
|
+
ttl: StateWriteOptions["ttl"],
|
|
776
|
+
nowMs = Date.now(),
|
|
777
|
+
): string {
|
|
756
778
|
const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
|
|
757
|
-
if (!match) return new Date(
|
|
779
|
+
if (!match) return new Date(nowMs + 3_600_000).toISOString();
|
|
758
780
|
const amount = Number(match[1]);
|
|
759
781
|
const unit = match[2];
|
|
760
782
|
const multiplier =
|
|
@@ -767,7 +789,7 @@ function resolveMemoryStateExpiresAt(ttl: StateWriteOptions["ttl"]): string {
|
|
|
767
789
|
: unit === "h"
|
|
768
790
|
? 3_600_000
|
|
769
791
|
: 86_400_000;
|
|
770
|
-
return new Date(
|
|
792
|
+
return new Date(nowMs + amount * multiplier).toISOString();
|
|
771
793
|
}
|
|
772
794
|
|
|
773
795
|
export function createRedisProviderRuntimeState(
|
|
@@ -792,8 +814,14 @@ export function createProviderRuntimeStateFromEnv(
|
|
|
792
814
|
return createUnsupportedProviderRuntimeState();
|
|
793
815
|
}
|
|
794
816
|
|
|
795
|
-
export function createMemoryProviderRuntimeState(
|
|
796
|
-
|
|
817
|
+
export function createMemoryProviderRuntimeState(options?: {
|
|
818
|
+
/** Clock for expiry and timestamps; defaults to `Date.now`. */
|
|
819
|
+
readonly now?: () => number;
|
|
820
|
+
}): ProviderRuntimeState {
|
|
821
|
+
return new MemoryProviderRuntimeState({
|
|
822
|
+
namespaces: new Map(),
|
|
823
|
+
now: options?.now ?? Date.now,
|
|
824
|
+
});
|
|
797
825
|
}
|
|
798
826
|
|
|
799
827
|
export function createUnsupportedProviderRuntimeState(): ProviderRuntimeState {
|
package/src/runtime/stt.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./diagnostic-env.js";
|
|
1
2
|
import { ProviderError, TransportError, ValidationError } from "../errors.js";
|
|
2
3
|
import type {
|
|
3
4
|
Bcp47Locale,
|
|
@@ -69,7 +70,7 @@ export function createUnsupportedSttClient(reason?: string): SttContext {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
function normalizedEnvValue(env: EnvLike, key: string): string | undefined {
|
|
72
|
-
const value = env
|
|
73
|
+
const value = readDiagnosticEnv(key, env)?.trim();
|
|
73
74
|
return value ? value : undefined;
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -99,7 +100,7 @@ export function createSttClientFromEnv(
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
const accountId = env.APIFUSE__CLOUDFLARE__ACCOUNT_ID?.trim();
|
|
102
|
-
const apiToken = env
|
|
103
|
+
const apiToken = readDiagnosticEnv(APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, env)?.trim();
|
|
103
104
|
if (!accountId || !apiToken) {
|
|
104
105
|
return createUnsupportedSttClient(
|
|
105
106
|
`STT backend ${backend} requires ${CLOUDFLARE_ACCOUNT_ID_ENV} and ${APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV}`,
|
package/src/runtime/trace.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
|
|
2
|
+
import { sanitizeSpanNameForOutput } from "../trace-sanitization.js";
|
|
3
3
|
import type {
|
|
4
4
|
TraceContext as BaseTraceContext,
|
|
5
5
|
TraceAttributeValue,
|
|
6
6
|
TraceConfig,
|
|
7
7
|
TraceSpan,
|
|
8
8
|
} from "../types.js";
|
|
9
|
+
import {
|
|
10
|
+
type DiagnosticRedactor,
|
|
11
|
+
diagnosticStructuredRedactor,
|
|
12
|
+
redactDiagnosticAttributeKey,
|
|
13
|
+
copyDiagnosticAttributeTypes,
|
|
14
|
+
redactedKeyAllocator,
|
|
15
|
+
isRedactedKey,
|
|
16
|
+
redactCriticalDiagnosticText,
|
|
17
|
+
diagnosticAttributeRedactor,
|
|
18
|
+
REDACTION_FAILED,
|
|
19
|
+
redactDiagnosticText,
|
|
20
|
+
} from "./diagnostic-redactor.js";
|
|
9
21
|
import { exportSpansOTLP, type OTLPExportOptions } from "./otlp.js";
|
|
22
|
+
import { REDACTED_QUERY_VALUE } from "./request-options.js";
|
|
10
23
|
|
|
11
24
|
export type SpanAttributeValue = TraceAttributeValue;
|
|
12
25
|
|
|
@@ -19,6 +32,8 @@ export interface TraceContext extends BaseTraceContext {
|
|
|
19
32
|
export interface CreateTraceContextOptions {
|
|
20
33
|
maxSpans?: number;
|
|
21
34
|
onSpan?: (span: Span) => void;
|
|
35
|
+
/** Redacts diagnostic attribute values and errors before recording or calling onSpan. */
|
|
36
|
+
redact?: DiagnosticRedactor;
|
|
22
37
|
exportOptions?: OTLPExportOptions;
|
|
23
38
|
resourceAttributes?: Record<string, string>;
|
|
24
39
|
/** W3C-compatible 32-character lowercase hexadecimal trace id used for export. */
|
|
@@ -42,7 +57,7 @@ type PendingSpan = {
|
|
|
42
57
|
startedAt: number;
|
|
43
58
|
parentId?: string;
|
|
44
59
|
sequence: number;
|
|
45
|
-
attributes: Record<string, SpanAttributeValue>;
|
|
60
|
+
attributes: Record<string, SpanAttributeValue | bigint>;
|
|
46
61
|
};
|
|
47
62
|
|
|
48
63
|
type CompletedSpanEntry = {
|
|
@@ -52,14 +67,22 @@ type CompletedSpanEntry = {
|
|
|
52
67
|
exported: boolean;
|
|
53
68
|
};
|
|
54
69
|
|
|
70
|
+
const TRACE_RESOURCE_ATTRIBUTE_SANITIZER = Symbol.for(
|
|
71
|
+
"@apifuse/provider-sdk/runtime/trace-resource-attribute-sanitizer",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
type ResourceAttributesWithSanitizer = Record<string, string> & {
|
|
75
|
+
[TRACE_RESOURCE_ATTRIBUTE_SANITIZER]?: (
|
|
76
|
+
attributes: Record<string, string>,
|
|
77
|
+
) => Record<string, string>;
|
|
78
|
+
};
|
|
79
|
+
|
|
55
80
|
export interface TraceRecorder {
|
|
56
81
|
runSpan<T>(name: string, fn: () => Promise<T> | T, options?: SpanHookOptions<T>): Promise<T>;
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
export const TRACE_RECORDER = Symbol.for("@apifuse/provider-sdk/runtime/trace-recorder");
|
|
60
|
-
const TRACE_EXPORT_METADATA = Symbol.for(
|
|
61
|
-
"@apifuse/provider-sdk/runtime/trace-export-metadata",
|
|
62
|
-
);
|
|
85
|
+
const TRACE_EXPORT_METADATA = Symbol.for("@apifuse/provider-sdk/runtime/trace-export-metadata");
|
|
63
86
|
|
|
64
87
|
type TraceExportMetadata = {
|
|
65
88
|
update(input: { traceId?: string; resourceAttributes?: Record<string, string> }): void;
|
|
@@ -109,30 +132,104 @@ export function resolveTraceContextOptions(config?: TraceConfig): CreateTraceCon
|
|
|
109
132
|
};
|
|
110
133
|
}
|
|
111
134
|
|
|
112
|
-
function normalizeAttributeValue(
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
function normalizeAttributeValue(
|
|
136
|
+
value: unknown,
|
|
137
|
+
redact?: DiagnosticRedactor,
|
|
138
|
+
retainBigints = false,
|
|
139
|
+
): SpanAttributeValue | bigint | undefined {
|
|
140
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
141
|
+
// Typed columns are contracts: unlike string tokens, number/boolean/bigint
|
|
142
|
+
// values only consult the registry at >=8 characters in string form.
|
|
143
|
+
const text = String(value);
|
|
144
|
+
if (text.length < 8) return value;
|
|
145
|
+
const redacted = redactDiagnosticText(text, diagnosticStructuredRedactor(redact));
|
|
146
|
+
return redacted === text ? value : redacted;
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === "bigint") {
|
|
149
|
+
// Bigints are not valid OTLP attribute primitives, so retain their prior
|
|
150
|
+
// string normalization while still checking their diagnostic form.
|
|
151
|
+
const text = String(value);
|
|
152
|
+
if (text.length < 8) return retainBigints ? value : text;
|
|
153
|
+
const redacted = redactDiagnosticText(text, diagnosticStructuredRedactor(redact));
|
|
154
|
+
return retainBigints && redacted === text ? value : redacted;
|
|
115
155
|
}
|
|
116
156
|
|
|
117
157
|
if (value === null || value === undefined) {
|
|
118
158
|
return undefined;
|
|
119
159
|
}
|
|
120
160
|
|
|
121
|
-
return String(value);
|
|
161
|
+
return redactDiagnosticText(String(value), redact);
|
|
122
162
|
}
|
|
123
163
|
|
|
164
|
+
function normalizeAttributes(
|
|
165
|
+
attributes: Record<string, unknown> | undefined,
|
|
166
|
+
redact: DiagnosticRedactor | undefined,
|
|
167
|
+
retainBigints: true,
|
|
168
|
+
): Record<string, SpanAttributeValue | bigint>;
|
|
169
|
+
function normalizeAttributes(
|
|
170
|
+
attributes?: Record<string, unknown>,
|
|
171
|
+
redact?: DiagnosticRedactor,
|
|
172
|
+
): Record<string, SpanAttributeValue>;
|
|
124
173
|
function normalizeAttributes(
|
|
125
174
|
attributes?: Record<string, unknown>,
|
|
126
|
-
|
|
175
|
+
redact?: DiagnosticRedactor,
|
|
176
|
+
retainBigints = false,
|
|
177
|
+
): Record<string, SpanAttributeValue | bigint> {
|
|
127
178
|
if (!attributes) {
|
|
128
179
|
return {};
|
|
129
180
|
}
|
|
130
181
|
|
|
182
|
+
const nextKey = redactedKeyAllocator(Object.keys(attributes));
|
|
131
183
|
const normalizedEntries = Object.entries(attributes)
|
|
132
|
-
.map(([key, value]) =>
|
|
133
|
-
|
|
184
|
+
.map(([key, value]) => {
|
|
185
|
+
const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
|
|
186
|
+
const keyChanged = redactedKey !== key;
|
|
187
|
+
const normalizedKey = keyChanged ? nextKey() : key;
|
|
188
|
+
// Once a key changes, its original sensitivity classification is no
|
|
189
|
+
// longer available to later output sanitizers. Suppress the value here.
|
|
190
|
+
const normalizedValue = keyChanged
|
|
191
|
+
? redactedKey === REDACTION_FAILED
|
|
192
|
+
? REDACTION_FAILED
|
|
193
|
+
: REDACTED_QUERY_VALUE
|
|
194
|
+
: normalizeAttributeValue(value, diagnosticAttributeRedactor(key, redact), retainBigints);
|
|
195
|
+
return [normalizedKey, normalizedValue] as const;
|
|
196
|
+
})
|
|
197
|
+
.filter(
|
|
198
|
+
(entry): entry is readonly [string, SpanAttributeValue | bigint] => entry[1] !== undefined,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return copyDiagnosticAttributeTypes(attributes, Object.fromEntries(normalizedEntries));
|
|
202
|
+
}
|
|
134
203
|
|
|
135
|
-
|
|
204
|
+
function prepareResourceAttributesForExport(
|
|
205
|
+
attributes: Record<string, string> | undefined,
|
|
206
|
+
redact?: DiagnosticRedactor,
|
|
207
|
+
sanitize?: (attributes: Record<string, string>) => Record<string, string>,
|
|
208
|
+
): Record<string, string> | undefined {
|
|
209
|
+
if (!attributes) return undefined;
|
|
210
|
+
if (sanitize) {
|
|
211
|
+
const sanitized = sanitize({ ...attributes });
|
|
212
|
+
if (!sanitized || typeof sanitized !== "object") {
|
|
213
|
+
throw new Error("resource attribute sanitizer returned no attributes");
|
|
214
|
+
}
|
|
215
|
+
return sanitized;
|
|
216
|
+
}
|
|
217
|
+
const nextKey = redactedKeyAllocator(Object.keys(attributes));
|
|
218
|
+
return Object.fromEntries(
|
|
219
|
+
Object.entries(attributes).map(([key, value]) => {
|
|
220
|
+
const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
|
|
221
|
+
const keyChanged = redactedKey !== key;
|
|
222
|
+
const sanitizedKey = keyChanged ? nextKey() : sanitizeSpanNameForOutput(key);
|
|
223
|
+
return [
|
|
224
|
+
sanitizedKey,
|
|
225
|
+
keyChanged
|
|
226
|
+
? redactedKey === REDACTION_FAILED
|
|
227
|
+
? REDACTION_FAILED
|
|
228
|
+
: REDACTED_QUERY_VALUE
|
|
229
|
+
: sanitizeSpanNameForOutput(value, diagnosticAttributeRedactor(key, redact)),
|
|
230
|
+
];
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
136
233
|
}
|
|
137
234
|
|
|
138
235
|
function insertCompletedSpan(
|
|
@@ -158,7 +255,10 @@ function prepareSpanForExport(
|
|
|
158
255
|
span: Span,
|
|
159
256
|
sanitize: CreateTraceContextOptions["sanitizeSpanForExport"],
|
|
160
257
|
): Span {
|
|
161
|
-
const copy: Span = {
|
|
258
|
+
const copy: Span = {
|
|
259
|
+
...span,
|
|
260
|
+
attributes: copyDiagnosticAttributeTypes(span.attributes, { ...span.attributes }),
|
|
261
|
+
};
|
|
162
262
|
if (!sanitize) return copy;
|
|
163
263
|
const sanitized = sanitize(copy);
|
|
164
264
|
if (!sanitized || typeof sanitized !== "object") {
|
|
@@ -171,6 +271,25 @@ export function getTraceRecorder(trace: BaseTraceContext): TraceRecorder | null
|
|
|
171
271
|
return (trace as Partial<InternalTraceContext>)[TRACE_RECORDER] ?? null;
|
|
172
272
|
}
|
|
173
273
|
|
|
274
|
+
const droppedOTLPSpans = new Map<string, number>();
|
|
275
|
+
|
|
276
|
+
/** Internal, read-only process counters; no reset can re-arm a warning. */
|
|
277
|
+
export function getDroppedOTLPSpanCount(reason: string): number {
|
|
278
|
+
return droppedOTLPSpans.get(reason) ?? 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function warnDroppedOTLPSpans(reason: string, count: number): void {
|
|
282
|
+
const previous = getDroppedOTLPSpanCount(reason);
|
|
283
|
+
const total = previous + count;
|
|
284
|
+
droppedOTLPSpans.set(reason, total);
|
|
285
|
+
if (previous !== 0) return;
|
|
286
|
+
try {
|
|
287
|
+
console.warn(`[apifuse] OTLP export skipped; reason=${reason}; dropped_spans=${total}`);
|
|
288
|
+
} catch {
|
|
289
|
+
// Warning sinks are best-effort. The counter/latch survives a failed sink.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
174
293
|
export function createTraceContext(options: CreateTraceContextOptions = {}): TraceContext {
|
|
175
294
|
if (options.traceId !== undefined) assertValidTraceId(options.traceId);
|
|
176
295
|
const maxSpans = options.maxSpans ?? 1000;
|
|
@@ -180,9 +299,14 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
|
|
|
180
299
|
// Export configuration (which can carry collector credentials) stays in this closure;
|
|
181
300
|
// the context object handed to provider code never exposes it.
|
|
182
301
|
const exportOptions = options.exportOptions;
|
|
183
|
-
const
|
|
184
|
-
|
|
302
|
+
const resourceAttributeOptions = options.resourceAttributes as
|
|
303
|
+
| ResourceAttributesWithSanitizer
|
|
304
|
+
| undefined;
|
|
305
|
+
const exportResourceAttributes = resourceAttributeOptions
|
|
306
|
+
? { ...resourceAttributeOptions }
|
|
185
307
|
: undefined;
|
|
308
|
+
const exportResourceAttributeSanitizer =
|
|
309
|
+
resourceAttributeOptions?.[TRACE_RESOURCE_ATTRIBUTE_SANITIZER];
|
|
186
310
|
// One trace id per context so every export batch of this request shares it and
|
|
187
311
|
// two processes can never mint the same id.
|
|
188
312
|
let exportTraceId = options.traceId ?? crypto.randomUUID().replace(/-/g, "");
|
|
@@ -203,15 +327,30 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
|
|
|
203
327
|
if (pending.length === 0) return;
|
|
204
328
|
// Sanitization runs off the request path; a faulty sanitizer drops the batch, never the request.
|
|
205
329
|
let spans: Span[];
|
|
330
|
+
let resourceAttributes: Record<string, string> | undefined;
|
|
206
331
|
try {
|
|
207
332
|
spans = pending.map((entry) =>
|
|
208
333
|
prepareSpanForExport(entry.span, options.sanitizeSpanForExport),
|
|
209
334
|
);
|
|
335
|
+
resourceAttributes = prepareResourceAttributesForExport(
|
|
336
|
+
exportResourceAttributes,
|
|
337
|
+
options.redact,
|
|
338
|
+
exportResourceAttributeSanitizer,
|
|
339
|
+
);
|
|
210
340
|
} catch {
|
|
211
|
-
|
|
341
|
+
try {
|
|
342
|
+
console.warn("[apifuse] OTLP export skipped; span sanitization failed.");
|
|
343
|
+
} catch {
|
|
344
|
+
// A broken diagnostic sink must not escape this detached callback.
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// A matching trace ID cannot be replaced with non-hex text on the wire. Emit less.
|
|
349
|
+
if (redactCriticalDiagnosticText(exportTraceId, options.redact) !== exportTraceId) {
|
|
350
|
+
warnDroppedOTLPSpans("unverifiable_trace_id", pending.length);
|
|
212
351
|
return;
|
|
213
352
|
}
|
|
214
|
-
void exportSpansOTLP(spans, exportOptions,
|
|
353
|
+
void exportSpansOTLP(spans, exportOptions, resourceAttributes, exportTraceId);
|
|
215
354
|
});
|
|
216
355
|
};
|
|
217
356
|
|
|
@@ -219,11 +358,11 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
|
|
|
219
358
|
async runSpan(name, fn, spanOptions = {}) {
|
|
220
359
|
const pendingSpan: PendingSpan = {
|
|
221
360
|
id: crypto.randomUUID(),
|
|
222
|
-
name,
|
|
361
|
+
name: redactCriticalDiagnosticText(name, options.redact),
|
|
223
362
|
startedAt: Date.now(),
|
|
224
363
|
parentId: activeSpanStorage.getStore()?.id,
|
|
225
364
|
sequence: sequence++,
|
|
226
|
-
attributes: normalizeAttributes(spanOptions.attributes),
|
|
365
|
+
attributes: normalizeAttributes(spanOptions.attributes, options.redact, true),
|
|
227
366
|
};
|
|
228
367
|
|
|
229
368
|
const finalize = (
|
|
@@ -235,7 +374,7 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
|
|
|
235
374
|
const duration = endedAt - pendingSpan.startedAt;
|
|
236
375
|
const attributes = {
|
|
237
376
|
...pendingSpan.attributes,
|
|
238
|
-
...
|
|
377
|
+
...extraAttributes,
|
|
239
378
|
};
|
|
240
379
|
|
|
241
380
|
if (attributes.duration_ms === undefined) {
|
|
@@ -243,15 +382,18 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
|
|
|
243
382
|
}
|
|
244
383
|
|
|
245
384
|
const span: Span = {
|
|
246
|
-
id: pendingSpan.id,
|
|
247
|
-
|
|
385
|
+
id: redactCriticalDiagnosticText(pendingSpan.id, options.redact),
|
|
386
|
+
// Recheck at recording: harvest and live env reads may follow span start.
|
|
387
|
+
name: redactCriticalDiagnosticText(pendingSpan.name, options.redact),
|
|
248
388
|
startedAt: pendingSpan.startedAt,
|
|
249
389
|
endedAt,
|
|
250
390
|
duration_ms: duration,
|
|
251
391
|
status,
|
|
252
|
-
attributes,
|
|
253
|
-
...(error ? { error } : {}),
|
|
254
|
-
...(pendingSpan.parentId
|
|
392
|
+
attributes: normalizeAttributes(attributes, options.redact),
|
|
393
|
+
...(error ? { error: redactDiagnosticText(error, options.redact) } : {}),
|
|
394
|
+
...(pendingSpan.parentId
|
|
395
|
+
? { parentId: redactCriticalDiagnosticText(pendingSpan.parentId, options.redact) }
|
|
396
|
+
: {}),
|
|
255
397
|
};
|
|
256
398
|
|
|
257
399
|
insertCompletedSpan(
|
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
|
-
|
|
214
|
-
...(
|
|
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");
|
package/src/server/index.ts
CHANGED
|
@@ -128,3 +128,20 @@ export {
|
|
|
128
128
|
OperationRequestSchema,
|
|
129
129
|
OperationSuccessResponseSchema,
|
|
130
130
|
} from "./types.js";
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
NativeTelemetryCollector,
|
|
134
|
+
type NativeAttemptSample,
|
|
135
|
+
type NativeConnectTelemetryEvent,
|
|
136
|
+
type NativeLifecycleTelemetryEvent,
|
|
137
|
+
type NativeTelemetryDiagnostics,
|
|
138
|
+
type NativeTelemetryErrorCode,
|
|
139
|
+
type NativeTelemetryHeaderPayload,
|
|
140
|
+
type NativeTelemetryKind,
|
|
141
|
+
type NativeTelemetryLifecycleKind,
|
|
142
|
+
type NativeTelemetryLogPayload,
|
|
143
|
+
type NativeTelemetryOutcome,
|
|
144
|
+
type NativeTelemetrySink,
|
|
145
|
+
type NativeTelemetryVendorSkipReason,
|
|
146
|
+
type NativeVendorSkipTelemetryEvent,
|
|
147
|
+
} from "../runtime/native-telemetry.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ProviderDefinition } from "../types.js";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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(
|