cli-claw-kit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/config/default-groups.json +1 -0
  4. package/config/global-agents-md.template.md +37 -0
  5. package/config/mount-allowlist.json +11 -0
  6. package/container/Dockerfile +160 -0
  7. package/container/agent-runner/dist/.tsbuildinfo +1 -0
  8. package/container/agent-runner/dist/agent-definitions.js +22 -0
  9. package/container/agent-runner/dist/channel-prefixes.js +16 -0
  10. package/container/agent-runner/dist/codex-config.js +29 -0
  11. package/container/agent-runner/dist/image-detector.js +96 -0
  12. package/container/agent-runner/dist/index.js +2587 -0
  13. package/container/agent-runner/dist/mcp-tools.js +1076 -0
  14. package/container/agent-runner/dist/stream-event.types.js +5 -0
  15. package/container/agent-runner/dist/stream-processor.js +867 -0
  16. package/container/agent-runner/dist/types.js +6 -0
  17. package/container/agent-runner/dist/utils.js +115 -0
  18. package/container/agent-runner/package.json +36 -0
  19. package/container/agent-runner/prompts/security-rules.md +31 -0
  20. package/container/agent-runner/src/agent-definitions.ts +27 -0
  21. package/container/agent-runner/src/channel-prefixes.ts +16 -0
  22. package/container/agent-runner/src/codex-config.ts +40 -0
  23. package/container/agent-runner/src/image-detector.ts +116 -0
  24. package/container/agent-runner/src/index.ts +3107 -0
  25. package/container/agent-runner/src/mcp-tools.ts +1295 -0
  26. package/container/agent-runner/src/stream-event.types.ts +10 -0
  27. package/container/agent-runner/src/stream-processor.ts +932 -0
  28. package/container/agent-runner/src/types.ts +75 -0
  29. package/container/agent-runner/src/utils.ts +114 -0
  30. package/container/agent-runner/tsconfig.json +17 -0
  31. package/container/build.sh +28 -0
  32. package/container/entrypoint.sh +64 -0
  33. package/container/skills/agent-browser/SKILL.md +159 -0
  34. package/container/skills/install-skill/SKILL.md +64 -0
  35. package/container/skills/post-test-cleanup/SKILL.md +121 -0
  36. package/dist/.tsbuildinfo +1 -0
  37. package/dist/agent-output-parser.js +459 -0
  38. package/dist/app-root.js +52 -0
  39. package/dist/assistant-meta-footer.js +1 -0
  40. package/dist/auth.js +91 -0
  41. package/dist/billing.js +694 -0
  42. package/dist/channel-prefixes.js +16 -0
  43. package/dist/cli.js +86 -0
  44. package/dist/commands.js +79 -0
  45. package/dist/config.js +120 -0
  46. package/dist/container-runner.js +981 -0
  47. package/dist/daily-summary.js +210 -0
  48. package/dist/db.js +3683 -0
  49. package/dist/dingtalk.js +1347 -0
  50. package/dist/feishu-markdown-style.js +97 -0
  51. package/dist/feishu-streaming-card.js +1875 -0
  52. package/dist/feishu.js +1628 -0
  53. package/dist/file-manager.js +270 -0
  54. package/dist/group-queue.js +1070 -0
  55. package/dist/group-runtime.js +35 -0
  56. package/dist/host-workspace-cwd.js +85 -0
  57. package/dist/im-channel.js +384 -0
  58. package/dist/im-command-utils.js +142 -0
  59. package/dist/im-downloader.js +45 -0
  60. package/dist/im-manager.js +527 -0
  61. package/dist/im-utils.js +53 -0
  62. package/dist/image-detector.js +96 -0
  63. package/dist/index.js +5828 -0
  64. package/dist/logger.js +22 -0
  65. package/dist/mcp-utils.js +66 -0
  66. package/dist/message-attachments.js +69 -0
  67. package/dist/message-notifier.js +36 -0
  68. package/dist/middleware/auth.js +85 -0
  69. package/dist/mount-security.js +315 -0
  70. package/dist/permissions.js +67 -0
  71. package/dist/project-memory.js +6 -0
  72. package/dist/provider-pool.js +189 -0
  73. package/dist/qq.js +826 -0
  74. package/dist/reset-admin.js +42 -0
  75. package/dist/routes/admin.js +543 -0
  76. package/dist/routes/agent-definitions.js +241 -0
  77. package/dist/routes/agents.js +533 -0
  78. package/dist/routes/auth.js +675 -0
  79. package/dist/routes/billing.js +490 -0
  80. package/dist/routes/browse.js +210 -0
  81. package/dist/routes/bug-report.js +387 -0
  82. package/dist/routes/config.js +1868 -0
  83. package/dist/routes/files.js +671 -0
  84. package/dist/routes/groups.js +1367 -0
  85. package/dist/routes/mcp-servers.js +320 -0
  86. package/dist/routes/memory.js +523 -0
  87. package/dist/routes/monitor.js +307 -0
  88. package/dist/routes/skills.js +777 -0
  89. package/dist/routes/tasks.js +509 -0
  90. package/dist/routes/usage.js +64 -0
  91. package/dist/routes/workspace-config.js +458 -0
  92. package/dist/runtime-build.js +112 -0
  93. package/dist/runtime-command-handler.js +189 -0
  94. package/dist/runtime-command-registry.js +1 -0
  95. package/dist/runtime-config.js +1777 -0
  96. package/dist/runtime-identity.js +52 -0
  97. package/dist/schemas.js +590 -0
  98. package/dist/script-runner.js +64 -0
  99. package/dist/sdk-query.js +82 -0
  100. package/dist/skill-utils.js +145 -0
  101. package/dist/sqlite-compat.js +19 -0
  102. package/dist/stream-event.types.js +5 -0
  103. package/dist/streaming-runtime-meta.js +29 -0
  104. package/dist/task-scheduler.js +695 -0
  105. package/dist/task-utils.js +13 -0
  106. package/dist/telegram-pairing.js +59 -0
  107. package/dist/telegram.js +897 -0
  108. package/dist/terminal-manager.js +307 -0
  109. package/dist/tool-step-display.js +1 -0
  110. package/dist/types.js +1 -0
  111. package/dist/utils.js +85 -0
  112. package/dist/web-context.js +161 -0
  113. package/dist/web.js +1377 -0
  114. package/dist/wechat-crypto.js +182 -0
  115. package/dist/wechat.js +589 -0
  116. package/dist/workspace-runtime-reset.js +35 -0
  117. package/package.json +107 -0
  118. package/shared/assistant-meta-footer.ts +127 -0
  119. package/shared/channel-prefixes.ts +16 -0
  120. package/shared/dist/assistant-meta-footer.d.ts +29 -0
  121. package/shared/dist/assistant-meta-footer.js +85 -0
  122. package/shared/dist/channel-prefixes.d.ts +4 -0
  123. package/shared/dist/channel-prefixes.js +16 -0
  124. package/shared/dist/image-detector.d.ts +20 -0
  125. package/shared/dist/image-detector.js +96 -0
  126. package/shared/dist/runtime-command-registry.d.ts +38 -0
  127. package/shared/dist/runtime-command-registry.js +185 -0
  128. package/shared/dist/stream-event.d.ts +65 -0
  129. package/shared/dist/stream-event.js +8 -0
  130. package/shared/dist/tool-step-display.d.ts +4 -0
  131. package/shared/dist/tool-step-display.js +11 -0
  132. package/shared/image-detector.ts +116 -0
  133. package/shared/runtime-command-registry.ts +252 -0
  134. package/shared/stream-event.ts +67 -0
  135. package/shared/tool-step-display.ts +21 -0
  136. package/shared/tsconfig.json +24 -0
  137. package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
  138. package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
  139. package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
  140. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  141. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  142. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  143. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  144. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  145. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  146. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  147. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  148. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  149. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  150. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  151. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  152. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  153. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  154. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  155. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  156. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  157. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  158. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  159. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  160. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  161. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  162. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  163. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  164. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  165. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  166. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  167. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  168. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  169. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  170. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  171. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  172. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  173. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  174. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  175. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  176. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  177. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  178. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  179. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  180. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  181. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  182. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  183. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  184. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  185. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  186. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  187. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  188. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  189. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  190. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  191. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  192. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  193. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  194. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  195. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  196. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  197. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  198. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  199. package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
  200. package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
  201. package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
  202. package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
  203. package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
  204. package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
  205. package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
  206. package/web/dist/assets/band-CquvqAHh.js +1 -0
  207. package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
  208. package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
  209. package/web/dist/assets/channel-BOVj73LR.js +1 -0
  210. package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
  211. package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
  212. package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
  213. package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
  214. package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
  215. package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
  216. package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
  217. package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
  218. package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
  219. package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
  220. package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
  221. package/web/dist/assets/clone-BmaCesfa.js +1 -0
  222. package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
  223. package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  224. package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
  225. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  226. package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
  227. package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
  228. package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
  229. package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
  230. package/web/dist/assets/error-CGD5mp5f.js +1 -0
  231. package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
  232. package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
  233. package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
  234. package/web/dist/assets/graph-CeAEckur.js +1 -0
  235. package/web/dist/assets/index-CPnL1_qC.js +768 -0
  236. package/web/dist/assets/index-DVevCbcO.css +10 -0
  237. package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
  238. package/web/dist/assets/init-Dmth1JHB.js +1 -0
  239. package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
  240. package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
  241. package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
  242. package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
  243. package/web/dist/assets/linear-DiaJloY5.js +1 -0
  244. package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
  245. package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
  246. package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
  247. package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
  248. package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
  249. package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
  250. package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
  251. package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
  252. package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
  253. package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
  254. package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
  255. package/web/dist/assets/square-0CqMX1Q3.js +11 -0
  256. package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
  257. package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
  258. package/web/dist/assets/step-D51IIHGA.js +1 -0
  259. package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
  260. package/web/dist/assets/time-O8zIGux3.js +1 -0
  261. package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
  262. package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
  263. package/web/dist/assets/utils-KGAn0XTg.js +11 -0
  264. package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
  265. package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
  266. package/web/dist/assets/zap-_hKJYy7J.js +6 -0
  267. package/web/dist/favicon.svg +332 -0
  268. package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
  269. package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
  270. package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
  271. package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
  272. package/web/dist/fonts/DMSans-latin.woff2 +0 -0
  273. package/web/dist/icons/README.md +20 -0
  274. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  275. package/web/dist/icons/icon-128.png +0 -0
  276. package/web/dist/icons/icon-144.png +0 -0
  277. package/web/dist/icons/icon-152.png +0 -0
  278. package/web/dist/icons/icon-192.png +0 -0
  279. package/web/dist/icons/icon-192.svg +332 -0
  280. package/web/dist/icons/icon-384.png +0 -0
  281. package/web/dist/icons/icon-48.png +0 -0
  282. package/web/dist/icons/icon-512-maskable.png +0 -0
  283. package/web/dist/icons/icon-512.png +0 -0
  284. package/web/dist/icons/icon-512.svg +332 -0
  285. package/web/dist/icons/icon-72.png +0 -0
  286. package/web/dist/icons/icon-96.png +0 -0
  287. package/web/dist/icons/loading-logo.svg +332 -0
  288. package/web/dist/icons/logo-1024.png +0 -0
  289. package/web/dist/icons/logo-icon.svg +332 -0
  290. package/web/dist/icons/logo-text.svg +332 -0
  291. package/web/dist/index.html +30 -0
  292. package/web/dist/manifest.webmanifest +1 -0
  293. package/web/dist/registerSW.js +1 -0
  294. package/web/dist/sw.js +1 -0
  295. package/web/dist/workbox-08d6266a.js +1 -0
@@ -0,0 +1,42 @@
1
+ import { generateUserId, hashPassword } from './auth.js';
2
+ import { initDatabase, getUserByUsername, createUser, updateUserFields, deleteUserSessionsByUserId, } from './db.js';
3
+ async function main() {
4
+ initDatabase();
5
+ const username = (process.argv[2] || 'admin').trim();
6
+ const nextPassword = process.argv[3];
7
+ if (!nextPassword || nextPassword.trim().length < 8) {
8
+ console.error('Usage: npm run reset:admin -- <username> <new_password>');
9
+ console.error('new_password must be at least 8 characters');
10
+ process.exit(1);
11
+ }
12
+ const now = new Date().toISOString();
13
+ const passwordHash = await hashPassword(nextPassword);
14
+ const existing = getUserByUsername(username);
15
+ if (existing) {
16
+ updateUserFields(existing.id, {
17
+ role: 'admin',
18
+ status: 'active',
19
+ password_hash: passwordHash,
20
+ must_change_password: false,
21
+ disable_reason: null,
22
+ deleted_at: null,
23
+ });
24
+ deleteUserSessionsByUserId(existing.id);
25
+ console.log(`[OK] Reset admin account: ${username}`);
26
+ return;
27
+ }
28
+ createUser({
29
+ id: generateUserId(),
30
+ username,
31
+ password_hash: passwordHash,
32
+ display_name: username,
33
+ role: 'admin',
34
+ status: 'active',
35
+ must_change_password: false,
36
+ created_at: now,
37
+ updated_at: now,
38
+ notes: 'Created by reset-admin script',
39
+ });
40
+ console.log(`[OK] Created admin account: ${username}`);
41
+ }
42
+ void main();
@@ -0,0 +1,543 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { Hono } from 'hono';
4
+ import { authMiddleware, usersManageMiddleware, inviteManageMiddleware, auditViewMiddleware, } from '../middleware/auth.js';
5
+ import { AdminCreateUserSchema, AdminPatchUserSchema, InviteCreateSchema, } from '../schemas.js';
6
+ import { isUsernameConflictError, toUserPublic } from './auth.js';
7
+ import { invalidateUserSessions } from '../web-context.js';
8
+ import { listUsers, getUserById, getUserByUsername, createUser, updateUserFields, deleteUser, restoreUser, deleteUserSessionsByUserId, getActiveAdminCount, logAuthEvent, getAllInviteCodes, getInviteCode, createInviteCode as dbCreateInviteCode, deleteInviteCode, queryAuthAuditLogs, } from '../db.js';
9
+ import { validateUsername, validatePassword, generateUserId, hashPassword, generateInviteCode, } from '../auth.js';
10
+ import { normalizePermissions, ALL_PERMISSIONS, PERMISSION_TEMPLATES, resolveTemplate, hasPermission, } from '../permissions.js';
11
+ import { getClientIp } from '../utils.js';
12
+ import { DATA_DIR } from '../config.js';
13
+ import { logger } from '../logger.js';
14
+ const adminRoutes = new Hono();
15
+ // ISO 8601 日期格式验证正则(审计日志查询 from/to 参数)
16
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
17
+ // --- User Management ---
18
+ // GET /api/admin/users - 获取用户列表
19
+ adminRoutes.get('/users', authMiddleware, usersManageMiddleware, (c) => {
20
+ const query = c.req.query('q') || '';
21
+ const roleRaw = c.req.query('role') || 'all';
22
+ const statusRaw = c.req.query('status') || 'all';
23
+ const page = parseInt(c.req.query('page') || '1', 10);
24
+ const pageSize = parseInt(c.req.query('pageSize') || '50', 10);
25
+ const role = roleRaw === 'admin' || roleRaw === 'member' ? roleRaw : 'all';
26
+ const status = statusRaw === 'active' ||
27
+ statusRaw === 'disabled' ||
28
+ statusRaw === 'deleted'
29
+ ? statusRaw
30
+ : 'all';
31
+ const result = listUsers({
32
+ query,
33
+ role,
34
+ status,
35
+ page: Number.isFinite(page) ? page : 1,
36
+ pageSize: Number.isFinite(pageSize) ? pageSize : 50,
37
+ });
38
+ return c.json(result);
39
+ });
40
+ // GET /api/admin/permission-templates - 获取权限模板
41
+ adminRoutes.get('/permission-templates', authMiddleware, (c) => {
42
+ const user = c.get('user');
43
+ if (!hasPermission(user, 'manage_users') &&
44
+ !hasPermission(user, 'manage_invites')) {
45
+ return c.json({ error: 'Forbidden' }, 403);
46
+ }
47
+ return c.json({
48
+ permissions: ALL_PERMISSIONS,
49
+ templates: Object.values(PERMISSION_TEMPLATES).map((item) => ({
50
+ key: item.key,
51
+ label: item.label,
52
+ role: item.role,
53
+ permissions: item.permissions,
54
+ })),
55
+ });
56
+ });
57
+ // POST /api/admin/users - 创建用户
58
+ adminRoutes.post('/users', authMiddleware, usersManageMiddleware, async (c) => {
59
+ const body = await c.req.json().catch(() => ({}));
60
+ const validation = AdminCreateUserSchema.safeParse(body);
61
+ if (!validation.success) {
62
+ return c.json({ error: 'Invalid request', details: validation.error.format() }, 400);
63
+ }
64
+ const { username, password, display_name, role, permissions, must_change_password, notes, } = validation.data;
65
+ const actor = c.get('user');
66
+ const usernameError = validateUsername(username);
67
+ if (usernameError)
68
+ return c.json({ error: usernameError }, 400);
69
+ if (getUserByUsername(username)) {
70
+ return c.json({ error: 'Username already taken' }, 409);
71
+ }
72
+ const passwordError = validatePassword(password);
73
+ if (passwordError)
74
+ return c.json({ error: passwordError }, 400);
75
+ const now = new Date().toISOString();
76
+ const userId = generateUserId();
77
+ const passwordHash = await hashPassword(password);
78
+ const finalRole = role || 'member';
79
+ const finalPermissions = normalizePermissions(permissions ?? (finalRole === 'admin' ? ALL_PERMISSIONS : []));
80
+ if (actor.role !== 'admin') {
81
+ if (finalRole === 'admin') {
82
+ return c.json({ error: 'Forbidden: only admin can create admin users' }, 403);
83
+ }
84
+ const allowed = new Set(actor.permissions);
85
+ const forbidden = finalPermissions.filter((perm) => !allowed.has(perm));
86
+ if (forbidden.length > 0) {
87
+ return c.json({
88
+ error: `Forbidden: cannot grant permissions [${forbidden.join(', ')}]`,
89
+ }, 403);
90
+ }
91
+ }
92
+ try {
93
+ createUser({
94
+ id: userId,
95
+ username,
96
+ password_hash: passwordHash,
97
+ display_name: display_name || username,
98
+ role: finalRole,
99
+ status: 'active',
100
+ permissions: finalPermissions,
101
+ must_change_password: must_change_password ?? true,
102
+ notes: notes ?? null,
103
+ created_at: now,
104
+ updated_at: now,
105
+ });
106
+ }
107
+ catch (err) {
108
+ if (isUsernameConflictError(err)) {
109
+ return c.json({ error: 'Username already taken' }, 409);
110
+ }
111
+ throw err;
112
+ }
113
+ logAuthEvent({
114
+ event_type: 'user_created',
115
+ username,
116
+ actor_username: actor.username,
117
+ ip_address: getClientIp(c),
118
+ details: {
119
+ role: finalRole,
120
+ permissions: finalPermissions,
121
+ must_change_password: must_change_password ?? true,
122
+ },
123
+ });
124
+ const newUser = getUserById(userId);
125
+ return c.json({ success: true, user: toUserPublic(newUser) }, 201);
126
+ });
127
+ // PATCH /api/admin/users/:id - 更新用户
128
+ adminRoutes.patch('/users/:id', authMiddleware, usersManageMiddleware, async (c) => {
129
+ const id = c.req.param('id');
130
+ const body = await c.req.json().catch(() => ({}));
131
+ const validation = AdminPatchUserSchema.safeParse(body);
132
+ if (!validation.success) {
133
+ return c.json({ error: 'Invalid request', details: validation.error.format() }, 400);
134
+ }
135
+ const target = getUserById(id);
136
+ if (!target)
137
+ return c.json({ error: 'User not found' }, 404);
138
+ const actor = c.get('user');
139
+ if (actor.role !== 'admin' && target.role === 'admin') {
140
+ return c.json({ error: 'Forbidden: only admin can manage admin users' }, 403);
141
+ }
142
+ if (actor.role !== 'admin' &&
143
+ validation.data.role !== undefined &&
144
+ validation.data.role !== target.role) {
145
+ return c.json({ error: 'Forbidden: only admin can change roles' }, 403);
146
+ }
147
+ if (target.id === actor.id) {
148
+ if (validation.data.role && validation.data.role !== 'admin') {
149
+ return c.json({ error: 'Cannot remove your own admin role' }, 400);
150
+ }
151
+ if (validation.data.status === 'disabled' ||
152
+ validation.data.status === 'deleted') {
153
+ return c.json({ error: 'Cannot disable your own account' }, 400);
154
+ }
155
+ }
156
+ const nextRole = validation.data.role ?? target.role;
157
+ const nextStatus = validation.data.status ?? target.status;
158
+ const targetIsActiveAdmin = target.role === 'admin' && target.status === 'active';
159
+ const targetRemainsActiveAdmin = nextRole === 'admin' && nextStatus === 'active';
160
+ if (targetIsActiveAdmin &&
161
+ !targetRemainsActiveAdmin &&
162
+ getActiveAdminCount() <= 1) {
163
+ return c.json({ error: 'Cannot remove the last active admin' }, 400);
164
+ }
165
+ const updates = {};
166
+ if (validation.data.role !== undefined) {
167
+ updates.role = validation.data.role;
168
+ if (validation.data.permissions === undefined &&
169
+ validation.data.role !== target.role) {
170
+ updates.permissions =
171
+ validation.data.role === 'admin' ? [...ALL_PERMISSIONS] : [];
172
+ }
173
+ if (validation.data.role !== target.role) {
174
+ logAuthEvent({
175
+ event_type: 'role_changed',
176
+ username: target.username,
177
+ actor_username: actor.username,
178
+ ip_address: getClientIp(c),
179
+ details: { from: target.role, to: validation.data.role },
180
+ });
181
+ }
182
+ }
183
+ if (validation.data.permissions !== undefined) {
184
+ const nextPermissions = normalizePermissions(validation.data.permissions);
185
+ if (actor.role !== 'admin') {
186
+ const allowed = new Set(actor.permissions);
187
+ const forbidden = nextPermissions.filter((perm) => !allowed.has(perm));
188
+ if (forbidden.length > 0) {
189
+ return c.json({
190
+ error: `Forbidden: cannot assign permissions [${forbidden.join(', ')}]`,
191
+ }, 403);
192
+ }
193
+ }
194
+ updates.permissions = nextPermissions;
195
+ }
196
+ if (validation.data.notes !== undefined) {
197
+ updates.notes = validation.data.notes;
198
+ }
199
+ if (validation.data.status !== undefined) {
200
+ if (validation.data.status === 'deleted') {
201
+ deleteUser(id);
202
+ logAuthEvent({
203
+ event_type: 'user_deleted',
204
+ username: target.username,
205
+ actor_username: actor.username,
206
+ ip_address: getClientIp(c),
207
+ });
208
+ const deleted = getUserById(id);
209
+ return c.json({ success: true, user: toUserPublic(deleted) });
210
+ }
211
+ updates.status = validation.data.status;
212
+ if (validation.data.status === 'disabled') {
213
+ // Revoke all sessions when disabling + clean caches
214
+ invalidateUserSessions(id);
215
+ deleteUserSessionsByUserId(id);
216
+ updates.disable_reason =
217
+ validation.data.disable_reason ?? 'disabled_by_admin';
218
+ logAuthEvent({
219
+ event_type: 'user_disabled',
220
+ username: target.username,
221
+ actor_username: actor.username,
222
+ ip_address: getClientIp(c),
223
+ });
224
+ }
225
+ else if (validation.data.status === 'active') {
226
+ updates.disable_reason = null;
227
+ if (target.status === 'disabled') {
228
+ logAuthEvent({
229
+ event_type: 'user_enabled',
230
+ username: target.username,
231
+ actor_username: actor.username,
232
+ ip_address: getClientIp(c),
233
+ });
234
+ }
235
+ if (target.status === 'deleted') {
236
+ updates.deleted_at = null;
237
+ logAuthEvent({
238
+ event_type: 'user_restored',
239
+ username: target.username,
240
+ actor_username: actor.username,
241
+ ip_address: getClientIp(c),
242
+ });
243
+ }
244
+ }
245
+ }
246
+ if (validation.data.display_name !== undefined)
247
+ updates.display_name = validation.data.display_name;
248
+ if (validation.data.disable_reason !== undefined &&
249
+ validation.data.status !== 'disabled') {
250
+ updates.disable_reason = validation.data.disable_reason;
251
+ }
252
+ if (validation.data.password !== undefined) {
253
+ updates.password_hash = await hashPassword(validation.data.password);
254
+ updates.must_change_password = true;
255
+ invalidateUserSessions(id);
256
+ deleteUserSessionsByUserId(id);
257
+ logAuthEvent({
258
+ event_type: 'password_changed',
259
+ username: target.username,
260
+ actor_username: actor.username,
261
+ ip_address: getClientIp(c),
262
+ details: { admin_reset: true },
263
+ });
264
+ logAuthEvent({
265
+ event_type: 'session_revoked',
266
+ username: target.username,
267
+ actor_username: actor.username,
268
+ ip_address: getClientIp(c),
269
+ details: { action: 'password_reset_revoke_all' },
270
+ });
271
+ }
272
+ if (Object.keys(updates).length > 0) {
273
+ logAuthEvent({
274
+ event_type: 'user_updated',
275
+ username: target.username,
276
+ actor_username: actor.username,
277
+ ip_address: getClientIp(c),
278
+ details: { fields: Object.keys(updates) },
279
+ });
280
+ }
281
+ updateUserFields(id, updates);
282
+ const updated = getUserById(id);
283
+ return c.json({ success: true, user: toUserPublic(updated) });
284
+ });
285
+ // DELETE /api/admin/users/:id - 删除用户
286
+ adminRoutes.delete('/users/:id', authMiddleware, usersManageMiddleware, (c) => {
287
+ const id = c.req.param('id');
288
+ const target = getUserById(id);
289
+ if (!target)
290
+ return c.json({ error: 'User not found' }, 404);
291
+ const actor = c.get('user');
292
+ if (actor.role !== 'admin' && target.role === 'admin') {
293
+ return c.json({ error: 'Forbidden: only admin can delete admin users' }, 403);
294
+ }
295
+ if (target.id === actor.id) {
296
+ return c.json({ error: 'Cannot delete yourself' }, 400);
297
+ }
298
+ if (target.role === 'admin' &&
299
+ target.status === 'active' &&
300
+ getActiveAdminCount() <= 1) {
301
+ return c.json({ error: 'Cannot delete the last active admin' }, 400);
302
+ }
303
+ deleteUser(id);
304
+ // Cleanup avatar files for deleted user
305
+ try {
306
+ const avatarsDir = path.join(DATA_DIR, 'avatars');
307
+ if (fs.existsSync(avatarsDir)) {
308
+ const files = fs
309
+ .readdirSync(avatarsDir)
310
+ .filter((f) => f.startsWith(`${id}-`));
311
+ for (const file of files) {
312
+ fs.unlinkSync(path.join(avatarsDir, file));
313
+ }
314
+ }
315
+ }
316
+ catch (e) {
317
+ // Avatar cleanup failure should not block user deletion
318
+ logger.warn({ error: e, userId: id }, 'Failed to cleanup avatar files');
319
+ }
320
+ logAuthEvent({
321
+ event_type: 'user_deleted',
322
+ username: target.username,
323
+ actor_username: actor.username,
324
+ ip_address: getClientIp(c),
325
+ details: { action: 'deleted' },
326
+ });
327
+ return c.json({ success: true });
328
+ });
329
+ // POST /api/admin/users/:id/restore - 恢复已删除用户
330
+ adminRoutes.post('/users/:id/restore', authMiddleware, usersManageMiddleware, (c) => {
331
+ const id = c.req.param('id');
332
+ const target = getUserById(id);
333
+ if (!target)
334
+ return c.json({ error: 'User not found' }, 404);
335
+ const actor = c.get('user');
336
+ if (actor.role !== 'admin' && target.role === 'admin') {
337
+ return c.json({ error: 'Forbidden: only admin can restore admin users' }, 403);
338
+ }
339
+ if (target.status !== 'deleted')
340
+ return c.json({ error: 'User is not deleted' }, 400);
341
+ restoreUser(id);
342
+ logAuthEvent({
343
+ event_type: 'user_restored',
344
+ username: target.username,
345
+ actor_username: actor.username,
346
+ ip_address: getClientIp(c),
347
+ });
348
+ const restored = getUserById(id);
349
+ return c.json({ success: true, user: toUserPublic(restored) });
350
+ });
351
+ // DELETE /api/admin/users/:id/sessions - 撤销用户所有会话
352
+ adminRoutes.delete('/users/:id/sessions', authMiddleware, usersManageMiddleware, (c) => {
353
+ const id = c.req.param('id');
354
+ const target = getUserById(id);
355
+ if (!target)
356
+ return c.json({ error: 'User not found' }, 404);
357
+ const actor = c.get('user');
358
+ if (actor.role !== 'admin' && target.role === 'admin') {
359
+ return c.json({ error: 'Forbidden: only admin can revoke admin sessions' }, 403);
360
+ }
361
+ invalidateUserSessions(id);
362
+ deleteUserSessionsByUserId(id);
363
+ logAuthEvent({
364
+ event_type: 'session_revoked',
365
+ username: target.username,
366
+ actor_username: actor.username,
367
+ ip_address: getClientIp(c),
368
+ details: { action: 'revoke_all' },
369
+ });
370
+ return c.json({ success: true });
371
+ });
372
+ // --- Invite Codes ---
373
+ // GET /api/admin/invites - 获取邀请码列表
374
+ adminRoutes.get('/invites', authMiddleware, inviteManageMiddleware, (c) => {
375
+ return c.json({ invites: getAllInviteCodes() });
376
+ });
377
+ // POST /api/admin/invites - 创建邀请码
378
+ adminRoutes.post('/invites', authMiddleware, inviteManageMiddleware, async (c) => {
379
+ const body = await c.req.json().catch(() => ({}));
380
+ const validation = InviteCreateSchema.safeParse(body);
381
+ if (!validation.success) {
382
+ return c.json({ error: 'Invalid request', details: validation.error.format() }, 400);
383
+ }
384
+ const actor = c.get('user');
385
+ const template = resolveTemplate(validation.data.permission_template);
386
+ if (template &&
387
+ validation.data.role !== undefined &&
388
+ validation.data.role !== template.role) {
389
+ return c.json({ error: 'role conflicts with permission_template' }, 400);
390
+ }
391
+ const role = template?.role || validation.data.role || 'member';
392
+ const permissions = normalizePermissions(validation.data.permissions ??
393
+ template?.permissions ??
394
+ (role === 'admin' ? ALL_PERMISSIONS : []));
395
+ if (actor.role !== 'admin') {
396
+ if (role === 'admin') {
397
+ return c.json({ error: 'Forbidden: only admin can create admin invites' }, 403);
398
+ }
399
+ const allowed = new Set(actor.permissions);
400
+ const forbidden = permissions.filter((perm) => !allowed.has(perm));
401
+ if (forbidden.length > 0) {
402
+ return c.json({
403
+ error: `Forbidden: cannot grant permissions [${forbidden.join(', ')}]`,
404
+ }, 403);
405
+ }
406
+ }
407
+ const code = generateInviteCode();
408
+ const now = new Date().toISOString();
409
+ const expiresAt = validation.data.expires_in_hours
410
+ ? new Date(Date.now() + validation.data.expires_in_hours * 60 * 60 * 1000).toISOString()
411
+ : null;
412
+ dbCreateInviteCode({
413
+ code,
414
+ created_by: actor.id,
415
+ role,
416
+ permission_template: validation.data.permission_template ?? null,
417
+ permissions,
418
+ max_uses: validation.data.max_uses ?? 1,
419
+ used_count: 0,
420
+ expires_at: expiresAt,
421
+ created_at: now,
422
+ });
423
+ logAuthEvent({
424
+ event_type: 'invite_created',
425
+ username: actor.username,
426
+ ip_address: getClientIp(c),
427
+ details: {
428
+ code_prefix: code.slice(0, 8),
429
+ role,
430
+ permission_template: validation.data.permission_template || null,
431
+ permissions,
432
+ },
433
+ });
434
+ return c.json({ success: true, code }, 201);
435
+ });
436
+ // DELETE /api/admin/invites/:code - 删除邀请码
437
+ adminRoutes.delete('/invites/:code', authMiddleware, inviteManageMiddleware, (c) => {
438
+ const code = c.req.param('code');
439
+ const invite = getInviteCode(code);
440
+ if (!invite)
441
+ return c.json({ error: 'Invite not found' }, 404);
442
+ deleteInviteCode(code);
443
+ const actor = c.get('user');
444
+ logAuthEvent({
445
+ event_type: 'invite_deleted',
446
+ username: actor.username,
447
+ actor_username: actor.username,
448
+ ip_address: getClientIp(c),
449
+ details: { code_prefix: code.slice(0, 8) },
450
+ });
451
+ return c.json({ success: true });
452
+ });
453
+ // --- Audit Log ---
454
+ // GET /api/admin/audit-log - 获取审计日志
455
+ adminRoutes.get('/audit-log', authMiddleware, auditViewMiddleware, (c) => {
456
+ const limit = parseInt(c.req.query('limit') || '100', 10);
457
+ const offset = parseInt(c.req.query('offset') || '0', 10);
458
+ const event_type = c.req.query('event_type');
459
+ const username = c.req.query('username');
460
+ const actor_username = c.req.query('actor_username');
461
+ const from = c.req.query('from');
462
+ const to = c.req.query('to');
463
+ if (from && !ISO_DATE_RE.test(from)) {
464
+ return c.json({ error: 'Invalid "from" date format (expected ISO 8601)' }, 400);
465
+ }
466
+ if (to && !ISO_DATE_RE.test(to)) {
467
+ return c.json({ error: 'Invalid "to" date format (expected ISO 8601)' }, 400);
468
+ }
469
+ const result = queryAuthAuditLogs({
470
+ limit: Number.isFinite(limit) ? Math.min(limit, 500) : 100,
471
+ offset: Number.isFinite(offset) ? offset : 0,
472
+ event_type: (event_type || 'all'),
473
+ username: username || undefined,
474
+ actor_username: actor_username || undefined,
475
+ from: from || undefined,
476
+ to: to || undefined,
477
+ });
478
+ return c.json(result);
479
+ });
480
+ // GET /api/admin/audit-log/export - 导出审计日志为 CSV
481
+ adminRoutes.get('/audit-log/export', authMiddleware, auditViewMiddleware, (c) => {
482
+ const limit = parseInt(c.req.query('limit') || '2000', 10);
483
+ const offset = parseInt(c.req.query('offset') || '0', 10);
484
+ const event_type = c.req.query('event_type');
485
+ const username = c.req.query('username');
486
+ const actor_username = c.req.query('actor_username');
487
+ const from = c.req.query('from');
488
+ const to = c.req.query('to');
489
+ if (from && !ISO_DATE_RE.test(from)) {
490
+ return c.json({ error: 'Invalid "from" date format (expected ISO 8601)' }, 400);
491
+ }
492
+ if (to && !ISO_DATE_RE.test(to)) {
493
+ return c.json({ error: 'Invalid "to" date format (expected ISO 8601)' }, 400);
494
+ }
495
+ const result = queryAuthAuditLogs({
496
+ limit: Number.isFinite(limit) ? Math.min(limit, 5000) : 2000,
497
+ offset: Number.isFinite(offset) ? offset : 0,
498
+ event_type: (event_type || 'all'),
499
+ username: username || undefined,
500
+ actor_username: actor_username || undefined,
501
+ from: from || undefined,
502
+ to: to || undefined,
503
+ });
504
+ const escapeCsv = (value) => {
505
+ const text = value === null || value === undefined ? '' : String(value);
506
+ if (text.includes(',') || text.includes('"') || text.includes('\n')) {
507
+ return `"${text.replace(/"/g, '""')}"`;
508
+ }
509
+ return text;
510
+ };
511
+ const rows = [
512
+ [
513
+ 'id',
514
+ 'event_type',
515
+ 'username',
516
+ 'actor_username',
517
+ 'ip_address',
518
+ 'user_agent',
519
+ 'created_at',
520
+ 'details_json',
521
+ ].join(','),
522
+ ...result.logs.map((log) => [
523
+ log.id,
524
+ log.event_type,
525
+ log.username,
526
+ log.actor_username ?? '',
527
+ log.ip_address ?? '',
528
+ log.user_agent ?? '',
529
+ log.created_at,
530
+ JSON.stringify(log.details ?? {}),
531
+ ]
532
+ .map(escapeCsv)
533
+ .join(',')),
534
+ ];
535
+ return new Response(rows.join('\n'), {
536
+ status: 200,
537
+ headers: {
538
+ 'Content-Type': 'text/csv; charset=utf-8',
539
+ 'Content-Disposition': `attachment; filename="audit-log-${Date.now()}.csv"`,
540
+ },
541
+ });
542
+ });
543
+ export default adminRoutes;