palaryn 0.1.0 → 0.3.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 (344) hide show
  1. package/README.md +243 -588
  2. package/dist/sdk/typescript/src/client.js +2 -2
  3. package/dist/sdk/typescript/src/client.js.map +1 -1
  4. package/dist/src/anomaly/detector.d.ts +7 -4
  5. package/dist/src/anomaly/detector.d.ts.map +1 -1
  6. package/dist/src/anomaly/detector.js +22 -12
  7. package/dist/src/anomaly/detector.js.map +1 -1
  8. package/dist/src/audit/logger.d.ts +10 -0
  9. package/dist/src/audit/logger.d.ts.map +1 -1
  10. package/dist/src/audit/logger.js +52 -38
  11. package/dist/src/audit/logger.js.map +1 -1
  12. package/dist/src/auth/routes.d.ts.map +1 -1
  13. package/dist/src/auth/routes.js +35 -0
  14. package/dist/src/auth/routes.js.map +1 -1
  15. package/dist/src/budget/manager.d.ts +5 -0
  16. package/dist/src/budget/manager.d.ts.map +1 -1
  17. package/dist/src/budget/manager.js +32 -0
  18. package/dist/src/budget/manager.js.map +1 -1
  19. package/dist/src/budget/model-pricing.d.ts +20 -0
  20. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  21. package/dist/src/budget/model-pricing.js +107 -0
  22. package/dist/src/budget/model-pricing.js.map +1 -0
  23. package/dist/src/budget/usage-extractor.d.ts +3 -1
  24. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  25. package/dist/src/budget/usage-extractor.js +47 -3
  26. package/dist/src/budget/usage-extractor.js.map +1 -1
  27. package/dist/src/config/defaults.d.ts.map +1 -1
  28. package/dist/src/config/defaults.js +65 -13
  29. package/dist/src/config/defaults.js.map +1 -1
  30. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  31. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  32. package/dist/src/dlp/tool-patterns.js +34 -0
  33. package/dist/src/dlp/tool-patterns.js.map +1 -0
  34. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  35. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  36. package/dist/src/executor/filesystem-executor.js +192 -0
  37. package/dist/src/executor/filesystem-executor.js.map +1 -0
  38. package/dist/src/executor/http-executor.d.ts.map +1 -1
  39. package/dist/src/executor/http-executor.js +22 -2
  40. package/dist/src/executor/http-executor.js.map +1 -1
  41. package/dist/src/executor/index.d.ts +4 -0
  42. package/dist/src/executor/index.d.ts.map +1 -1
  43. package/dist/src/executor/index.js +9 -1
  44. package/dist/src/executor/index.js.map +1 -1
  45. package/dist/src/executor/shell-executor.d.ts +22 -0
  46. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  47. package/dist/src/executor/shell-executor.js +119 -0
  48. package/dist/src/executor/shell-executor.js.map +1 -0
  49. package/dist/src/executor/sql-executor.d.ts +29 -0
  50. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  51. package/dist/src/executor/sql-executor.js +114 -0
  52. package/dist/src/executor/sql-executor.js.map +1 -0
  53. package/dist/src/executor/websocket-executor.d.ts +26 -0
  54. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  55. package/dist/src/executor/websocket-executor.js +205 -0
  56. package/dist/src/executor/websocket-executor.js.map +1 -0
  57. package/dist/src/interceptor/index.d.ts +2 -0
  58. package/dist/src/interceptor/index.d.ts.map +1 -0
  59. package/dist/src/interceptor/index.js +6 -0
  60. package/dist/src/interceptor/index.js.map +1 -0
  61. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  62. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  63. package/dist/src/interceptor/provider-interceptor.js +302 -0
  64. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  65. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  66. package/dist/src/mcp/auth-verifier.js +3 -2
  67. package/dist/src/mcp/auth-verifier.js.map +1 -1
  68. package/dist/src/mcp/bridge.d.ts +14 -10
  69. package/dist/src/mcp/bridge.d.ts.map +1 -1
  70. package/dist/src/mcp/bridge.js +51 -227
  71. package/dist/src/mcp/bridge.js.map +1 -1
  72. package/dist/src/mcp/http-transport.d.ts +2 -0
  73. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  74. package/dist/src/mcp/http-transport.js +117 -66
  75. package/dist/src/mcp/http-transport.js.map +1 -1
  76. package/dist/src/mcp/internal-auth.d.ts +13 -0
  77. package/dist/src/mcp/internal-auth.d.ts.map +1 -0
  78. package/dist/src/mcp/internal-auth.js +12 -0
  79. package/dist/src/mcp/internal-auth.js.map +1 -0
  80. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  81. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  82. package/dist/src/mcp/tool-definitions.js +491 -0
  83. package/dist/src/mcp/tool-definitions.js.map +1 -0
  84. package/dist/src/middleware/auth.js.map +1 -1
  85. package/dist/src/middleware/session.js.map +1 -1
  86. package/dist/src/middleware/validate.d.ts +8 -0
  87. package/dist/src/middleware/validate.d.ts.map +1 -1
  88. package/dist/src/middleware/validate.js +45 -0
  89. package/dist/src/middleware/validate.js.map +1 -1
  90. package/dist/src/policy/engine.d.ts +4 -0
  91. package/dist/src/policy/engine.d.ts.map +1 -1
  92. package/dist/src/policy/engine.js +117 -0
  93. package/dist/src/policy/engine.js.map +1 -1
  94. package/dist/src/saas/routes.d.ts.map +1 -1
  95. package/dist/src/saas/routes.js +355 -22
  96. package/dist/src/saas/routes.js.map +1 -1
  97. package/dist/src/server/app.d.ts.map +1 -1
  98. package/dist/src/server/app.js +24 -3
  99. package/dist/src/server/app.js.map +1 -1
  100. package/dist/src/server/gateway.d.ts.map +1 -1
  101. package/dist/src/server/gateway.js +17 -0
  102. package/dist/src/server/gateway.js.map +1 -1
  103. package/dist/src/server/index.d.ts.map +1 -1
  104. package/dist/src/server/index.js +18 -0
  105. package/dist/src/server/index.js.map +1 -1
  106. package/dist/src/storage/interfaces.d.ts +14 -3
  107. package/dist/src/storage/interfaces.d.ts.map +1 -1
  108. package/dist/src/storage/memory.d.ts +2 -0
  109. package/dist/src/storage/memory.d.ts.map +1 -1
  110. package/dist/src/storage/memory.js +6 -0
  111. package/dist/src/storage/memory.js.map +1 -1
  112. package/dist/src/storage/postgres.d.ts +5 -0
  113. package/dist/src/storage/postgres.d.ts.map +1 -1
  114. package/dist/src/storage/postgres.js +16 -0
  115. package/dist/src/storage/postgres.js.map +1 -1
  116. package/dist/src/storage/redis.d.ts +10 -0
  117. package/dist/src/storage/redis.d.ts.map +1 -1
  118. package/dist/src/storage/redis.js +65 -0
  119. package/dist/src/storage/redis.js.map +1 -1
  120. package/dist/src/types/budget.d.ts +4 -0
  121. package/dist/src/types/budget.d.ts.map +1 -1
  122. package/dist/src/types/config.d.ts +58 -0
  123. package/dist/src/types/config.d.ts.map +1 -1
  124. package/dist/src/types/events.d.ts +1 -0
  125. package/dist/src/types/events.d.ts.map +1 -1
  126. package/dist/src/types/policy.d.ts +11 -1
  127. package/dist/src/types/policy.d.ts.map +1 -1
  128. package/dist/src/types/tool-result.d.ts +11 -0
  129. package/dist/src/types/tool-result.d.ts.map +1 -1
  130. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  131. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  132. package/dist/tests/unit/app-routes.test.js +715 -0
  133. package/dist/tests/unit/app-routes.test.js.map +1 -0
  134. package/dist/tests/unit/audit-logger.test.js +105 -0
  135. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  136. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  137. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  138. package/dist/tests/unit/auth-providers.test.js +279 -0
  139. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  140. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  141. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  142. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  143. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  144. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  145. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  146. package/dist/tests/unit/auth-verifier.test.js +505 -0
  147. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  148. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  149. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  150. package/dist/tests/unit/billing-routes.test.js +432 -0
  151. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  152. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  153. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  154. package/dist/tests/unit/config-defaults.test.js +119 -0
  155. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  156. package/dist/tests/unit/defaults.test.js +0 -10
  157. package/dist/tests/unit/defaults.test.js.map +1 -1
  158. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  159. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  160. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  161. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  162. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  163. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  164. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  165. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  166. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  167. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  168. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  169. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  170. package/dist/tests/unit/logger.test.d.ts +2 -0
  171. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  172. package/dist/tests/unit/logger.test.js +97 -0
  173. package/dist/tests/unit/logger.test.js.map +1 -0
  174. package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
  175. package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
  176. package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
  177. package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
  178. package/dist/tests/unit/metrics.test.js +102 -0
  179. package/dist/tests/unit/metrics.test.js.map +1 -1
  180. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  181. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  182. package/dist/tests/unit/model-pricing.test.js +87 -0
  183. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  184. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  185. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  186. package/dist/tests/unit/oauth-stores.test.js +260 -0
  187. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  188. package/dist/tests/unit/policy-engine.test.js +466 -0
  189. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  190. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  191. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  192. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  193. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  194. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  195. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  196. package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
  197. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  198. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  199. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  200. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  201. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  202. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  203. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  204. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  205. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  206. package/dist/tests/unit/saas-routes.test.js +3 -3
  207. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  208. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  209. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  210. package/dist/tests/unit/shell-executor.test.js +145 -0
  211. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  212. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  213. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  214. package/dist/tests/unit/sql-executor.test.js +177 -0
  215. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  216. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  217. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  218. package/dist/tests/unit/stream-proxy.test.js +147 -0
  219. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  220. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  221. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  222. package/dist/tests/unit/tool-definitions.test.js +184 -0
  223. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  224. package/dist/tests/unit/usage-extractor.test.js +140 -0
  225. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  226. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  227. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  228. package/dist/tests/unit/webhook-handler.test.js +453 -0
  229. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  230. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  231. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  232. package/dist/tests/unit/webhook-routes.test.js +69 -0
  233. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  234. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  235. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  236. package/dist/tests/unit/websocket-executor.test.js +121 -0
  237. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  238. package/package.json +8 -2
  239. package/policy-packs/demo_fail.yaml +41 -0
  240. package/policy-packs/full_tools.yaml +136 -0
  241. package/src/admin/index.ts +1 -0
  242. package/src/admin/routes.ts +509 -0
  243. package/src/admin/templates.ts +572 -0
  244. package/src/anomaly/detector.ts +730 -0
  245. package/src/anomaly/index.ts +1 -0
  246. package/src/approval/manager.ts +569 -0
  247. package/src/approval/webhook.ts +133 -0
  248. package/src/audit/logger.ts +490 -0
  249. package/src/auth/index.ts +5 -0
  250. package/src/auth/password.ts +21 -0
  251. package/src/auth/pkce.ts +22 -0
  252. package/src/auth/providers.ts +208 -0
  253. package/src/auth/routes.ts +561 -0
  254. package/src/auth/session.ts +84 -0
  255. package/src/billing/index.ts +6 -0
  256. package/src/billing/plan-enforcer.ts +135 -0
  257. package/src/billing/routes.ts +229 -0
  258. package/src/billing/stripe-client.ts +58 -0
  259. package/src/billing/webhook-handler.ts +182 -0
  260. package/src/billing/webhook-routes.ts +28 -0
  261. package/src/budget/manager.ts +679 -0
  262. package/src/budget/model-pricing.ts +119 -0
  263. package/src/budget/usage-extractor.ts +214 -0
  264. package/src/cli.ts +91 -0
  265. package/src/config/defaults.ts +261 -0
  266. package/src/config/validate.ts +88 -0
  267. package/src/dlp/composite-scanner.ts +213 -0
  268. package/src/dlp/index.ts +9 -0
  269. package/src/dlp/interfaces.ts +34 -0
  270. package/src/dlp/patterns.ts +30 -0
  271. package/src/dlp/prompt-injection-backend.ts +181 -0
  272. package/src/dlp/prompt-injection-patterns.ts +302 -0
  273. package/src/dlp/regex-backend.ts +181 -0
  274. package/src/dlp/scanner.ts +502 -0
  275. package/src/dlp/text-normalizer.ts +225 -0
  276. package/src/dlp/tool-patterns.ts +35 -0
  277. package/src/dlp/trufflehog-backend.ts +190 -0
  278. package/src/executor/filesystem-executor.ts +196 -0
  279. package/src/executor/http-executor.ts +349 -0
  280. package/src/executor/index.ts +9 -0
  281. package/src/executor/interfaces.ts +11 -0
  282. package/src/executor/noop-executor.ts +23 -0
  283. package/src/executor/registry.ts +64 -0
  284. package/src/executor/shell-executor.ts +148 -0
  285. package/src/executor/slack-executor.ts +176 -0
  286. package/src/executor/sql-executor.ts +146 -0
  287. package/src/executor/websocket-executor.ts +211 -0
  288. package/src/index.ts +24 -0
  289. package/src/interceptor/index.ts +1 -0
  290. package/src/interceptor/provider-interceptor.ts +315 -0
  291. package/src/mcp/auth-verifier.ts +152 -0
  292. package/src/mcp/bridge.ts +703 -0
  293. package/src/mcp/http-transport.ts +698 -0
  294. package/src/mcp/index.ts +9 -0
  295. package/src/mcp/internal-auth.ts +14 -0
  296. package/src/mcp/oauth-pages.ts +139 -0
  297. package/src/mcp/oauth-postgres-stores.ts +278 -0
  298. package/src/mcp/oauth-provider.ts +536 -0
  299. package/src/mcp/oauth-stores.ts +202 -0
  300. package/src/mcp/server.ts +55 -0
  301. package/src/mcp/tool-definitions.ts +562 -0
  302. package/src/metrics/collector.ts +357 -0
  303. package/src/metrics/index.ts +1 -0
  304. package/src/middleware/auth.ts +814 -0
  305. package/src/middleware/session.ts +85 -0
  306. package/src/middleware/validate.ts +130 -0
  307. package/src/policy/engine.ts +815 -0
  308. package/src/policy/index.ts +2 -0
  309. package/src/policy/opa-engine.ts +829 -0
  310. package/src/proxy/forward-proxy.ts +649 -0
  311. package/src/proxy/index.ts +1 -0
  312. package/src/ratelimit/limiter.ts +196 -0
  313. package/src/replay/engine.ts +142 -0
  314. package/src/replay/index.ts +1 -0
  315. package/src/saas/index.ts +1 -0
  316. package/src/saas/routes.ts +2178 -0
  317. package/src/server/app.ts +985 -0
  318. package/src/server/errors.ts +49 -0
  319. package/src/server/gateway.ts +1130 -0
  320. package/src/server/index.ts +307 -0
  321. package/src/server/logger.ts +255 -0
  322. package/src/server/stream-proxy.ts +202 -0
  323. package/src/storage/file-persistence.ts +315 -0
  324. package/src/storage/index.ts +4 -0
  325. package/src/storage/interfaces.ts +287 -0
  326. package/src/storage/memory.ts +686 -0
  327. package/src/storage/postgres.ts +1831 -0
  328. package/src/storage/redis.ts +835 -0
  329. package/src/tracing/index.ts +1 -0
  330. package/src/tracing/provider.ts +100 -0
  331. package/src/trust/calculator.ts +141 -0
  332. package/src/trust/index.ts +7 -0
  333. package/src/types/budget.ts +36 -0
  334. package/src/types/config.ts +278 -0
  335. package/src/types/events.ts +41 -0
  336. package/src/types/express.d.ts +14 -0
  337. package/src/types/index.ts +7 -0
  338. package/src/types/policy.ts +83 -0
  339. package/src/types/stripe-config.ts +11 -0
  340. package/src/types/subscription.ts +59 -0
  341. package/src/types/tool-call.ts +47 -0
  342. package/src/types/tool-result.ts +82 -0
  343. package/src/types/user.ts +125 -0
  344. package/tsconfig.json +24 -0
@@ -0,0 +1,572 @@
1
+ export interface DashboardStats {
2
+ total_requests: number;
3
+ pending_approvals: number;
4
+ active_workspaces: number;
5
+ policy_rules: number;
6
+ recent_events: any[];
7
+ }
8
+
9
+ export interface BudgetInfo {
10
+ key: string;
11
+ type: 'task' | 'user_daily' | 'user_monthly' | 'workspace_daily' | 'workspace_monthly';
12
+ label: string;
13
+ spent: number;
14
+ limit: number | undefined;
15
+ }
16
+
17
+ export interface ApiKeyInfo {
18
+ key: string;
19
+ workspace_id: string;
20
+ description?: string;
21
+ roles?: string[];
22
+ created_at?: string;
23
+ last_used_at?: string;
24
+ revoked?: boolean;
25
+ expires_at?: string;
26
+ }
27
+
28
+ export function escapeHtml(str: string): string {
29
+ return String(str)
30
+ .replace(/&/g, '&')
31
+ .replace(/</g, '&lt;')
32
+ .replace(/>/g, '&gt;')
33
+ .replace(/"/g, '&quot;')
34
+ .replace(/'/g, '&#039;');
35
+ }
36
+
37
+ export function layoutTemplate(title: string, activeNav: string, content: string): string {
38
+ return `<!DOCTYPE html>
39
+ <html>
40
+ <head>
41
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'">
42
+ <title>Palaryn Admin - ${escapeHtml(title)}</title>
43
+ <style>
44
+ * { margin: 0; padding: 0; box-sizing: border-box; }
45
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; min-height: 100vh; background: #f5f7fa; }
46
+ .sidebar { width: 240px; background: #1a1d23; color: #fff; padding: 20px 0; }
47
+ .sidebar h1 { font-size: 18px; padding: 0 20px 20px; border-bottom: 1px solid #2d3139; }
48
+ .sidebar nav a { display: block; padding: 12px 20px; color: #9ca3af; text-decoration: none; }
49
+ .sidebar nav a:hover, .sidebar nav a.active { background: #2d3139; color: #fff; }
50
+ .main { flex: 1; padding: 30px; }
51
+ .card { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
52
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
53
+ .stat-card { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
54
+ .stat-card .value { font-size: 28px; font-weight: 700; color: #1a1d23; }
55
+ .stat-card .label { font-size: 13px; color: #6b7280; margin-top: 4px; }
56
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; }
57
+ .badge-green { background: #d1fae5; color: #065f46; }
58
+ .badge-red { background: #fee2e2; color: #991b1b; }
59
+ .badge-yellow { background: #fef3c7; color: #92400e; }
60
+ .badge-blue { background: #dbeafe; color: #1e40af; }
61
+ table { width: 100%; border-collapse: collapse; }
62
+ th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #e5e7eb; }
63
+ th { font-size: 12px; text-transform: uppercase; color: #6b7280; font-weight: 600; }
64
+ .btn { display: inline-block; padding: 6px 14px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; font-weight: 500; }
65
+ .btn-primary { background: #3b82f6; color: #fff; }
66
+ .btn-danger { background: #ef4444; color: #fff; }
67
+ .btn-success { background: #10b981; color: #fff; }
68
+ input, textarea, select { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; width: 100%; }
69
+ textarea { min-height: 200px; font-family: monospace; }
70
+ .form-group { margin-bottom: 16px; }
71
+ .form-group label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: #374151; }
72
+ pre { background: #1a1d23; color: #e5e7eb; padding: 16px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
73
+ .alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; }
74
+ .alert-success { background: #d1fae5; color: #065f46; }
75
+ .alert-error { background: #fee2e2; color: #991b1b; }
76
+ </style>
77
+ </head>
78
+ <body>
79
+ <div class="sidebar">
80
+ <h1>Palaryn Admin</h1>
81
+ <nav>
82
+ <a href="/admin" class="${activeNav === 'dashboard' ? 'active' : ''}">Dashboard</a>
83
+ <a href="/admin/approvals" class="${activeNav === 'approvals' ? 'active' : ''}">Approvals</a>
84
+ <a href="/admin/policies" class="${activeNav === 'policies' ? 'active' : ''}">Policies</a>
85
+ <a href="/admin/budgets" class="${activeNav === 'budgets' ? 'active' : ''}">Budgets</a>
86
+ <a href="/admin/traces" class="${activeNav === 'traces' ? 'active' : ''}">Traces</a>
87
+ <a href="/admin/api-keys" class="${activeNav === 'api-keys' ? 'active' : ''}">API Keys</a>
88
+ </nav>
89
+ </div>
90
+ <div class="main">
91
+ <h2 style="margin-bottom: 24px; color: #1a1d23;">${escapeHtml(title)}</h2>
92
+ ${content}
93
+ </div>
94
+ </body>
95
+ </html>`;
96
+ }
97
+
98
+ export function dashboardTemplate(stats: DashboardStats): string {
99
+ const recentEventsRows = stats.recent_events.slice(0, 20).map(event => {
100
+ const eventType = escapeHtml(event.event_type || '');
101
+ const badgeClass = eventType.includes('DENIED') || eventType.includes('INCIDENT')
102
+ ? 'badge-red'
103
+ : eventType.includes('APPROVAL_REQUESTED') || eventType.includes('APPROVAL_EXPIRED')
104
+ ? 'badge-yellow'
105
+ : eventType.includes('APPROVED') || eventType.includes('EXECUTED') || eventType.includes('RETURNED')
106
+ ? 'badge-green'
107
+ : 'badge-blue';
108
+
109
+ return `<tr>
110
+ <td><span class="badge ${badgeClass}">${eventType}</span></td>
111
+ <td>${escapeHtml(event.tool_name || '-')}</td>
112
+ <td>${escapeHtml(event.actor_id || '-')}</td>
113
+ <td>${escapeHtml(event.timestamp || '-')}</td>
114
+ </tr>`;
115
+ }).join('\n');
116
+
117
+ const content = `
118
+ <div class="stats-grid">
119
+ <div class="stat-card">
120
+ <div class="value">${stats.total_requests}</div>
121
+ <div class="label">Total Requests</div>
122
+ </div>
123
+ <div class="stat-card">
124
+ <div class="value">${stats.pending_approvals}</div>
125
+ <div class="label">Pending Approvals</div>
126
+ </div>
127
+ <div class="stat-card">
128
+ <div class="value">${stats.active_workspaces}</div>
129
+ <div class="label">Active Workspaces</div>
130
+ </div>
131
+ <div class="stat-card">
132
+ <div class="value">${stats.policy_rules}</div>
133
+ <div class="label">Policy Rules</div>
134
+ </div>
135
+ </div>
136
+ <div class="card">
137
+ <h3 style="margin-bottom: 16px; color: #374151;">Recent Events</h3>
138
+ ${stats.recent_events.length === 0
139
+ ? '<p style="color: #6b7280;">No events recorded yet.</p>'
140
+ : `<table>
141
+ <thead>
142
+ <tr>
143
+ <th>Event</th>
144
+ <th>Tool</th>
145
+ <th>Actor</th>
146
+ <th>Time</th>
147
+ </tr>
148
+ </thead>
149
+ <tbody>
150
+ ${recentEventsRows}
151
+ </tbody>
152
+ </table>`
153
+ }
154
+ </div>`;
155
+
156
+ return layoutTemplate('Dashboard', 'dashboard', content);
157
+ }
158
+
159
+ export function approvalsTemplate(approvals: any[], csrfToken?: string): string {
160
+ const csrfField = csrfToken ? `<input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}">` : '';
161
+
162
+ const rows = approvals.map(a => {
163
+ const statusBadge = a.status === 'pending'
164
+ ? '<span class="badge badge-yellow">pending</span>'
165
+ : a.status === 'approved'
166
+ ? '<span class="badge badge-green">approved</span>'
167
+ : a.status === 'denied'
168
+ ? '<span class="badge badge-red">denied</span>'
169
+ : '<span class="badge badge-blue">' + escapeHtml(a.status) + '</span>';
170
+
171
+ const actions = a.status === 'pending'
172
+ ? `<form method="POST" action="/admin/approvals/${escapeHtml(a.approval_id)}/approve" style="display:inline;">
173
+ ${csrfField}
174
+ <button type="submit" class="btn btn-success">Approve</button>
175
+ </form>
176
+ <form method="POST" action="/admin/approvals/${escapeHtml(a.approval_id)}/deny" style="display:inline; margin-left:4px;">
177
+ ${csrfField}
178
+ <button type="submit" class="btn btn-danger">Deny</button>
179
+ </form>`
180
+ : '-';
181
+
182
+ return `<tr>
183
+ <td>${escapeHtml(a.approval_id)}</td>
184
+ <td>${escapeHtml(a.tool_name || '-')}</td>
185
+ <td>${escapeHtml(a.actor_id || '-')}</td>
186
+ <td>${escapeHtml(a.scope || '-')}</td>
187
+ <td>${statusBadge}</td>
188
+ <td>${escapeHtml(a.reason || '-')}</td>
189
+ <td>${escapeHtml(a.created_at || '-')}</td>
190
+ <td>${actions}</td>
191
+ </tr>`;
192
+ }).join('\n');
193
+
194
+ const content = `
195
+ <div class="card">
196
+ ${approvals.length === 0
197
+ ? '<p style="color: #6b7280;">No pending approvals.</p>'
198
+ : `<table>
199
+ <thead>
200
+ <tr>
201
+ <th>ID</th>
202
+ <th>Tool</th>
203
+ <th>Actor</th>
204
+ <th>Scope</th>
205
+ <th>Status</th>
206
+ <th>Reason</th>
207
+ <th>Created</th>
208
+ <th>Actions</th>
209
+ </tr>
210
+ </thead>
211
+ <tbody>
212
+ ${rows}
213
+ </tbody>
214
+ </table>`
215
+ }
216
+ </div>`;
217
+
218
+ return layoutTemplate('Approvals', 'approvals', content);
219
+ }
220
+
221
+ export function policiesTemplate(policy: any, validationResult?: any, csrfToken?: string, policyYaml?: string, applyResult?: { success: boolean; message: string }): string {
222
+ const csrfField = csrfToken ? `<input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}">` : '';
223
+ const policyJson = JSON.stringify(policy, null, 2);
224
+
225
+ const validationAlert = validationResult
226
+ ? validationResult.valid
227
+ ? '<div class="alert alert-success">Policy is valid.</div>'
228
+ : `<div class="alert alert-error">Validation errors: ${escapeHtml((validationResult.errors || []).join('; '))}</div>`
229
+ : '';
230
+
231
+ const applyAlert = applyResult
232
+ ? applyResult.success
233
+ ? `<div class="alert alert-success">${escapeHtml(applyResult.message)}</div>`
234
+ : `<div class="alert alert-error">${escapeHtml(applyResult.message)}</div>`
235
+ : '';
236
+
237
+ const yamlContent = policyYaml || '';
238
+
239
+ const content = `
240
+ ${applyAlert}
241
+ ${validationAlert}
242
+ <div class="card">
243
+ <h3 style="margin-bottom: 16px; color: #374151;">Edit Policy (YAML)</h3>
244
+ <form method="POST" action="/admin/policies/apply">
245
+ ${csrfField}
246
+ <div class="form-group">
247
+ <label for="policy_editor">Policy Pack YAML</label>
248
+ <div style="position: relative;">
249
+ <pre id="policy_highlight" aria-hidden="true" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: 0; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 13px; line-height: 1.5; tab-size: 2; overflow: auto; pointer-events: none; white-space: pre-wrap; word-wrap: break-word; color: transparent; background: transparent;"></pre>
250
+ <textarea id="policy_editor" name="policy_yaml" style="position: relative; min-height: 400px; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 13px; line-height: 1.5; tab-size: 2; background: transparent; caret-color: #1a1d23; z-index: 1;">${escapeHtml(yamlContent)}</textarea>
251
+ </div>
252
+ <script>
253
+ (function() {
254
+ var ta = document.getElementById('policy_editor');
255
+ var pre = document.getElementById('policy_highlight');
256
+ if (!ta || !pre) return;
257
+ function highlightYaml(text) {
258
+ var escaped = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
259
+ return escaped
260
+ .replace(/^(\\s*#.*)$/gm, '<span style="color:#6b7280;font-style:italic;">$1</span>')
261
+ .replace(/^(\\s*[\\w][\\w.-]*)(\\s*:)/gm, '<span style="color:#2563eb;">$1</span>$2')
262
+ .replace(/:\\s*("(?:[^"\\\\]|\\\\.)*")/g, ': <span style="color:#059669;">$1</span>')
263
+ .replace(/:\\s*('(?:[^'\\\\]|\\\\.)*')/g, ': <span style="color:#059669;">$1</span>')
264
+ .replace(/:\\s*(\\d+\\.?\\d*)/g, ': <span style="color:#d97706;">$1</span>')
265
+ .replace(/:\\s*(true|false|null|yes|no)/gi, ': <span style="color:#dc2626;">$1</span>')
266
+ .replace(/(ALLOW|DENY|TRANSFORM|REQUIRE_APPROVAL)/g, '<span style="color:#7c3aed;font-weight:600;">$1</span>');
267
+ }
268
+ function sync() { pre.innerHTML = highlightYaml(ta.value) + '\\n'; }
269
+ ta.addEventListener('input', sync);
270
+ ta.addEventListener('scroll', function() { pre.scrollTop = ta.scrollTop; pre.scrollLeft = ta.scrollLeft; });
271
+ sync();
272
+ })();
273
+ </script>
274
+ </div>
275
+ <div style="display: flex; gap: 12px; align-items: center;">
276
+ <button type="submit" class="btn btn-success" style="font-size: 15px; padding: 10px 24px;">Save &amp; Apply</button>
277
+ </div>
278
+ </form>
279
+ <form method="POST" action="/admin/policies/reload" style="margin-top: 12px;">
280
+ ${csrfField}
281
+ <button type="submit" class="btn btn-primary">Reload from Disk</button>
282
+ </form>
283
+ </div>
284
+ <div class="card">
285
+ <h3 style="margin-bottom: 16px; color: #374151;">Active Policy Pack (read-only)</h3>
286
+ <pre>${escapeHtml(policyJson)}</pre>
287
+ </div>
288
+ <div class="card">
289
+ <h3 style="margin-bottom: 16px; color: #374151;">Validate Policy</h3>
290
+ <form method="POST" action="/admin/policies/validate">
291
+ ${csrfField}
292
+ <div class="form-group">
293
+ <label for="policy_validate_json">Policy Pack (JSON)</label>
294
+ <textarea id="policy_validate_json" name="policy_json" placeholder='{"name": "test", "version": "1.0", "rules": []}'></textarea>
295
+ </div>
296
+ <button type="submit" class="btn btn-primary">Validate</button>
297
+ </form>
298
+ </div>`;
299
+
300
+ return layoutTemplate('Policies', 'policies', content);
301
+ }
302
+
303
+ export interface CostSummary {
304
+ estimated_total: number;
305
+ actual_total: number;
306
+ top_tools: { tool_name: string; cost: number }[];
307
+ }
308
+
309
+ export function budgetsTemplate(budgets: BudgetInfo[], costSummary?: CostSummary): string {
310
+ const rows = budgets.map(b => {
311
+ const limitStr = b.limit !== undefined ? `$${b.limit.toFixed(2)}` : 'unlimited';
312
+ const pct = b.limit !== undefined && b.limit > 0 ? Math.min(100, (b.spent / b.limit) * 100) : 0;
313
+ const barColor = pct > 80 ? '#ef4444' : pct > 50 ? '#f59e0b' : '#10b981';
314
+
315
+ return `<tr>
316
+ <td>${escapeHtml(b.key)}</td>
317
+ <td>${escapeHtml(b.label)}</td>
318
+ <td>$${b.spent.toFixed(4)}</td>
319
+ <td>${limitStr}</td>
320
+ <td>
321
+ <div style="background:#e5e7eb; border-radius:4px; height:8px; width:120px; display:inline-block; vertical-align:middle;">
322
+ <div style="background:${barColor}; border-radius:4px; height:8px; width:${pct}%;"></div>
323
+ </div>
324
+ <span style="font-size:12px; color:#6b7280; margin-left:6px;">${pct.toFixed(0)}%</span>
325
+ </td>
326
+ </tr>`;
327
+ }).join('\n');
328
+
329
+ const costSummaryHtml = costSummary ? `
330
+ <div class="stats-grid" style="margin-bottom: 24px;">
331
+ <div class="stat-card">
332
+ <div class="value">$${costSummary.estimated_total.toFixed(4)}</div>
333
+ <div class="label">Estimated Cost (Total)</div>
334
+ </div>
335
+ <div class="stat-card">
336
+ <div class="value">$${costSummary.actual_total.toFixed(4)}</div>
337
+ <div class="label">Actual Cost (Total)</div>
338
+ </div>
339
+ </div>
340
+ ${costSummary.top_tools.length > 0 ? `<div class="card" style="margin-bottom: 20px;">
341
+ <h3 style="margin-bottom: 16px; color: #374151;">Top Tools by Cost</h3>
342
+ <table>
343
+ <thead><tr><th>Tool</th><th>Cost (USD)</th></tr></thead>
344
+ <tbody>
345
+ ${costSummary.top_tools.map(t =>
346
+ `<tr><td>${escapeHtml(t.tool_name)}</td><td>$${t.cost.toFixed(4)}</td></tr>`
347
+ ).join('\n')}
348
+ </tbody>
349
+ </table>
350
+ </div>` : ''}` : '';
351
+
352
+ const content = `
353
+ ${costSummaryHtml}
354
+ <div class="card">
355
+ ${budgets.length === 0
356
+ ? '<p style="color: #6b7280;">No budget data available.</p>'
357
+ : `<table>
358
+ <thead>
359
+ <tr>
360
+ <th>Key</th>
361
+ <th>Type</th>
362
+ <th>Spent</th>
363
+ <th>Limit</th>
364
+ <th>Usage</th>
365
+ </tr>
366
+ </thead>
367
+ <tbody>
368
+ ${rows}
369
+ </tbody>
370
+ </table>`
371
+ }
372
+ </div>`;
373
+
374
+ return layoutTemplate('Budgets', 'budgets', content);
375
+ }
376
+
377
+ export interface PaginationInfo {
378
+ page: number;
379
+ limit: number;
380
+ total: number;
381
+ totalPages: number;
382
+ }
383
+
384
+ function paginationControls(info: PaginationInfo, baseUrl: string): string {
385
+ if (info.totalPages <= 1) return '';
386
+ const prevDisabled = info.page <= 1;
387
+ const nextDisabled = info.page >= info.totalPages;
388
+ return `<div style="display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #e5e7eb;">
389
+ ${prevDisabled
390
+ ? '<span class="btn" style="opacity: 0.5; cursor: default;">&larr; Previous</span>'
391
+ : `<a href="${baseUrl}?page=${info.page - 1}&limit=${info.limit}" class="btn btn-primary">&larr; Previous</a>`}
392
+ <span style="font-size: 14px; color: #374151;">Page ${info.page} of ${info.totalPages} (${info.total} total)</span>
393
+ ${nextDisabled
394
+ ? '<span class="btn" style="opacity: 0.5; cursor: default;">Next &rarr;</span>'
395
+ : `<a href="${baseUrl}?page=${info.page + 1}&limit=${info.limit}" class="btn btn-primary">Next &rarr;</a>`}
396
+ </div>`;
397
+ }
398
+
399
+ export function traceListTemplate(traces: any[], pagination?: PaginationInfo): string {
400
+ const content = `
401
+ <div class="card">
402
+ <h3 style="margin-bottom: 16px; color: #374151;">Search Traces</h3>
403
+ <form method="GET" action="/admin/traces" style="display: flex; gap: 8px; align-items: flex-end;">
404
+ <div class="form-group" style="flex: 1; margin-bottom: 0;">
405
+ <label for="task_id">Task ID</label>
406
+ <input type="text" id="task_id" name="task_id" placeholder="Enter a task_id to search..." />
407
+ </div>
408
+ <button type="submit" class="btn btn-primary" style="height: 38px;">Search</button>
409
+ </form>
410
+ </div>
411
+ <div class="card">
412
+ <h3 style="margin-bottom: 16px; color: #374151;">Recent Task IDs</h3>
413
+ ${traces.length === 0
414
+ ? '<p style="color: #6b7280;">No trace data available.</p>'
415
+ : `<table>
416
+ <thead>
417
+ <tr>
418
+ <th>Task ID</th>
419
+ <th>Events</th>
420
+ <th>First Seen</th>
421
+ <th>Actions</th>
422
+ </tr>
423
+ </thead>
424
+ <tbody>
425
+ ${traces.map(t => `<tr>
426
+ <td>${escapeHtml(t.task_id)}</td>
427
+ <td>${t.event_count}</td>
428
+ <td>${escapeHtml(t.first_seen || '-')}</td>
429
+ <td><a href="/admin/traces/${escapeHtml(t.task_id)}" class="btn btn-primary">View</a></td>
430
+ </tr>`).join('\n')}
431
+ </tbody>
432
+ </table>
433
+ ${pagination ? paginationControls(pagination, '/admin/traces') : ''}`
434
+ }
435
+ </div>`;
436
+
437
+ return layoutTemplate('Traces', 'traces', content);
438
+ }
439
+
440
+ export function traceDetailTemplate(taskId: string, events: any[]): string {
441
+ const rows = events.map(e => {
442
+ const eventType = escapeHtml(e.event_type || '');
443
+ const badgeClass = eventType.includes('DENIED') || eventType.includes('INCIDENT')
444
+ ? 'badge-red'
445
+ : eventType.includes('APPROVAL_REQUESTED') || eventType.includes('APPROVAL_EXPIRED')
446
+ ? 'badge-yellow'
447
+ : eventType.includes('APPROVED') || eventType.includes('EXECUTED') || eventType.includes('RETURNED')
448
+ ? 'badge-green'
449
+ : 'badge-blue';
450
+
451
+ return `<tr>
452
+ <td>${escapeHtml(e.event_id || '-')}</td>
453
+ <td><span class="badge ${badgeClass}">${eventType}</span></td>
454
+ <td>${escapeHtml(e.tool_name || '-')}</td>
455
+ <td>${escapeHtml(e.actor_id || '-')}</td>
456
+ <td>${escapeHtml(e.timestamp || '-')}</td>
457
+ <td><pre style="margin:0; padding:4px 8px; font-size:11px; max-width:400px; overflow-x:auto; white-space:pre-wrap; word-break:break-word;">${escapeHtml(JSON.stringify(e.metadata || {}, null, 2))}</pre></td>
458
+ </tr>`;
459
+ }).join('\n');
460
+
461
+ const content = `
462
+ <div style="margin-bottom: 16px;">
463
+ <a href="/admin/traces" style="color: #3b82f6; text-decoration: none;">&larr; Back to Traces</a>
464
+ </div>
465
+ <div class="card">
466
+ <h3 style="margin-bottom: 16px; color: #374151;">Task: ${escapeHtml(taskId)}</h3>
467
+ ${events.length === 0
468
+ ? '<p style="color: #6b7280;">No events found for this task.</p>'
469
+ : `<table>
470
+ <thead>
471
+ <tr>
472
+ <th>Event ID</th>
473
+ <th>Type</th>
474
+ <th>Tool</th>
475
+ <th>Actor</th>
476
+ <th>Time</th>
477
+ <th>Metadata</th>
478
+ </tr>
479
+ </thead>
480
+ <tbody>
481
+ ${rows}
482
+ </tbody>
483
+ </table>`
484
+ }
485
+ </div>`;
486
+
487
+ return layoutTemplate(`Trace: ${taskId}`, 'traces', content);
488
+ }
489
+
490
+ export function apiKeysTemplate(keys: ApiKeyInfo[], csrfToken?: string, createdKey?: string): string {
491
+ const csrfField = csrfToken ? `<input type="hidden" name="_csrf" value="${escapeHtml(csrfToken)}">` : '';
492
+
493
+ const createdAlert = createdKey
494
+ ? `<div class="alert alert-success">
495
+ New API key created: <code style="background:#065f46; color:#d1fae5; padding:2px 8px; border-radius:4px; font-size:14px; user-select:all;">${escapeHtml(createdKey)}</code>
496
+ <br><strong>Copy this key now &mdash; it won&rsquo;t be shown again.</strong>
497
+ </div>`
498
+ : '';
499
+
500
+ const rows = keys.map(k => {
501
+ const maskedKey = k.key.length > 8
502
+ ? k.key.substring(0, 4) + '...' + k.key.substring(k.key.length - 4)
503
+ : k.key;
504
+
505
+ const statusBadge = k.revoked
506
+ ? '<span class="badge badge-red">revoked</span>'
507
+ : k.expires_at && new Date(k.expires_at) < new Date()
508
+ ? '<span class="badge badge-yellow">expired</span>'
509
+ : '<span class="badge badge-green">active</span>';
510
+
511
+ const revokeAction = !k.revoked
512
+ ? `<form method="POST" action="/admin/api-keys/${escapeHtml(k.key)}/revoke" style="display:inline;">
513
+ ${csrfField}
514
+ <button type="submit" class="btn btn-danger">Revoke</button>
515
+ </form>`
516
+ : '-';
517
+
518
+ return `<tr>
519
+ <td>${escapeHtml(maskedKey)}</td>
520
+ <td>${escapeHtml(k.workspace_id)}</td>
521
+ <td>${escapeHtml(k.description || '-')}</td>
522
+ <td>${(k.roles || []).map(r => `<span class="badge badge-blue">${escapeHtml(r)}</span>`).join(' ') || '-'}</td>
523
+ <td>${statusBadge}</td>
524
+ <td>${escapeHtml(k.created_at || '-')}</td>
525
+ <td>${revokeAction}</td>
526
+ </tr>`;
527
+ }).join('\n');
528
+
529
+ const content = `
530
+ ${createdAlert}
531
+ <div class="card">
532
+ <h3 style="margin-bottom: 16px; color: #374151;">Create New API Key</h3>
533
+ <form method="POST" action="/admin/api-keys">
534
+ ${csrfField}
535
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
536
+ <div class="form-group">
537
+ <label for="workspace_id">Workspace ID</label>
538
+ <input type="text" id="workspace_id" name="workspace_id" placeholder="ws_example" required />
539
+ </div>
540
+ <div class="form-group">
541
+ <label for="description">Description</label>
542
+ <input type="text" id="description" name="description" placeholder="Key description" />
543
+ </div>
544
+ </div>
545
+ <button type="submit" class="btn btn-primary">Create Key</button>
546
+ </form>
547
+ </div>
548
+ <div class="card">
549
+ <h3 style="margin-bottom: 16px; color: #374151;">API Keys</h3>
550
+ ${keys.length === 0
551
+ ? '<p style="color: #6b7280;">No API keys configured.</p>'
552
+ : `<table>
553
+ <thead>
554
+ <tr>
555
+ <th>Key</th>
556
+ <th>Workspace</th>
557
+ <th>Description</th>
558
+ <th>Roles</th>
559
+ <th>Status</th>
560
+ <th>Created</th>
561
+ <th>Actions</th>
562
+ </tr>
563
+ </thead>
564
+ <tbody>
565
+ ${rows}
566
+ </tbody>
567
+ </table>`
568
+ }
569
+ </div>`;
570
+
571
+ return layoutTemplate('API Keys', 'api-keys', content);
572
+ }