@pensar/apex 2.1.3-canary.fff17bfd → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/build/agent-3cdfwxcd.js +19 -0
  2. package/build/agent-q5hwjdtb.js +27 -0
  3. package/build/{agent-c06jb5zv.js → agent-yayy5x2a.js} +10 -8
  4. package/build/{apps-mxhfapvz.js → apps-0es69e2s.js} +17 -16
  5. package/build/{auth-9ysskv2q.js → auth-q73k9bh9.js} +17 -16
  6. package/build/authentication-tqx2brcv.js +19 -0
  7. package/build/blackboxAgent-d0smgqt7.js +19 -0
  8. package/build/{blackboxPentest-pfs3d1nx.js → blackboxPentest-h28nykk5.js} +15 -14
  9. package/build/{cli-90dw3gm4.js → cli-1cf5vy5m.js} +9 -6
  10. package/build/{cli-f8tgnegf.js → cli-1f636vzd.js} +2 -2
  11. package/build/{cli-tg7mcs43.js → cli-1re2kv5v.js} +5 -3
  12. package/build/cli-2jn8104w.js +195 -0
  13. package/build/{cli-29yqhrjq.js → cli-3k1x5rn3.js} +1 -1
  14. package/build/{cli-k941a7ft.js → cli-4tc6888c.js} +1 -1
  15. package/build/{cli-qb3eddc7.js → cli-51dkmt5p.js} +1 -1
  16. package/build/{cli-v34r5t8t.js → cli-6kx46ck0.js} +6186 -513
  17. package/build/{cli-a1acedxf.js → cli-7e5ad5js.js} +15 -6
  18. package/build/{cli-8qb62k90.js → cli-7n43wz82.js} +1 -1
  19. package/build/{cli-fj04wady.js → cli-knqx15w6.js} +5 -2
  20. package/build/{cli-9wd7jy2s.js → cli-n114n8mk.js} +102 -21
  21. package/build/{cli-9h847y2w.js → cli-ptzy9ak5.js} +2 -2
  22. package/build/{cli-g6cjpbr9.js → cli-s43t5ay0.js} +1 -1
  23. package/build/{cli-nth9zebw.js → cli-tz9yg4ew.js} +1147 -336
  24. package/build/{cli-2cvp6x4p.js → cli-vgg6h2ax.js} +1 -1
  25. package/build/{cli-akhax3yn.js → cli-xbdn034b.js} +671 -105
  26. package/build/{cli-wkpsqyjc.js → cli-xpn9tsw0.js} +27 -5
  27. package/build/cli.js +48 -39
  28. package/build/{config-ky5za80x.js → config-xcf7rc6x.js} +3 -3
  29. package/build/{doctor-t94dqmcs.js → doctor-98zperf9.js} +7 -7
  30. package/build/fastStrike-9tm6bedh.js +156 -0
  31. package/build/{fixes-fv2zmk0m.js → fixes-1dry8j61.js} +17 -16
  32. package/build/{index-s2jr2ve2.js → index-0j880wqc.js} +70 -44
  33. package/build/{index-8mgv8hke.js → index-31rwrakh.js} +2 -2
  34. package/build/{index-b7zrdy9b.js → index-34h2nb6x.js} +3 -3
  35. package/build/{index-pmsx73c3.js → index-4hqbyzf5.js} +11 -9
  36. package/build/{index-qshyncep.js → index-6dbym5jy.js} +8 -7
  37. package/build/{index-pycryjc3.js → index-f6ttv7np.js} +4 -4
  38. package/build/{index-n1yh9e86.js → index-gq9h8j5j.js} +1 -1
  39. package/build/{index-rxskwf3k.js → index-pxwwcefn.js} +10 -10
  40. package/build/{issues-bwdnb233.js → issues-wjakj8yy.js} +28 -16
  41. package/build/{logs-2g3rsh9f.js → logs-ewyzcspy.js} +17 -16
  42. package/build/{offesecAgent-4ykghkvs.js → offesecAgent-7m9smy0t.js} +9 -9
  43. package/build/pentest-1cb5t0dt.js +29 -0
  44. package/build/{pentests-d9dt495f.js → pentests-1hcqjyk2.js} +17 -16
  45. package/build/targetedPentest-3x13kqvc.js +56 -0
  46. package/build/{targets-3n7wsm3g.js → targets-nqz8w37e.js} +17 -16
  47. package/build/threatModel-7f02av7y.js +27 -0
  48. package/build/{uninstall-jvjv6d2n.js → uninstall-p1q5pkx2.js} +1 -1
  49. package/build/{upload-x2b8s1xb.js → upload-x7etpfp4.js} +7 -7
  50. package/build/{utils-kx8jqmv0.js → utils-kw6yh5yn.js} +7 -7
  51. package/package.json +2 -2
  52. package/build/agent-7ab546zf.js +0 -19
  53. package/build/agent-dsqssedb.js +0 -25
  54. package/build/authentication-h1zxd74t.js +0 -19
  55. package/build/blackboxAgent-32zwxt6w.js +0 -19
  56. package/build/pentest-91bygv3h.js +0 -28
  57. package/build/targetedPentest-eavq5h7v.js +0 -43
  58. package/build/threatModel-2re9tykx.js +0 -26
@@ -1,13 +1,20 @@
1
1
  import {
2
2
  BlackboxAttackSurfaceAgent
3
- } from "./cli-tg7mcs43.js";
3
+ } from "./cli-1re2kv5v.js";
4
+ import {
5
+ REPORT_FILENAME_JSON,
6
+ REPORT_FILENAME_MD,
7
+ buildPentestReport,
8
+ renderJson,
9
+ renderMarkdown
10
+ } from "./cli-2jn8104w.js";
4
11
  import {
5
12
  TargetedPentestAgent,
6
13
  buildPentestSystemPrompt
7
- } from "./cli-9wd7jy2s.js";
14
+ } from "./cli-n114n8mk.js";
8
15
  import {
9
16
  CodeAgent
10
- } from "./cli-fj04wady.js";
17
+ } from "./cli-knqx15w6.js";
11
18
  import {
12
19
  AppsDiscoveryResultSchema,
13
20
  DiscoverySummarySchema,
@@ -15,20 +22,16 @@ import {
15
22
  WHITEBOX_APPS_DISCOVERY_SYSTEM_PROMPT,
16
23
  WHITEBOX_DISCOVERY_SYSTEM_PROMPT,
17
24
  WHITEBOX_ENDPOINT_DOCUMENTATION_SYSTEM_PROMPT
18
- } from "./cli-a1acedxf.js";
19
- import {
20
- EvidenceFileEntrySchema
21
- } from "./cli-9h847y2w.js";
25
+ } from "./cli-7e5ad5js.js";
22
26
  import {
23
- CweEntrySchema,
24
27
  FindingsRegistry,
25
28
  OffensiveSecurityAgent,
26
29
  PLAN_MODE_TOOL_NAMES,
27
- ValidatedCweEntrySchema,
28
- hasCanonicalName,
30
+ grpcAuthorityTarget,
29
31
  pLimit,
30
- runWithBoundedConcurrency
31
- } from "./cli-akhax3yn.js";
32
+ runWithBoundedConcurrency,
33
+ toGrpcPentestContext
34
+ } from "./cli-xbdn034b.js";
32
35
  import {
33
36
  createThreatModelPrompt
34
37
  } from "./cli-fw5r7pfj.js";
@@ -36,20 +39,21 @@ import {
36
39
  createLogger,
37
40
  hasToolCall,
38
41
  init_dist,
42
+ init_id,
39
43
  init_lazyLogger,
40
44
  init_session,
41
45
  init_structured,
46
+ newSessionId,
42
47
  scopedLogger
43
- } from "./cli-v34r5t8t.js";
48
+ } from "./cli-6kx46ck0.js";
44
49
  import {
45
- exports_external,
46
- init_zod
47
- } from "./cli-29yqhrjq.js";
50
+ __require
51
+ } from "./cli-8rxa073f.js";
48
52
 
49
53
  // src/core/workflows/pentest.ts
50
54
  init_dist();
51
55
  import { existsSync as existsSync13, readdirSync as readdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
52
- import { join as join17 } from "node:path";
56
+ import { join as join18 } from "node:path";
53
57
 
54
58
  // src/core/agents/specialized/pentest/planPrompt.ts
55
59
  var PLAN_PHASE_SYSTEM_PROMPT = `You are an expert penetration tester performing reconnaissance and planning.
@@ -76,14 +80,23 @@ Guidelines:
76
80
  - Rank attack vectors by likelihood of success and potential impact
77
81
  - Each task should be atomic and testable independently
78
82
  - Include authentication bypass, injection, access control, and business logic testing as appropriate`;
79
- function buildPlanPrompt(target, objectives) {
83
+ function buildPlanPrompt(target, objectives, grpc) {
80
84
  const objectiveList = objectives.map((o, i) => `${i + 1}. ${o}`).join(`
81
85
  `);
86
+ const grpcSection = grpc ? `
87
+ ## gRPC Target
88
+ This target is a **gRPC method**, not a plain HTTP endpoint — plan gRPC-specific attack vectors (reflection/schema recovery, per-method missing-auth, BOLA on request-message IDs, metadata trust, \`:path\` authz bypass, protobuf fuzzing, streaming abuse) rather than HTTP-centric ones.
89
+ - **Transport:** ${grpc.transport}
90
+ - **Service:** ${grpc.serviceFqn}
91
+ - **Method:** ${grpc.method}${grpc.streamingType ? `
92
+ - **Streaming:** ${grpc.streamingType}` : ""}${grpc.host ? `
93
+ - **Authority (host):** ${grpc.host}` : ""}
94
+ ` : "";
82
95
  return `# Planning Assignment
83
96
 
84
97
  ## Target
85
98
  - **URL:** ${target}
86
-
99
+ ${grpcSection}
87
100
  ## Objectives
88
101
  ${objectiveList}
89
102
 
@@ -97,185 +110,6 @@ ${objectiveList}
97
110
  // src/core/workflows/pentest.ts
98
111
  init_structured();
99
112
 
100
- // src/core/report/schemas.ts
101
- init_zod();
102
- var PentestReportFindingSchema = exports_external.object({
103
- title: exports_external.string(),
104
- severity: exports_external.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
105
- description: exports_external.string(),
106
- impact: exports_external.string(),
107
- evidence: exports_external.string(),
108
- endpoint: exports_external.string(),
109
- pocPath: exports_external.string(),
110
- remediation: exports_external.string(),
111
- references: exports_external.string().optional(),
112
- cwes: exports_external.array(ValidatedCweEntrySchema.or(CweEntrySchema)).optional(),
113
- rootCauseGroup: exports_external.string().optional(),
114
- relatedFindings: exports_external.array(exports_external.string()).optional(),
115
- rootCauseLead: exports_external.boolean().optional(),
116
- evidenceFiles: exports_external.array(EvidenceFileEntrySchema).optional()
117
- });
118
- var PentestReportSchema = exports_external.object({
119
- version: exports_external.string().regex(/^1\.\d+$/),
120
- metadata: exports_external.object({
121
- target: exports_external.string(),
122
- model: exports_external.string(),
123
- timestamp: exports_external.string(),
124
- sessionId: exports_external.string(),
125
- mode: exports_external.enum(["blackbox", "whitebox", "targeted"])
126
- }),
127
- summary: exports_external.object({
128
- totalFindings: exports_external.number(),
129
- bySeverity: exports_external.object({
130
- CRITICAL: exports_external.number(),
131
- HIGH: exports_external.number(),
132
- MEDIUM: exports_external.number(),
133
- LOW: exports_external.number()
134
- })
135
- }),
136
- findings: exports_external.array(PentestReportFindingSchema)
137
- });
138
- var REPORT_VERSION = "1.0";
139
-
140
- // src/core/report/builder.ts
141
- var SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
142
- function buildPentestReport(findings, context) {
143
- const sorted = [...findings].sort((a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity));
144
- const bySeverity = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
145
- for (const f of findings) {
146
- bySeverity[f.severity]++;
147
- }
148
- return {
149
- version: REPORT_VERSION,
150
- metadata: {
151
- target: context.target,
152
- model: context.model,
153
- timestamp: new Date().toISOString(),
154
- sessionId: context.sessionId,
155
- mode: context.mode
156
- },
157
- summary: {
158
- totalFindings: findings.length,
159
- bySeverity
160
- },
161
- findings: sorted.map((f) => ({
162
- title: f.title,
163
- severity: f.severity,
164
- description: f.description,
165
- impact: f.impact,
166
- evidence: f.evidence,
167
- endpoint: f.endpoint,
168
- pocPath: f.pocPath,
169
- remediation: f.remediation,
170
- references: f.references,
171
- cwes: f.cwes,
172
- rootCauseGroup: f.rootCauseGroup,
173
- relatedFindings: f.relatedFindings,
174
- rootCauseLead: f.rootCauseLead,
175
- evidenceFiles: f.evidenceFiles
176
- }))
177
- };
178
- }
179
- // src/core/report/renderers/json.ts
180
- function renderJson(report) {
181
- return JSON.stringify(report, null, 2);
182
- }
183
- // src/core/report/renderers/markdown.ts
184
- function renderMarkdown(report) {
185
- const { metadata, findings, summary } = report;
186
- const header = [
187
- `# Pentest Report — ${metadata.target}`,
188
- "",
189
- `**Date:** ${metadata.timestamp} `,
190
- `**Session:** ${metadata.sessionId} `,
191
- `**Model:** ${metadata.model} `,
192
- `**Mode:** ${metadata.mode}`,
193
- "",
194
- `**Findings:** ${summary.totalFindings}`,
195
- ""
196
- ];
197
- if (findings.length === 0) {
198
- return [
199
- ...header,
200
- "No findings were identified during this assessment.",
201
- ""
202
- ].join(`
203
- `);
204
- }
205
- const body = findings.map((finding) => renderFinding(finding, metadata)).join(`
206
- `);
207
- return [...header, body].join(`
208
- `);
209
- }
210
- function renderFinding(finding, metadata) {
211
- const lines = [
212
- `# ${finding.title}`,
213
- "",
214
- `**Severity:** ${finding.severity} `,
215
- `**Target:** ${metadata.target} `,
216
- `**Endpoint:** ${finding.endpoint} `,
217
- `**Date:** ${metadata.timestamp} `,
218
- `**Session:** ${metadata.sessionId}`,
219
- "",
220
- "## Description",
221
- "",
222
- finding.description,
223
- "",
224
- "## Impact",
225
- "",
226
- finding.impact,
227
- "",
228
- "## Evidence",
229
- "",
230
- "```",
231
- finding.evidence,
232
- "```",
233
- "",
234
- ...finding.evidenceFiles?.length ? [
235
- "## Evidence Files",
236
- "",
237
- ...finding.evidenceFiles.map((ef) => `- **[${ef.type}]** \`${ef.path}\` — ${ef.description}`),
238
- ""
239
- ] : [],
240
- ...finding.cwes?.length ? [
241
- "## CWE Classification",
242
- "",
243
- ...finding.cwes.map((cwe) => `- **${cwe.id}**${hasCanonicalName(cwe) ? `: ${cwe.name}` : ""} — ${cwe.reasoning}`),
244
- ""
245
- ] : [],
246
- ...finding.rootCauseGroup ? [
247
- "## Root Cause Group",
248
- "",
249
- `**Group:** \`${finding.rootCauseGroup}\``,
250
- ...finding.relatedFindings?.length ? [
251
- "",
252
- "**Related Findings:**",
253
- ...finding.relatedFindings.map((rf) => `- ${rf}`)
254
- ] : [],
255
- ""
256
- ] : [],
257
- "## POC",
258
- "",
259
- `Path: \`${finding.pocPath}\``,
260
- "",
261
- "## Remediation",
262
- "",
263
- finding.remediation,
264
- ...finding.references ? ["", `## References`, "", finding.references] : [],
265
- "",
266
- "---",
267
- "",
268
- "*This finding was automatically documented by the Pensar penetration testing agent.*",
269
- ""
270
- ];
271
- return lines.join(`
272
- `);
273
- }
274
-
275
- // src/core/report/index.ts
276
- var REPORT_FILENAME_MD = "pentest-report.md";
277
- var REPORT_FILENAME_JSON = "pentest-report.json";
278
-
279
113
  // src/core/session/execution-metrics.ts
280
114
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
281
115
  import { join } from "node:path";
@@ -363,6 +197,7 @@ init_lazyLogger();
363
197
  init_structured();
364
198
  init_lazyLogger();
365
199
  init_session();
200
+ init_id();
366
201
  import {
367
202
  existsSync as existsSync2,
368
203
  mkdirSync,
@@ -441,15 +276,34 @@ function readAgentManifest(session) {
441
276
  }
442
277
  }
443
278
  function buildManifestEntries(targets) {
444
- return targets.map((t, i) => ({
445
- id: `pentest-agent-${i + 1}`,
446
- name: `Pentest Agent ${i + 1}`,
447
- target: t.target,
448
- vulnerabilityClass: t.objectives[0] || "general",
449
- objective: t.objectives.join("; "),
450
- status: "running",
451
- spawnedAt: new Date().toISOString()
452
- }));
279
+ return targets.map((t, i) => {
280
+ const sessionId = newSessionId();
281
+ return {
282
+ id: sessionId,
283
+ sessionId,
284
+ name: `Pentest Agent ${i + 1}`,
285
+ target: t.target,
286
+ vulnerabilityClass: t.objectives[0] || "general",
287
+ objective: t.objectives.join("; "),
288
+ status: "running",
289
+ spawnedAt: new Date().toISOString()
290
+ };
291
+ });
292
+ }
293
+ function reconcileManifestOnResume(existing, fresh) {
294
+ return fresh.map((f, i) => {
295
+ const prev = existing[i];
296
+ if (!prev || prev.target !== f.target)
297
+ return f;
298
+ if (prev.status === "completed")
299
+ return prev;
300
+ return {
301
+ ...f,
302
+ id: prev.id,
303
+ sessionId: prev.sessionId ?? prev.id,
304
+ spawnedAt: prev.spawnedAt
305
+ };
306
+ });
453
307
  }
454
308
  function finalizeManifest(session, entries, results) {
455
309
  const finalManifest = entries.map((entry, i) => {
@@ -470,7 +324,7 @@ function updateManifestEntryStatus(session, agentId, status) {
470
324
  }
471
325
  function getCompletedAgentIds(session) {
472
326
  const manifest = readAgentManifest(session);
473
- return new Set(manifest.filter((e) => e.id.startsWith("pentest-agent-") && e.status === "completed").map((e) => e.id));
327
+ return new Set(manifest.filter((e) => e.status === "completed").map((e) => e.id));
474
328
  }
475
329
  function parseSubagentFilename(filename) {
476
330
  if (filename.startsWith("attack-surface-agent")) {
@@ -611,6 +465,17 @@ function convertModelMessagesToUI(messages) {
611
465
  }
612
466
  function loadSubagents(rootPath) {
613
467
  const subagentsPath = join2(rootPath, SUBAGENTS_DIR);
468
+ const manifestNameById = new Map;
469
+ {
470
+ const manifestPath2 = join2(rootPath, MANIFEST_FILE);
471
+ if (existsSync2(manifestPath2)) {
472
+ try {
473
+ const manifest = JSON.parse(readFileSync2(manifestPath2, "utf-8"));
474
+ for (const entry of manifest)
475
+ manifestNameById.set(entry.id, entry.name);
476
+ } catch {}
477
+ }
478
+ }
614
479
  const subagents = [];
615
480
  const agentNameIndex = new Map;
616
481
  if (existsSync2(subagentsPath)) {
@@ -654,7 +519,7 @@ function loadSubagents(rootPath) {
654
519
  agentNameIndex.set(data.agentName, subagents.length);
655
520
  subagents.push({
656
521
  id: data.agentName,
657
- name: data.agentName === "attack-surface-agent" ? "Attack Surface Discovery" : name,
522
+ name: manifestNameById.get(data.agentName) ?? (data.agentName === "attack-surface-agent" ? "Attack Surface Discovery" : name),
658
523
  type: agentType,
659
524
  target: data.target || "Unknown",
660
525
  messages,
@@ -685,7 +550,7 @@ function loadSubagents(rootPath) {
685
550
  agentNameIndex.set(entry, subagents.length);
686
551
  subagents.push({
687
552
  id: entry,
688
- name,
553
+ name: manifestNameById.get(entry) ?? name,
689
554
  type: agentType,
690
555
  target: "Unknown",
691
556
  messages,
@@ -762,16 +627,14 @@ import {
762
627
  statSync as statSync2,
763
628
  writeFileSync as writeFileSync3
764
629
  } from "node:fs";
765
- import { join as join15 } from "node:path";
630
+ import { join as join16 } from "node:path";
766
631
 
767
632
  // src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.ts
633
+ init_id();
768
634
  init_structured();
769
635
  init_lazyLogger();
770
636
  var log3 = scopedLogger(() => createLogger("endpoint-documentation-agent"));
771
637
  var ENDPOINT_DOCUMENTATION_CONCURRENCY = 10;
772
- function slug(name) {
773
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
774
- }
775
638
  function getDocumentMethod(endpoint) {
776
639
  return endpoint.kind === "page" ? ["PAGE"] : endpoint.method;
777
640
  }
@@ -782,6 +645,18 @@ function buildEndpointDocumentationObjective(opts) {
782
645
  const methodDisplay = documentMethod.join(",");
783
646
  const methodValue = JSON.stringify(documentMethod.length === 1 ? documentMethod[0] : documentMethod);
784
647
  const endpointType = endpoint.kind === "page" ? "web-endpoint" : "api-endpoint";
648
+ const isGrpc = !!endpoint.transport && endpoint.transport !== "http";
649
+ const grpcSection = isGrpc ? `
650
+ ## gRPC (deterministically extracted by surface)
651
+
652
+ This endpoint is a **gRPC method**, not a plain HTTP route. \`routePath\` is the wire path \`/package.Service/Method\` — do NOT glue a host onto it.
653
+
654
+ - **transport**: ${endpoint.transport}
655
+ - **grpc.serviceFqn**: ${endpoint.grpc?.serviceFqn ?? "(derive from routePath)"}
656
+ - **grpc.method**: ${endpoint.grpc?.method ?? "(derive from routePath)"}
657
+ - **grpc.streamingType**: ${endpoint.grpc?.streamingType ?? "unary"}
658
+ ` : "";
659
+ const grpcInstruction = isGrpc ? ` Because this is a gRPC method, ALSO pass \`transport: "${endpoint.transport}"\` and a \`grpc\` object built from the values above (\`serviceFqn\`, \`method\`, \`streamingType\`, plus \`schemaSource: "proto"\`); you MAY add \`reflectionAvailable\` or \`frontingGatewayOperation\` when the source makes them clear.` : "";
785
660
  const auth = endpoint.auth.length > 0 ? endpoint.auth.join(", ") : "none";
786
661
  const authPrefill = endpoint.auth.length > 0;
787
662
  return `# Document Endpoint: ${methodDisplay} ${endpoint.path}
@@ -801,10 +676,10 @@ function buildEndpointDocumentationObjective(opts) {
801
676
  - **endpointType**: ${endpointType}
802
677
  - **auth signals**: ${auth}
803
678
  - **prefilled authRequired**: ${authPrefill}
804
-
679
+ ${grpcSection}
805
680
  ## Task
806
681
 
807
- Document this **single endpoint** by calling \`document_endpoint\` exactly once. Use the deterministic fields above as-is for \`appName\` (\`"${app.name}"\`), \`routePath\`, \`method\` (\`${methodValue}\`), \`endpointType\` (\`"${endpointType}"\`), \`file\`, \`line\`, and \`handler\`. The remaining fields you must produce yourself:
682
+ Document this **single endpoint** by calling \`document_endpoint\` exactly once. Use the deterministic fields above as-is for \`appName\` (\`"${app.name}"\`), \`routePath\`, \`method\` (\`${methodValue}\`), \`endpointType\` (\`"${endpointType}"\`), \`file\`, \`line\`, and \`handler\`.${grpcInstruction} The remaining fields you must produce yourself:
808
683
 
809
684
  - **authRequired**: start from the prefilled value above. Override only after reading the middleware chain at the indicated file when the signals are genuinely ambiguous.
810
685
  - **description**: 1–2 sentences explaining what this endpoint does in plain English. Read the handler at \`${endpoint.file}:${endpoint.line}\` to ground it.
@@ -840,11 +715,12 @@ async function runEndpointDocumentationAgent(opts) {
840
715
  onCacheMetrics,
841
716
  openAIReasoningEffort,
842
717
  enableThinking,
718
+ thinkingEffort,
843
719
  projectThreatModel,
844
720
  parentSubagentId,
845
721
  agentLimiter
846
722
  } = opts;
847
- const subagentId = `endpoint-doc-${slug(app.name)}-${slug(endpoint.path)}`;
723
+ const subagentId = newSessionId();
848
724
  const displayMethod = getDocumentMethod(endpoint);
849
725
  const displayName = `${app.name}: ${displayMethod.join(",")} ${endpoint.path}`;
850
726
  eventBus?.emit("subagent-spawn", {
@@ -875,10 +751,12 @@ async function runEndpointDocumentationAgent(opts) {
875
751
  attackSurfaceRegistry,
876
752
  eventBus,
877
753
  subagentId,
754
+ subagentName: displayName,
878
755
  onStepFinish: (event) => onStepFinish?.(event),
879
756
  onCacheMetrics,
880
757
  openAIReasoningEffort,
881
758
  enableThinking,
759
+ thinkingEffort,
882
760
  responseSchema: DiscoverySummarySchema,
883
761
  excludeTools: ["document_app", "list_files", "grep"],
884
762
  projectThreatModel
@@ -916,13 +794,16 @@ async function runAppEndpointDocumentation(opts) {
916
794
  }
917
795
  }
918
796
 
797
+ // src/core/workflows/whiteboxAttackSurface.ts
798
+ init_id();
799
+
919
800
  // src/core/integrations/surface/index.ts
920
801
  import { existsSync as existsSync10 } from "node:fs";
921
- import { dirname as dirname2, relative as relative3, resolve as resolve2, sep } from "node:path";
802
+ import { dirname as dirname3, relative as relative3, resolve as resolve2, sep } from "node:path";
922
803
 
923
804
  // node_modules/@pensar/surface/dist/index.js
924
805
  import { existsSync as existsSync9, readdirSync as readdirSync9, readFileSync as readFileSync8 } from "fs";
925
- import { join as join14, resolve } from "path";
806
+ import { join as join15, resolve } from "path";
926
807
  import { readdirSync as readdirSync2, readFileSync as readFileSync4, lstatSync, realpathSync } from "fs";
927
808
  import { join as join4, relative, extname } from "path";
928
809
  import { readdirSync as readdirSync22, existsSync as existsSync4, lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
@@ -3624,16 +3505,17 @@ var jsYaml = {
3624
3505
  };
3625
3506
 
3626
3507
  // node_modules/@pensar/surface/dist/index.js
3508
+ import { dirname as dirname2, join as join9 } from "path";
3627
3509
  import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync5 } from "fs";
3628
- import { join as join10, basename as basename2 } from "path";
3510
+ import { join as join11, basename as basename2 } from "path";
3629
3511
  import { readFileSync as readFileSync42 } from "fs";
3630
- import { join as join9 } from "path";
3512
+ import { join as join10 } from "path";
3631
3513
  import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync6 } from "fs";
3632
- import { join as join11 } from "path";
3633
- import { existsSync as existsSync7, readdirSync as readdirSync7 } from "fs";
3634
3514
  import { join as join12 } from "path";
3515
+ import { existsSync as existsSync7, readdirSync as readdirSync7 } from "fs";
3516
+ import { join as join13 } from "path";
3635
3517
  import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync8 } from "fs";
3636
- import { join as join13, basename as basename3 } from "path";
3518
+ import { join as join14, basename as basename3 } from "path";
3637
3519
  var EndpointIndex = class {
3638
3520
  _endpoints;
3639
3521
  _byFramework = null;
@@ -3979,6 +3861,10 @@ function endpoint(e) {
3979
3861
  auth: e.auth ?? [],
3980
3862
  internal: false
3981
3863
  };
3864
+ if (e.transport)
3865
+ ep.transport = e.transport;
3866
+ if (e.grpc)
3867
+ ep.grpc = e.grpc;
3982
3868
  if (e.service)
3983
3869
  ep.service = e.service;
3984
3870
  if (e.handlerFile)
@@ -4014,6 +3900,7 @@ var flask = {
4014
3900
  bpPrefixes[v] = p;
4015
3901
  }
4016
3902
  const routeRe = /@(\w+)\.(?:route|get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]*)\])?\s*\)(.*?)(?:async\s+)?def\s+(\w+)\s*\(/gs;
3903
+ const bodyEndRe = /^(?:@\w|(?:async\s+)?def\s|class\s)/m;
4017
3904
  for (const f of pyFiles) {
4018
3905
  const content = ctx.readFile(f);
4019
3906
  if (!content)
@@ -4038,9 +3925,15 @@ var flask = {
4038
3925
  }
4039
3926
  const auth = findAuthDecorators(between);
4040
3927
  const params = extractPathParams(fullPath);
3928
+ const bodyStart = m.index + m[0].length;
3929
+ const rest = content.slice(bodyStart);
3930
+ const endMatch = bodyEndRe.exec(rest);
3931
+ const body = endMatch ? rest.slice(0, endMatch.index) : rest;
3932
+ const kind = /\brender_template(?:_string)?\s*\(/.test(body) ? "page" : "api";
4041
3933
  for (const method of methods) {
4042
3934
  endpoints.push(endpoint({
4043
3935
  method,
3936
+ kind,
4044
3937
  path: fullPath,
4045
3938
  handler: funcName,
4046
3939
  file: rel,
@@ -4081,7 +3974,7 @@ var fastapi = {
4081
3974
  for (const [v, p] of Object.entries(includePrefixes)) {
4082
3975
  routerPrefixes[v] = p;
4083
3976
  }
4084
- const routeRe = /@(\w+)\.(get|post|put|delete|patch|head|options|websocket)\s*\(\s*['"]([^'"]+)['"]\s*(?:,.*?)?\)(.*?)(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/gs;
3977
+ const routeRe = /@(\w+)\.(get|post|put|delete|patch|head|options|websocket)\s*\(\s*['"]([^'"]+)['"]\s*((?:,.*?)?)\)(.*?)(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/gs;
4085
3978
  for (const f of pyFiles) {
4086
3979
  const content = ctx.readFile(f);
4087
3980
  if (!content)
@@ -4092,13 +3985,21 @@ var fastapi = {
4092
3985
  const varName = m[1];
4093
3986
  let httpMethod = m[2].toUpperCase();
4094
3987
  const routePath = m[3];
4095
- const between = m[4];
4096
- const funcName = m[5];
4097
- const funcParams = m[6];
3988
+ const decoratorArgs = m[4];
3989
+ const between = m[5];
3990
+ const funcName = m[6];
3991
+ const funcParams = m[7];
4098
3992
  const line = lines.lineAt(m.index);
4099
3993
  const isWebSocket = httpMethod === "WEBSOCKET";
4100
3994
  if (isWebSocket)
4101
3995
  httpMethod = "WS";
3996
+ const bodyStart = m.index + m[0].length;
3997
+ const after = content.slice(bodyStart, bodyStart + 4000);
3998
+ const nextRouteRe = /\n(?=@\w+\.|def\s|async\s+def\s)/;
3999
+ const nextMatch = after.match(nextRouteRe);
4000
+ const body = nextMatch ? after.slice(0, nextMatch.index) : after;
4001
+ const isPage = !isWebSocket && (/response_class\s*=\s*\w*HTMLResponse\b/.test(decoratorArgs) || /\bHTMLResponse\s*\(/.test(body) || /\.TemplateResponse\s*\(/.test(body));
4002
+ const kind = isWebSocket ? "websocket" : isPage ? "page" : "api";
4102
4003
  const prefix = routerPrefixes[varName] ?? "";
4103
4004
  const fullPath = normalizePath(prefix + routePath);
4104
4005
  const auth = findAuthDecorators(between + funcParams);
@@ -4114,7 +4015,7 @@ var fastapi = {
4114
4015
  }
4115
4016
  endpoints.push(endpoint({
4116
4017
  method: httpMethod,
4117
- kind: isWebSocket ? "websocket" : "api",
4018
+ kind,
4118
4019
  path: fullPath,
4119
4020
  handler: funcName,
4120
4021
  file: rel,
@@ -4129,12 +4030,105 @@ var fastapi = {
4129
4030
  }
4130
4031
  };
4131
4032
  var PY_EXTS3 = [".py"];
4033
+ var PAGE_CBV_BASES = /* @__PURE__ */ new Set([
4034
+ "TemplateView",
4035
+ "ListView",
4036
+ "DetailView",
4037
+ "CreateView",
4038
+ "UpdateView",
4039
+ "DeleteView",
4040
+ "FormView"
4041
+ ]);
4132
4042
  var django = {
4133
4043
  id: "django",
4134
4044
  detect: { depKeywords: ["django"], markers: ["manage.py"], scope: "all" },
4135
4045
  extract(ctx) {
4136
4046
  const endpoints = [];
4137
4047
  const pyFiles = ctx.iterFiles(PY_EXTS3);
4048
+ const dirOf2 = (f) => {
4049
+ const norm = f.replace(/\\/g, "/");
4050
+ const i2 = norm.lastIndexOf("/");
4051
+ return i2 < 0 ? "" : norm.slice(0, i2);
4052
+ };
4053
+ const classesByDir = /* @__PURE__ */ new Map;
4054
+ const funcsByDir = /* @__PURE__ */ new Map;
4055
+ const globalClasses = {};
4056
+ const globalFuncs = {};
4057
+ const classRe = /^class\s+(\w+)\s*\(([^)]*)\)\s*:/gm;
4058
+ const funcRe = /^(?:async\s+)?def\s+(\w+)\s*\([^)]*\)\s*(?:->[^:\n]+)?:[ \t]*\n?([\s\S]*?)(?=^(?:(?:async\s+)?def |class )|$(?![\s\S]))/gm;
4059
+ for (const f of pyFiles) {
4060
+ const content = ctx.readFile(f);
4061
+ if (!content)
4062
+ continue;
4063
+ const dir = dirOf2(f);
4064
+ const dirClasses = classesByDir.get(dir) ?? {};
4065
+ const dirFuncs = funcsByDir.get(dir) ?? {};
4066
+ for (const m of content.matchAll(classRe)) {
4067
+ const name = m[1];
4068
+ const bases = m[2].split(",").map((b) => b.trim()).filter(Boolean).map((b) => b.split(".").pop().replace(/\s+/g, ""));
4069
+ dirClasses[name] = { bases };
4070
+ globalClasses[name] = { bases };
4071
+ }
4072
+ for (const m of content.matchAll(funcRe)) {
4073
+ const name = m[1];
4074
+ const body = m[2] ?? "";
4075
+ dirFuncs[name] = { body };
4076
+ globalFuncs[name] = { body };
4077
+ }
4078
+ classesByDir.set(dir, dirClasses);
4079
+ funcsByDir.set(dir, dirFuncs);
4080
+ }
4081
+ const resolveHandlerName = (viewRef) => {
4082
+ const parts = viewRef.split(".");
4083
+ const last = parts[parts.length - 1];
4084
+ if ((last === "as_view" || last === "as_asgi") && parts.length >= 2) {
4085
+ return parts[parts.length - 2];
4086
+ }
4087
+ return last;
4088
+ };
4089
+ const lookupInApp = (byDir, global, name, dir) => {
4090
+ const exact = byDir.get(dir)?.[name];
4091
+ if (exact)
4092
+ return exact;
4093
+ let best;
4094
+ const prefix = dir + "/";
4095
+ for (const [d, rec] of byDir) {
4096
+ if (!d.startsWith(prefix))
4097
+ continue;
4098
+ const def = rec[name];
4099
+ if (def && (!best || d.length < best.depth)) {
4100
+ best = { def, depth: d.length };
4101
+ }
4102
+ }
4103
+ return best?.def ?? global[name];
4104
+ };
4105
+ const lookupClass = (name, dir) => lookupInApp(classesByDir, globalClasses, name, dir);
4106
+ const lookupFunc = (name, dir) => lookupInApp(funcsByDir, globalFuncs, name, dir);
4107
+ const isPageClass = (name, dir, seen = /* @__PURE__ */ new Set) => {
4108
+ if (seen.has(name))
4109
+ return false;
4110
+ seen.add(name);
4111
+ const cls = lookupClass(name, dir);
4112
+ if (!cls)
4113
+ return false;
4114
+ for (const b of cls.bases) {
4115
+ if (PAGE_CBV_BASES.has(b))
4116
+ return true;
4117
+ if (isPageClass(b, dir, seen))
4118
+ return true;
4119
+ }
4120
+ return false;
4121
+ };
4122
+ const classifyView = (viewRef, dir) => {
4123
+ const handler = resolveHandlerName(viewRef);
4124
+ if (lookupClass(handler, dir)) {
4125
+ return isPageClass(handler, dir) ? "page" : "api";
4126
+ }
4127
+ const fn = lookupFunc(handler, dir);
4128
+ if (fn && /\brender\s*\(/.test(fn.body))
4129
+ return "page";
4130
+ return "api";
4131
+ };
4138
4132
  const urlFiles = pyFiles.filter((f) => {
4139
4133
  const name = f.split("/").pop();
4140
4134
  return name === "urls.py" || name === "routes.py";
@@ -4164,16 +4158,19 @@ var django = {
4164
4158
  break;
4165
4159
  }
4166
4160
  }
4161
+ const urlDir = dirOf2(f);
4167
4162
  const lines = buildLineIndex(content);
4168
4163
  for (const m of content.matchAll(directPathRe)) {
4169
4164
  const routePath = m[1];
4170
4165
  const viewRef = m[2];
4171
4166
  const line = lines.lineAt(m.index);
4172
4167
  const fullPath = normalizePath(ownPrefix + routePath);
4168
+ const kind = classifyView(viewRef, urlDir);
4173
4169
  endpoints.push(endpoint({
4174
4170
  method: "ANY",
4171
+ kind,
4175
4172
  path: fullPath,
4176
- handler: viewRef.split(".").pop(),
4173
+ handler: resolveHandlerName(viewRef),
4177
4174
  file: rel,
4178
4175
  line,
4179
4176
  framework: "django",
@@ -4207,10 +4204,119 @@ var django = {
4207
4204
  }
4208
4205
  }
4209
4206
  }
4207
+ const wsBlockRe = /websocket_urlpatterns\s*=\s*\[([\s\S]*?)\]/g;
4208
+ const wsEntryRe = /(?:re_path|path|url)\s*\(\s*r?['"]([^'"]*)['"]\s*,\s*(\w[\w.]*)/g;
4209
+ for (const f of pyFiles) {
4210
+ const content = ctx.readFile(f);
4211
+ if (!content || !content.includes("websocket_urlpatterns"))
4212
+ continue;
4213
+ const rel = ctx.rel(f);
4214
+ const lines = buildLineIndex(content);
4215
+ for (const block of content.matchAll(wsBlockRe)) {
4216
+ const blockBody = block[1];
4217
+ const blockOffset = block.index + block[0].indexOf(blockBody);
4218
+ for (const m of blockBody.matchAll(wsEntryRe)) {
4219
+ const routePath = m[1];
4220
+ const consumerRef = m[2];
4221
+ const absOffset = blockOffset + m.index;
4222
+ const line = lines.lineAt(absOffset);
4223
+ const fullPath = normalizePath(routePath);
4224
+ endpoints.push(endpoint({
4225
+ method: "WS",
4226
+ kind: "websocket",
4227
+ path: fullPath,
4228
+ handler: resolveHandlerName(consumerRef),
4229
+ file: rel,
4230
+ line,
4231
+ framework: "django",
4232
+ params: extractPathParams(fullPath)
4233
+ }));
4234
+ }
4235
+ }
4236
+ }
4210
4237
  return endpoints;
4211
4238
  }
4212
4239
  };
4213
4240
  var JS_EXTS = [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"];
4241
+ var PAGE_RENDER_RE = /\bres\s*\.\s*render\s*\(/;
4242
+ var PAGE_SENDFILE_RE = /\bres\s*\.\s*sendFile\s*\(/;
4243
+ function findMatchingParen(content, start) {
4244
+ let depth = 0;
4245
+ let i2 = start;
4246
+ const len = content.length;
4247
+ while (i2 < len) {
4248
+ const ch = content[i2];
4249
+ if (ch === "/" && content[i2 + 1] === "/") {
4250
+ const nl = content.indexOf(`
4251
+ `, i2);
4252
+ if (nl === -1)
4253
+ return -1;
4254
+ i2 = nl + 1;
4255
+ continue;
4256
+ }
4257
+ if (ch === "/" && content[i2 + 1] === "*") {
4258
+ const end = content.indexOf("*/", i2 + 2);
4259
+ if (end === -1)
4260
+ return -1;
4261
+ i2 = end + 2;
4262
+ continue;
4263
+ }
4264
+ if (ch === "'" || ch === '"') {
4265
+ i2++;
4266
+ while (i2 < len) {
4267
+ const c = content[i2];
4268
+ if (c === "\\") {
4269
+ i2 += 2;
4270
+ continue;
4271
+ }
4272
+ if (c === ch) {
4273
+ i2++;
4274
+ break;
4275
+ }
4276
+ i2++;
4277
+ }
4278
+ continue;
4279
+ }
4280
+ if (ch === "`") {
4281
+ i2++;
4282
+ while (i2 < len) {
4283
+ const c = content[i2];
4284
+ if (c === "\\") {
4285
+ i2 += 2;
4286
+ continue;
4287
+ }
4288
+ if (c === "`") {
4289
+ i2++;
4290
+ break;
4291
+ }
4292
+ if (c === "$" && content[i2 + 1] === "{") {
4293
+ i2 += 2;
4294
+ let d = 1;
4295
+ while (i2 < len && d > 0) {
4296
+ const cc = content[i2];
4297
+ if (cc === "{")
4298
+ d++;
4299
+ else if (cc === "}")
4300
+ d--;
4301
+ i2++;
4302
+ }
4303
+ continue;
4304
+ }
4305
+ i2++;
4306
+ }
4307
+ continue;
4308
+ }
4309
+ if (ch === "(" || ch === "[" || ch === "{")
4310
+ depth++;
4311
+ else if (ch === ")" || ch === "]" || ch === "}") {
4312
+ depth--;
4313
+ if (depth === 0 && ch === ")")
4314
+ return i2;
4315
+ }
4316
+ i2++;
4317
+ }
4318
+ return -1;
4319
+ }
4214
4320
  var express = {
4215
4321
  id: "express",
4216
4322
  detect: {
@@ -4232,28 +4338,49 @@ var express = {
4232
4338
  mountPrefixes[m[2]] = m[1];
4233
4339
  }
4234
4340
  }
4235
- const routeRe = /(\w+)\s*\.\s*(get|post|put|delete|patch|all|head|options)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(.*?)\)/gs;
4341
+ const routeOpenerRe = /(\w+)\s*\.\s*(get|post|put|delete|patch|all|head|options|ws)\s*\(\s*(['"])([^'"]+)\3\s*,/g;
4236
4342
  for (const f of jsFiles) {
4237
4343
  const content = ctx.readFile(f);
4238
4344
  if (!content)
4239
4345
  continue;
4240
4346
  const rel = ctx.rel(f);
4241
4347
  const lines = buildLineIndex(content);
4242
- for (const m of content.matchAll(routeRe)) {
4348
+ for (const m of content.matchAll(routeOpenerRe)) {
4243
4349
  const varName = m[1];
4244
- let httpMethod = m[2].toUpperCase();
4245
- const routePath = m[3];
4246
- const handlerArgs = m[4];
4247
- const line = lines.lineAt(m.index);
4350
+ const methodTok = m[2].toLowerCase();
4351
+ const routePath = m[4];
4352
+ const matchStart = m.index;
4353
+ const line = lines.lineAt(matchStart);
4354
+ const openParen = content.indexOf("(", matchStart);
4355
+ if (openParen === -1)
4356
+ continue;
4357
+ const closeParen = findMatchingParen(content, openParen);
4358
+ if (closeParen === -1)
4359
+ continue;
4360
+ const callBody = content.slice(openParen + 1, closeParen);
4361
+ const afterPathComma = callBody.indexOf(",");
4362
+ const handlerArgs = afterPathComma >= 0 ? callBody.slice(afterPathComma + 1) : callBody;
4363
+ const isWs = methodTok === "ws";
4364
+ let httpMethod = methodTok.toUpperCase();
4248
4365
  if (httpMethod === "ALL")
4249
4366
  httpMethod = "ANY";
4367
+ if (isWs)
4368
+ httpMethod = "WS";
4250
4369
  const prefix = mountPrefixes[varName] ?? "";
4251
4370
  const fullPath = normalizePath(prefix + routePath);
4252
- const handlerMatch = handlerArgs.match(/(\w+)\s*[,)]/);
4371
+ const handlerNameMatch = handlerArgs.match(/^\s*(\w+)\s*[,)]/);
4372
+ const handlerName = handlerNameMatch ? handlerNameMatch[1] : "<anonymous>";
4373
+ let kind = "api";
4374
+ if (isWs) {
4375
+ kind = "websocket";
4376
+ } else if (PAGE_RENDER_RE.test(handlerArgs) || PAGE_SENDFILE_RE.test(handlerArgs)) {
4377
+ kind = "page";
4378
+ }
4253
4379
  endpoints.push(endpoint({
4254
4380
  method: httpMethod,
4381
+ kind,
4255
4382
  path: fullPath,
4256
- handler: handlerMatch ? handlerMatch[1] : "<anonymous>",
4383
+ handler: handlerName,
4257
4384
  file: rel,
4258
4385
  line,
4259
4386
  framework: "express",
@@ -4262,9 +4389,219 @@ var express = {
4262
4389
  }));
4263
4390
  }
4264
4391
  }
4392
+ const ioOfRe = /\b(\w+)\s*\.\s*of\s*\(\s*(['"])([^'"]+)\2\s*\)/g;
4393
+ const ioOnConnRe = /\b(\w+)\s*\.\s*on\s*\(\s*(['"])connection\2\s*,\s*(.*?)\)/gs;
4394
+ const wsServerRe = /new\s+(?:WebSocket\s*\.\s*Server|WebSocketServer)\s*\(/g;
4395
+ for (const f of jsFiles) {
4396
+ const content = ctx.readFile(f);
4397
+ if (!content)
4398
+ continue;
4399
+ const rel = ctx.rel(f);
4400
+ const lines = buildLineIndex(content);
4401
+ const importsSocketIo = /(?:from\s+|require\(\s*)['"]socket\.io['"]/.test(content);
4402
+ const ioInstances = /* @__PURE__ */ new Set;
4403
+ if (importsSocketIo) {
4404
+ for (const m of content.matchAll(/\b(\w+)\s*=\s*new\s+(?:Server|SocketIOServer|IOServer)\s*\(/g))
4405
+ ioInstances.add(m[1]);
4406
+ for (const m of content.matchAll(/\b(\w+)\s*=\s*require\(\s*['"]socket\.io['"]\s*\)\s*\(/g))
4407
+ ioInstances.add(m[1]);
4408
+ const factories = /* @__PURE__ */ new Set;
4409
+ for (const m of content.matchAll(/\b(\w+)\s*=\s*require\(\s*['"]socket\.io['"]\s*\)(?!\s*\()/g))
4410
+ factories.add(m[1]);
4411
+ for (const m of content.matchAll(/import\s+(\w+)\s+from\s+['"]socket\.io['"]/g))
4412
+ factories.add(m[1]);
4413
+ for (const fac of factories) {
4414
+ for (const m of content.matchAll(new RegExp(`\\b(\\w+)\\s*=\\s*${fac}\\s*\\(`, "g")))
4415
+ ioInstances.add(m[1]);
4416
+ }
4417
+ }
4418
+ const namespacePaths = /* @__PURE__ */ new Set;
4419
+ let emittedRootIo = false;
4420
+ if (ioInstances.size > 0) {
4421
+ for (const m of content.matchAll(ioOfRe)) {
4422
+ if (!ioInstances.has(m[1]))
4423
+ continue;
4424
+ const ns = m[3];
4425
+ namespacePaths.add(ns);
4426
+ endpoints.push(endpoint({
4427
+ method: "WS",
4428
+ kind: "websocket",
4429
+ path: normalizePath(ns),
4430
+ handler: "<anonymous>",
4431
+ file: rel,
4432
+ line: lines.lineAt(m.index),
4433
+ framework: "express",
4434
+ params: extractPathParams(normalizePath(ns))
4435
+ }));
4436
+ }
4437
+ for (const m of content.matchAll(ioOnConnRe)) {
4438
+ if (!ioInstances.has(m[1]))
4439
+ continue;
4440
+ if (!namespacePaths.has("/")) {
4441
+ endpoints.push(endpoint({
4442
+ method: "WS",
4443
+ kind: "websocket",
4444
+ path: "/",
4445
+ handler: "<anonymous>",
4446
+ file: rel,
4447
+ line: lines.lineAt(m.index),
4448
+ framework: "express"
4449
+ }));
4450
+ emittedRootIo = true;
4451
+ }
4452
+ break;
4453
+ }
4454
+ }
4455
+ ioOnConnRe.lastIndex = 0;
4456
+ const hasRootIoEndpoint = namespacePaths.has("/") || emittedRootIo;
4457
+ if (!hasRootIoEndpoint) {
4458
+ for (const m of content.matchAll(wsServerRe)) {
4459
+ endpoints.push(endpoint({
4460
+ method: "WS",
4461
+ kind: "websocket",
4462
+ path: "/",
4463
+ handler: "<anonymous>",
4464
+ file: rel,
4465
+ line: lines.lineAt(m.index),
4466
+ framework: "express"
4467
+ }));
4468
+ break;
4469
+ }
4470
+ }
4471
+ }
4265
4472
  return endpoints;
4266
4473
  }
4267
4474
  };
4475
+ function maskComments(src) {
4476
+ let out = "";
4477
+ let i2 = 0;
4478
+ const n = src.length;
4479
+ while (i2 < n) {
4480
+ const c = src[i2];
4481
+ if (c === '"' || c === "'" || c === "`") {
4482
+ out += c;
4483
+ i2++;
4484
+ while (i2 < n) {
4485
+ if (src[i2] === "\\" && i2 + 1 < n) {
4486
+ out += src[i2] + src[i2 + 1];
4487
+ i2 += 2;
4488
+ continue;
4489
+ }
4490
+ out += src[i2];
4491
+ const done = src[i2] === c;
4492
+ i2++;
4493
+ if (done)
4494
+ break;
4495
+ }
4496
+ } else if (c === "/" && src[i2 + 1] === "/") {
4497
+ while (i2 < n && src[i2] !== `
4498
+ `) {
4499
+ out += " ";
4500
+ i2++;
4501
+ }
4502
+ } else if (c === "/" && src[i2 + 1] === "*") {
4503
+ out += " ";
4504
+ i2 += 2;
4505
+ while (i2 < n && !(src[i2] === "*" && src[i2 + 1] === "/")) {
4506
+ out += src[i2] === `
4507
+ ` ? `
4508
+ ` : " ";
4509
+ i2++;
4510
+ }
4511
+ if (i2 < n) {
4512
+ out += " ";
4513
+ i2 += 2;
4514
+ }
4515
+ } else {
4516
+ out += c;
4517
+ i2++;
4518
+ }
4519
+ }
4520
+ return out;
4521
+ }
4522
+ function classDecoratorBlock(content, classStart) {
4523
+ const lineStart = (pos) => {
4524
+ let p = pos;
4525
+ while (p > 0 && content[p - 1] !== `
4526
+ `)
4527
+ p--;
4528
+ return p;
4529
+ };
4530
+ const isStackLine = (line) => line === "" || line.startsWith("@") || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*") || line.endsWith(",") || line.endsWith("(") || line.endsWith(")");
4531
+ const endsPrevStatement = (line) => line.endsWith("}") || line.endsWith(";");
4532
+ let start = lineStart(classStart);
4533
+ while (start > 0) {
4534
+ const prev = lineStart(start - 1);
4535
+ const line = content.slice(prev, start - 1).trim();
4536
+ if (endsPrevStatement(line))
4537
+ break;
4538
+ if (!isStackLine(line))
4539
+ break;
4540
+ start = prev;
4541
+ }
4542
+ return content.slice(start, classStart);
4543
+ }
4544
+ function findClasses(content) {
4545
+ const classes = [];
4546
+ const classRe = /\bclass\s+(\w+)/g;
4547
+ const scan = maskComments(content);
4548
+ for (const m of scan.matchAll(classRe)) {
4549
+ const start = m.index;
4550
+ const name = m[1];
4551
+ const decorators = classDecoratorBlock(content, start);
4552
+ if (classes.length > 0)
4553
+ classes[classes.length - 1].end = start;
4554
+ classes.push({ start, end: content.length, name, decorators });
4555
+ }
4556
+ return classes;
4557
+ }
4558
+ function methodDecoratorBlock(content, offset) {
4559
+ const lineStart = (pos) => {
4560
+ let p = pos;
4561
+ while (p > 0 && content[p - 1] !== `
4562
+ `)
4563
+ p--;
4564
+ return p;
4565
+ };
4566
+ const isStackLine = (line) => line === "" || line.startsWith("@") || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*") || line.endsWith(",") || line.endsWith("(") || line.endsWith(")");
4567
+ const endsPrevStatement = (line) => line.endsWith("}") || line.endsWith(";");
4568
+ let start = lineStart(offset);
4569
+ while (start > 0) {
4570
+ const prev = lineStart(start - 1);
4571
+ const line = content.slice(prev, start - 1).trim();
4572
+ if (endsPrevStatement(line))
4573
+ break;
4574
+ if (!isStackLine(line))
4575
+ break;
4576
+ start = prev;
4577
+ }
4578
+ let end = offset;
4579
+ while (end < content.length && content[end] !== `
4580
+ `)
4581
+ end++;
4582
+ while (end < content.length) {
4583
+ const nextStart = end + 1;
4584
+ let nextEnd = nextStart;
4585
+ while (nextEnd < content.length && content[nextEnd] !== `
4586
+ `)
4587
+ nextEnd++;
4588
+ const line = content.slice(nextStart, nextEnd).trim();
4589
+ if (line === "") {
4590
+ end = nextEnd;
4591
+ continue;
4592
+ }
4593
+ if (line.startsWith("@") || line.startsWith("//") || line.startsWith("*")) {
4594
+ end = nextEnd;
4595
+ continue;
4596
+ }
4597
+ if (line.endsWith(",") || line.endsWith(")")) {
4598
+ end = nextEnd;
4599
+ continue;
4600
+ }
4601
+ break;
4602
+ }
4603
+ return content.slice(start, end);
4604
+ }
4268
4605
  var nestjs = {
4269
4606
  id: "nestjs",
4270
4607
  detect: { depKeywords: ["@nestjs/core"], markers: [], scope: "root" },
@@ -4272,31 +4609,77 @@ var nestjs = {
4272
4609
  const endpoints = [];
4273
4610
  const tsFiles = ctx.iterFiles([".ts"]);
4274
4611
  const controllerRe = /@Controller\s*\(\s*['"]([^'"]*)['"]\s*\)/;
4612
+ const wsGatewayRe = /@WebSocketGateway\s*\(([^)]*)\)|@WebSocketGateway\b/;
4275
4613
  const methodRe = /@(Get|Post|Put|Delete|Patch|Head|Options)\s*\(\s*['"]?([^'")]*?)['"]?\s*\)(.*?)(?:async\s+)?(\w+)\s*\(/gs;
4614
+ const subscribeRe = /@SubscribeMessage\s*\(\s*['"]([^'"]*)['"]\s*\)(.*?)(?:async\s+)?(\w+)\s*\(/gs;
4615
+ const grpcRe = /@(GrpcMethod|GrpcStreamMethod)\s*\(([^)]*)\)[\s\S]{0,160}?(?:async\s+)?(\w+)\s*\(/g;
4276
4616
  const guardRe = /@UseGuards?\s*\(\s*(\w+)/g;
4277
4617
  for (const f of tsFiles) {
4278
4618
  const content = ctx.readFile(f);
4279
- if (!content || !content.includes("@Controller"))
4619
+ if (!content)
4620
+ continue;
4621
+ if (!content.includes("@Controller") && !content.includes("@WebSocketGateway") && !content.includes("@Grpc"))
4280
4622
  continue;
4281
4623
  const rel = ctx.rel(f);
4282
- const ctrlMatch = controllerRe.exec(content);
4283
- const controllerPrefix = ctrlMatch?.[1] ?? "";
4284
- const classAuth = [];
4285
- const classIdx = content.indexOf("class ");
4286
- if (classIdx > 0) {
4287
- const preClass = content.slice(0, classIdx);
4624
+ const lines = buildLineIndex(content);
4625
+ const classes = findClasses(content);
4626
+ const fileCtrlMatch = controllerRe.exec(content);
4627
+ const fileControllerPrefix = fileCtrlMatch?.[1] ?? "";
4628
+ const fileClassAuth = [];
4629
+ const firstClassIdx = content.indexOf("class ");
4630
+ if (firstClassIdx > 0) {
4631
+ const preClass = content.slice(0, firstClassIdx);
4288
4632
  for (const gm of preClass.matchAll(guardRe)) {
4289
- classAuth.push(`@UseGuards(${gm[1]})`);
4633
+ fileClassAuth.push(`@UseGuards(${gm[1]})`);
4290
4634
  }
4291
4635
  }
4292
- const lines = buildLineIndex(content);
4636
+ const classAt = (offset) => {
4637
+ for (const c of classes) {
4638
+ if (offset >= c.start && offset < c.end)
4639
+ return c;
4640
+ }
4641
+ return null;
4642
+ };
4643
+ const classCtx = /* @__PURE__ */ new Map;
4644
+ for (const c of classes) {
4645
+ const ctrl = controllerRe.exec(c.decorators);
4646
+ const prefix = ctrl?.[1] ?? "";
4647
+ const auth = [];
4648
+ for (const gm of c.decorators.matchAll(guardRe)) {
4649
+ auth.push(`@UseGuards(${gm[1]})`);
4650
+ }
4651
+ const ws = wsGatewayRe.exec(c.decorators);
4652
+ const isGateway = !!ws;
4653
+ let wsNamespace = "";
4654
+ if (ws && ws[1]) {
4655
+ const nsMatch = /namespace\s*:\s*['"]([^'"]*)['"]/.exec(ws[1]);
4656
+ if (nsMatch)
4657
+ wsNamespace = nsMatch[1];
4658
+ }
4659
+ classCtx.set(c.name, { prefix, auth, isGateway, wsNamespace });
4660
+ }
4293
4661
  for (const m of content.matchAll(methodRe)) {
4294
4662
  const httpMethod = m[1].toUpperCase();
4295
4663
  const routePath = m[2];
4296
4664
  const between = m[3];
4297
- const handler = m[4];
4298
- const line = lines.lineAt(m.index);
4299
- const fullPath = normalizePath(controllerPrefix + "/" + routePath);
4665
+ let handler = m[4];
4666
+ const offset = m.index;
4667
+ const line = lines.lineAt(offset);
4668
+ if (between.includes("@")) {
4669
+ const fwd = content.slice(offset, offset + 600);
4670
+ const after = fwd.indexOf(")");
4671
+ if (after >= 0) {
4672
+ const handlerRe = /(?:@\w+\s*\([^)]*\)\s*)*(?:async\s+)?(\w+)\s*\(/g;
4673
+ handlerRe.lastIndex = after + 1;
4674
+ const hm = handlerRe.exec(fwd);
4675
+ if (hm)
4676
+ handler = hm[1];
4677
+ }
4678
+ }
4679
+ const cls = classAt(offset);
4680
+ const prefix = cls ? classCtx.get(cls.name)?.prefix ?? "" : fileControllerPrefix;
4681
+ const classAuth = cls ? classCtx.get(cls.name)?.auth ?? [] : fileClassAuth;
4682
+ const fullPath = normalizePath(prefix + "/" + routePath);
4300
4683
  const params = extractPathParams(fullPath);
4301
4684
  const methodAuth = [...classAuth];
4302
4685
  for (const gm of between.matchAll(/@UseGuards?\s*\(\s*(\w+)/g)) {
@@ -4309,6 +4692,9 @@ var nestjs = {
4309
4692
  for (const pm of searchText.matchAll(/@Param\s*\(\s*['"]?(\w+)/g)) {
4310
4693
  params.push({ name: pm[1], location: "path", required: true });
4311
4694
  }
4695
+ const decoratorBlock = methodDecoratorBlock(content, offset);
4696
+ const hasRender = /@Render\s*\(/.test(decoratorBlock);
4697
+ const kind = hasRender ? "page" : "api";
4312
4698
  endpoints.push(endpoint({
4313
4699
  method: httpMethod,
4314
4700
  path: fullPath,
@@ -4316,10 +4702,64 @@ var nestjs = {
4316
4702
  file: rel,
4317
4703
  line,
4318
4704
  framework: "nestjs",
4705
+ kind,
4319
4706
  params,
4320
4707
  auth: methodAuth
4321
4708
  }));
4322
4709
  }
4710
+ for (const m of content.matchAll(subscribeRe)) {
4711
+ const eventName = m[1];
4712
+ const between = m[2];
4713
+ const handler = m[3];
4714
+ const offset = m.index;
4715
+ const line = lines.lineAt(offset);
4716
+ const cls = classAt(offset);
4717
+ if (!cls)
4718
+ continue;
4719
+ const info = classCtx.get(cls.name);
4720
+ if (!info || !info.isGateway)
4721
+ continue;
4722
+ const fullPath = normalizePath((info.wsNamespace || "") + "/" + eventName);
4723
+ const methodAuth = [...info.auth];
4724
+ for (const gm of between.matchAll(/@UseGuards?\s*\(\s*(\w+)/g)) {
4725
+ methodAuth.push(`@UseGuards(${gm[1]})`);
4726
+ }
4727
+ endpoints.push(endpoint({
4728
+ method: "WS",
4729
+ path: fullPath,
4730
+ handler,
4731
+ file: rel,
4732
+ line,
4733
+ framework: "nestjs",
4734
+ kind: "websocket",
4735
+ params: extractPathParams(fullPath),
4736
+ auth: methodAuth
4737
+ }));
4738
+ }
4739
+ for (const m of content.matchAll(grpcRe)) {
4740
+ const decorator = m[1];
4741
+ const args = [...m[2].matchAll(/['"]([^'"]+)['"]/g)].map((a) => a[1]);
4742
+ const handler = m[3];
4743
+ const offset = m.index;
4744
+ const line = lines.lineAt(offset);
4745
+ const cls = classAt(offset);
4746
+ const service = args[0] ?? (cls?.name ?? "").replace(/Controller$/, "");
4747
+ const method = args[1] ?? handler.charAt(0).toUpperCase() + handler.slice(1);
4748
+ endpoints.push(endpoint({
4749
+ method: "ANY",
4750
+ path: `/${service}/${method}`,
4751
+ handler,
4752
+ file: rel,
4753
+ line,
4754
+ framework: "nestjs",
4755
+ transport: "grpc",
4756
+ grpc: {
4757
+ serviceFqn: service,
4758
+ method,
4759
+ streamingType: decorator === "GrpcStreamMethod" ? "bidi" : "unary"
4760
+ }
4761
+ }));
4762
+ }
4323
4763
  }
4324
4764
  return endpoints;
4325
4765
  }
@@ -4535,9 +4975,50 @@ var nextjs = {
4535
4975
  };
4536
4976
  var GO_EXTS = [".go"];
4537
4977
  var GO_GROUP_RE = /(\w+)\s*[:=]+\s*(\w+)\.Group\s*\(\s*['"]([^'"]+)['"]/g;
4978
+ var dirOf = (f) => {
4979
+ const norm = f.replace(/\\/g, "/");
4980
+ const i2 = norm.lastIndexOf("/");
4981
+ return i2 < 0 ? "" : norm.slice(0, i2);
4982
+ };
4983
+ var GORILLA_WS_IMPORT_RE = /["']github\.com\/gorilla\/websocket["']/;
4984
+ var UPGRADE_CALL_RE = /\.\s*Upgrade\s*\(/;
4985
+ function findWebsocketHandlers(ctx, goFiles) {
4986
+ const byDir = /* @__PURE__ */ new Map;
4987
+ const funcRe = /\bfunc\s+(?:\([^)]*\)\s+)?(\w+)\s*\([^)]*\)[^{]*\{/g;
4988
+ for (const f of goFiles) {
4989
+ const content = ctx.readFile(f);
4990
+ if (!content)
4991
+ continue;
4992
+ if (!GORILLA_WS_IMPORT_RE.test(content))
4993
+ continue;
4994
+ const dir = dirOf(f);
4995
+ const set2 = byDir.get(dir) ?? /* @__PURE__ */ new Set;
4996
+ for (const m of content.matchAll(funcRe)) {
4997
+ const name = m[1];
4998
+ const bodyStart = m.index + m[0].length;
4999
+ let depth = 1;
5000
+ let i2 = bodyStart;
5001
+ const len = content.length;
5002
+ while (i2 < len && depth > 0) {
5003
+ const ch = content[i2];
5004
+ if (ch === "{")
5005
+ depth++;
5006
+ else if (ch === "}")
5007
+ depth--;
5008
+ i2++;
5009
+ }
5010
+ const body = content.slice(bodyStart, i2 - 1);
5011
+ if (UPGRADE_CALL_RE.test(body))
5012
+ set2.add(name);
5013
+ }
5014
+ byDir.set(dir, set2);
5015
+ }
5016
+ return byDir;
5017
+ }
4538
5018
  function extractGoFramework(ctx, framework, routeRe) {
4539
5019
  const endpoints = [];
4540
5020
  const goFiles = ctx.iterFiles(GO_EXTS);
5021
+ const wsHandlersByDir = findWebsocketHandlers(ctx, goFiles);
4541
5022
  const groupPrefixes = {};
4542
5023
  const groupParents = {};
4543
5024
  for (const f of goFiles) {
@@ -4567,6 +5048,8 @@ function extractGoFramework(ctx, framework, routeRe) {
4567
5048
  continue;
4568
5049
  const rel = ctx.rel(f);
4569
5050
  const lines = buildLineIndex(content);
5051
+ const fileImportsGorilla = GORILLA_WS_IMPORT_RE.test(content);
5052
+ const wsHandlers = wsHandlersByDir.get(dirOf(f)) ?? /* @__PURE__ */ new Set;
4570
5053
  for (const m of content.matchAll(routeRe)) {
4571
5054
  const varName = m[1];
4572
5055
  let httpMethod = m[2].toUpperCase();
@@ -4577,11 +5060,15 @@ function extractGoFramework(ctx, framework, routeRe) {
4577
5060
  httpMethod = "ANY";
4578
5061
  const prefix = resolved[varName] ?? "";
4579
5062
  const fullPath = normalizePath(prefix + routePath);
4580
- const handlerMatch = handlerArgs.match(/(\w+)\s*[,)]/);
5063
+ const idMatches = [...handlerArgs.matchAll(/(\w+)/g)];
5064
+ const handlerName = idMatches.length > 0 ? idMatches[idMatches.length - 1][1] : "<anonymous>";
5065
+ const inlineUpgrade = fileImportsGorilla && UPGRADE_CALL_RE.test(handlerArgs);
5066
+ const isWebsocket = wsHandlers.has(handlerName) || inlineUpgrade;
4581
5067
  endpoints.push(endpoint({
4582
- method: httpMethod,
5068
+ method: isWebsocket ? "WS" : httpMethod,
5069
+ kind: isWebsocket ? "websocket" : "api",
4583
5070
  path: fullPath,
4584
- handler: handlerMatch ? handlerMatch[1] : "<anonymous>",
5071
+ handler: handlerName,
4585
5072
  file: rel,
4586
5073
  line,
4587
5074
  framework,
@@ -4631,18 +5118,23 @@ var netHttp = {
4631
5118
  const endpoints = [];
4632
5119
  const goFiles = ctx.iterFiles(GO_EXTS);
4633
5120
  const handleRe = /(?:http\.HandleFunc|mux\.HandleFunc|http\.Handle|mux\.Handle)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)/g;
5121
+ const wsHandlersByDir = findWebsocketHandlers(ctx, goFiles);
4634
5122
  for (const f of goFiles) {
4635
5123
  const content = ctx.readFile(f);
4636
5124
  if (!content)
4637
5125
  continue;
4638
5126
  const rel = ctx.rel(f);
5127
+ const wsHandlers = wsHandlersByDir.get(dirOf(f)) ?? /* @__PURE__ */ new Set;
4639
5128
  const lines = buildLineIndex(content);
4640
5129
  for (const m of content.matchAll(handleRe)) {
4641
5130
  const fullPath = normalizePath(m[1]);
5131
+ const handlerName = m[2];
5132
+ const isWebsocket = wsHandlers.has(handlerName);
4642
5133
  endpoints.push(endpoint({
4643
- method: "ANY",
5134
+ method: isWebsocket ? "WS" : "ANY",
5135
+ kind: isWebsocket ? "websocket" : "api",
4644
5136
  path: fullPath,
4645
- handler: m[2],
5137
+ handler: handlerName,
4646
5138
  file: rel,
4647
5139
  line: lines.lineAt(m.index),
4648
5140
  framework: "net_http",
@@ -4727,6 +5219,34 @@ var METHOD_MAP = {
4727
5219
  DeleteMapping: "DELETE",
4728
5220
  PatchMapping: "PATCH"
4729
5221
  };
5222
+ function collectClassContexts(content) {
5223
+ const out = [];
5224
+ const classRe = /\bclass\s+\w+/g;
5225
+ for (const m of content.matchAll(classRe)) {
5226
+ const classStart = m.index;
5227
+ const windowStart = Math.max(0, classStart - 600);
5228
+ let preamble = content.slice(windowStart, classStart);
5229
+ const lastBrace = Math.max(preamble.lastIndexOf("}"), preamble.lastIndexOf(";"));
5230
+ if (lastBrace >= 0)
5231
+ preamble = preamble.slice(lastBrace + 1);
5232
+ const hasRest = /@RestController\b/.test(preamble);
5233
+ const hasController = /@Controller\b/.test(preamble);
5234
+ if (!hasRest && !hasController)
5235
+ continue;
5236
+ out.push({ start: classStart, kind: hasRest ? "api" : "page" });
5237
+ }
5238
+ return out;
5239
+ }
5240
+ function kindForOffset(classes, offset) {
5241
+ let current = "api";
5242
+ for (const c of classes) {
5243
+ if (c.start <= offset)
5244
+ current = c.kind;
5245
+ else
5246
+ break;
5247
+ }
5248
+ return current;
5249
+ }
4730
5250
  var spring = {
4731
5251
  id: "spring",
4732
5252
  detect: { depKeywords: ["spring-boot"], markers: [], scope: "root" },
@@ -4736,6 +5256,7 @@ var spring = {
4736
5256
  const classMappingRe = /@RequestMapping\s*\(\s*(?:value\s*=\s*)?['"]([^'"]+)['"]/;
4737
5257
  const methodMappingRe = /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*\(\s*(?:value\s*=\s*)?['"]?([^'")]*?)['"]?\s*\)(.*?)(?:public|private|protected)?\s*\w[\w<>,\s]*\s+(\w+)\s*\(/gs;
4738
5258
  const reqMappingMethodRe = /@RequestMapping\s*\([^)]*method\s*=\s*RequestMethod\.(\w+)[^)]*(?:value\s*=\s*)?['"]?([^'")]*?)['"]?[^)]*\)(.*?)(?:public|private|protected)?\s*\w[\w<>,\s]*\s+(\w+)\s*\(/gs;
5259
+ const wsMappingRe = /@(MessageMapping|SubscribeMapping)\s*\(\s*(?:value\s*=\s*)?['"]([^'"]+)['"]\s*\)(.*?)(?:public|private|protected)?\s*\w[\w<>,\s]*\s+(\w+)\s*\(/gs;
4739
5260
  for (const f of javaFiles) {
4740
5261
  const content = ctx.readFile(f);
4741
5262
  if (!content)
@@ -4744,6 +5265,7 @@ var spring = {
4744
5265
  const cm = classMappingRe.exec(content);
4745
5266
  const classPrefix = cm?.[1] ?? "";
4746
5267
  const lines = buildLineIndex(content);
5268
+ const classCtxs = collectClassContexts(content);
4747
5269
  for (const m of content.matchAll(methodMappingRe)) {
4748
5270
  const fullPath = normalizePath(classPrefix + "/" + m[2]);
4749
5271
  endpoints.push(endpoint({
@@ -4753,6 +5275,7 @@ var spring = {
4753
5275
  file: rel,
4754
5276
  line: lines.lineAt(m.index),
4755
5277
  framework: "spring",
5278
+ kind: kindForOffset(classCtxs, m.index),
4756
5279
  params: extractPathParams(fullPath),
4757
5280
  auth: findAuthDecorators(m[3])
4758
5281
  }));
@@ -4766,6 +5289,21 @@ var spring = {
4766
5289
  file: rel,
4767
5290
  line: lines.lineAt(m.index),
4768
5291
  framework: "spring",
5292
+ kind: kindForOffset(classCtxs, m.index),
5293
+ params: extractPathParams(fullPath),
5294
+ auth: findAuthDecorators(m[3])
5295
+ }));
5296
+ }
5297
+ for (const m of content.matchAll(wsMappingRe)) {
5298
+ const fullPath = normalizePath(m[2]);
5299
+ endpoints.push(endpoint({
5300
+ method: "WS",
5301
+ path: fullPath,
5302
+ handler: m[4],
5303
+ file: rel,
5304
+ line: lines.lineAt(m.index),
5305
+ framework: "spring",
5306
+ kind: "websocket",
4769
5307
  params: extractPathParams(fullPath),
4770
5308
  auth: findAuthDecorators(m[3])
4771
5309
  }));
@@ -4782,6 +5320,53 @@ var REST_ACTIONS = [
4782
5320
  ["PATCH", "update", "/:id"],
4783
5321
  ["DELETE", "destroy", "/:id"]
4784
5322
  ];
5323
+ function createControllerResolver(ctx) {
5324
+ const kindCache = /* @__PURE__ */ new Map;
5325
+ function controllerPath(className) {
5326
+ const parts = className.replace(/Controller$/i, "").split("::").map((p) => p.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase()).filter(Boolean);
5327
+ return join32(ctx.repoPath, "app", "controllers", ...parts.slice(0, -1), `${parts[parts.length - 1]}_controller.rb`);
5328
+ }
5329
+ function classNameFromRef(ref) {
5330
+ const head = ref.split("#")[0];
5331
+ if (head.includes("::")) {
5332
+ return head.endsWith("Controller") ? head : `${head}Controller`;
5333
+ }
5334
+ const segs = head.split("/").filter(Boolean);
5335
+ const camel = segs.map((s) => s.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(""));
5336
+ return camel.join("::") + "Controller";
5337
+ }
5338
+ function classifyClass(className, seen) {
5339
+ const cached = kindCache.get(className);
5340
+ if (cached)
5341
+ return cached;
5342
+ if (seen.has(className))
5343
+ return "api";
5344
+ seen.add(className);
5345
+ const path = controllerPath(className);
5346
+ const src = ctx.readFile(path);
5347
+ if (!src)
5348
+ return "api";
5349
+ const m = /class\s+([A-Za-z0-9_:]+)\s*<\s*([A-Za-z0-9_:]+(?:::[A-Za-z0-9_]+)*)/.exec(src);
5350
+ if (!m)
5351
+ return "api";
5352
+ const parent = m[2];
5353
+ let kind;
5354
+ if (parent === "ActionController::Base")
5355
+ kind = "page";
5356
+ else if (parent === "ActionController::API")
5357
+ kind = "api";
5358
+ else
5359
+ kind = classifyClass(parent, seen);
5360
+ kindCache.set(className, kind);
5361
+ return kind;
5362
+ }
5363
+ return {
5364
+ resolve(controllerRef) {
5365
+ const className = classNameFromRef(controllerRef);
5366
+ return classifyClass(className, /* @__PURE__ */ new Set);
5367
+ }
5368
+ };
5369
+ }
4785
5370
  var rails = {
4786
5371
  id: "rails",
4787
5372
  detect: {
@@ -4797,35 +5382,63 @@ var rails = {
4797
5382
  return endpoints;
4798
5383
  ctx.filesScanned++;
4799
5384
  const rel = ctx.rel(routesFile);
4800
- const routeRe = /(get|post|put|patch|delete)\s+['"]([^'"]+)['"](?:\s*,\s*to:\s*['"]([^'"]+)['"])?/g;
5385
+ const resolver = createControllerResolver(ctx);
5386
+ const routeRe = /(get|post|put|patch|delete)\s+['"]([^'"]+)['"](?:\s*(?:,\s*to:|=>)\s*['"]([^'"]+)['"])?/g;
4801
5387
  const resourcesRe = /resources?\s+:(\w+)/g;
4802
- const namespaceRe = /namespace\s+:(\w+)/g;
4803
- const namespaces = [];
4804
- for (const line of content.split(`
4805
- `)) {
4806
- const nsM = namespaceRe.exec(line);
4807
- if (nsM)
4808
- namespaces.push(nsM[1]);
4809
- namespaceRe.lastIndex = 0;
4810
- }
4811
- const prefix = namespaces.length ? "/" + namespaces.join("/") : "";
5388
+ const namespaceLineRe = /\bnamespace\s+:(\w+)\b/;
4812
5389
  const lines = buildLineIndex(content);
5390
+ const sourceLines = content.split(`
5391
+ `);
5392
+ const nsStack = [];
5393
+ let depth = 0;
5394
+ const prefixAtLine = new Array(sourceLines.length + 1).fill("");
5395
+ for (let i2 = 0;i2 < sourceLines.length; i2++) {
5396
+ const line = sourceLines[i2];
5397
+ const code = line.replace(/#.*$/, "");
5398
+ const nsM = namespaceLineRe.exec(code);
5399
+ if (nsM) {
5400
+ nsStack.push({ name: nsM[1], depth });
5401
+ }
5402
+ const endMatches = /^\s*end\b/.test(code);
5403
+ if (endMatches) {
5404
+ depth = Math.max(0, depth - 1);
5405
+ while (nsStack.length > 0 && nsStack[nsStack.length - 1].depth >= depth) {
5406
+ nsStack.pop();
5407
+ }
5408
+ }
5409
+ const doMatches = code.match(/\bdo\b(?:\s*\|[^|]*\|)?\s*$/);
5410
+ if (doMatches)
5411
+ depth++;
5412
+ const prefix = nsStack.length ? "/" + nsStack.map((n) => n.name).join("/") : "";
5413
+ prefixAtLine[i2 + 1] = prefix;
5414
+ }
5415
+ const prefixAt = (offset) => {
5416
+ const ln = lines.lineAt(offset);
5417
+ return prefixAtLine[ln] ?? "";
5418
+ };
4813
5419
  for (const m of content.matchAll(routeRe)) {
5420
+ const prefix = prefixAt(m.index);
4814
5421
  const fullPath = normalizePath(prefix + "/" + m[2]);
5422
+ const handler = m[3] || m[2].replace(/\//g, "_");
5423
+ const kind = m[3] ? resolver.resolve(m[3]) : "api";
4815
5424
  endpoints.push(endpoint({
4816
5425
  method: m[1].toUpperCase(),
4817
5426
  path: fullPath,
4818
- handler: m[3] || m[2].replace(/\//g, "_"),
5427
+ handler,
4819
5428
  file: rel,
4820
5429
  line: lines.lineAt(m.index),
4821
5430
  framework: "rails",
5431
+ kind,
4822
5432
  params: extractPathParams(fullPath)
4823
5433
  }));
4824
5434
  }
4825
5435
  for (const m of content.matchAll(resourcesRe)) {
4826
5436
  const resource = m[1];
4827
5437
  const line = lines.lineAt(m.index);
5438
+ const prefix = prefixAt(m.index);
4828
5439
  const base = normalizePath(prefix + "/" + resource);
5440
+ const controllerRef = (prefix.replace(/^\//, "") + "/" + resource).replace(/^\/+/, "").replace(/\/+/g, "/");
5441
+ const kind = resolver.resolve(`${controllerRef}#index`);
4829
5442
  for (const [method, action, suffix] of REST_ACTIONS) {
4830
5443
  const fullPath = normalizePath(base + suffix);
4831
5444
  endpoints.push(endpoint({
@@ -4835,6 +5448,7 @@ var rails = {
4835
5448
  file: rel,
4836
5449
  line,
4837
5450
  framework: "rails",
5451
+ kind,
4838
5452
  params: extractPathParams(fullPath)
4839
5453
  }));
4840
5454
  }
@@ -4864,7 +5478,9 @@ var laravel = {
4864
5478
  continue;
4865
5479
  ctx.filesScanned++;
4866
5480
  const rel = ctx.rel(rf);
5481
+ const isWeb = rf.endsWith("web.php");
4867
5482
  const filePrefix = rf.endsWith("api.php") ? "/api" : "";
5483
+ const kind = isWeb ? "page" : "api";
4868
5484
  const groupPrefixes = [];
4869
5485
  for (const pm of content.matchAll(prefixGroupRe)) {
4870
5486
  groupPrefixes.push(pm[1]);
@@ -4891,6 +5507,7 @@ var laravel = {
4891
5507
  file: rel,
4892
5508
  line: lines.lineAt(m.index),
4893
5509
  framework: "laravel",
5510
+ kind,
4894
5511
  params: extractPathParams(fullPath),
4895
5512
  auth: [...fileAuth]
4896
5513
  }));
@@ -5279,6 +5896,160 @@ var openapi = {
5279
5896
  return endpoints;
5280
5897
  }
5281
5898
  };
5899
+ var CONNECT_HINTS = [
5900
+ "connectrpc.com/connect",
5901
+ "@connectrpc/",
5902
+ "protoc-gen-connect",
5903
+ "connect-go",
5904
+ "connect-es",
5905
+ "connectrpc"
5906
+ ];
5907
+ var CONNECT_DEP_FILES = [
5908
+ "buf.gen.yaml",
5909
+ "buf.yaml",
5910
+ "package.json",
5911
+ "go.mod"
5912
+ ];
5913
+ function makeConnectResolver(ctx) {
5914
+ const memo = /* @__PURE__ */ new Map;
5915
+ const root = ctx.repoPath;
5916
+ const dirUsesConnect = (dir) => {
5917
+ const cached = memo.get(dir);
5918
+ if (cached !== undefined)
5919
+ return cached;
5920
+ let result = false;
5921
+ for (const name of CONNECT_DEP_FILES) {
5922
+ const c = ctx.readFile(join9(dir, name));
5923
+ if (c && CONNECT_HINTS.some((h) => c.toLowerCase().includes(h))) {
5924
+ result = true;
5925
+ break;
5926
+ }
5927
+ }
5928
+ if (!result && dir !== root && dir.startsWith(root)) {
5929
+ const parent = dirname2(dir);
5930
+ if (parent !== dir)
5931
+ result = dirUsesConnect(parent);
5932
+ }
5933
+ memo.set(dir, result);
5934
+ return result;
5935
+ };
5936
+ return (file) => dirUsesConnect(dirname2(file));
5937
+ }
5938
+ function stripComments(src) {
5939
+ let out = "";
5940
+ for (let i2 = 0;i2 < src.length; ) {
5941
+ const c = src[i2];
5942
+ if (c === '"' || c === "'") {
5943
+ out += c;
5944
+ i2++;
5945
+ while (i2 < src.length && src[i2] !== c) {
5946
+ if (src[i2] === "\\" && i2 + 1 < src.length) {
5947
+ out += src[i2] + src[i2 + 1];
5948
+ i2 += 2;
5949
+ } else {
5950
+ out += src[i2];
5951
+ i2++;
5952
+ }
5953
+ }
5954
+ if (i2 < src.length) {
5955
+ out += src[i2];
5956
+ i2++;
5957
+ }
5958
+ } else if (c === "/" && src[i2 + 1] === "/") {
5959
+ while (i2 < src.length && src[i2] !== `
5960
+ `) {
5961
+ out += " ";
5962
+ i2++;
5963
+ }
5964
+ } else if (c === "/" && src[i2 + 1] === "*") {
5965
+ out += " ";
5966
+ i2 += 2;
5967
+ while (i2 < src.length && !(src[i2] === "*" && src[i2 + 1] === "/")) {
5968
+ out += src[i2] === `
5969
+ ` ? `
5970
+ ` : " ";
5971
+ i2++;
5972
+ }
5973
+ if (i2 < src.length) {
5974
+ out += " ";
5975
+ i2 += 2;
5976
+ }
5977
+ } else {
5978
+ out += c;
5979
+ i2++;
5980
+ }
5981
+ }
5982
+ return out;
5983
+ }
5984
+ function streaming(client, server) {
5985
+ if (client && server)
5986
+ return "bidi";
5987
+ if (client)
5988
+ return "client_stream";
5989
+ if (server)
5990
+ return "server_stream";
5991
+ return "unary";
5992
+ }
5993
+ var PACKAGE_RE = /\bpackage\s+([A-Za-z_][\w.]*)\s*;/;
5994
+ var SERVICE_RE = /\bservice\s+([A-Za-z_]\w*)\s*\{/g;
5995
+ var RPC_RE = /\brpc\s+([A-Za-z_]\w*)\s*\(\s*(stream\s+)?[.\w]+\s*\)\s*returns\s*\(\s*(stream\s+)?[.\w]+\s*\)/g;
5996
+ var grpc = {
5997
+ id: "grpc",
5998
+ detect: (_repoPath, ctx) => ctx.iterFiles([".proto"]).length > 0,
5999
+ extract(ctx) {
6000
+ const endpoints = [];
6001
+ const connectForFile = makeConnectResolver(ctx);
6002
+ for (const file of ctx.iterFiles([".proto"])) {
6003
+ const raw = ctx.readFile(file);
6004
+ if (!raw)
6005
+ continue;
6006
+ const framework = connectForFile(file) ? "connect" : "grpc";
6007
+ const transport = framework === "connect" ? "connect" : "grpc";
6008
+ const src = stripComments(raw);
6009
+ const rel = ctx.rel(file);
6010
+ const lines = buildLineIndex(raw);
6011
+ const pkg = PACKAGE_RE.exec(src)?.[1] ?? "";
6012
+ const services = [];
6013
+ SERVICE_RE.lastIndex = 0;
6014
+ let s;
6015
+ while (s = SERVICE_RE.exec(src)) {
6016
+ services.push({ name: s[1], index: s.index });
6017
+ }
6018
+ if (!services.length)
6019
+ continue;
6020
+ RPC_RE.lastIndex = 0;
6021
+ let m;
6022
+ while (m = RPC_RE.exec(src)) {
6023
+ let svc = null;
6024
+ for (const cand of services) {
6025
+ if (cand.index < m.index)
6026
+ svc = cand;
6027
+ else
6028
+ break;
6029
+ }
6030
+ if (!svc)
6031
+ continue;
6032
+ const serviceFqn = pkg ? `${pkg}.${svc.name}` : svc.name;
6033
+ const method = m[1];
6034
+ endpoints.push(endpoint({
6035
+ method: "ANY",
6036
+ path: `/${serviceFqn}/${method}`,
6037
+ handler: method,
6038
+ file: rel,
6039
+ line: lines.lineAt(m.index),
6040
+ framework,
6041
+ transport,
6042
+ grpc: {
6043
+ serviceFqn,
6044
+ method,
6045
+ streamingType: streaming(!!m[2], !!m[3])
6046
+ }
6047
+ }));
6048
+ }
6049
+ }
6050
+ return endpoints;
6051
+ }
6052
+ };
5282
6053
  var ALL_EXTRACTORS = [
5283
6054
  flask,
5284
6055
  fastapi,
@@ -5296,9 +6067,11 @@ var ALL_EXTRACTORS = [
5296
6067
  laravel,
5297
6068
  sst,
5298
6069
  serverActions,
5299
- openapi
6070
+ openapi,
6071
+ grpc
5300
6072
  ];
5301
6073
  var EXTRACTOR_MAP = new Map(ALL_EXTRACTORS.map((e) => [e.id, e]));
6074
+ EXTRACTOR_MAP.set("connect", grpc);
5302
6075
  function getExtractor(id) {
5303
6076
  return EXTRACTOR_MAP.get(id);
5304
6077
  }
@@ -5330,7 +6103,7 @@ var PY_DEP_TO_TYPE = [
5330
6103
  ];
5331
6104
  function inferServiceType(absDir) {
5332
6105
  try {
5333
- const content = readFileSync42(join9(absDir, "package.json"), "utf-8");
6106
+ const content = readFileSync42(join10(absDir, "package.json"), "utf-8");
5334
6107
  const lower = content.toLowerCase();
5335
6108
  for (const [dep, type2] of PKG_DEP_TO_TYPE) {
5336
6109
  if (lower.includes(`"${dep}"`))
@@ -5339,7 +6112,7 @@ function inferServiceType(absDir) {
5339
6112
  return "generic";
5340
6113
  } catch {}
5341
6114
  try {
5342
- const content = readFileSync42(join9(absDir, "go.mod"), "utf-8");
6115
+ const content = readFileSync42(join10(absDir, "go.mod"), "utf-8");
5343
6116
  for (const [dep, type2] of GO_DEP_TO_TYPE) {
5344
6117
  if (content.includes(dep))
5345
6118
  return type2;
@@ -5347,7 +6120,7 @@ function inferServiceType(absDir) {
5347
6120
  return "generic";
5348
6121
  } catch {}
5349
6122
  try {
5350
- const content = readFileSync42(join9(absDir, "Cargo.toml"), "utf-8");
6123
+ const content = readFileSync42(join10(absDir, "Cargo.toml"), "utf-8");
5351
6124
  for (const [dep, type2] of CARGO_DEP_TO_TYPE) {
5352
6125
  if (content.includes(dep))
5353
6126
  return type2;
@@ -5355,7 +6128,7 @@ function inferServiceType(absDir) {
5355
6128
  return "generic";
5356
6129
  } catch {}
5357
6130
  try {
5358
- const content = readFileSync42(join9(absDir, "requirements.txt"), "utf-8");
6131
+ const content = readFileSync42(join10(absDir, "requirements.txt"), "utf-8");
5359
6132
  for (const [dep, type2] of PY_DEP_TO_TYPE) {
5360
6133
  if (content.includes(dep))
5361
6134
  return type2;
@@ -5376,7 +6149,7 @@ function expandWorkspaceGlobs(repoPath, patterns) {
5376
6149
  const clean = pattern.replace(/\/+$/, "");
5377
6150
  if (clean.endsWith("/*") || clean.endsWith("/**")) {
5378
6151
  const parentDir = clean.replace(/\/\*\*?$/, "");
5379
- const absParent = join10(repoPath, parentDir);
6152
+ const absParent = join11(repoPath, parentDir);
5380
6153
  try {
5381
6154
  for (const entry of readdirSync5(absParent, { withFileTypes: true })) {
5382
6155
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
@@ -5385,7 +6158,7 @@ function expandWorkspaceGlobs(repoPath, patterns) {
5385
6158
  }
5386
6159
  } catch {}
5387
6160
  } else {
5388
- if (existsSync5(join10(repoPath, clean))) {
6161
+ if (existsSync5(join11(repoPath, clean))) {
5389
6162
  dirs.push(clean);
5390
6163
  }
5391
6164
  }
@@ -5393,7 +6166,7 @@ function expandWorkspaceGlobs(repoPath, patterns) {
5393
6166
  return dirs;
5394
6167
  }
5395
6168
  function detectWorkspaces(repoPath, _ctx, _existing) {
5396
- const rootPkgPath = join10(repoPath, "package.json");
6169
+ const rootPkgPath = join11(repoPath, "package.json");
5397
6170
  let rootPkg;
5398
6171
  try {
5399
6172
  rootPkg = readFileSync5(rootPkgPath, "utf-8");
@@ -5418,7 +6191,7 @@ function detectWorkspaces(repoPath, _ctx, _existing) {
5418
6191
  return dirsToServices(repoPath, dirs);
5419
6192
  }
5420
6193
  function detectPnpmWorkspaces(repoPath) {
5421
- const wsPath = join10(repoPath, "pnpm-workspace.yaml");
6194
+ const wsPath = join11(repoPath, "pnpm-workspace.yaml");
5422
6195
  let content;
5423
6196
  try {
5424
6197
  content = readFileSync5(wsPath, "utf-8");
@@ -5448,7 +6221,7 @@ function detectPnpmWorkspaces(repoPath) {
5448
6221
  function dirsToServices(repoPath, dirs) {
5449
6222
  const services = [];
5450
6223
  for (const dir of dirs) {
5451
- const pkgPath = join10(repoPath, dir, "package.json");
6224
+ const pkgPath = join11(repoPath, dir, "package.json");
5452
6225
  let pkgContent;
5453
6226
  try {
5454
6227
  pkgContent = readFileSync5(pkgPath, "utf-8");
@@ -5464,7 +6237,7 @@ function dirsToServices(repoPath, dirs) {
5464
6237
  }
5465
6238
  services.push({
5466
6239
  name,
5467
- type: inferServiceType(join10(repoPath, dir)),
6240
+ type: inferServiceType(join11(repoPath, dir)),
5468
6241
  root: dir
5469
6242
  });
5470
6243
  }
@@ -5531,7 +6304,7 @@ function isInfraService(name) {
5531
6304
  return INFRA_SUFFIXES.some((suffix) => lower.endsWith(suffix));
5532
6305
  }
5533
6306
  function hasSourceCode(absDir) {
5534
- if (SOURCE_MARKERS.some((f) => existsSync6(join11(absDir, f))))
6307
+ if (SOURCE_MARKERS.some((f) => existsSync6(join12(absDir, f))))
5535
6308
  return true;
5536
6309
  try {
5537
6310
  const entries = readdirSync6(absDir, { withFileTypes: true });
@@ -5541,7 +6314,7 @@ function hasSourceCode(absDir) {
5541
6314
  if (!entry.isDirectory() || entry.name.startsWith("."))
5542
6315
  continue;
5543
6316
  try {
5544
- const subEntries = readdirSync6(join11(absDir, entry.name));
6317
+ const subEntries = readdirSync6(join12(absDir, entry.name));
5545
6318
  if (subEntries.some((e) => SOURCE_EXTENSIONS.some((ext) => e.endsWith(ext))))
5546
6319
  return true;
5547
6320
  } catch {}
@@ -5561,7 +6334,7 @@ function detectCompose(repoPath, _ctx, existing) {
5561
6334
  let composeContent = null;
5562
6335
  for (const name of composeNames) {
5563
6336
  try {
5564
- composeContent = readFileSync6(join11(repoPath, name), "utf-8");
6337
+ composeContent = readFileSync6(join12(repoPath, name), "utf-8");
5565
6338
  break;
5566
6339
  } catch {}
5567
6340
  }
@@ -5586,26 +6359,26 @@ function detectCompose(repoPath, _ctx, existing) {
5586
6359
  continue;
5587
6360
  if (isInfraService(name))
5588
6361
  continue;
5589
- const absDir = join11(repoPath, root);
6362
+ const absDir = join12(repoPath, root);
5590
6363
  if (!existsSync6(absDir) || !hasSourceCode(absDir))
5591
6364
  continue;
5592
6365
  services.push({
5593
6366
  name,
5594
- type: inferServiceType(join11(repoPath, root)),
6367
+ type: inferServiceType(join12(repoPath, root)),
5595
6368
  root
5596
6369
  });
5597
6370
  }
5598
6371
  return services;
5599
6372
  }
5600
6373
  function detectSstServices(repoPath, ctx, existing) {
5601
- if (!existsSync7(join12(repoPath, "sst.config.ts")))
6374
+ if (!existsSync7(join13(repoPath, "sst.config.ts")))
5602
6375
  return [];
5603
- const infraDir = join12(repoPath, "infra");
6376
+ const infraDir = join13(repoPath, "infra");
5604
6377
  if (!existsSync7(infraDir))
5605
6378
  return [];
5606
6379
  let files;
5607
6380
  try {
5608
- files = readdirSync7(infraDir).filter((f) => f.endsWith(".ts")).map((f) => join12(infraDir, f));
6381
+ files = readdirSync7(infraDir).filter((f) => f.endsWith(".ts")).map((f) => join13(infraDir, f));
5609
6382
  } catch {
5610
6383
  return [];
5611
6384
  }
@@ -5651,7 +6424,7 @@ function detectDirectories(repoPath, _ctx, existing) {
5651
6424
  if (existing.length > 0)
5652
6425
  return [];
5653
6426
  const services = [];
5654
- const rootHasMarker = SERVICE_MARKERS.some((f) => existsSync8(join13(repoPath, f)));
6427
+ const rootHasMarker = SERVICE_MARKERS.some((f) => existsSync8(join14(repoPath, f)));
5655
6428
  if (rootHasMarker) {
5656
6429
  const name = inferRootName(repoPath);
5657
6430
  services.push({
@@ -5668,11 +6441,11 @@ function detectDirectories(repoPath, _ctx, existing) {
5668
6441
  return [];
5669
6442
  }
5670
6443
  for (const child of children) {
5671
- const hasMarker = SERVICE_MARKERS.some((f) => existsSync8(join13(repoPath, child, f)));
6444
+ const hasMarker = SERVICE_MARKERS.some((f) => existsSync8(join14(repoPath, child, f)));
5672
6445
  if (hasMarker) {
5673
6446
  services.push({
5674
6447
  name: child,
5675
- type: inferServiceType(join13(repoPath, child)),
6448
+ type: inferServiceType(join14(repoPath, child)),
5676
6449
  root: child
5677
6450
  });
5678
6451
  }
@@ -5680,7 +6453,7 @@ function detectDirectories(repoPath, _ctx, existing) {
5680
6453
  const claimedRoots = new Set(services.map((s) => s.root));
5681
6454
  const serviceDirs = ["src/services", "services", "apps", "src"];
5682
6455
  for (const svcDir of serviceDirs) {
5683
- const absDir = join13(repoPath, svcDir);
6456
+ const absDir = join14(repoPath, svcDir);
5684
6457
  if (!existsSync8(absDir))
5685
6458
  continue;
5686
6459
  try {
@@ -5690,11 +6463,11 @@ function detectDirectories(repoPath, _ctx, existing) {
5690
6463
  const childPath = svcDir + "/" + entry.name;
5691
6464
  if (claimedRoots.has(childPath))
5692
6465
  continue;
5693
- const hasMarker = SERVICE_MARKERS.some((f) => existsSync8(join13(repoPath, childPath, f)));
6466
+ const hasMarker = SERVICE_MARKERS.some((f) => existsSync8(join14(repoPath, childPath, f)));
5694
6467
  if (hasMarker) {
5695
6468
  services.push({
5696
6469
  name: entry.name,
5697
- type: inferServiceType(join13(repoPath, childPath)),
6470
+ type: inferServiceType(join14(repoPath, childPath)),
5698
6471
  root: childPath
5699
6472
  });
5700
6473
  }
@@ -5705,7 +6478,7 @@ function detectDirectories(repoPath, _ctx, existing) {
5705
6478
  }
5706
6479
  function inferRootName(repoPath) {
5707
6480
  try {
5708
- const pkg = JSON.parse(readFileSync7(join13(repoPath, "package.json"), "utf-8"));
6481
+ const pkg = JSON.parse(readFileSync7(join14(repoPath, "package.json"), "utf-8"));
5709
6482
  if (pkg.name)
5710
6483
  return stripOrgScope(pkg.name);
5711
6484
  } catch {}
@@ -5787,7 +6560,7 @@ function readDepFiles(repoPath) {
5787
6560
  const rootParts = [];
5788
6561
  for (const fname of DEP_FILENAMES) {
5789
6562
  try {
5790
- rootParts.push(readFileSync8(join14(repoPath, fname), "utf-8"));
6563
+ rootParts.push(readFileSync8(join15(repoPath, fname), "utf-8"));
5791
6564
  } catch {}
5792
6565
  }
5793
6566
  const workspaceParts = [];
@@ -5796,7 +6569,7 @@ function readDepFiles(repoPath) {
5796
6569
  if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
5797
6570
  continue;
5798
6571
  try {
5799
- workspaceParts.push(readFileSync8(join14(repoPath, child.name, "package.json"), "utf-8"));
6572
+ workspaceParts.push(readFileSync8(join15(repoPath, child.name, "package.json"), "utf-8"));
5800
6573
  } catch {}
5801
6574
  }
5802
6575
  } catch {}
@@ -5815,10 +6588,10 @@ function getChildDirs(repoPath) {
5815
6588
  }
5816
6589
  function findMarkerFile(repoPath, markers, childDirs) {
5817
6590
  for (const marker of markers) {
5818
- if (existsSync9(join14(repoPath, marker)))
6591
+ if (existsSync9(join15(repoPath, marker)))
5819
6592
  return true;
5820
6593
  for (const child of childDirs) {
5821
- if (existsSync9(join14(repoPath, child, marker)))
6594
+ if (existsSync9(join15(repoPath, child, marker)))
5822
6595
  return true;
5823
6596
  }
5824
6597
  }
@@ -5854,7 +6627,7 @@ function detectFrameworks(repoPath, ctx) {
5854
6627
  detected.push(extractor.id);
5855
6628
  }
5856
6629
  }
5857
- if (!GO_FRAMEWORKS.some((gf) => detected.includes(gf)) && existsSync9(join14(repoPath, "go.mod"))) {
6630
+ if (!GO_FRAMEWORKS.some((gf) => detected.includes(gf)) && existsSync9(join15(repoPath, "go.mod"))) {
5858
6631
  for (const f of ctx.iterFiles([".go"])) {
5859
6632
  const content = ctx.readFile(f);
5860
6633
  if (content && /"net\/http"/.test(content)) {
@@ -5869,7 +6642,7 @@ function detectFrameworksInServices(repoPath, services, ctx, alreadyDetected) {
5869
6642
  const additional = [];
5870
6643
  const seen = new Set(alreadyDetected);
5871
6644
  for (const svc of services) {
5872
- const svcPath = join14(repoPath, svc.root);
6645
+ const svcPath = join15(repoPath, svc.root);
5873
6646
  if (!existsSync9(svcPath))
5874
6647
  continue;
5875
6648
  const deps = readDepFiles(svcPath);
@@ -5895,7 +6668,7 @@ function detectFrameworksInServices(repoPath, services, ctx, alreadyDetected) {
5895
6668
  seen.add(extractor.id);
5896
6669
  }
5897
6670
  }
5898
- if (!GO_FRAMEWORKS.some((gf) => seen.has(gf)) && !seen.has("net_http") && existsSync9(join14(svcPath, "go.mod"))) {
6671
+ if (!GO_FRAMEWORKS.some((gf) => seen.has(gf)) && !seen.has("net_http") && existsSync9(join15(svcPath, "go.mod"))) {
5899
6672
  for (const f of ctx.iterFiles([".go"])) {
5900
6673
  const content = ctx.readFile(f);
5901
6674
  if (content && /"net\/http"/.test(content)) {
@@ -5999,15 +6772,19 @@ function mapRaw(repoPath, options = {}) {
5999
6772
  filesScanned: ctx.filesScanned
6000
6773
  };
6001
6774
  }
6775
+ var isProtoGrpc = (ep) => ep.framework === "grpc" || ep.framework === "connect";
6002
6776
  function map2(repoPath, options = {}) {
6003
6777
  const raw = mapRaw(repoPath, options);
6004
- const seen = /* @__PURE__ */ new Set;
6778
+ const indexByKey = /* @__PURE__ */ new Map;
6005
6779
  const unique = [];
6006
6780
  for (const ep of raw.endpoints) {
6007
- const key = `${ep.method}::${ep.path}`;
6008
- if (!seen.has(key)) {
6009
- seen.add(key);
6781
+ const key = `${ep.transport ?? "http"}::${ep.method}::${ep.path}`;
6782
+ const idx = indexByKey.get(key);
6783
+ if (idx === undefined) {
6784
+ indexByKey.set(key, unique.length);
6010
6785
  unique.push(ep);
6786
+ } else if (isProtoGrpc(ep) && !isProtoGrpc(unique[idx])) {
6787
+ unique[idx] = ep;
6011
6788
  }
6012
6789
  }
6013
6790
  const filtered = options.includeInternal ? unique : unique.filter((e) => !e.internal);
@@ -6088,7 +6865,7 @@ function findDependencyRoot(appPath, repoRoot) {
6088
6865
  }
6089
6866
  if (current === absRoot)
6090
6867
  break;
6091
- const parent = dirname2(current);
6868
+ const parent = dirname3(current);
6092
6869
  if (parent === current)
6093
6870
  break;
6094
6871
  if (!parent.startsWith(absRoot))
@@ -6113,7 +6890,9 @@ function consolidateBySameRoute(endpoints) {
6113
6890
  framework: ep.framework,
6114
6891
  kind: ep.kind,
6115
6892
  auth: [...ep.auth],
6116
- internal: ep.internal
6893
+ internal: ep.internal,
6894
+ transport: ep.transport,
6895
+ grpc: ep.grpc
6117
6896
  });
6118
6897
  handlerSets.set(key, new Set(ep.handler ? [ep.handler] : []));
6119
6898
  continue;
@@ -6234,6 +7013,7 @@ async function runWhiteboxAttackSurfaceWorkflow(input) {
6234
7013
  onCacheMetrics,
6235
7014
  openAIReasoningEffort,
6236
7015
  enableThinking,
7016
+ thinkingEffort,
6237
7017
  domains,
6238
7018
  projectThreatModel,
6239
7019
  environments,
@@ -6242,6 +7022,7 @@ async function runWhiteboxAttackSurfaceWorkflow(input) {
6242
7022
  } = input;
6243
7023
  const agentSlots = pLimit(Math.max(1, maxConcurrentAgents));
6244
7024
  const agentLimiter = (fn) => agentSlots(fn);
7025
+ const WORKFLOW_UMBRELLA_ID = newSessionId();
6245
7026
  const appsAgent = new CodeAgent({
6246
7027
  codebasePath,
6247
7028
  objective: buildAppsDiscoveryObjective(codebasePath, domains, environments),
@@ -6252,17 +7033,18 @@ async function runWhiteboxAttackSurfaceWorkflow(input) {
6252
7033
  abortSignal,
6253
7034
  attackSurfaceRegistry,
6254
7035
  eventBus,
6255
- subagentId: "whitebox-apps-discovery",
7036
+ subagentId: WORKFLOW_UMBRELLA_ID,
7037
+ subagentName: "Whitebox Apps Discovery",
6256
7038
  onStepFinish: (event) => onStepFinish?.(event),
6257
7039
  onCacheMetrics,
6258
7040
  openAIReasoningEffort,
6259
7041
  enableThinking,
7042
+ thinkingEffort,
6260
7043
  responseSchema: AppsDiscoveryResultSchema,
6261
7044
  projectThreatModel,
6262
7045
  excludeTools: ["document_endpoint"]
6263
7046
  });
6264
7047
  log4.info(`Phase 1: discovering apps in ${codebasePath}${domains?.length ? ` (${domains.length} known domains)` : ""}`);
6265
- const WORKFLOW_UMBRELLA_ID = "whitebox-apps-discovery";
6266
7048
  eventBus?.emit("subagent-spawn", {
6267
7049
  subagentId: WORKFLOW_UMBRELLA_ID,
6268
7050
  name: "Whitebox Apps Discovery",
@@ -6292,12 +7074,12 @@ async function runWhiteboxAttackSurfaceWorkflow(input) {
6292
7074
  }
6293
7075
  };
6294
7076
  }
6295
- const assetsPath = join15(session.rootPath, "assets");
7077
+ const assetsPath = join16(session.rootPath, "assets");
6296
7078
  mkdirSync2(assetsPath, { recursive: true });
6297
7079
  for (const app of appsResult.apps) {
6298
- const appDir = join15(assetsPath, sanitizeName(app.name));
7080
+ const appDir = join16(assetsPath, sanitizeName(app.name));
6299
7081
  mkdirSync2(appDir, { recursive: true });
6300
- writeFileSync3(join15(appDir, "app.json"), JSON.stringify(toAppMetadata(app), null, 2), "utf-8");
7082
+ writeFileSync3(join16(appDir, "app.json"), JSON.stringify(toAppMetadata(app), null, 2), "utf-8");
6301
7083
  }
6302
7084
  log4.info(`Phase 2: surfaceIntegrationEnabled=${surfaceIntegrationEnabled}`);
6303
7085
  const NON_SERVICE_TYPES = ["cloud_resource", "storage", "database"];
@@ -6323,7 +7105,7 @@ async function runWhiteboxAttackSurfaceWorkflow(input) {
6323
7105
  });
6324
7106
  }
6325
7107
  const spawnDiscoveryAgent = async (app, type2, objective) => {
6326
- const subagentId = `${type2}-${app.name}`;
7108
+ const subagentId = newSessionId();
6327
7109
  const appNodeId = appNodeIdFor(app.name);
6328
7110
  log4.debug(`Phase 2: spawning agent id="${subagentId}" parent="${appNodeId}" (app="${app.name}", type=${type2}, appType=${app.type})`);
6329
7111
  eventBus?.emit("subagent-spawn", {
@@ -6343,10 +7125,12 @@ async function runWhiteboxAttackSurfaceWorkflow(input) {
6343
7125
  attackSurfaceRegistry,
6344
7126
  eventBus,
6345
7127
  subagentId,
7128
+ subagentName: TASK_TYPE_LABELS[type2],
6346
7129
  onStepFinish: (event) => onStepFinish?.(event),
6347
7130
  onCacheMetrics,
6348
7131
  openAIReasoningEffort,
6349
7132
  enableThinking,
7133
+ thinkingEffort,
6350
7134
  responseSchema: DiscoverySummarySchema,
6351
7135
  excludeTools: ["document_app"],
6352
7136
  projectThreatModel
@@ -6384,7 +7168,7 @@ async function runWhiteboxAttackSurfaceWorkflow(input) {
6384
7168
  spawnApiEndpointsAgent(app)
6385
7169
  ]);
6386
7170
  } else {
6387
- const surfaceResult = mapAppWithSurface(join15(codebasePath, app.location), codebasePath, { isSingleAppRepo: serviceApps.length === 1 });
7171
+ const surfaceResult = mapAppWithSurface(join16(codebasePath, app.location), codebasePath, { isSingleAppRepo: serviceApps.length === 1 });
6388
7172
  if (surfaceResult.mode === "surface") {
6389
7173
  log4.debug(`${app.name}: surface-driven (${surfaceResult.endpoints.length} endpoints, frameworks=${surfaceResult.frameworks.join(",")})`);
6390
7174
  await runAppEndpointDocumentation({
@@ -6402,6 +7186,7 @@ async function runWhiteboxAttackSurfaceWorkflow(input) {
6402
7186
  onCacheMetrics,
6403
7187
  openAIReasoningEffort,
6404
7188
  enableThinking,
7189
+ thinkingEffort,
6405
7190
  projectThreatModel,
6406
7191
  parentSubagentId: appNodeId,
6407
7192
  agentLimiter
@@ -6471,12 +7256,12 @@ function readAppsFromAssetsDirectory(assetsPath, appsDiscovery) {
6471
7256
  log4.debug(`readAssets: found ${entries.length} entries in ${assetsPath}: [${entries.join(", ")}]`);
6472
7257
  const apps = [];
6473
7258
  for (const entry of entries) {
6474
- const entryPath = join15(assetsPath, entry);
7259
+ const entryPath = join16(assetsPath, entry);
6475
7260
  if (!statSync2(entryPath).isDirectory()) {
6476
7261
  log4.debug(`readAssets: skipping non-directory: ${entry}`);
6477
7262
  continue;
6478
7263
  }
6479
- const appJsonPath = join15(entryPath, "app.json");
7264
+ const appJsonPath = join16(entryPath, "app.json");
6480
7265
  let metadata;
6481
7266
  if (existsSync12(appJsonPath)) {
6482
7267
  try {
@@ -6496,7 +7281,7 @@ function readAppsFromAssetsDirectory(assetsPath, appsDiscovery) {
6496
7281
  let parseFailed = 0;
6497
7282
  for (const file of assetFiles) {
6498
7283
  try {
6499
- const raw = readFileSync9(join15(entryPath, file), "utf-8");
7284
+ const raw = readFileSync9(join16(entryPath, file), "utf-8");
6500
7285
  const data = JSON.parse(raw);
6501
7286
  const endpoint2 = assetRecordToEndpoint(data);
6502
7287
  if (!endpoint2) {
@@ -6542,7 +7327,9 @@ function assetRecordToEndpoint(record) {
6542
7327
  description: record.description,
6543
7328
  pentestObjectives: record.pentestObjectives ?? [],
6544
7329
  riskScore: record.riskScore,
6545
- threatModel: record.threatModel
7330
+ threatModel: record.threatModel,
7331
+ transport: record.transport,
7332
+ grpc: record.grpc
6546
7333
  });
6547
7334
  return parsed.success ? parsed.data : null;
6548
7335
  }
@@ -6743,6 +7530,15 @@ Find ALL API endpoints **whose route definitions live in this application's sour
6743
7530
  - **Rails**: routes.rb API namespaces, resources, controller actions
6744
7531
  - **Spring**: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @RequestMapping
6745
7532
  - **Go**: http.HandleFunc, mux.Handle, gin router methods
7533
+ - **gRPC**: \`.proto\` service definitions (\`service X { rpc Method (Req) returns (Resp); }\`), \`buf.yaml\`/\`buf.gen.yaml\`, or generated gRPC stubs
7534
+
7535
+ ### gRPC / Connect services — do NOT flatten into HTTP paths
7536
+ A gRPC method looks like a path (\`/package.Service/Method\`) but is NOT an HTTP route. When this app defines gRPC/Connect services, document **one \`document_endpoint\` per \`rpc\` method** with:
7537
+ - **endpointType**: \`"api-endpoint"\` (a gRPC method is still an API endpoint)
7538
+ - **transport**: \`"grpc"\` (or \`"grpc_web"\` / \`"connect"\` if the service is served that way)
7539
+ - **routePath**: the wire path \`/package.Service/Method\` (e.g. \`/account.v1.AccountService/GetAccount\`) — do NOT prepend a host
7540
+ - **grpc**: \`{ serviceFqn, method, streamingType, schemaSource: "proto" }\` where \`serviceFqn\` is the fully-qualified service (\`account.v1.AccountService\`), \`method\` is the rpc name, and \`streamingType\` is one of \`unary | server_stream | client_stream | bidi\` (based on the \`stream\` keywords in the rpc signature). Set \`reflectionAvailable: true\` if the server registers the gRPC reflection service.
7541
+ - If a REST/GraphQL gateway (e.g. grpc-gateway annotations, a GraphQL resolver, a Connect handler) maps to this rpc, set \`grpc.frontingGatewayOperation\` — prefer marking this whenever a gateway mapping exists, since it's what lets us later compute "shadow" methods (proto methods reachable directly but NOT exposed via the gateway, where auth checks usually live).
6746
7542
 
6747
7543
  ### How to document each endpoint
6748
7544
  For each **unique route path**, call \`document_endpoint\` with:
@@ -6756,6 +7552,7 @@ For each **unique route path**, call \`document_endpoint\` with:
6756
7552
  - **handler**: Handler function name (comma-separate if multiple handlers for different methods)
6757
7553
  - **authRequired**: Whether the endpoint requires authentication (true if ANY method requires it)
6758
7554
  - **riskLevel**: CRITICAL for auth/payment/admin, HIGH for user data mutations, MEDIUM for general, LOW for read-only public
7555
+ - **transport / grpc**: for gRPC / Connect / gRPC-Web methods, this checklist is not enough — ALSO pass the \`transport\` and \`grpc\` fields exactly as described in the "gRPC / Connect services" section above. Never document an \`rpc\` as a plain HTTP route.
6759
7556
 
6760
7557
  **CRITICAL: ONE entry per route path.** If \`/api/products\` has GET (list) and POST (create), document it as ONE entry with \`method: ["GET", "POST"]\`. Do NOT create two separate entries.
6761
7558
 
@@ -6865,21 +7662,19 @@ async function runPentestSwarm(input) {
6865
7662
  concurrency = DEFAULT_CONCURRENCY2,
6866
7663
  onStepFinish,
6867
7664
  enableThinking,
7665
+ thinkingEffort,
6868
7666
  openAIReasoningEffort,
6869
7667
  display
6870
7668
  } = input;
6871
7669
  const completedIds = getCompletedAgentIds(session);
6872
7670
  const existingManifest = readAgentManifest(session);
6873
7671
  const freshEntries = buildManifestEntries(targets);
6874
- const manifestEntries = freshEntries.map((fresh) => {
6875
- const existing = existingManifest.find((e) => e.id === fresh.id);
6876
- if (existing && existing.status === "completed")
6877
- return existing;
6878
- return fresh;
6879
- });
7672
+ const manifestEntries = reconcileManifestOnResume(existingManifest, freshEntries);
6880
7673
  writeAgentManifest(session, manifestEntries);
6881
7674
  const results = await runWithBoundedConcurrency(targets, concurrency, async (target, index) => {
6882
- const subagentId = `pentest-agent-${index + 1}`;
7675
+ const entry = manifestEntries[index];
7676
+ const subagentId = entry.id;
7677
+ const subagentName = target.name ?? entry.name;
6883
7678
  if (completedIds.has(subagentId))
6884
7679
  return null;
6885
7680
  const previousMessages = loadSubagentMessages(session, subagentId);
@@ -6892,18 +7687,19 @@ async function runPentestSwarm(input) {
6892
7687
  };
6893
7688
  eventBus?.emit("subagent-spawn", {
6894
7689
  subagentId,
6895
- name: target.name,
7690
+ name: subagentName,
6896
7691
  input: { target: target.target, objectives: target.objectives }
6897
7692
  });
6898
7693
  if (session.config?.requirePlan && session.config?.taskDriven && previousMessages.length === 0) {
6899
7694
  try {
6900
- const executionTasksDir = join17(session.rootPath, "subagents", `${subagentId}-tasks`);
7695
+ const executionTasksDir = join18(session.rootPath, "subagents", `${subagentId}-tasks`);
6901
7696
  const planAgent = new OffensiveSecurityAgent({
6902
7697
  system: PLAN_PHASE_SYSTEM_PROMPT,
6903
- prompt: buildPlanPrompt(target.target, target.objectives),
7698
+ prompt: buildPlanPrompt(target.target, target.objectives, target.grpc),
6904
7699
  model,
6905
7700
  session,
6906
7701
  target: target.target,
7702
+ grpc: target.grpc,
6907
7703
  mode: "plan",
6908
7704
  activeTools: [...PLAN_MODE_TOOL_NAMES],
6909
7705
  stopWhen: hasToolCall("submit_plan"),
@@ -6911,10 +7707,12 @@ async function runPentestSwarm(input) {
6911
7707
  abortSignal,
6912
7708
  findingsRegistry,
6913
7709
  subagentId: `${subagentId}-plan`,
7710
+ subagentName: `${subagentName} (plan)`,
6914
7711
  tasksDir: executionTasksDir,
6915
7712
  planSubagentId: subagentId,
6916
7713
  eventBus,
6917
7714
  enableThinking,
7715
+ thinkingEffort,
6918
7716
  openAIReasoningEffort,
6919
7717
  display
6920
7718
  });
@@ -6928,6 +7726,7 @@ async function runPentestSwarm(input) {
6928
7726
  try {
6929
7727
  agent = new TargetedPentestAgent({
6930
7728
  target: target.target,
7729
+ grpc: target.grpc,
6931
7730
  objectives: target.objectives,
6932
7731
  model,
6933
7732
  session,
@@ -6938,7 +7737,9 @@ async function runPentestSwarm(input) {
6938
7737
  messages: previousMessages.length > 0 ? previousMessages : undefined,
6939
7738
  eventBus,
6940
7739
  subagentId,
7740
+ subagentName,
6941
7741
  enableThinking,
7742
+ thinkingEffort,
6942
7743
  openAIReasoningEffort,
6943
7744
  display
6944
7745
  });
@@ -6984,6 +7785,10 @@ async function runPentestSwarm(input) {
6984
7785
  return results;
6985
7786
  }
6986
7787
  async function runPentestWorkflow(input) {
7788
+ if (input.fastStrike) {
7789
+ const { runFastStrike } = await import("./fastStrike-9tm6bedh.js");
7790
+ return runFastStrike(input);
7791
+ }
6987
7792
  const {
6988
7793
  target,
6989
7794
  cwd,
@@ -6995,6 +7800,7 @@ async function runPentestWorkflow(input) {
6995
7800
  onStepFinish,
6996
7801
  onCacheMetrics,
6997
7802
  enableThinking,
7803
+ thinkingEffort,
6998
7804
  openAIReasoningEffort,
6999
7805
  prompt,
7000
7806
  threatModel,
@@ -7100,8 +7906,8 @@ ${combined}` : combined;
7100
7906
  sessionId: session.id,
7101
7907
  mode
7102
7908
  });
7103
- const mdPath2 = join17(session.rootPath, REPORT_FILENAME_MD);
7104
- const jsonPath2 = join17(session.rootPath, REPORT_FILENAME_JSON);
7909
+ const mdPath2 = join18(session.rootPath, REPORT_FILENAME_MD);
7910
+ const jsonPath2 = join18(session.rootPath, REPORT_FILENAME_JSON);
7105
7911
  writeFileSync4(mdPath2, renderMarkdown(report2));
7106
7912
  writeFileSync4(jsonPath2, renderJson(report2));
7107
7913
  return {
@@ -7132,6 +7938,7 @@ ${combined}` : combined;
7132
7938
  eventBus,
7133
7939
  onStepFinish: wrappedOnStepFinish,
7134
7940
  enableThinking,
7941
+ thinkingEffort,
7135
7942
  openAIReasoningEffort
7136
7943
  });
7137
7944
  }
@@ -7150,8 +7957,8 @@ ${combined}` : combined;
7150
7957
  sessionId: session.id,
7151
7958
  mode
7152
7959
  });
7153
- const mdPath = join17(session.rootPath, REPORT_FILENAME_MD);
7154
- const jsonPath = join17(session.rootPath, REPORT_FILENAME_JSON);
7960
+ const mdPath = join18(session.rootPath, REPORT_FILENAME_MD);
7961
+ const jsonPath = join18(session.rootPath, REPORT_FILENAME_JSON);
7155
7962
  writeFileSync4(mdPath, renderMarkdown(report));
7156
7963
  writeFileSync4(jsonPath, renderJson(report));
7157
7964
  const severityCounts = {};
@@ -7196,10 +8003,14 @@ async function runWhiteboxPhase(opts) {
7196
8003
  surfaceIntegrationEnabled: opts.surfaceIntegrationEnabled
7197
8004
  };
7198
8005
  const result = await runWhiteboxAttackSurfaceWorkflow(workflowInput);
7199
- return result.apps.flatMap((app) => [...app.pages, ...app.apiEndpoints].map((ep) => ({
7200
- target: ep.path.startsWith("http") ? ep.path : `${opts.baseTarget}${ep.path}`,
7201
- objectives: ep.pentestObjectives
7202
- })));
8006
+ return result.apps.flatMap((app) => [...app.pages, ...app.apiEndpoints].map((ep) => {
8007
+ const grpc2 = toGrpcPentestContext(ep.transport, ep.grpc, ep.path);
8008
+ return {
8009
+ target: grpcAuthorityTarget(grpc2, ep.path) ?? (ep.path.startsWith("http") ? ep.path : `${opts.baseTarget}${ep.path}`),
8010
+ objectives: ep.pentestObjectives,
8011
+ grpc: grpc2
8012
+ };
8013
+ }));
7203
8014
  }
7204
8015
  async function runBlackboxPhase(opts) {
7205
8016
  let lastMessages = [];
@@ -7257,4 +8068,4 @@ async function runBlackboxPhase(opts) {
7257
8068
  throw e;
7258
8069
  }
7259
8070
  }
7260
- export { REPORT_FILENAME_MD, readExecutionMetrics, writeExecutionMetrics, convertModelMessagesToUI, loadSubagents, DEFAULT_CONCURRENCY2 as DEFAULT_CONCURRENCY, runPentestSwarm, runPentestWorkflow };
8071
+ export { readExecutionMetrics, writeExecutionMetrics, convertModelMessagesToUI, loadSubagents, DEFAULT_CONCURRENCY2 as DEFAULT_CONCURRENCY, runPentestSwarm, runPentestWorkflow };