browsertrack 0.0.1 → 0.1.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.
Files changed (101) hide show
  1. package/README.md +143 -0
  2. package/dist/chunk-6A7FFDIB.js +221 -0
  3. package/dist/chunk-6A7FFDIB.js.map +1 -0
  4. package/dist/chunk-ANMR5WBH.js +659 -0
  5. package/dist/chunk-ANMR5WBH.js.map +1 -0
  6. package/dist/chunk-BSKNG4V7.js +1199 -0
  7. package/dist/chunk-BSKNG4V7.js.map +1 -0
  8. package/dist/chunk-X7C5CHBO.js +2080 -0
  9. package/dist/chunk-X7C5CHBO.js.map +1 -0
  10. package/dist/chunk-ZLNGOOH2.js +554 -0
  11. package/dist/chunk-ZLNGOOH2.js.map +1 -0
  12. package/dist/cli/index.js +2829 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/client/index.cjs +2218 -0
  15. package/dist/client/index.d.ts +197 -0
  16. package/dist/client/index.js +20 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client.iife.js +309 -0
  19. package/dist/core/index.d.ts +78 -0
  20. package/dist/core/index.js +31 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/daemon/index.d.ts +58 -0
  23. package/dist/daemon/index.js +32 -0
  24. package/dist/daemon/index.js.map +1 -0
  25. package/dist/engine-DKloFjvs.d.ts +157 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +50 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mcp/index.d.ts +11 -0
  30. package/dist/mcp/index.js +12 -0
  31. package/dist/mcp/index.js.map +1 -0
  32. package/dist/notes-CBvN91Wf.d.ts +260 -0
  33. package/dist/projects-CY8ungMt.d.ts +99 -0
  34. package/dist/server-CN-se8td.d.ts +55 -0
  35. package/examples/test-app/index.html +299 -0
  36. package/package.json +52 -7
  37. package/packages/cli/src/index.ts +413 -0
  38. package/packages/client/package.json +28 -0
  39. package/packages/client/src/breadcrumbs.ts +30 -0
  40. package/packages/client/src/client.ts +278 -0
  41. package/packages/client/src/commands/handler.ts +301 -0
  42. package/packages/client/src/config.ts +37 -0
  43. package/packages/client/src/index.ts +50 -0
  44. package/packages/client/src/interceptors/console.ts +68 -0
  45. package/packages/client/src/interceptors/interaction.ts +119 -0
  46. package/packages/client/src/interceptors/navigation.ts +93 -0
  47. package/packages/client/src/interceptors/network.ts +165 -0
  48. package/packages/client/src/interceptors/runtime.ts +65 -0
  49. package/packages/client/src/notes/inspector.ts +943 -0
  50. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  51. package/packages/client/src/screenshot/driver.ts +14 -0
  52. package/packages/client/src/transport/websocket.ts +179 -0
  53. package/packages/client/tsconfig.json +8 -0
  54. package/packages/core/dist/index.d.ts +317 -0
  55. package/packages/core/dist/index.js +221 -0
  56. package/packages/core/package.json +23 -0
  57. package/packages/core/src/fingerprint.ts +120 -0
  58. package/packages/core/src/index.ts +9 -0
  59. package/packages/core/src/redaction.ts +139 -0
  60. package/packages/core/src/selector.ts +77 -0
  61. package/packages/core/src/types/commands.ts +101 -0
  62. package/packages/core/src/types/events.ts +108 -0
  63. package/packages/core/src/types/incidents.ts +54 -0
  64. package/packages/core/src/types/notes.ts +106 -0
  65. package/packages/core/src/types/probes.ts +44 -0
  66. package/packages/core/src/types/projects.ts +20 -0
  67. package/packages/core/tsconfig.json +8 -0
  68. package/packages/daemon/src/config.ts +29 -0
  69. package/packages/daemon/src/incidents/engine.ts +211 -0
  70. package/packages/daemon/src/index.ts +18 -0
  71. package/packages/daemon/src/notes/engine.ts +92 -0
  72. package/packages/daemon/src/notes/verification.ts +191 -0
  73. package/packages/daemon/src/server/daemon.ts +112 -0
  74. package/packages/daemon/src/server/http.ts +165 -0
  75. package/packages/daemon/src/server/ws.ts +175 -0
  76. package/packages/daemon/src/session/manager.ts +126 -0
  77. package/packages/daemon/src/storage/db.ts +733 -0
  78. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  79. package/packages/daemon/src/verification/engine.ts +264 -0
  80. package/packages/mcp/src/handlers.ts +398 -0
  81. package/packages/mcp/src/index.ts +3 -0
  82. package/packages/mcp/src/server.ts +86 -0
  83. package/packages/mcp/src/tools.ts +236 -0
  84. package/src/index.ts +4 -0
  85. package/test/client/interceptors.test.ts +69 -0
  86. package/test/core/fingerprint.test.ts +53 -0
  87. package/test/core/notes.test.ts +66 -0
  88. package/test/core/redaction.test.ts +48 -0
  89. package/test/daemon/incident-engine.test.ts +98 -0
  90. package/test/daemon/notes-storage.test.ts +130 -0
  91. package/test/daemon/notes-verification.test.ts +135 -0
  92. package/test/daemon/storage.test.ts +123 -0
  93. package/test/daemon/verification-engine.test.ts +88 -0
  94. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  95. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  96. package/test/mcp/handlers.test.ts +116 -0
  97. package/test/mcp/notes.test.ts +107 -0
  98. package/tsconfig.base.json +17 -0
  99. package/tsconfig.json +26 -0
  100. package/tsup.config.ts +54 -0
  101. package/vitest.config.ts +9 -0
@@ -0,0 +1,221 @@
1
+ // src/fingerprint.ts
2
+ function normalizeErrorMessage(message) {
3
+ if (!message) return "unknown_error";
4
+ return message.trim().replace(/0x[0-9a-fA-F]+/g, "0x<HEX>").replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g, "<UUID>").replace(/\?[tv]=[\w.-]+/g, "").replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?/g, "<TIMESTAMP>").replace(/#\d+/g, "#<ID>").replace(/\s+/g, " ");
5
+ }
6
+ function normalizeSourceFile(filename) {
7
+ if (!filename) return "unknown_source";
8
+ let cleaned = filename.trim();
9
+ try {
10
+ if (cleaned.startsWith("http://") || cleaned.startsWith("https://")) {
11
+ const url = new URL(cleaned);
12
+ cleaned = url.pathname;
13
+ }
14
+ } catch {
15
+ cleaned = cleaned.replace(/^https?:\/\/[^/]+/, "");
16
+ }
17
+ cleaned = cleaned.split("?")[0].split("#")[0];
18
+ cleaned = cleaned.replace(/\\/g, "/");
19
+ return cleaned || "unknown_source";
20
+ }
21
+ function extractSourceFromStack(stack) {
22
+ if (!stack) return null;
23
+ const lines = stack.split("\n");
24
+ for (const line of lines) {
25
+ const match = line.match(/(?:at\s+(?:.*?\s+\()?)?(https?:\/\/[^\s)]+|file:\/\/[^\s)]+|\/[^\s)]+):(\d+):(\d+)\)?/);
26
+ if (match) {
27
+ return {
28
+ file: normalizeSourceFile(match[1]),
29
+ line: parseInt(match[2], 10),
30
+ column: parseInt(match[3], 10)
31
+ };
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ function djb2Hash(str) {
37
+ let hash = 5381;
38
+ for (let i = 0; i < str.length; i++) {
39
+ hash = hash * 33 ^ str.charCodeAt(i);
40
+ }
41
+ return (hash >>> 0).toString(16).padStart(8, "0");
42
+ }
43
+ function computeFingerprint(input) {
44
+ const normType = (input.type || "Error").trim().toLowerCase();
45
+ const normMsg = normalizeErrorMessage(input.message);
46
+ let sourceFile = normalizeSourceFile(input.sourceFile);
47
+ let line = input.line || 0;
48
+ if ((sourceFile === "unknown_source" || line === 0) && input.stack) {
49
+ const extracted = extractSourceFromStack(input.stack);
50
+ if (extracted) {
51
+ sourceFile = extracted.file;
52
+ line = extracted.line;
53
+ }
54
+ }
55
+ const rawKey = `${normType}::${normMsg}::${sourceFile}::${line}`;
56
+ const hash = djb2Hash(rawKey);
57
+ return `fp_${hash}`;
58
+ }
59
+
60
+ // src/redaction.ts
61
+ var SENSITIVE_KEY_PATTERNS = [
62
+ /^authorization$/i,
63
+ /^cookie$/i,
64
+ /^set-cookie$/i,
65
+ /password/i,
66
+ /token/i,
67
+ /secret/i,
68
+ /apikey/i,
69
+ /api_key/i,
70
+ /access_token/i,
71
+ /refresh_token/i,
72
+ /auth/i,
73
+ /credentials/i,
74
+ /private_key/i,
75
+ /ssn/i,
76
+ /credit_card/i,
77
+ /cvv/i
78
+ ];
79
+ var SENSITIVE_QUERY_PARAMS = [
80
+ "token",
81
+ "auth",
82
+ "key",
83
+ "apikey",
84
+ "api_key",
85
+ "secret",
86
+ "password",
87
+ "access_token",
88
+ "refresh_token",
89
+ "code",
90
+ "signature"
91
+ ];
92
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
93
+ function isSensitiveKey(key) {
94
+ if (!key) return false;
95
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key));
96
+ }
97
+ function redactUrl(rawUrl) {
98
+ if (!rawUrl) return rawUrl;
99
+ try {
100
+ const isRelative = !rawUrl.startsWith("http://") && !rawUrl.startsWith("https://") && !rawUrl.startsWith("ws://") && !rawUrl.startsWith("wss://");
101
+ const base = "http://localhost";
102
+ const parsed = new URL(rawUrl, base);
103
+ let changed = false;
104
+ for (const param of SENSITIVE_QUERY_PARAMS) {
105
+ if (parsed.searchParams.has(param)) {
106
+ parsed.searchParams.set(param, REDACTED_PLACEHOLDER);
107
+ changed = true;
108
+ }
109
+ }
110
+ for (const key of Array.from(parsed.searchParams.keys())) {
111
+ if (isSensitiveKey(key)) {
112
+ parsed.searchParams.set(key, REDACTED_PLACEHOLDER);
113
+ changed = true;
114
+ }
115
+ }
116
+ if (!changed) return rawUrl;
117
+ if (isRelative) {
118
+ return parsed.pathname + parsed.search + parsed.hash;
119
+ }
120
+ return parsed.toString();
121
+ } catch {
122
+ let safe = rawUrl;
123
+ for (const param of SENSITIVE_QUERY_PARAMS) {
124
+ const reg = new RegExp(`([?&]${param}=)[^&#]+`, "gi");
125
+ safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);
126
+ }
127
+ return safe;
128
+ }
129
+ }
130
+ function redactHeaders(headers) {
131
+ if (!headers || typeof headers !== "object") return {};
132
+ const safe = {};
133
+ for (const [key, value] of Object.entries(headers)) {
134
+ if (isSensitiveKey(key)) {
135
+ safe[key] = REDACTED_PLACEHOLDER;
136
+ } else {
137
+ safe[key] = value;
138
+ }
139
+ }
140
+ return safe;
141
+ }
142
+ function redactSensitiveData(data, maxDepth = 4, currentDepth = 0) {
143
+ if (data === null || data === void 0) return data;
144
+ if (typeof data !== "object") return data;
145
+ if (currentDepth > maxDepth) return "[DEPTH_EXCEEDED]";
146
+ if (Array.isArray(data)) {
147
+ return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1));
148
+ }
149
+ const result = {};
150
+ for (const [key, value] of Object.entries(data)) {
151
+ if (isSensitiveKey(key)) {
152
+ result[key] = REDACTED_PLACEHOLDER;
153
+ } else if (typeof value === "string") {
154
+ if (value.startsWith("http://") || value.startsWith("https://") || value.includes("?")) {
155
+ result[key] = redactUrl(value);
156
+ } else {
157
+ result[key] = value;
158
+ }
159
+ } else if (typeof value === "object" && value !== null) {
160
+ result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);
161
+ } else {
162
+ result[key] = value;
163
+ }
164
+ }
165
+ return result;
166
+ }
167
+
168
+ // src/selector.ts
169
+ function getSemanticSelector(element, options = {}) {
170
+ if (!element || !element.tagName) return "unknown";
171
+ const maxClasses = options.maxClasses ?? 2;
172
+ const testId = element.getAttribute("data-testid") || element.getAttribute("data-test") || element.getAttribute("data-cy") || element.getAttribute("data-qa");
173
+ if (testId) {
174
+ return `[data-testid="${testId}"]`;
175
+ }
176
+ const id = element.id;
177
+ if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {
178
+ return `#${id}`;
179
+ }
180
+ const ariaLabel = element.getAttribute("aria-label");
181
+ if (ariaLabel && ariaLabel.length < 30) {
182
+ return `${element.tagName.toLowerCase()}[aria-label="${ariaLabel}"]`;
183
+ }
184
+ const nameAttr = element.getAttribute("name");
185
+ if (nameAttr) {
186
+ return `${element.tagName.toLowerCase()}[name="${nameAttr}"]`;
187
+ }
188
+ const tag = element.tagName.toLowerCase();
189
+ const classList = Array.from(element.classList || []).filter((c) => !c.startsWith("css-") && !c.startsWith("_") && !/^[a-z0-9]{5,}$/i.test(c)).slice(0, maxClasses);
190
+ if (classList.length > 0) {
191
+ const classStr = classList.map((c) => `.${c}`).join("");
192
+ return `${tag}${classStr}`;
193
+ }
194
+ if (element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement) {
195
+ const parentSelector = getSemanticSelector(element.parentElement, { maxClasses: 1 });
196
+ if (parentSelector && parentSelector !== "unknown" && !parentSelector.includes(">")) {
197
+ return `${parentSelector} > ${tag}`;
198
+ }
199
+ }
200
+ return tag;
201
+ }
202
+ function truncate(str, maxLength = 200) {
203
+ if (!str) return "";
204
+ if (str.length <= maxLength) return str;
205
+ return str.slice(0, maxLength) + "...";
206
+ }
207
+ export {
208
+ REDACTED_PLACEHOLDER,
209
+ SENSITIVE_KEY_PATTERNS,
210
+ SENSITIVE_QUERY_PARAMS,
211
+ computeFingerprint,
212
+ extractSourceFromStack,
213
+ getSemanticSelector,
214
+ isSensitiveKey,
215
+ normalizeErrorMessage,
216
+ normalizeSourceFile,
217
+ redactHeaders,
218
+ redactSensitiveData,
219
+ redactUrl,
220
+ truncate
221
+ };
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@browserdiag/core",
3
+ "version": "0.1.0",
4
+ "description": "Core types, schemas, fingerprinting, and utilities for browserdiag",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format esm --dts --clean",
16
+ "dev": "tsup src/index.ts --format esm --dts --watch",
17
+ "clean": "rm -rf dist"
18
+ },
19
+ "devDependencies": {
20
+ "tsup": "^8.3.6",
21
+ "typescript": "^5.7.3"
22
+ }
23
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Error fingerprinting and normalization utilities
3
+ */
4
+
5
+ export interface FingerprintInput {
6
+ type: string;
7
+ message: string;
8
+ sourceFile?: string;
9
+ line?: number;
10
+ column?: number;
11
+ stack?: string;
12
+ }
13
+
14
+ /**
15
+ * Normalizes error messages by stripping dynamic content like UUIDs, timestamps,
16
+ * hex addresses, and file timestamps.
17
+ */
18
+ export function normalizeErrorMessage(message: string): string {
19
+ if (!message) return 'unknown_error';
20
+
21
+ return message
22
+ // Normalize string representation
23
+ .trim()
24
+ // Replace hex addresses (e.g. 0x7ffee4b2a8)
25
+ .replace(/0x[0-9a-fA-F]+/g, '0x<HEX>')
26
+ // Replace UUIDs
27
+ .replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g, '<UUID>')
28
+ // Replace timestamp query params like ?t=1719283748 or ?v=1.2.3
29
+ .replace(/\?[tv]=[\w.-]+/g, '')
30
+ // Replace ISO timestamps
31
+ .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?/g, '<TIMESTAMP>')
32
+ // Replace numbers in quotes/brackets if likely IDs (e.g., user "12345")
33
+ .replace(/#\d+/g, '#<ID>')
34
+ // Normalize whitespaces
35
+ .replace(/\s+/g, ' ');
36
+ }
37
+
38
+ /**
39
+ * Normalizes source filenames by removing origin and cache-busting query strings.
40
+ */
41
+ export function normalizeSourceFile(filename?: string): string {
42
+ if (!filename) return 'unknown_source';
43
+
44
+ let cleaned = filename.trim();
45
+ // Strip protocol and origin if present (e.g. http://localhost:5173/src/App.tsx?t=123)
46
+ try {
47
+ if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
48
+ const url = new URL(cleaned);
49
+ cleaned = url.pathname;
50
+ }
51
+ } catch {
52
+ // fallback string manipulation
53
+ cleaned = cleaned.replace(/^https?:\/\/[^/]+/, '');
54
+ }
55
+
56
+ // Remove query parameters
57
+ cleaned = cleaned.split('?')[0].split('#')[0];
58
+
59
+ // Normalize windows path separators
60
+ cleaned = cleaned.replace(/\\/g, '/');
61
+
62
+ return cleaned || 'unknown_source';
63
+ }
64
+
65
+ /**
66
+ * Extracts source location from stack if not explicitly provided
67
+ */
68
+ export function extractSourceFromStack(stack?: string): { file: string; line: number; column?: number } | null {
69
+ if (!stack) return null;
70
+
71
+ const lines = stack.split('\n');
72
+ for (const line of lines) {
73
+ // Match standard stack line: at functionName (http://localhost:5173/src/main.tsx:15:3) or at http://...:15:3
74
+ const match = line.match(/(?:at\s+(?:.*?\s+\()?)?(https?:\/\/[^\s)]+|file:\/\/[^\s)]+|\/[^\s)]+):(\d+):(\d+)\)?/);
75
+ if (match) {
76
+ return {
77
+ file: normalizeSourceFile(match[1]),
78
+ line: parseInt(match[2], 10),
79
+ column: parseInt(match[3], 10),
80
+ };
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Fast synchronous djb2-based hash for universal client/server runtime
88
+ */
89
+ function djb2Hash(str: string): string {
90
+ let hash = 5381;
91
+ for (let i = 0; i < str.length; i++) {
92
+ hash = (hash * 33) ^ str.charCodeAt(i);
93
+ }
94
+ // Convert to unsigned 32-bit hex string
95
+ return (hash >>> 0).toString(16).padStart(8, '0');
96
+ }
97
+
98
+ /**
99
+ * Computes a deterministic fingerprint for an error
100
+ */
101
+ export function computeFingerprint(input: FingerprintInput): string {
102
+ const normType = (input.type || 'Error').trim().toLowerCase();
103
+ const normMsg = normalizeErrorMessage(input.message);
104
+
105
+ let sourceFile = normalizeSourceFile(input.sourceFile);
106
+ let line = input.line || 0;
107
+
108
+ if ((sourceFile === 'unknown_source' || line === 0) && input.stack) {
109
+ const extracted = extractSourceFromStack(input.stack);
110
+ if (extracted) {
111
+ sourceFile = extracted.file;
112
+ line = extracted.line;
113
+ }
114
+ }
115
+
116
+ const rawKey = `${normType}::${normMsg}::${sourceFile}::${line}`;
117
+ const hash = djb2Hash(rawKey);
118
+
119
+ return `fp_${hash}`;
120
+ }
@@ -0,0 +1,9 @@
1
+ export * from './types/events.js';
2
+ export * from './types/incidents.js';
3
+ export * from './types/commands.js';
4
+ export * from './types/probes.js';
5
+ export * from './types/projects.js';
6
+ export * from './types/notes.js';
7
+ export * from './fingerprint.js';
8
+ export * from './redaction.js';
9
+ export * from './selector.js';
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Security and Redaction Utilities
3
+ */
4
+
5
+ export const SENSITIVE_KEY_PATTERNS = [
6
+ /^authorization$/i,
7
+ /^cookie$/i,
8
+ /^set-cookie$/i,
9
+ /password/i,
10
+ /token/i,
11
+ /secret/i,
12
+ /api[-_]?key/i,
13
+ /access[-_]?token/i,
14
+ /refresh[-_]?token/i,
15
+ /credentials/i,
16
+ /private[-_]?key/i,
17
+ /ssn/i,
18
+ /credit[-_]?card/i,
19
+ /cvv/i,
20
+ ];
21
+
22
+ export const SENSITIVE_QUERY_PARAMS = [
23
+ 'token',
24
+ 'auth',
25
+ 'key',
26
+ 'apikey',
27
+ 'api_key',
28
+ 'secret',
29
+ 'password',
30
+ 'access_token',
31
+ 'refresh_token',
32
+ 'code',
33
+ 'signature',
34
+ ];
35
+
36
+ export const REDACTED_PLACEHOLDER = '[REDACTED]';
37
+
38
+ /**
39
+ * Checks if a property name matches any sensitive pattern
40
+ */
41
+ export function isSensitiveKey(key: string): boolean {
42
+ if (!key) return false;
43
+ const cleaned = key.replace(/[-_]/g, '');
44
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));
45
+ }
46
+
47
+ /**
48
+ * Redacts sensitive query parameters from a URL string
49
+ */
50
+ export function redactUrl(rawUrl: string): string {
51
+ if (!rawUrl) return rawUrl;
52
+
53
+ try {
54
+ const isRelative = !rawUrl.startsWith('http://') && !rawUrl.startsWith('https://') && !rawUrl.startsWith('ws://') && !rawUrl.startsWith('wss://');
55
+ const base = 'http://localhost';
56
+ const parsed = new URL(rawUrl, base);
57
+
58
+ let changed = false;
59
+ for (const param of SENSITIVE_QUERY_PARAMS) {
60
+ if (parsed.searchParams.has(param)) {
61
+ parsed.searchParams.set(param, REDACTED_PLACEHOLDER);
62
+ changed = true;
63
+ }
64
+ }
65
+
66
+ // Also check all query params against isSensitiveKey
67
+ for (const key of Array.from(parsed.searchParams.keys())) {
68
+ if (isSensitiveKey(key)) {
69
+ parsed.searchParams.set(key, REDACTED_PLACEHOLDER);
70
+ changed = true;
71
+ }
72
+ }
73
+
74
+ if (!changed) return rawUrl;
75
+
76
+ let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();
77
+ // Normalize URL-encoded brackets %5BREDACTED%5D back to [REDACTED]
78
+ result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);
79
+ return result;
80
+ } catch {
81
+ // If URL parsing fails, perform regex replace
82
+ let safe = rawUrl;
83
+ for (const param of SENSITIVE_QUERY_PARAMS) {
84
+ const reg = new RegExp(`([?&]${param}=)[^&#]+`, 'gi');
85
+ safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);
86
+ }
87
+ return safe;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Redacts sensitive HTTP headers
93
+ */
94
+ export function redactHeaders(headers: Record<string, string>): Record<string, string> {
95
+ if (!headers || typeof headers !== 'object') return {};
96
+
97
+ const safe: Record<string, string> = {};
98
+ for (const [key, value] of Object.entries(headers)) {
99
+ if (isSensitiveKey(key)) {
100
+ safe[key] = REDACTED_PLACEHOLDER;
101
+ } else {
102
+ safe[key] = value;
103
+ }
104
+ }
105
+ return safe;
106
+ }
107
+
108
+ /**
109
+ * Deeply redacts sensitive keys from an object or array
110
+ */
111
+ export function redactSensitiveData<T = any>(data: T, maxDepth = 6, currentDepth = 0): T {
112
+ if (data === null || data === undefined) return data;
113
+ if (typeof data !== 'object') return data;
114
+ if (currentDepth > maxDepth) return '[DEPTH_EXCEEDED]' as any;
115
+
116
+ if (Array.isArray(data)) {
117
+ return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1)) as any;
118
+ }
119
+
120
+ const result: Record<string, any> = {};
121
+ for (const [key, value] of Object.entries(data as Record<string, any>)) {
122
+ if (typeof value === 'object' && value !== null) {
123
+ result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);
124
+ } else if (isSensitiveKey(key)) {
125
+ result[key] = REDACTED_PLACEHOLDER;
126
+ } else if (typeof value === 'string') {
127
+ // Check if value is a URL containing secrets
128
+ if (value.startsWith('http://') || value.startsWith('https://') || value.includes('?')) {
129
+ result[key] = redactUrl(value);
130
+ } else {
131
+ result[key] = value;
132
+ }
133
+ } else {
134
+ result[key] = value;
135
+ }
136
+ }
137
+
138
+ return result as T;
139
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Semantic element selector generation and inspection
3
+ */
4
+
5
+ export interface SelectorOptions {
6
+ maxClasses?: number;
7
+ maxOuterHTMLLength?: number;
8
+ maxInnerTextLength?: number;
9
+ }
10
+
11
+ /**
12
+ * Builds a clean, human-readable semantic selector for a DOM element.
13
+ * Prioritizes data-testid/data-test, id, semantic tag + specific class, or concise parent path.
14
+ */
15
+ export function getSemanticSelector(element: HTMLElement | Element, options: SelectorOptions = {}): string {
16
+ if (!element || !element.tagName) return 'unknown';
17
+
18
+ const maxClasses = options.maxClasses ?? 2;
19
+
20
+ // 1. Check for standard test attributes
21
+ const testId =
22
+ element.getAttribute('data-testid') ||
23
+ element.getAttribute('data-test') ||
24
+ element.getAttribute('data-cy') ||
25
+ element.getAttribute('data-qa');
26
+
27
+ if (testId) {
28
+ return `[data-testid="${testId}"]`;
29
+ }
30
+
31
+ // 2. Check for meaningful ID (skip dynamic UUID/numeric IDs)
32
+ const id = element.id;
33
+ if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {
34
+ return `#${id}`;
35
+ }
36
+
37
+ // 3. Check for aria-label or name on buttons/inputs
38
+ const ariaLabel = element.getAttribute('aria-label');
39
+ if (ariaLabel && ariaLabel.length < 30) {
40
+ return `${element.tagName.toLowerCase()}[aria-label="${ariaLabel}"]`;
41
+ }
42
+
43
+ const nameAttr = element.getAttribute('name');
44
+ if (nameAttr) {
45
+ return `${element.tagName.toLowerCase()}[name="${nameAttr}"]`;
46
+ }
47
+
48
+ // 4. Tag + curated classes
49
+ const tag = element.tagName.toLowerCase();
50
+ const classList = Array.from(element.classList || [])
51
+ .filter((c) => !c.startsWith('css-') && !c.startsWith('_') && !/^[a-z0-9]{5,}$/i.test(c)) // filter hashed utility classes
52
+ .slice(0, maxClasses);
53
+
54
+ if (classList.length > 0) {
55
+ const classStr = classList.map((c) => `.${c}`).join('');
56
+ return `${tag}${classStr}`;
57
+ }
58
+
59
+ // 5. If generic element (div, span, p) without attributes, try parent context
60
+ if (element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement) {
61
+ const parentSelector = getSemanticSelector(element.parentElement, { maxClasses: 1 });
62
+ if (parentSelector && parentSelector !== 'unknown' && !parentSelector.includes('>')) {
63
+ return `${parentSelector} > ${tag}`;
64
+ }
65
+ }
66
+
67
+ return tag;
68
+ }
69
+
70
+ /**
71
+ * Truncates string to specified max length with ellipsis
72
+ */
73
+ export function truncate(str: string | undefined | null, maxLength = 200): string {
74
+ if (!str) return '';
75
+ if (str.length <= maxLength) return str;
76
+ return str.slice(0, maxLength) + '...';
77
+ }
@@ -0,0 +1,101 @@
1
+ import type { ElementSummary } from './events.js';
2
+
3
+ export type CommandType =
4
+ | 'reload'
5
+ | 'navigate'
6
+ | 'get_page_state'
7
+ | 'query_element'
8
+ | 'capture_element'
9
+ | 'check_overflow'
10
+ | 'get_element_rect'
11
+ | 'get_element_style';
12
+
13
+ export interface ClientCommand<T = any> {
14
+ id: string;
15
+ type: CommandType;
16
+ params?: T;
17
+ }
18
+
19
+ export interface ReloadParams {
20
+ force?: boolean;
21
+ }
22
+
23
+ export interface NavigateParams {
24
+ url: string;
25
+ }
26
+
27
+ export interface QueryElementParams {
28
+ selector: string;
29
+ }
30
+
31
+ export interface CaptureElementParams {
32
+ selector?: string;
33
+ }
34
+
35
+ export interface QueryElementResult {
36
+ exists: boolean;
37
+ visible?: boolean;
38
+ tag?: string;
39
+ id?: string;
40
+ classes?: string[];
41
+ boundingRect?: {
42
+ x: number;
43
+ y: number;
44
+ width: number;
45
+ height: number;
46
+ top: number;
47
+ left: number;
48
+ bottom: number;
49
+ right: number;
50
+ };
51
+ innerText?: string;
52
+ outerHTML?: string;
53
+ }
54
+
55
+ export interface PageStateResult {
56
+ url: string;
57
+ route: string;
58
+ title: string;
59
+ readyState: string;
60
+ activeElement?: ElementSummary;
61
+ }
62
+
63
+ export interface CaptureElementResult {
64
+ dataUrl?: string;
65
+ format?: string;
66
+ width?: number;
67
+ height?: number;
68
+ }
69
+
70
+ export interface OverflowCheckResult {
71
+ selector: string;
72
+ overflow: boolean;
73
+ viewportWidth: number;
74
+ viewportHeight: number;
75
+ rect?: {
76
+ x: number;
77
+ y: number;
78
+ width: number;
79
+ height: number;
80
+ top: number;
81
+ left: number;
82
+ bottom: number;
83
+ right: number;
84
+ };
85
+ overflowRightPx: number;
86
+ overflowBottomPx: number;
87
+ parentOverflow?: boolean;
88
+ }
89
+
90
+ export interface ElementStyleResult {
91
+ selector: string;
92
+ styles: Record<string, string>;
93
+ }
94
+
95
+ export interface CommandResponse<T = any> {
96
+ id: string;
97
+ ok: boolean;
98
+ result?: T;
99
+ error?: string;
100
+ reason?: string;
101
+ }