brakit 0.6.2 → 0.7.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.
@@ -1,614 +1,711 @@
1
- // src/proxy/server.ts
2
- import { createServer } from "http";
3
-
4
- // src/proxy/handler.ts
5
- import {
6
- request as httpRequest
7
- } from "http";
8
- import { randomUUID } from "crypto";
9
-
10
- // src/constants/routes.ts
11
- var DASHBOARD_PREFIX = "/__brakit";
12
-
13
- // src/constants/limits.ts
14
- var MAX_REQUEST_ENTRIES = 1e3;
15
- var MAX_TELEMETRY_ENTRIES = 1e3;
16
-
17
- // src/constants/thresholds.ts
18
- var FLOW_GAP_MS = 5e3;
19
- var SLOW_REQUEST_THRESHOLD_MS = 2e3;
20
- var MIN_POLLING_SEQUENCE = 3;
21
- var ENDPOINT_TRUNCATE_LENGTH = 12;
22
- var N1_QUERY_THRESHOLD = 5;
23
- var ERROR_RATE_THRESHOLD_PCT = 20;
24
- var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
25
- var MIN_REQUESTS_FOR_INSIGHT = 2;
26
- var HIGH_QUERY_COUNT_PER_REQ = 5;
27
- var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
28
- var CROSS_ENDPOINT_PCT = 50;
29
- var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
30
- var REDUNDANT_QUERY_MIN_COUNT = 2;
31
- var LARGE_RESPONSE_BYTES = 51200;
32
- var HIGH_ROW_COUNT = 100;
33
- var OVERFETCH_MIN_REQUESTS = 2;
34
- var OVERFETCH_MIN_FIELDS = 8;
35
- var OVERFETCH_MIN_INTERNAL_IDS = 2;
36
- var OVERFETCH_NULL_RATIO = 0.3;
37
-
38
- // src/constants/headers.ts
39
- var BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
1
+ // src/detect/project.ts
2
+ import { readFile as readFile2 } from "fs/promises";
3
+ import { join } from "path";
40
4
 
41
- // src/proxy/static-patterns.ts
42
- var STATIC_PATTERNS = [
43
- /^\/_next\//,
44
- /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
45
- /^\/favicon/,
46
- /^\/__nextjs/
47
- ];
48
- function isStaticPath(urlPath) {
49
- return STATIC_PATTERNS.some((p) => p.test(urlPath));
5
+ // src/utils/fs.ts
6
+ import { access } from "fs/promises";
7
+ import { existsSync, readFileSync, writeFileSync } from "fs";
8
+ import { resolve } from "path";
9
+ async function fileExists(path) {
10
+ try {
11
+ await access(path);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
50
16
  }
51
17
 
52
- // src/store/request-store.ts
53
- function flattenHeaders(headers) {
54
- const flat = {};
55
- for (const [key, value] of Object.entries(headers)) {
56
- if (value === void 0) continue;
57
- flat[key] = Array.isArray(value) ? value.join(", ") : value;
18
+ // src/detect/project.ts
19
+ var FRAMEWORKS = [
20
+ { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
21
+ { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
22
+ { name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
23
+ { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] },
24
+ { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
25
+ ];
26
+ async function detectProject(rootDir) {
27
+ const pkgPath = join(rootDir, "package.json");
28
+ const raw = await readFile2(pkgPath, "utf-8");
29
+ const pkg = JSON.parse(raw);
30
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
31
+ let framework = "unknown";
32
+ let devCommand = "";
33
+ let devBin = "";
34
+ let defaultPort = 3e3;
35
+ for (const f of FRAMEWORKS) {
36
+ if (allDeps[f.dep]) {
37
+ framework = f.name;
38
+ devCommand = f.devCmd;
39
+ devBin = join(rootDir, "node_modules", ".bin", f.bin);
40
+ defaultPort = f.defaultPort;
41
+ break;
42
+ }
58
43
  }
59
- return flat;
44
+ const packageManager = await detectPackageManager(rootDir);
45
+ return { framework, devCommand, devBin, defaultPort, packageManager };
60
46
  }
61
- var RequestStore = class {
62
- constructor(maxEntries = MAX_REQUEST_ENTRIES) {
63
- this.maxEntries = maxEntries;
47
+ async function detectPackageManager(rootDir) {
48
+ if (await fileExists(join(rootDir, "bun.lockb"))) return "bun";
49
+ if (await fileExists(join(rootDir, "bun.lock"))) return "bun";
50
+ if (await fileExists(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
51
+ if (await fileExists(join(rootDir, "yarn.lock"))) return "yarn";
52
+ if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
53
+ return "unknown";
54
+ }
55
+
56
+ // src/instrument/adapter-registry.ts
57
+ var AdapterRegistry = class {
58
+ adapters = [];
59
+ active = [];
60
+ register(adapter) {
61
+ this.adapters.push(adapter);
64
62
  }
65
- requests = [];
66
- listeners = [];
67
- capture(input) {
68
- const url = input.url;
69
- const path = url.split("?")[0];
70
- let requestBodyStr = null;
71
- if (input.requestBody && input.requestBody.length > 0) {
72
- requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
73
- }
74
- let responseBodyStr = null;
75
- if (input.responseBody && input.responseBody.length > 0) {
76
- const ct = input.responseContentType;
77
- if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
78
- responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
63
+ patchAll(emit) {
64
+ for (const adapter of this.adapters) {
65
+ try {
66
+ if (adapter.detect()) {
67
+ adapter.patch(emit);
68
+ this.active.push(adapter);
69
+ }
70
+ } catch {
79
71
  }
80
72
  }
81
- const entry = {
82
- id: input.requestId,
83
- method: input.method,
84
- url,
85
- path,
86
- headers: flattenHeaders(input.requestHeaders),
87
- requestBody: requestBodyStr,
88
- statusCode: input.statusCode,
89
- responseHeaders: flattenHeaders(input.responseHeaders),
90
- responseBody: responseBodyStr,
91
- startedAt: input.startTime,
92
- durationMs: Math.round(performance.now() - input.startTime),
93
- responseSize: input.responseBody?.length ?? 0,
94
- isStatic: isStaticPath(path)
95
- };
96
- this.requests.push(entry);
97
- if (this.requests.length > this.maxEntries) {
98
- this.requests.shift();
99
- }
100
- for (const fn of this.listeners) {
101
- fn(entry);
102
- }
103
- return entry;
104
73
  }
105
- getAll() {
106
- return this.requests;
107
- }
108
- clear() {
109
- this.requests.length = 0;
110
- }
111
- onRequest(fn) {
112
- this.listeners.push(fn);
74
+ unpatchAll() {
75
+ for (const adapter of this.active) {
76
+ try {
77
+ adapter.unpatch?.();
78
+ } catch {
79
+ }
80
+ }
81
+ this.active = [];
113
82
  }
114
- offRequest(fn) {
115
- const idx = this.listeners.indexOf(fn);
116
- if (idx !== -1) this.listeners.splice(idx, 1);
83
+ getActive() {
84
+ return this.active;
117
85
  }
118
86
  };
119
87
 
120
- // src/proxy/request-log.ts
121
- var defaultStore = new RequestStore();
122
- var captureRequest = (input) => defaultStore.capture(input);
123
- var getRequests = () => defaultStore.getAll();
124
- var onRequest = (fn) => defaultStore.onRequest(fn);
125
- var offRequest = (fn) => defaultStore.offRequest(fn);
88
+ // src/analysis/rules/patterns.ts
89
+ var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
90
+ var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
91
+ var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
92
+ var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections/;
93
+ var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
94
+ var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
95
+ var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
96
+ var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
97
+ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
98
+ var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
99
+ var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
100
+ var INTERNAL_ID_SUFFIX = /Id$|_id$/;
101
+ var RULE_HINTS = {
102
+ "exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
103
+ "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
104
+ "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
105
+ "error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
106
+ "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
107
+ "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
108
+ "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
109
+ "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
110
+ };
126
111
 
127
- // src/proxy/handler.ts
128
- function proxyRequest(clientReq, clientRes, config) {
129
- const startTime = performance.now();
130
- const method = clientReq.method ?? "GET";
131
- const requestId = randomUUID();
132
- const shouldCaptureBody = method !== "GET" && method !== "HEAD";
133
- const bodyChunks = [];
134
- let bodySize = 0;
135
- if (shouldCaptureBody) {
136
- clientReq.on("data", (chunk) => {
137
- if (bodySize < config.maxBodyCapture) {
138
- bodyChunks.push(chunk);
139
- bodySize += chunk.length;
140
- }
141
- });
112
+ // src/analysis/rules/exposed-secret.ts
113
+ function tryParseJson(body) {
114
+ if (!body) return null;
115
+ try {
116
+ return JSON.parse(body);
117
+ } catch {
118
+ return null;
142
119
  }
143
- const proxyHeaders = { ...clientReq.headers };
144
- proxyHeaders["accept-encoding"] = "identity";
145
- proxyHeaders[BRAKIT_REQUEST_ID_HEADER] = requestId;
146
- const proxyReq = httpRequest(
147
- {
148
- hostname: "127.0.0.1",
149
- port: config.targetPort,
150
- path: clientReq.url,
151
- method,
152
- headers: proxyHeaders
153
- },
154
- (proxyRes) => {
155
- handleProxyResponse(
156
- clientReq,
157
- clientRes,
158
- proxyRes,
159
- startTime,
160
- shouldCaptureBody ? bodyChunks : [],
161
- config,
162
- requestId
163
- );
164
- }
165
- );
166
- proxyReq.on("error", (err) => {
167
- if (clientRes.headersSent) return;
168
- const code = err.code;
169
- if (code === "ECONNREFUSED" || code === "ECONNRESET") {
170
- clientRes.writeHead(502, { "content-type": "text/html" });
171
- clientRes.end(
172
- `<html><body style="font-family:system-ui;padding:40px;text-align:center"><h2>brakit</h2><p>Waiting for dev server on port ${config.targetPort}...</p><script>setTimeout(()=>location.reload(),2000)</script></body></html>`
173
- );
174
- } else {
175
- clientRes.writeHead(502, { "content-type": "text/plain" });
176
- clientRes.end(`brakit proxy error: ${err.message}
177
- `);
178
- }
179
- });
180
- clientReq.pipe(proxyReq);
181
120
  }
182
- function handleProxyResponse(clientReq, clientRes, proxyRes, startTime, bodyChunks, config, requestId) {
183
- const responseChunks = [];
184
- let responseSize = 0;
185
- clientRes.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
186
- proxyRes.on("data", (chunk) => {
187
- clientRes.write(chunk);
188
- if (responseSize < config.maxBodyCapture) {
189
- responseChunks.push(chunk);
190
- responseSize += chunk.length;
121
+ function findSecretKeys(obj, prefix) {
122
+ const found = [];
123
+ if (!obj || typeof obj !== "object") return found;
124
+ if (Array.isArray(obj)) {
125
+ for (let i = 0; i < Math.min(obj.length, 5); i++) {
126
+ found.push(...findSecretKeys(obj[i], prefix));
191
127
  }
192
- });
193
- proxyRes.on("end", () => {
194
- clientRes.end();
195
- const requestBody = bodyChunks.length > 0 ? Buffer.concat(bodyChunks) : null;
196
- const responseBody = responseChunks.length > 0 ? Buffer.concat(responseChunks) : null;
197
- captureRequest({
198
- requestId,
199
- method: clientReq.method ?? "GET",
200
- url: clientReq.url ?? "/",
201
- requestHeaders: clientReq.headers,
202
- requestBody,
203
- statusCode: proxyRes.statusCode ?? 0,
204
- responseHeaders: proxyRes.headers,
205
- responseBody,
206
- responseContentType: proxyRes.headers["content-type"] ?? "",
207
- startTime,
208
- config
209
- });
210
- });
211
- proxyRes.on("error", () => {
212
- clientRes.end();
213
- });
214
- }
215
-
216
- // src/proxy/websocket.ts
217
- import {
218
- request as httpRequest2
219
- } from "http";
220
- function handleUpgrade(req, clientSocket, head, targetPort) {
221
- const targetReq = httpRequest2({
222
- hostname: "127.0.0.1",
223
- port: targetPort,
224
- path: req.url,
225
- method: req.method,
226
- headers: req.headers
227
- });
228
- targetReq.on("upgrade", (_targetRes, targetSocket, targetHead) => {
229
- const statusLine = `HTTP/1.1 101 Switching Protocols`;
230
- const headerLines = [statusLine];
231
- if (_targetRes.headers) {
232
- for (const [key, value] of Object.entries(_targetRes.headers)) {
233
- if (value === void 0) continue;
234
- const vals = Array.isArray(value) ? value : [value];
235
- for (const v of vals) {
236
- headerLines.push(`${key}: ${v}`);
237
- }
238
- }
128
+ return found;
129
+ }
130
+ for (const k of Object.keys(obj)) {
131
+ const val = obj[k];
132
+ if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= 8 && !MASKED_RE.test(val)) {
133
+ found.push(k);
239
134
  }
240
- headerLines.push("", "");
241
- clientSocket.write(headerLines.join("\r\n"));
242
- if (targetHead.length > 0) {
243
- clientSocket.write(targetHead);
135
+ if (typeof val === "object" && val !== null) {
136
+ found.push(...findSecretKeys(val, prefix + k + "."));
244
137
  }
245
- targetSocket.pipe(clientSocket);
246
- clientSocket.pipe(targetSocket);
247
- targetSocket.on("error", () => clientSocket.destroy());
248
- clientSocket.on("error", () => targetSocket.destroy());
249
- });
250
- targetReq.on("error", () => {
251
- clientSocket.destroy();
252
- });
253
- targetReq.write(head);
254
- targetReq.end();
255
- }
256
-
257
- // src/analysis/group.ts
258
- import { randomUUID as randomUUID2 } from "crypto";
259
-
260
- // src/analysis/categorize.ts
261
- function detectCategory(req) {
262
- const { method, url, statusCode, responseHeaders } = req;
263
- if (req.isStatic) return "static";
264
- if (statusCode === 307 && (url.includes("__clerk_handshake") || url.includes("__clerk_db_jwt"))) {
265
- return "auth-handshake";
266
- }
267
- const effectivePath = getEffectivePath(req);
268
- if (/^\/api\/auth/i.test(effectivePath) || /^\/(api\/)?clerk/i.test(effectivePath)) {
269
- return "auth-check";
270
- }
271
- if (method === "POST" && !effectivePath.startsWith("/api/")) {
272
- return "server-action";
273
- }
274
- if (effectivePath.startsWith("/api/") && method !== "GET" && method !== "HEAD") {
275
- return "api-call";
276
138
  }
277
- if (effectivePath.startsWith("/api/") && method === "GET") {
278
- return "data-fetch";
279
- }
280
- if (url.includes("_rsc=")) {
281
- return "navigation";
282
- }
283
- if (responseHeaders["x-middleware-rewrite"]) {
284
- return "middleware";
285
- }
286
- if (method === "GET") {
287
- const ct = responseHeaders["content-type"] ?? "";
288
- if (ct.includes("text/html")) return "page-load";
289
- }
290
- return "unknown";
139
+ return found;
291
140
  }
292
- function getEffectivePath(req) {
293
- const rewrite = req.responseHeaders["x-middleware-rewrite"];
294
- if (!rewrite) return req.path;
295
- try {
296
- const url = new URL(rewrite, "http://localhost");
297
- return url.pathname;
298
- } catch {
299
- return rewrite.startsWith("/") ? rewrite : req.path;
141
+ var exposedSecretRule = {
142
+ id: "exposed-secret",
143
+ severity: "critical",
144
+ name: "Exposed Secret in Response",
145
+ hint: RULE_HINTS["exposed-secret"],
146
+ check(ctx) {
147
+ const findings = [];
148
+ const seen = /* @__PURE__ */ new Map();
149
+ for (const r of ctx.requests) {
150
+ if (r.statusCode >= 400) continue;
151
+ const parsed = tryParseJson(r.responseBody);
152
+ if (!parsed) continue;
153
+ const keys = findSecretKeys(parsed, "");
154
+ if (keys.length === 0) continue;
155
+ const ep = `${r.method} ${r.path}`;
156
+ const dedupKey = `${ep}:${keys.sort().join(",")}`;
157
+ const existing = seen.get(dedupKey);
158
+ if (existing) {
159
+ existing.count++;
160
+ continue;
161
+ }
162
+ const finding = {
163
+ severity: "critical",
164
+ rule: "exposed-secret",
165
+ title: "Exposed Secret in Response",
166
+ desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
167
+ hint: this.hint,
168
+ endpoint: ep,
169
+ count: 1
170
+ };
171
+ seen.set(dedupKey, finding);
172
+ findings.push(finding);
173
+ }
174
+ return findings;
300
175
  }
301
- }
176
+ };
302
177
 
303
- // src/analysis/label.ts
304
- function extractSourcePage(req) {
305
- const referer = req.headers["referer"] ?? req.headers["Referer"];
306
- if (!referer) return void 0;
307
- try {
308
- const url = new URL(referer);
309
- return url.pathname;
310
- } catch {
311
- return void 0;
178
+ // src/analysis/rules/token-in-url.ts
179
+ var tokenInUrlRule = {
180
+ id: "token-in-url",
181
+ severity: "critical",
182
+ name: "Auth Token in URL",
183
+ hint: RULE_HINTS["token-in-url"],
184
+ check(ctx) {
185
+ const findings = [];
186
+ const seen = /* @__PURE__ */ new Map();
187
+ for (const r of ctx.requests) {
188
+ const qIdx = r.url.indexOf("?");
189
+ if (qIdx === -1) continue;
190
+ const params = r.url.substring(qIdx + 1).split("&");
191
+ const flagged = [];
192
+ for (const param of params) {
193
+ const [name, ...rest] = param.split("=");
194
+ const val = rest.join("=");
195
+ if (SAFE_PARAMS.test(name)) continue;
196
+ if (TOKEN_PARAMS.test(name) && val && val.length > 0) {
197
+ flagged.push(name);
198
+ }
199
+ }
200
+ if (flagged.length === 0) continue;
201
+ const ep = `${r.method} ${r.path}`;
202
+ const dedupKey = `${ep}:${flagged.sort().join(",")}`;
203
+ const existing = seen.get(dedupKey);
204
+ if (existing) {
205
+ existing.count++;
206
+ continue;
207
+ }
208
+ const finding = {
209
+ severity: "critical",
210
+ rule: "token-in-url",
211
+ title: "Auth Token in URL",
212
+ desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
213
+ hint: this.hint,
214
+ endpoint: ep,
215
+ count: 1
216
+ };
217
+ seen.set(dedupKey, finding);
218
+ findings.push(finding);
219
+ }
220
+ return findings;
312
221
  }
313
- }
314
- function labelRequest(req) {
315
- const category = detectCategory(req);
316
- const label = generateHumanLabel(req, category);
317
- const sourcePage = extractSourcePage(req);
318
- return { ...req, category, label, sourcePage };
319
- }
320
- function generateHumanLabel(req, category) {
321
- const effectivePath = getEffectivePath(req);
322
- const endpointName = getEndpointName(effectivePath);
323
- const failed = req.statusCode >= 400;
324
- switch (category) {
325
- case "auth-handshake":
326
- return "Auth handshake";
327
- case "auth-check":
328
- return failed ? "Auth check failed" : "Checked auth";
329
- case "middleware": {
330
- const rewritePath = effectivePath !== req.path ? effectivePath : "";
331
- return rewritePath ? `Redirected to ${rewritePath}` : "Middleware";
222
+ };
223
+
224
+ // src/analysis/rules/stack-trace-leak.ts
225
+ var stackTraceLeakRule = {
226
+ id: "stack-trace-leak",
227
+ severity: "critical",
228
+ name: "Stack Trace Leaked to Client",
229
+ hint: RULE_HINTS["stack-trace-leak"],
230
+ check(ctx) {
231
+ const findings = [];
232
+ const seen = /* @__PURE__ */ new Map();
233
+ for (const r of ctx.requests) {
234
+ if (!r.responseBody) continue;
235
+ if (!STACK_TRACE_RE.test(r.responseBody)) continue;
236
+ const ep = `${r.method} ${r.path}`;
237
+ const existing = seen.get(ep);
238
+ if (existing) {
239
+ existing.count++;
240
+ continue;
241
+ }
242
+ const finding = {
243
+ severity: "critical",
244
+ rule: "stack-trace-leak",
245
+ title: "Stack Trace Leaked to Client",
246
+ desc: `${ep} \u2014 response exposes internal stack trace`,
247
+ hint: this.hint,
248
+ endpoint: ep,
249
+ count: 1
250
+ };
251
+ seen.set(ep, finding);
252
+ findings.push(finding);
332
253
  }
333
- case "server-action": {
334
- const name = prettifyEndpoint(req.path);
335
- return failed ? `${name} failed` : name;
254
+ return findings;
255
+ }
256
+ };
257
+
258
+ // src/analysis/rules/error-info-leak.ts
259
+ var CRITICAL_PATTERNS = [
260
+ { re: DB_CONN_RE, label: "database connection string" },
261
+ { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
262
+ { re: SECRET_VAL_RE, label: "secret value" }
263
+ ];
264
+ var errorInfoLeakRule = {
265
+ id: "error-info-leak",
266
+ severity: "critical",
267
+ name: "Sensitive Data in Error Response",
268
+ hint: RULE_HINTS["error-info-leak"],
269
+ check(ctx) {
270
+ const findings = [];
271
+ const seen = /* @__PURE__ */ new Map();
272
+ for (const r of ctx.requests) {
273
+ if (r.statusCode < 400) continue;
274
+ if (!r.responseBody) continue;
275
+ if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
276
+ const ep = `${r.method} ${r.path}`;
277
+ for (const p of CRITICAL_PATTERNS) {
278
+ if (!p.re.test(r.responseBody)) continue;
279
+ const dedupKey = `${ep}:${p.label}`;
280
+ const existing = seen.get(dedupKey);
281
+ if (existing) {
282
+ existing.count++;
283
+ continue;
284
+ }
285
+ const finding = {
286
+ severity: "critical",
287
+ rule: "error-info-leak",
288
+ title: "Sensitive Data in Error Response",
289
+ desc: `${ep} \u2014 error response exposes ${p.label}`,
290
+ hint: this.hint,
291
+ endpoint: ep,
292
+ count: 1
293
+ };
294
+ seen.set(dedupKey, finding);
295
+ findings.push(finding);
296
+ }
336
297
  }
337
- case "api-call": {
338
- const action = deriveActionVerb(req.method, endpointName);
339
- const name = prettifyEndpoint(endpointName);
340
- if (failed) return `${action} ${name} failed`;
341
- return `${action} ${name}`;
298
+ return findings;
299
+ }
300
+ };
301
+
302
+ // src/analysis/rules/insecure-cookie.ts
303
+ function isFrameworkResponse(r) {
304
+ if (r.statusCode >= 300 && r.statusCode < 400) return true;
305
+ if (r.path?.startsWith("/__")) return true;
306
+ if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
307
+ return false;
308
+ }
309
+ var insecureCookieRule = {
310
+ id: "insecure-cookie",
311
+ severity: "warning",
312
+ name: "Insecure Cookie",
313
+ hint: RULE_HINTS["insecure-cookie"],
314
+ check(ctx) {
315
+ const findings = [];
316
+ const seen = /* @__PURE__ */ new Map();
317
+ for (const r of ctx.requests) {
318
+ if (!r.responseHeaders) continue;
319
+ if (isFrameworkResponse(r)) continue;
320
+ const setCookie = r.responseHeaders["set-cookie"];
321
+ if (!setCookie) continue;
322
+ const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
323
+ for (const cookie of cookies) {
324
+ const cookieName = cookie.trim().split("=")[0].trim();
325
+ const lower = cookie.toLowerCase();
326
+ const issues = [];
327
+ if (!lower.includes("httponly")) issues.push("HttpOnly");
328
+ if (!lower.includes("samesite")) issues.push("SameSite");
329
+ if (issues.length === 0) continue;
330
+ const dedupKey = `${cookieName}:${issues.join(",")}`;
331
+ const existing = seen.get(dedupKey);
332
+ if (existing) {
333
+ existing.count++;
334
+ continue;
335
+ }
336
+ const finding = {
337
+ severity: "warning",
338
+ rule: "insecure-cookie",
339
+ title: "Insecure Cookie",
340
+ desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
341
+ hint: this.hint,
342
+ endpoint: cookieName,
343
+ count: 1
344
+ };
345
+ seen.set(dedupKey, finding);
346
+ findings.push(finding);
347
+ }
342
348
  }
343
- case "data-fetch": {
344
- const name = prettifyEndpoint(endpointName);
345
- if (failed) return `Failed to load ${name}`;
346
- return `Loaded ${name}`;
349
+ return findings;
350
+ }
351
+ };
352
+
353
+ // src/analysis/rules/sensitive-logs.ts
354
+ var sensitiveLogsRule = {
355
+ id: "sensitive-logs",
356
+ severity: "warning",
357
+ name: "Sensitive Data in Logs",
358
+ hint: RULE_HINTS["sensitive-logs"],
359
+ check(ctx) {
360
+ let count = 0;
361
+ for (const log of ctx.logs) {
362
+ if (!log.message) continue;
363
+ if (log.message.startsWith("[brakit]")) continue;
364
+ if (LOG_SECRET_RE.test(log.message)) count++;
347
365
  }
348
- case "page-load":
349
- return failed ? "Page error" : "Loaded page";
350
- case "navigation":
351
- return "Navigated";
352
- case "static":
353
- return `Static: ${req.path.split("/").pop() ?? req.path}`;
354
- default:
355
- return failed ? `${req.method} ${req.path} failed` : `${req.method} ${req.path}`;
366
+ if (count === 0) return [];
367
+ return [{
368
+ severity: "warning",
369
+ rule: "sensitive-logs",
370
+ title: "Sensitive Data in Logs",
371
+ desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
372
+ hint: this.hint,
373
+ endpoint: "console",
374
+ count
375
+ }];
356
376
  }
357
- }
358
- function prettifyEndpoint(name) {
359
- const cleaned = name.replace(/^\/api\//, "").replace(/\//g, " ").replace(/\.\.\./g, "").trim();
360
- if (!cleaned) return "data";
361
- return cleaned.split(" ").map((word) => {
362
- if (word.endsWith("ses") || word.endsWith("us") || word.endsWith("ss"))
363
- return word;
364
- if (word.endsWith("ies")) return word.slice(0, -3) + "y";
365
- if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
366
- return word;
367
- }).join(" ");
368
- }
369
- function deriveActionVerb(method, endpointName) {
370
- const lower = endpointName.toLowerCase();
371
- const VERB_PATTERNS = [
372
- [/enhance/, "Enhanced"],
373
- [/generate/, "Generated"],
374
- [/create/, "Created"],
375
- [/update/, "Updated"],
376
- [/delete|remove/, "Deleted"],
377
- [/send/, "Sent"],
378
- [/upload/, "Uploaded"],
379
- [/save/, "Saved"],
380
- [/submit/, "Submitted"],
381
- [/login|signin/, "Logged in"],
382
- [/logout|signout/, "Logged out"],
383
- [/register|signup/, "Registered"]
384
- ];
385
- for (const [pattern, verb] of VERB_PATTERNS) {
386
- if (pattern.test(lower)) return verb;
377
+ };
378
+
379
+ // src/analysis/rules/cors-credentials.ts
380
+ var corsCredentialsRule = {
381
+ id: "cors-credentials",
382
+ severity: "warning",
383
+ name: "CORS Credentials with Wildcard",
384
+ hint: RULE_HINTS["cors-credentials"],
385
+ check(ctx) {
386
+ const findings = [];
387
+ const seen = /* @__PURE__ */ new Set();
388
+ for (const r of ctx.requests) {
389
+ if (!r.responseHeaders) continue;
390
+ const origin = r.responseHeaders["access-control-allow-origin"];
391
+ const creds = r.responseHeaders["access-control-allow-credentials"];
392
+ if (origin !== "*" || creds !== "true") continue;
393
+ const ep = `${r.method} ${r.path}`;
394
+ if (seen.has(ep)) continue;
395
+ seen.add(ep);
396
+ findings.push({
397
+ severity: "warning",
398
+ rule: "cors-credentials",
399
+ title: "CORS Credentials with Wildcard",
400
+ desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
401
+ hint: this.hint,
402
+ endpoint: ep,
403
+ count: 1
404
+ });
405
+ }
406
+ return findings;
387
407
  }
388
- switch (method) {
389
- case "POST":
390
- return "Created";
391
- case "PUT":
392
- case "PATCH":
393
- return "Updated";
394
- case "DELETE":
395
- return "Deleted";
396
- default:
397
- return "Called";
408
+ };
409
+
410
+ // src/analysis/rules/response-pii-leak.ts
411
+ var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
412
+ var FULL_RECORD_MIN_FIELDS = 5;
413
+ var LIST_PII_MIN_ITEMS = 2;
414
+ function tryParseJson2(body) {
415
+ if (!body) return null;
416
+ try {
417
+ return JSON.parse(body);
418
+ } catch {
419
+ return null;
398
420
  }
399
421
  }
400
- function getEndpointName(path) {
401
- const parts = path.replace(/^\/api\//, "").split("/");
402
- if (parts.length <= 2) return parts.join("/");
403
- return parts.map((p) => p.length > ENDPOINT_TRUNCATE_LENGTH ? "..." : p).join("/");
404
- }
405
- function prettifyPageName(path) {
406
- const clean = path.replace(/^\//, "").replace(/\/$/, "");
407
- if (!clean) return "Home";
408
- return clean.split("/").map((s) => capitalize(s.replace(/[-_]/g, " "))).join(" ");
422
+ function findEmails(obj) {
423
+ const emails = [];
424
+ if (!obj || typeof obj !== "object") return emails;
425
+ if (Array.isArray(obj)) {
426
+ for (let i = 0; i < Math.min(obj.length, 10); i++) {
427
+ emails.push(...findEmails(obj[i]));
428
+ }
429
+ return emails;
430
+ }
431
+ for (const v of Object.values(obj)) {
432
+ if (typeof v === "string" && EMAIL_RE.test(v)) {
433
+ emails.push(v);
434
+ } else if (typeof v === "object" && v !== null) {
435
+ emails.push(...findEmails(v));
436
+ }
437
+ }
438
+ return emails;
409
439
  }
410
- function capitalize(s) {
411
- return s.charAt(0).toUpperCase() + s.slice(1);
440
+ function topLevelFieldCount(obj) {
441
+ if (Array.isArray(obj)) {
442
+ return obj.length > 0 ? topLevelFieldCount(obj[0]) : 0;
443
+ }
444
+ if (obj && typeof obj === "object") return Object.keys(obj).length;
445
+ return 0;
412
446
  }
413
-
414
- // src/analysis/transforms.ts
415
- function markDuplicates(requests) {
416
- const counts = /* @__PURE__ */ new Map();
417
- for (const req of requests) {
418
- if (req.category !== "data-fetch" && req.category !== "auth-check")
419
- continue;
420
- const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
421
- counts.set(key, (counts.get(key) ?? 0) + 1);
447
+ function hasInternalIds(obj) {
448
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return false;
449
+ for (const key of Object.keys(obj)) {
450
+ if (INTERNAL_ID_KEYS.test(key) || INTERNAL_ID_SUFFIX.test(key)) return true;
422
451
  }
423
- const isStrictMode = counts.size > 0 && [...counts.values()].every((c) => c === 2);
424
- const seen = /* @__PURE__ */ new Set();
425
- for (const req of requests) {
426
- if (req.category !== "data-fetch" && req.category !== "auth-check")
427
- continue;
428
- const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
429
- if (seen.has(key)) {
430
- if (isStrictMode) {
431
- req.isStrictModeDupe = true;
432
- } else {
433
- req.isDuplicate = true;
452
+ return false;
453
+ }
454
+ function unwrapResponse(parsed) {
455
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
456
+ const obj = parsed;
457
+ const keys = Object.keys(obj);
458
+ if (keys.length > 3) return parsed;
459
+ let best = null;
460
+ let bestSize = 0;
461
+ for (const key of keys) {
462
+ const val = obj[key];
463
+ if (Array.isArray(val) && val.length > bestSize) {
464
+ best = val;
465
+ bestSize = val.length;
466
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
467
+ const size = Object.keys(val).length;
468
+ if (size > bestSize) {
469
+ best = val;
470
+ bestSize = size;
434
471
  }
435
- } else {
436
- seen.add(key);
437
472
  }
438
473
  }
474
+ return best && bestSize >= 3 ? best : parsed;
439
475
  }
440
- function collapsePolling(requests) {
441
- const result = [];
442
- let i = 0;
443
- while (i < requests.length) {
444
- const current = requests[i];
445
- const currentEffective = getEffectivePath(current).split("?")[0];
446
- if (current.method === "GET" && current.category === "data-fetch") {
447
- let j = i + 1;
448
- while (j < requests.length && requests[j].method === "GET" && getEffectivePath(requests[j]).split("?")[0] === currentEffective) {
449
- j++;
476
+ function detectPII(method, reqBody, resBody) {
477
+ const target = unwrapResponse(resBody);
478
+ if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
479
+ const reqEmails = findEmails(reqBody);
480
+ if (reqEmails.length > 0) {
481
+ const resEmails = findEmails(target);
482
+ const echoed = reqEmails.filter((e) => resEmails.includes(e));
483
+ if (echoed.length > 0) {
484
+ const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
485
+ if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
486
+ return { reason: "echo", emailCount: echoed.length };
487
+ }
450
488
  }
451
- const count = j - i;
452
- if (count >= MIN_POLLING_SEQUENCE) {
453
- const last = requests[j - 1];
454
- const pollingDuration = last.startedAt + last.durationMs - current.startedAt;
455
- const endpointName = prettifyEndpoint(currentEffective);
456
- result.push({
457
- ...current,
458
- category: "polling",
459
- label: `Polling ${endpointName} (${count}x, ${formatDurationLabel(pollingDuration)})`,
460
- pollingCount: count,
461
- pollingDurationMs: pollingDuration,
462
- isDuplicate: false
463
- });
464
- i = j;
465
- continue;
489
+ }
490
+ }
491
+ if (target && typeof target === "object" && !Array.isArray(target)) {
492
+ const fields = topLevelFieldCount(target);
493
+ if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
494
+ const emails = findEmails(target);
495
+ if (emails.length > 0) {
496
+ return { reason: "full-record", emailCount: emails.length };
466
497
  }
467
498
  }
468
- result.push(current);
469
- i++;
470
499
  }
471
- return result;
472
- }
473
- function formatDurationLabel(ms) {
474
- if (ms < 1e3) return `${ms}ms`;
475
- return `${(ms / 1e3).toFixed(1)}s`;
500
+ if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
501
+ let itemsWithEmail = 0;
502
+ for (let i = 0; i < Math.min(target.length, 10); i++) {
503
+ const item = target[i];
504
+ if (item && typeof item === "object") {
505
+ const emails = findEmails(item);
506
+ if (emails.length > 0) itemsWithEmail++;
507
+ }
508
+ }
509
+ if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
510
+ const first = target[0];
511
+ if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
512
+ return { reason: "list-pii", emailCount: itemsWithEmail };
513
+ }
514
+ }
515
+ }
516
+ return null;
476
517
  }
477
- function detectWarnings(requests) {
478
- const warnings = [];
479
- const duplicateCount = requests.filter((r) => r.isDuplicate).length;
480
- if (duplicateCount > 0) {
481
- const unique = new Set(
482
- requests.filter((r) => r.isDuplicate).map((r) => `${r.method} ${getEffectivePath(r).split("?")[0]}`)
483
- );
484
- const endpoints = unique.size;
485
- const sameData = requests.filter((r) => r.isDuplicate).every((r) => {
486
- const key = `${r.method} ${getEffectivePath(r).split("?")[0]}`;
487
- const first = requests.find(
488
- (o) => !o.isDuplicate && `${o.method} ${getEffectivePath(o).split("?")[0]}` === key
489
- );
490
- return first && first.responseBody === r.responseBody;
491
- });
492
- const suffix = sameData ? " \u2014 same data loaded twice" : "";
493
- warnings.push(
494
- `${duplicateCount} request${duplicateCount > 1 ? "s" : ""} duplicated across ${endpoints} endpoint${endpoints > 1 ? "s" : ""}${suffix}`
495
- );
518
+ var REASON_LABELS = {
519
+ echo: "echoes back PII from the request body",
520
+ "full-record": "returns a full record with email and internal IDs",
521
+ "list-pii": "returns a list of records containing email addresses"
522
+ };
523
+ var responsePiiLeakRule = {
524
+ id: "response-pii-leak",
525
+ severity: "warning",
526
+ name: "PII Leak in Response",
527
+ hint: RULE_HINTS["response-pii-leak"],
528
+ check(ctx) {
529
+ const findings = [];
530
+ const seen = /* @__PURE__ */ new Map();
531
+ for (const r of ctx.requests) {
532
+ if (r.statusCode >= 400) continue;
533
+ const resJson = tryParseJson2(r.responseBody);
534
+ if (!resJson) continue;
535
+ const reqJson = tryParseJson2(r.requestBody);
536
+ const detection = detectPII(r.method, reqJson, resJson);
537
+ if (!detection) continue;
538
+ const ep = `${r.method} ${r.path}`;
539
+ const dedupKey = `${ep}:${detection.reason}`;
540
+ const existing = seen.get(dedupKey);
541
+ if (existing) {
542
+ existing.count++;
543
+ continue;
544
+ }
545
+ const finding = {
546
+ severity: "warning",
547
+ rule: "response-pii-leak",
548
+ title: "PII Leak in Response",
549
+ desc: `${ep} \u2014 ${REASON_LABELS[detection.reason]}`,
550
+ hint: this.hint,
551
+ endpoint: ep,
552
+ count: 1
553
+ };
554
+ seen.set(dedupKey, finding);
555
+ findings.push(finding);
556
+ }
557
+ return findings;
496
558
  }
497
- const slowRequests = requests.filter(
498
- (r) => r.durationMs > SLOW_REQUEST_THRESHOLD_MS && r.category !== "polling"
499
- );
500
- for (const req of slowRequests) {
501
- warnings.push(`${req.label} took ${(req.durationMs / 1e3).toFixed(1)}s`);
559
+ };
560
+
561
+ // src/analysis/rules/scanner.ts
562
+ var SecurityScanner = class {
563
+ rules = [];
564
+ register(rule) {
565
+ this.rules.push(rule);
502
566
  }
503
- const errors = requests.filter((r) => r.statusCode >= 500);
504
- for (const req of errors) {
505
- warnings.push(`${req.label} \u2014 server error (${req.statusCode})`);
567
+ scan(ctx) {
568
+ const findings = [];
569
+ for (const rule of this.rules) {
570
+ try {
571
+ findings.push(...rule.check(ctx));
572
+ } catch {
573
+ }
574
+ }
575
+ return findings;
506
576
  }
507
- return warnings;
577
+ getRules() {
578
+ return this.rules;
579
+ }
580
+ };
581
+ function createDefaultScanner() {
582
+ const scanner = new SecurityScanner();
583
+ scanner.register(exposedSecretRule);
584
+ scanner.register(tokenInUrlRule);
585
+ scanner.register(stackTraceLeakRule);
586
+ scanner.register(errorInfoLeakRule);
587
+ scanner.register(insecureCookieRule);
588
+ scanner.register(sensitiveLogsRule);
589
+ scanner.register(corsCredentialsRule);
590
+ scanner.register(responsePiiLeakRule);
591
+ return scanner;
592
+ }
593
+
594
+ // src/constants/routes.ts
595
+ var DASHBOARD_PREFIX = "/__brakit";
596
+
597
+ // src/constants/limits.ts
598
+ var MAX_REQUEST_ENTRIES = 1e3;
599
+ var MAX_TELEMETRY_ENTRIES = 1e3;
600
+
601
+ // src/constants/thresholds.ts
602
+ var FLOW_GAP_MS = 5e3;
603
+ var SLOW_REQUEST_THRESHOLD_MS = 2e3;
604
+ var MIN_POLLING_SEQUENCE = 3;
605
+ var ENDPOINT_TRUNCATE_LENGTH = 12;
606
+ var N1_QUERY_THRESHOLD = 5;
607
+ var ERROR_RATE_THRESHOLD_PCT = 20;
608
+ var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
609
+ var MIN_REQUESTS_FOR_INSIGHT = 2;
610
+ var HIGH_QUERY_COUNT_PER_REQ = 5;
611
+ var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
612
+ var CROSS_ENDPOINT_PCT = 50;
613
+ var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
614
+ var REDUNDANT_QUERY_MIN_COUNT = 2;
615
+ var LARGE_RESPONSE_BYTES = 51200;
616
+ var HIGH_ROW_COUNT = 100;
617
+ var OVERFETCH_MIN_REQUESTS = 2;
618
+ var OVERFETCH_MIN_FIELDS = 8;
619
+ var OVERFETCH_MIN_INTERNAL_IDS = 2;
620
+ var OVERFETCH_NULL_RATIO = 0.3;
621
+
622
+ // src/utils/static-patterns.ts
623
+ var STATIC_PATTERNS = [
624
+ /^\/_next\//,
625
+ /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
626
+ /^\/favicon/,
627
+ /^\/__nextjs/
628
+ ];
629
+ function isStaticPath(urlPath) {
630
+ return STATIC_PATTERNS.some((p) => p.test(urlPath));
508
631
  }
509
632
 
510
- // src/analysis/group.ts
511
- function groupRequestsIntoFlows(requests) {
512
- if (requests.length === 0) return [];
513
- const flows = [];
514
- let currentRequests = [];
515
- let currentSourcePage;
516
- let lastEndTime = 0;
517
- for (const req of requests) {
518
- if (req.path.startsWith(DASHBOARD_PREFIX)) continue;
519
- const labeled = labelRequest(req);
520
- if (labeled.category === "static") continue;
521
- const sourcePage = labeled.sourcePage;
522
- const gap = currentRequests.length > 0 ? req.startedAt - lastEndTime : 0;
523
- const isNewPage = currentRequests.length > 0 && sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
524
- const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
525
- const isTimeGap = currentRequests.length > 0 && gap > FLOW_GAP_MS;
526
- if (currentRequests.length > 0 && (isNewPage || isTimeGap || isPageLoad)) {
527
- flows.push(buildFlow(currentRequests));
528
- currentRequests = [];
529
- }
530
- currentRequests.push(labeled);
531
- currentSourcePage = sourcePage ?? currentSourcePage;
532
- lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
533
- }
534
- if (currentRequests.length > 0) {
535
- flows.push(buildFlow(currentRequests));
633
+ // src/store/request-store.ts
634
+ function flattenHeaders(headers) {
635
+ const flat = {};
636
+ for (const [key, value] of Object.entries(headers)) {
637
+ if (value === void 0) continue;
638
+ flat[key] = Array.isArray(value) ? value.join(", ") : value;
536
639
  }
537
- return flows;
538
- }
539
- function buildFlow(rawRequests) {
540
- markDuplicates(rawRequests);
541
- const requests = collapsePolling(rawRequests);
542
- const first = requests[0];
543
- const startTime = first.startedAt;
544
- const endTime = Math.max(
545
- ...requests.map(
546
- (r) => r.pollingDurationMs ? r.startedAt + r.pollingDurationMs : r.startedAt + r.durationMs
547
- )
548
- );
549
- const duplicateCount = rawRequests.filter((r) => r.isDuplicate).length;
550
- const nonStaticCount = rawRequests.length;
551
- const redundancyPct = nonStaticCount > 0 ? Math.round(duplicateCount / nonStaticCount * 100) : 0;
552
- const sourcePage = getDominantSourcePage(rawRequests);
553
- return {
554
- id: randomUUID2(),
555
- label: deriveFlowLabel(requests, sourcePage),
556
- requests,
557
- startTime,
558
- totalDurationMs: Math.round(endTime - startTime),
559
- hasErrors: requests.some((r) => r.statusCode >= 400),
560
- warnings: detectWarnings(rawRequests),
561
- sourcePage,
562
- redundancyPct
563
- };
640
+ return flat;
564
641
  }
565
- function getDominantSourcePage(requests) {
566
- const counts = /* @__PURE__ */ new Map();
567
- for (const req of requests) {
568
- if (req.sourcePage) {
569
- counts.set(req.sourcePage, (counts.get(req.sourcePage) ?? 0) + 1);
570
- }
642
+ var RequestStore = class {
643
+ constructor(maxEntries = MAX_REQUEST_ENTRIES) {
644
+ this.maxEntries = maxEntries;
571
645
  }
572
- let best = "";
573
- let bestCount = 0;
574
- for (const [page, count] of counts) {
575
- if (count > bestCount) {
576
- best = page;
577
- bestCount = count;
646
+ requests = [];
647
+ listeners = [];
648
+ capture(input) {
649
+ const url = input.url;
650
+ const path = url.split("?")[0];
651
+ let requestBodyStr = null;
652
+ if (input.requestBody && input.requestBody.length > 0) {
653
+ requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
654
+ }
655
+ let responseBodyStr = null;
656
+ if (input.responseBody && input.responseBody.length > 0) {
657
+ const ct = input.responseContentType;
658
+ if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
659
+ responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
660
+ }
661
+ }
662
+ const entry = {
663
+ id: input.requestId,
664
+ method: input.method,
665
+ url,
666
+ path,
667
+ headers: flattenHeaders(input.requestHeaders),
668
+ requestBody: requestBodyStr,
669
+ statusCode: input.statusCode,
670
+ responseHeaders: flattenHeaders(input.responseHeaders),
671
+ responseBody: responseBodyStr,
672
+ startedAt: input.startTime,
673
+ durationMs: Math.round(performance.now() - input.startTime),
674
+ responseSize: input.responseBody?.length ?? 0,
675
+ isStatic: isStaticPath(path)
676
+ };
677
+ this.requests.push(entry);
678
+ if (this.requests.length > this.maxEntries) {
679
+ this.requests.shift();
578
680
  }
681
+ for (const fn of this.listeners) {
682
+ fn(entry);
683
+ }
684
+ return entry;
579
685
  }
580
- return best || requests[0]?.path?.split("?")[0] || "/";
581
- }
582
- function deriveFlowLabel(requests, sourcePage) {
583
- const trigger = requests.find((r) => r.category === "api-call") ?? requests.find((r) => r.category === "server-action") ?? requests.find((r) => r.category === "page-load") ?? requests.find((r) => r.category === "navigation") ?? requests.find((r) => r.category === "data-fetch") ?? requests[0];
584
- if (trigger.category === "page-load" || trigger.category === "navigation") {
585
- const pageName = prettifyPageName(trigger.path.split("?")[0]);
586
- return `${pageName} Page`;
686
+ getAll() {
687
+ return this.requests;
587
688
  }
588
- if (trigger.category === "api-call") {
589
- const effectivePath = getEffectivePath(trigger);
590
- const parts = effectivePath.replace(/^\/api\//, "").split("/");
591
- const endpointName = parts.length <= 2 ? parts.join("/") : parts.map((p) => p.length > 12 ? "..." : p).join("/");
592
- const action = deriveActionVerb(trigger.method, endpointName);
593
- const name = prettifyEndpoint(endpointName);
594
- return `${action} ${capitalize(name)}`;
689
+ clear() {
690
+ this.requests.length = 0;
595
691
  }
596
- if (trigger.category === "server-action") {
597
- const name = prettifyEndpoint(trigger.path);
598
- return capitalize(name);
692
+ onRequest(fn) {
693
+ this.listeners.push(fn);
599
694
  }
600
- if (trigger.category === "data-fetch" || trigger.category === "polling") {
601
- if (sourcePage && sourcePage !== "/") {
602
- const pageName = prettifyPageName(sourcePage);
603
- return `${pageName} Page`;
604
- }
605
- return trigger.label;
695
+ offRequest(fn) {
696
+ const idx = this.listeners.indexOf(fn);
697
+ if (idx !== -1) this.listeners.splice(idx, 1);
606
698
  }
607
- return trigger.label;
608
- }
699
+ };
700
+
701
+ // src/store/request-log.ts
702
+ var defaultStore = new RequestStore();
703
+ var getRequests = () => defaultStore.getAll();
704
+ var onRequest = (fn) => defaultStore.onRequest(fn);
705
+ var offRequest = (fn) => defaultStore.offRequest(fn);
609
706
 
610
707
  // src/store/telemetry-store.ts
611
- import { randomUUID as randomUUID3 } from "crypto";
708
+ import { randomUUID } from "crypto";
612
709
  var TelemetryStore = class {
613
710
  constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
614
711
  this.maxEntries = maxEntries;
@@ -616,7 +713,7 @@ var TelemetryStore = class {
616
713
  entries = [];
617
714
  listeners = [];
618
715
  add(data) {
619
- const entry = { id: randomUUID3(), ...data };
716
+ const entry = { id: randomUUID(), ...data };
620
717
  this.entries.push(entry);
621
718
  if (this.entries.length > this.maxEntries) this.entries.shift();
622
719
  for (const fn of this.listeners) fn(entry);
@@ -661,7 +758,7 @@ var QueryStore = class extends TelemetryStore {
661
758
  var defaultQueryStore = new QueryStore();
662
759
 
663
760
  // src/store/metrics/metrics-store.ts
664
- import { randomUUID as randomUUID4 } from "crypto";
761
+ import { randomUUID as randomUUID2 } from "crypto";
665
762
 
666
763
  // src/store/metrics/persistence.ts
667
764
  import {
@@ -673,641 +770,357 @@ import {
673
770
  } from "fs";
674
771
  import { resolve as resolve2 } from "path";
675
772
 
676
- // src/utils/fs.ts
677
- import { access } from "fs/promises";
678
- import { existsSync, readFileSync, writeFileSync } from "fs";
679
- import { resolve } from "path";
680
- async function fileExists(path) {
681
- try {
682
- await access(path);
683
- return true;
684
- } catch {
685
- return false;
686
- }
687
- }
688
-
689
- // src/dashboard/client/constants/thresholds.ts
690
- var HEALTH_FAST_MS = 100;
691
- var HEALTH_GOOD_MS = 300;
692
- var HEALTH_OK_MS = 800;
693
- var HEALTH_SLOW_MS = 2e3;
694
-
695
- // src/dashboard/client/constants/display.ts
696
- var HEALTH_GRADES = `[
697
- { max: ${HEALTH_FAST_MS}, label: 'Fast', color: 'var(--green)', bg: 'rgba(22,163,74,0.08)', border: 'rgba(22,163,74,0.2)' },
698
- { max: ${HEALTH_GOOD_MS}, label: 'Good', color: 'var(--green)', bg: 'rgba(22,163,74,0.06)', border: 'rgba(22,163,74,0.15)' },
699
- { max: ${HEALTH_OK_MS}, label: 'OK', color: 'var(--amber)', bg: 'rgba(217,119,6,0.06)', border: 'rgba(217,119,6,0.15)' },
700
- { max: ${HEALTH_SLOW_MS}, label: 'Slow', color: 'var(--red)', bg: 'rgba(220,38,38,0.06)', border: 'rgba(220,38,38,0.15)' },
701
- { max: Infinity, label: 'Critical', color: 'var(--red)', bg: 'rgba(220,38,38,0.08)', border: 'rgba(220,38,38,0.2)' }
702
- ]`;
703
-
704
- // src/telemetry/index.ts
705
- import { platform, release, arch } from "os";
706
-
707
- // src/telemetry/config.ts
708
- import { homedir } from "os";
709
- import { join } from "path";
710
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
711
- import { randomUUID as randomUUID5 } from "crypto";
712
- var CONFIG_DIR = join(homedir(), ".brakit");
713
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
714
-
715
- // src/dashboard/router.ts
716
- function isDashboardRequest(url) {
717
- return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
718
- }
719
-
720
- // src/proxy/server.ts
721
- function createProxyServer(config, handleDashboard) {
722
- const server = createServer((clientReq, clientRes) => {
723
- if (isDashboardRequest(clientReq.url ?? "")) {
724
- handleDashboard(clientReq, clientRes, config);
725
- return;
726
- }
727
- proxyRequest(clientReq, clientRes, config);
728
- });
729
- server.on("upgrade", (req, socket, head) => {
730
- handleUpgrade(req, socket, head, config.targetPort);
731
- });
732
- return server;
733
- }
773
+ // src/analysis/group.ts
774
+ import { randomUUID as randomUUID3 } from "crypto";
734
775
 
735
- // src/detect/project.ts
736
- import { readFile as readFile2 } from "fs/promises";
737
- import { join as join2 } from "path";
738
- var FRAMEWORKS = [
739
- { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
740
- { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
741
- { name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
742
- { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] },
743
- { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
744
- ];
745
- async function detectProject(rootDir) {
746
- const pkgPath = join2(rootDir, "package.json");
747
- const raw = await readFile2(pkgPath, "utf-8");
748
- const pkg = JSON.parse(raw);
749
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
750
- let framework = "unknown";
751
- let devCommand = "";
752
- let devBin = "";
753
- let defaultPort = 3e3;
754
- for (const f of FRAMEWORKS) {
755
- if (allDeps[f.dep]) {
756
- framework = f.name;
757
- devCommand = f.devCmd;
758
- devBin = join2(rootDir, "node_modules", ".bin", f.bin);
759
- defaultPort = f.defaultPort;
760
- break;
761
- }
776
+ // src/analysis/categorize.ts
777
+ function detectCategory(req) {
778
+ const { method, url, statusCode, responseHeaders } = req;
779
+ if (req.isStatic) return "static";
780
+ if (statusCode === 307 && (url.includes("__clerk_handshake") || url.includes("__clerk_db_jwt"))) {
781
+ return "auth-handshake";
762
782
  }
763
- const packageManager = await detectPackageManager(rootDir);
764
- return { framework, devCommand, devBin, defaultPort, packageManager };
765
- }
766
- async function detectPackageManager(rootDir) {
767
- if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
768
- if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
769
- if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
770
- if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
771
- if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
772
- return "unknown";
773
- }
774
-
775
- // src/instrument/adapter-registry.ts
776
- var AdapterRegistry = class {
777
- adapters = [];
778
- active = [];
779
- register(adapter) {
780
- this.adapters.push(adapter);
783
+ const effectivePath = getEffectivePath(req);
784
+ if (/^\/api\/auth/i.test(effectivePath) || /^\/(api\/)?clerk/i.test(effectivePath)) {
785
+ return "auth-check";
781
786
  }
782
- patchAll(emit) {
783
- for (const adapter of this.adapters) {
784
- try {
785
- if (adapter.detect()) {
786
- adapter.patch(emit);
787
- this.active.push(adapter);
788
- }
789
- } catch {
790
- }
791
- }
787
+ if (method === "POST" && !effectivePath.startsWith("/api/")) {
788
+ return "server-action";
789
+ }
790
+ if (effectivePath.startsWith("/api/") && method !== "GET" && method !== "HEAD") {
791
+ return "api-call";
792
+ }
793
+ if (effectivePath.startsWith("/api/") && method === "GET") {
794
+ return "data-fetch";
795
+ }
796
+ if (url.includes("_rsc=")) {
797
+ return "navigation";
798
+ }
799
+ if (responseHeaders["x-middleware-rewrite"]) {
800
+ return "middleware";
792
801
  }
793
- unpatchAll() {
794
- for (const adapter of this.active) {
795
- try {
796
- adapter.unpatch?.();
797
- } catch {
798
- }
799
- }
800
- this.active = [];
802
+ if (method === "GET") {
803
+ const ct = responseHeaders["content-type"] ?? "";
804
+ if (ct.includes("text/html")) return "page-load";
801
805
  }
802
- getActive() {
803
- return this.active;
806
+ return "unknown";
807
+ }
808
+ function getEffectivePath(req) {
809
+ const rewrite = req.responseHeaders["x-middleware-rewrite"];
810
+ if (!rewrite) return req.path;
811
+ try {
812
+ const url = new URL(rewrite, "http://localhost");
813
+ return url.pathname;
814
+ } catch {
815
+ return rewrite.startsWith("/") ? rewrite : req.path;
804
816
  }
805
- };
806
-
807
- // src/analysis/rules/patterns.ts
808
- var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
809
- var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
810
- var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
811
- var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections/;
812
- var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
813
- var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
814
- var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
815
- var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
816
- var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
817
- var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
818
- var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
819
- var INTERNAL_ID_SUFFIX = /Id$|_id$/;
820
- var RULE_HINTS = {
821
- "exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
822
- "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
823
- "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
824
- "error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
825
- "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
826
- "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
827
- "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
828
- "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
829
- };
817
+ }
830
818
 
831
- // src/analysis/rules/exposed-secret.ts
832
- function tryParseJson(body) {
833
- if (!body) return null;
819
+ // src/analysis/label.ts
820
+ function extractSourcePage(req) {
821
+ const referer = req.headers["referer"] ?? req.headers["Referer"];
822
+ if (!referer) return void 0;
834
823
  try {
835
- return JSON.parse(body);
824
+ const url = new URL(referer);
825
+ return url.pathname;
836
826
  } catch {
837
- return null;
827
+ return void 0;
838
828
  }
839
829
  }
840
- function findSecretKeys(obj, prefix) {
841
- const found = [];
842
- if (!obj || typeof obj !== "object") return found;
843
- if (Array.isArray(obj)) {
844
- for (let i = 0; i < Math.min(obj.length, 5); i++) {
845
- found.push(...findSecretKeys(obj[i], prefix));
846
- }
847
- return found;
848
- }
849
- for (const k of Object.keys(obj)) {
850
- const val = obj[k];
851
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= 8 && !MASKED_RE.test(val)) {
852
- found.push(k);
853
- }
854
- if (typeof val === "object" && val !== null) {
855
- found.push(...findSecretKeys(val, prefix + k + "."));
856
- }
857
- }
858
- return found;
830
+ function labelRequest(req) {
831
+ const category = detectCategory(req);
832
+ const label = generateHumanLabel(req, category);
833
+ const sourcePage = extractSourcePage(req);
834
+ return { ...req, category, label, sourcePage };
859
835
  }
860
- var exposedSecretRule = {
861
- id: "exposed-secret",
862
- severity: "critical",
863
- name: "Exposed Secret in Response",
864
- hint: RULE_HINTS["exposed-secret"],
865
- check(ctx) {
866
- const findings = [];
867
- const seen = /* @__PURE__ */ new Map();
868
- for (const r of ctx.requests) {
869
- if (r.statusCode >= 400) continue;
870
- const parsed = tryParseJson(r.responseBody);
871
- if (!parsed) continue;
872
- const keys = findSecretKeys(parsed, "");
873
- if (keys.length === 0) continue;
874
- const ep = `${r.method} ${r.path}`;
875
- const dedupKey = `${ep}:${keys.sort().join(",")}`;
876
- const existing = seen.get(dedupKey);
877
- if (existing) {
878
- existing.count++;
879
- continue;
880
- }
881
- const finding = {
882
- severity: "critical",
883
- rule: "exposed-secret",
884
- title: "Exposed Secret in Response",
885
- desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
886
- hint: this.hint,
887
- endpoint: ep,
888
- count: 1
889
- };
890
- seen.set(dedupKey, finding);
891
- findings.push(finding);
892
- }
893
- return findings;
894
- }
895
- };
896
-
897
- // src/analysis/rules/token-in-url.ts
898
- var tokenInUrlRule = {
899
- id: "token-in-url",
900
- severity: "critical",
901
- name: "Auth Token in URL",
902
- hint: RULE_HINTS["token-in-url"],
903
- check(ctx) {
904
- const findings = [];
905
- const seen = /* @__PURE__ */ new Map();
906
- for (const r of ctx.requests) {
907
- const qIdx = r.url.indexOf("?");
908
- if (qIdx === -1) continue;
909
- const params = r.url.substring(qIdx + 1).split("&");
910
- const flagged = [];
911
- for (const param of params) {
912
- const [name, ...rest] = param.split("=");
913
- const val = rest.join("=");
914
- if (SAFE_PARAMS.test(name)) continue;
915
- if (TOKEN_PARAMS.test(name) && val && val.length > 0) {
916
- flagged.push(name);
917
- }
918
- }
919
- if (flagged.length === 0) continue;
920
- const ep = `${r.method} ${r.path}`;
921
- const dedupKey = `${ep}:${flagged.sort().join(",")}`;
922
- const existing = seen.get(dedupKey);
923
- if (existing) {
924
- existing.count++;
925
- continue;
926
- }
927
- const finding = {
928
- severity: "critical",
929
- rule: "token-in-url",
930
- title: "Auth Token in URL",
931
- desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
932
- hint: this.hint,
933
- endpoint: ep,
934
- count: 1
935
- };
936
- seen.set(dedupKey, finding);
937
- findings.push(finding);
836
+ function generateHumanLabel(req, category) {
837
+ const effectivePath = getEffectivePath(req);
838
+ const endpointName = getEndpointName(effectivePath);
839
+ const failed = req.statusCode >= 400;
840
+ switch (category) {
841
+ case "auth-handshake":
842
+ return "Auth handshake";
843
+ case "auth-check":
844
+ return failed ? "Auth check failed" : "Checked auth";
845
+ case "middleware": {
846
+ const rewritePath = effectivePath !== req.path ? effectivePath : "";
847
+ return rewritePath ? `Redirected to ${rewritePath}` : "Middleware";
938
848
  }
939
- return findings;
940
- }
941
- };
942
-
943
- // src/analysis/rules/stack-trace-leak.ts
944
- var stackTraceLeakRule = {
945
- id: "stack-trace-leak",
946
- severity: "critical",
947
- name: "Stack Trace Leaked to Client",
948
- hint: RULE_HINTS["stack-trace-leak"],
949
- check(ctx) {
950
- const findings = [];
951
- const seen = /* @__PURE__ */ new Map();
952
- for (const r of ctx.requests) {
953
- if (!r.responseBody) continue;
954
- if (!STACK_TRACE_RE.test(r.responseBody)) continue;
955
- const ep = `${r.method} ${r.path}`;
956
- const existing = seen.get(ep);
957
- if (existing) {
958
- existing.count++;
959
- continue;
960
- }
961
- const finding = {
962
- severity: "critical",
963
- rule: "stack-trace-leak",
964
- title: "Stack Trace Leaked to Client",
965
- desc: `${ep} \u2014 response exposes internal stack trace`,
966
- hint: this.hint,
967
- endpoint: ep,
968
- count: 1
969
- };
970
- seen.set(ep, finding);
971
- findings.push(finding);
849
+ case "server-action": {
850
+ const name = prettifyEndpoint(req.path);
851
+ return failed ? `${name} failed` : name;
972
852
  }
973
- return findings;
974
- }
975
- };
976
-
977
- // src/analysis/rules/error-info-leak.ts
978
- var CRITICAL_PATTERNS = [
979
- { re: DB_CONN_RE, label: "database connection string" },
980
- { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
981
- { re: SECRET_VAL_RE, label: "secret value" }
982
- ];
983
- var errorInfoLeakRule = {
984
- id: "error-info-leak",
985
- severity: "critical",
986
- name: "Sensitive Data in Error Response",
987
- hint: RULE_HINTS["error-info-leak"],
988
- check(ctx) {
989
- const findings = [];
990
- const seen = /* @__PURE__ */ new Map();
991
- for (const r of ctx.requests) {
992
- if (r.statusCode < 400) continue;
993
- if (!r.responseBody) continue;
994
- if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
995
- const ep = `${r.method} ${r.path}`;
996
- for (const p of CRITICAL_PATTERNS) {
997
- if (!p.re.test(r.responseBody)) continue;
998
- const dedupKey = `${ep}:${p.label}`;
999
- const existing = seen.get(dedupKey);
1000
- if (existing) {
1001
- existing.count++;
1002
- continue;
1003
- }
1004
- const finding = {
1005
- severity: "critical",
1006
- rule: "error-info-leak",
1007
- title: "Sensitive Data in Error Response",
1008
- desc: `${ep} \u2014 error response exposes ${p.label}`,
1009
- hint: this.hint,
1010
- endpoint: ep,
1011
- count: 1
1012
- };
1013
- seen.set(dedupKey, finding);
1014
- findings.push(finding);
1015
- }
853
+ case "api-call": {
854
+ const action = deriveActionVerb(req.method, endpointName);
855
+ const name = prettifyEndpoint(endpointName);
856
+ if (failed) return `${action} ${name} failed`;
857
+ return `${action} ${name}`;
1016
858
  }
1017
- return findings;
859
+ case "data-fetch": {
860
+ const name = prettifyEndpoint(endpointName);
861
+ if (failed) return `Failed to load ${name}`;
862
+ return `Loaded ${name}`;
863
+ }
864
+ case "page-load":
865
+ return failed ? "Page error" : "Loaded page";
866
+ case "navigation":
867
+ return "Navigated";
868
+ case "static":
869
+ return `Static: ${req.path.split("/").pop() ?? req.path}`;
870
+ default:
871
+ return failed ? `${req.method} ${req.path} failed` : `${req.method} ${req.path}`;
1018
872
  }
1019
- };
1020
-
1021
- // src/analysis/rules/insecure-cookie.ts
1022
- function isFrameworkResponse(r) {
1023
- if (r.statusCode >= 300 && r.statusCode < 400) return true;
1024
- if (r.path?.startsWith("/__")) return true;
1025
- if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
1026
- return false;
1027
873
  }
1028
- var insecureCookieRule = {
1029
- id: "insecure-cookie",
1030
- severity: "warning",
1031
- name: "Insecure Cookie",
1032
- hint: RULE_HINTS["insecure-cookie"],
1033
- check(ctx) {
1034
- const findings = [];
1035
- const seen = /* @__PURE__ */ new Map();
1036
- for (const r of ctx.requests) {
1037
- if (!r.responseHeaders) continue;
1038
- if (isFrameworkResponse(r)) continue;
1039
- const setCookie = r.responseHeaders["set-cookie"];
1040
- if (!setCookie) continue;
1041
- const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
1042
- for (const cookie of cookies) {
1043
- const cookieName = cookie.trim().split("=")[0].trim();
1044
- const lower = cookie.toLowerCase();
1045
- const issues = [];
1046
- if (!lower.includes("httponly")) issues.push("HttpOnly");
1047
- if (!lower.includes("samesite")) issues.push("SameSite");
1048
- if (issues.length === 0) continue;
1049
- const dedupKey = `${cookieName}:${issues.join(",")}`;
1050
- const existing = seen.get(dedupKey);
1051
- if (existing) {
1052
- existing.count++;
1053
- continue;
1054
- }
1055
- const finding = {
1056
- severity: "warning",
1057
- rule: "insecure-cookie",
1058
- title: "Insecure Cookie",
1059
- desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
1060
- hint: this.hint,
1061
- endpoint: cookieName,
1062
- count: 1
1063
- };
1064
- seen.set(dedupKey, finding);
1065
- findings.push(finding);
1066
- }
1067
- }
1068
- return findings;
874
+ function prettifyEndpoint(name) {
875
+ const cleaned = name.replace(/^\/api\//, "").replace(/\//g, " ").replace(/\.\.\./g, "").trim();
876
+ if (!cleaned) return "data";
877
+ return cleaned.split(" ").map((word) => {
878
+ if (word.endsWith("ses") || word.endsWith("us") || word.endsWith("ss"))
879
+ return word;
880
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
881
+ if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
882
+ return word;
883
+ }).join(" ");
884
+ }
885
+ function deriveActionVerb(method, endpointName) {
886
+ const lower = endpointName.toLowerCase();
887
+ const VERB_PATTERNS = [
888
+ [/enhance/, "Enhanced"],
889
+ [/generate/, "Generated"],
890
+ [/create/, "Created"],
891
+ [/update/, "Updated"],
892
+ [/delete|remove/, "Deleted"],
893
+ [/send/, "Sent"],
894
+ [/upload/, "Uploaded"],
895
+ [/save/, "Saved"],
896
+ [/submit/, "Submitted"],
897
+ [/login|signin/, "Logged in"],
898
+ [/logout|signout/, "Logged out"],
899
+ [/register|signup/, "Registered"]
900
+ ];
901
+ for (const [pattern, verb] of VERB_PATTERNS) {
902
+ if (pattern.test(lower)) return verb;
1069
903
  }
1070
- };
904
+ switch (method) {
905
+ case "POST":
906
+ return "Created";
907
+ case "PUT":
908
+ case "PATCH":
909
+ return "Updated";
910
+ case "DELETE":
911
+ return "Deleted";
912
+ default:
913
+ return "Called";
914
+ }
915
+ }
916
+ function getEndpointName(path) {
917
+ const parts = path.replace(/^\/api\//, "").split("/");
918
+ if (parts.length <= 2) return parts.join("/");
919
+ return parts.map((p) => p.length > ENDPOINT_TRUNCATE_LENGTH ? "..." : p).join("/");
920
+ }
921
+ function prettifyPageName(path) {
922
+ const clean = path.replace(/^\//, "").replace(/\/$/, "");
923
+ if (!clean) return "Home";
924
+ return clean.split("/").map((s) => capitalize(s.replace(/[-_]/g, " "))).join(" ");
925
+ }
926
+ function capitalize(s) {
927
+ return s.charAt(0).toUpperCase() + s.slice(1);
928
+ }
1071
929
 
1072
- // src/analysis/rules/sensitive-logs.ts
1073
- var sensitiveLogsRule = {
1074
- id: "sensitive-logs",
1075
- severity: "warning",
1076
- name: "Sensitive Data in Logs",
1077
- hint: RULE_HINTS["sensitive-logs"],
1078
- check(ctx) {
1079
- let count = 0;
1080
- for (const log of ctx.logs) {
1081
- if (!log.message) continue;
1082
- if (log.message.startsWith("[brakit]")) continue;
1083
- if (LOG_SECRET_RE.test(log.message)) count++;
930
+ // src/analysis/transforms.ts
931
+ function markDuplicates(requests) {
932
+ const counts = /* @__PURE__ */ new Map();
933
+ for (const req of requests) {
934
+ if (req.category !== "data-fetch" && req.category !== "auth-check")
935
+ continue;
936
+ const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
937
+ counts.set(key, (counts.get(key) ?? 0) + 1);
938
+ }
939
+ const isStrictMode = counts.size > 0 && [...counts.values()].every((c) => c === 2);
940
+ const seen = /* @__PURE__ */ new Set();
941
+ for (const req of requests) {
942
+ if (req.category !== "data-fetch" && req.category !== "auth-check")
943
+ continue;
944
+ const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
945
+ if (seen.has(key)) {
946
+ if (isStrictMode) {
947
+ req.isStrictModeDupe = true;
948
+ } else {
949
+ req.isDuplicate = true;
950
+ }
951
+ } else {
952
+ seen.add(key);
1084
953
  }
1085
- if (count === 0) return [];
1086
- return [{
1087
- severity: "warning",
1088
- rule: "sensitive-logs",
1089
- title: "Sensitive Data in Logs",
1090
- desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
1091
- hint: this.hint,
1092
- endpoint: "console",
1093
- count
1094
- }];
1095
954
  }
1096
- };
1097
-
1098
- // src/analysis/rules/cors-credentials.ts
1099
- var corsCredentialsRule = {
1100
- id: "cors-credentials",
1101
- severity: "warning",
1102
- name: "CORS Credentials with Wildcard",
1103
- hint: RULE_HINTS["cors-credentials"],
1104
- check(ctx) {
1105
- const findings = [];
1106
- const seen = /* @__PURE__ */ new Set();
1107
- for (const r of ctx.requests) {
1108
- if (!r.responseHeaders) continue;
1109
- const origin = r.responseHeaders["access-control-allow-origin"];
1110
- const creds = r.responseHeaders["access-control-allow-credentials"];
1111
- if (origin !== "*" || creds !== "true") continue;
1112
- const ep = `${r.method} ${r.path}`;
1113
- if (seen.has(ep)) continue;
1114
- seen.add(ep);
1115
- findings.push({
1116
- severity: "warning",
1117
- rule: "cors-credentials",
1118
- title: "CORS Credentials with Wildcard",
1119
- desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
1120
- hint: this.hint,
1121
- endpoint: ep,
1122
- count: 1
1123
- });
955
+ }
956
+ function collapsePolling(requests) {
957
+ const result = [];
958
+ let i = 0;
959
+ while (i < requests.length) {
960
+ const current = requests[i];
961
+ const currentEffective = getEffectivePath(current).split("?")[0];
962
+ if (current.method === "GET" && current.category === "data-fetch") {
963
+ let j = i + 1;
964
+ while (j < requests.length && requests[j].method === "GET" && getEffectivePath(requests[j]).split("?")[0] === currentEffective) {
965
+ j++;
966
+ }
967
+ const count = j - i;
968
+ if (count >= MIN_POLLING_SEQUENCE) {
969
+ const last = requests[j - 1];
970
+ const pollingDuration = last.startedAt + last.durationMs - current.startedAt;
971
+ const endpointName = prettifyEndpoint(currentEffective);
972
+ result.push({
973
+ ...current,
974
+ category: "polling",
975
+ label: `Polling ${endpointName} (${count}x, ${formatDurationLabel(pollingDuration)})`,
976
+ pollingCount: count,
977
+ pollingDurationMs: pollingDuration,
978
+ isDuplicate: false
979
+ });
980
+ i = j;
981
+ continue;
982
+ }
1124
983
  }
1125
- return findings;
984
+ result.push(current);
985
+ i++;
1126
986
  }
1127
- };
1128
-
1129
- // src/analysis/rules/response-pii-leak.ts
1130
- var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1131
- var FULL_RECORD_MIN_FIELDS = 5;
1132
- var LIST_PII_MIN_ITEMS = 2;
1133
- function tryParseJson2(body) {
1134
- if (!body) return null;
1135
- try {
1136
- return JSON.parse(body);
1137
- } catch {
1138
- return null;
987
+ return result;
988
+ }
989
+ function formatDurationLabel(ms) {
990
+ if (ms < 1e3) return `${ms}ms`;
991
+ return `${(ms / 1e3).toFixed(1)}s`;
992
+ }
993
+ function detectWarnings(requests) {
994
+ const warnings = [];
995
+ const duplicateCount = requests.filter((r) => r.isDuplicate).length;
996
+ if (duplicateCount > 0) {
997
+ const unique = new Set(
998
+ requests.filter((r) => r.isDuplicate).map((r) => `${r.method} ${getEffectivePath(r).split("?")[0]}`)
999
+ );
1000
+ const endpoints = unique.size;
1001
+ const sameData = requests.filter((r) => r.isDuplicate).every((r) => {
1002
+ const key = `${r.method} ${getEffectivePath(r).split("?")[0]}`;
1003
+ const first = requests.find(
1004
+ (o) => !o.isDuplicate && `${o.method} ${getEffectivePath(o).split("?")[0]}` === key
1005
+ );
1006
+ return first && first.responseBody === r.responseBody;
1007
+ });
1008
+ const suffix = sameData ? " \u2014 same data loaded twice" : "";
1009
+ warnings.push(
1010
+ `${duplicateCount} request${duplicateCount > 1 ? "s" : ""} duplicated across ${endpoints} endpoint${endpoints > 1 ? "s" : ""}${suffix}`
1011
+ );
1139
1012
  }
1140
- }
1141
- function findEmails(obj) {
1142
- const emails = [];
1143
- if (!obj || typeof obj !== "object") return emails;
1144
- if (Array.isArray(obj)) {
1145
- for (let i = 0; i < Math.min(obj.length, 10); i++) {
1146
- emails.push(...findEmails(obj[i]));
1147
- }
1148
- return emails;
1013
+ const slowRequests = requests.filter(
1014
+ (r) => r.durationMs > SLOW_REQUEST_THRESHOLD_MS && r.category !== "polling"
1015
+ );
1016
+ for (const req of slowRequests) {
1017
+ warnings.push(`${req.label} took ${(req.durationMs / 1e3).toFixed(1)}s`);
1149
1018
  }
1150
- for (const v of Object.values(obj)) {
1151
- if (typeof v === "string" && EMAIL_RE.test(v)) {
1152
- emails.push(v);
1153
- } else if (typeof v === "object" && v !== null) {
1154
- emails.push(...findEmails(v));
1155
- }
1019
+ const errors = requests.filter((r) => r.statusCode >= 500);
1020
+ for (const req of errors) {
1021
+ warnings.push(`${req.label} \u2014 server error (${req.statusCode})`);
1156
1022
  }
1157
- return emails;
1023
+ return warnings;
1158
1024
  }
1159
- function topLevelFieldCount(obj) {
1160
- if (Array.isArray(obj)) {
1161
- return obj.length > 0 ? topLevelFieldCount(obj[0]) : 0;
1025
+
1026
+ // src/analysis/group.ts
1027
+ function groupRequestsIntoFlows(requests) {
1028
+ if (requests.length === 0) return [];
1029
+ const flows = [];
1030
+ let currentRequests = [];
1031
+ let currentSourcePage;
1032
+ let lastEndTime = 0;
1033
+ for (const req of requests) {
1034
+ if (req.path.startsWith(DASHBOARD_PREFIX)) continue;
1035
+ const labeled = labelRequest(req);
1036
+ if (labeled.category === "static") continue;
1037
+ const sourcePage = labeled.sourcePage;
1038
+ const gap = currentRequests.length > 0 ? req.startedAt - lastEndTime : 0;
1039
+ const isNewPage = currentRequests.length > 0 && sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
1040
+ const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
1041
+ const isTimeGap = currentRequests.length > 0 && gap > FLOW_GAP_MS;
1042
+ if (currentRequests.length > 0 && (isNewPage || isTimeGap || isPageLoad)) {
1043
+ flows.push(buildFlow(currentRequests));
1044
+ currentRequests = [];
1045
+ }
1046
+ currentRequests.push(labeled);
1047
+ currentSourcePage = sourcePage ?? currentSourcePage;
1048
+ lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
1162
1049
  }
1163
- if (obj && typeof obj === "object") return Object.keys(obj).length;
1164
- return 0;
1165
- }
1166
- function hasInternalIds(obj) {
1167
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) return false;
1168
- for (const key of Object.keys(obj)) {
1169
- if (INTERNAL_ID_KEYS.test(key) || INTERNAL_ID_SUFFIX.test(key)) return true;
1050
+ if (currentRequests.length > 0) {
1051
+ flows.push(buildFlow(currentRequests));
1170
1052
  }
1171
- return false;
1053
+ return flows;
1172
1054
  }
1173
- function unwrapResponse(parsed) {
1174
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1175
- const obj = parsed;
1176
- const keys = Object.keys(obj);
1177
- if (keys.length > 3) return parsed;
1178
- let best = null;
1179
- let bestSize = 0;
1180
- for (const key of keys) {
1181
- const val = obj[key];
1182
- if (Array.isArray(val) && val.length > bestSize) {
1183
- best = val;
1184
- bestSize = val.length;
1185
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
1186
- const size = Object.keys(val).length;
1187
- if (size > bestSize) {
1188
- best = val;
1189
- bestSize = size;
1190
- }
1191
- }
1192
- }
1193
- return best && bestSize >= 3 ? best : parsed;
1055
+ function buildFlow(rawRequests) {
1056
+ markDuplicates(rawRequests);
1057
+ const requests = collapsePolling(rawRequests);
1058
+ const first = requests[0];
1059
+ const startTime = first.startedAt;
1060
+ const endTime = Math.max(
1061
+ ...requests.map(
1062
+ (r) => r.pollingDurationMs ? r.startedAt + r.pollingDurationMs : r.startedAt + r.durationMs
1063
+ )
1064
+ );
1065
+ const duplicateCount = rawRequests.filter((r) => r.isDuplicate).length;
1066
+ const nonStaticCount = rawRequests.length;
1067
+ const redundancyPct = nonStaticCount > 0 ? Math.round(duplicateCount / nonStaticCount * 100) : 0;
1068
+ const sourcePage = getDominantSourcePage(rawRequests);
1069
+ return {
1070
+ id: randomUUID3(),
1071
+ label: deriveFlowLabel(requests, sourcePage),
1072
+ requests,
1073
+ startTime,
1074
+ totalDurationMs: Math.round(endTime - startTime),
1075
+ hasErrors: requests.some((r) => r.statusCode >= 400),
1076
+ warnings: detectWarnings(rawRequests),
1077
+ sourcePage,
1078
+ redundancyPct
1079
+ };
1194
1080
  }
1195
- function detectPII(method, reqBody, resBody) {
1196
- const target = unwrapResponse(resBody);
1197
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
1198
- const reqEmails = findEmails(reqBody);
1199
- if (reqEmails.length > 0) {
1200
- const resEmails = findEmails(target);
1201
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
1202
- if (echoed.length > 0) {
1203
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1204
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
1205
- return { reason: "echo", emailCount: echoed.length };
1206
- }
1207
- }
1208
- }
1209
- }
1210
- if (target && typeof target === "object" && !Array.isArray(target)) {
1211
- const fields = topLevelFieldCount(target);
1212
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
1213
- const emails = findEmails(target);
1214
- if (emails.length > 0) {
1215
- return { reason: "full-record", emailCount: emails.length };
1216
- }
1081
+ function getDominantSourcePage(requests) {
1082
+ const counts = /* @__PURE__ */ new Map();
1083
+ for (const req of requests) {
1084
+ if (req.sourcePage) {
1085
+ counts.set(req.sourcePage, (counts.get(req.sourcePage) ?? 0) + 1);
1217
1086
  }
1218
1087
  }
1219
- if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
1220
- let itemsWithEmail = 0;
1221
- for (let i = 0; i < Math.min(target.length, 10); i++) {
1222
- const item = target[i];
1223
- if (item && typeof item === "object") {
1224
- const emails = findEmails(item);
1225
- if (emails.length > 0) itemsWithEmail++;
1226
- }
1227
- }
1228
- if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
1229
- const first = target[0];
1230
- if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
1231
- return { reason: "list-pii", emailCount: itemsWithEmail };
1232
- }
1088
+ let best = "";
1089
+ let bestCount = 0;
1090
+ for (const [page, count] of counts) {
1091
+ if (count > bestCount) {
1092
+ best = page;
1093
+ bestCount = count;
1233
1094
  }
1234
1095
  }
1235
- return null;
1096
+ return best || requests[0]?.path?.split("?")[0] || "/";
1236
1097
  }
1237
- var REASON_LABELS = {
1238
- echo: "echoes back PII from the request body",
1239
- "full-record": "returns a full record with email and internal IDs",
1240
- "list-pii": "returns a list of records containing email addresses"
1241
- };
1242
- var responsePiiLeakRule = {
1243
- id: "response-pii-leak",
1244
- severity: "warning",
1245
- name: "PII Leak in Response",
1246
- hint: RULE_HINTS["response-pii-leak"],
1247
- check(ctx) {
1248
- const findings = [];
1249
- const seen = /* @__PURE__ */ new Map();
1250
- for (const r of ctx.requests) {
1251
- if (r.statusCode >= 400) continue;
1252
- const resJson = tryParseJson2(r.responseBody);
1253
- if (!resJson) continue;
1254
- const reqJson = tryParseJson2(r.requestBody);
1255
- const detection = detectPII(r.method, reqJson, resJson);
1256
- if (!detection) continue;
1257
- const ep = `${r.method} ${r.path}`;
1258
- const dedupKey = `${ep}:${detection.reason}`;
1259
- const existing = seen.get(dedupKey);
1260
- if (existing) {
1261
- existing.count++;
1262
- continue;
1263
- }
1264
- const finding = {
1265
- severity: "warning",
1266
- rule: "response-pii-leak",
1267
- title: "PII Leak in Response",
1268
- desc: `${ep} \u2014 ${REASON_LABELS[detection.reason]}`,
1269
- hint: this.hint,
1270
- endpoint: ep,
1271
- count: 1
1272
- };
1273
- seen.set(dedupKey, finding);
1274
- findings.push(finding);
1275
- }
1276
- return findings;
1098
+ function deriveFlowLabel(requests, sourcePage) {
1099
+ const trigger = requests.find((r) => r.category === "api-call") ?? requests.find((r) => r.category === "server-action") ?? requests.find((r) => r.category === "page-load") ?? requests.find((r) => r.category === "navigation") ?? requests.find((r) => r.category === "data-fetch") ?? requests[0];
1100
+ if (trigger.category === "page-load" || trigger.category === "navigation") {
1101
+ const pageName = prettifyPageName(trigger.path.split("?")[0]);
1102
+ return `${pageName} Page`;
1277
1103
  }
1278
- };
1279
-
1280
- // src/analysis/rules/scanner.ts
1281
- var SecurityScanner = class {
1282
- rules = [];
1283
- register(rule) {
1284
- this.rules.push(rule);
1104
+ if (trigger.category === "api-call") {
1105
+ const effectivePath = getEffectivePath(trigger);
1106
+ const parts = effectivePath.replace(/^\/api\//, "").split("/");
1107
+ const endpointName = parts.length <= 2 ? parts.join("/") : parts.map((p) => p.length > 12 ? "..." : p).join("/");
1108
+ const action = deriveActionVerb(trigger.method, endpointName);
1109
+ const name = prettifyEndpoint(endpointName);
1110
+ return `${action} ${capitalize(name)}`;
1285
1111
  }
1286
- scan(ctx) {
1287
- const findings = [];
1288
- for (const rule of this.rules) {
1289
- try {
1290
- findings.push(...rule.check(ctx));
1291
- } catch {
1292
- }
1293
- }
1294
- return findings;
1112
+ if (trigger.category === "server-action") {
1113
+ const name = prettifyEndpoint(trigger.path);
1114
+ return capitalize(name);
1295
1115
  }
1296
- getRules() {
1297
- return this.rules;
1116
+ if (trigger.category === "data-fetch" || trigger.category === "polling") {
1117
+ if (sourcePage && sourcePage !== "/") {
1118
+ const pageName = prettifyPageName(sourcePage);
1119
+ return `${pageName} Page`;
1120
+ }
1121
+ return trigger.label;
1298
1122
  }
1299
- };
1300
- function createDefaultScanner() {
1301
- const scanner = new SecurityScanner();
1302
- scanner.register(exposedSecretRule);
1303
- scanner.register(tokenInUrlRule);
1304
- scanner.register(stackTraceLeakRule);
1305
- scanner.register(errorInfoLeakRule);
1306
- scanner.register(insecureCookieRule);
1307
- scanner.register(sensitiveLogsRule);
1308
- scanner.register(corsCredentialsRule);
1309
- scanner.register(responsePiiLeakRule);
1310
- return scanner;
1123
+ return trigger.label;
1311
1124
  }
1312
1125
 
1313
1126
  // src/instrument/adapters/normalize.ts
@@ -1805,7 +1618,7 @@ var AnalysisEngine = class {
1805
1618
  };
1806
1619
 
1807
1620
  // src/index.ts
1808
- var VERSION = "0.6.2";
1621
+ var VERSION = "0.7.1";
1809
1622
  export {
1810
1623
  AdapterRegistry,
1811
1624
  AnalysisEngine,
@@ -1813,6 +1626,5 @@ export {
1813
1626
  VERSION,
1814
1627
  computeInsights,
1815
1628
  createDefaultScanner,
1816
- createProxyServer,
1817
1629
  detectProject
1818
1630
  };