browsertrack 0.0.1 → 0.1.1
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/.github/workflows/docs.yml +54 -0
- package/AGENTS.md +133 -0
- package/README.md +143 -0
- package/dist/chunk-6VA7GBAO.js +246 -0
- package/dist/chunk-6VA7GBAO.js.map +1 -0
- package/dist/chunk-G2Y3CXCY.js +659 -0
- package/dist/chunk-G2Y3CXCY.js.map +1 -0
- package/dist/chunk-ILRYKMME.js +1228 -0
- package/dist/chunk-ILRYKMME.js.map +1 -0
- package/dist/chunk-SKCMT2DE.js +2641 -0
- package/dist/chunk-SKCMT2DE.js.map +1 -0
- package/dist/chunk-SPCIROIU.js +616 -0
- package/dist/chunk-SPCIROIU.js.map +1 -0
- package/dist/cli/index.js +2942 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.cjs +2780 -0
- package/dist/client/index.d.ts +211 -0
- package/dist/client/index.js +20 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.iife.js +630 -0
- package/dist/core/index.d.ts +86 -0
- package/dist/core/index.js +37 -0
- package/dist/core/index.js.map +1 -0
- package/dist/daemon/index.d.ts +58 -0
- package/dist/daemon/index.js +32 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/engine-CeT9URuN.d.ts +161 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +11 -0
- package/dist/mcp/index.js +12 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/notes-CBvN91Wf.d.ts +260 -0
- package/dist/projects-CY8ungMt.d.ts +99 -0
- package/dist/server-Dd8NX2Mk.d.ts +55 -0
- package/docboot.config.js +16 -0
- package/docs/cli.md +51 -0
- package/docs/closed-loop-verification.md +57 -0
- package/docs/getting-started.md +122 -0
- package/docs/incidents-diagnostics.md +61 -0
- package/docs/index.md +46 -0
- package/docs/mcp-reference.md +128 -0
- package/docs/security-privacy.md +32 -0
- package/docs/visual-notes.md +67 -0
- package/examples/test-app/index.html +299 -0
- package/package.json +56 -7
- package/packages/cli/src/index.ts +413 -0
- package/packages/client/package.json +28 -0
- package/packages/client/src/breadcrumbs.ts +30 -0
- package/packages/client/src/client.ts +278 -0
- package/packages/client/src/commands/handler.ts +301 -0
- package/packages/client/src/config.ts +37 -0
- package/packages/client/src/index.ts +50 -0
- package/packages/client/src/interceptors/console.ts +68 -0
- package/packages/client/src/interceptors/interaction.ts +119 -0
- package/packages/client/src/interceptors/navigation.ts +93 -0
- package/packages/client/src/interceptors/network.ts +165 -0
- package/packages/client/src/interceptors/runtime.ts +65 -0
- package/packages/client/src/notes/inspector.ts +1553 -0
- package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
- package/packages/client/src/screenshot/driver.ts +14 -0
- package/packages/client/src/transport/websocket.ts +194 -0
- package/packages/client/tsconfig.json +8 -0
- package/packages/core/package.json +26 -0
- package/packages/core/src/fingerprint.ts +120 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/redaction.ts +174 -0
- package/packages/core/src/selector.ts +77 -0
- package/packages/core/src/types/commands.ts +101 -0
- package/packages/core/src/types/events.ts +108 -0
- package/packages/core/src/types/incidents.ts +54 -0
- package/packages/core/src/types/notes.ts +106 -0
- package/packages/core/src/types/probes.ts +44 -0
- package/packages/core/src/types/projects.ts +20 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/daemon/src/config.ts +29 -0
- package/packages/daemon/src/incidents/engine.ts +211 -0
- package/packages/daemon/src/index.ts +18 -0
- package/packages/daemon/src/notes/engine.ts +92 -0
- package/packages/daemon/src/notes/verification.ts +191 -0
- package/packages/daemon/src/server/daemon.ts +112 -0
- package/packages/daemon/src/server/http.ts +165 -0
- package/packages/daemon/src/server/ws.ts +246 -0
- package/packages/daemon/src/session/manager.ts +147 -0
- package/packages/daemon/src/storage/db.ts +742 -0
- package/packages/daemon/src/storage/screenshot-store.ts +49 -0
- package/packages/daemon/src/verification/engine.ts +264 -0
- package/packages/mcp/src/handlers.ts +398 -0
- package/packages/mcp/src/index.ts +3 -0
- package/packages/mcp/src/server.ts +86 -0
- package/packages/mcp/src/tools.ts +236 -0
- package/src/index.ts +4 -0
- package/test/client/interceptors.test.ts +130 -0
- package/test/core/fingerprint.test.ts +53 -0
- package/test/core/notes.test.ts +66 -0
- package/test/core/redaction.test.ts +60 -0
- package/test/daemon/incident-engine.test.ts +98 -0
- package/test/daemon/notes-storage.test.ts +130 -0
- package/test/daemon/notes-verification.test.ts +135 -0
- package/test/daemon/storage.test.ts +123 -0
- package/test/daemon/verification-engine.test.ts +88 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
- package/test/e2e/visual-notes-e2e.test.ts +205 -0
- package/test/mcp/handlers.test.ts +116 -0
- package/test/mcp/notes.test.ts +107 -0
- package/tsconfig.base.json +17 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +54 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Generated by Docboot
|
|
2
|
+
name: Deploy Docs
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pages: write
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: pages
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout repository
|
|
24
|
+
uses: actions/checkout@v4
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
|
|
28
|
+
- name: Setup Node.js
|
|
29
|
+
uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version: '22'
|
|
32
|
+
cache: 'npm'
|
|
33
|
+
|
|
34
|
+
- name: Install dependencies
|
|
35
|
+
run: npm ci
|
|
36
|
+
|
|
37
|
+
- name: Build documentation
|
|
38
|
+
run: npx -y docboot build
|
|
39
|
+
|
|
40
|
+
- name: Upload Pages artifact
|
|
41
|
+
uses: actions/upload-pages-artifact@v3
|
|
42
|
+
with:
|
|
43
|
+
path: dist-docs
|
|
44
|
+
|
|
45
|
+
deploy:
|
|
46
|
+
needs: build
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
environment:
|
|
49
|
+
name: github-pages
|
|
50
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
51
|
+
steps:
|
|
52
|
+
- name: Deploy to GitHub Pages
|
|
53
|
+
id: deployment
|
|
54
|
+
uses: actions/deploy-pages@v4
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# BrowserTrack — AI Agent Guidelines & MCP Instructions
|
|
2
|
+
|
|
3
|
+
This repository is integrated with **BrowserTrack**, a local browser diagnostics daemon and Model Context Protocol (MCP) bridge. As an AI coding agent, you have direct access to live browser runtime errors, console logs, network failures, user breadcrumbs, visual notes, screenshots, and automated closed-loop verification tools.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🎯 When to Use BrowserTrack Tools
|
|
8
|
+
|
|
9
|
+
Always check BrowserTrack when:
|
|
10
|
+
1. **The user reports a bug, crash, or visual issue in the browser.**
|
|
11
|
+
2. **The user leaves visual annotations or design feedback** on elements/pages (Alt+Click notes).
|
|
12
|
+
3. **You modify frontend code** (React, Vue, HTML, CSS, JS) and need to verify whether runtime errors or visual regressions occurred.
|
|
13
|
+
4. **You need real-time page state, live DOM hierarchy, or screenshots** without asking the user to manually copy-paste logs or send screenshots.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 🧭 Core Agent Workflows
|
|
18
|
+
|
|
19
|
+
### 1. Visual Notes & UI Fixes Workflow
|
|
20
|
+
|
|
21
|
+
When the user asks to fix a UI issue, layout bug, or style note:
|
|
22
|
+
|
|
23
|
+
```mermaid
|
|
24
|
+
graph TD
|
|
25
|
+
A[list_notes] --> B[get_note noteId]
|
|
26
|
+
B --> C[Analyze DOM Context, Viewport & Screenshot]
|
|
27
|
+
C --> D[Modify CSS / Component Code]
|
|
28
|
+
D --> E[verify_note noteId]
|
|
29
|
+
E --> F{Resolved?}
|
|
30
|
+
F -->|Yes| G[resolve_note noteId]
|
|
31
|
+
F -->|No| D
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
1. **Find open notes:** Call `list_notes({ status: "OPEN" })`.
|
|
35
|
+
2. **Inspect note context:** Call `get_note({ noteId: "note_xxx" })`.
|
|
36
|
+
- Review the target selector (`target.selector`), viewport dimensions, bounding box (`boundingRect`), and screenshot path.
|
|
37
|
+
- Check if layout overflow (`overflow-x`, min-width, flex wrapping) is causing the issue.
|
|
38
|
+
3. **Apply the fix:** Edit the corresponding CSS, style rules, or component markup.
|
|
39
|
+
4. **Verify the fix:** Call `verify_note({ noteId: "note_xxx" })`.
|
|
40
|
+
- This automatically reloads the browser, checks element visibility, tests viewport overflow, and captures an after-fix screenshot.
|
|
41
|
+
5. **Close the note:** If verified, call `resolve_note({ noteId: "note_xxx" })`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### 2. Runtime Error & Crash Diagnostics Workflow
|
|
46
|
+
|
|
47
|
+
When an error or unexpected behavior occurs in the frontend:
|
|
48
|
+
|
|
49
|
+
1. **List open incidents:** Call `list_incidents({ status: "OPEN" })`.
|
|
50
|
+
2. **Retrieve incident details:** Call `get_incident({ incidentId: "inc_xxx" })`.
|
|
51
|
+
- Inspect stack traces, exact source line/column, last user interactions, and failing network calls.
|
|
52
|
+
3. **Inspect interaction timeline (if needed):** Call `get_breadcrumbs({ incidentId: "inc_xxx" })` to see what clicks, inputs, or navigations led to the failure.
|
|
53
|
+
4. **Inspect failed network requests (if needed):** Call `get_network_failures()` to examine 4xx/5xx status codes, request URLs, and response details.
|
|
54
|
+
5. **Apply code fix:** Edit the source file causing the bug.
|
|
55
|
+
6. **Trigger closed-loop verification:** Call `verify_incident({ incidentId: "inc_xxx" })`.
|
|
56
|
+
- Optionally supply `expect` probes (e.g. `[{ type: "no_incident" }, { type: "element_visible", selector: ".my-component" }]`).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🛠️ Complete MCP Tool Reference
|
|
61
|
+
|
|
62
|
+
### 📋 Incidents & Diagnostics
|
|
63
|
+
| MCP Tool | Purpose | Key Arguments |
|
|
64
|
+
| :--- | :--- | :--- |
|
|
65
|
+
| `list_incidents` | List grouped browser runtime errors and unhandled rejections | `status`, `severity`, `projectId`, `limit` |
|
|
66
|
+
| `get_incident` | Retrieve stack trace, breadcrumbs, screenshot, and context for an incident | `incidentId` *(required)* |
|
|
67
|
+
| `get_console` | Fetch recent console errors, warnings, and logs | `sessionId`, `limit` |
|
|
68
|
+
| `get_network_failures` | Fetch recent 4xx/5xx HTTP failures and aborted requests | `sessionId`, `limit` |
|
|
69
|
+
| `get_breadcrumbs` | Fetch chronological user interaction and navigation history | `incidentId`, `sessionId`, `limit` |
|
|
70
|
+
| `get_page_state` | Query current URL, document title, readyState, and active element | `sessionId` |
|
|
71
|
+
| `capture_element` | Capture an on-demand screenshot of a selector or full viewport | `selector`, `sessionId` |
|
|
72
|
+
|
|
73
|
+
### 📝 Visual Notes & Layout Inspector
|
|
74
|
+
| MCP Tool | Purpose | Key Arguments |
|
|
75
|
+
| :--- | :--- | :--- |
|
|
76
|
+
| `list_notes` | List visual annotations left on elements/regions | `status`, `projectId`, `limit` |
|
|
77
|
+
| `get_note` | Get full note details (DOM context, selector, bounding rect, screenshot) | `noteId` *(required)* |
|
|
78
|
+
| `capture_note_context` | Inspect live DOM bounding box, styles, and overflow on an element | `selector` *(required)*, `sessionId` |
|
|
79
|
+
| `verify_note` | Run automated layout verification and before/after comparison | `noteId` *(required)*, `observationWindowMs` |
|
|
80
|
+
| `get_note_verification`| Retrieve latest layout verification verdict and screenshots | `noteId` *(required)* |
|
|
81
|
+
| `resolve_note` | Mark a visual note as RESOLVED | `noteId` *(required)* |
|
|
82
|
+
| `reopen_note` | Reopen a previously resolved visual note | `noteId` *(required)* |
|
|
83
|
+
|
|
84
|
+
### 🎛️ Session & Project Management
|
|
85
|
+
| MCP Tool | Purpose | Key Arguments |
|
|
86
|
+
| :--- | :--- | :--- |
|
|
87
|
+
| `list_projects` | List all registered projects and mapped paths | *(none)* |
|
|
88
|
+
| `list_sessions` | List active browser tabs connected via WebSocket | `projectId`, `activeOnly` |
|
|
89
|
+
| `verify_incident` | Verify bug fix by reloading browser and evaluating probes | `incidentId` *(required)*, `expect`, `route` |
|
|
90
|
+
| `get_verification` | Retrieve verification verdict (`VERIFIED`, `FAILED`, `INCONCLUSIVE`) | `incidentId` *(required)* |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## ⚡ Quick Setup for Projects
|
|
95
|
+
|
|
96
|
+
If BrowserTrack is not yet active in your project:
|
|
97
|
+
|
|
98
|
+
### 1. Ensure Daemon is Running
|
|
99
|
+
```bash
|
|
100
|
+
browsertrack start
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 2. Connect the Browser Client
|
|
104
|
+
- **Via HTML script tag:**
|
|
105
|
+
```html
|
|
106
|
+
<script src="http://127.0.0.1:7331/client.js"></script>
|
|
107
|
+
```
|
|
108
|
+
- **Or via NPM package:**
|
|
109
|
+
```typescript
|
|
110
|
+
import 'browsertrack/client';
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 3. MCP Configuration
|
|
114
|
+
Ensure BrowserTrack MCP is configured in your editor:
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"mcpServers": {
|
|
118
|
+
"browsertrack": {
|
|
119
|
+
"command": "browsertrack",
|
|
120
|
+
"args": ["mcp"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 💡 Best Practices for AI Agents
|
|
129
|
+
|
|
130
|
+
- **Don't guess error causes:** Always check `get_incident` or `get_breadcrumbs` before modifying code.
|
|
131
|
+
- **Verify after fixing:** Call `verify_incident` or `verify_note` after code edits to confirm the fix works in the real browser.
|
|
132
|
+
- **Respect privacy redaction:** Passwords, auth tokens, and sensitive query parameters are redacted by BrowserTrack by design.
|
|
133
|
+
- **Check responsive viewports:** When resolving layout notes, observe the `viewport` dimensions reported in `get_note`.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# BrowserTrack 🔍
|
|
2
|
+
|
|
3
|
+
> **Local browser diagnostics shared with coding agents through MCP.**
|
|
4
|
+
|
|
5
|
+
BrowserTrack is a lightweight, framework-agnostic local development daemon and Model Context Protocol (MCP) bridge. It automatically collects browser-side runtime errors, unhandled rejections, console logs, network failures, user breadcrumbs, and DOM snapshots—making them instantly accessible to AI coding assistants (Antigravity, Cursor, Claude Code) for rapid debugging and closed-loop verification.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ⚡ Quick Start
|
|
10
|
+
|
|
11
|
+
### 1. Start the Local Daemon
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g browsertrack
|
|
15
|
+
browsertrack start
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This starts the local diagnostics server at `http://127.0.0.1:7331` (WebSocket at `ws://127.0.0.1:7331`).
|
|
19
|
+
|
|
20
|
+
### 2. Connect Your Web Project
|
|
21
|
+
|
|
22
|
+
#### Option A: Zero-install script tag (Plain HTML / Vite / Next.js)
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<script src="http://127.0.0.1:7331/client.js"></script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### Option B: NPM package (React / Vue / Svelte / Astro)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -D browsertrack
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
In your main application entry point (e.g. `main.ts` or `index.tsx`):
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import 'browsertrack/client';
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 🤖 Coding Agent (MCP) Setup
|
|
43
|
+
|
|
44
|
+
Add BrowserTrack to your MCP client configuration:
|
|
45
|
+
|
|
46
|
+
### Cursor (`~/.cursor/mcp.json`) & Claude Code
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"browsertrack": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["browsertrack", "mcp"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Antigravity (`~/.gemini/antigravity-ide/mcp_config.json`)
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"browsertrack": {
|
|
63
|
+
"command": "browsertrack",
|
|
64
|
+
"args": ["mcp"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🛠️ MCP Tools Exposed to AI Agents
|
|
73
|
+
|
|
74
|
+
| MCP Tool | Description |
|
|
75
|
+
| :--- | :--- |
|
|
76
|
+
| `list_projects` | Lists registered local development projects and mapped filesystem paths. |
|
|
77
|
+
| `list_sessions` | Lists active browser tabs connected over WebSocket. |
|
|
78
|
+
| `list_incidents` | Lists grouped error incidents with occurrences, severity, and status. |
|
|
79
|
+
| `get_incident` | Retrieves concise, high-signal context (stack trace, breadcrumbs, network failures, last interacted element, error screenshot). |
|
|
80
|
+
| `get_console` | Retrieves recent console errors/warnings. |
|
|
81
|
+
| `get_network_failures` | Retrieves recent 4xx/5xx HTTP failures and aborted requests. |
|
|
82
|
+
| `get_breadcrumbs` | Returns user interaction timeline leading up to an error. |
|
|
83
|
+
| `get_page_state` | Queries live DOM/URL state from the active browser tab. |
|
|
84
|
+
| `capture_element` | Captures an on-demand screenshot of a specific element or full page. |
|
|
85
|
+
| `verify_incident` | Runs closed-loop verification: reloads browser, checks if error reoccurs, executes probes, captures after-screenshot, and returns verdict (`VERIFIED`, `FAILED`, `INCONCLUSIVE`). |
|
|
86
|
+
| `get_verification` | Returns latest verification result and before/after screenshot artifacts. |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 💻 CLI Commands
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
browsertrack start # Start local daemon
|
|
94
|
+
browsertrack start --verbose # Verbose logging mode
|
|
95
|
+
browsertrack stop # Stop background daemon
|
|
96
|
+
browsertrack status # Check status & connected sessions
|
|
97
|
+
|
|
98
|
+
browsertrack projects # List tracked projects
|
|
99
|
+
browsertrack project add myapp --origin http://localhost:5173 --path ~/projects/myapp
|
|
100
|
+
|
|
101
|
+
browsertrack errors # List open runtime errors
|
|
102
|
+
browsertrack errors --project myapp
|
|
103
|
+
|
|
104
|
+
browsertrack clear # Clear stored logs & incidents
|
|
105
|
+
browsertrack mcp # Run MCP server over stdio
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 🛡️ Security & Privacy
|
|
111
|
+
|
|
112
|
+
- **Localhost Only**: Daemon strictly binds to `127.0.0.1`.
|
|
113
|
+
- **Sensitive Key Redaction**: Powered by `@visulima/redact` — `Authorization`, `Cookie`, `Set-Cookie`, passwords, API tokens, bearer tokens, JWTs, AWS credentials, credit cards, SSNs, and secret query parameters are automatically sanitized.
|
|
114
|
+
- **No Form Values**: Form inputs (`<input>`, `<textarea>`) are never inspected or collected.
|
|
115
|
+
- **Zero Request/Response Body**: Payload bodies are disabled by default.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 🏗️ Architecture
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
Browser Client (Vanilla / React / Vue / Vite)
|
|
123
|
+
↕ WebSocket (ws://127.0.0.1:7331)
|
|
124
|
+
Local Daemon (SQLite Persistence + Verification Engine)
|
|
125
|
+
↕ MCP (stdio)
|
|
126
|
+
Coding Agents (Antigravity / Cursor / Claude Code)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🧪 Testing
|
|
132
|
+
|
|
133
|
+
Run the automated test suite:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm test
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Start the interactive fixture app for manual and end-to-end verification:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx serve examples/test-app
|
|
143
|
+
```
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// packages/core/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
|
+
// packages/core/src/redaction.ts
|
|
61
|
+
import { redact as visulimaRedact, standardRules } from "@visulima/redact";
|
|
62
|
+
var REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
63
|
+
var SENSITIVE_KEY_PATTERNS = [
|
|
64
|
+
/^authorization$/i,
|
|
65
|
+
/^cookie$/i,
|
|
66
|
+
/^set-cookie$/i,
|
|
67
|
+
/password/i,
|
|
68
|
+
/token/i,
|
|
69
|
+
/secret/i,
|
|
70
|
+
/api[-_]?key/i,
|
|
71
|
+
/access[-_]?token/i,
|
|
72
|
+
/refresh[-_]?token/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 BROWSER_SECURITY_RULES = [
|
|
93
|
+
{ deep: true, key: "password", replacement: REDACTED_PLACEHOLDER },
|
|
94
|
+
{ deep: true, key: "secret", replacement: REDACTED_PLACEHOLDER },
|
|
95
|
+
{ deep: true, key: "token", replacement: REDACTED_PLACEHOLDER },
|
|
96
|
+
{ deep: true, key: "authorization", replacement: REDACTED_PLACEHOLDER },
|
|
97
|
+
{ deep: true, key: "cookie", replacement: REDACTED_PLACEHOLDER },
|
|
98
|
+
{ deep: true, key: "set-cookie", replacement: REDACTED_PLACEHOLDER },
|
|
99
|
+
{ deep: true, key: "apikey", replacement: REDACTED_PLACEHOLDER },
|
|
100
|
+
{ deep: true, key: "api_key", replacement: REDACTED_PLACEHOLDER },
|
|
101
|
+
{ deep: true, key: "creditcard", pattern: /(?:\d[ -]*?){13,16}/, replacement: REDACTED_PLACEHOLDER },
|
|
102
|
+
{ deep: true, key: "cvv", replacement: REDACTED_PLACEHOLDER },
|
|
103
|
+
{ deep: true, key: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/, replacement: REDACTED_PLACEHOLDER },
|
|
104
|
+
{ deep: true, key: "awsid", pattern: /\bAKIA[0-9A-Z]{16}\b/, replacement: REDACTED_PLACEHOLDER },
|
|
105
|
+
{ deep: true, key: "awskey", pattern: /\b[0-9a-zA-Z/+]{40}\b/, replacement: REDACTED_PLACEHOLDER },
|
|
106
|
+
{ deep: true, key: "jwt", pattern: /\beyJ[0-9a-zA-Z_\-]*\.[0-9a-zA-Z_\-]*\.[0-9a-zA-Z_\-]*\b/, replacement: REDACTED_PLACEHOLDER },
|
|
107
|
+
{ deep: true, key: "slack_token", pattern: /\bxox[baprs]-[0-9a-zA-Z]{10,48}\b/, replacement: REDACTED_PLACEHOLDER }
|
|
108
|
+
];
|
|
109
|
+
function isSensitiveKey(key) {
|
|
110
|
+
if (!key) return false;
|
|
111
|
+
const cleaned = key.replace(/[-_]/g, "");
|
|
112
|
+
return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));
|
|
113
|
+
}
|
|
114
|
+
function redactUrl(rawUrl) {
|
|
115
|
+
if (!rawUrl) return rawUrl;
|
|
116
|
+
try {
|
|
117
|
+
const isRelative = !rawUrl.startsWith("http://") && !rawUrl.startsWith("https://") && !rawUrl.startsWith("ws://") && !rawUrl.startsWith("wss://");
|
|
118
|
+
const base = "http://localhost";
|
|
119
|
+
const parsed = new URL(rawUrl, base);
|
|
120
|
+
let changed = false;
|
|
121
|
+
for (const param of SENSITIVE_QUERY_PARAMS) {
|
|
122
|
+
if (parsed.searchParams.has(param)) {
|
|
123
|
+
parsed.searchParams.set(param, REDACTED_PLACEHOLDER);
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const key of Array.from(parsed.searchParams.keys())) {
|
|
128
|
+
if (isSensitiveKey(key)) {
|
|
129
|
+
parsed.searchParams.set(key, REDACTED_PLACEHOLDER);
|
|
130
|
+
changed = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!changed) return rawUrl;
|
|
134
|
+
let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();
|
|
135
|
+
result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);
|
|
136
|
+
return result;
|
|
137
|
+
} catch {
|
|
138
|
+
let safe = rawUrl;
|
|
139
|
+
for (const param of SENSITIVE_QUERY_PARAMS) {
|
|
140
|
+
const reg = new RegExp(`([?&]${param}=)[^&#]+`, "gi");
|
|
141
|
+
safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);
|
|
142
|
+
}
|
|
143
|
+
return safe;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function redactHeaders(headers) {
|
|
147
|
+
if (!headers || typeof headers !== "object") return {};
|
|
148
|
+
const safe = {};
|
|
149
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
150
|
+
if (isSensitiveKey(key)) {
|
|
151
|
+
safe[key] = REDACTED_PLACEHOLDER;
|
|
152
|
+
} else {
|
|
153
|
+
safe[key] = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return safe;
|
|
157
|
+
}
|
|
158
|
+
function redactSensitiveData(data, maxDepth = 6, currentDepth = 0) {
|
|
159
|
+
if (data === null || data === void 0) return data;
|
|
160
|
+
if (typeof data !== "object") return data;
|
|
161
|
+
if (currentDepth > maxDepth) return "[DEPTH_EXCEEDED]";
|
|
162
|
+
if (Array.isArray(data)) {
|
|
163
|
+
return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1));
|
|
164
|
+
}
|
|
165
|
+
const result = {};
|
|
166
|
+
for (const [key, value] of Object.entries(data)) {
|
|
167
|
+
if (typeof value === "object" && value !== null) {
|
|
168
|
+
result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);
|
|
169
|
+
} else if (isSensitiveKey(key)) {
|
|
170
|
+
result[key] = REDACTED_PLACEHOLDER;
|
|
171
|
+
} else if (typeof value === "string") {
|
|
172
|
+
if (value.startsWith("http://") || value.startsWith("https://") || value.includes("?")) {
|
|
173
|
+
result[key] = redactUrl(value);
|
|
174
|
+
} else {
|
|
175
|
+
try {
|
|
176
|
+
result[key] = visulimaRedact(value, BROWSER_SECURITY_RULES);
|
|
177
|
+
} catch {
|
|
178
|
+
result[key] = value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
result[key] = value;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// packages/core/src/selector.ts
|
|
189
|
+
function getSemanticSelector(element, options = {}) {
|
|
190
|
+
if (!element || !element.tagName) return "unknown";
|
|
191
|
+
const maxClasses = options.maxClasses ?? 2;
|
|
192
|
+
const testId = element.getAttribute("data-testid") || element.getAttribute("data-test") || element.getAttribute("data-cy") || element.getAttribute("data-qa");
|
|
193
|
+
if (testId) {
|
|
194
|
+
return `[data-testid="${testId}"]`;
|
|
195
|
+
}
|
|
196
|
+
const id = element.id;
|
|
197
|
+
if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {
|
|
198
|
+
return `#${id}`;
|
|
199
|
+
}
|
|
200
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
201
|
+
if (ariaLabel && ariaLabel.length < 30) {
|
|
202
|
+
return `${element.tagName.toLowerCase()}[aria-label="${ariaLabel}"]`;
|
|
203
|
+
}
|
|
204
|
+
const nameAttr = element.getAttribute("name");
|
|
205
|
+
if (nameAttr) {
|
|
206
|
+
return `${element.tagName.toLowerCase()}[name="${nameAttr}"]`;
|
|
207
|
+
}
|
|
208
|
+
const tag = element.tagName.toLowerCase();
|
|
209
|
+
const classList = Array.from(element.classList || []).filter((c) => !c.startsWith("css-") && !c.startsWith("_") && !/^[a-z0-9]{5,}$/i.test(c)).slice(0, maxClasses);
|
|
210
|
+
if (classList.length > 0) {
|
|
211
|
+
const classStr = classList.map((c) => `.${c}`).join("");
|
|
212
|
+
return `${tag}${classStr}`;
|
|
213
|
+
}
|
|
214
|
+
if (element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement) {
|
|
215
|
+
const parentSelector = getSemanticSelector(element.parentElement, { maxClasses: 1 });
|
|
216
|
+
if (parentSelector && parentSelector !== "unknown" && !parentSelector.includes(">")) {
|
|
217
|
+
return `${parentSelector} > ${tag}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return tag;
|
|
221
|
+
}
|
|
222
|
+
function truncate(str, maxLength = 200) {
|
|
223
|
+
if (!str) return "";
|
|
224
|
+
if (str.length <= maxLength) return str;
|
|
225
|
+
return str.slice(0, maxLength) + "...";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
normalizeErrorMessage,
|
|
230
|
+
normalizeSourceFile,
|
|
231
|
+
extractSourceFromStack,
|
|
232
|
+
computeFingerprint,
|
|
233
|
+
visulimaRedact,
|
|
234
|
+
standardRules,
|
|
235
|
+
REDACTED_PLACEHOLDER,
|
|
236
|
+
SENSITIVE_KEY_PATTERNS,
|
|
237
|
+
SENSITIVE_QUERY_PARAMS,
|
|
238
|
+
BROWSER_SECURITY_RULES,
|
|
239
|
+
isSensitiveKey,
|
|
240
|
+
redactUrl,
|
|
241
|
+
redactHeaders,
|
|
242
|
+
redactSensitiveData,
|
|
243
|
+
getSemanticSelector,
|
|
244
|
+
truncate
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=chunk-6VA7GBAO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../packages/core/src/fingerprint.ts","../packages/core/src/redaction.ts","../packages/core/src/selector.ts"],"sourcesContent":["/**\n * Error fingerprinting and normalization utilities\n */\n\nexport interface FingerprintInput {\n type: string;\n message: string;\n sourceFile?: string;\n line?: number;\n column?: number;\n stack?: string;\n}\n\n/**\n * Normalizes error messages by stripping dynamic content like UUIDs, timestamps,\n * hex addresses, and file timestamps.\n */\nexport function normalizeErrorMessage(message: string): string {\n if (!message) return 'unknown_error';\n\n return message\n // Normalize string representation\n .trim()\n // Replace hex addresses (e.g. 0x7ffee4b2a8)\n .replace(/0x[0-9a-fA-F]+/g, '0x<HEX>')\n // Replace UUIDs\n .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>')\n // Replace timestamp query params like ?t=1719283748 or ?v=1.2.3\n .replace(/\\?[tv]=[\\w.-]+/g, '')\n // Replace ISO timestamps\n .replace(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z?/g, '<TIMESTAMP>')\n // Replace numbers in quotes/brackets if likely IDs (e.g., user \"12345\")\n .replace(/#\\d+/g, '#<ID>')\n // Normalize whitespaces\n .replace(/\\s+/g, ' ');\n}\n\n/**\n * Normalizes source filenames by removing origin and cache-busting query strings.\n */\nexport function normalizeSourceFile(filename?: string): string {\n if (!filename) return 'unknown_source';\n\n let cleaned = filename.trim();\n // Strip protocol and origin if present (e.g. http://localhost:5173/src/App.tsx?t=123)\n try {\n if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {\n const url = new URL(cleaned);\n cleaned = url.pathname;\n }\n } catch {\n // fallback string manipulation\n cleaned = cleaned.replace(/^https?:\\/\\/[^/]+/, '');\n }\n\n // Remove query parameters\n cleaned = cleaned.split('?')[0].split('#')[0];\n\n // Normalize windows path separators\n cleaned = cleaned.replace(/\\\\/g, '/');\n\n return cleaned || 'unknown_source';\n}\n\n/**\n * Extracts source location from stack if not explicitly provided\n */\nexport function extractSourceFromStack(stack?: string): { file: string; line: number; column?: number } | null {\n if (!stack) return null;\n\n const lines = stack.split('\\n');\n for (const line of lines) {\n // Match standard stack line: at functionName (http://localhost:5173/src/main.tsx:15:3) or at http://...:15:3\n const match = line.match(/(?:at\\s+(?:.*?\\s+\\()?)?(https?:\\/\\/[^\\s)]+|file:\\/\\/[^\\s)]+|\\/[^\\s)]+):(\\d+):(\\d+)\\)?/);\n if (match) {\n return {\n file: normalizeSourceFile(match[1]),\n line: parseInt(match[2], 10),\n column: parseInt(match[3], 10),\n };\n }\n }\n return null;\n}\n\n/**\n * Fast synchronous djb2-based hash for universal client/server runtime\n */\nfunction djb2Hash(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 33) ^ str.charCodeAt(i);\n }\n // Convert to unsigned 32-bit hex string\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n\n/**\n * Computes a deterministic fingerprint for an error\n */\nexport function computeFingerprint(input: FingerprintInput): string {\n const normType = (input.type || 'Error').trim().toLowerCase();\n const normMsg = normalizeErrorMessage(input.message);\n\n let sourceFile = normalizeSourceFile(input.sourceFile);\n let line = input.line || 0;\n\n if ((sourceFile === 'unknown_source' || line === 0) && input.stack) {\n const extracted = extractSourceFromStack(input.stack);\n if (extracted) {\n sourceFile = extracted.file;\n line = extracted.line;\n }\n }\n\n const rawKey = `${normType}::${normMsg}::${sourceFile}::${line}`;\n const hash = djb2Hash(rawKey);\n\n return `fp_${hash}`;\n}\n","import { redact as visulimaRedact, standardRules, type Rules } from '@visulima/redact';\n\n/**\n * Security and Redaction Utilities powered by @visulima/redact\n */\n\nexport const REDACTED_PLACEHOLDER = '[REDACTED]';\n\nexport const SENSITIVE_KEY_PATTERNS = [\n /^authorization$/i,\n /^cookie$/i,\n /^set-cookie$/i,\n /password/i,\n /token/i,\n /secret/i,\n /api[-_]?key/i,\n /access[-_]?token/i,\n /refresh[-_]?token/i,\n /credentials/i,\n /private[-_]?key/i,\n /ssn/i,\n /credit[-_]?card/i,\n /cvv/i,\n];\n\nexport const SENSITIVE_QUERY_PARAMS = [\n 'token',\n 'auth',\n 'key',\n 'apikey',\n 'api_key',\n 'secret',\n 'password',\n 'access_token',\n 'refresh_token',\n 'code',\n 'signature',\n];\n\n/**\n * Developer security rules for @visulima/redact\n */\nexport const BROWSER_SECURITY_RULES: Rules = [\n { deep: true, key: 'password', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'secret', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'token', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'authorization', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'cookie', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'set-cookie', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'apikey', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'api_key', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'creditcard', pattern: /(?:\\d[ -]*?){13,16}/, replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'cvv', replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'ssn', pattern: /\\b\\d{3}-\\d{2}-\\d{4}\\b/, replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'awsid', pattern: /\\bAKIA[0-9A-Z]{16}\\b/, replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'awskey', pattern: /\\b[0-9a-zA-Z/+]{40}\\b/, replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'jwt', pattern: /\\beyJ[0-9a-zA-Z_\\-]*\\.[0-9a-zA-Z_\\-]*\\.[0-9a-zA-Z_\\-]*\\b/, replacement: REDACTED_PLACEHOLDER },\n { deep: true, key: 'slack_token', pattern: /\\bxox[baprs]-[0-9a-zA-Z]{10,48}\\b/, replacement: REDACTED_PLACEHOLDER },\n];\n\nexport { visulimaRedact, standardRules };\nexport type { Rules };\n\n/**\n * Checks if a property name matches any sensitive pattern\n */\nexport function isSensitiveKey(key: string): boolean {\n if (!key) return false;\n const cleaned = key.replace(/[-_]/g, '');\n return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));\n}\n\n/**\n * Redacts sensitive query parameters from a URL string\n */\nexport function redactUrl(rawUrl: string): string {\n if (!rawUrl) return rawUrl;\n\n try {\n const isRelative =\n !rawUrl.startsWith('http://') &&\n !rawUrl.startsWith('https://') &&\n !rawUrl.startsWith('ws://') &&\n !rawUrl.startsWith('wss://');\n const base = 'http://localhost';\n const parsed = new URL(rawUrl, base);\n\n let changed = false;\n for (const param of SENSITIVE_QUERY_PARAMS) {\n if (parsed.searchParams.has(param)) {\n parsed.searchParams.set(param, REDACTED_PLACEHOLDER);\n changed = true;\n }\n }\n\n // Also check all query params against isSensitiveKey\n for (const key of Array.from(parsed.searchParams.keys())) {\n if (isSensitiveKey(key)) {\n parsed.searchParams.set(key, REDACTED_PLACEHOLDER);\n changed = true;\n }\n }\n\n if (!changed) return rawUrl;\n\n let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();\n // Normalize URL-encoded brackets %5BREDACTED%5D back to [REDACTED]\n result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);\n return result;\n } catch {\n // If URL parsing fails, perform regex replace\n let safe = rawUrl;\n for (const param of SENSITIVE_QUERY_PARAMS) {\n const reg = new RegExp(`([?&]${param}=)[^&#]+`, 'gi');\n safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);\n }\n return safe;\n }\n}\n\n/**\n * Redacts sensitive HTTP headers\n */\nexport function redactHeaders(headers: Record<string, string>): Record<string, string> {\n if (!headers || typeof headers !== 'object') return {};\n\n const safe: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (isSensitiveKey(key)) {\n safe[key] = REDACTED_PLACEHOLDER;\n } else {\n safe[key] = value;\n }\n }\n return safe;\n}\n\n/**\n * Deeply redacts sensitive keys and payload data from an object or array,\n * leveraging @visulima/redact rules combined with BrowserTrack URL/header sanitizers.\n */\nexport function redactSensitiveData<T = any>(data: T, maxDepth = 6, currentDepth = 0): T {\n if (data === null || data === undefined) return data;\n if (typeof data !== 'object') return data;\n if (currentDepth > maxDepth) return '[DEPTH_EXCEEDED]' as any;\n\n if (Array.isArray(data)) {\n return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1)) as any;\n }\n\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(data as Record<string, any>)) {\n if (typeof value === 'object' && value !== null) {\n result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);\n } else if (isSensitiveKey(key)) {\n result[key] = REDACTED_PLACEHOLDER;\n } else if (typeof value === 'string') {\n // Check if value is a URL containing secrets\n if (value.startsWith('http://') || value.startsWith('https://') || value.includes('?')) {\n result[key] = redactUrl(value);\n } else {\n try {\n result[key] = visulimaRedact(value, BROWSER_SECURITY_RULES);\n } catch {\n result[key] = value;\n }\n }\n } else {\n result[key] = value;\n }\n }\n\n return result as T;\n}\n","/**\n * Semantic element selector generation and inspection\n */\n\nexport interface SelectorOptions {\n maxClasses?: number;\n maxOuterHTMLLength?: number;\n maxInnerTextLength?: number;\n}\n\n/**\n * Builds a clean, human-readable semantic selector for a DOM element.\n * Prioritizes data-testid/data-test, id, semantic tag + specific class, or concise parent path.\n */\nexport function getSemanticSelector(element: HTMLElement | Element, options: SelectorOptions = {}): string {\n if (!element || !element.tagName) return 'unknown';\n\n const maxClasses = options.maxClasses ?? 2;\n\n // 1. Check for standard test attributes\n const testId =\n element.getAttribute('data-testid') ||\n element.getAttribute('data-test') ||\n element.getAttribute('data-cy') ||\n element.getAttribute('data-qa');\n\n if (testId) {\n return `[data-testid=\"${testId}\"]`;\n }\n\n // 2. Check for meaningful ID (skip dynamic UUID/numeric IDs)\n const id = element.id;\n if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {\n return `#${id}`;\n }\n\n // 3. Check for aria-label or name on buttons/inputs\n const ariaLabel = element.getAttribute('aria-label');\n if (ariaLabel && ariaLabel.length < 30) {\n return `${element.tagName.toLowerCase()}[aria-label=\"${ariaLabel}\"]`;\n }\n\n const nameAttr = element.getAttribute('name');\n if (nameAttr) {\n return `${element.tagName.toLowerCase()}[name=\"${nameAttr}\"]`;\n }\n\n // 4. Tag + curated classes\n const tag = element.tagName.toLowerCase();\n const classList = Array.from(element.classList || [])\n .filter((c) => !c.startsWith('css-') && !c.startsWith('_') && !/^[a-z0-9]{5,}$/i.test(c)) // filter hashed utility classes\n .slice(0, maxClasses);\n\n if (classList.length > 0) {\n const classStr = classList.map((c) => `.${c}`).join('');\n return `${tag}${classStr}`;\n }\n\n // 5. If generic element (div, span, p) without attributes, try parent context\n if (element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement) {\n const parentSelector = getSemanticSelector(element.parentElement, { maxClasses: 1 });\n if (parentSelector && parentSelector !== 'unknown' && !parentSelector.includes('>')) {\n return `${parentSelector} > ${tag}`;\n }\n }\n\n return tag;\n}\n\n/**\n * Truncates string to specified max length with ellipsis\n */\nexport function truncate(str: string | undefined | null, maxLength = 200): string {\n if (!str) return '';\n if (str.length <= maxLength) return str;\n return str.slice(0, maxLength) + '...';\n}\n"],"mappings":"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","names":[]}
|