@webblackbox/protocol
Advanced tools
+354
-37
@@ -45,2 +45,3 @@ // src/blob.ts | ||
| "meta.config", | ||
| "privacy.violation", | ||
| "sys.debugger.attach", | ||
@@ -117,2 +118,79 @@ "sys.debugger.detach", | ||
| // src/defaults.ts | ||
| var DEFAULT_REDACTION_PROFILE = { | ||
| redactHeaders: [ | ||
| "authorization", | ||
| "cookie", | ||
| "set-cookie", | ||
| "proxy-authorization", | ||
| "x-api-key", | ||
| "x-auth-token", | ||
| "x-csrf-token", | ||
| "x-xsrf-token" | ||
| ], | ||
| redactCookieNames: ["token", "session", "auth", "jwt", "refresh_token", "csrf", "xsrf"], | ||
| redactBodyPatterns: [ | ||
| "password", | ||
| "token", | ||
| "secret", | ||
| "otp", | ||
| "credential", | ||
| "api_key", | ||
| "apikey", | ||
| "private_key", | ||
| "refresh_token" | ||
| ], | ||
| blockedSelectors: [ | ||
| ".secret", | ||
| "[data-sensitive]", | ||
| "[data-webblackbox-redact]", | ||
| "input[type='password']", | ||
| "input[name*='token']", | ||
| "input[name*='secret']", | ||
| "input[autocomplete='cc-number']" | ||
| ], | ||
| hashSensitiveValues: true | ||
| }; | ||
| var DEFAULT_CAPTURE_POLICY = { | ||
| schemaVersion: 2, | ||
| mode: "private", | ||
| captureContext: "real-user", | ||
| consent: { | ||
| id: "default-private-consent", | ||
| provenance: "self-recording", | ||
| purpose: "debugging", | ||
| grantedAt: "1970-01-01T00:00:00.000Z" | ||
| }, | ||
| unmaskPolicySource: "none", | ||
| scope: { | ||
| tabId: 0, | ||
| origin: "", | ||
| allowedOrigins: [], | ||
| deniedOrigins: [], | ||
| includeSubframes: false, | ||
| stopOnOriginChange: true, | ||
| excludedUrlPatterns: [] | ||
| }, | ||
| categories: { | ||
| actions: "metadata", | ||
| inputs: "length-only", | ||
| dom: "masked", | ||
| screenshots: "off", | ||
| console: "metadata", | ||
| network: "metadata", | ||
| storage: "counts-only", | ||
| indexedDb: "counts-only", | ||
| cookies: "count-only", | ||
| cdp: "off", | ||
| heapProfiles: "off" | ||
| }, | ||
| redaction: DEFAULT_REDACTION_PROFILE, | ||
| encryption: { | ||
| localAtRest: "required", | ||
| archive: "required", | ||
| archiveKeyEnvelope: "passphrase" | ||
| }, | ||
| retention: { | ||
| localTtlMs: 24 * 60 * 60 * 1e3 | ||
| } | ||
| }; | ||
| var DEFAULT_RECORDER_CONFIG = { | ||
@@ -128,45 +206,13 @@ mode: "lite", | ||
| domFlushMs: 100, | ||
| screenshotIdleMs: 8e3, | ||
| screenshotIdleMs: 0, | ||
| snapshotIntervalMs: 2e4, | ||
| actionWindowMs: 1500, | ||
| bodyCaptureMaxBytes: 262144 | ||
| bodyCaptureMaxBytes: 0 | ||
| }, | ||
| redaction: { | ||
| redactHeaders: [ | ||
| "authorization", | ||
| "cookie", | ||
| "set-cookie", | ||
| "proxy-authorization", | ||
| "x-api-key", | ||
| "x-auth-token", | ||
| "x-csrf-token", | ||
| "x-xsrf-token" | ||
| ], | ||
| redactCookieNames: ["token", "session", "auth", "jwt", "refresh_token", "csrf", "xsrf"], | ||
| redactBodyPatterns: [ | ||
| "password", | ||
| "token", | ||
| "secret", | ||
| "otp", | ||
| "credential", | ||
| "api_key", | ||
| "apikey", | ||
| "private_key", | ||
| "refresh_token" | ||
| ], | ||
| blockedSelectors: [ | ||
| ".secret", | ||
| "[data-sensitive]", | ||
| "[data-webblackbox-redact]", | ||
| "input[type='password']", | ||
| "input[name*='token']", | ||
| "input[name*='secret']", | ||
| "input[autocomplete='cc-number']" | ||
| ], | ||
| hashSensitiveValues: true | ||
| }, | ||
| redaction: DEFAULT_REDACTION_PROFILE, | ||
| capturePolicy: DEFAULT_CAPTURE_POLICY, | ||
| sitePolicies: [] | ||
| }; | ||
| var DEFAULT_EXPORT_POLICY = { | ||
| includeScreenshots: true, | ||
| includeScreenshots: false, | ||
| maxArchiveBytes: 100 * 1024 * 1024, | ||
@@ -229,2 +275,17 @@ recentWindowMs: 20 * 60 * 1e3 | ||
| }).strict(); | ||
| var privacyClassificationSchema = z.object({ | ||
| category: z.enum([ | ||
| "actions", | ||
| "inputs", | ||
| "dom", | ||
| "screenshots", | ||
| "console", | ||
| "network", | ||
| "storage", | ||
| "performance", | ||
| "system" | ||
| ]), | ||
| sensitivity: z.enum(["low", "medium", "high"]), | ||
| redacted: z.boolean() | ||
| }).strict(); | ||
| var eventEnvelopeSchema = z.object({ | ||
@@ -245,2 +306,3 @@ v: z.literal(WEBBLACKBOX_PROTOCOL_VERSION), | ||
| ref: eventReferenceSchema.optional(), | ||
| privacy: privacyClassificationSchema.optional(), | ||
| data: z.unknown() | ||
@@ -273,2 +335,58 @@ }).strict(); | ||
| }).strict(); | ||
| var captureConsentSchema = z.object({ | ||
| id: z.string().min(1), | ||
| provenance: z.enum(["self-recording", "support-assisted", "enterprise-admin-policy"]), | ||
| purpose: z.enum(["debugging", "support", "qa", "incident-response", "other"]), | ||
| grantedBy: z.string().min(1).optional(), | ||
| grantedAt: z.string().datetime(), | ||
| expiresAt: z.string().datetime().optional(), | ||
| revocationRef: z.string().min(1).optional() | ||
| }).strict(); | ||
| var captureScopeSchema = z.object({ | ||
| tabId: z.number().int().nonnegative(), | ||
| origin: z.string(), | ||
| allowedOrigins: stringArray, | ||
| deniedOrigins: stringArray, | ||
| includeSubframes: z.boolean(), | ||
| stopOnOriginChange: z.boolean(), | ||
| excludedUrlPatterns: stringArray | ||
| }).strict(); | ||
| var capturePolicySchema = z.object({ | ||
| schemaVersion: z.literal(2), | ||
| mode: z.enum(["private", "debug", "lab"]), | ||
| captureContext: z.enum(["real-user", "synthetic", "local-debug"]), | ||
| captureContextEvidenceRef: z.string().min(1).optional(), | ||
| consent: captureConsentSchema, | ||
| unmaskPolicySource: z.enum(["none", "extension-managed", "enterprise", "signed-site-owner"]), | ||
| scope: captureScopeSchema, | ||
| categories: z.object({ | ||
| actions: z.enum(["metadata", "masked", "allow"]), | ||
| inputs: z.enum(["none", "length-only", "masked", "allow"]), | ||
| dom: z.enum(["off", "wireframe", "masked", "allow"]), | ||
| screenshots: z.enum(["off", "masked", "allow"]), | ||
| console: z.enum(["off", "metadata", "sanitized", "allow"]), | ||
| network: z.enum(["metadata", "headers-allowlist", "body-allowlist"]), | ||
| storage: z.enum(["off", "counts-only", "names-only", "lengths-only", "allow"]), | ||
| indexedDb: z.enum(["off", "counts-only", "names-only"]), | ||
| cookies: z.enum(["off", "count-only", "names-only"]), | ||
| cdp: z.enum(["off", "safe-subset", "full"]), | ||
| heapProfiles: z.enum(["off", "lab-only"]) | ||
| }).strict(), | ||
| redaction: redactionProfileSchema, | ||
| encryption: z.object({ | ||
| localAtRest: z.literal("required"), | ||
| archive: z.enum(["required", "synthetic-local-debug-exempt", "explicit-low-risk-override"]), | ||
| archiveKeyEnvelope: z.enum([ | ||
| "passphrase", | ||
| "enterprise-managed", | ||
| "client-side-share-fragment", | ||
| "none" | ||
| ]), | ||
| overrideReasonRef: z.string().min(1).optional() | ||
| }).strict(), | ||
| retention: z.object({ | ||
| localTtlMs: z.number().int().positive(), | ||
| shareTtlMs: z.number().int().positive().optional() | ||
| }).strict() | ||
| }).strict(); | ||
| var recorderConfigSchema = z.object({ | ||
@@ -282,2 +400,3 @@ mode: captureModeSchema, | ||
| redaction: redactionProfileSchema, | ||
| capturePolicy: capturePolicySchema.optional(), | ||
| sitePolicies: z.array(siteCapturePolicySchema) | ||
@@ -341,2 +460,73 @@ }).strict(); | ||
| }).strict(); | ||
| var privacyScannerFindingSchema = z.object({ | ||
| kind: z.enum([ | ||
| "jwt", | ||
| "bearer-token", | ||
| "api-key", | ||
| "oauth-code", | ||
| "session-cookie", | ||
| "email", | ||
| "phone", | ||
| "credit-card", | ||
| "ssn", | ||
| "private-key", | ||
| "long-secret" | ||
| ]), | ||
| severity: z.literal("high"), | ||
| path: z.string().min(1), | ||
| matchCount: z.number().int().positive(), | ||
| sampleSha256: z.string().regex(/^[a-f0-9]{64}$/) | ||
| }).strict(); | ||
| var privacyScannerResultSchema = z.object({ | ||
| scannedAt: z.string().datetime(), | ||
| preEncryption: z.boolean(), | ||
| status: z.enum(["passed", "blocked"]), | ||
| findings: z.array(privacyScannerFindingSchema) | ||
| }).strict(); | ||
| var privacyManifestCategorySummarySchema = z.object({ | ||
| category: privacyClassificationSchema.shape.category, | ||
| events: z.number().int().nonnegative(), | ||
| low: z.number().int().nonnegative(), | ||
| medium: z.number().int().nonnegative(), | ||
| high: z.number().int().nonnegative(), | ||
| redacted: z.number().int().nonnegative(), | ||
| unredacted: z.number().int().nonnegative() | ||
| }).strict(); | ||
| var privacyManifestSchema = z.object({ | ||
| schemaVersion: z.literal(1), | ||
| generatedAt: z.string().datetime(), | ||
| effectivePolicy: capturePolicySchema.optional(), | ||
| consent: captureConsentSchema.optional(), | ||
| transfer: z.object({ | ||
| destination: z.enum([ | ||
| "local-download", | ||
| "public-cloud-share", | ||
| "support-upload", | ||
| "enterprise-upload" | ||
| ]), | ||
| archiveKeyEnvelope: z.enum([ | ||
| "passphrase", | ||
| "enterprise-managed", | ||
| "client-side-share-fragment", | ||
| "none" | ||
| ]), | ||
| encrypted: z.boolean(), | ||
| includeScreenshots: z.boolean(), | ||
| maxArchiveBytes: z.number().int().positive().nullable(), | ||
| recentWindowMs: z.number().int().positive().nullable(), | ||
| shareEligible: z.boolean(), | ||
| computedAt: z.string().datetime() | ||
| }).strict().optional(), | ||
| categories: z.array(privacyManifestCategorySummarySchema), | ||
| scanner: privacyScannerResultSchema, | ||
| encryption: z.object({ | ||
| archive: z.enum(["encrypted", "plaintext"]), | ||
| algorithm: z.literal("AES-GCM").optional() | ||
| }).strict(), | ||
| totals: z.object({ | ||
| events: z.number().int().nonnegative(), | ||
| blobs: z.number().int().nonnegative(), | ||
| privacyViolations: z.number().int().nonnegative() | ||
| }).strict() | ||
| }).strict(); | ||
| var exportManifestSchema = z.object({ | ||
@@ -608,7 +798,124 @@ protocolVersion: z.literal(WEBBLACKBOX_PROTOCOL_VERSION), | ||
| } | ||
| // src/privacy.ts | ||
| var ABSOLUTE_URL_PATTERN = /^[a-z][a-z\d+.-]*:/i; | ||
| var UUID_SEGMENT_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; | ||
| var HEX_SEGMENT_PATTERN = /^[0-9a-f]{8,}$/i; | ||
| var BASE64URL_SEGMENT_PATTERN = /^[a-z\d_-]{16,}$/i; | ||
| var EMAIL_LIKE_PATTERN = /^[^\s/@]+@[^\s/@]+\.[^\s/@]+$/; | ||
| var SECRET_PREFIX_SEGMENT_PATTERN = /^(?:sk|pk)_(?:live|test)_[a-z\d_-]{8,}$|^gh[pousr]_[a-z\d_]{8,}$|^github_pat_[a-z\d_]{8,}$/i; | ||
| var SENSITIVE_SEGMENT_WORDS = [ | ||
| "token", | ||
| "secret", | ||
| "session", | ||
| "jwt", | ||
| "oauth", | ||
| "code", | ||
| "key", | ||
| "reset", | ||
| "invite" | ||
| ]; | ||
| function sanitizeUrlForPrivacy(value) { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| return trimmed; | ||
| } | ||
| if (trimmed.startsWith("#")) { | ||
| return ""; | ||
| } | ||
| if (ABSOLUTE_URL_PATTERN.test(trimmed)) { | ||
| return sanitizeAbsoluteUrl(trimmed); | ||
| } | ||
| return sanitizeRelativeUrl(trimmed); | ||
| } | ||
| function routeTemplatePath(pathname) { | ||
| const hasLeadingSlash = pathname.startsWith("/"); | ||
| const hasTrailingSlash = pathname.length > 1 && pathname.endsWith("/"); | ||
| const segments = pathname.split("/").map((segment) => templatePathSegment(segment)); | ||
| let output = segments.join("/"); | ||
| if (hasLeadingSlash && !output.startsWith("/")) { | ||
| output = `/${output}`; | ||
| } | ||
| if (hasTrailingSlash && !output.endsWith("/")) { | ||
| output = `${output}/`; | ||
| } | ||
| return output || (hasLeadingSlash ? "/" : ""); | ||
| } | ||
| function sanitizeAbsoluteUrl(value) { | ||
| let url; | ||
| try { | ||
| url = new URL(value); | ||
| } catch { | ||
| return sanitizeRelativeUrl(value); | ||
| } | ||
| if (url.protocol === "http:" || url.protocol === "https:") { | ||
| return `${url.origin}${routeTemplatePath(url.pathname)}`; | ||
| } | ||
| if (url.protocol === "about:") { | ||
| return `${url.protocol}${routeTemplatePath(url.pathname)}`; | ||
| } | ||
| if (url.protocol === "chrome-extension:") { | ||
| return `${url.protocol}${routeTemplatePath(url.pathname)}`; | ||
| } | ||
| if (url.protocol === "file:") { | ||
| return `${url.protocol}[redacted]`; | ||
| } | ||
| return `${url.protocol}[redacted]`; | ||
| } | ||
| function sanitizeRelativeUrl(value) { | ||
| const hashIndex = value.indexOf("#"); | ||
| const withoutHash = hashIndex >= 0 ? value.slice(0, hashIndex) : value; | ||
| const queryIndex = withoutHash.indexOf("?"); | ||
| const withoutQuery = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash; | ||
| if (!withoutQuery) { | ||
| return ""; | ||
| } | ||
| return routeTemplatePath(withoutQuery); | ||
| } | ||
| function templatePathSegment(segment) { | ||
| if (!segment) { | ||
| return segment; | ||
| } | ||
| const decoded = safeDecodeURIComponent(segment); | ||
| const normalized = decoded.trim().toLowerCase(); | ||
| if (!normalized) { | ||
| return segment; | ||
| } | ||
| if (/^\d+$/.test(normalized)) { | ||
| return ":id"; | ||
| } | ||
| if (EMAIL_LIKE_PATTERN.test(normalized)) { | ||
| return ":id"; | ||
| } | ||
| if (UUID_SEGMENT_PATTERN.test(normalized) || HEX_SEGMENT_PATTERN.test(normalized)) { | ||
| return ":id"; | ||
| } | ||
| if (SECRET_PREFIX_SEGMENT_PATTERN.test(normalized)) { | ||
| return ":token"; | ||
| } | ||
| if (SENSITIVE_SEGMENT_WORDS.some((word) => normalized !== word && normalized.includes(word))) { | ||
| return ":token"; | ||
| } | ||
| if (BASE64URL_SEGMENT_PATTERN.test(normalized) && /[a-z]/i.test(normalized) && /\d/.test(normalized)) { | ||
| return ":token"; | ||
| } | ||
| if (normalized.length >= 6 && /[a-z]/i.test(normalized) && /\d/.test(normalized)) { | ||
| return ":id"; | ||
| } | ||
| return segment; | ||
| } | ||
| function safeDecodeURIComponent(value) { | ||
| try { | ||
| return decodeURIComponent(value); | ||
| } catch { | ||
| return value; | ||
| } | ||
| } | ||
| export { | ||
| CAPTURE_MODES, | ||
| CHUNK_CODECS, | ||
| DEFAULT_CAPTURE_POLICY, | ||
| DEFAULT_EXPORT_POLICY, | ||
| DEFAULT_RECORDER_CONFIG, | ||
| DEFAULT_REDACTION_PROFILE, | ||
| EVENT_LEVELS, | ||
@@ -623,3 +930,6 @@ EventIdFactory, | ||
| buildIndexMessageSchema, | ||
| captureConsentSchema, | ||
| captureModeSchema, | ||
| capturePolicySchema, | ||
| captureScopeSchema, | ||
| chunkCodecSchema, | ||
@@ -652,2 +962,7 @@ chunkPutMessageSchema, | ||
| networkBodyCaptureRuleSchema, | ||
| privacyClassificationSchema, | ||
| privacyManifestCategorySummarySchema, | ||
| privacyManifestSchema, | ||
| privacyScannerFindingSchema, | ||
| privacyScannerResultSchema, | ||
| recorderConfigSchema, | ||
@@ -657,3 +972,5 @@ redactionProfileSchema, | ||
| requestIndexSchema, | ||
| routeTemplatePath, | ||
| samplingProfileSchema, | ||
| sanitizeUrlForPrivacy, | ||
| sessionMetadataSchema, | ||
@@ -660,0 +977,0 @@ siteCapturePolicySchema, |
+1
-1
| { | ||
| "name": "@webblackbox/protocol", | ||
| "description": "Shared event types, schemas, IDs, and configuration defaults for the WebBlackbox ecosystem.", | ||
| "version": "0.4.5", | ||
| "version": "0.5.0", | ||
| "type": "module", | ||
@@ -6,0 +6,0 @@ "main": "./dist/index.js", |
+3
-3
@@ -249,4 +249,4 @@ <p align="center"> | ||
| // - mousemoveHz: 20, scrollHz: 15 | ||
| // - screenshotIdleMs: 8000 | ||
| // - bodyCaptureMaxBytes: 262144 (256 KiB base profile) | ||
| // - screenshotIdleMs: 0 (screenshots disabled unless explicitly enabled) | ||
| // - bodyCaptureMaxBytes: 0 (response bodies disabled unless explicitly enabled) | ||
| // - Redacts: authorization, cookie, set-cookie, proxy auth, API key, auth token, CSRF/XSRF headers | ||
@@ -257,3 +257,3 @@ // - Redacts cookie names such as token, session, auth, jwt, refresh_token, csrf, xsrf | ||
| // Export policy defaults: | ||
| // - includeScreenshots: true | ||
| // - includeScreenshots: false | ||
| // - maxArchiveBytes: 100 * 1024 * 1024 | ||
@@ -260,0 +260,0 @@ // - recentWindowMs: 20 * 60 * 1000 |
Sorry, the diff of this file is too big to display
126582
69.89%3256
80.19%