web-agent-bridge 3.2.0 → 3.4.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 (256) hide show
  1. package/LICENSE +84 -72
  2. package/README.ar.md +1304 -1152
  3. package/README.md +298 -1635
  4. package/bin/agent-runner.js +474 -474
  5. package/bin/cli.js +237 -138
  6. package/bin/wab-init.js +223 -0
  7. package/bin/wab.js +80 -80
  8. package/examples/azure-dns-wab.js +83 -0
  9. package/examples/bidi-agent.js +119 -119
  10. package/examples/cloudflare-wab-dns.js +121 -0
  11. package/examples/cpanel-wab-dns.js +114 -0
  12. package/examples/cross-site-agent.js +91 -91
  13. package/examples/dns-discovery-agent.js +166 -0
  14. package/examples/gcp-dns-wab.js +76 -0
  15. package/examples/governance-agent.js +169 -0
  16. package/examples/mcp-agent.js +94 -94
  17. package/examples/next-app-router/README.md +44 -44
  18. package/examples/plesk-wab-dns.js +103 -0
  19. package/examples/puppeteer-agent.js +108 -108
  20. package/examples/route53-wab-dns.js +144 -0
  21. package/examples/saas-dashboard/README.md +55 -55
  22. package/examples/safe-mode-agent.js +96 -0
  23. package/examples/shopify-hydrogen/README.md +74 -74
  24. package/examples/vision-agent.js +171 -171
  25. package/examples/wab-sign.js +74 -0
  26. package/examples/wab-verify.js +60 -0
  27. package/examples/wordpress-elementor/README.md +77 -77
  28. package/package.json +19 -6
  29. package/public/.well-known/agent-tools.json +180 -180
  30. package/public/.well-known/ai-assets.json +59 -59
  31. package/public/.well-known/security.txt +8 -0
  32. package/public/.well-known/wab.json +28 -0
  33. package/public/activate.html +368 -0
  34. package/public/adoption-metrics.html +188 -0
  35. package/public/agent-workspace.html +349 -349
  36. package/public/ai.html +198 -198
  37. package/public/api.html +413 -412
  38. package/public/azure-dns-integration.html +289 -0
  39. package/public/browser.html +486 -486
  40. package/public/cloudflare-integration.html +380 -0
  41. package/public/commander-dashboard.html +243 -243
  42. package/public/cookies.html +210 -210
  43. package/public/cpanel-integration.html +398 -0
  44. package/public/css/agent-workspace.css +1713 -1713
  45. package/public/css/premium.css +317 -317
  46. package/public/css/styles.css +1263 -1235
  47. package/public/dashboard.html +707 -706
  48. package/public/dns.html +436 -0
  49. package/public/docs.html +588 -587
  50. package/public/feed.xml +89 -89
  51. package/public/gcp-dns-integration.html +318 -0
  52. package/public/growth.html +465 -463
  53. package/public/index.html +1266 -982
  54. package/public/integrations.html +556 -0
  55. package/public/js/activate.js +145 -0
  56. package/public/js/agent-workspace.js +1740 -1740
  57. package/public/js/auth-nav.js +65 -31
  58. package/public/js/auth-redirect.js +12 -12
  59. package/public/js/cookie-consent.js +56 -56
  60. package/public/js/dns.js +438 -0
  61. package/public/js/wab-demo-page.js +721 -721
  62. package/public/js/ws-client.js +74 -74
  63. package/public/llms-full.txt +360 -360
  64. package/public/llms.txt +125 -125
  65. package/public/login.html +85 -85
  66. package/public/mesh-dashboard.html +328 -328
  67. package/public/openapi.json +669 -580
  68. package/public/phone-shield.html +281 -0
  69. package/public/plesk-integration.html +375 -0
  70. package/public/premium-dashboard.html +2489 -2489
  71. package/public/premium.html +793 -793
  72. package/public/privacy.html +297 -297
  73. package/public/provider-onboarding.html +172 -0
  74. package/public/provider-sandbox.html +134 -0
  75. package/public/providers.html +359 -0
  76. package/public/register.html +105 -105
  77. package/public/registrar-integrations.html +141 -0
  78. package/public/robots.txt +99 -87
  79. package/public/route53-integration.html +531 -0
  80. package/public/script/wab-consent.d.ts +36 -36
  81. package/public/script/wab-consent.js +104 -104
  82. package/public/script/wab-schema.js +131 -131
  83. package/public/script/wab.d.ts +108 -108
  84. package/public/script/wab.min.js +580 -580
  85. package/public/security.txt +8 -0
  86. package/public/shieldqr.html +231 -0
  87. package/public/sitemap.xml +6 -0
  88. package/public/terms.html +256 -256
  89. package/public/wab-trust.html +200 -0
  90. package/public/wab-vs-protocols.html +210 -0
  91. package/public/whitepaper.html +449 -0
  92. package/script/ai-agent-bridge.js +1754 -1754
  93. package/sdk/README.md +99 -99
  94. package/sdk/agent-mesh.js +449 -449
  95. package/sdk/auto-discovery.js +288 -0
  96. package/sdk/commander.js +262 -262
  97. package/sdk/governance.js +262 -0
  98. package/sdk/index.d.ts +464 -464
  99. package/sdk/index.js +25 -1
  100. package/sdk/multi-agent.js +318 -318
  101. package/sdk/package.json +2 -2
  102. package/sdk/safe-mode.js +221 -0
  103. package/sdk/safety-shield.js +219 -0
  104. package/sdk/schema-discovery.js +83 -83
  105. package/server/adapters/index.js +520 -520
  106. package/server/config/plans.js +367 -367
  107. package/server/config/secrets.js +102 -102
  108. package/server/control-plane/index.js +301 -301
  109. package/server/data-plane/index.js +354 -354
  110. package/server/index.js +670 -427
  111. package/server/llm/index.js +404 -404
  112. package/server/middleware/adminAuth.js +35 -35
  113. package/server/middleware/auth.js +50 -50
  114. package/server/middleware/featureGate.js +88 -88
  115. package/server/middleware/rateLimits.js +100 -100
  116. package/server/middleware/sensitiveAction.js +157 -0
  117. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  118. package/server/migrations/002_premium_features.sql +418 -418
  119. package/server/migrations/003_ads_integer_cents.sql +33 -33
  120. package/server/migrations/004_agent_os.sql +158 -158
  121. package/server/migrations/005_marketplace_metering.sql +126 -126
  122. package/server/migrations/007_governance.sql +106 -0
  123. package/server/migrations/008_plans.sql +144 -0
  124. package/server/migrations/009_shieldqr.sql +30 -0
  125. package/server/migrations/010_extended_trust.sql +33 -0
  126. package/server/models/adapters/index.js +33 -33
  127. package/server/models/adapters/mysql.js +183 -183
  128. package/server/models/adapters/postgresql.js +172 -172
  129. package/server/models/adapters/sqlite.js +7 -7
  130. package/server/models/db.js +740 -681
  131. package/server/observability/failure-analysis.js +337 -337
  132. package/server/observability/index.js +394 -394
  133. package/server/protocol/capabilities.js +223 -223
  134. package/server/protocol/index.js +243 -243
  135. package/server/protocol/schema.js +584 -584
  136. package/server/registry/certification.js +271 -271
  137. package/server/registry/index.js +326 -326
  138. package/server/routes/admin-plans.js +76 -0
  139. package/server/routes/admin-premium.js +673 -671
  140. package/server/routes/admin-shieldqr.js +90 -0
  141. package/server/routes/admin-trust-monitor.js +83 -0
  142. package/server/routes/admin.js +549 -261
  143. package/server/routes/ads.js +130 -130
  144. package/server/routes/agent-workspace.js +540 -540
  145. package/server/routes/api.js +150 -150
  146. package/server/routes/auth.js +71 -71
  147. package/server/routes/billing.js +57 -45
  148. package/server/routes/commander.js +316 -316
  149. package/server/routes/demo-showcase.js +332 -332
  150. package/server/routes/demo-store.js +154 -0
  151. package/server/routes/discovery.js +2348 -417
  152. package/server/routes/gateway.js +173 -157
  153. package/server/routes/governance.js +208 -0
  154. package/server/routes/license.js +251 -240
  155. package/server/routes/mesh.js +469 -469
  156. package/server/routes/noscript.js +543 -543
  157. package/server/routes/plans.js +33 -0
  158. package/server/routes/premium-v2.js +686 -686
  159. package/server/routes/premium.js +724 -724
  160. package/server/routes/providers.js +650 -0
  161. package/server/routes/runtime.js +2148 -2147
  162. package/server/routes/shieldqr.js +88 -0
  163. package/server/routes/sovereign.js +465 -385
  164. package/server/routes/universal.js +200 -185
  165. package/server/routes/wab-api.js +850 -501
  166. package/server/runtime/container-worker.js +111 -111
  167. package/server/runtime/container.js +448 -448
  168. package/server/runtime/distributed-worker.js +362 -362
  169. package/server/runtime/event-bus.js +210 -210
  170. package/server/runtime/index.js +253 -253
  171. package/server/runtime/queue.js +599 -599
  172. package/server/runtime/replay.js +666 -666
  173. package/server/runtime/sandbox.js +266 -266
  174. package/server/runtime/scheduler.js +534 -534
  175. package/server/runtime/session-engine.js +293 -293
  176. package/server/runtime/state-manager.js +188 -188
  177. package/server/security/cross-site-redactor.js +196 -0
  178. package/server/security/dry-run.js +180 -0
  179. package/server/security/human-gate-rate-limit.js +147 -0
  180. package/server/security/human-gate-transports.js +178 -0
  181. package/server/security/human-gate.js +281 -0
  182. package/server/security/index.js +368 -368
  183. package/server/security/intent-engine.js +245 -0
  184. package/server/security/reward-guard.js +171 -0
  185. package/server/security/rollback-store.js +239 -0
  186. package/server/security/token-scope.js +404 -0
  187. package/server/security/url-policy.js +139 -0
  188. package/server/services/agent-chat.js +506 -506
  189. package/server/services/agent-learning.js +601 -575
  190. package/server/services/agent-memory.js +625 -625
  191. package/server/services/agent-mesh.js +555 -539
  192. package/server/services/agent-symphony.js +717 -717
  193. package/server/services/agent-tasks.js +1807 -1807
  194. package/server/services/api-key-engine.js +292 -261
  195. package/server/services/cluster.js +894 -894
  196. package/server/services/commander.js +738 -738
  197. package/server/services/edge-compute.js +440 -440
  198. package/server/services/email.js +233 -204
  199. package/server/services/governance.js +466 -0
  200. package/server/services/hosted-runtime.js +205 -205
  201. package/server/services/lfd.js +635 -635
  202. package/server/services/local-ai.js +389 -389
  203. package/server/services/marketplace.js +270 -270
  204. package/server/services/metering.js +182 -182
  205. package/server/services/modules/affiliate-intelligence.js +93 -93
  206. package/server/services/modules/agent-firewall.js +90 -90
  207. package/server/services/modules/bounty.js +89 -89
  208. package/server/services/modules/collective-bargaining.js +92 -92
  209. package/server/services/modules/dark-pattern.js +66 -66
  210. package/server/services/modules/gov-intelligence.js +45 -45
  211. package/server/services/modules/neural.js +55 -55
  212. package/server/services/modules/notary.js +49 -49
  213. package/server/services/modules/price-time-machine.js +86 -86
  214. package/server/services/modules/protocol.js +104 -104
  215. package/server/services/negotiation.js +439 -439
  216. package/server/services/plans.js +214 -0
  217. package/server/services/plugins.js +771 -771
  218. package/server/services/premium.js +1 -1
  219. package/server/services/price-intelligence.js +566 -566
  220. package/server/services/price-shield.js +1137 -1137
  221. package/server/services/provider-clients.js +740 -0
  222. package/server/services/reputation.js +465 -465
  223. package/server/services/search-engine.js +357 -357
  224. package/server/services/security.js +513 -513
  225. package/server/services/self-healing.js +843 -843
  226. package/server/services/shieldqr.js +322 -0
  227. package/server/services/sovereign-shield.js +542 -0
  228. package/server/services/ssl-inspector.js +42 -0
  229. package/server/services/ssl-monitor.js +167 -0
  230. package/server/services/stripe.js +205 -192
  231. package/server/services/swarm.js +788 -788
  232. package/server/services/universal-scraper.js +662 -661
  233. package/server/services/verification.js +481 -481
  234. package/server/services/vision.js +1163 -1163
  235. package/server/services/wab-crypto.js +178 -0
  236. package/server/utils/cache.js +125 -125
  237. package/server/utils/migrate.js +81 -81
  238. package/server/utils/safe-fetch.js +228 -0
  239. package/server/utils/secureFields.js +50 -50
  240. package/server/ws.js +161 -161
  241. package/templates/artisan-marketplace.yaml +104 -104
  242. package/templates/book-price-scout.yaml +98 -98
  243. package/templates/electronics-price-tracker.yaml +108 -108
  244. package/templates/flight-deal-hunter.yaml +113 -113
  245. package/templates/freelancer-direct.yaml +116 -116
  246. package/templates/grocery-price-compare.yaml +93 -93
  247. package/templates/hotel-direct-booking.yaml +113 -113
  248. package/templates/local-services.yaml +98 -98
  249. package/templates/olive-oil-tunisia.yaml +88 -88
  250. package/templates/organic-farm-fresh.yaml +101 -101
  251. package/templates/restaurant-direct.yaml +97 -97
  252. package/public/score.html +0 -263
  253. package/server/migrations/006_growth_suite.sql +0 -138
  254. package/server/routes/growth.js +0 -962
  255. package/server/services/fairness-engine.js +0 -409
  256. package/server/services/fairness.js +0 -420
@@ -1,261 +1,549 @@
1
- /**
2
- * Admin API Routes
3
- * Full admin panel backend: users, sites, analytics, Stripe, SMTP, grants
4
- */
5
-
6
- const express = require('express');
7
- const router = express.Router();
8
- const { authenticateAdmin, generateAdminToken } = require('../middleware/adminAuth');
9
- const { adminLoginLimiter } = require('../middleware/rateLimits');
10
- const { auditLog, revokeJWT } = require('../services/security');
11
- const {
12
- loginAdmin, findAdminById, createAdmin,
13
- getAllUsers, getAllSites, getAdminStats, getPlatformAnalytics,
14
- getUserFullDetails, adminUpdateUserTier, adminUpdateSite, adminDeleteUser,
15
- grantFreeTier, revokeGrant, getActiveGrants,
16
- getSmtpSettings, updateSmtpSettings, getNotificationLogs,
17
- getPayments, getPlatformSetting, setPlatformSetting,
18
- findUserByEmail,
19
- findSiteById,
20
- getAnalyticsBySite,
21
- getAnalyticsTimeline
22
- } = require('../models/db');
23
- const { sendEmail } = require('../services/email');
24
- const { createCheckoutSession, createPortalSession, isStripeConfigured, getStripePrices } = require('../services/stripe');
25
-
26
- // ─── Auth ──────────────────────────────────────────────────────────────
27
-
28
- router.post('/login', adminLoginLimiter, (req, res) => {
29
- const { email, password } = req.body;
30
- if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
31
-
32
- const admin = loginAdmin({ email, password });
33
- if (!admin) {
34
- auditLog({ actorType: 'admin', action: 'admin_login_failed', details: { email }, ip: req.ip, outcome: 'denied', severity: 'warning' });
35
- return res.status(401).json({ error: 'Invalid credentials' });
36
- }
37
-
38
- const token = generateAdminToken(admin);
39
- auditLog({ actorType: 'admin', actorId: String(admin.id), action: 'admin_login', ip: req.ip });
40
- res.json({ admin, token });
41
- });
42
-
43
- router.post('/logout', authenticateAdmin, (req, res) => {
44
- if (req._rawToken) {
45
- revokeJWT(req._rawToken, 'admin_logout');
46
- auditLog({ actorType: 'admin', actorId: String(req.admin.id), action: 'admin_logout', ip: req.ip });
47
- }
48
- res.json({ success: true });
49
- });
50
-
51
- router.get('/me', authenticateAdmin, (req, res) => {
52
- const admin = findAdminById(req.admin.id);
53
- if (!admin) return res.status(404).json({ error: 'Admin not found' });
54
- res.json({ admin });
55
- });
56
-
57
- // ─── Dashboard Stats ──────────────────────────────────────────────────
58
-
59
- router.get('/stats', authenticateAdmin, (req, res) => {
60
- const stats = getAdminStats();
61
- stats.stripeConfigured = isStripeConfigured();
62
- res.json(stats);
63
- });
64
-
65
- router.get('/analytics', authenticateAdmin, (req, res) => {
66
- const days = parseInt(req.query.days) || 30;
67
- const data = getPlatformAnalytics(days);
68
- res.json(data);
69
- });
70
-
71
- // ─── Users Management ─────────────────────────────────────────────────
72
-
73
- router.get('/users', authenticateAdmin, (req, res) => {
74
- const users = getAllUsers();
75
- res.json({ users });
76
- });
77
-
78
- router.get('/users/:id', authenticateAdmin, (req, res) => {
79
- const user = getUserFullDetails(req.params.id);
80
- if (!user) return res.status(404).json({ error: 'User not found' });
81
- res.json({ user });
82
- });
83
-
84
- router.put('/users/:id/tier', authenticateAdmin, (req, res) => {
85
- const { tier, siteId } = req.body;
86
- if (!['free', 'starter', 'pro', 'enterprise'].includes(tier)) {
87
- return res.status(400).json({ error: 'Invalid tier' });
88
- }
89
- adminUpdateUserTier(req.params.id, siteId, tier);
90
- res.json({ success: true });
91
- });
92
-
93
- router.delete('/users/:id', authenticateAdmin, (req, res) => {
94
- adminDeleteUser(req.params.id);
95
- res.json({ success: true });
96
- });
97
-
98
- // ─── Sites Management ─────────────────────────────────────────────────
99
-
100
- router.get('/sites', authenticateAdmin, (req, res) => {
101
- const sites = getAllSites();
102
- res.json({ sites });
103
- });
104
-
105
- router.put('/sites/:id', authenticateAdmin, (req, res) => {
106
- const { tier, active } = req.body;
107
- const ok = adminUpdateSite(req.params.id, { tier, active });
108
- if (!ok) return res.status(404).json({ error: 'Site not found or invalid tier' });
109
- res.json({ success: true });
110
- });
111
-
112
- router.get('/sites/:id/analytics', authenticateAdmin, (req, res) => {
113
- const site = findSiteById.get(req.params.id);
114
- if (!site) return res.status(404).json({ error: 'Site not found' });
115
- const days = parseInt(req.query.days, 10) || 30;
116
- const since = new Date(Date.now() - days * 86400000).toISOString();
117
- const summary = getAnalyticsBySite.all(site.id, since);
118
- const timeline = getAnalyticsTimeline.all(site.id, since);
119
- res.json({
120
- site: {
121
- id: site.id,
122
- name: site.name,
123
- domain: site.domain,
124
- tier: site.tier,
125
- license_key: site.license_key
126
- },
127
- summary,
128
- timeline,
129
- period: `${days} days`
130
- });
131
- });
132
-
133
- // ─── Free Grants ──────────────────────────────────────────────────────
134
-
135
- router.get('/grants', authenticateAdmin, (req, res) => {
136
- const grants = getActiveGrants();
137
- res.json({ grants });
138
- });
139
-
140
- router.post('/grants', authenticateAdmin, (req, res) => {
141
- const { userId, siteId, tier, reason, expiresAt } = req.body;
142
- if (!userId || !tier) return res.status(400).json({ error: 'userId and tier required' });
143
- if (!['starter', 'pro', 'enterprise'].includes(tier)) return res.status(400).json({ error: 'Invalid tier' });
144
-
145
- const grant = grantFreeTier({ userId, siteId, tier, reason, grantedBy: req.admin.id, expiresAt });
146
-
147
- // Send notification email
148
- const user = getUserFullDetails(userId);
149
- if (user) {
150
- sendEmail({
151
- to: user.email,
152
- template: 'tier_upgrade',
153
- data: { name: user.name, tier, reason: reason || 'Complimentary upgrade' },
154
- userId
155
- }).catch(() => {});
156
- }
157
-
158
- res.status(201).json({ grant });
159
- });
160
-
161
- router.delete('/grants/:id', authenticateAdmin, (req, res) => {
162
- const ok = revokeGrant(req.params.id);
163
- if (!ok) return res.status(404).json({ error: 'Grant not found' });
164
- res.json({ success: true });
165
- });
166
-
167
- // ─── Stripe Settings ─────────────────────────────────────────────────
168
-
169
- router.get('/stripe/config', authenticateAdmin, (req, res) => {
170
- const secretKey = getPlatformSetting('stripe_secret_key');
171
- const publishableKey = getPlatformSetting('stripe_publishable_key');
172
- const webhookSecret = getPlatformSetting('stripe_webhook_secret');
173
- const prices = getStripePrices();
174
-
175
- res.json({
176
- configured: isStripeConfigured(),
177
- hasSecretKey: !!secretKey,
178
- publishableKey: publishableKey || '',
179
- webhookSecret: webhookSecret ? '••••' + webhookSecret.slice(-4) : '',
180
- prices
181
- });
182
- });
183
-
184
- router.put('/stripe/config', authenticateAdmin, (req, res) => {
185
- const { secretKey, publishableKey, webhookSecret, priceStarter, pricePro, priceEnterprise } = req.body;
186
-
187
- if (secretKey) setPlatformSetting('stripe_secret_key', secretKey);
188
- if (publishableKey) setPlatformSetting('stripe_publishable_key', publishableKey);
189
- if (webhookSecret) setPlatformSetting('stripe_webhook_secret', webhookSecret);
190
- if (priceStarter) setPlatformSetting('stripe_price_starter', priceStarter);
191
- if (pricePro) setPlatformSetting('stripe_price_pro', pricePro);
192
- if (priceEnterprise) setPlatformSetting('stripe_price_enterprise', priceEnterprise);
193
-
194
- res.json({ success: true });
195
- });
196
-
197
- // ─── Payments ──────────────────────────────────────────────────────────
198
-
199
- router.get('/payments', authenticateAdmin, (req, res) => {
200
- const limit = parseInt(req.query.limit) || 50;
201
- const payments = getPayments(limit);
202
- res.json({ payments });
203
- });
204
-
205
- // ─── SMTP Settings ────────────────────────────────────────────────────
206
-
207
- router.get('/smtp', authenticateAdmin, (req, res) => {
208
- const settings = getSmtpSettings();
209
- // Mask password
210
- if (settings && settings.password) {
211
- settings.password = '••••••••';
212
- }
213
- res.json({ settings });
214
- });
215
-
216
- router.put('/smtp', authenticateAdmin, (req, res) => {
217
- const { host, port, secure, username, password, fromName, fromEmail, enabled } = req.body;
218
- if (!host || !username || !fromEmail) {
219
- return res.status(400).json({ error: 'Host, username, and fromEmail are required' });
220
- }
221
- updateSmtpSettings({ host, port, secure, username, password, fromName, fromEmail, enabled });
222
- res.json({ success: true });
223
- });
224
-
225
- router.post('/smtp/test', authenticateAdmin, (req, res) => {
226
- const { testEmail } = req.body;
227
- if (!testEmail) return res.status(400).json({ error: 'testEmail required' });
228
-
229
- sendEmail({
230
- to: testEmail,
231
- template: 'welcome',
232
- data: { name: 'Test User', dashboardUrl: 'https://webagentbridge.com/dashboard' }
233
- }).then(result => {
234
- res.json(result);
235
- }).catch(err => {
236
- res.status(500).json({ success: false, error: err.message });
237
- });
238
- });
239
-
240
- // ─── Notification Logs ────────────────────────────────────────────────
241
-
242
- router.get('/notifications', authenticateAdmin, (req, res) => {
243
- const limit = parseInt(req.query.limit) || 100;
244
- const logs = getNotificationLogs(limit);
245
- res.json({ logs });
246
- });
247
-
248
- // ─── Send Custom Notification ─────────────────────────────────────────
249
-
250
- router.post('/notifications/send', authenticateAdmin, (req, res) => {
251
- const { userId, email, template, data } = req.body;
252
- if (!email || !template) return res.status(400).json({ error: 'email and template required' });
253
-
254
- sendEmail({ to: email, template, data: data || {}, userId }).then(result => {
255
- res.json(result);
256
- }).catch(err => {
257
- res.status(500).json({ success: false, error: err.message });
258
- });
259
- });
260
-
261
- module.exports = router;
1
+ /**
2
+ * Admin API Routes
3
+ * Full admin panel backend: users, sites, analytics, Stripe, SMTP, grants
4
+ */
5
+
6
+ const express = require('express');
7
+ const router = express.Router();
8
+ const { authenticateAdmin, generateAdminToken } = require('../middleware/adminAuth');
9
+ const { adminLoginLimiter } = require('../middleware/rateLimits');
10
+ const { auditLog, revokeJWT } = require('../services/security');
11
+ const {
12
+ loginAdmin, findAdminById, createAdmin,
13
+ getAllUsers, getAllSites, getAdminStats, getPlatformAnalytics,
14
+ getUserFullDetails, adminUpdateUserTier, adminUpdateSite, adminDeleteUser,
15
+ grantFreeTier, revokeGrant, getActiveGrants,
16
+ getSmtpSettings, updateSmtpSettings, getNotificationLogs,
17
+ getPayments, getPlatformSetting, setPlatformSetting,
18
+ findUserByEmail,
19
+ findSiteById,
20
+ getAnalyticsBySite,
21
+ getAnalyticsTimeline,
22
+ db
23
+ } = require('../models/db');
24
+ const { sendEmail } = require('../services/email');
25
+ const { createCheckoutSession, createPortalSession, isStripeConfigured, getStripePrices } = require('../services/stripe');
26
+
27
+ // ─── Auth ──────────────────────────────────────────────────────────────
28
+
29
+ router.post('/login', adminLoginLimiter, (req, res) => {
30
+ const { email, password } = req.body;
31
+ if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
32
+
33
+ const admin = loginAdmin({ email, password });
34
+ if (!admin) {
35
+ auditLog({ actorType: 'admin', action: 'admin_login_failed', details: { email }, ip: req.ip, outcome: 'denied', severity: 'warning' });
36
+ return res.status(401).json({ error: 'Invalid credentials' });
37
+ }
38
+
39
+ const token = generateAdminToken(admin);
40
+ auditLog({ actorType: 'admin', actorId: String(admin.id), action: 'admin_login', ip: req.ip });
41
+ res.json({ admin, token });
42
+ });
43
+
44
+ router.post('/logout', authenticateAdmin, (req, res) => {
45
+ if (req._rawToken) {
46
+ revokeJWT(req._rawToken, 'admin_logout');
47
+ auditLog({ actorType: 'admin', actorId: String(req.admin.id), action: 'admin_logout', ip: req.ip });
48
+ }
49
+ res.json({ success: true });
50
+ });
51
+
52
+ router.get('/me', authenticateAdmin, (req, res) => {
53
+ const admin = findAdminById(req.admin.id);
54
+ if (!admin) return res.status(404).json({ error: 'Admin not found' });
55
+ res.json({ admin });
56
+ });
57
+
58
+ // ─── Dashboard Stats ──────────────────────────────────────────────────
59
+
60
+ router.get('/stats', authenticateAdmin, (req, res) => {
61
+ const stats = getAdminStats();
62
+ stats.stripeConfigured = isStripeConfigured();
63
+ res.json(stats);
64
+ });
65
+
66
+ router.get('/analytics', authenticateAdmin, (req, res) => {
67
+ const days = parseInt(req.query.days) || 30;
68
+ const data = getPlatformAnalytics(days);
69
+ res.json(data);
70
+ });
71
+
72
+ // ─── Users Management ─────────────────────────────────────────────────
73
+
74
+ router.get('/users', authenticateAdmin, (req, res) => {
75
+ const users = getAllUsers();
76
+ res.json({ users });
77
+ });
78
+
79
+ router.get('/users/:id', authenticateAdmin, (req, res) => {
80
+ const user = getUserFullDetails(req.params.id);
81
+ if (!user) return res.status(404).json({ error: 'User not found' });
82
+ res.json({ user });
83
+ });
84
+
85
+ router.put('/users/:id/tier', authenticateAdmin, (req, res) => {
86
+ const { tier, siteId } = req.body;
87
+ if (!['free', 'starter', 'pro', 'enterprise'].includes(tier)) {
88
+ return res.status(400).json({ error: 'Invalid tier' });
89
+ }
90
+ adminUpdateUserTier(req.params.id, siteId, tier);
91
+ res.json({ success: true });
92
+ });
93
+
94
+ router.delete('/users/:id', authenticateAdmin, (req, res) => {
95
+ adminDeleteUser(req.params.id);
96
+ res.json({ success: true });
97
+ });
98
+
99
+ // ─── Sites Management ─────────────────────────────────────────────────
100
+
101
+ router.get('/sites', authenticateAdmin, (req, res) => {
102
+ const sites = getAllSites();
103
+ res.json({ sites });
104
+ });
105
+
106
+ router.put('/sites/:id', authenticateAdmin, (req, res) => {
107
+ const { tier, active } = req.body;
108
+ const ok = adminUpdateSite(req.params.id, { tier, active });
109
+ if (!ok) return res.status(404).json({ error: 'Site not found or invalid tier' });
110
+ res.json({ success: true });
111
+ });
112
+
113
+ router.get('/sites/:id/analytics', authenticateAdmin, (req, res) => {
114
+ const site = findSiteById.get(req.params.id);
115
+ if (!site) return res.status(404).json({ error: 'Site not found' });
116
+ const days = parseInt(req.query.days, 10) || 30;
117
+ const since = new Date(Date.now() - days * 86400000).toISOString();
118
+ const summary = getAnalyticsBySite.all(site.id, since);
119
+ const timeline = getAnalyticsTimeline.all(site.id, since);
120
+ res.json({
121
+ site: {
122
+ id: site.id,
123
+ name: site.name,
124
+ domain: site.domain,
125
+ tier: site.tier,
126
+ license_key: site.license_key
127
+ },
128
+ summary,
129
+ timeline,
130
+ period: `${days} days`
131
+ });
132
+ });
133
+
134
+ // ─── Free Grants ──────────────────────────────────────────────────────
135
+
136
+ router.get('/grants', authenticateAdmin, (req, res) => {
137
+ const grants = getActiveGrants();
138
+ res.json({ grants });
139
+ });
140
+
141
+ router.post('/grants', authenticateAdmin, (req, res) => {
142
+ const { userId, siteId, tier, reason, expiresAt } = req.body;
143
+ if (!userId || !tier) return res.status(400).json({ error: 'userId and tier required' });
144
+ if (!['starter', 'pro', 'enterprise'].includes(tier)) return res.status(400).json({ error: 'Invalid tier' });
145
+
146
+ const grant = grantFreeTier({ userId, siteId, tier, reason, grantedBy: req.admin.id, expiresAt });
147
+
148
+ // Send notification email
149
+ const user = getUserFullDetails(userId);
150
+ if (user) {
151
+ sendEmail({
152
+ to: user.email,
153
+ template: 'tier_upgrade',
154
+ data: { name: user.name, tier, reason: reason || 'Complimentary upgrade' },
155
+ userId
156
+ }).catch(() => {});
157
+ }
158
+
159
+ res.status(201).json({ grant });
160
+ });
161
+
162
+ router.delete('/grants/:id', authenticateAdmin, (req, res) => {
163
+ const ok = revokeGrant(req.params.id);
164
+ if (!ok) return res.status(404).json({ error: 'Grant not found' });
165
+ res.json({ success: true });
166
+ });
167
+
168
+ // ─── Stripe Settings ─────────────────────────────────────────────────
169
+
170
+ router.get('/stripe/config', authenticateAdmin, (req, res) => {
171
+ const secretKey = getPlatformSetting('stripe_secret_key');
172
+ const publishableKey = getPlatformSetting('stripe_publishable_key');
173
+ const webhookSecret = getPlatformSetting('stripe_webhook_secret');
174
+ const prices = getStripePrices();
175
+
176
+ res.json({
177
+ configured: isStripeConfigured(),
178
+ hasSecretKey: !!secretKey,
179
+ publishableKey: publishableKey || '',
180
+ webhookSecret: webhookSecret ? '••••' + webhookSecret.slice(-4) : '',
181
+ prices
182
+ });
183
+ });
184
+
185
+ router.put('/stripe/config', authenticateAdmin, (req, res) => {
186
+ const { secretKey, publishableKey, webhookSecret, priceStarter, pricePro, priceEnterprise } = req.body;
187
+
188
+ if (secretKey) setPlatformSetting('stripe_secret_key', secretKey);
189
+ if (publishableKey) setPlatformSetting('stripe_publishable_key', publishableKey);
190
+ if (webhookSecret) setPlatformSetting('stripe_webhook_secret', webhookSecret);
191
+ if (priceStarter) setPlatformSetting('stripe_price_starter', priceStarter);
192
+ if (pricePro) setPlatformSetting('stripe_price_pro', pricePro);
193
+ if (priceEnterprise) setPlatformSetting('stripe_price_enterprise', priceEnterprise);
194
+
195
+ res.json({ success: true });
196
+ });
197
+
198
+ // ─── Payments ──────────────────────────────────────────────────────────
199
+
200
+ router.get('/payments', authenticateAdmin, (req, res) => {
201
+ const limit = parseInt(req.query.limit) || 50;
202
+ const payments = getPayments(limit);
203
+ res.json({ payments });
204
+ });
205
+
206
+ // ─── SMTP Settings ────────────────────────────────────────────────────
207
+
208
+ router.get('/smtp', authenticateAdmin, (req, res) => {
209
+ const settings = getSmtpSettings();
210
+ // Mask password
211
+ if (settings && settings.password) {
212
+ settings.password = '••••••••';
213
+ }
214
+ res.json({ settings });
215
+ });
216
+
217
+ router.put('/smtp', authenticateAdmin, (req, res) => {
218
+ const { host, port, secure, username, password, fromName, fromEmail, enabled } = req.body;
219
+ if (!host || !username || !fromEmail) {
220
+ return res.status(400).json({ error: 'Host, username, and fromEmail are required' });
221
+ }
222
+ updateSmtpSettings({ host, port, secure, username, password, fromName, fromEmail, enabled });
223
+ res.json({ success: true });
224
+ });
225
+
226
+ router.post('/smtp/test', authenticateAdmin, (req, res) => {
227
+ const { testEmail } = req.body;
228
+ if (!testEmail) return res.status(400).json({ error: 'testEmail required' });
229
+
230
+ sendEmail({
231
+ to: testEmail,
232
+ template: 'welcome',
233
+ data: { name: 'Test User', dashboardUrl: 'https://webagentbridge.com/dashboard' }
234
+ }).then(result => {
235
+ res.json(result);
236
+ }).catch(err => {
237
+ res.status(500).json({ success: false, error: err.message });
238
+ });
239
+ });
240
+
241
+ // ─── Notification Logs ────────────────────────────────────────────────
242
+
243
+ router.get('/notifications', authenticateAdmin, (req, res) => {
244
+ const limit = parseInt(req.query.limit) || 100;
245
+ const logs = getNotificationLogs(limit);
246
+ res.json({ logs });
247
+ });
248
+
249
+ // ─── Send Custom Notification ─────────────────────────────────────────
250
+
251
+ router.post('/notifications/send', authenticateAdmin, (req, res) => {
252
+ const { userId, email, template, data } = req.body;
253
+ if (!email || !template) return res.status(400).json({ error: 'email and template required' });
254
+
255
+ sendEmail({ to: email, template, data: data || {}, userId }).then(result => {
256
+ res.json(result);
257
+ }).catch(err => {
258
+ res.status(500).json({ success: false, error: err.message });
259
+ });
260
+ });
261
+
262
+ // ─── DNS Discovery Oversight ──────────────────────────────────────────
263
+ // Real-data views into discovery_usage_runs / discovery_trust_runs.
264
+ // Tables are auto-created by routes/discovery.js.
265
+
266
+ function tableExists(name) {
267
+ try {
268
+ return !!db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`).get(name);
269
+ } catch { return false; }
270
+ }
271
+
272
+ router.get('/discovery/stats', authenticateAdmin, (_req, res) => {
273
+ if (!tableExists('discovery_usage_runs')) {
274
+ return res.json({ enabled: false, totals: {}, last7d: [], topDomains: [] });
275
+ }
276
+ const totals = db.prepare(`
277
+ SELECT
278
+ COUNT(*) AS runs,
279
+ COUNT(DISTINCT domain) AS domains,
280
+ SUM(CASE WHEN execution_succeeded = 1 THEN 1 ELSE 0 END) AS successes,
281
+ AVG(value_score) AS avg_score
282
+ FROM discovery_usage_runs
283
+ WHERE created_at >= datetime('now', '-30 days')
284
+ `).get();
285
+ const last7d = db.prepare(`
286
+ SELECT date(created_at) AS day, COUNT(*) AS runs,
287
+ SUM(CASE WHEN execution_succeeded = 1 THEN 1 ELSE 0 END) AS successes
288
+ FROM discovery_usage_runs
289
+ WHERE created_at >= datetime('now', '-7 days')
290
+ GROUP BY day ORDER BY day ASC
291
+ `).all();
292
+ const topDomains = db.prepare(`
293
+ SELECT domain, COUNT(*) AS runs, AVG(value_score) AS score
294
+ FROM discovery_usage_runs
295
+ WHERE created_at >= datetime('now', '-30 days')
296
+ GROUP BY domain ORDER BY runs DESC LIMIT 25
297
+ `).all();
298
+ res.json({ enabled: true, totals, last7d, topDomains });
299
+ });
300
+
301
+ router.get('/discovery/runs', authenticateAdmin, (req, res) => {
302
+ if (!tableExists('discovery_usage_runs')) return res.json({ runs: [] });
303
+ const limit = Math.min(parseInt(req.query.limit, 10) || 100, 500);
304
+ const runs = db.prepare(`
305
+ SELECT id, domain, mode, preferred_use_case, selected_action,
306
+ readiness_ok, execution_attempted, execution_succeeded,
307
+ value_score, end_to_end_ms, created_at
308
+ FROM discovery_usage_runs
309
+ ORDER BY created_at DESC LIMIT ?
310
+ `).all(limit);
311
+ res.json({ runs });
312
+ });
313
+
314
+ router.get('/trust/stats', authenticateAdmin, (_req, res) => {
315
+ if (!tableExists('discovery_trust_runs')) {
316
+ return res.json({ enabled: false, totals: {}, leaderboard: [] });
317
+ }
318
+ const totals = db.prepare(`
319
+ SELECT COUNT(*) AS runs, COUNT(DISTINCT domain) AS domains,
320
+ AVG(score) AS avg_score,
321
+ SUM(sig_valid) AS valid_sigs,
322
+ SUM(signed_manifest) AS signed_manifests,
323
+ SUM(has_pk) AS domains_with_pk
324
+ FROM discovery_trust_runs
325
+ WHERE created_at >= datetime('now', '-30 days')
326
+ `).get();
327
+ const leaderboard = db.prepare(`
328
+ SELECT domain, MAX(score) AS score, MAX(created_at) AS last_run
329
+ FROM discovery_trust_runs
330
+ GROUP BY domain ORDER BY score DESC, last_run DESC LIMIT 25
331
+ `).all();
332
+ res.json({ enabled: true, totals, leaderboard });
333
+ });
334
+
335
+ router.get('/trust/runs', authenticateAdmin, (req, res) => {
336
+ if (!tableExists('discovery_trust_runs')) return res.json({ runs: [] });
337
+ const limit = Math.min(parseInt(req.query.limit, 10) || 100, 500);
338
+ const runs = db.prepare(`
339
+ SELECT id, domain, score, dnssec, has_pk, signed_manifest, sig_valid, https_ok, created_at
340
+ FROM discovery_trust_runs
341
+ ORDER BY created_at DESC LIMIT ?
342
+ `).all(limit);
343
+ res.json({ runs });
344
+ });
345
+
346
+ // ─── Providers Oversight (cross-user) ─────────────────────────────────
347
+
348
+ router.get('/providers/stats', authenticateAdmin, (_req, res) => {
349
+ if (!tableExists('provider_accounts')) {
350
+ return res.json({ enabled: false, totals: {}, byProvider: [], recentActions: [] });
351
+ }
352
+ const totals = db.prepare(`
353
+ SELECT COUNT(*) AS accounts,
354
+ SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) AS active,
355
+ SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errored,
356
+ SUM(domains_count) AS domains_under_management
357
+ FROM provider_accounts
358
+ `).get();
359
+ const byProvider = db.prepare(`
360
+ SELECT provider_type, COUNT(*) AS accounts,
361
+ SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) AS active,
362
+ COALESCE(SUM(domains_count), 0) AS domains
363
+ FROM provider_accounts
364
+ GROUP BY provider_type ORDER BY accounts DESC
365
+ `).all();
366
+ const recentActions = tableExists('provider_action_log')
367
+ ? db.prepare(`
368
+ SELECT l.id, l.account_id, a.provider_type, l.domain, l.action, l.status,
369
+ l.duration_ms, l.detail, l.created_at
370
+ FROM provider_action_log l
371
+ LEFT JOIN provider_accounts a ON a.id = l.account_id
372
+ ORDER BY l.created_at DESC LIMIT 50
373
+ `).all()
374
+ : [];
375
+ const wabEnabled = tableExists('provider_domains')
376
+ ? db.prepare(`SELECT COUNT(*) AS c FROM provider_domains WHERE wab_enabled = 1`).get().c
377
+ : 0;
378
+ res.json({ enabled: true, totals: { ...totals, wab_enabled_domains: wabEnabled }, byProvider, recentActions });
379
+ });
380
+
381
+ router.get('/providers/accounts', authenticateAdmin, (req, res) => {
382
+ if (!tableExists('provider_accounts')) return res.json({ accounts: [] });
383
+ const limit = Math.min(parseInt(req.query.limit, 10) || 200, 1000);
384
+ const rows = db.prepare(`
385
+ SELECT a.id, a.user_id, u.email AS user_email, a.provider_type, a.label,
386
+ a.status, a.last_test_at, a.last_test_ok, a.last_test_error,
387
+ a.last_sync_at, a.domains_count, a.created_at, a.updated_at
388
+ FROM provider_accounts a
389
+ LEFT JOIN users u ON u.id = a.user_id
390
+ ORDER BY a.created_at DESC LIMIT ?
391
+ `).all(limit);
392
+ res.json({ accounts: rows });
393
+ });
394
+
395
+ router.get('/providers/domains', authenticateAdmin, (req, res) => {
396
+ if (!tableExists('provider_domains')) return res.json({ domains: [] });
397
+ const limit = Math.min(parseInt(req.query.limit, 10) || 200, 1000);
398
+ const rows = db.prepare(`
399
+ SELECT d.id, d.account_id, a.provider_type, a.user_id, u.email AS user_email,
400
+ d.domain, d.zone_id, d.wab_enabled, d.wab_record_value,
401
+ d.last_action, d.last_action_at, d.last_action_status, d.last_action_error
402
+ FROM provider_domains d
403
+ LEFT JOIN provider_accounts a ON a.id = d.account_id
404
+ LEFT JOIN users u ON u.id = a.user_id
405
+ ORDER BY d.updated_at DESC LIMIT ?
406
+ `).all(limit);
407
+ res.json({ domains: rows });
408
+ });
409
+
410
+ // ─── API Modules: real counts (replaces hardcoded dashboard placeholders) ──
411
+ router.get('/modules/stats', authenticateAdmin, (_req, res) => {
412
+ // Source of truth: server/routes/* + service registry. We treat each
413
+ // top-level route module as one "API module" and return real numbers.
414
+ const modules = [
415
+ { id: 'auth', label: 'Authentication', openness: 'open' },
416
+ { id: 'wab', label: 'WAB Core API', openness: 'open' },
417
+ { id: 'discovery', label: 'DNS Discovery', openness: 'open' },
418
+ { id: 'sovereign', label: 'Sovereign Mode', openness: 'open' },
419
+ { id: 'commander', label: 'Commander SDK', openness: 'partial' },
420
+ { id: 'mesh', label: 'Agent Mesh', openness: 'partial' },
421
+ { id: 'workspace', label: 'Agent Workspace', openness: 'partial' },
422
+ { id: 'universal', label: 'Universal Agent', openness: 'partial' },
423
+ { id: 'gateway', label: 'Gateway (v1)', openness: 'closed' },
424
+ { id: 'premium', label: 'Premium APIs', openness: 'closed' },
425
+ { id: 'admin', label: 'Admin APIs', openness: 'closed' },
426
+ { id: 'providers', label: 'DNS Providers', openness: 'open' },
427
+ ];
428
+ const counts = modules.reduce((acc, m) => { acc[m.openness] = (acc[m.openness] || 0) + 1; return acc; }, {});
429
+ res.json({
430
+ total: modules.length,
431
+ open: counts.open || 0,
432
+ partial: counts.partial || 0,
433
+ closed: counts.closed || 0,
434
+ modules,
435
+ });
436
+ });
437
+
438
+ // ─── Governance Layer (Phase 20) — admin oversight of all agents ──
439
+ // These endpoints expose the same data as /api/governance/* but authenticate
440
+ // via admin token instead of an agent's own token, so an admin can see and
441
+ // act across every agent on the platform.
442
+ const gov = (() => { try { return require('../services/governance'); } catch { return null; } });
443
+ function ensureGovTables() {
444
+ return tableExists('gov_agents');
445
+ }
446
+
447
+ router.get('/governance/stats', authenticateAdmin, (_req, res) => {
448
+ if (!ensureGovTables()) return res.json({ enabled: false, totals: {} });
449
+ const totals = db.prepare(`
450
+ SELECT
451
+ COUNT(*) AS agents,
452
+ SUM(CASE WHEN status='alive' THEN 1 ELSE 0 END) AS alive,
453
+ SUM(CASE WHEN status='killed' THEN 1 ELSE 0 END) AS killed,
454
+ SUM(CASE WHEN status='suspended' THEN 1 ELSE 0 END) AS suspended
455
+ FROM gov_agents
456
+ `).get() || {};
457
+ const policies = db.prepare(`SELECT COUNT(*) AS c FROM gov_policies`).get().c;
458
+ const audit = db.prepare(`SELECT COUNT(*) AS c FROM gov_audit`).get().c;
459
+ const pending = db.prepare(`SELECT COUNT(*) AS c FROM gov_approvals WHERE status='pending'`).get().c;
460
+ const recentDenies = db.prepare(`
461
+ SELECT id, agent_id, ts, resource, action, decision, reason
462
+ FROM gov_audit WHERE decision='deny' ORDER BY id DESC LIMIT 25
463
+ `).all();
464
+ res.json({ enabled: true, totals: { ...totals, policies, audit_events: audit, pending_approvals: pending }, recentDenies });
465
+ });
466
+
467
+ router.get('/governance/agents', authenticateAdmin, (_req, res) => {
468
+ if (!ensureGovTables()) return res.json({ agents: [] });
469
+ const rows = db.prepare(`
470
+ SELECT a.agent_id, a.owner_id, u.email AS owner_email, a.display_name,
471
+ a.status, a.killed_at, a.killed_reason, a.created_at, a.updated_at,
472
+ (SELECT COUNT(*) FROM gov_policies p WHERE p.agent_id = a.agent_id) AS policies,
473
+ (SELECT COUNT(*) FROM gov_audit g WHERE g.agent_id = a.agent_id) AS audit_events,
474
+ (SELECT COUNT(*) FROM gov_approvals ap WHERE ap.agent_id = a.agent_id AND ap.status='pending') AS pending_approvals
475
+ FROM gov_agents a
476
+ LEFT JOIN users u ON u.id = a.owner_id
477
+ ORDER BY a.created_at DESC
478
+ `).all();
479
+ res.json({ agents: rows });
480
+ });
481
+
482
+ router.get('/governance/agents/:id', authenticateAdmin, (req, res) => {
483
+ if (!ensureGovTables()) return res.status(404).json({ error: 'governance_disabled' });
484
+ const a = db.prepare(`SELECT agent_id, owner_id, display_name, status, killed_at, killed_reason, metadata, created_at, updated_at FROM gov_agents WHERE agent_id = ?`).get(req.params.id);
485
+ if (!a) return res.status(404).json({ error: 'agent_not_found' });
486
+ const policies = db.prepare(`SELECT * FROM gov_policies WHERE agent_id = ? ORDER BY id DESC`).all(req.params.id);
487
+ const approvals = db.prepare(`SELECT * FROM gov_approvals WHERE agent_id = ? AND status='pending' ORDER BY created_at DESC LIMIT 50`).all(req.params.id);
488
+ const audit = db.prepare(`SELECT id, ts, event_type, resource, action, scope, amount, currency, decision, reason FROM gov_audit WHERE agent_id = ? ORDER BY id DESC LIMIT 200`).all(req.params.id);
489
+ res.json({ agent: a, policies, approvals, audit });
490
+ });
491
+
492
+ router.get('/governance/agents/:id/audit/verify', authenticateAdmin, (req, res) => {
493
+ const g = gov(); if (!g) return res.status(503).json({ error: 'governance_unavailable' });
494
+ res.json(g.verifyAuditChain(req.params.id));
495
+ });
496
+
497
+ router.post('/governance/agents/:id/kill', authenticateAdmin, (req, res) => {
498
+ const g = gov(); if (!g) return res.status(503).json({ error: 'governance_unavailable' });
499
+ const reason = (req.body && req.body.reason) || ('admin:' + (req.admin.email || req.admin.id));
500
+ const ok = g.killAgent(req.params.id, reason);
501
+ auditLog({ actorType: 'admin', actorId: String(req.admin.id), action: 'governance_kill', details: { agent_id: req.params.id, reason } });
502
+ res.json({ ok, status: 'killed', reason });
503
+ });
504
+
505
+ router.post('/governance/agents/:id/revive', authenticateAdmin, (req, res) => {
506
+ const g = gov(); if (!g) return res.status(503).json({ error: 'governance_unavailable' });
507
+ const reason = (req.body && req.body.reason) || ('admin:' + (req.admin.email || req.admin.id));
508
+ const ok = g.reviveAgent(req.params.id, reason);
509
+ auditLog({ actorType: 'admin', actorId: String(req.admin.id), action: 'governance_revive', details: { agent_id: req.params.id, reason } });
510
+ res.json({ ok, status: 'alive', reason });
511
+ });
512
+
513
+ router.post('/governance/agents/:id/policies', authenticateAdmin, (req, res) => {
514
+ const g = gov(); if (!g) return res.status(503).json({ error: 'governance_unavailable' });
515
+ const b = req.body || {};
516
+ if (!b.resource || !b.action) return res.status(400).json({ error: 'missing_fields', need: ['resource', 'action'] });
517
+ const r = g.definePolicy({
518
+ agentId: req.params.id, resource: String(b.resource), action: String(b.action),
519
+ scope: b.scope || null, maxAmount: b.max_amount, currency: b.currency,
520
+ dailyCap: b.daily_cap, perCallRate: b.per_call_rate,
521
+ requiresApproval: !!b.requires_approval,
522
+ effect: b.effect === 'deny' ? 'deny' : 'allow',
523
+ expiresAt: b.expires_at || null,
524
+ });
525
+ auditLog({ actorType: 'admin', actorId: String(req.admin.id), action: 'governance_policy_create', details: { agent_id: req.params.id, policy_id: r.id } });
526
+ res.status(201).json({ ok: true, id: r.id });
527
+ });
528
+
529
+ router.delete('/governance/agents/:id/policies/:pid', authenticateAdmin, (req, res) => {
530
+ const g = gov(); if (!g) return res.status(503).json({ error: 'governance_unavailable' });
531
+ const ok = g.deletePolicy(req.params.id, Number(req.params.pid));
532
+ auditLog({ actorType: 'admin', actorId: String(req.admin.id), action: 'governance_policy_delete', details: { agent_id: req.params.id, policy_id: req.params.pid } });
533
+ res.json({ ok });
534
+ });
535
+
536
+ router.post('/governance/approvals/:rid/decide', authenticateAdmin, (req, res) => {
537
+ const g = gov(); if (!g) return res.status(503).json({ error: 'governance_unavailable' });
538
+ const decision = req.body?.decision === 'approved' ? 'approved' : 'rejected';
539
+ const out = g.decideApproval(req.params.rid, {
540
+ decision,
541
+ decidedBy: 'admin:' + req.admin.id,
542
+ note: req.body?.note || null,
543
+ });
544
+ if (!out.ok) return res.status(409).json(out);
545
+ auditLog({ actorType: 'admin', actorId: String(req.admin.id), action: 'governance_approval_decide', details: { request_id: req.params.rid, decision } });
546
+ res.json(out);
547
+ });
548
+
549
+ module.exports = router;