@mcp-shark/mcp-shark 1.5.13 → 1.7.2

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 (158) hide show
  1. package/README.md +482 -56
  2. package/bin/mcp-shark.js +146 -52
  3. package/core/cli/AutoFixEngine.js +93 -0
  4. package/core/cli/ConfigScanner.js +193 -0
  5. package/core/cli/DataLoader.js +200 -0
  6. package/core/cli/DeclarativeRuleEngine.js +363 -0
  7. package/core/cli/DoctorCommand.js +218 -0
  8. package/core/cli/FixHandlers.js +222 -0
  9. package/core/cli/HtmlReportGenerator.js +203 -0
  10. package/core/cli/IdeConfigPaths.js +175 -0
  11. package/core/cli/ListCommand.js +255 -0
  12. package/core/cli/LockCommand.js +164 -0
  13. package/core/cli/LockDiffEngine.js +152 -0
  14. package/core/cli/RuleRegistryConfig.js +131 -0
  15. package/core/cli/ScanCommand.js +244 -0
  16. package/core/cli/ScanService.js +200 -0
  17. package/core/cli/SecretDetector.js +92 -0
  18. package/core/cli/SharkScoreCalculator.js +109 -0
  19. package/core/cli/ToolClassifications.js +51 -0
  20. package/core/cli/ToxicFlowAnalyzer.js +212 -0
  21. package/core/cli/UpdateCommand.js +188 -0
  22. package/core/cli/WalkthroughGenerator.js +195 -0
  23. package/core/cli/WatchCommand.js +129 -0
  24. package/core/cli/YamlRuleEngine.js +197 -0
  25. package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
  26. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  27. package/core/cli/data/rule-packs/general-security.json +173 -0
  28. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  29. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  30. package/core/cli/data/rule-sources.json +5 -0
  31. package/core/cli/data/secret-patterns.json +18 -0
  32. package/core/cli/data/tool-classifications.json +111 -0
  33. package/core/cli/data/toxic-flow-rules.json +47 -0
  34. package/core/cli/index.js +23 -0
  35. package/core/cli/output/Banner.js +52 -0
  36. package/core/cli/output/Formatter.js +183 -0
  37. package/core/cli/output/JsonFormatter.js +106 -0
  38. package/core/cli/output/index.js +16 -0
  39. package/core/cli/secureRegistryFetch.js +157 -0
  40. package/core/cli/symbols.js +16 -0
  41. package/core/configs/environment.js +3 -1
  42. package/core/configs/index.js +3 -64
  43. package/core/container/DependencyContainer.js +4 -1
  44. package/core/mcp-server/index.js +4 -1
  45. package/core/mcp-server/server/external/all.js +10 -3
  46. package/core/mcp-server/server/external/config.js +62 -5
  47. package/core/models/RequestFilters.js +3 -0
  48. package/core/repositories/PacketRepository.js +16 -0
  49. package/core/services/AuditService.js +2 -0
  50. package/core/services/ConfigService.js +9 -1
  51. package/core/services/ConfigTransformService.js +34 -2
  52. package/core/services/RequestService.js +58 -5
  53. package/core/services/ServerManagementService.js +59 -4
  54. package/core/services/security/StaticRulesService.js +69 -13
  55. package/core/services/security/TrafficAnalysisService.js +19 -1
  56. package/core/services/security/TrafficToxicFlowService.js +154 -0
  57. package/core/services/security/aauthGraph.js +199 -0
  58. package/core/services/security/aauthParser.js +274 -0
  59. package/core/services/security/aauthSelfTest.js +346 -0
  60. package/core/services/security/index.js +2 -1
  61. package/core/services/security/rules/index.js +25 -59
  62. package/core/services/security/rules/scans/configPermissions.js +91 -0
  63. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  64. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  65. package/core/services/security/rules/scans/missingContainment.js +123 -0
  66. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  67. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  68. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  69. package/core/tui/App.js +144 -0
  70. package/core/tui/FindingsPanel.js +115 -0
  71. package/core/tui/FixPanel.js +132 -0
  72. package/core/tui/Header.js +51 -0
  73. package/core/tui/HelpBar.js +42 -0
  74. package/core/tui/ServersPanel.js +109 -0
  75. package/core/tui/ToxicFlowsPanel.js +100 -0
  76. package/core/tui/h.js +8 -0
  77. package/core/tui/index.js +11 -0
  78. package/core/tui/render.js +22 -0
  79. package/package.json +24 -16
  80. package/ui/dist/assets/index-D6zDrtMV.js +81 -0
  81. package/ui/dist/index.html +1 -1
  82. package/ui/server/controllers/AauthController.js +279 -0
  83. package/ui/server/controllers/RequestController.js +12 -1
  84. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  85. package/ui/server/routes/aauth.js +18 -0
  86. package/ui/server/routes/requests.js +8 -1
  87. package/ui/server/routes/security.js +5 -1
  88. package/ui/server/setup.js +224 -6
  89. package/ui/server/swagger/paths/components.js +55 -0
  90. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  91. package/ui/server/swagger/paths.js +2 -2
  92. package/ui/server/swagger/swagger.js +5 -2
  93. package/ui/server.js +1 -1
  94. package/ui/src/App.jsx +26 -52
  95. package/ui/src/PacketFilters.jsx +31 -1
  96. package/ui/src/PacketList.jsx +2 -2
  97. package/ui/src/Security.jsx +10 -0
  98. package/ui/src/TabNavigation.jsx +8 -0
  99. package/ui/src/components/AAuthBadge.jsx +92 -0
  100. package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
  101. package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
  102. package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
  103. package/ui/src/components/App/ActionMenu.jsx +4 -31
  104. package/ui/src/components/App/ApiDocsButton.jsx +0 -1
  105. package/ui/src/components/App/ShutdownButton.jsx +0 -1
  106. package/ui/src/components/App/useAppState.js +19 -26
  107. package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
  108. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
  109. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
  110. package/ui/src/components/DetectedPathsList.jsx +1 -5
  111. package/ui/src/components/FileInput.jsx +0 -1
  112. package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
  113. package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
  114. package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
  115. package/ui/src/components/Security/ScannerContent.jsx +33 -1
  116. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  117. package/ui/src/components/Security/securityApi.js +15 -0
  118. package/ui/src/components/Security/useSecurity.js +60 -3
  119. package/ui/src/components/ServerControl.jsx +0 -1
  120. package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
  121. package/ui/src/components/TabNavigationIcons.jsx +5 -0
  122. package/ui/src/components/ViewModeTabs.jsx +0 -1
  123. package/ui/src/utils/animations.js +26 -9
  124. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  125. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  126. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  127. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  128. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  129. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  130. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  131. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  132. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  133. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  134. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  135. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  136. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  137. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  138. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  139. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  140. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  141. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  142. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
  143. package/ui/dist/assets/index-CiCSDYf-.js +0 -97
  144. package/ui/server/routes/help.js +0 -44
  145. package/ui/server/swagger/paths/help.js +0 -82
  146. package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
  147. package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
  148. package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
  149. package/ui/src/HelpGuide.jsx +0 -78
  150. package/ui/src/IntroTour.jsx +0 -154
  151. package/ui/src/components/App/HelpButton.jsx +0 -90
  152. package/ui/src/components/TourOverlay.jsx +0 -117
  153. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
  154. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
  155. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
  156. package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
  157. package/ui/src/components/TourTooltip.jsx +0 -91
  158. package/ui/src/config/tourSteps.jsx +0 -140
@@ -0,0 +1,387 @@
1
+ import { IconFlask, IconKey, IconRefresh, IconShieldCheck } from '@tabler/icons-react';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { colors, fonts } from '../../theme.js';
4
+ import AauthExplorerGraph from './AauthExplorerGraph.jsx';
5
+ import NodeDetailPanel from './NodeDetailPanel.jsx';
6
+
7
+ const CATEGORY_META = {
8
+ agent: { label: 'Agents', color: '#1a73e8' },
9
+ mission: { label: 'Missions', color: '#e8710a' },
10
+ resource: { label: 'Resources', color: '#137333' },
11
+ signing: { label: 'Signing', color: '#9334e6' },
12
+ access: { label: 'Access modes', color: '#b06000' },
13
+ };
14
+
15
+ const POLL_INTERVAL_MS = 5000;
16
+
17
+ /**
18
+ * Top-level AAuth Explorer page.
19
+ *
20
+ * Pulls the live AAuth knowledge graph from /api/aauth/graph, renders it as a
21
+ * force-directed view (AauthExplorerGraph), and shows a side panel of
22
+ * underlying packets when a node is clicked.
23
+ *
24
+ * A "Generate sample data" button is included so a developer can populate
25
+ * the view with FAKE traffic for demos / screenshots / first-time orientation
26
+ * before any real AAuth-aware MCP exists in their stack. Synthetic packets
27
+ * are deliberately tagged `user-agent: mcp-shark-self-test/1.0` so they are
28
+ * trivially distinguishable from real captures.
29
+ *
30
+ * Inspired by https://mcp-shark.github.io/aauth-explorer/, but every node
31
+ * here is grounded in observed packets rather than a static spec figure.
32
+ */
33
+ export default function AauthExplorerView({ onOpenPacket }) {
34
+ const [graph, setGraph] = useState(null);
35
+ const [loading, setLoading] = useState(false);
36
+ const [running, setRunning] = useState(false);
37
+ const [error, setError] = useState(null);
38
+ const [selection, setSelection] = useState(null);
39
+ const [autoRefresh, setAutoRefresh] = useState(true);
40
+ const [lastRun, setLastRun] = useState(null);
41
+ const [upstreamCount, setUpstreamCount] = useState(0);
42
+
43
+ const load = useCallback(async () => {
44
+ setError(null);
45
+ try {
46
+ const [gRes, uRes] = await Promise.all([
47
+ fetch('/api/aauth/graph'),
48
+ fetch('/api/aauth/upstreams'),
49
+ ]);
50
+ if (!gRes.ok) {
51
+ throw new Error(`HTTP ${gRes.status}`);
52
+ }
53
+ const json = await gRes.json();
54
+ setGraph(json);
55
+ if (uRes.ok) {
56
+ const u = await uRes.json();
57
+ setUpstreamCount(u.count || 0);
58
+ }
59
+ } catch (err) {
60
+ setError(err.message || 'Failed to load AAuth graph');
61
+ }
62
+ }, []);
63
+
64
+ const generateSampleTraffic = useCallback(async () => {
65
+ setRunning(true);
66
+ setError(null);
67
+ try {
68
+ const res = await fetch('/api/aauth/self-test', {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ rounds: 2 }),
72
+ });
73
+ if (!res.ok) {
74
+ throw new Error(`HTTP ${res.status}`);
75
+ }
76
+ const result = await res.json();
77
+ setLastRun(result);
78
+ await load();
79
+ } catch (err) {
80
+ setError(err.message || 'Failed to generate sample traffic');
81
+ } finally {
82
+ setRunning(false);
83
+ }
84
+ }, [load]);
85
+
86
+ useEffect(() => {
87
+ setLoading(true);
88
+ load().finally(() => setLoading(false));
89
+ }, [load]);
90
+
91
+ useEffect(() => {
92
+ if (!autoRefresh) {
93
+ return undefined;
94
+ }
95
+ const id = setInterval(load, POLL_INTERVAL_MS);
96
+ return () => clearInterval(id);
97
+ }, [autoRefresh, load]);
98
+
99
+ const stats = graph?.stats || { observed_packets: 0, node_counts: {}, edge_count: 0 };
100
+
101
+ return (
102
+ <div
103
+ style={{
104
+ display: 'flex',
105
+ flexDirection: 'column',
106
+ height: '100%',
107
+ width: '100%',
108
+ background: colors.bgPrimary,
109
+ }}
110
+ >
111
+ <Header
112
+ running={running}
113
+ loading={loading}
114
+ upstreamCount={upstreamCount}
115
+ autoRefresh={autoRefresh}
116
+ onToggleAutoRefresh={() => setAutoRefresh((v) => !v)}
117
+ onRefresh={() => {
118
+ setLoading(true);
119
+ load().finally(() => setLoading(false));
120
+ }}
121
+ onGenerateSample={generateSampleTraffic}
122
+ />
123
+
124
+ {lastRun && (
125
+ <div
126
+ style={{
127
+ padding: '8px 16px',
128
+ background: `${colors.warning}1A`,
129
+ borderBottom: `1px solid ${colors.warning}55`,
130
+ fontSize: '11px',
131
+ fontFamily: fonts.body,
132
+ color: colors.textSecondary,
133
+ }}
134
+ >
135
+ <strong>Sample data inserted:</strong> {lastRun.inserted} fake packets across{' '}
136
+ {lastRun.targets?.length || 0} upstream
137
+ {(lastRun.targets?.length || 0) === 1 ? '' : 's'} (tagged{' '}
138
+ <code style={{ fontFamily: fonts.mono }}>user-agent: mcp-shark-self-test/1.0</code>) ·{' '}
139
+ {Object.entries(lastRun.by_posture || {})
140
+ .map(([k, v]) => `${k}=${v}`)
141
+ .join(' · ')}
142
+ </div>
143
+ )}
144
+
145
+ {error && (
146
+ <div
147
+ style={{
148
+ padding: '10px 16px',
149
+ background: colors.errorBg,
150
+ borderBottom: `1px solid ${colors.error}55`,
151
+ fontSize: '12px',
152
+ fontFamily: fonts.body,
153
+ color: colors.error,
154
+ }}
155
+ >
156
+ {error}
157
+ </div>
158
+ )}
159
+
160
+ <StatsStrip stats={stats} />
161
+
162
+ <div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
163
+ <div
164
+ style={{
165
+ flex: 1,
166
+ position: 'relative',
167
+ minWidth: 0,
168
+ background: colors.bgPrimary,
169
+ borderTop: `1px solid ${colors.borderLight}`,
170
+ }}
171
+ >
172
+ <AauthExplorerGraph
173
+ graph={graph}
174
+ onNodeSelect={(sel) => setSelection(sel)}
175
+ selectedNodeId={selection?.nodeKey || null}
176
+ />
177
+ <DisclaimerBadge />
178
+ </div>
179
+ {selection && (
180
+ <NodeDetailPanel
181
+ selection={selection}
182
+ onClose={() => setSelection(null)}
183
+ onOpenPacket={onOpenPacket}
184
+ />
185
+ )}
186
+ </div>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ function Header({
192
+ running,
193
+ loading,
194
+ upstreamCount,
195
+ autoRefresh,
196
+ onToggleAutoRefresh,
197
+ onRefresh,
198
+ onGenerateSample,
199
+ }) {
200
+ return (
201
+ <div
202
+ style={{
203
+ padding: '16px 20px',
204
+ background: colors.bgCard,
205
+ borderBottom: `1px solid ${colors.borderLight}`,
206
+ display: 'flex',
207
+ alignItems: 'center',
208
+ justifyContent: 'space-between',
209
+ gap: '16px',
210
+ flexWrap: 'wrap',
211
+ }}
212
+ >
213
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
214
+ <IconKey size={22} stroke={1.5} style={{ color: '#1a73e8' }} />
215
+ <div>
216
+ <div
217
+ style={{
218
+ fontSize: '16px',
219
+ fontWeight: 600,
220
+ fontFamily: fonts.body,
221
+ color: colors.textPrimary,
222
+ lineHeight: 1.1,
223
+ }}
224
+ >
225
+ AAuth Explorer
226
+ </div>
227
+ <div
228
+ style={{
229
+ fontSize: '11px',
230
+ color: colors.textSecondary,
231
+ fontFamily: fonts.body,
232
+ marginTop: '2px',
233
+ }}
234
+ >
235
+ Auto-detected from captured traffic ·{' '}
236
+ {upstreamCount > 0
237
+ ? `${upstreamCount} HTTP upstream${upstreamCount === 1 ? '' : 's'} configured`
238
+ : 'no HTTP upstreams configured'}{' '}
239
+ · observation only, no verification
240
+ </div>
241
+ </div>
242
+ </div>
243
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
244
+ <label
245
+ style={{
246
+ display: 'inline-flex',
247
+ alignItems: 'center',
248
+ gap: '6px',
249
+ fontSize: '11px',
250
+ fontFamily: fonts.body,
251
+ color: colors.textSecondary,
252
+ cursor: 'pointer',
253
+ }}
254
+ >
255
+ <input
256
+ type="checkbox"
257
+ checked={autoRefresh}
258
+ onChange={onToggleAutoRefresh}
259
+ style={{ cursor: 'pointer' }}
260
+ />
261
+ Live refresh
262
+ </label>
263
+ <button
264
+ type="button"
265
+ onClick={onRefresh}
266
+ disabled={loading}
267
+ aria-label="Refresh AAuth graph"
268
+ style={{
269
+ display: 'inline-flex',
270
+ alignItems: 'center',
271
+ gap: '6px',
272
+ padding: '6px 12px',
273
+ background: 'transparent',
274
+ border: `1px solid ${colors.borderLight}`,
275
+ borderRadius: '6px',
276
+ color: colors.textSecondary,
277
+ fontSize: '12px',
278
+ fontFamily: fonts.body,
279
+ cursor: loading ? 'wait' : 'pointer',
280
+ }}
281
+ >
282
+ <IconRefresh size={13} stroke={1.5} />
283
+ Refresh
284
+ </button>
285
+ <button
286
+ type="button"
287
+ onClick={onGenerateSample}
288
+ disabled={running}
289
+ title="Inserts fake AAuth packets so you can see how the visualization works. Not real traffic — synthetic packets are tagged with user-agent mcp-shark-self-test/1.0."
290
+ aria-label="Generate fake sample AAuth traffic for demo purposes"
291
+ style={{
292
+ display: 'inline-flex',
293
+ alignItems: 'center',
294
+ gap: '6px',
295
+ padding: '6px 14px',
296
+ background: colors.warning,
297
+ border: `1px solid ${colors.warning}`,
298
+ borderRadius: '6px',
299
+ color: '#fff',
300
+ fontSize: '12px',
301
+ fontFamily: fonts.body,
302
+ fontWeight: 500,
303
+ cursor: running ? 'wait' : 'pointer',
304
+ opacity: running ? 0.7 : 1,
305
+ }}
306
+ >
307
+ <IconFlask size={13} stroke={1.5} />
308
+ {running ? 'Generating…' : 'Generate sample data'}
309
+ </button>
310
+ </div>
311
+ </div>
312
+ );
313
+ }
314
+
315
+ function StatsStrip({ stats }) {
316
+ const counts = stats.node_counts || {};
317
+ return (
318
+ <div
319
+ style={{
320
+ display: 'flex',
321
+ gap: '24px',
322
+ padding: '10px 20px',
323
+ background: colors.bgSecondary,
324
+ borderBottom: `1px solid ${colors.borderLight}`,
325
+ fontFamily: fonts.body,
326
+ fontSize: '11px',
327
+ color: colors.textSecondary,
328
+ flexWrap: 'wrap',
329
+ }}
330
+ >
331
+ {Object.entries(CATEGORY_META).map(([id, meta]) => (
332
+ <div key={id} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
333
+ <span
334
+ aria-hidden="true"
335
+ style={{
336
+ width: '10px',
337
+ height: '10px',
338
+ borderRadius: '50%',
339
+ background: meta.color,
340
+ display: 'inline-block',
341
+ }}
342
+ />
343
+ <span>{meta.label}</span>
344
+ <strong style={{ color: colors.textPrimary, fontFamily: fonts.mono }}>
345
+ {counts[id] || 0}
346
+ </strong>
347
+ </div>
348
+ ))}
349
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginLeft: 'auto' }}>
350
+ <span>Edges</span>
351
+ <strong style={{ color: colors.textPrimary, fontFamily: fonts.mono }}>
352
+ {stats.edge_count || 0}
353
+ </strong>
354
+ <span style={{ marginLeft: '12px' }}>Observed packets</span>
355
+ <strong style={{ color: colors.textPrimary, fontFamily: fonts.mono }}>
356
+ {stats.observed_packets || 0}
357
+ </strong>
358
+ </div>
359
+ </div>
360
+ );
361
+ }
362
+
363
+ function DisclaimerBadge() {
364
+ return (
365
+ <div
366
+ style={{
367
+ position: 'absolute',
368
+ bottom: 12,
369
+ left: 12,
370
+ padding: '6px 10px',
371
+ background: `${colors.bgCard}E6`,
372
+ border: `1px solid ${colors.borderLight}`,
373
+ borderRadius: '6px',
374
+ fontSize: '10px',
375
+ fontFamily: fonts.body,
376
+ color: colors.textSecondary,
377
+ display: 'inline-flex',
378
+ alignItems: 'center',
379
+ gap: '6px',
380
+ boxShadow: `0 1px 2px ${colors.shadowSm}`,
381
+ }}
382
+ >
383
+ <IconShieldCheck size={11} stroke={1.5} />
384
+ Observation only · no signature verification is performed
385
+ </div>
386
+ );
387
+ }
@@ -0,0 +1,272 @@
1
+ import { IconExternalLink, IconX } from '@tabler/icons-react';
2
+ import { useEffect, useState } from 'react';
3
+ import { colors, fonts } from '../../theme.js';
4
+
5
+ const CATEGORY_LABELS = {
6
+ agent: 'Agent',
7
+ mission: 'Mission',
8
+ resource: 'Resource',
9
+ signing: 'Signing algorithm',
10
+ access: 'Access mode',
11
+ };
12
+
13
+ const POSTURE_COLORS = {
14
+ signed: colors.success,
15
+ 'aauth-aware': '#1a73e8',
16
+ bearer: colors.warning,
17
+ none: colors.textTertiary,
18
+ };
19
+
20
+ /**
21
+ * Side panel that lists the captured packets backing a single AAuth Explorer
22
+ * node. Calls GET /api/aauth/node/:category/:id and renders a compact
23
+ * scrollable list. Each row deep-links into the Traffic tab via the supplied
24
+ * onOpenPacket callback (so the developer can pivot to the raw packet inspector).
25
+ */
26
+ export default function NodeDetailPanel({ selection, onClose, onOpenPacket }) {
27
+ const [data, setData] = useState(null);
28
+ const [loading, setLoading] = useState(false);
29
+ const [error, setError] = useState(null);
30
+
31
+ useEffect(() => {
32
+ if (!selection) {
33
+ setData(null);
34
+ return undefined;
35
+ }
36
+ let cancelled = false;
37
+ setLoading(true);
38
+ setError(null);
39
+ fetch(
40
+ `/api/aauth/node/${encodeURIComponent(selection.category)}/${encodeURIComponent(selection.id)}`
41
+ )
42
+ .then(async (res) => {
43
+ if (!res.ok) {
44
+ throw new Error(`HTTP ${res.status}`);
45
+ }
46
+ return res.json();
47
+ })
48
+ .then((json) => {
49
+ if (!cancelled) {
50
+ setData(json);
51
+ }
52
+ })
53
+ .catch((err) => {
54
+ if (!cancelled) {
55
+ setError(err.message || 'Failed to load node packets');
56
+ }
57
+ })
58
+ .finally(() => {
59
+ if (!cancelled) {
60
+ setLoading(false);
61
+ }
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }, [selection]);
67
+
68
+ if (!selection) {
69
+ return null;
70
+ }
71
+
72
+ return (
73
+ <div
74
+ style={{
75
+ width: '380px',
76
+ minWidth: '320px',
77
+ height: '100%',
78
+ background: colors.bgCard,
79
+ borderLeft: `1px solid ${colors.borderLight}`,
80
+ display: 'flex',
81
+ flexDirection: 'column',
82
+ boxShadow: `-2px 0 8px ${colors.shadowSm}`,
83
+ }}
84
+ >
85
+ <div
86
+ style={{
87
+ padding: '14px 16px',
88
+ borderBottom: `1px solid ${colors.borderLight}`,
89
+ display: 'flex',
90
+ alignItems: 'flex-start',
91
+ justifyContent: 'space-between',
92
+ gap: '12px',
93
+ }}
94
+ >
95
+ <div style={{ minWidth: 0 }}>
96
+ <div
97
+ style={{
98
+ fontSize: '11px',
99
+ fontFamily: fonts.body,
100
+ color: colors.textTertiary,
101
+ textTransform: 'uppercase',
102
+ letterSpacing: '0.05em',
103
+ }}
104
+ >
105
+ {CATEGORY_LABELS[selection.category] || selection.category}
106
+ </div>
107
+ <div
108
+ style={{
109
+ fontSize: '14px',
110
+ fontFamily: fonts.body,
111
+ fontWeight: 600,
112
+ color: colors.textPrimary,
113
+ marginTop: '2px',
114
+ wordBreak: 'break-all',
115
+ }}
116
+ >
117
+ {selection.id}
118
+ </div>
119
+ {selection.raw?.packet_count != null && (
120
+ <div
121
+ style={{
122
+ fontSize: '11px',
123
+ fontFamily: fonts.body,
124
+ color: colors.textSecondary,
125
+ marginTop: '4px',
126
+ }}
127
+ >
128
+ {selection.raw.packet_count} observation
129
+ {selection.raw.packet_count === 1 ? '' : 's'}
130
+ </div>
131
+ )}
132
+ </div>
133
+ <button
134
+ type="button"
135
+ onClick={onClose}
136
+ aria-label="Close detail panel"
137
+ style={{
138
+ padding: '4px',
139
+ background: 'transparent',
140
+ border: 'none',
141
+ cursor: 'pointer',
142
+ color: colors.textSecondary,
143
+ }}
144
+ >
145
+ <IconX size={16} stroke={1.5} />
146
+ </button>
147
+ </div>
148
+
149
+ <div style={{ flex: 1, overflowY: 'auto', padding: '8px 0' }}>
150
+ {loading && (
151
+ <div
152
+ style={{
153
+ padding: '14px 16px',
154
+ fontSize: '12px',
155
+ fontFamily: fonts.body,
156
+ color: colors.textSecondary,
157
+ }}
158
+ >
159
+ Loading observations…
160
+ </div>
161
+ )}
162
+ {error && (
163
+ <div
164
+ style={{
165
+ padding: '14px 16px',
166
+ fontSize: '12px',
167
+ fontFamily: fonts.body,
168
+ color: colors.error,
169
+ }}
170
+ >
171
+ {error}
172
+ </div>
173
+ )}
174
+ {data && data.packets?.length === 0 && !loading && (
175
+ <div
176
+ style={{
177
+ padding: '14px 16px',
178
+ fontSize: '12px',
179
+ fontFamily: fonts.body,
180
+ color: colors.textSecondary,
181
+ }}
182
+ >
183
+ No matching packets are currently in the capture buffer.
184
+ </div>
185
+ )}
186
+ {data?.packets?.map((p) => (
187
+ <button
188
+ key={p.frame_number}
189
+ type="button"
190
+ onClick={() => onOpenPacket?.(p.frame_number)}
191
+ style={{
192
+ display: 'block',
193
+ width: '100%',
194
+ textAlign: 'left',
195
+ padding: '10px 16px',
196
+ borderTop: 'none',
197
+ borderRight: 'none',
198
+ borderLeft: `3px solid ${POSTURE_COLORS[p.posture] || colors.textTertiary}`,
199
+ borderBottom: `1px solid ${colors.borderLight}`,
200
+ background: 'transparent',
201
+ cursor: 'pointer',
202
+ fontFamily: fonts.body,
203
+ }}
204
+ onMouseEnter={(e) => {
205
+ e.currentTarget.style.background = colors.bgHover;
206
+ }}
207
+ onMouseLeave={(e) => {
208
+ e.currentTarget.style.background = 'transparent';
209
+ }}
210
+ >
211
+ <div
212
+ style={{
213
+ display: 'flex',
214
+ alignItems: 'center',
215
+ justifyContent: 'space-between',
216
+ gap: '8px',
217
+ marginBottom: '4px',
218
+ }}
219
+ >
220
+ <span
221
+ style={{
222
+ fontSize: '11px',
223
+ fontFamily: fonts.mono,
224
+ color: colors.textSecondary,
225
+ }}
226
+ >
227
+ #{p.frame_number} · {p.direction}
228
+ {p.method ? ` · ${p.method}` : ''}
229
+ {p.status_code ? ` · ${p.status_code}` : ''}
230
+ </span>
231
+ <IconExternalLink size={11} stroke={1.5} style={{ color: colors.textTertiary }} />
232
+ </div>
233
+ <div
234
+ style={{
235
+ fontSize: '12px',
236
+ color: colors.textPrimary,
237
+ wordBreak: 'break-all',
238
+ }}
239
+ >
240
+ {p.jsonrpc_method || p.url || p.host || '(no destination)'}
241
+ </div>
242
+ <div
243
+ style={{
244
+ fontSize: '10px',
245
+ marginTop: '4px',
246
+ display: 'flex',
247
+ gap: '8px',
248
+ flexWrap: 'wrap',
249
+ color: colors.textTertiary,
250
+ fontFamily: fonts.mono,
251
+ }}
252
+ >
253
+ <span style={{ color: POSTURE_COLORS[p.posture] || colors.textTertiary }}>
254
+ {p.posture}
255
+ </span>
256
+ {p.agent && <span>agent={truncate(p.agent, 32)}</span>}
257
+ {p.mission && <span>mission={truncate(p.mission, 28)}</span>}
258
+ {p.sig_alg && <span>alg={p.sig_alg}</span>}
259
+ </div>
260
+ </button>
261
+ ))}
262
+ </div>
263
+ </div>
264
+ );
265
+ }
266
+
267
+ function truncate(s, n) {
268
+ if (!s || s.length <= n) {
269
+ return s;
270
+ }
271
+ return `${s.slice(0, n - 1)}…`;
272
+ }