@capxul/observability 4.1.3 → 4.2.0
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/README.md +26 -17
- package/dist/engineering.d.mts +1 -1
- package/dist/engineering.mjs +90 -25
- package/dist/index.d.mts +10 -13
- package/dist/index.mjs +73 -90
- package/dist/src-CWfRYsBI.mjs +302 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,10 @@ This public package defines event names and journeys, validates
|
|
|
7
7
|
event properties, marks and redacts PII, and provides recording, in-memory,
|
|
8
8
|
and PostHog-compatible adapters.
|
|
9
9
|
|
|
10
|
+
Activity action events accept the implemented `unresolved` Payment status in
|
|
11
|
+
committed filters and export filters. The event records the selected status;
|
|
12
|
+
it does not infer settlement from an unresolved result.
|
|
13
|
+
|
|
10
14
|
## The required envelope (ADR-0020 A1 amendment)
|
|
11
15
|
|
|
12
16
|
One PostHog project holds every environment, so `capxul_env` — not project
|
|
@@ -22,23 +26,28 @@ optional and snake_case; the camelCase spellings are retired.
|
|
|
22
26
|
One boundary, two named postures, both built on the same sanitizer, token
|
|
23
27
|
guard and missing-key opt-out:
|
|
24
28
|
|
|
25
|
-
| | `utilityAppAnalytics` | `productAppAnalytics`
|
|
26
|
-
| --------------- | ------------------------------------------ |
|
|
27
|
-
| For | capability-URL apps (approval links, docs) | the Capxul product web app
|
|
28
|
-
| Persistence | off | **on** — J1 spans an OTP reload
|
|
29
|
-
| Person profiles | identified-only, nothing identifies | identified-only, identify-driven
|
|
30
|
-
| Autocapture | off | off
|
|
31
|
-
| Session replay | off | **on in every environment**,
|
|
32
|
-
| Exceptions | off | on
|
|
33
|
-
|
|
34
|
-
Both stamp `capxul_env` and `producer: "browser"` through `before_send
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
| | `utilityAppAnalytics` | `productAppAnalytics` |
|
|
30
|
+
| --------------- | ------------------------------------------ | ----------------------------------------------- |
|
|
31
|
+
| For | capability-URL apps (approval links, docs) | the Capxul product web app |
|
|
32
|
+
| Persistence | off | **on** — J1 spans an OTP reload |
|
|
33
|
+
| Person profiles | identified-only, nothing identifies | identified-only, identify-driven |
|
|
34
|
+
| Autocapture | off | off |
|
|
35
|
+
| Session replay | off | **on in every environment**, credentials masked |
|
|
36
|
+
| Exceptions | off | on |
|
|
37
|
+
|
|
38
|
+
Both stamp `capxul_env` and `producer: "browser"` through `before_send`.
|
|
39
|
+
Utility analytics removes URL queries, fragments, and promoted campaign/search
|
|
40
|
+
values. Product analytics preserves safe URL components and masks credentials.
|
|
41
|
+
Both profiles preserve the PostHog routing token required for transport.
|
|
42
|
+
`createPostHogBrowserPageviewTracker` reports pathname-only initial loads and
|
|
43
|
+
SPA route changes. Vendor initialization and pageview capture failures degrade
|
|
44
|
+
to disabled analytics.
|
|
45
|
+
|
|
46
|
+
[The redaction rule](../../docs/rules/redaction.md) defines the data classes.
|
|
47
|
+
Product replay masks password, OTP, and credential controls. Ordinary input
|
|
48
|
+
remains visible. Network copies mask credential fields, headers, and URL parts
|
|
49
|
+
while retaining safe codes, public chain evidence, and replay identifiers.
|
|
50
|
+
Existing event schemas and the four auth-event email hashes remain unchanged.
|
|
42
51
|
|
|
43
52
|
### Import the standard `posthog-js` build
|
|
44
53
|
|
package/dist/engineering.d.mts
CHANGED
|
@@ -12,7 +12,7 @@ interface EngineeringTelemetryConfig {
|
|
|
12
12
|
readonly sdkVersion: string;
|
|
13
13
|
readonly serviceName?: string;
|
|
14
14
|
}
|
|
15
|
-
declare const traceHeaderFilter: (
|
|
15
|
+
declare const traceHeaderFilter: (_name: string) => boolean;
|
|
16
16
|
declare const postHogOtlpEndpoints: (host: string) => {
|
|
17
17
|
readonly logs: `${string}/i/v1/logs`;
|
|
18
18
|
readonly traces: `${string}/i/v1/traces`;
|
package/dist/engineering.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as redactUrlSecrets, n as isCredentialField, r as redactSecrets } from "./src-CWfRYsBI.mjs";
|
|
2
|
+
import { Cause, Effect, Exit, FiberSet, Layer, Stream, Tracer } from "effect";
|
|
2
3
|
import { FetchHttpClient, Headers, HttpClient } from "effect/unstable/http";
|
|
3
4
|
import { OtlpExporter, OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
|
|
4
5
|
//#region src/engineering.ts
|
|
@@ -8,21 +9,8 @@ const ENGINEERING_CAPXUL_ENVS = [
|
|
|
8
9
|
"production",
|
|
9
10
|
"local"
|
|
10
11
|
];
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
"content-type",
|
|
14
|
-
"traceparent",
|
|
15
|
-
"tracestate",
|
|
16
|
-
"x-request-id"
|
|
17
|
-
];
|
|
18
|
-
const ENGINEERING_REDACTED_HEADER_NAMES = Object.freeze([
|
|
19
|
-
"authorization",
|
|
20
|
-
"cookie",
|
|
21
|
-
"set-cookie",
|
|
22
|
-
"x-api-key",
|
|
23
|
-
/auth|email|key|otp|secret|session|token|wallet/i
|
|
24
|
-
]);
|
|
25
|
-
const traceHeaderFilter = (name) => SAFE_TRACED_HEADER_NAMES.includes(name.toLowerCase());
|
|
12
|
+
const traceHeaderFilter = (_name) => true;
|
|
13
|
+
const credentialHeaderPattern = Object.assign(/./u, { test: (name) => isCredentialField(name, "header") });
|
|
26
14
|
const postHogOtlpEndpoints = (host) => {
|
|
27
15
|
const base = host.replace(/\/+$/, "");
|
|
28
16
|
return {
|
|
@@ -54,23 +42,79 @@ const validateEngineeringTelemetryConfig = (config) => {
|
|
|
54
42
|
headers: Object.freeze({ authorization: headerEntries[0][1] })
|
|
55
43
|
};
|
|
56
44
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
const failureText = (failure, key, limit, source) => {
|
|
46
|
+
if (failure === null || typeof failure !== "object") return void 0;
|
|
47
|
+
try {
|
|
48
|
+
let owner = failure;
|
|
49
|
+
let descriptor;
|
|
50
|
+
for (let depth = 0; owner !== null && depth < 4; depth += 1) {
|
|
51
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, key);
|
|
52
|
+
if (descriptor !== void 0 || key !== "name") break;
|
|
53
|
+
owner = Object.getPrototypeOf(owner);
|
|
54
|
+
}
|
|
55
|
+
const value = descriptor?.value;
|
|
56
|
+
return typeof value === "string" || key === "code" && typeof value === "number" ? redactSecrets(String(value), source).slice(0, limit) : void 0;
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const exportFailure = (failure, source) => {
|
|
62
|
+
const message = failureText(failure, "message", 2048, source) ?? (typeof failure === "string" ? redactSecrets(failure, source).slice(0, 2048) : "Engineering operation failed");
|
|
63
|
+
const code = failureText(failure, "code", 128);
|
|
64
|
+
const error = new Error((code === void 0 ? message : `${message} [code=${code}]`).slice(0, 2048));
|
|
65
|
+
error.name = failureText(failure, "_tag", 128) ?? failureText(failure, "name", 128) ?? "Error";
|
|
66
|
+
error.stack = failureText(failure, "stack", 8192, source) ?? `${error.name}: ${error.message}`;
|
|
67
|
+
return error;
|
|
68
|
+
};
|
|
69
|
+
const exportFailureExit = (cause, source) => Exit.failCause(Cause.fromReasons(cause.reasons.filter((reason) => !Cause.isInterruptReason(reason)).slice(0, 8).map((reason) => Cause.isFailReason(reason) ? Cause.makeFailReason(exportFailure(reason.error, source)) : Cause.makeDieReason(exportFailure(reason.defect, source)))));
|
|
70
|
+
const BROWSER_KEEPALIVE_BYTES = 65536;
|
|
71
|
+
let unfinishedBrowserExportBytes = 0;
|
|
72
|
+
const hasBrowserDocument = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
73
|
+
const browserExportClient = (client) => HttpClient.transform(client, (response, request) => Effect.acquireUseRelease(Effect.sync(() => {
|
|
74
|
+
const bytes = "contentLength" in request.body ? request.body.contentLength : void 0;
|
|
75
|
+
if (request.body._tag === "Stream" || request.body._tag === "FormData" || bytes === void 0 || !Number.isSafeInteger(bytes) || bytes < 0 || unfinishedBrowserExportBytes + bytes > BROWSER_KEEPALIVE_BYTES) return;
|
|
76
|
+
unfinishedBrowserExportBytes += bytes;
|
|
77
|
+
return bytes;
|
|
78
|
+
}), (bytes) => response.pipe(Effect.provideService(FetchHttpClient.RequestInit, { keepalive: bytes !== void 0 }), Effect.tap((result) => bytes === void 0 ? Effect.void : Stream.runDrain(result.stream).pipe(Effect.catch((error) => error.reason._tag === "EmptyBodyError" ? Effect.void : Effect.fail(error))))), (bytes) => Effect.sync(() => {
|
|
79
|
+
if (bytes !== void 0) unfinishedBrowserExportBytes -= bytes;
|
|
80
|
+
})));
|
|
81
|
+
const spanUpstream = (span) => {
|
|
82
|
+
let current = span;
|
|
83
|
+
while (current?._tag === "Span") {
|
|
84
|
+
const upstream = current.attributes.get("chain.upstream");
|
|
85
|
+
if (upstream === "alchemy" || upstream === "infura") return upstream;
|
|
86
|
+
current = current.parent._tag === "Some" ? current.parent.value : void 0;
|
|
62
87
|
}
|
|
63
88
|
};
|
|
64
|
-
const REDACTED_SPAN_FAILURE = Exit.fail(new RedactedEngineeringSpanFailure());
|
|
65
89
|
/** Preserve domain exits while preventing the OTLP serializer from seeing raw causes. */
|
|
66
90
|
const makeLeakSafeEngineeringTracer = (delegate) => Tracer.make({
|
|
67
91
|
span(options) {
|
|
68
92
|
const span = delegate.span(options);
|
|
69
93
|
const wrapped = Object.create(span);
|
|
94
|
+
Object.defineProperty(wrapped, "attribute", { value: (name, value) => {
|
|
95
|
+
const source = spanUpstream(span);
|
|
96
|
+
const header = /^http\.(?:request|response)\.header\.(.+)$/u.exec(name)?.[1];
|
|
97
|
+
if (header !== void 0 && isCredentialField(header, "header")) span.attribute(name, "[REDACTED]");
|
|
98
|
+
else if (typeof value === "string" && name === "url.query") span.attribute(name, redactUrlSecrets(`?${value}`).slice(1, 2049));
|
|
99
|
+
else if (typeof value === "string" && name === "url.path") {
|
|
100
|
+
const fullUrl = span.attributes.get("url.full");
|
|
101
|
+
const path = typeof fullUrl === "string" ? /^https?:\/\/[^/]+([^?#]*)/u.exec(fullUrl)?.[1] ?? value : value;
|
|
102
|
+
span.attribute(name, redactUrlSecrets(path, source).slice(0, 2048));
|
|
103
|
+
} else if (typeof value === "string" && (name === "url.full" || header === "location" || header === "referer" || header === "referrer")) span.attribute(name, redactUrlSecrets(value, source).slice(0, 2048));
|
|
104
|
+
else if (header !== void 0 && typeof value === "string") span.attribute(name, redactSecrets(value, source).slice(0, 2048));
|
|
105
|
+
else span.attribute(name, value);
|
|
106
|
+
} });
|
|
70
107
|
Object.defineProperty(wrapped, "end", {
|
|
71
108
|
configurable: false,
|
|
72
109
|
enumerable: false,
|
|
73
|
-
value: (endTime, exit) =>
|
|
110
|
+
value: (endTime, exit) => {
|
|
111
|
+
if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
|
|
112
|
+
const first = exit.cause.reasons.find((reason) => !Cause.isInterruptReason(reason));
|
|
113
|
+
const code = failureText(first === void 0 ? void 0 : Cause.isFailReason(first) ? first.error : first.defect, "code", 128);
|
|
114
|
+
if (code !== void 0) span.attribute("error.code", code);
|
|
115
|
+
span.end(endTime, exportFailureExit(exit.cause, spanUpstream(span)));
|
|
116
|
+
} else span.end(endTime, exit);
|
|
117
|
+
},
|
|
74
118
|
writable: false
|
|
75
119
|
});
|
|
76
120
|
return wrapped;
|
|
@@ -101,8 +145,29 @@ const makeEngineeringTelemetryLayer = (config) => {
|
|
|
101
145
|
resource,
|
|
102
146
|
mergeWithExisting: true
|
|
103
147
|
});
|
|
104
|
-
const headerPolicy = Layer.merge(Layer.succeed(HttpClient.TracerHeaderFilter, traceHeaderFilter), Layer.succeed(Headers.CurrentRedactedNames,
|
|
105
|
-
|
|
148
|
+
const headerPolicy = Layer.merge(Layer.succeed(HttpClient.TracerHeaderFilter, traceHeaderFilter), Layer.succeed(Headers.CurrentRedactedNames, [credentialHeaderPattern]));
|
|
149
|
+
const browserLifecycle = Layer.effectDiscard(Effect.gen(function* () {
|
|
150
|
+
if (validated.producer !== "browser" || !hasBrowserDocument()) return;
|
|
151
|
+
const browserWindow = window;
|
|
152
|
+
const browserDocument = document;
|
|
153
|
+
const flusher = yield* OtlpExporter.Flusher;
|
|
154
|
+
const run = yield* FiberSet.makeRuntime();
|
|
155
|
+
const flush = () => {
|
|
156
|
+
run(flusher.flush);
|
|
157
|
+
};
|
|
158
|
+
const visibilityChanged = () => {
|
|
159
|
+
if (browserDocument.visibilityState === "hidden") flush();
|
|
160
|
+
};
|
|
161
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
162
|
+
browserDocument.addEventListener("visibilitychange", visibilityChanged);
|
|
163
|
+
browserWindow.addEventListener("pagehide", flush);
|
|
164
|
+
}), () => Effect.sync(() => {
|
|
165
|
+
browserDocument.removeEventListener("visibilitychange", visibilityChanged);
|
|
166
|
+
browserWindow.removeEventListener("pagehide", flush);
|
|
167
|
+
}));
|
|
168
|
+
}));
|
|
169
|
+
const transport = Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, (client) => validated.producer === "browser" && hasBrowserDocument() ? browserExportClient(client) : client)).pipe(Layer.provide(FetchHttpClient.layer));
|
|
170
|
+
return browserLifecycle.pipe(Layer.provideMerge(Layer.mergeAll(tracing, logging, headerPolicy)), Layer.provide(OtlpSerialization.layerJson), Layer.provide(transport));
|
|
106
171
|
};
|
|
107
172
|
//#endregion
|
|
108
173
|
export { ENGINEERING_CAPXUL_ENVS, makeEngineeringTelemetryLayer, makeLeakSafeEngineeringTracer, postHogOtlpEndpoints, traceHeaderFilter };
|
package/dist/index.d.mts
CHANGED
|
@@ -1910,7 +1910,7 @@ declare const UiActivityActionCompletedTelemetryEventSchema: Schema.Struct<{
|
|
|
1910
1910
|
readonly sdk_version: Schema.optional<Schema.String>;
|
|
1911
1911
|
readonly filter_kind: Schema.optional<Schema.Literals<readonly ["all", "payment", "movement"]>>;
|
|
1912
1912
|
readonly filter_direction: Schema.optional<Schema.Literals<readonly ["all", "in", "out", "self"]>>;
|
|
1913
|
-
readonly filter_status: Schema.optional<Schema.$Array<Schema.Literals<readonly ["pending", "settling", "pending_claim", "scheduled", "streaming", "settled", "cancelled", "failed"]>>>;
|
|
1913
|
+
readonly filter_status: Schema.optional<Schema.$Array<Schema.Literals<readonly ["pending", "settling", "unresolved", "pending_claim", "scheduled", "streaming", "settled", "cancelled", "failed"]>>>;
|
|
1914
1914
|
readonly scope_kind: Schema.Literals<readonly ["account", "organization"]>;
|
|
1915
1915
|
readonly reason_code: Schema.optional<Schema.Literals<readonly ["NOT_AUTHENTICATED", "EMAIL_DELIVERY_FAILED", "PROFILE_NOT_FOUND", "SMART_ACCOUNT_MISSING", "PLAYER_NOT_FOUND", "ACCOUNT_NOT_FOUND", "ACCOUNT_CLAIMED_BY_OTHER_SIGNER", "PROVIDER_ERROR", "CAPABILITY_UNAVAILABLE", "INVALID_INPUT", "ENV_MISSING", "NOT_IMPLEMENTED", "VERIFICATION_REQUIRED", "INSUFFICIENT_BALANCE", "INVALID_RECIPIENT", "ROLE_PERMISSION_DENIED", "TRANSACTION_FAILED", "RATE_LIMITED", "NETWORK_ERROR", "UNKNOWN", "OTP_EXPIRED", "SIGNER_REJECTED", "CANCELLED", "WRONG_STATE", "STALE_EPOCH", "SUPERSEDED", "WORK_DIED", "ACTOR_STOPPED"]>>;
|
|
1916
1916
|
readonly action: Schema.Literal<"filter">;
|
|
@@ -1967,7 +1967,7 @@ declare const UiActivityActionCompletedTelemetryEventSchema: Schema.Struct<{
|
|
|
1967
1967
|
readonly sdk_version: Schema.optional<Schema.String>;
|
|
1968
1968
|
readonly filter_kind: Schema.optional<Schema.Literals<readonly ["all", "payment", "movement"]>>;
|
|
1969
1969
|
readonly filter_direction: Schema.optional<Schema.Literals<readonly ["all", "in", "out", "self"]>>;
|
|
1970
|
-
readonly filter_status: Schema.optional<Schema.$Array<Schema.Literals<readonly ["pending", "settling", "pending_claim", "scheduled", "streaming", "settled", "cancelled", "failed"]>>>;
|
|
1970
|
+
readonly filter_status: Schema.optional<Schema.$Array<Schema.Literals<readonly ["pending", "settling", "unresolved", "pending_claim", "scheduled", "streaming", "settled", "cancelled", "failed"]>>>;
|
|
1971
1971
|
readonly scope_kind: Schema.Literals<readonly ["account", "organization"]>;
|
|
1972
1972
|
readonly reason_code: Schema.optional<Schema.Literals<readonly ["NOT_AUTHENTICATED", "EMAIL_DELIVERY_FAILED", "PROFILE_NOT_FOUND", "SMART_ACCOUNT_MISSING", "PLAYER_NOT_FOUND", "ACCOUNT_NOT_FOUND", "ACCOUNT_CLAIMED_BY_OTHER_SIGNER", "PROVIDER_ERROR", "CAPABILITY_UNAVAILABLE", "INVALID_INPUT", "ENV_MISSING", "NOT_IMPLEMENTED", "VERIFICATION_REQUIRED", "INSUFFICIENT_BALANCE", "INVALID_RECIPIENT", "ROLE_PERMISSION_DENIED", "TRANSACTION_FAILED", "RATE_LIMITED", "NETWORK_ERROR", "UNKNOWN", "OTP_EXPIRED", "SIGNER_REJECTED", "CANCELLED", "WRONG_STATE", "STALE_EPOCH", "SUPERSEDED", "WORK_DIED", "ACTOR_STOPPED"]>>;
|
|
1973
1973
|
readonly action: Schema.Literal<"export">;
|
|
@@ -2057,7 +2057,7 @@ declare const TELEMETRY_EVENT_SCHEMAS: {
|
|
|
2057
2057
|
readonly sdk_version: Schema.optional<Schema.String>;
|
|
2058
2058
|
readonly filter_kind: Schema.optional<Schema.Literals<readonly ["all", "payment", "movement"]>>;
|
|
2059
2059
|
readonly filter_direction: Schema.optional<Schema.Literals<readonly ["all", "in", "out", "self"]>>;
|
|
2060
|
-
readonly filter_status: Schema.optional<Schema.$Array<Schema.Literals<readonly ["pending", "settling", "pending_claim", "scheduled", "streaming", "settled", "cancelled", "failed"]>>>;
|
|
2060
|
+
readonly filter_status: Schema.optional<Schema.$Array<Schema.Literals<readonly ["pending", "settling", "unresolved", "pending_claim", "scheduled", "streaming", "settled", "cancelled", "failed"]>>>;
|
|
2061
2061
|
readonly scope_kind: Schema.Literals<readonly ["account", "organization"]>;
|
|
2062
2062
|
readonly reason_code: Schema.optional<Schema.Literals<readonly ["NOT_AUTHENTICATED", "EMAIL_DELIVERY_FAILED", "PROFILE_NOT_FOUND", "SMART_ACCOUNT_MISSING", "PLAYER_NOT_FOUND", "ACCOUNT_NOT_FOUND", "ACCOUNT_CLAIMED_BY_OTHER_SIGNER", "PROVIDER_ERROR", "CAPABILITY_UNAVAILABLE", "INVALID_INPUT", "ENV_MISSING", "NOT_IMPLEMENTED", "VERIFICATION_REQUIRED", "INSUFFICIENT_BALANCE", "INVALID_RECIPIENT", "ROLE_PERMISSION_DENIED", "TRANSACTION_FAILED", "RATE_LIMITED", "NETWORK_ERROR", "UNKNOWN", "OTP_EXPIRED", "SIGNER_REJECTED", "CANCELLED", "WRONG_STATE", "STALE_EPOCH", "SUPERSEDED", "WORK_DIED", "ACTOR_STOPPED"]>>;
|
|
2063
2063
|
readonly action: Schema.Literal<"filter">;
|
|
@@ -2114,7 +2114,7 @@ declare const TELEMETRY_EVENT_SCHEMAS: {
|
|
|
2114
2114
|
readonly sdk_version: Schema.optional<Schema.String>;
|
|
2115
2115
|
readonly filter_kind: Schema.optional<Schema.Literals<readonly ["all", "payment", "movement"]>>;
|
|
2116
2116
|
readonly filter_direction: Schema.optional<Schema.Literals<readonly ["all", "in", "out", "self"]>>;
|
|
2117
|
-
readonly filter_status: Schema.optional<Schema.$Array<Schema.Literals<readonly ["pending", "settling", "pending_claim", "scheduled", "streaming", "settled", "cancelled", "failed"]>>>;
|
|
2117
|
+
readonly filter_status: Schema.optional<Schema.$Array<Schema.Literals<readonly ["pending", "settling", "unresolved", "pending_claim", "scheduled", "streaming", "settled", "cancelled", "failed"]>>>;
|
|
2118
2118
|
readonly scope_kind: Schema.Literals<readonly ["account", "organization"]>;
|
|
2119
2119
|
readonly reason_code: Schema.optional<Schema.Literals<readonly ["NOT_AUTHENTICATED", "EMAIL_DELIVERY_FAILED", "PROFILE_NOT_FOUND", "SMART_ACCOUNT_MISSING", "PLAYER_NOT_FOUND", "ACCOUNT_NOT_FOUND", "ACCOUNT_CLAIMED_BY_OTHER_SIGNER", "PROVIDER_ERROR", "CAPABILITY_UNAVAILABLE", "INVALID_INPUT", "ENV_MISSING", "NOT_IMPLEMENTED", "VERIFICATION_REQUIRED", "INSUFFICIENT_BALANCE", "INVALID_RECIPIENT", "ROLE_PERMISSION_DENIED", "TRANSACTION_FAILED", "RATE_LIMITED", "NETWORK_ERROR", "UNKNOWN", "OTP_EXPIRED", "SIGNER_REJECTED", "CANCELLED", "WRONG_STATE", "STALE_EPOCH", "SUPERSEDED", "WORK_DIED", "ACTOR_STOPPED"]>>;
|
|
2120
2120
|
readonly action: Schema.Literal<"export">;
|
|
@@ -3181,12 +3181,12 @@ interface PostHogBrowserCapture {
|
|
|
3181
3181
|
* The callback mutates and returns the supplied event, matching PostHog's
|
|
3182
3182
|
* `before_send` contract while preserving the event's full structural type.
|
|
3183
3183
|
*/
|
|
3184
|
-
declare function sanitizePostHogBrowserEvent<T extends PostHogBrowserCapture | null>(event: T): T;
|
|
3184
|
+
declare function sanitizePostHogBrowserEvent<T extends PostHogBrowserCapture | null>(event: T, urlPolicy?: "path-only" | "diagnostic"): T;
|
|
3185
3185
|
/**
|
|
3186
3186
|
* Drop local events before transport. Sanitize and stamp every other browser
|
|
3187
3187
|
* event with `capxul_env` and `producer: "browser"`.
|
|
3188
3188
|
*/
|
|
3189
|
-
declare function createCapxulBeforeSend(capxulEnv: CapxulEnv): <T extends PostHogBrowserCapture | null>(event: T) => T | null;
|
|
3189
|
+
declare function createCapxulBeforeSend(capxulEnv: CapxulEnv, urlPolicy?: "path-only" | "diagnostic"): <T extends PostHogBrowserCapture | null>(event: T) => T | null;
|
|
3190
3190
|
type PostHogBeforeSend = ReturnType<typeof createCapxulBeforeSend>;
|
|
3191
3191
|
/**
|
|
3192
3192
|
* ADR-0016 posture, unchanged: capability-URL utility apps (approval links,
|
|
@@ -3227,7 +3227,7 @@ type PostHogBrowserOptions = PostHogUtilityAppOptions;
|
|
|
3227
3227
|
* funnel lies.
|
|
3228
3228
|
* - identify-driven person profiles (ADR-0020 A2 wires the calls at L1).
|
|
3229
3229
|
* - session replay in EVERY environment. Ordinary text and input values remain
|
|
3230
|
-
* visible.
|
|
3230
|
+
* visible. Password, OTP, and credential controls are masked. Network
|
|
3231
3231
|
* timing plus request/response headers and bodies are recorded, so a failed
|
|
3232
3232
|
* call shows its status and body in the replay. Canvas stays off. The
|
|
3233
3233
|
* recorder script loads from PostHog, so external dependency loading is on.
|
|
@@ -3298,17 +3298,14 @@ declare function utilityAppAnalytics(input: CapxulBrowserAnalyticsInput): PostHo
|
|
|
3298
3298
|
* generic over the real request type and returns the same type it was given.
|
|
3299
3299
|
*/
|
|
3300
3300
|
interface CapturedNetworkRequestLike {
|
|
3301
|
+
readonly name?: string | undefined;
|
|
3302
|
+
readonly url?: string | undefined;
|
|
3301
3303
|
readonly requestHeaders?: Record<string, string> | undefined;
|
|
3302
3304
|
readonly responseHeaders?: Record<string, string> | undefined;
|
|
3303
3305
|
readonly requestBody?: unknown;
|
|
3304
3306
|
readonly responseBody?: unknown;
|
|
3305
3307
|
}
|
|
3306
|
-
/**
|
|
3307
|
-
* Replay keeps the request line, status, timing, headers, and bodies, so a
|
|
3308
|
-
* failed call carries its evidence. Credential headers and secret material
|
|
3309
|
-
* never leave the browser: bearer tokens, cookies, API keys, and the OTP a
|
|
3310
|
-
* user just typed are masked before the recording is sent.
|
|
3311
|
-
*/
|
|
3308
|
+
/** Keep request evidence while masking credential values at the browser boundary. */
|
|
3312
3309
|
declare function maskCapturedNetworkRequest<T extends CapturedNetworkRequestLike>(request: T): T;
|
|
3313
3310
|
declare function productAppAnalytics(input: CapxulBrowserAnalyticsInput): PostHogProductAppOptions;
|
|
3314
3311
|
type PostHogBrowserInitialize<Options, T> = (key: string, options: Options) => T;
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { i as redactUrlSecrets, n as isCredentialField, r as redactSecrets, t as CAPXUL_ERROR_CODES } from "./src-CWfRYsBI.mjs";
|
|
1
2
|
import { Effect, Schema, SchemaGetter } from "effect";
|
|
2
3
|
//#region ../errors/src/chain-cause.ts
|
|
3
4
|
/**
|
|
@@ -35,39 +36,6 @@ const CHAIN_UPSTREAMS = [
|
|
|
35
36
|
"gateway"
|
|
36
37
|
];
|
|
37
38
|
//#endregion
|
|
38
|
-
//#region ../errors/src/errors.ts
|
|
39
|
-
const CAPXUL_ERROR_CODES = [
|
|
40
|
-
"NOT_AUTHENTICATED",
|
|
41
|
-
"EMAIL_DELIVERY_FAILED",
|
|
42
|
-
"PROFILE_NOT_FOUND",
|
|
43
|
-
"SMART_ACCOUNT_MISSING",
|
|
44
|
-
"PLAYER_NOT_FOUND",
|
|
45
|
-
"ACCOUNT_NOT_FOUND",
|
|
46
|
-
"ACCOUNT_CLAIMED_BY_OTHER_SIGNER",
|
|
47
|
-
"PROVIDER_ERROR",
|
|
48
|
-
"CAPABILITY_UNAVAILABLE",
|
|
49
|
-
"INVALID_INPUT",
|
|
50
|
-
"ENV_MISSING",
|
|
51
|
-
"NOT_IMPLEMENTED",
|
|
52
|
-
"VERIFICATION_REQUIRED",
|
|
53
|
-
"INSUFFICIENT_BALANCE",
|
|
54
|
-
"INVALID_RECIPIENT",
|
|
55
|
-
"ROLE_PERMISSION_DENIED",
|
|
56
|
-
"TRANSACTION_FAILED",
|
|
57
|
-
"RATE_LIMITED",
|
|
58
|
-
"NETWORK_ERROR",
|
|
59
|
-
"UNKNOWN",
|
|
60
|
-
"OTP_EXPIRED",
|
|
61
|
-
"SIGNER_REJECTED",
|
|
62
|
-
"CANCELLED",
|
|
63
|
-
"WRONG_STATE",
|
|
64
|
-
"STALE_EPOCH",
|
|
65
|
-
"SUPERSEDED",
|
|
66
|
-
"WORK_DIED",
|
|
67
|
-
"ACTOR_STOPPED"
|
|
68
|
-
];
|
|
69
|
-
new Set(CAPXUL_ERROR_CODES);
|
|
70
|
-
//#endregion
|
|
71
39
|
//#region ../types/src/index.ts
|
|
72
40
|
const EVM_ADDRESS_RE = /^0x[0-9a-f]{40}$/i;
|
|
73
41
|
const BYTES32_RE = /^0x[0-9a-f]{64}$/i;
|
|
@@ -1536,6 +1504,7 @@ const ActivityFilterProps = {
|
|
|
1536
1504
|
filter_status: Schema.optional(Schema.Array(Schema.Literals([
|
|
1537
1505
|
"pending",
|
|
1538
1506
|
"settling",
|
|
1507
|
+
"unresolved",
|
|
1539
1508
|
"pending_claim",
|
|
1540
1509
|
"scheduled",
|
|
1541
1510
|
"streaming",
|
|
@@ -1774,22 +1743,24 @@ function redactTelemetryProps(name, props, options = {}) {
|
|
|
1774
1743
|
* The callback mutates and returns the supplied event, matching PostHog's
|
|
1775
1744
|
* `before_send` contract while preserving the event's full structural type.
|
|
1776
1745
|
*/
|
|
1777
|
-
function sanitizePostHogBrowserEvent(event) {
|
|
1746
|
+
function sanitizePostHogBrowserEvent(event, urlPolicy = "path-only") {
|
|
1778
1747
|
if (event === null) return event;
|
|
1748
|
+
const transportToken = event.properties.token;
|
|
1779
1749
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
1780
|
-
sanitizePostHogBrowserValue(event.properties, seen);
|
|
1781
|
-
if (
|
|
1782
|
-
if (event.$
|
|
1750
|
+
sanitizePostHogBrowserValue(event.properties, seen, urlPolicy);
|
|
1751
|
+
if (transportToken !== void 0) event.properties.token = transportToken;
|
|
1752
|
+
if (event.$set !== void 0) sanitizePostHogBrowserValue(event.$set, seen, urlPolicy);
|
|
1753
|
+
if (event.$set_once !== void 0) sanitizePostHogBrowserValue(event.$set_once, seen, urlPolicy);
|
|
1783
1754
|
return event;
|
|
1784
1755
|
}
|
|
1785
1756
|
/**
|
|
1786
1757
|
* Drop local events before transport. Sanitize and stamp every other browser
|
|
1787
1758
|
* event with `capxul_env` and `producer: "browser"`.
|
|
1788
1759
|
*/
|
|
1789
|
-
function createCapxulBeforeSend(capxulEnv) {
|
|
1760
|
+
function createCapxulBeforeSend(capxulEnv, urlPolicy = "path-only") {
|
|
1790
1761
|
return (event) => {
|
|
1791
1762
|
if (capxulEnv === "local") return null;
|
|
1792
|
-
const sanitized = sanitizePostHogBrowserEvent(event);
|
|
1763
|
+
const sanitized = sanitizePostHogBrowserEvent(event, urlPolicy);
|
|
1793
1764
|
if (sanitized === null) return sanitized;
|
|
1794
1765
|
sanitized.properties.capxul_env = capxulEnv;
|
|
1795
1766
|
sanitized.properties.producer ??= "browser";
|
|
@@ -1820,67 +1791,64 @@ function utilityAppAnalytics(input) {
|
|
|
1820
1791
|
before_send: createCapxulBeforeSend(input.capxulEnv)
|
|
1821
1792
|
};
|
|
1822
1793
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
"x-auth-token"
|
|
1832
|
-
]);
|
|
1833
|
-
/** Any JSON field whose name says "credential", however it is spelled. */
|
|
1834
|
-
const CREDENTIAL_FIELD_NAME = /(?:otp|code|pass(?:word|wd|phrase)?|token|secret|credential|api[_-]?key|private[_-]?key|authorization|cookie|session)/iu;
|
|
1835
|
-
/** Secret material in captured text: bearer tokens, JWTs, keys, and credential fields. */
|
|
1836
|
-
const NETWORK_TEXT_MASKS = [
|
|
1837
|
-
[/\bBearer\s+[^\s,;"]+/giu, "Bearer [REDACTED]"],
|
|
1838
|
-
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[REDACTED]"],
|
|
1839
|
-
[/\b0x[a-fA-F0-9]{40,}\b/gu, "[REDACTED]"],
|
|
1840
|
-
[/\b([A-Za-z_][A-Za-z0-9_.-]*=)[^&\s"]+/gu, "$1[REDACTED]"]
|
|
1841
|
-
];
|
|
1842
|
-
/** `"name": "value"` JSON pairs; the name decides whether the value is masked. */
|
|
1843
|
-
const JSON_STRING_FIELD = /("([^"\\]*)"\s*:\s*")((?:[^"\\]|\\.)*)(")/gu;
|
|
1844
|
-
const QUERY_PAIR = /\b([A-Za-z_][A-Za-z0-9_.-]*=)[^&\s"]+/gu;
|
|
1845
|
-
function maskNetworkText(text) {
|
|
1846
|
-
return NETWORK_TEXT_MASKS.slice(0, 3).reduce((current, [pattern, replacement]) => current.replace(pattern, replacement), text).replace(JSON_STRING_FIELD, (whole, open, name, _value, close) => CREDENTIAL_FIELD_NAME.test(name) ? `${open}[REDACTED]${close}` : whole).replace(QUERY_PAIR, (whole, name) => CREDENTIAL_FIELD_NAME.test(name) ? `${name}[REDACTED]` : whole);
|
|
1794
|
+
/** In unstructured bodies, preserve text around explicitly named credentials. */
|
|
1795
|
+
const NETWORK_FIELD = /((?:"?)([A-Za-z_][A-Za-z0-9_.-]*)(?:"?)\s*[:=]\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s,;&}]+)/gu;
|
|
1796
|
+
function maskNetworkText(text, context) {
|
|
1797
|
+
return redactSecrets(text).replace(NETWORK_FIELD, (whole, prefix, name, value) => {
|
|
1798
|
+
if (!isCredentialField(name, context)) return whole;
|
|
1799
|
+
const quote = value.startsWith("\"") ? "\"" : value.startsWith("'") ? "'" : "";
|
|
1800
|
+
return `${prefix}${quote}[REDACTED]${quote}`;
|
|
1801
|
+
});
|
|
1847
1802
|
}
|
|
1848
1803
|
function maskNetworkHeaders(headers) {
|
|
1849
1804
|
if (headers === void 0) return void 0;
|
|
1850
1805
|
const masked = {};
|
|
1851
|
-
for (const [name, value] of Object.entries(headers)) masked[name] =
|
|
1806
|
+
for (const [name, value] of Object.entries(headers)) masked[name] = isCredentialField(name, "header") ? "[REDACTED]" : /^(?:location|content-location|referer)$/iu.test(name) ? redactUrlSecrets(value) : maskNetworkText(value);
|
|
1852
1807
|
return masked;
|
|
1853
1808
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
if (typeof value === "string") return maskNetworkText(value);
|
|
1809
|
+
function maskJsonValue(value, depth = 0, context) {
|
|
1810
|
+
if (typeof value === "function" || typeof value === "symbol") return "[UNREADABLE]";
|
|
1811
|
+
if (typeof value === "string") return maskNetworkText(value, context);
|
|
1857
1812
|
if (typeof value !== "object" || value === null) return value;
|
|
1858
1813
|
if (depth >= 16) return "[REDACTED]";
|
|
1859
|
-
if (Array.isArray(value)) return value.map((item) => maskJsonValue(item, depth + 1));
|
|
1860
|
-
const masked =
|
|
1861
|
-
for (const [key,
|
|
1814
|
+
if (Array.isArray(value)) return value.map((item) => maskJsonValue(item, depth + 1, context));
|
|
1815
|
+
const masked = Object.create(null);
|
|
1816
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
1817
|
+
if (!descriptor.enumerable) continue;
|
|
1818
|
+
masked[key] = isCredentialField(key, context) ? "[REDACTED]" : !("value" in descriptor) ? "[UNREADABLE]" : typeof descriptor.value === "string" && isPostHogUrlProperty(key) ? redactUrlSecrets(descriptor.value) : maskJsonValue(descriptor.value, depth + 1, context);
|
|
1819
|
+
}
|
|
1862
1820
|
return masked;
|
|
1863
1821
|
}
|
|
1864
|
-
function maskNetworkBody(body) {
|
|
1865
|
-
if (typeof body !== "string") return body;
|
|
1822
|
+
function maskNetworkBody(body, context) {
|
|
1866
1823
|
try {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1824
|
+
if (typeof body !== "string") return maskJsonValue(body, 0, context);
|
|
1825
|
+
let parsed;
|
|
1826
|
+
try {
|
|
1827
|
+
parsed = JSON.parse(body);
|
|
1828
|
+
} catch {
|
|
1829
|
+
if (/^[^=&\s]+=[\s\S]*$/u.test(body)) return new URLSearchParams(Array.from(new URLSearchParams(body), ([name, value]) => [name, isCredentialField(name, context) ? "[REDACTED]" : maskNetworkText(value)])).toString();
|
|
1830
|
+
return maskNetworkText(body, context);
|
|
1831
|
+
}
|
|
1832
|
+
return JSON.stringify(maskJsonValue(parsed, 0, context));
|
|
1833
|
+
} catch {
|
|
1834
|
+
return "[UNREADABLE]";
|
|
1835
|
+
}
|
|
1871
1836
|
}
|
|
1872
|
-
/**
|
|
1873
|
-
* Replay keeps the request line, status, timing, headers, and bodies, so a
|
|
1874
|
-
* failed call carries its evidence. Credential headers and secret material
|
|
1875
|
-
* never leave the browser: bearer tokens, cookies, API keys, and the OTP a
|
|
1876
|
-
* user just typed are masked before the recording is sent.
|
|
1877
|
-
*/
|
|
1837
|
+
/** Keep request evidence while masking credential values at the browser boundary. */
|
|
1878
1838
|
function maskCapturedNetworkRequest(request) {
|
|
1839
|
+
const requestUrl = request.name ?? request.url ?? "";
|
|
1840
|
+
let context;
|
|
1841
|
+
try {
|
|
1842
|
+
const path = new URL(requestUrl, "https://capxul.invalid").pathname;
|
|
1843
|
+
if (/\/(?:auth|oauth|sign-in|signin|login|verify-otp|verify-email)(?:\/|$)/iu.test(path)) context = "authentication";
|
|
1844
|
+
} catch {}
|
|
1879
1845
|
return {
|
|
1880
1846
|
...request,
|
|
1847
|
+
...request.name === void 0 ? {} : { name: redactUrlSecrets(request.name) },
|
|
1848
|
+
...request.url === void 0 ? {} : { url: redactUrlSecrets(request.url) },
|
|
1881
1849
|
requestHeaders: maskNetworkHeaders(request.requestHeaders),
|
|
1882
1850
|
responseHeaders: maskNetworkHeaders(request.responseHeaders),
|
|
1883
|
-
requestBody: maskNetworkBody(request.requestBody),
|
|
1851
|
+
requestBody: maskNetworkBody(request.requestBody, context),
|
|
1884
1852
|
responseBody: maskNetworkBody(request.responseBody)
|
|
1885
1853
|
};
|
|
1886
1854
|
}
|
|
@@ -1910,13 +1878,20 @@ function productAppAnalytics(input) {
|
|
|
1910
1878
|
save_referrer: false,
|
|
1911
1879
|
session_recording: {
|
|
1912
1880
|
maskAllInputs: true,
|
|
1913
|
-
maskInputFn: (text, element) =>
|
|
1881
|
+
maskInputFn: (text, element) => {
|
|
1882
|
+
const autocomplete = element?.getAttribute("autocomplete")?.toLowerCase().split(/\s+/u) ?? [];
|
|
1883
|
+
return element?.getAttribute("type")?.toLowerCase() === "password" || autocomplete.some((value) => [
|
|
1884
|
+
"one-time-code",
|
|
1885
|
+
"current-password",
|
|
1886
|
+
"new-password"
|
|
1887
|
+
].includes(value)) || isCredentialField(element?.getAttribute("name") ?? "") || isCredentialField(element?.getAttribute("id") ?? "") ? "*".repeat(text.length) : maskNetworkText(text);
|
|
1888
|
+
},
|
|
1914
1889
|
captureCanvas: { recordCanvas: false },
|
|
1915
1890
|
recordHeaders: true,
|
|
1916
1891
|
recordBody: true,
|
|
1917
1892
|
maskCapturedNetworkRequestFn: maskCapturedNetworkRequest
|
|
1918
1893
|
},
|
|
1919
|
-
before_send: createCapxulBeforeSend(input.capxulEnv)
|
|
1894
|
+
before_send: createCapxulBeforeSend(input.capxulEnv, "diagnostic")
|
|
1920
1895
|
};
|
|
1921
1896
|
}
|
|
1922
1897
|
/**
|
|
@@ -1973,11 +1948,15 @@ function createPostHogBrowserPageviewTracker(client) {
|
|
|
1973
1948
|
} catch {}
|
|
1974
1949
|
};
|
|
1975
1950
|
}
|
|
1976
|
-
function sanitizePostHogBrowserValue(value, seen) {
|
|
1951
|
+
function sanitizePostHogBrowserValue(value, seen, urlPolicy) {
|
|
1977
1952
|
if (typeof value !== "object" || value === null || seen.has(value)) return;
|
|
1978
1953
|
seen.add(value);
|
|
1979
1954
|
if (Array.isArray(value)) {
|
|
1980
|
-
for (
|
|
1955
|
+
for (let index = 0; index < value.length; index++) {
|
|
1956
|
+
const item = value[index];
|
|
1957
|
+
if (typeof item === "string") value[index] = maskNetworkText(item);
|
|
1958
|
+
else sanitizePostHogBrowserValue(item, seen, urlPolicy);
|
|
1959
|
+
}
|
|
1981
1960
|
return;
|
|
1982
1961
|
}
|
|
1983
1962
|
const properties = value;
|
|
@@ -1986,11 +1965,15 @@ function sanitizePostHogBrowserValue(value, seen) {
|
|
|
1986
1965
|
delete properties[key];
|
|
1987
1966
|
continue;
|
|
1988
1967
|
}
|
|
1989
|
-
if (
|
|
1990
|
-
properties[key] =
|
|
1968
|
+
if (isCredentialField(key)) {
|
|
1969
|
+
properties[key] = "[REDACTED]";
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
if (typeof nestedValue === "string") {
|
|
1973
|
+
properties[key] = isPostHogUrlProperty(key) ? urlPolicy === "path-only" ? stripUrlQueryAndFragment(nestedValue) : redactUrlSecrets(nestedValue) : maskNetworkText(nestedValue);
|
|
1991
1974
|
continue;
|
|
1992
1975
|
}
|
|
1993
|
-
sanitizePostHogBrowserValue(nestedValue, seen);
|
|
1976
|
+
sanitizePostHogBrowserValue(nestedValue, seen, urlPolicy);
|
|
1994
1977
|
}
|
|
1995
1978
|
}
|
|
1996
1979
|
const POSTHOG_QUERY_DERIVED_PROPERTY_NAMES = [
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
//#region ../errors/src/secret-material.ts
|
|
2
|
+
const CREDENTIAL_FIELDS = /* @__PURE__ */ new Set([
|
|
3
|
+
"password",
|
|
4
|
+
"passwd",
|
|
5
|
+
"pwd",
|
|
6
|
+
"pass",
|
|
7
|
+
"passphrase",
|
|
8
|
+
"currentpassword",
|
|
9
|
+
"newpassword",
|
|
10
|
+
"confirmpassword",
|
|
11
|
+
"passwordconfirmation",
|
|
12
|
+
"passwordhash",
|
|
13
|
+
"hashedpassword",
|
|
14
|
+
"pin",
|
|
15
|
+
"privatekey",
|
|
16
|
+
"walletprivatekey",
|
|
17
|
+
"secretkey",
|
|
18
|
+
"signingkey",
|
|
19
|
+
"signingsecret",
|
|
20
|
+
"seed",
|
|
21
|
+
"seedphrase",
|
|
22
|
+
"mnemonic",
|
|
23
|
+
"recoveryphrase",
|
|
24
|
+
"recovery",
|
|
25
|
+
"recoverycode",
|
|
26
|
+
"recoverycodes",
|
|
27
|
+
"keyshare",
|
|
28
|
+
"signingmaterial",
|
|
29
|
+
"recoverymaterial",
|
|
30
|
+
"privatekeybytes",
|
|
31
|
+
"signingprivatekey",
|
|
32
|
+
"encryptionkey",
|
|
33
|
+
"decryptionkey",
|
|
34
|
+
"authtoken",
|
|
35
|
+
"apitoken",
|
|
36
|
+
"bearertoken",
|
|
37
|
+
"oauthtoken",
|
|
38
|
+
"csrftoken",
|
|
39
|
+
"clientassertion",
|
|
40
|
+
"token",
|
|
41
|
+
"accesstoken",
|
|
42
|
+
"refreshtoken",
|
|
43
|
+
"idtoken",
|
|
44
|
+
"sessiontoken",
|
|
45
|
+
"session",
|
|
46
|
+
"sessionkey",
|
|
47
|
+
"sessionsecret",
|
|
48
|
+
"authsessionid",
|
|
49
|
+
"sessioncredential",
|
|
50
|
+
"sessioncredentials",
|
|
51
|
+
"sessioncookie",
|
|
52
|
+
"auth",
|
|
53
|
+
"authentication",
|
|
54
|
+
"authorization",
|
|
55
|
+
"proxyauthorization",
|
|
56
|
+
"cookie",
|
|
57
|
+
"cookies",
|
|
58
|
+
"setcookie",
|
|
59
|
+
"apikey",
|
|
60
|
+
"xapikey",
|
|
61
|
+
"xauthtoken",
|
|
62
|
+
"clientsecret",
|
|
63
|
+
"secret",
|
|
64
|
+
"secrets",
|
|
65
|
+
"credential",
|
|
66
|
+
"credentials",
|
|
67
|
+
"signature",
|
|
68
|
+
"otp",
|
|
69
|
+
"otpcode",
|
|
70
|
+
"totpsecret",
|
|
71
|
+
"onetimecode",
|
|
72
|
+
"onetimepassword",
|
|
73
|
+
"verificationcode",
|
|
74
|
+
"authcode",
|
|
75
|
+
"authorizationcode",
|
|
76
|
+
"oauthcode",
|
|
77
|
+
"codeverifier"
|
|
78
|
+
]);
|
|
79
|
+
/** Match credential fields by meaning, without hiding token addresses or replay IDs. */
|
|
80
|
+
function isCredentialField(name, context) {
|
|
81
|
+
const normalized = name.toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
82
|
+
return CREDENTIAL_FIELDS.has(normalized) || /(?:password(?:hash)?|passwd|passphrase|privatekey|signingkey|apikey|clientsecret|accesstoken|refreshtoken|sessiontoken)$/u.test(normalized) || context === "authentication" && (normalized === "code" || normalized === "sessionid") || context === "header" && /(?:token|secret|apikey|privatekey|signingkey|password|authorization|cookie|session)$/u.test(normalized);
|
|
83
|
+
}
|
|
84
|
+
function decodeUrlComponent(text) {
|
|
85
|
+
try {
|
|
86
|
+
return decodeURIComponent(text);
|
|
87
|
+
} catch {
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Preserve URL diagnostics while removing credentials from their declared components. */
|
|
92
|
+
function redactUrlSecrets(value, source) {
|
|
93
|
+
try {
|
|
94
|
+
const absolute = /^[a-z][a-z0-9+.-]*:/iu.test(value);
|
|
95
|
+
const protocolRelative = value.startsWith("//");
|
|
96
|
+
const url = new URL(value, "https://redaction.invalid");
|
|
97
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return maskText(value);
|
|
98
|
+
let changed = url.username.length > 0 || url.password.length > 0;
|
|
99
|
+
const maskParams = (params) => new URLSearchParams(Array.from(params, ([name, entry]) => {
|
|
100
|
+
const safeName = maskText(name);
|
|
101
|
+
const safeEntry = isCredentialField(name) || /^(?:code|draft|__posthog)$/iu.test(name) ? "[REDACTED]" : maskText(entry);
|
|
102
|
+
changed ||= safeName !== name || safeEntry !== entry;
|
|
103
|
+
return [safeName, safeEntry];
|
|
104
|
+
})).toString();
|
|
105
|
+
const provider = source !== void 0 || /(?:^|\.)(?:alchemy\.com|alchemyapi\.io|infura\.io)$/iu.test(url.hostname);
|
|
106
|
+
const segments = (absolute || protocolRelative ? url.pathname : value.split(/[?#]/u)[0] ?? "").split("/");
|
|
107
|
+
const path = segments.map((segment, index) => {
|
|
108
|
+
const decoded = decodeUrlComponent(segment);
|
|
109
|
+
const masked = provider && /^v[23]$/u.test(decodeUrlComponent(segments[index - 1] ?? "")) && decoded.length > 0 ? "[REDACTED]" : maskText(decoded);
|
|
110
|
+
changed ||= masked !== decoded;
|
|
111
|
+
return masked === decoded ? segment : encodeURIComponent(masked);
|
|
112
|
+
}).join("/");
|
|
113
|
+
const query = maskParams(url.searchParams);
|
|
114
|
+
const fragment = url.hash.slice(1);
|
|
115
|
+
const decodedFragment = decodeUrlComponent(fragment);
|
|
116
|
+
const maskedFragment = maskText(decodedFragment);
|
|
117
|
+
changed ||= maskedFragment !== decodedFragment;
|
|
118
|
+
const fragmentQuery = fragment.indexOf("?");
|
|
119
|
+
const fragmentPath = fragment.slice(0, fragmentQuery);
|
|
120
|
+
const maskedFragmentPath = maskText(decodeUrlComponent(fragmentPath));
|
|
121
|
+
changed ||= maskedFragmentPath !== decodeUrlComponent(fragmentPath);
|
|
122
|
+
const hash = fragmentQuery >= 0 ? `${maskedFragmentPath === decodeUrlComponent(fragmentPath) ? fragmentPath : encodeURIComponent(maskedFragmentPath)}?${maskParams(new URLSearchParams(fragment.slice(fragmentQuery + 1)))}` : fragment.includes("=") ? maskParams(new URLSearchParams(fragment)) : maskedFragment === decodedFragment ? fragment : encodeURIComponent(maskedFragment);
|
|
123
|
+
const origin = absolute ? url.origin : protocolRelative ? `//${url.host}` : "";
|
|
124
|
+
if (!changed) return value;
|
|
125
|
+
return `${origin}${path}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
|
|
126
|
+
} catch {
|
|
127
|
+
return maskText(value.replace(/(https?:\/\/)[^/\s@]+@/giu, "$1[REDACTED]@").replace(/([?&#])([^=&#]+)=([^&#]*)/gu, (whole, separator, name) => isCredentialField(decodeUrlComponent(name)) || /^(?:code|draft|__posthog)$/iu.test(decodeUrlComponent(name)) ? `${separator}${name}=[REDACTED]` : whole));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const JSON_FIELD = /("(?:[^"\\]|\\.)*")(\s*:\s*)("(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/gu;
|
|
131
|
+
const PROSE_FIELD = /(?=(\b([A-Za-z][A-Za-z0-9_-]*(?:[ \t]+[A-Za-z][A-Za-z0-9_-]*){0,3})\s*[:=]\s*((?:Bearer|Basic)[ \t]+[^\s,;"']+|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s,;&"']+)))/gu;
|
|
132
|
+
const EMBEDDED_URL = /(https?:\/\/[^\s"<>]*)/giu;
|
|
133
|
+
function isSecretLabel(name) {
|
|
134
|
+
return isCredentialField(name) || /^request[ _-]?body$/iu.test(name);
|
|
135
|
+
}
|
|
136
|
+
const JSON_ENCODING_MAX = 8;
|
|
137
|
+
const JSON_NESTING_MAX = 64;
|
|
138
|
+
const JSON_KEY = /("(?:[^"\\]|\\.)*")\s*:/gu;
|
|
139
|
+
const JSON_CONTAINER_START = /(?:\{\s*["}]|\[\s*[[\]{}"0-9tfn-])/uy;
|
|
140
|
+
function hasSecretJsonKey(value) {
|
|
141
|
+
for (const match of value.matchAll(JSON_KEY)) try {
|
|
142
|
+
if (match[1] !== void 0 && isSecretLabel(JSON.parse(match[1]))) return true;
|
|
143
|
+
} catch {}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
/** Project a complete fragment once. Never retry all of its nested substrings. */
|
|
147
|
+
function projectJsonFragment(value, encodingDepth, overDepth) {
|
|
148
|
+
if (overDepth || encodingDepth >= JSON_ENCODING_MAX) return "[REDACTED]";
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(value);
|
|
152
|
+
} catch {
|
|
153
|
+
return hasSecretJsonKey(value) ? "[REDACTED]" : value;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
let changed = false;
|
|
157
|
+
const masked = JSON.stringify(parsed, (name, nested) => {
|
|
158
|
+
if (isSecretLabel(name)) {
|
|
159
|
+
changed ||= nested !== "[REDACTED]";
|
|
160
|
+
return "[REDACTED]";
|
|
161
|
+
}
|
|
162
|
+
if (typeof nested === "string") {
|
|
163
|
+
const projected = maskJsonFragments(nested, encodingDepth + 1);
|
|
164
|
+
changed ||= projected !== nested;
|
|
165
|
+
return projected;
|
|
166
|
+
}
|
|
167
|
+
return nested;
|
|
168
|
+
});
|
|
169
|
+
return changed ? masked : value;
|
|
170
|
+
} catch {
|
|
171
|
+
return "[REDACTED]";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Scan balanced object/array fragments once, respecting JSON quotes and escapes. */
|
|
175
|
+
function maskJsonFragments(value, encodingDepth = 0) {
|
|
176
|
+
if (value.trimStart().startsWith("\"")) try {
|
|
177
|
+
const decoded = JSON.parse(value);
|
|
178
|
+
if (typeof decoded === "string") {
|
|
179
|
+
if (encodingDepth >= JSON_ENCODING_MAX) return "\"[REDACTED]\"";
|
|
180
|
+
const masked = maskJsonFragments(decoded, encodingDepth + 1);
|
|
181
|
+
return masked === decoded ? value : JSON.stringify(masked);
|
|
182
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
let output = "";
|
|
185
|
+
let cursor = 0;
|
|
186
|
+
let start = -1;
|
|
187
|
+
let depth = 0;
|
|
188
|
+
let overDepth = false;
|
|
189
|
+
let quoted = false;
|
|
190
|
+
let escaped = false;
|
|
191
|
+
for (let index = 0; index < value.length; index++) {
|
|
192
|
+
const character = value[index];
|
|
193
|
+
if (start < 0) {
|
|
194
|
+
JSON_CONTAINER_START.lastIndex = index;
|
|
195
|
+
if (!JSON_CONTAINER_START.test(value)) continue;
|
|
196
|
+
start = index;
|
|
197
|
+
depth = 1;
|
|
198
|
+
overDepth = false;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (quoted) {
|
|
202
|
+
if (escaped) escaped = false;
|
|
203
|
+
else if (character === "\\") escaped = true;
|
|
204
|
+
else if (character === "\"") quoted = false;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (character === "\"") quoted = true;
|
|
208
|
+
else if (character === "{" || character === "[") {
|
|
209
|
+
depth++;
|
|
210
|
+
overDepth ||= depth > JSON_NESTING_MAX;
|
|
211
|
+
} else if (character === "}" || character === "]") {
|
|
212
|
+
depth--;
|
|
213
|
+
if (depth !== 0) continue;
|
|
214
|
+
output += value.slice(cursor, start) + projectJsonFragment(value.slice(start, index + 1), encodingDepth, overDepth);
|
|
215
|
+
cursor = index + 1;
|
|
216
|
+
start = -1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (start >= 0) {
|
|
220
|
+
const fragment = value.slice(start);
|
|
221
|
+
output += value.slice(cursor, start) + (overDepth || hasSecretJsonKey(fragment) ? "[REDACTED]" : fragment);
|
|
222
|
+
cursor = value.length;
|
|
223
|
+
}
|
|
224
|
+
return output + value.slice(cursor);
|
|
225
|
+
}
|
|
226
|
+
function maskLabelledText(value) {
|
|
227
|
+
const jsonMasked = maskJsonFragments(value).replace(JSON_FIELD, (whole, quotedName, separator) => {
|
|
228
|
+
try {
|
|
229
|
+
return isSecretLabel(JSON.parse(quotedName)) ? `${quotedName}${separator}"[REDACTED]"` : whole;
|
|
230
|
+
} catch {
|
|
231
|
+
return whole;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
let output = "";
|
|
235
|
+
let cursor = 0;
|
|
236
|
+
for (const match of jsonMasked.matchAll(PROSE_FIELD)) {
|
|
237
|
+
const [, whole, label, entry] = match;
|
|
238
|
+
if (whole === void 0 || label === void 0 || entry === void 0 || match.index < cursor || /^Bearer[ \t]+/iu.test(entry)) continue;
|
|
239
|
+
const words = label.split(/[ \t]+/u);
|
|
240
|
+
for (let index = 0; index < words.length; index++) {
|
|
241
|
+
const name = words.slice(index).join(" ");
|
|
242
|
+
if (isSecretLabel(name)) {
|
|
243
|
+
const prefix = words.slice(0, index).join(" ");
|
|
244
|
+
output += jsonMasked.slice(cursor, match.index) + `${prefix ? `${prefix} ` : ""}${name}=[REDACTED]`;
|
|
245
|
+
cursor = match.index + whole.length;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return output + jsonMasked.slice(cursor);
|
|
251
|
+
}
|
|
252
|
+
const TOKEN_MASKS = [
|
|
253
|
+
[/\bBearer\s+(?!(?:realm|error|error_description|scope|authorization_uri|resource|claims)\s*=)[^\s,;"']+/giu, "Bearer [REDACTED]"],
|
|
254
|
+
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[REDACTED]"],
|
|
255
|
+
[/(^|[^a-fA-F0-9])(?<!0[xX])[a-fA-F0-9]{64}(?=$|[^a-fA-F0-9])/gu, "$1[REDACTED]"],
|
|
256
|
+
[/(?:(?:sk|pk|rk)_(?:live|test)|(?:phc|phx|ghp|gho|ghu|ghs|ghr|whsec)_|github_pat_|xox[baprs]-|AKIA[0-9A-Z]{16}|AIza)[A-Za-z0-9_-]*/giu, "[REDACTED]"]
|
|
257
|
+
];
|
|
258
|
+
/** URL components and malformed URLs use this non-recursive masking layer. */
|
|
259
|
+
function maskText(value) {
|
|
260
|
+
let text = maskLabelledText(value);
|
|
261
|
+
for (const [pattern, replacement] of TOKEN_MASKS) text = text.replace(pattern, replacement);
|
|
262
|
+
return text;
|
|
263
|
+
}
|
|
264
|
+
/** Mask credential fields and embedded URLs while preserving surrounding evidence. */
|
|
265
|
+
function redactSecrets(value, source) {
|
|
266
|
+
return maskLabelledText(value).split(EMBEDDED_URL).map((part, index) => index % 2 === 1 ? redactUrlSecrets(part, source).replaceAll("%5BREDACTED%5D", "[REDACTED]") : maskText(part)).join("");
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region ../errors/src/errors.ts
|
|
270
|
+
const CAPXUL_ERROR_CODES = [
|
|
271
|
+
"NOT_AUTHENTICATED",
|
|
272
|
+
"EMAIL_DELIVERY_FAILED",
|
|
273
|
+
"PROFILE_NOT_FOUND",
|
|
274
|
+
"SMART_ACCOUNT_MISSING",
|
|
275
|
+
"PLAYER_NOT_FOUND",
|
|
276
|
+
"ACCOUNT_NOT_FOUND",
|
|
277
|
+
"ACCOUNT_CLAIMED_BY_OTHER_SIGNER",
|
|
278
|
+
"PROVIDER_ERROR",
|
|
279
|
+
"CAPABILITY_UNAVAILABLE",
|
|
280
|
+
"INVALID_INPUT",
|
|
281
|
+
"ENV_MISSING",
|
|
282
|
+
"NOT_IMPLEMENTED",
|
|
283
|
+
"VERIFICATION_REQUIRED",
|
|
284
|
+
"INSUFFICIENT_BALANCE",
|
|
285
|
+
"INVALID_RECIPIENT",
|
|
286
|
+
"ROLE_PERMISSION_DENIED",
|
|
287
|
+
"TRANSACTION_FAILED",
|
|
288
|
+
"RATE_LIMITED",
|
|
289
|
+
"NETWORK_ERROR",
|
|
290
|
+
"UNKNOWN",
|
|
291
|
+
"OTP_EXPIRED",
|
|
292
|
+
"SIGNER_REJECTED",
|
|
293
|
+
"CANCELLED",
|
|
294
|
+
"WRONG_STATE",
|
|
295
|
+
"STALE_EPOCH",
|
|
296
|
+
"SUPERSEDED",
|
|
297
|
+
"WORK_DIED",
|
|
298
|
+
"ACTOR_STOPPED"
|
|
299
|
+
];
|
|
300
|
+
new Set(CAPXUL_ERROR_CODES);
|
|
301
|
+
//#endregion
|
|
302
|
+
export { redactUrlSecrets as i, isCredentialField as n, redactSecrets as r, CAPXUL_ERROR_CODES as t };
|