@omniaura/solid-pulse 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.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @omniaura/solid-pulse
2
+
3
+ > Flash what actually updates in a SolidJS app — and give agents the same controls as humans.
4
+
5
+ `solid-pulse` is dev-only instrumentation for Solid. It watches the reactive graph through Solid's official dev hooks, the DOM through a `MutationObserver`, and the network through `fetch`/`WebSocket`/`EventSource`, and turns what it sees into precisely named events:
6
+
7
+ | Event | Meaning |
8
+ | --- | --- |
9
+ | `solid.flush` | a reactive update completed: *n* computations re-ran, by kind (`memo`/`computed`/`effect`/`render`) and by owning component |
10
+ | `solid.computation` | one computation re-ran (only with `verboseComputations`) |
11
+ | `solid.component.mount` / `.dispose` / `.remount` | a component function ran / its owner was disposed / the same-named child was disposed and recreated within a flush (`gapMs`) |
12
+ | `solid.root` | a reactive root was created (multiple roots are counted, not conflated) |
13
+ | `dom.mutation` | the DOM actually changed (targets, attribute names, added/removed counts, attribution to the flush's component) |
14
+ | `dom.detach` | a subtree left the document, recording the `scrollTop` of every recently-scrolled descendant and whether focus was inside |
15
+ | `dom.reattach` | the **same node instance** came back: `gapMs`, `scrollReset: before→after`, `focusLost`, `suspenseInChain`, a stable `selector` |
16
+ | `focus.lost` | the focused element vanished from the document |
17
+ | `net.fetch.start/end/error` | fetch lifecycle; SSE responses detected by content-type and counted frame by frame; aborts flagged |
18
+ | `net.ws.open/message/close/error` | WebSocket lifecycle with subprotocols, direction, counts, `type` field of JSON frames, close codes |
19
+ | `net.sse.*` | `EventSource` lifecycle (and fetch-based SSE) |
20
+ | `query.observe` | a Solid Query observer subscribed — `role: "initiating"` (first observer, its mount causes the fetch) or `"sharing"` (query already had observers) — attributed to the Solid component |
21
+ | `query.fetch.start/success/error` | with `trigger: observer-mount \| invalidation \| background-refetch \| initial` |
22
+ | `query.invalidate`, `query.update`, `query.unobserve`, `query.added/removed`, `mutation.*` | the rest of the cache lifecycle |
23
+
24
+ There is deliberately **no "rerender" event**: Solid never re-runs component bodies. If one ever did, you would see a `pulse.note` saying so.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ bun add -d @omniaura/solid-pulse
30
+ ```
31
+
32
+ ```ts
33
+ // vite.config.ts — dev server only (`apply: "serve"`), production builds never include it
34
+ import solidPulse from "@omniaura/solid-pulse/vite";
35
+ export default defineConfig({ plugins: [solidPulse(), solid()] });
36
+ ```
37
+
38
+ The plugin injects the runtime as the first module script (so Solid's dev hooks are installed before your first render), mounts the raw-DOM panel, mounts the bridge on the Vite dev server at `/__pulse`, and writes `node_modules/.vite/solid-pulse.json` so the CLI finds it.
39
+
40
+ Manual setup (no Vite, or custom options):
41
+
42
+ ```ts
43
+ import { initPulse } from "@omniaura/solid-pulse";
44
+ import { mountPanel } from "@omniaura/solid-pulse/panel";
45
+ if (import.meta.env.DEV) {
46
+ const pulse = initPulse({ bridge: "ws://localhost:4567/__pulse/ws" }); // or bridge: true for same-origin
47
+ mountPanel(pulse);
48
+ }
49
+ ```
50
+
51
+ ### Solid Query adapter (optional)
52
+
53
+ ```ts
54
+ import { attachQueryClient } from "@omniaura/solid-pulse/query";
55
+ attachQueryClient(pulse, queryClient);
56
+ ```
57
+
58
+ Read-only: it subscribes to the `QueryCache`/`MutationCache` and never changes query behaviour. With the Vite plugin, point `setupModule: "/src/pulse-setup.ts"` at a module that `export default (pulse) => { attachQueryClient(pulse, queryClient) }` — it is evaluated before your app's entry module, so the very first observers are attributed.
59
+
60
+ ### Overlay
61
+
62
+ A single `position: fixed; pointer-events: none` container draws flash rectangles (amber = DOM change, green = mount, red = reattach, blue = query, purple = highlight) and a transient badge centred on the component that initiates/observes a query, showing the redacted query key. No layout shift, no focus, capped at 48 live rectangles and 4 badges; respects `prefers-reduced-motion`.
63
+
64
+ ## Human ⇄ agent parity
65
+
66
+ Every panel control carries `data-command="<name>"` and calls `controller.run(name, args)`; the CLI and HTTP API call the same function. `solid-pulse commands` prints the contract — including where the human control lives — and the test suite asserts the two sets match.
67
+
68
+ ```bash
69
+ solid-pulse status # bridge + connected pages
70
+ solid-pulse commands # the contract
71
+ solid-pulse events kinds=dom,query limit=50 # buffered events through the active filters
72
+ solid-pulse tail kinds=dom.reattach,focus.lost # live (SSE)
73
+ solid-pulse features.set name=flash on=false
74
+ solid-pulse features.set name=verboseComputations on=true
75
+ solid-pulse filters.set kinds=query component=ChatFeed
76
+ solid-pulse inspect.components name=Chat
77
+ solid-pulse inspect.queries active=true
78
+ solid-pulse inspect.element selector='[data-testid=composer]' # source + component context (solid-grab aware)
79
+ solid-pulse dom.highlight selector='.scroll-view' all=true # show a human what the agent is looking at
80
+ solid-pulse record.start id=qa-1 · record.stop · export recording=qa-1 --json > qa-1.json
81
+ solid-pulse note text='step 3: switch thread'
82
+ solid-pulse panel.open tab=query · panel.close
83
+ solid-pulse query.invalidate key='["conversations"]'
84
+ solid-pulse scenario.select name=chat-stream-drop # when @omniaura/scenario-sim is attached
85
+ ```
86
+
87
+ Options: `--url http://host:port[/__pulse]` (or `SOLID_PULSE_URL`; auto-discovered from `node_modules/.vite/solid-pulse.json`), `--client <id>` when several tabs are connected, `--json`.
88
+
89
+ In-page: `window.__SOLID_PULSE__.run("events.list", { kinds: "dom" })` — handy from `agent-browser eval` or Playwright `page.evaluate`.
90
+
91
+ ### HTTP API (what the CLI uses)
92
+
93
+ ```
94
+ GET /__pulse/api/status
95
+ GET /__pulse/api/clients
96
+ GET /__pulse/api/commands?client=
97
+ POST /__pulse/api/command {client?, name, args}
98
+ GET /__pulse/api/command?name=&args=<json>&client=
99
+ GET /__pulse/api/events?client=&since=&kinds=&limit=
100
+ GET /__pulse/api/events/stream?client=&kinds= (text/event-stream)
101
+ ```
102
+
103
+ ## Security and overhead
104
+
105
+ - **Dev only.** The Vite plugin is `apply: "serve"`; the runtime refuses to initialise twice; nothing is imported in production builds (`bun run smoke` proves it against the example app).
106
+ - **Loopback only.** The bridge checks both the peer address and the `Host` header; `allowRemote: true` is an explicit opt-in.
107
+ - **Redaction.** URLs lose `token|key|secret|auth|session|ticket|password|signature|code` params, JWT and bearer tokens are scrubbed, headers are never captured, bodies only with the `captureBodies` feature (truncated).
108
+ - **Bounded.** Ring buffer (2000 events), per-flush aggregation (individual computations only in verbose mode), per-frame flash caps, per-stream message caps (first 200 then every 50th), recordings capped and limited to the last five.
109
+ - **Non-interfering.** The overlay and panel are raw DOM (no Solid), so they never appear in their own event stream, never take focus, and never shift layout. Query instrumentation is a cache subscription — no query behaviour changes.
110
+ - **Hooks, not guesses.** Solid instrumentation uses `DEV.hooks.afterCreateOwner`/`afterUpdate` from Solid's development build and chains any hook already installed (e.g. `solid-devtools`); in a production Solid build it becomes a no-op and says so.
111
+
112
+ ## Reading a Suspense flip
113
+
114
+ The bug class this was built for: a tracked `query.data` read under `<Suspense>` flips the boundary to its fallback and back within a couple of milliseconds, detaching the live surface — `scrollTop` resets to 0 and focus drops to `<body>` with no component cleanup. It reads as:
115
+
116
+ ```
117
+ dom.detach <ChatFeed> section[data-testid=chat] scrollers=1 had focus
118
+ dom.reattach <ChatFeed> section[data-testid=chat] gap=2.1ms SCROLL RESET 1834→0 FOCUS LOST (Suspense in chain)
119
+ focus.lost textarea — element removed from document
120
+ ```
121
+
122
+ `solid-pulse tail kinds=dom.reattach,focus.lost` while a scroll harness runs turns days of frame-by-frame probing into a one-line alarm.
123
+
124
+ ## solid-grab
125
+
126
+ If [`solid-grab`](https://github.com/omniaura/solid-grab) is installed, `inspect.element` returns its formatted source context (file:line:column and component chain) in addition to pulse's own attribution, and the panel's Grab tab explains Alt+click. solid-grab's `data-solid-component` attributes also improve the overlay's component→DOM mapping.
127
+
128
+ MIT © omniaura
@@ -0,0 +1,202 @@
1
+ import { Server, IncomingMessage, ServerResponse } from 'node:http';
2
+ import { WebSocket } from 'ws';
3
+
4
+ /** Fixed-capacity FIFO. Overwrites the oldest entry; never grows. */
5
+ declare class RingBuffer<T> {
6
+ readonly capacity: number;
7
+ private items;
8
+ private head;
9
+ private count;
10
+ /** Total number of pushes since creation (dropped + retained). */
11
+ pushed: number;
12
+ constructor(capacity: number);
13
+ get size(): number;
14
+ get dropped(): number;
15
+ push(item: T): void;
16
+ /** Oldest → newest. */
17
+ toArray(): T[];
18
+ clear(): void;
19
+ }
20
+
21
+ /**
22
+ * Event model. Every observation the runtime makes is one PulseEvent with a
23
+ * precise `kind`. Kinds are deliberately specific: Solid has no "rerender", so
24
+ * we never report one. What actually happens is one of:
25
+ *
26
+ * solid.flush a reactive flush completed (n computations re-ran)
27
+ * solid.computation one memo/effect/render-effect re-ran (verbose mode)
28
+ * solid.component.mount a component function ran (fresh mount, or hydrate)
29
+ * solid.component.dispose
30
+ * solid.component.remount same-named component disposed + mounted in one flush
31
+ * solid.root a reactive root was created (multiple roots are fine)
32
+ * dom.mutation the DOM actually changed (childList/attr/text)
33
+ * dom.detach a subtree left the document (with scroll/focus state)
34
+ * dom.reattach the same node instance came back (Suspense flip etc.)
35
+ * focus.lost the focused element vanished from the document
36
+ * net.fetch.* fetch lifecycle (start/end/error), SSE-aware
37
+ * net.ws.* WebSocket lifecycle (open/message/close/error)
38
+ * net.sse.* EventSource lifecycle
39
+ * query.* Solid Query cache/observer events (adapter)
40
+ * mutation.* Solid Query mutation cache events (adapter)
41
+ * pulse.* runtime lifecycle / control-plane notes
42
+ */
43
+ type PulseEventKind = "solid.flush" | "solid.computation" | "solid.component.mount" | "solid.component.dispose" | "solid.component.remount" | "solid.root" | "dom.mutation" | "dom.detach" | "dom.reattach" | "focus.lost" | "net.fetch.start" | "net.fetch.end" | "net.fetch.error" | "net.ws.open" | "net.ws.message" | "net.ws.close" | "net.ws.error" | "net.sse.open" | "net.sse.message" | "net.sse.error" | "net.sse.close" | "query.added" | "query.removed" | "query.observe" | "query.unobserve" | "query.fetch.start" | "query.fetch.success" | "query.fetch.error" | "query.invalidate" | "query.update" | "mutation.start" | "mutation.success" | "mutation.error" | "pulse.note";
44
+ interface ComponentRef {
45
+ id: number;
46
+ name: string;
47
+ /** Component ancestry, innermost first (names only). */
48
+ chain?: string[];
49
+ /** Source location if solid-grab's data-solid-source attribute was found. */
50
+ source?: string | null;
51
+ }
52
+ interface PulseEventBase {
53
+ /** Monotonic per-runtime sequence. */
54
+ seq: number;
55
+ /** performance.now() at capture. */
56
+ t: number;
57
+ /** Date.now() at capture (for cross-process correlation). */
58
+ wall: number;
59
+ kind: PulseEventKind;
60
+ /** Flush group the event belongs to, when attributable. */
61
+ flush?: number;
62
+ /** Component attribution, when known. */
63
+ component?: ComponentRef | null;
64
+ /** Free-form structured payload; shape depends on `kind`. */
65
+ data: Record<string, unknown>;
66
+ }
67
+ type PulseEvent = PulseEventBase;
68
+
69
+ interface CommandSpec {
70
+ /** Dotted command name, e.g. `features.set`. Also the CLI verb. */
71
+ name: string;
72
+ summary: string;
73
+ /** Argument name → short description. */
74
+ args?: Record<string, string>;
75
+ /** Where the equivalent human control lives in the panel (parity doc). */
76
+ ui?: string;
77
+ }
78
+ type CommandResult = {
79
+ ok: true;
80
+ value: unknown;
81
+ } | {
82
+ ok: false;
83
+ error: string;
84
+ };
85
+
86
+ /**
87
+ * Bridge protocol (JSON over WebSocket) between a page runtime and the bridge
88
+ * server, and the HTTP shape the server exposes to CLIs and agents.
89
+ *
90
+ * Page → server
91
+ * hello first frame; identifies the tab
92
+ * events batched PulseEvents (≤ 50 ms coalescing)
93
+ * result reply to a `command`
94
+ * Server → page
95
+ * command run a controller command; page answers with `result`
96
+ * welcome ack of hello with the server-assigned client id
97
+ */
98
+
99
+ interface ClientSummary {
100
+ clientId: string;
101
+ url: string;
102
+ title: string;
103
+ userAgent: string;
104
+ connectedWall: number;
105
+ lastSeenWall: number;
106
+ events: number;
107
+ commands: number;
108
+ }
109
+
110
+ /**
111
+ * Bridge server (Node/Bun). Accepts page runtimes over WebSocket at
112
+ * `<path>/ws`, mirrors their events into a per-client ring buffer, and exposes
113
+ * a small HTTP API for CLIs and agents at `<path>/api/*`:
114
+ *
115
+ * GET /api/status server + connected clients
116
+ * GET /api/clients
117
+ * GET /api/commands?client= the page's command contract
118
+ * POST /api/command {client?,name,args} run a command in the page
119
+ * GET /api/command?name=&args=<json>&client= (curl-friendly)
120
+ * GET /api/events?client=&since=&kinds=&limit=
121
+ * GET /api/events/stream?client=&kinds= SSE, one `pulse` event per PulseEvent
122
+ *
123
+ * Access: loopback only unless `allowRemote` is set — remote address and Host
124
+ * header are both checked, so a LAN dev server does not leak instrumentation.
125
+ */
126
+
127
+ interface BridgeServerOptions {
128
+ /** URL prefix (default `/__pulse`). */
129
+ path?: string;
130
+ /** Accept non-loopback peers and hosts (default false). */
131
+ allowRemote?: boolean;
132
+ /** Per-client mirror buffer (default 5000 events). */
133
+ bufferSize?: number;
134
+ /** Command timeout in ms (default 10000). */
135
+ commandTimeoutMs?: number;
136
+ log?: (message: string) => void;
137
+ }
138
+ interface ClientState {
139
+ ws: WebSocket;
140
+ summary: ClientSummary;
141
+ commands: CommandSpec[];
142
+ buffer: RingBuffer<PulseEvent>;
143
+ pending: Map<string, {
144
+ resolve: (r: CommandResult) => void;
145
+ timer: ReturnType<typeof setTimeout>;
146
+ }>;
147
+ }
148
+ type EventListener = (clientId: string, events: PulseEvent[]) => void;
149
+ declare class BridgeServer {
150
+ readonly path: string;
151
+ private clients;
152
+ private wss;
153
+ private listeners;
154
+ private nextCommand;
155
+ private startedWall;
156
+ private opts;
157
+ constructor(options?: BridgeServerOptions);
158
+ /** Attach the WebSocket upgrade handler to an existing http server (e.g. Vite's). */
159
+ attach(server: Server): void;
160
+ isAllowed(req: IncomingMessage): boolean;
161
+ /** Connect middleware: handles `<path>/api/*`; returns false when the URL is not ours. */
162
+ handleHttp(req: IncomingMessage, res: ServerResponse): boolean;
163
+ private route;
164
+ private noClientMessage;
165
+ private onConnection;
166
+ /** Pick a client: explicit id, else the most recently seen. */
167
+ pick(clientId?: string): ClientState | null;
168
+ listClients(): ClientSummary[];
169
+ status(): {
170
+ tool: string;
171
+ protocol: number;
172
+ path: string;
173
+ uptimeMs: number;
174
+ allowRemote: boolean;
175
+ clients: ClientSummary[];
176
+ };
177
+ events(clientId: string, opts?: {
178
+ since?: number;
179
+ kinds?: string[];
180
+ limit?: number;
181
+ }): PulseEvent[];
182
+ command(clientId: string, name: string, args?: Record<string, unknown>): Promise<CommandResult>;
183
+ onEvents(listener: EventListener): () => boolean;
184
+ close(): void;
185
+ }
186
+ interface StandaloneOptions extends BridgeServerOptions {
187
+ port?: number;
188
+ host?: string;
189
+ }
190
+ /** Run the bridge as its own process (for non-Vite setups): `solid-pulse bridge --port 4567`. */
191
+ declare function startBridgeServer(options?: StandaloneOptions): {
192
+ bridge: BridgeServer;
193
+ server: Server<typeof IncomingMessage, typeof ServerResponse>;
194
+ ready: Promise<{
195
+ url: string;
196
+ port: number;
197
+ }>;
198
+ url: string;
199
+ close: () => Promise<void>;
200
+ };
201
+
202
+ export { BridgeServer, type BridgeServerOptions, type StandaloneOptions, startBridgeServer };
package/dist/bridge.js ADDED
@@ -0,0 +1,10 @@
1
+ import {
2
+ BridgeServer,
3
+ startBridgeServer
4
+ } from "./chunk-TVSI7G5S.js";
5
+ import "./chunk-4QA2G6S3.js";
6
+ export {
7
+ BridgeServer,
8
+ startBridgeServer
9
+ };
10
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,15 @@
1
+ // src/core/protocol.ts
2
+ var PROTOCOL_VERSION = 1;
3
+ var DEFAULT_PATH = "/__pulse";
4
+ function isPageFrame(value) {
5
+ if (!value || typeof value !== "object") return false;
6
+ const t = value.type;
7
+ return t === "hello" || t === "events" || t === "result";
8
+ }
9
+
10
+ export {
11
+ PROTOCOL_VERSION,
12
+ DEFAULT_PATH,
13
+ isPageFrame
14
+ };
15
+ //# sourceMappingURL=chunk-4QA2G6S3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/protocol.ts"],"sourcesContent":["/**\n * Bridge protocol (JSON over WebSocket) between a page runtime and the bridge\n * server, and the HTTP shape the server exposes to CLIs and agents.\n *\n * Page → server\n * hello first frame; identifies the tab\n * events batched PulseEvents (≤ 50 ms coalescing)\n * result reply to a `command`\n * Server → page\n * command run a controller command; page answers with `result`\n * welcome ack of hello with the server-assigned client id\n */\n\nimport type { PulseEvent } from \"./events.js\";\nimport type { CommandResult, CommandSpec } from \"./controller.js\";\n\nexport const PROTOCOL_VERSION = 1;\nexport const DEFAULT_PATH = \"/__pulse\";\n\nexport interface HelloFrame {\n type: \"hello\";\n protocol: number;\n clientId: string;\n url: string;\n title: string;\n userAgent: string;\n commands: CommandSpec[];\n startedWall: number;\n}\n\nexport interface EventsFrame {\n type: \"events\";\n events: PulseEvent[];\n}\n\nexport interface ResultFrame {\n type: \"result\";\n id: string;\n result: CommandResult;\n}\n\nexport interface CommandFrame {\n type: \"command\";\n id: string;\n name: string;\n args: Record<string, unknown>;\n}\n\nexport interface WelcomeFrame {\n type: \"welcome\";\n clientId: string;\n protocol: number;\n}\n\nexport type PageFrame = HelloFrame | EventsFrame | ResultFrame;\nexport type ServerFrame = CommandFrame | WelcomeFrame;\n\nexport interface ClientSummary {\n clientId: string;\n url: string;\n title: string;\n userAgent: string;\n connectedWall: number;\n lastSeenWall: number;\n events: number;\n commands: number;\n}\n\nexport function isPageFrame(value: unknown): value is PageFrame {\n if (!value || typeof value !== \"object\") return false;\n const t = (value as { type?: unknown }).type;\n return t === \"hello\" || t === \"events\" || t === \"result\";\n}\n\nexport function isServerFrame(value: unknown): value is ServerFrame {\n if (!value || typeof value !== \"object\") return false;\n const t = (value as { type?: unknown }).type;\n return t === \"command\" || t === \"welcome\";\n}\n"],"mappings":";AAgBO,IAAM,mBAAmB;AACzB,IAAM,eAAe;AAmDrB,SAAS,YAAY,OAAoC;AAC9D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,IAAK,MAA6B;AACxC,SAAO,MAAM,WAAW,MAAM,YAAY,MAAM;AAClD;","names":[]}
@@ -0,0 +1,66 @@
1
+ // src/core/redact.ts
2
+ var SENSITIVE_PARAM = /(token|key|secret|auth|session|ticket|password|passwd|signature|sig|code)/i;
3
+ var SENSITIVE_HEADER = /^(authorization|cookie|set-cookie|x-api-key|x-auth-token|proxy-authorization)$/i;
4
+ var BEARER = /\b(bearer|basic)\s+[a-z0-9._~+/=-]{8,}/gi;
5
+ var JWT = /\beyJ[a-zA-Z0-9_-]{5,}\.[a-zA-Z0-9_-]{5,}\.[a-zA-Z0-9_-]{5,}\b/g;
6
+ var REDACTED = "[redacted]";
7
+ function redactUrl(input) {
8
+ let url;
9
+ try {
10
+ url = new URL(input, "http://localhost/");
11
+ } catch {
12
+ return redactText(input);
13
+ }
14
+ if (url.username || url.password) {
15
+ url.username = "";
16
+ url.password = "";
17
+ }
18
+ let changed = false;
19
+ for (const key of [...url.searchParams.keys()]) {
20
+ if (SENSITIVE_PARAM.test(key)) {
21
+ url.searchParams.set(key, REDACTED);
22
+ changed = true;
23
+ }
24
+ }
25
+ if (url.hash && SENSITIVE_PARAM.test(url.hash)) {
26
+ url.hash = "";
27
+ changed = true;
28
+ }
29
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(input)) {
30
+ const rel = url.pathname + url.search + url.hash;
31
+ return changed ? rel : redactText(input);
32
+ }
33
+ return redactText(url.toString());
34
+ }
35
+ function redactText(text) {
36
+ return text.replace(JWT, REDACTED).replace(BEARER, (_, scheme) => `${scheme} ${REDACTED}`);
37
+ }
38
+ function redactHeaders(headers) {
39
+ const out = {};
40
+ for (const [k, v] of headers) {
41
+ out[k.toLowerCase()] = SENSITIVE_HEADER.test(k) ? REDACTED : redactText(v);
42
+ }
43
+ return out;
44
+ }
45
+ function redactValue(value, depth = 0) {
46
+ if (depth > 6 || value === null || value === void 0) return value;
47
+ if (typeof value === "string") return redactText(value);
48
+ if (Array.isArray(value)) return value.map((v) => redactValue(v, depth + 1));
49
+ if (typeof value === "object") {
50
+ const out = {};
51
+ for (const [k, v] of Object.entries(value)) {
52
+ out[k] = SENSITIVE_PARAM.test(k) && typeof v === "string" ? REDACTED : redactValue(v, depth + 1);
53
+ }
54
+ return out;
55
+ }
56
+ return value;
57
+ }
58
+
59
+ export {
60
+ REDACTED,
61
+ redactUrl,
62
+ redactText,
63
+ redactHeaders,
64
+ redactValue
65
+ };
66
+ //# sourceMappingURL=chunk-5FYH2KEZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/redact.ts"],"sourcesContent":["/**\n * Redaction. Devtools traffic leaves the page (bridge, CLI, exports), so\n * anything that looks like a credential is scrubbed before it becomes an event.\n * Bodies are never captured unless `captureBodies` is turned on explicitly.\n */\n\nconst SENSITIVE_PARAM = /(token|key|secret|auth|session|ticket|password|passwd|signature|sig|code)/i;\nconst SENSITIVE_HEADER = /^(authorization|cookie|set-cookie|x-api-key|x-auth-token|proxy-authorization)$/i;\nconst BEARER = /\\b(bearer|basic)\\s+[a-z0-9._~+/=-]{8,}/gi;\nconst JWT = /\\beyJ[a-zA-Z0-9_-]{5,}\\.[a-zA-Z0-9_-]{5,}\\.[a-zA-Z0-9_-]{5,}\\b/g;\n\nexport const REDACTED = \"[redacted]\";\n\nexport function redactUrl(input: string): string {\n let url: URL;\n try {\n url = new URL(input, \"http://localhost/\");\n } catch {\n return redactText(input);\n }\n if (url.username || url.password) {\n url.username = \"\";\n url.password = \"\";\n }\n let changed = false;\n for (const key of [...url.searchParams.keys()]) {\n if (SENSITIVE_PARAM.test(key)) {\n url.searchParams.set(key, REDACTED);\n changed = true;\n }\n }\n if (url.hash && SENSITIVE_PARAM.test(url.hash)) {\n url.hash = \"\";\n changed = true;\n }\n // Preserve relative inputs as given (the base above is only for parsing).\n if (!/^[a-z][a-z0-9+.-]*:/i.test(input)) {\n const rel = url.pathname + url.search + url.hash;\n return changed ? rel : redactText(input);\n }\n return redactText(url.toString());\n}\n\nexport function redactText(text: string): string {\n return text.replace(JWT, REDACTED).replace(BEARER, (_, scheme: string) => `${scheme} ${REDACTED}`);\n}\n\nexport function redactHeaders(headers: Iterable<[string, string]>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of headers) {\n out[k.toLowerCase()] = SENSITIVE_HEADER.test(k) ? REDACTED : redactText(v);\n }\n return out;\n}\n\n/** Best-effort key redaction inside small structured payloads. */\nexport function redactValue<T>(value: T, depth = 0): T {\n if (depth > 6 || value === null || value === undefined) return value;\n if (typeof value === \"string\") return redactText(value) as T;\n if (Array.isArray(value)) return value.map((v) => redactValue(v, depth + 1)) as T;\n if (typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = SENSITIVE_PARAM.test(k) && typeof v === \"string\" ? REDACTED : redactValue(v, depth + 1);\n }\n return out as T;\n }\n return value;\n}\n"],"mappings":";AAMA,IAAM,kBAAkB;AACxB,IAAM,mBAAmB;AACzB,IAAM,SAAS;AACf,IAAM,MAAM;AAEL,IAAM,WAAW;AAEjB,SAAS,UAAU,OAAuB;AAC/C,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,OAAO,mBAAmB;AAAA,EAC1C,QAAQ;AACN,WAAO,WAAW,KAAK;AAAA,EACzB;AACA,MAAI,IAAI,YAAY,IAAI,UAAU;AAChC,QAAI,WAAW;AACf,QAAI,WAAW;AAAA,EACjB;AACA,MAAI,UAAU;AACd,aAAW,OAAO,CAAC,GAAG,IAAI,aAAa,KAAK,CAAC,GAAG;AAC9C,QAAI,gBAAgB,KAAK,GAAG,GAAG;AAC7B,UAAI,aAAa,IAAI,KAAK,QAAQ;AAClC,gBAAU;AAAA,IACZ;AAAA,EACF;AACA,MAAI,IAAI,QAAQ,gBAAgB,KAAK,IAAI,IAAI,GAAG;AAC9C,QAAI,OAAO;AACX,cAAU;AAAA,EACZ;AAEA,MAAI,CAAC,uBAAuB,KAAK,KAAK,GAAG;AACvC,UAAM,MAAM,IAAI,WAAW,IAAI,SAAS,IAAI;AAC5C,WAAO,UAAU,MAAM,WAAW,KAAK;AAAA,EACzC;AACA,SAAO,WAAW,IAAI,SAAS,CAAC;AAClC;AAEO,SAAS,WAAW,MAAsB;AAC/C,SAAO,KAAK,QAAQ,KAAK,QAAQ,EAAE,QAAQ,QAAQ,CAAC,GAAG,WAAmB,GAAG,MAAM,IAAI,QAAQ,EAAE;AACnG;AAEO,SAAS,cAAc,SAA6D;AACzF,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,GAAG,CAAC,KAAK,SAAS;AAC5B,QAAI,EAAE,YAAY,CAAC,IAAI,iBAAiB,KAAK,CAAC,IAAI,WAAW,WAAW,CAAC;AAAA,EAC3E;AACA,SAAO;AACT;AAGO,SAAS,YAAe,OAAU,QAAQ,GAAM;AACrD,MAAI,QAAQ,KAAK,UAAU,QAAQ,UAAU,OAAW,QAAO;AAC/D,MAAI,OAAO,UAAU,SAAU,QAAO,WAAW,KAAK;AACtD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC;AAC3E,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACrE,UAAI,CAAC,IAAI,gBAAgB,KAAK,CAAC,KAAK,OAAO,MAAM,WAAW,WAAW,YAAY,GAAG,QAAQ,CAAC;AAAA,IACjG;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}