@genn-inc/cluebase-frontend-sdk 0.0.6 → 0.0.9
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/dist/_generated/correlation-attribute-keys.d.ts +3 -0
- package/dist/_generated/correlation-attribute-keys.js +35 -0
- package/dist/_generated/diagnostic-codes.d.ts +4 -0
- package/dist/_generated/diagnostic-codes.js +48 -0
- package/dist/_generated/drop-reasons.d.ts +2 -0
- package/dist/_generated/drop-reasons.js +25 -0
- package/dist/_generated/ingest-routes.d.ts +5 -0
- package/dist/_generated/ingest-routes.js +12 -0
- package/dist/context/trace.js +2 -4
- package/dist/exporter/batch.d.ts +23 -26
- package/dist/exporter/batch.js +254 -289
- package/dist/exporter/browser-outbox-records.d.ts +7 -24
- package/dist/exporter/browser-outbox-records.js +6 -94
- package/dist/exporter/browser-outbox.d.ts +23 -28
- package/dist/exporter/browser-outbox.js +99 -211
- package/dist/exporter/client.d.ts +24 -34
- package/dist/exporter/client.js +107 -318
- package/dist/exporter/delivery-pipeline.d.ts +72 -0
- package/dist/exporter/delivery-pipeline.js +199 -0
- package/dist/exporter/delivery-response-contract.d.ts +5 -81
- package/dist/exporter/delivery-response-contract.js +4 -166
- package/dist/exporter/delivery-response.d.ts +8 -4
- package/dist/exporter/delivery-response.js +62 -133
- package/dist/exporter/diagnostics.d.ts +2 -1
- package/dist/exporter/diagnostics.js +1 -25
- package/dist/exporter/event-id.d.ts +1 -0
- package/dist/exporter/event-id.js +3 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +1 -1
- package/dist/exporter/observation-source-event-exporter.js +2 -2
- package/dist/exporter/parameter-encryption.d.ts +10 -0
- package/dist/exporter/parameter-encryption.js +219 -0
- package/dist/exporter/parameter-snapshot.js +25 -3
- package/dist/exporter/sdk-core-delivery.d.ts +226 -0
- package/dist/exporter/sdk-core-delivery.js +97 -0
- package/dist/exporter/sdk-core-wasm.d.ts +24 -1
- package/dist/exporter/sdk-core-wasm.js +31 -3
- package/dist/exporter/source-event.d.ts +2 -0
- package/dist/exporter/source-event.js +17 -0
- package/dist/exporter/span-to-source-signal.d.ts +1 -1
- package/dist/exporter/span-to-source-signal.js +20 -140
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -3
- package/dist/instrumentation/network.d.ts +1 -1
- package/dist/instrumentation/network.js +17 -0
- package/dist/provider/_version.d.ts +1 -1
- package/dist/provider/_version.js +1 -1
- package/dist/provider/config.js +16 -3
- package/dist/provider/constants.js +0 -1
- package/dist/provider/contracts.d.ts +2 -0
- package/dist/provider/contracts.js +2 -0
- package/dist/provider/lifecycle-event-dedup.d.ts +7 -0
- package/dist/provider/lifecycle-event-dedup.js +31 -0
- package/dist/provider/sdk-encryption-store.d.ts +7 -0
- package/dist/provider/sdk-encryption-store.js +15 -0
- package/dist/provider/sdk-event-pipeline.d.ts +1 -3
- package/dist/provider/sdk-event-pipeline.js +0 -15
- package/dist/provider/sdk-otel-pipeline.d.ts +2 -1
- package/dist/provider/sdk-otel-pipeline.js +2 -11
- package/dist/provider/sdk-runtime-surfaces.d.ts +3 -2
- package/dist/provider/sdk-runtime-surfaces.js +7 -6
- package/dist/provider/sdk.js +88 -54
- package/dist/provider/types.d.ts +14 -19
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +18 -9
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +170 -29
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +10 -9
- package/package.json +1 -1
- package/dist/provider/usage-governor.d.ts +0 -7
- package/dist/provider/usage-governor.js +0 -114
|
@@ -1,143 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
stale_browser_token: "retry_same_batch",
|
|
13
|
-
origin_not_allowed: "stop_and_report",
|
|
14
|
-
authorization_denied: "stop_and_report",
|
|
15
|
-
idempotency_conflict: "mark_failed",
|
|
16
|
-
batch_too_large: "split_batch",
|
|
17
|
-
all_events_dropped: "drop_event",
|
|
18
|
-
usage_governor_denied: "retry_same_batch",
|
|
19
|
-
all_events_rejected_by_ingest_controls: "retry_same_batch",
|
|
20
|
-
route_throttled: "retry_same_batch",
|
|
21
|
-
raw_ingest_produce_failed: "retry_same_batch",
|
|
22
|
-
raw_ingest_ack_unknown: "retry_same_batch",
|
|
23
|
-
raw_ingest_receipt_metadata_failed: "retry_same_batch",
|
|
24
|
-
raw_ingest_receipt_unprocessable: "stop_and_report",
|
|
25
|
-
ingest_config_changed_retryable: "retry_same_batch",
|
|
26
|
-
usage_meter_unavailable: "retry_same_batch",
|
|
1
|
+
import { evaluateResponse, } from "./sdk-core-delivery.js";
|
|
2
|
+
const RETRY_STRATEGY_BY_OUTCOME = {
|
|
3
|
+
accepted_complete: null,
|
|
4
|
+
accepted_filtered: null,
|
|
5
|
+
accepted_partial: null,
|
|
6
|
+
retry_same_batch: "retry_same_batch",
|
|
7
|
+
split_batch: "split_batch",
|
|
8
|
+
terminal_stop: "stop_and_report",
|
|
9
|
+
terminal_failed: "mark_failed",
|
|
10
|
+
terminal_drop: "drop_event",
|
|
11
|
+
invalid_response: "retry_same_batch",
|
|
27
12
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (partial) {
|
|
42
|
-
diagnosticCodes.push(parsed.data.dropped_events.length > 0
|
|
43
|
-
? "intentional_drop"
|
|
44
|
-
: "duplicate_drop_details_unavailable");
|
|
45
|
-
}
|
|
46
|
-
if (input.status !== 202) {
|
|
47
|
-
diagnosticCodes.push("non_canonical_success_status");
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
ok: true,
|
|
51
|
-
retryStrategy: null,
|
|
52
|
-
privacyPolicy: parsed.data.privacy_policy,
|
|
53
|
-
governor: parseGovernorFeedbackEnvelope(parsed.data),
|
|
54
|
-
observationIngestBatchId: parsed.data.observation_ingest_batch_id,
|
|
55
|
-
droppedEventCount: parsed.data.dropped_event_count,
|
|
56
|
-
acceptedEventCount: parsed.data.accepted_event_count,
|
|
57
|
-
acceptedPartial: partial,
|
|
58
|
-
reason: partial ? "accepted_partial" : null,
|
|
59
|
-
diagnosticCodes,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
if (BROWSER_REJECTION_STRATEGIES[parsed.data.reason] !==
|
|
63
|
-
parsed.data.retry_strategy) {
|
|
64
|
-
return invalidResponse();
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
ok: false,
|
|
68
|
-
retryStrategy: parsed.data.retry_strategy,
|
|
69
|
-
governor: null,
|
|
70
|
-
droppedEventCount: parsed.data.dropped_event_count ?? 0,
|
|
71
|
-
acceptedEventCount: parsed.data.accepted_event_count ?? 0,
|
|
72
|
-
acceptedPartial: false,
|
|
73
|
-
reason: parsed.data.reason,
|
|
74
|
-
diagnosticCodes: [],
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
return invalidResponse();
|
|
13
|
+
const ACCEPTED_OUTCOMES = new Set([
|
|
14
|
+
"accepted_complete",
|
|
15
|
+
"accepted_filtered",
|
|
16
|
+
"accepted_partial",
|
|
17
|
+
]);
|
|
18
|
+
export const evaluateBrowserDeliveryResponse = (input) => translate({ kind: "response", status: input.status, body: input.bodyText }, input.localSubmittedCount);
|
|
19
|
+
export const evaluateBrowserTransportOutcome = (outcome) => translate(outcome, 1);
|
|
20
|
+
export const classifyIngestHttpStatus = (status) => translate({ kind: "response", status, body: "" }, 1).retryStrategy ??
|
|
21
|
+
"retry_same_batch";
|
|
22
|
+
const translate = (outcome, localSubmittedCount) => {
|
|
23
|
+
const evaluation = evaluateResponse(outcome, localSubmittedCount);
|
|
24
|
+
if (evaluation.kind !== "decision") {
|
|
25
|
+
return consentRevoked();
|
|
78
26
|
}
|
|
79
|
-
return
|
|
27
|
+
return project(evaluation.decision);
|
|
80
28
|
};
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
29
|
+
const project = (decision) => {
|
|
30
|
+
const accepted = ACCEPTED_OUTCOMES.has(decision.outcome);
|
|
31
|
+
const batchId = decision.result.observation_ingest_batch_id;
|
|
32
|
+
return {
|
|
33
|
+
ok: accepted,
|
|
34
|
+
retryStrategy: accepted
|
|
35
|
+
? null
|
|
36
|
+
: (RETRY_STRATEGY_BY_OUTCOME[decision.outcome] ?? "retry_same_batch"),
|
|
37
|
+
...(decision.privacy_policy === undefined
|
|
38
|
+
? {}
|
|
39
|
+
: { privacyPolicy: decision.privacy_policy }),
|
|
40
|
+
...(decision.sdk_encryption === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: { sdkEncryption: decision.sdk_encryption }),
|
|
43
|
+
directiveUnreadable: decision.directive_unreadable === true,
|
|
44
|
+
...(batchId === undefined ? {} : { observationIngestBatchId: batchId }),
|
|
45
|
+
droppedEventCount: decision.result.dropped_event_count ?? 0,
|
|
46
|
+
acceptedEventCount: decision.result.accepted_event_count ?? 0,
|
|
47
|
+
acceptedPartial: decision.outcome === "accepted_partial",
|
|
48
|
+
acceptedFiltered: decision.outcome === "accepted_filtered",
|
|
49
|
+
reason: reasonOf(decision),
|
|
50
|
+
diagnosticCodes: decision.diagnostic_codes,
|
|
51
|
+
};
|
|
93
52
|
};
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
droppedEventCount: 0,
|
|
103
|
-
acceptedEventCount: 0,
|
|
104
|
-
acceptedPartial: false,
|
|
105
|
-
reason: "invalid_response",
|
|
106
|
-
diagnosticCodes: ["invalid_response"],
|
|
107
|
-
});
|
|
108
|
-
export const classifyIngestHttpStatus = (status) => {
|
|
109
|
-
if (status >= 200 && status < 300)
|
|
110
|
-
return "retry_same_batch";
|
|
111
|
-
if (status === 409)
|
|
112
|
-
return "mark_failed";
|
|
113
|
-
if (status === 413)
|
|
114
|
-
return "split_batch";
|
|
115
|
-
if ([400, 401, 403, 404, 405, 406, 410, 415].includes(status)) {
|
|
116
|
-
return "stop_and_report";
|
|
117
|
-
}
|
|
118
|
-
return "retry_same_batch";
|
|
53
|
+
const reasonOf = (decision) => {
|
|
54
|
+
if (decision.outcome === "accepted_complete")
|
|
55
|
+
return null;
|
|
56
|
+
if (decision.outcome === "accepted_partial")
|
|
57
|
+
return "accepted_partial";
|
|
58
|
+
if (decision.outcome === "accepted_filtered")
|
|
59
|
+
return "accepted_filtered";
|
|
60
|
+
return decision.result.reason ?? decision.outcome;
|
|
119
61
|
};
|
|
120
|
-
const
|
|
121
|
-
const fallback = (status) => ({
|
|
62
|
+
const consentRevoked = () => ({
|
|
122
63
|
ok: false,
|
|
123
|
-
retryStrategy:
|
|
124
|
-
|
|
64
|
+
retryStrategy: "drop_event",
|
|
65
|
+
directiveUnreadable: false,
|
|
125
66
|
droppedEventCount: 0,
|
|
126
67
|
acceptedEventCount: 0,
|
|
127
68
|
acceptedPartial: false,
|
|
128
|
-
|
|
129
|
-
|
|
69
|
+
acceptedFiltered: false,
|
|
70
|
+
reason: "consent_denied",
|
|
71
|
+
diagnosticCodes: [],
|
|
130
72
|
});
|
|
131
|
-
const fallbackDiagnosticCodes = (status) => {
|
|
132
|
-
if (classifyIngestHttpStatus(status) !== "retry_same_batch")
|
|
133
|
-
return [];
|
|
134
|
-
if (status >= 200 && status <= 299)
|
|
135
|
-
return ["invalid_response"];
|
|
136
|
-
if (status >= 300 && status <= 399)
|
|
137
|
-
return ["protocol_redirect"];
|
|
138
|
-
if (status >= 500 && status <= 599)
|
|
139
|
-
return [];
|
|
140
|
-
if (RETRYABLE_CLIENT_STATUSES.includes(status))
|
|
141
|
-
return ["invalid_response"];
|
|
142
|
-
return ["unknown_status"];
|
|
143
|
-
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type DiagnosticCode } from "../_generated/diagnostic-codes.js";
|
|
1
2
|
import type { IngestRetryStrategy } from "./delivery-response-contract.js";
|
|
2
|
-
export type DiagnosticCode
|
|
3
|
+
export type { DiagnosticCode };
|
|
3
4
|
export type DiagnosticLifecycle = "open" | "closing" | "closing_failed" | "closed";
|
|
4
5
|
export type DiagnosticFlushOutcome = "clean" | "failed" | null;
|
|
5
6
|
export type DiagnosticCloseOutcome = "clean" | "retryable_failure" | "terminal_failure" | null;
|
|
@@ -1,31 +1,7 @@
|
|
|
1
1
|
import { hmac } from "@noble/hashes/hmac";
|
|
2
2
|
import { sha256 } from "@noble/hashes/sha256";
|
|
3
3
|
import { bytesToHex, randomBytes, utf8ToBytes } from "@noble/hashes/utils";
|
|
4
|
-
|
|
5
|
-
"batch_split",
|
|
6
|
-
"consent_persistence_failed",
|
|
7
|
-
"consent_purge",
|
|
8
|
-
"consent_purge_failed",
|
|
9
|
-
"consent_state_unavailable",
|
|
10
|
-
"durable_outbox_unavailable",
|
|
11
|
-
"duplicate_drop_details_unavailable",
|
|
12
|
-
"event_oversize",
|
|
13
|
-
"intentional_drop",
|
|
14
|
-
"invalid_response",
|
|
15
|
-
"legacy_pre_revocation_event_blocked",
|
|
16
|
-
"legacy_bridge_capacity_reached",
|
|
17
|
-
"legacy_writer_detected",
|
|
18
|
-
"non_canonical_success_status",
|
|
19
|
-
"outbox_key_collision",
|
|
20
|
-
"outbox_record_corrupt",
|
|
21
|
-
"protocol_redirect",
|
|
22
|
-
"queue_overflow",
|
|
23
|
-
"retry_exhausted",
|
|
24
|
-
"retry_scheduled",
|
|
25
|
-
"shutdown_failure",
|
|
26
|
-
"terminal_rejection",
|
|
27
|
-
"unknown_status",
|
|
28
|
-
];
|
|
4
|
+
import { DIAGNOSTIC_CODES as CODES, } from "../_generated/diagnostic-codes.js";
|
|
29
5
|
const CODE_SET = new Set(CODES);
|
|
30
6
|
const STRATEGIES = new Set([
|
|
31
7
|
"split_batch",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const generateMonotonicId: (prefix?: string) => string;
|
|
2
2
|
export declare const generateTraceId: () => string;
|
|
3
3
|
export declare const generateEventId: () => string;
|
|
4
|
+
export declare const generateBatchId: () => string;
|
|
4
5
|
export declare const generateAnonymousId: () => string;
|
|
5
6
|
export declare const generateSessionId: () => string;
|
|
6
7
|
export declare const generateTabId: () => string;
|
|
@@ -49,6 +49,9 @@ export const generateTraceId = () => {
|
|
|
49
49
|
export const generateEventId = () => {
|
|
50
50
|
return generateMonotonicId();
|
|
51
51
|
};
|
|
52
|
+
export const generateBatchId = () => {
|
|
53
|
+
return toHex(randomBytes(16));
|
|
54
|
+
};
|
|
52
55
|
export const generateAnonymousId = () => {
|
|
53
56
|
return generateMonotonicId("anon");
|
|
54
57
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExportResult, ExportResultCode } from "@opentelemetry/core";
|
|
2
2
|
import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
3
3
|
import type { CluebaseContext } from "../provider/types.js";
|
|
4
|
-
import type { CollectionPermit } from "./
|
|
4
|
+
import type { CollectionPermit } from "./sdk-core-delivery.js";
|
|
5
5
|
import type { BrowserSourceEvent, BrowserSourceSignalEvent } from "./source-event.js";
|
|
6
6
|
export declare const EXPORT_RESULT_SUCCESS: ExportResultCode;
|
|
7
7
|
export declare const EXPORT_RESULT_FAILED: ExportResultCode;
|
|
@@ -15,7 +15,7 @@ export class ObservationSourceEventExporter {
|
|
|
15
15
|
this.shutdownImpl = options.shutdownImpl;
|
|
16
16
|
this.getContext = options.getContext;
|
|
17
17
|
this.deniedKeys = options.deniedKeys ?? [];
|
|
18
|
-
this.ignoreUrlPrefixes =
|
|
18
|
+
this.ignoreUrlPrefixes = options.ignoreUrlPrefixes ?? [];
|
|
19
19
|
}
|
|
20
20
|
ignoreSpanReason(span) {
|
|
21
21
|
const spanFields = span;
|
|
@@ -30,7 +30,7 @@ export class ObservationSourceEventExporter {
|
|
|
30
30
|
? fullUrl
|
|
31
31
|
: undefined;
|
|
32
32
|
if (url &&
|
|
33
|
-
this.ignoreUrlPrefixes.some((prefix) => url.startsWith(prefix))) {
|
|
33
|
+
this.ignoreUrlPrefixes.some((prefix) => prefix.length > 0 && url.startsWith(prefix))) {
|
|
34
34
|
return "loop";
|
|
35
35
|
}
|
|
36
36
|
if (!Object.keys(attrs).some((key) => key.startsWith("cluebase."))) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ParameterValueFact } from "./sdk-core-wasm.js";
|
|
2
|
+
export declare const parameterValueFact: (value: unknown) => ParameterValueFact;
|
|
3
|
+
export declare const beginParameterCaptureDecision: (key: string, value: unknown) => string;
|
|
4
|
+
export declare const canEncryptParameters: () => boolean;
|
|
5
|
+
export declare const beginParameterEncryption: (plaintext: string) => {
|
|
6
|
+
handle: string;
|
|
7
|
+
keyVersion: number;
|
|
8
|
+
} | null;
|
|
9
|
+
export declare const settleParameterCaptureDecisions: (node: unknown) => void;
|
|
10
|
+
export declare const settlePendingEncryptions: <T>(value: T) => Promise<T>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { currentSdkEncryption, } from "../provider/sdk-encryption-store.js";
|
|
2
|
+
import { decideParameterCaptures, } from "./sdk-core-wasm.js";
|
|
3
|
+
const PENDING_PREFIX = "cbenc~";
|
|
4
|
+
const PENDING_DECISION_PREFIX = "cbdec~";
|
|
5
|
+
const MAX_PENDING_ENCRYPTIONS = 1000;
|
|
6
|
+
const pending = new Map();
|
|
7
|
+
const pendingDecisions = new Map();
|
|
8
|
+
const importedKeys = new Map();
|
|
9
|
+
const toBase64 = (bytes) => {
|
|
10
|
+
let binary = "";
|
|
11
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
12
|
+
binary += String.fromCharCode(bytes[index] ?? 0);
|
|
13
|
+
}
|
|
14
|
+
return btoa(binary);
|
|
15
|
+
};
|
|
16
|
+
const fromBase64 = (text) => {
|
|
17
|
+
const binary = atob(text);
|
|
18
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
19
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
20
|
+
bytes[index] = binary.charCodeAt(index);
|
|
21
|
+
}
|
|
22
|
+
return bytes;
|
|
23
|
+
};
|
|
24
|
+
const subtle = () => typeof crypto !== "undefined" && crypto.subtle ? crypto.subtle : null;
|
|
25
|
+
const importPublicKey = (config) => {
|
|
26
|
+
const cacheKey = `${config.keyVersion}:${config.publicKey}`;
|
|
27
|
+
const cached = importedKeys.get(cacheKey);
|
|
28
|
+
if (cached)
|
|
29
|
+
return cached;
|
|
30
|
+
const api = subtle();
|
|
31
|
+
const imported = api
|
|
32
|
+
? api
|
|
33
|
+
.importKey("spki", fromBase64(config.publicKey), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"])
|
|
34
|
+
.catch(() => null)
|
|
35
|
+
: Promise.resolve(null);
|
|
36
|
+
importedKeys.set(cacheKey, imported);
|
|
37
|
+
return imported;
|
|
38
|
+
};
|
|
39
|
+
const encryptNow = async (config, plaintext) => {
|
|
40
|
+
const api = subtle();
|
|
41
|
+
const bytes = new TextEncoder().encode(plaintext);
|
|
42
|
+
try {
|
|
43
|
+
const key = await importPublicKey(config);
|
|
44
|
+
if (!api || !key)
|
|
45
|
+
return null;
|
|
46
|
+
const encrypted = await api.encrypt({ name: "RSA-OAEP" }, key, bytes);
|
|
47
|
+
return {
|
|
48
|
+
keyVersion: config.keyVersion,
|
|
49
|
+
ciphertext: toBase64(new Uint8Array(encrypted)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
bytes.fill(0);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const newHandle = (prefix) => {
|
|
60
|
+
const random = new Uint8Array(12);
|
|
61
|
+
crypto.getRandomValues(random);
|
|
62
|
+
return `${prefix}${toBase64(random).replace(/[^A-Za-z0-9]/g, "")}`;
|
|
63
|
+
};
|
|
64
|
+
export const parameterValueFact = (value) => {
|
|
65
|
+
if (typeof value === "boolean")
|
|
66
|
+
return { type: "boolean" };
|
|
67
|
+
if (typeof value === "number")
|
|
68
|
+
return { type: "number", number: value };
|
|
69
|
+
if (typeof value === "string")
|
|
70
|
+
return { type: "text", text: value };
|
|
71
|
+
return { type: "other" };
|
|
72
|
+
};
|
|
73
|
+
export const beginParameterCaptureDecision = (key, value) => {
|
|
74
|
+
const handle = newHandle(PENDING_DECISION_PREFIX);
|
|
75
|
+
if (pendingDecisions.size >= MAX_PENDING_ENCRYPTIONS) {
|
|
76
|
+
const oldest = pendingDecisions.keys().next().value;
|
|
77
|
+
if (oldest !== undefined)
|
|
78
|
+
pendingDecisions.delete(oldest);
|
|
79
|
+
}
|
|
80
|
+
pendingDecisions.set(handle, {
|
|
81
|
+
key,
|
|
82
|
+
value,
|
|
83
|
+
fact: parameterValueFact(value),
|
|
84
|
+
});
|
|
85
|
+
return handle;
|
|
86
|
+
};
|
|
87
|
+
export const canEncryptParameters = () => currentSdkEncryption() !== null && subtle() !== null;
|
|
88
|
+
export const beginParameterEncryption = (plaintext) => {
|
|
89
|
+
const config = currentSdkEncryption();
|
|
90
|
+
if (!config || !subtle())
|
|
91
|
+
return null;
|
|
92
|
+
const handle = newHandle(PENDING_PREFIX);
|
|
93
|
+
if (pending.size >= MAX_PENDING_ENCRYPTIONS) {
|
|
94
|
+
const oldest = pending.keys().next().value;
|
|
95
|
+
if (oldest !== undefined)
|
|
96
|
+
pending.delete(oldest);
|
|
97
|
+
}
|
|
98
|
+
pending.set(handle, encryptNow(config, plaintext));
|
|
99
|
+
return { handle, keyVersion: config.keyVersion };
|
|
100
|
+
};
|
|
101
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
102
|
+
const MASKED_FALLBACK = "__cluebase_masked__";
|
|
103
|
+
const settleLeaf = async (leaf) => {
|
|
104
|
+
const handle = leaf.ciphertext;
|
|
105
|
+
if (typeof handle !== "string" || !handle.startsWith(PENDING_PREFIX))
|
|
106
|
+
return;
|
|
107
|
+
const result = await pending.get(handle);
|
|
108
|
+
pending.delete(handle);
|
|
109
|
+
if (result) {
|
|
110
|
+
leaf.key_version = result.keyVersion;
|
|
111
|
+
leaf.ciphertext = result.ciphertext;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
leaf.capture_mode = "masked_fingerprint";
|
|
115
|
+
leaf.value = MASKED_FALLBACK;
|
|
116
|
+
delete leaf.key_version;
|
|
117
|
+
delete leaf.ciphertext;
|
|
118
|
+
};
|
|
119
|
+
const collectPendingDecisionLeaves = (node, found) => {
|
|
120
|
+
if (Array.isArray(node)) {
|
|
121
|
+
for (const child of node)
|
|
122
|
+
collectPendingDecisionLeaves(child, found);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!isRecord(node))
|
|
126
|
+
return;
|
|
127
|
+
const handle = node.ciphertext;
|
|
128
|
+
if (typeof handle === "string" &&
|
|
129
|
+
handle.startsWith(PENDING_DECISION_PREFIX)) {
|
|
130
|
+
found.push({ leaf: node, handle });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
for (const child of Object.values(node)) {
|
|
134
|
+
collectPendingDecisionLeaves(child, found);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const applyCaptureDecision = (leaf, value, decision) => {
|
|
138
|
+
if (decision === "plain") {
|
|
139
|
+
leaf.value = value;
|
|
140
|
+
leaf.capture_mode = "plain";
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (decision !== "encrypt" || typeof value !== "string")
|
|
144
|
+
return;
|
|
145
|
+
const encryption = beginParameterEncryption(value);
|
|
146
|
+
if (!encryption)
|
|
147
|
+
return;
|
|
148
|
+
leaf.capture_mode = "encrypted";
|
|
149
|
+
leaf.key_version = encryption.keyVersion;
|
|
150
|
+
leaf.ciphertext = encryption.handle;
|
|
151
|
+
};
|
|
152
|
+
export const settleParameterCaptureDecisions = (node) => {
|
|
153
|
+
const leaves = [];
|
|
154
|
+
collectPendingDecisionLeaves(node, leaves);
|
|
155
|
+
if (leaves.length === 0)
|
|
156
|
+
return;
|
|
157
|
+
const requested = leaves.map(({ handle }) => pendingDecisions.get(handle));
|
|
158
|
+
const decisions = decideParameterCaptures(requested
|
|
159
|
+
.filter((entry) => entry !== undefined)
|
|
160
|
+
.map(({ key, fact }) => ({ key, value: fact })));
|
|
161
|
+
let answered = 0;
|
|
162
|
+
for (const [index, { leaf, handle }] of leaves.entries()) {
|
|
163
|
+
delete leaf.ciphertext;
|
|
164
|
+
pendingDecisions.delete(handle);
|
|
165
|
+
const entry = requested[index];
|
|
166
|
+
if (entry === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
applyCaptureDecision(leaf, entry.value, decisions[answered]);
|
|
169
|
+
answered += 1;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const settleTree = async (node) => {
|
|
173
|
+
if (Array.isArray(node)) {
|
|
174
|
+
for (const child of node)
|
|
175
|
+
await settleTree(child);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!isRecord(node))
|
|
179
|
+
return;
|
|
180
|
+
if (node.capture_mode === "encrypted") {
|
|
181
|
+
await settleLeaf(node);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const child of Object.values(node))
|
|
185
|
+
await settleTree(child);
|
|
186
|
+
};
|
|
187
|
+
const settleText = async (text) => {
|
|
188
|
+
if (!text.includes(PENDING_PREFIX) &&
|
|
189
|
+
!text.includes(PENDING_DECISION_PREFIX)) {
|
|
190
|
+
return text;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(text);
|
|
194
|
+
settleParameterCaptureDecisions(parsed);
|
|
195
|
+
await settleTree(parsed);
|
|
196
|
+
return JSON.stringify(parsed);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return text;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
export const settlePendingEncryptions = async (value) => {
|
|
203
|
+
if (typeof value === "string")
|
|
204
|
+
return (await settleText(value));
|
|
205
|
+
if (Array.isArray(value)) {
|
|
206
|
+
const settled = [];
|
|
207
|
+
for (const child of value)
|
|
208
|
+
settled.push(await settlePendingEncryptions(child));
|
|
209
|
+
return settled;
|
|
210
|
+
}
|
|
211
|
+
if (isRecord(value)) {
|
|
212
|
+
const settled = {};
|
|
213
|
+
for (const [key, child] of Object.entries(value)) {
|
|
214
|
+
settled[key] = await settlePendingEncryptions(child);
|
|
215
|
+
}
|
|
216
|
+
return settled;
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { hasParameterSnapshotLeafShape } from "../_generated/parameter-snapshot-leaf.js";
|
|
2
2
|
import { FORBIDDEN_PARAMETER_VALUE, MASKED_VALUE, MAX_SANITIZE_DEPTH, TRUNCATED_PARAMETER_VALUE, } from "../provider/constants.js";
|
|
3
3
|
import { PARAMETER_CAPTURE_MODE } from "../provider/contracts.js";
|
|
4
|
+
import { beginParameterCaptureDecision, canEncryptParameters, settleParameterCaptureDecisions, } from "./parameter-encryption.js";
|
|
4
5
|
import { isDeniedKey } from "./sanitize.js";
|
|
6
|
+
import { isSdkCoreWasmReady } from "./sdk-core-wasm.js";
|
|
5
7
|
const SNAPSHOT_UNAVAILABLE_KEY = "_cluebase_snapshot_unavailable";
|
|
6
8
|
const normalizeParameterLookupPath = (path) => {
|
|
7
9
|
return path.replace(/\[\d+\]/g, "[]");
|
|
@@ -60,6 +62,19 @@ const buildLeaf = (value, captureMode, released = false) => {
|
|
|
60
62
|
: {}),
|
|
61
63
|
};
|
|
62
64
|
};
|
|
65
|
+
const leafKeyOf = (path) => {
|
|
66
|
+
const trimmed = path.replace(/(?:\[\])+$/, "");
|
|
67
|
+
return trimmed.slice(trimmed.lastIndexOf(".") + 1);
|
|
68
|
+
};
|
|
69
|
+
const buildClassifiedLeaf = (path, value) => {
|
|
70
|
+
const masked = buildLeaf(value, PARAMETER_CAPTURE_MODE.maskedFingerprint);
|
|
71
|
+
if (!path)
|
|
72
|
+
return masked;
|
|
73
|
+
return {
|
|
74
|
+
...masked,
|
|
75
|
+
ciphertext: beginParameterCaptureDecision(leafKeyOf(path), value),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
63
78
|
export const buildUnavailableParameterSnapshot = () => ({
|
|
64
79
|
[SNAPSHOT_UNAVAILABLE_KEY]: buildLeaf("snapshot_unavailable", PARAMETER_CAPTURE_MODE.maskedFingerprint),
|
|
65
80
|
});
|
|
@@ -173,10 +188,15 @@ export const buildParameterSnapshotBundle = (value, options) => {
|
|
|
173
188
|
const matchedAllowedPath = path
|
|
174
189
|
? (allowedPathLookup.get(normalizeParameterLookupPath(path)) ?? null)
|
|
175
190
|
: null;
|
|
176
|
-
|
|
191
|
+
if (matchedAllowedPath !== null && !canEncryptParameters()) {
|
|
192
|
+
return {
|
|
193
|
+
snapshot: buildLeaf(current, PARAMETER_CAPTURE_MODE.maskedFingerprint, true),
|
|
194
|
+
allowedValueKeys: [matchedAllowedPath],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
177
197
|
return {
|
|
178
|
-
snapshot:
|
|
179
|
-
allowedValueKeys:
|
|
198
|
+
snapshot: buildClassifiedLeaf(path, current),
|
|
199
|
+
allowedValueKeys: [],
|
|
180
200
|
};
|
|
181
201
|
};
|
|
182
202
|
const rootBundle = walk(value, rootPathPrefix, 0);
|
|
@@ -185,6 +205,8 @@ export const buildParameterSnapshotBundle = (value, options) => {
|
|
|
185
205
|
!Array.isArray(rootBundle.snapshot)
|
|
186
206
|
? rootBundle.snapshot
|
|
187
207
|
: {};
|
|
208
|
+
if (isSdkCoreWasmReady())
|
|
209
|
+
settleParameterCaptureDecisions(snapshot);
|
|
188
210
|
return {
|
|
189
211
|
snapshot,
|
|
190
212
|
allowedValueKeys: [...new Set(rootBundle.allowedValueKeys)].sort(),
|