@webblackbox/recorder
Advanced tools
+259
-11
@@ -141,3 +141,6 @@ // src/action-span.ts | ||
| // src/normalizer.ts | ||
| import { extractRequestIdFromPayload } from "@webblackbox/protocol"; | ||
| import { | ||
| extractRequestIdFromPayload, | ||
| sanitizeUrlForPrivacy | ||
| } from "@webblackbox/protocol"; | ||
| var CDP_EVENT_MAP = { | ||
@@ -181,2 +184,3 @@ "Network.requestWillBeSent": "network.request", | ||
| vitals: "perf.vitals", | ||
| privacyViolation: "privacy.violation", | ||
| localStorageOp: "storage.local.op", | ||
@@ -307,3 +311,3 @@ localStorageSnapshot: "storage.local.snapshot", | ||
| const method = (asString(payload?.method) ?? "GET").toUpperCase(); | ||
| const url = asString(payload?.url) ?? "unknown://request"; | ||
| const url = sanitizeUrlForPrivacy(asString(payload?.url) ?? "unknown://request"); | ||
| const reqId = readRequestId(payload) ?? buildFallbackReqId(method, url); | ||
@@ -322,3 +326,4 @@ return stripUndefined({ | ||
| const url = asString(payload?.url); | ||
| const reqId = readRequestId(payload) ?? buildFallbackReqId(method ?? "GET", url ?? "unknown://request"); | ||
| const sanitizedUrl = url ? sanitizeUrlForPrivacy(url) : void 0; | ||
| const reqId = readRequestId(payload) ?? buildFallbackReqId(method ?? "GET", sanitizedUrl ?? "unknown://request"); | ||
| return stripUndefined({ | ||
@@ -328,3 +333,3 @@ reqId, | ||
| method: method ? method.toUpperCase() : void 0, | ||
| url, | ||
| url: sanitizedUrl, | ||
| status: asFiniteNumber(payload?.status) ?? void 0, | ||
@@ -338,3 +343,3 @@ statusText: asString(payload?.statusText) ?? void 0, | ||
| redirected: asBoolean(payload?.redirected), | ||
| responseUrl: asString(payload?.responseUrl) ?? void 0, | ||
| responseUrl: sanitizeOptionalUrl(asString(payload?.responseUrl)), | ||
| failed: asBoolean(payload?.failed) | ||
@@ -346,3 +351,4 @@ }); | ||
| const url = asString(payload?.url); | ||
| const reqId = readRequestId(payload) ?? buildFallbackReqId(method ?? "GET", url ?? "unknown://request"); | ||
| const sanitizedUrl = url ? sanitizeUrlForPrivacy(url) : void 0; | ||
| const reqId = readRequestId(payload) ?? buildFallbackReqId(method ?? "GET", sanitizedUrl ?? "unknown://request"); | ||
| return stripUndefined({ | ||
@@ -352,3 +358,3 @@ reqId, | ||
| method: method ? method.toUpperCase() : void 0, | ||
| url, | ||
| url: sanitizedUrl, | ||
| duration: asFiniteNumber(payload?.duration) ?? void 0, | ||
@@ -360,3 +366,3 @@ message: asString(payload?.message) ?? void 0, | ||
| function normalizeContentNetworkBodyPayload(payload) { | ||
| const reqId = readRequestId(payload) ?? buildFallbackReqId("GET", asString(payload?.url) ?? "unknown://request"); | ||
| const reqId = readRequestId(payload) ?? buildFallbackReqId("GET", sanitizeOptionalUrl(asString(payload?.url)) ?? "unknown://request"); | ||
| return stripUndefined({ | ||
@@ -478,3 +484,3 @@ reqId, | ||
| } | ||
| const url = asString(frame.url) ?? "(anonymous)"; | ||
| const url = sanitizeOptionalUrl(asString(frame.url)) ?? "(anonymous)"; | ||
| const line = asFiniteNumber(frame.lineNumber); | ||
@@ -535,2 +541,5 @@ const col = asFiniteNumber(frame.columnNumber); | ||
| } | ||
| function sanitizeOptionalUrl(value) { | ||
| return value ? sanitizeUrlForPrivacy(value) : void 0; | ||
| } | ||
| function compactText(value, maxLength) { | ||
@@ -558,2 +567,5 @@ return value.length > maxLength ? `${value.slice(0, Math.max(0, maxLength - 3))}...` : value; | ||
| } | ||
| if (rawType === "privacyViolation") { | ||
| return "privacy.violation"; | ||
| } | ||
| if (rawType === "cdp.network.body") { | ||
@@ -763,2 +775,3 @@ return "network.body"; | ||
| import { | ||
| DEFAULT_CAPTURE_POLICY, | ||
| EventIdFactory | ||
@@ -768,2 +781,3 @@ } from "@webblackbox/protocol"; | ||
| // src/redaction.ts | ||
| import { sanitizeUrlForPrivacy as sanitizeUrlForPrivacy2 } from "@webblackbox/protocol"; | ||
| var REDACTED = "[REDACTED]"; | ||
@@ -848,2 +862,10 @@ var SHA_256_K = [ | ||
| const normalizedKey = key.toLowerCase(); | ||
| if (isUrlLikeField(normalizedKey) && typeof value === "string") { | ||
| output[key] = sanitizeUrlForPrivacy2(value); | ||
| continue; | ||
| } | ||
| if (normalizedKey === "selector" && typeof value === "string") { | ||
| output[key] = profile.hashSensitiveValues ? `selector:${hashValue(value).slice(0, 12)}` : "[REDACTED_SELECTOR]"; | ||
| continue; | ||
| } | ||
| if (profile.redactHeaders.includes(normalizedKey) || isSensitiveKey(normalizedKey, profile)) { | ||
@@ -991,2 +1013,5 @@ output[key] = profile.hashSensitiveValues && typeof value === "string" ? hashValue(value) : REDACTED; | ||
| } | ||
| function isUrlLikeField(key) { | ||
| return key === "url" || key === "href" || key === "responseurl" || key === "documenturl" || key === "requesturl" || key.endsWith("url"); | ||
| } | ||
| function containsSensitivePattern(value, profile) { | ||
@@ -1152,2 +1177,13 @@ const lowered = value.toLowerCase(); | ||
| const redactedPayload = redactPayload(normalized.payload, this.config.redaction); | ||
| const privacy = classifyPrivacy( | ||
| normalized.eventType, | ||
| redactedPayload, | ||
| this.config.capturePolicy | ||
| ); | ||
| const violation = evaluateCapturePolicy( | ||
| normalized.eventType, | ||
| redactedPayload, | ||
| privacy, | ||
| this.config.capturePolicy | ||
| ); | ||
| const event = { | ||
@@ -1163,5 +1199,10 @@ v: 1, | ||
| mono: nextRawEvent.mono, | ||
| type: normalized.eventType, | ||
| type: violation ? "privacy.violation" : normalized.eventType, | ||
| id: this.idFactory.next(), | ||
| data: redactedPayload | ||
| privacy: violation ? { | ||
| category: "system", | ||
| sensitivity: "medium", | ||
| redacted: true | ||
| } : privacy, | ||
| data: violation ?? redactedPayload | ||
| }; | ||
@@ -1240,2 +1281,209 @@ const actionLinkedEvent = this.actionSpanTracker.assign(event); | ||
| } | ||
| function classifyPrivacy(eventType, payload, policy) { | ||
| const effectivePolicy = policy ?? DEFAULT_CAPTURE_POLICY; | ||
| const category = classifyCategory(eventType); | ||
| const sensitivity = classifySensitivity(eventType); | ||
| return { | ||
| category, | ||
| sensitivity, | ||
| redacted: isRedactedByPolicy(eventType, category, effectivePolicy) || hasRedactionSignal(payload) | ||
| }; | ||
| } | ||
| function evaluateCapturePolicy(eventType, payload, privacy, policy) { | ||
| if (eventType === "privacy.violation") { | ||
| return null; | ||
| } | ||
| if (!policy) { | ||
| return createPrivacyViolation(eventType, privacy, "missing-capture-policy", "missing"); | ||
| } | ||
| const reason = findPolicyViolationReason(eventType, payload, policy); | ||
| if (!reason) { | ||
| return null; | ||
| } | ||
| return createPrivacyViolation(eventType, privacy, reason, policy.mode); | ||
| } | ||
| function createPrivacyViolation(eventType, privacy, reason, policyMode) { | ||
| return { | ||
| blockedType: eventType, | ||
| category: privacy.category, | ||
| sensitivity: privacy.sensitivity, | ||
| reason, | ||
| policyMode, | ||
| redacted: true | ||
| }; | ||
| } | ||
| function findPolicyViolationReason(eventType, payload, policy) { | ||
| if (eventType === "screen.screenshot" && policy.categories.screenshots === "off") { | ||
| return "screenshots-disabled"; | ||
| } | ||
| if (eventType === "network.body" && policy.categories.network !== "body-allowlist") { | ||
| return "network-body-disabled"; | ||
| } | ||
| if (eventType === "dom.snapshot") { | ||
| if (policy.categories.dom === "off") { | ||
| return "dom-disabled"; | ||
| } | ||
| if (policy.categories.dom !== "allow" && hasBlobReference(payload)) { | ||
| return "dom-raw-snapshot-disabled"; | ||
| } | ||
| } | ||
| if (eventType.startsWith("dom.") && policy.categories.dom === "off") { | ||
| return "dom-disabled"; | ||
| } | ||
| if (eventType === "user.input") { | ||
| if (policy.categories.inputs === "none") { | ||
| return "inputs-disabled"; | ||
| } | ||
| if (policy.categories.inputs === "length-only" && hasRawInputValue(payload)) { | ||
| return "raw-input-value-disabled"; | ||
| } | ||
| } | ||
| if (eventType.startsWith("console.") || eventType.startsWith("error.")) { | ||
| if (policy.categories.console === "off") { | ||
| return "console-disabled"; | ||
| } | ||
| if (policy.categories.console === "metadata" && hasConsoleTextPayload(payload)) { | ||
| return "console-payload-disabled"; | ||
| } | ||
| } | ||
| if (eventType.startsWith("storage.")) { | ||
| if (isStorageDisabledByPolicy(eventType, policy)) { | ||
| return "storage-disabled"; | ||
| } | ||
| if (isStorageCountsOnlyByPolicy(eventType, policy) && hasStorageDetail(payload)) { | ||
| return "storage-detail-disabled"; | ||
| } | ||
| } | ||
| if (eventType === "perf.heap.snapshot" && (policy.categories.heapProfiles !== "lab-only" || policy.mode !== "lab")) { | ||
| return "heap-profile-disabled"; | ||
| } | ||
| if ((eventType === "perf.trace" || eventType === "perf.cpu.profile") && policy.categories.cdp !== "full") { | ||
| return "cdp-profile-disabled"; | ||
| } | ||
| return null; | ||
| } | ||
| function classifyCategory(eventType) { | ||
| if (eventType === "user.input") { | ||
| return "inputs"; | ||
| } | ||
| if (eventType.startsWith("user.")) { | ||
| return "actions"; | ||
| } | ||
| if (eventType.startsWith("dom.")) { | ||
| return "dom"; | ||
| } | ||
| if (eventType.startsWith("screen.")) { | ||
| return "screenshots"; | ||
| } | ||
| if (eventType.startsWith("console.") || eventType.startsWith("error.")) { | ||
| return "console"; | ||
| } | ||
| if (eventType.startsWith("network.")) { | ||
| return "network"; | ||
| } | ||
| if (eventType.startsWith("storage.")) { | ||
| return "storage"; | ||
| } | ||
| if (eventType.startsWith("perf.")) { | ||
| return "performance"; | ||
| } | ||
| return "system"; | ||
| } | ||
| function classifySensitivity(eventType) { | ||
| if (eventType === "privacy.violation") { | ||
| return "medium"; | ||
| } | ||
| if (eventType === "user.input" || eventType === "dom.snapshot" || eventType === "network.body" || eventType === "screen.screenshot" || eventType.startsWith("storage.")) { | ||
| return "high"; | ||
| } | ||
| if (eventType.startsWith("dom.") || eventType.startsWith("console.") || eventType.startsWith("error.") || eventType.startsWith("network.")) { | ||
| return "medium"; | ||
| } | ||
| return "low"; | ||
| } | ||
| function isRedactedByPolicy(eventType, category, policy) { | ||
| switch (category) { | ||
| case "actions": | ||
| return policy.categories.actions !== "allow"; | ||
| case "inputs": | ||
| return policy.categories.inputs !== "allow"; | ||
| case "dom": | ||
| return policy.categories.dom !== "allow"; | ||
| case "screenshots": | ||
| return policy.categories.screenshots !== "allow"; | ||
| case "console": | ||
| return policy.categories.console !== "allow"; | ||
| case "network": | ||
| return eventType === "network.body" ? policy.categories.network !== "body-allowlist" : policy.categories.network === "metadata"; | ||
| case "storage": | ||
| if (eventType.startsWith("storage.cookie.")) { | ||
| return policy.categories.cookies !== "names-only"; | ||
| } | ||
| if (eventType.startsWith("storage.idb.")) { | ||
| return policy.categories.indexedDb !== "names-only"; | ||
| } | ||
| return policy.categories.storage !== "allow"; | ||
| case "performance": | ||
| case "system": | ||
| return false; | ||
| } | ||
| } | ||
| function isStorageDisabledByPolicy(eventType, policy) { | ||
| if (eventType.startsWith("storage.cookie.")) { | ||
| return policy.categories.cookies === "off"; | ||
| } | ||
| if (eventType.startsWith("storage.idb.")) { | ||
| return policy.categories.indexedDb === "off"; | ||
| } | ||
| return policy.categories.storage === "off"; | ||
| } | ||
| function isStorageCountsOnlyByPolicy(eventType, policy) { | ||
| if (eventType.startsWith("storage.cookie.")) { | ||
| return policy.categories.cookies === "count-only"; | ||
| } | ||
| if (eventType.startsWith("storage.idb.")) { | ||
| return policy.categories.indexedDb === "counts-only"; | ||
| } | ||
| return policy.categories.storage === "counts-only"; | ||
| } | ||
| function hasBlobReference(payload) { | ||
| const row = asRecord3(payload); | ||
| if (!row) { | ||
| return false; | ||
| } | ||
| return typeof row.contentHash === "string" || typeof row.hash === "string"; | ||
| } | ||
| function hasRawInputValue(payload) { | ||
| const row = asRecord3(payload); | ||
| if (!row) { | ||
| return false; | ||
| } | ||
| return typeof row.value === "string" || typeof row.text === "string"; | ||
| } | ||
| function hasConsoleTextPayload(payload) { | ||
| const row = asRecord3(payload); | ||
| if (!row) { | ||
| return false; | ||
| } | ||
| const args = row.args; | ||
| return typeof row.text === "string" && row.text.length > 0 || typeof row.message === "string" && row.message.length > 0 || typeof row.stack === "string" && row.stack.length > 0 || Array.isArray(args) && args.length > 0; | ||
| } | ||
| function hasStorageDetail(payload) { | ||
| const row = asRecord3(payload); | ||
| if (!row) { | ||
| return false; | ||
| } | ||
| return hasBlobReference(row) || typeof row.key === "string" || Array.isArray(row.names) || Array.isArray(row.databaseNames) || asRecord3(row.entries) !== null; | ||
| } | ||
| function hasRedactionSignal(payload) { | ||
| const row = asRecord3(payload); | ||
| if (!row) { | ||
| return false; | ||
| } | ||
| const target = row.target; | ||
| return row.redacted === true || row.valueRedacted === true || row.selectorRedacted === true || target !== null && typeof target === "object" && !Array.isArray(target) && target.selectorRedacted === true; | ||
| } | ||
| function asRecord3(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null; | ||
| } | ||
| export { | ||
@@ -1242,0 +1490,0 @@ ActionSpanTracker, |
+2
-2
| { | ||
| "name": "@webblackbox/recorder", | ||
| "description": "Browser event recorder and normalization engine for WebBlackbox capture pipelines.", | ||
| "version": "0.4.5", | ||
| "version": "0.5.0", | ||
| "type": "module", | ||
@@ -41,3 +41,3 @@ "main": "./dist/index.js", | ||
| "dependencies": { | ||
| "@webblackbox/protocol": "0.4.5" | ||
| "@webblackbox/protocol": "0.5.0" | ||
| }, | ||
@@ -44,0 +44,0 @@ "scripts": { |
63891
15.67%1578
18.65%+ Added
- Removed
Updated