@sleep2agi/commhub-server 0.9.0-preview.14 → 0.9.0-preview.140

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 (341) hide show
  1. package/README.md +130 -10
  2. package/bin/commhub.ts +99 -11
  3. package/package.json +7 -7
  4. package/src/ack-create-request-transport.test.ts +182 -0
  5. package/src/ack-create-request.test.ts +50 -7
  6. package/src/admin-delete-network-http.test.ts +228 -0
  7. package/src/admin-networks-http.test.ts +88 -0
  8. package/src/adoption-candidates-http.test.ts +137 -0
  9. package/src/adoption-candidates.ts +158 -0
  10. package/src/agent-access-batch.test.ts +210 -0
  11. package/src/agent-access.ts +487 -0
  12. package/src/agent-acl-groups-http.test.ts +247 -0
  13. package/src/agent-acl-http.test.ts +659 -0
  14. package/src/agent-acl-slice3-http.test.ts +267 -0
  15. package/src/agent-department-http.test.ts +148 -0
  16. package/src/agent-groups-perf.test.ts +223 -0
  17. package/src/agent-notice.ts +48 -0
  18. package/src/agent-team-whoami-http.test.ts +98 -0
  19. package/src/agent-teams-http.test.ts +323 -0
  20. package/src/agent-teams.ts +244 -0
  21. package/src/alias-filter.test.ts +72 -0
  22. package/src/alias-filter.ts +42 -0
  23. package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
  24. package/src/api-host-supervisors-fallback.test.ts +271 -0
  25. package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
  26. package/src/api-nodes-shape.test.ts +26 -10
  27. package/src/auth-password-change-cli-tokens-http.test.ts +423 -0
  28. package/src/auth-sessions-http.test.ts +250 -0
  29. package/src/auth-validate.test.ts +5 -1
  30. package/src/auth-weak-password-login.test.ts +176 -0
  31. package/src/auth.ts +604 -51
  32. package/src/avatar-validate.test.ts +163 -0
  33. package/src/avatar-validate.ts +103 -0
  34. package/src/binding-generation-http.test.ts +121 -0
  35. package/src/blocked-exit-on-reply.test.ts +122 -0
  36. package/src/boot-side-effect.test.ts +124 -0
  37. package/src/chain-attempt-dedup.test.ts +447 -0
  38. package/src/clone-node.test.ts +248 -0
  39. package/src/clone-node.ts +289 -0
  40. package/src/codex-config-flags.ts +77 -0
  41. package/src/codex-fork-contract.ts +23 -0
  42. package/src/codex-fork-migration.test.ts +38 -0
  43. package/src/commhub-bin-args.test.ts +85 -0
  44. package/src/config-apply-sec1.test.ts +107 -0
  45. package/src/config-apply-validate.test.ts +134 -1
  46. package/src/config-apply-validate.ts +111 -5
  47. package/src/create-node-blocked-gate.test.ts +356 -0
  48. package/src/create-node-network-id.test.ts +79 -0
  49. package/src/create-node-tool-schema.test.ts +49 -0
  50. package/src/create-node-unicode-name.test.ts +120 -0
  51. package/src/create-node-validate.test.ts +199 -7
  52. package/src/create-node-validate.ts +232 -15
  53. package/src/create-node-workdir.test.ts +289 -0
  54. package/src/create-node.ts +88 -23
  55. package/src/daemon-provider-snapshots.test.ts +421 -0
  56. package/src/daemon-provider-snapshots.ts +327 -0
  57. package/src/daemon-token-owner-http.test.ts +236 -0
  58. package/src/dashboard-slash-routing-http.test.ts +109 -0
  59. package/src/db-adapter-guard.test.ts +148 -0
  60. package/src/db-adapter-translate.test.ts +128 -0
  61. package/src/db-adapter.ts +323 -87
  62. package/src/db-timestamp.test.ts +38 -0
  63. package/src/db-timestamp.ts +15 -0
  64. package/src/db.ts +1111 -42
  65. package/src/delivered-without-push.test.ts +140 -0
  66. package/src/department-groups-http.test.ts +209 -0
  67. package/src/department-groups-sync-http.test.ts +328 -0
  68. package/src/department-groups.ts +434 -0
  69. package/src/department-heads-http.test.ts +330 -0
  70. package/src/department-heads.ts +226 -0
  71. package/src/departments-http.test.ts +185 -0
  72. package/src/departments.ts +276 -0
  73. package/src/desktop-message-seam.test.ts +150 -0
  74. package/src/desktop-message.test.ts +368 -0
  75. package/src/dm-file-scope-http.test.ts +178 -0
  76. package/src/env-transport-gate-http.test.ts +146 -0
  77. package/src/env-transport-gate.ts +74 -0
  78. package/src/env-transport-policy.ts +21 -0
  79. package/src/ext-token-shared.test.ts +60 -0
  80. package/src/ext-validation-regression.test.ts +131 -0
  81. package/src/external-schedule-edits-http.test.ts +321 -0
  82. package/src/external-schedule-edits-timestamps.test.ts +64 -0
  83. package/src/external-schedule-edits.ts +327 -0
  84. package/src/file-download-authz-dev-open.test.ts +135 -0
  85. package/src/file-download-authz.test.ts +404 -0
  86. package/src/file-download-date-bucket.test.ts +436 -0
  87. package/src/file-network-scope-dev-open.test.ts +97 -0
  88. package/src/file-network-scope.test.ts +732 -0
  89. package/src/group-membership-events-http.test.ts +264 -0
  90. package/src/group-messages-http.test.ts +482 -0
  91. package/src/group-messages.ts +297 -0
  92. package/src/health-redaction.test.ts +642 -0
  93. package/src/host-supervisor-identity-http.test.ts +152 -0
  94. package/src/host-supervisor-identity.ts +92 -0
  95. package/src/http-gzip.test.ts +94 -0
  96. package/src/http-gzip.ts +112 -0
  97. package/src/hub-catalog-read-http.test.ts +187 -0
  98. package/src/hub-provider-config.test.ts +139 -0
  99. package/src/hub-provider-config.ts +105 -0
  100. package/src/hub-timestamp.test.ts +38 -0
  101. package/src/hub-timestamp.ts +27 -0
  102. package/src/human-dm.ts +215 -0
  103. package/src/human-inbox.ts +42 -0
  104. package/src/inbox-caller.ts +49 -0
  105. package/src/inbox-count-sse.test.ts +310 -0
  106. package/src/inbox-count.ts +29 -0
  107. package/src/index.ts +15 -2362
  108. package/src/lifecycle-guard.ts +1 -1
  109. package/src/list-host-supervisors.test.ts +5 -4
  110. package/src/mcp-context-cost-http.test.ts +123 -0
  111. package/src/mcp-projects-events-http.test.ts +162 -0
  112. package/src/mcp-route-label-http.test.ts +120 -0
  113. package/src/mcp-write-network-resolution.test.ts +432 -0
  114. package/src/member-audit-network-id-http.test.ts +93 -0
  115. package/src/member-presence-http.test.ts +210 -0
  116. package/src/messages-ack-by-agent-http.test.ts +142 -0
  117. package/src/migrate-sqlite-to-pg.test.ts +47 -0
  118. package/src/migrate-sqlite-to-pg.ts +263 -0
  119. package/src/model-auth-notify-http.test.ts +181 -0
  120. package/src/model-auth-notify.ts +116 -0
  121. package/src/network-name-http.test.ts +75 -0
  122. package/src/network-scope.ts +276 -0
  123. package/src/network-token-membership-http.test.ts +139 -0
  124. package/src/node-alias-control.ts +12 -0
  125. package/src/node-attrs-validate.ts +88 -0
  126. package/src/node-attrs.test.ts +217 -0
  127. package/src/node-caller-identity-http.test.ts +1083 -0
  128. package/src/node-create-request-status-http.test.ts +105 -0
  129. package/src/node-daemon-bindings.test.ts +132 -0
  130. package/src/node-daemon-bindings.ts +135 -0
  131. package/src/node-files-transport.test.ts +344 -0
  132. package/src/node-health-codex-login.test.ts +120 -0
  133. package/src/node-health-dispatch-http.test.ts +279 -0
  134. package/src/node-health-guard.ts +105 -0
  135. package/src/node-health-passthrough.test.ts +115 -0
  136. package/src/node-health-store.ts +101 -0
  137. package/src/node-id-alias.test.ts +190 -0
  138. package/src/node-identity-conflict-http.test.ts +269 -0
  139. package/src/node-identity-conflict.ts +142 -0
  140. package/src/node-identity-mismatch.test.ts +74 -0
  141. package/src/node-lifecycle-controllable.test.ts +64 -0
  142. package/src/node-lifecycle-controllable.ts +53 -0
  143. package/src/node-lifecycle-read-http.test.ts +245 -0
  144. package/src/node-lifecycle-read.ts +121 -0
  145. package/src/node-logs-transport.test.ts +263 -0
  146. package/src/node-permissions-http.test.ts +276 -0
  147. package/src/node-permissions.ts +285 -0
  148. package/src/node-request-retention.test.ts +281 -0
  149. package/src/node-request-retention.ts +120 -0
  150. package/src/node-secret-rest-http.test.ts +190 -0
  151. package/src/node-skills-transport.test.ts +209 -0
  152. package/src/node-token-identity.ts +35 -0
  153. package/src/node-token-ownership.ts +51 -0
  154. package/src/node-token-privilege-http.test.ts +158 -0
  155. package/src/observer-avatar-http.test.ts +442 -0
  156. package/src/observer-push.test.ts +252 -0
  157. package/src/opencode-v2-catalog.test.ts +268 -0
  158. package/src/opencode-v2-catalog.ts +352 -0
  159. package/src/package-version-consistency.test.ts +25 -0
  160. package/src/password-network-tokens.ts +25 -0
  161. package/src/peer-reply-atomic.test.ts +455 -0
  162. package/src/peer-reply-capability.test.ts +70 -0
  163. package/src/pg-worker.ts +87 -0
  164. package/src/project-dir-warn-clear-http.test.ts +132 -0
  165. package/src/project-viewer-can-http.test.ts +114 -0
  166. package/src/push.ts +429 -10
  167. package/src/redact-tokens.test.ts +65 -0
  168. package/src/redact-tokens.ts +40 -0
  169. package/src/rename.ts +38 -10
  170. package/src/reply-target-derivation.test.ts +96 -0
  171. package/src/report-status-handover-capabilities-http.test.ts +124 -0
  172. package/src/report-status-host-schema.test.ts +235 -0
  173. package/src/report-status-resume-id-handover.test.ts +184 -0
  174. package/src/report-status-secret-keys.test.ts +140 -0
  175. package/src/require-explicit-test-db.ts +9 -0
  176. package/src/requirement-due-reminders-http.test.ts +620 -0
  177. package/src/requirement-due-reminders.ts +416 -0
  178. package/src/requirement-events-http.test.ts +268 -0
  179. package/src/requirement-events.test.ts +45 -0
  180. package/src/requirement-events.ts +144 -0
  181. package/src/requirement-notify.ts +165 -0
  182. package/src/requirement-participant-notify-http.test.ts +309 -0
  183. package/src/requirement-tag-ops-http.test.ts +266 -0
  184. package/src/requirement-tags.ts +89 -0
  185. package/src/requirements-abandoned-http.test.ts +236 -0
  186. package/src/requirements-comment-http.test.ts +167 -0
  187. package/src/requirements-completed-at-migration.test.ts +70 -0
  188. package/src/requirements-errors-mcp-http.test.ts +164 -0
  189. package/src/requirements-errors.ts +143 -0
  190. package/src/requirements-etag-http.test.ts +91 -0
  191. package/src/requirements-http.test.ts +709 -0
  192. package/src/requirements-last-event-http.test.ts +349 -0
  193. package/src/requirements-list-mcp-slim-http.test.ts +155 -0
  194. package/src/requirements-list-slim-http.test.ts +319 -0
  195. package/src/requirements-mcp.test.ts +163 -0
  196. package/src/requirements-migrate.ts +259 -0
  197. package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
  198. package/src/requirements-people-http.test.ts +114 -0
  199. package/src/requirements-people-mcp-http.test.ts +225 -0
  200. package/src/requirements-people.ts +131 -0
  201. package/src/requirements-priority-lowest.test.ts +114 -0
  202. package/src/requirements-search-http.test.ts +182 -0
  203. package/src/requirements-search.test.ts +32 -0
  204. package/src/requirements-search.ts +145 -0
  205. package/src/requirements-seq-http.test.ts +193 -0
  206. package/src/requirements-seq-migration.test.ts +63 -0
  207. package/src/requirements-start-date.test.ts +125 -0
  208. package/src/requirements-stats-http.test.ts +357 -0
  209. package/src/requirements-stats.ts +195 -0
  210. package/src/requirements-status-participants-http.test.ts +222 -0
  211. package/src/requirements-upgrade-e2e.test.ts +126 -0
  212. package/src/requirements-upgrade-migration.test.ts +98 -0
  213. package/src/requirements.ts +1806 -0
  214. package/src/resolve-port.test.ts +57 -0
  215. package/src/resolve-port.ts +44 -0
  216. package/src/response-charset.test.ts +130 -0
  217. package/src/rest-explicit-columns-http.test.ts +222 -0
  218. package/src/rest-identity.test.ts +130 -0
  219. package/src/rest-identity.ts +141 -0
  220. package/src/rest-projections.ts +70 -0
  221. package/src/rest-write-network-resolution.test.ts +133 -0
  222. package/src/restricted-files.ts +76 -0
  223. package/src/retention.ts +15 -3
  224. package/src/route-callers-http.test.ts +120 -0
  225. package/src/route-timing.test.ts +211 -0
  226. package/src/route-timing.ts +258 -0
  227. package/src/rules-file-duplicate-alias.test.ts +234 -0
  228. package/src/rules-file-session-target.test.ts +207 -0
  229. package/src/rules-file-transport.test.ts +237 -0
  230. package/src/runtime-label.test.ts +22 -0
  231. package/src/runtime-label.ts +21 -0
  232. package/src/runtime-readiness.ts +90 -0
  233. package/src/schedule-agent-mcp-http.test.ts +381 -0
  234. package/src/schedule-agent.ts +192 -0
  235. package/src/scheduled-backoff-http.test.ts +193 -0
  236. package/src/scheduled-backoff.ts +64 -0
  237. package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
  238. package/src/scheduled-failures-http.test.ts +233 -0
  239. package/src/scheduled-failures.ts +179 -0
  240. package/src/scheduled-offline.test.ts +131 -0
  241. package/src/scheduled-offline.ts +66 -0
  242. package/src/scheduled-outcome-guidance-http.test.ts +139 -0
  243. package/src/scheduled-outcome-guidance.ts +9 -0
  244. package/src/scheduled-reply-unread-http.test.ts +212 -0
  245. package/src/scheduled-requirement-wake-http.test.ts +264 -0
  246. package/src/scheduled-requirement-wake.ts +125 -0
  247. package/src/scheduled-run-terminal.test.ts +268 -0
  248. package/src/scheduled-skip-blocking-task.test.ts +120 -0
  249. package/src/scheduled-stuck-http.test.ts +312 -0
  250. package/src/scheduled-stuck.ts +186 -0
  251. package/src/scheduled-tasks-http.test.ts +638 -0
  252. package/src/scheduled-tasks.ts +819 -0
  253. package/src/scheduled-work-outcome-http.test.ts +154 -0
  254. package/src/scheduled-work-outcome.ts +50 -0
  255. package/src/scoped-secrets.ts +139 -0
  256. package/src/send-dedup-request-id.test.ts +229 -0
  257. package/src/send-reply-agent-warning.test.ts +202 -0
  258. package/src/send-reply-alias-optional.test.ts +131 -0
  259. package/src/send-reply-attachments.test.ts +458 -0
  260. package/src/send_dedup.test.ts +54 -0
  261. package/src/send_dedup.ts +52 -6
  262. package/src/serve-error.test.ts +38 -0
  263. package/src/serve-error.ts +27 -0
  264. package/src/server.ts +5398 -0
  265. package/src/session-task-on-dispatch.ts +14 -0
  266. package/src/session-token-ownership-http.test.ts +122 -0
  267. package/src/shared/codex-inventory.ts +41 -0
  268. package/src/shared/daemon-provider-login.ts +31 -0
  269. package/src/shared/daemon-provider-snapshot.ts +52 -0
  270. package/src/shared/external-schedule-contract.ts +60 -0
  271. package/src/shared/node-name.test.ts +62 -0
  272. package/src/shared/node-name.ts +161 -0
  273. package/src/shared/provider-application.ts +29 -0
  274. package/src/shared/provider-collection.ts +14 -0
  275. package/src/shared/provider-initialization.ts +8 -0
  276. package/src/shared/reserved-env-drift.test.ts +10 -0
  277. package/src/shared/scoped-secret-contract.ts +5 -0
  278. package/src/shared/task-content-limit-drift.test.ts +28 -0
  279. package/src/shared/task-content-limit.ts +25 -0
  280. package/src/side-thread-command-transport.test.ts +312 -0
  281. package/src/side-thread-command-transport.ts +318 -0
  282. package/src/side-thread-http-integration.test.ts +233 -0
  283. package/src/side-thread-http.ts +504 -0
  284. package/src/side-thread-production.test.ts +72 -0
  285. package/src/side-thread-production.ts +118 -0
  286. package/src/side-thread.test.ts +1067 -0
  287. package/src/side-thread.ts +2499 -0
  288. package/src/skillhub-http.test.ts +108 -0
  289. package/src/sqlite-close-checkpoint.test.ts +78 -0
  290. package/src/start-node.test.ts +264 -0
  291. package/src/status-alias-filter-http.test.ts +106 -0
  292. package/src/status-alias-resolver-http.test.ts +163 -0
  293. package/src/status-alias-resolver.ts +19 -0
  294. package/src/status-node-id-filter-http.test.ts +161 -0
  295. package/src/status-read-cache-http.test.ts +229 -0
  296. package/src/status-read-cache.ts +122 -0
  297. package/src/stop-delete-node.test.ts +509 -2
  298. package/src/task-access-http.test.ts +402 -0
  299. package/src/task-access.ts +420 -0
  300. package/src/task-activity.ts +60 -0
  301. package/src/task-auth-origin-http.test.ts +93 -0
  302. package/src/task-auth-origin.test.ts +24 -0
  303. package/src/task-auth-origin.ts +7 -0
  304. package/src/task-consumption.test.ts +482 -0
  305. package/src/task-diagnostic-http.test.ts +149 -0
  306. package/src/task-diagnostic.test.ts +115 -0
  307. package/src/task-diagnostic.ts +118 -0
  308. package/src/task-expiry-notice-http.test.ts +287 -0
  309. package/src/task-expiry-notice.ts +315 -0
  310. package/src/task-idempotency-mcp.test.ts +142 -0
  311. package/src/task-idempotency.test.ts +60 -0
  312. package/src/task-idempotency.ts +75 -0
  313. package/src/task-late-replies.ts +54 -0
  314. package/src/task-late-reply-http.test.ts +152 -0
  315. package/src/task-lifecycle-watcher.test.ts +204 -0
  316. package/src/task-lifecycle-watcher.ts +49 -0
  317. package/src/task-network-resolution-http.test.ts +140 -0
  318. package/src/task-orphan-http.test.ts +287 -0
  319. package/src/task-orphan.ts +119 -0
  320. package/src/task-queue-ahead-http.test.ts +309 -0
  321. package/src/task-queue-ahead-scale.test.ts +236 -0
  322. package/src/task-queue-ahead.ts +186 -0
  323. package/src/task-rejection-log-http.test.ts +160 -0
  324. package/src/task-rejection-log.ts +49 -0
  325. package/src/task-sender-http.test.ts +143 -0
  326. package/src/task-stale-open-http.test.ts +290 -0
  327. package/src/task-stale-open.ts +103 -0
  328. package/src/tasks-pagination-http.test.ts +141 -0
  329. package/src/tool-audience-http.test.ts +220 -0
  330. package/src/tool-audience.ts +118 -0
  331. package/src/tools-list-json-schema.test.ts +67 -0
  332. package/src/tools.ts +4008 -355
  333. package/src/upload-early-reject-guard.test.ts +155 -0
  334. package/src/uploads-http.test.ts +97 -15
  335. package/src/uploads.ts +103 -6
  336. package/src/user-avatar-http.test.ts +74 -0
  337. package/src/user-avatar-migration.test.ts +25 -0
  338. package/src/user-inbox-by-agent-http.test.ts +161 -0
  339. package/src/user-inbox-netid-notnull.test.ts +122 -0
  340. package/src/user-inbox-read-http.test.ts +223 -0
  341. package/src/user-inbox-schema.test.ts +54 -0
@@ -0,0 +1,423 @@
1
+ // #711 改密码只让浏览器 / app 的登录会话下线,命令行和脚本令牌默认保留。
2
+ //
3
+ // main 上 POST /api/auth/password 跑的是 `DELETE FROM api_tokens WHERE user_id = ? AND network_id IS NULL
4
+ // AND token_id != ?` —— 每台机器上 `anet login` 拿到的令牌、POST /api/auth/tokens 建的具名脚本令牌,
5
+ // 跟浏览器会话一起被删光。安全复审定的形状:默认不变,保留是显式选项。这里每条都钉住:
6
+ // · 默认(旧 app、dashboard 代理不带字段):撤销全部不绑网络的令牌 —— 客户端自报的 cli 也活不下来;
7
+ // · keep_cli_tokens=true(anet passwd 默认带):只撤其他 kind='login' 会话,保留的 cli 令牌列在响应里、不含令牌值;
8
+ // · 当前会话(换发后的新令牌)永远可用,而且沿用当前令牌的种类;
9
+ // · 节点令牌不动;具名网络令牌默认保留,可显式revoke_network_tokens;
10
+ // · 启动迁移按 client_label / scope 回填存量令牌的 kind(夹具库,跑两遍幂等)。
11
+ // SQLite 由 test798 自动收进;PostgreSQL 由 tests/test2123-hub-postgres-ladder 的 run_pg_tests_rc 跑同一个文件。
12
+
13
+ import { describe, expect, test, beforeAll, afterAll } from "bun:test";
14
+ import { mkdtempSync, rmSync } from "fs";
15
+ import { join } from "path";
16
+ import { tmpdir } from "os";
17
+ import { spawnSync } from "child_process";
18
+ import { register, createNetworkTokenForNode, createToken, createNetwork, changePassword } from "./auth.js";
19
+ import { db, hashToken } from "./db.js";
20
+
21
+ const DIR = mkdtempSync(join(tmpdir(), "anet-pwchange-kind-"));
22
+ let BASE = "";
23
+ let hub: any = null;
24
+ const PW = "PwChangeKindPassw0rd!";
25
+ const PW2 = "PwChangeKindPassw0rd!2";
26
+ const CLI_LABEL = "anet 2.5.0 · test-box · login";
27
+
28
+ async function call(token: string, method: string, path: string, body?: unknown) {
29
+ const res = await fetch(`${BASE}${path}`, {
30
+ method,
31
+ headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
32
+ body: body === undefined ? undefined : JSON.stringify(body),
33
+ });
34
+ return { status: res.status, body: await res.json().catch(() => null) as any };
35
+ }
36
+ const works = async (token: string) => (await call(token, "GET", "/api/auth/me")).status === 200;
37
+ const kindOf = (token: string) => db.get<any>("SELECT kind FROM api_tokens WHERE token_hash = ?1", hashToken(token))?.kind ?? null;
38
+
39
+ type Fleet = {
40
+ user: string; userId: string; net: string;
41
+ app: string; appB: string; regToken: string;
42
+ cliByLabel: string; cliByKind: string; apiToken: string;
43
+ nodeToken: string; netToken: string;
44
+ };
45
+
46
+ let seq = 0;
47
+ async function fleet(): Promise<Fleet> {
48
+ const user = `pwk_${Date.now()}_${++seq}`;
49
+ const reg = register(user, PW);
50
+ expect(reg.ok).toBe(true);
51
+ const login = async (extra: Record<string, unknown>) => {
52
+ const r = await call("", "POST", "/api/auth/login", { username: user, password: PW, ...extra });
53
+ expect(r.status).toBe(200);
54
+ return r.body.token as string;
55
+ };
56
+ const node = createNetworkTokenForNode(reg.user!.user_id, reg.network_id!, "pwk-node", `node_pwk_${seq}`);
57
+ expect(node.ok).toBe(true);
58
+ const api = createToken(reg.user!.user_id, "ci-script");
59
+ expect(api.ok).toBe(true);
60
+ const netTok = createToken(reg.user!.user_id, "net-script", reg.network_id!);
61
+ expect(netTok.ok).toBe(true);
62
+ return {
63
+ user, userId: reg.user!.user_id, net: reg.network_id!,
64
+ regToken: reg.token!,
65
+ app: await login({ client_label: "Desktop app" }),
66
+ appB: await login({}),
67
+ cliByLabel: await login({ client_label: CLI_LABEL }),
68
+ cliByKind: await login({ client_kind: "cli" }),
69
+ apiToken: api.token!,
70
+ nodeToken: node.token!,
71
+ netToken: netTok.token!,
72
+ };
73
+ }
74
+
75
+ beforeAll(async () => {
76
+ process.env.COMMHUB_DB ||= join(DIR, "hub.db");
77
+ process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
78
+ process.env.HOST = "127.0.0.1";
79
+ process.env.COMMHUB_LOGIN_IP_MAX = "1000";
80
+ const mod: any = await import("./server.js");
81
+ hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
82
+ BASE = `http://127.0.0.1:${hub.port}`;
83
+ }, 30_000);
84
+
85
+ afterAll(() => {
86
+ try { hub?.stop?.(true); } catch {}
87
+ try { rmSync(DIR, { recursive: true, force: true }); } catch {}
88
+ });
89
+
90
+ describe("new tokens are classified at issue time", () => {
91
+ test("browser/app logins are 'login'; anet-labelled, client_kind=cli and named API tokens are 'cli'; node tokens unclassified", async () => {
92
+ const f = await fleet();
93
+ expect(kindOf(f.regToken)).toBe("login");
94
+ expect(kindOf(f.app)).toBe("login");
95
+ expect(kindOf(f.appB)).toBe("login");
96
+ expect(kindOf(f.cliByLabel)).toBe("cli");
97
+ expect(kindOf(f.cliByKind)).toBe("cli");
98
+ expect(kindOf(f.apiToken)).toBe("cli");
99
+ expect(kindOf(f.nodeToken)).toBe(null);
100
+ expect(kindOf(f.netToken)).toBe(null);
101
+ });
102
+ });
103
+
104
+ // 响应里出现任何令牌值都算泄漏:换发给调用方的那一条(body.token)除外,它本来就要交给调用方。
105
+ function assertNoTokenValues(body: any, f: Fleet) {
106
+ const { token: _rotated, ...rest } = body;
107
+ const text = JSON.stringify(rest);
108
+ for (const t of [f.regToken, f.app, f.appB, f.cliByLabel, f.cliByKind, f.apiToken, f.nodeToken, f.netToken]) {
109
+ expect(text.includes(t)).toBe(false);
110
+ }
111
+ expect(/\b(utok|atok|ntok)_[A-Za-z0-9]{8,}/.test(text)).toBe(false);
112
+ expect(text.includes("token_hash")).toBe(false);
113
+ }
114
+
115
+ describe("POST /api/auth/password (default: old apps, dashboard proxy — no field)", () => {
116
+ test("every other non-network token is revoked (pre-#711 behaviour); node tokens and the rotated current session survive", async () => {
117
+ const f = await fleet();
118
+ const r = await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2 });
119
+ expect(r.status).toBe(200);
120
+ expect(r.body.ok).toBe(true);
121
+ expect(r.body.revoked_login).toBe(2); // reg + appB
122
+ expect(r.body.revoked_cli).toBe(3); // cliByLabel + cliByKind + apiToken
123
+ expect(r.body.revoked).toBe(5);
124
+ expect(r.body.kept_cli_tokens).toBeUndefined();
125
+ for (const t of [f.regToken, f.appB, f.cliByLabel, f.cliByKind, f.apiToken]) expect(await works(t)).toBe(false);
126
+ expect(await works(f.nodeToken)).toBe(true);
127
+ expect(await works(f.netToken)).toBe(true);
128
+ expect(await works(r.body.token)).toBe(true);
129
+ expect(kindOf(r.body.token)).toBe("login");
130
+ expect(await works(f.app)).toBe(false);
131
+ });
132
+
133
+ test("a client-declared cli token (client_kind / anet label) does NOT survive a default password change", async () => {
134
+ const f = await fleet();
135
+ expect(kindOf(f.cliByKind)).toBe("cli");
136
+ const r = await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2 });
137
+ expect(r.status).toBe(200);
138
+ expect(await works(f.cliByKind)).toBe(false);
139
+ expect(await works(f.cliByLabel)).toBe(false);
140
+ });
141
+
142
+ test("a non-boolean keep_cli_tokens is not an opt-in; the removed revoke_cli_tokens field does nothing", async () => {
143
+ const f = await fleet();
144
+ const r = await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2, keep_cli_tokens: "true", revoke_cli_tokens: false });
145
+ expect(r.status).toBe(200);
146
+ expect(r.body.revoked_cli).toBe(3);
147
+ expect(await works(f.cliByLabel)).toBe(false);
148
+ expect(await works(f.apiToken)).toBe(false);
149
+ });
150
+ });
151
+
152
+ describe("POST /api/auth/password with keep_cli_tokens=true (anet passwd)", () => {
153
+ test("only other login sessions are revoked; CLI/script tokens are kept and listed without token values", async () => {
154
+ const f = await fleet();
155
+ const r = await call(f.cliByLabel, "POST", "/api/auth/password", { old_password: PW, new_password: PW2, keep_cli_tokens: true });
156
+ expect(r.status).toBe(200);
157
+ expect(r.body.revoked_login).toBe(3); // reg + app + appB
158
+ expect(r.body.revoked_cli).toBe(0);
159
+ expect(r.body.revoked).toBe(3);
160
+ for (const t of [f.regToken, f.app, f.appB]) expect(await works(t)).toBe(false);
161
+ expect(await works(f.cliByKind)).toBe(true);
162
+ expect(await works(f.apiToken)).toBe(true);
163
+ expect(await works(f.nodeToken)).toBe(true);
164
+ expect(await works(f.netToken)).toBe(true);
165
+ // 当前会话:换发的新令牌可用,沿用 cli
166
+ expect(await works(r.body.token)).toBe(true);
167
+ expect(kindOf(r.body.token)).toBe("cli");
168
+ // 保留清单:不含当前这条(它已被换掉),只含另外两条 cli;字段齐全、无令牌值
169
+ const kept = r.body.kept_cli_tokens as any[];
170
+ const idOf = (t: string) => db.get<any>("SELECT token_id FROM api_tokens WHERE token_hash = ?1", hashToken(t)).token_id;
171
+ expect(kept.map((k) => k.token_id).sort()).toEqual([idOf(f.cliByKind), idOf(f.apiToken)].sort());
172
+ for (const k of kept) {
173
+ expect(Object.keys(k).sort()).toEqual(["client_label", "created_at", "last_used_at", "name", "token_id"]);
174
+ expect(k.token_id).toMatch(/^tok_[0-9a-f]{12}$/);
175
+ }
176
+ expect(kept.find((k) => k.token_id === idOf(f.apiToken)).name).toBe("ci-script");
177
+ assertNoTokenValues(r.body, f);
178
+ // 可以逐条撤销
179
+ expect((await call(r.body.token, "DELETE", `/api/auth/tokens/${idOf(f.apiToken)}`)).status).toBe(200);
180
+ expect(await works(f.apiToken)).toBe(false);
181
+ });
182
+
183
+ test("default response carries no token values either", async () => {
184
+ const f = await fleet();
185
+ const r = await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2 });
186
+ expect(r.status).toBe(200);
187
+ assertNoTokenValues(r.body, f);
188
+ });
189
+ });
190
+
191
+ describe("changePassword() keeps the current token row in both modes", () => {
192
+ test("no opts at all = default: CLI tokens revoked, current kept", async () => {
193
+ const f = await fleet();
194
+ const cur = db.get<any>("SELECT token_id FROM api_tokens WHERE token_hash = ?1", hashToken(f.app)).token_id;
195
+ const r = changePassword(f.userId, PW, PW2, cur);
196
+ expect(r.ok).toBe(true);
197
+ expect(r.revoked_cli).toBe(3);
198
+ expect(r.kept_cli_tokens).toBeUndefined();
199
+ expect(await works(f.app)).toBe(true);
200
+ expect(await works(f.cliByKind)).toBe(false);
201
+ expect(await works(f.apiToken)).toBe(false);
202
+ });
203
+
204
+ for (const keepCliTokens of [false, true]) {
205
+ test(`keepCliTokens=${keepCliTokens}: current login token kept`, async () => {
206
+ const f = await fleet();
207
+ const cur = db.get<any>("SELECT token_id FROM api_tokens WHERE token_hash = ?1", hashToken(f.app)).token_id;
208
+ const r = changePassword(f.userId, PW, PW2, cur, { keepCliTokens });
209
+ expect(r.ok).toBe(true);
210
+ expect(await works(f.app)).toBe(true);
211
+ expect(await works(f.appB)).toBe(false);
212
+ expect(await works(f.cliByLabel)).toBe(keepCliTokens);
213
+ expect(await works(f.nodeToken)).toBe(true);
214
+ });
215
+ test(`keepCliTokens=${keepCliTokens}: current CLI token kept`, async () => {
216
+ const f = await fleet();
217
+ const cur = db.get<any>("SELECT token_id FROM api_tokens WHERE token_hash = ?1", hashToken(f.cliByKind)).token_id;
218
+ const r = changePassword(f.userId, PW, PW2, cur, { keepCliTokens });
219
+ expect(r.ok).toBe(true);
220
+ expect(await works(f.cliByKind)).toBe(true);
221
+ expect(await works(f.app)).toBe(false);
222
+ expect(await works(f.apiToken)).toBe(keepCliTokens);
223
+ });
224
+ }
225
+ });
226
+
227
+ describe("startup migration backfills api_tokens.kind on a fixture DB", () => {
228
+ // 把 kind 列拿掉 = 回到 #711 之前的表结构;塞存量行;再起一个进程加载 db.ts(= Hub 启动迁移)。
229
+ function bootMigration() {
230
+ const r = spawnSync(process.execPath, ["-e", `await import(${JSON.stringify(join(import.meta.dir, "db.ts"))}); process.exit(0);`], {
231
+ cwd: join(import.meta.dir, ".."),
232
+ env: { ...process.env, NODE_ENV: "test" },
233
+ encoding: "utf8",
234
+ timeout: 60_000,
235
+ });
236
+ if (r.status !== 0) console.log(String(r.stderr || "").slice(-2000));
237
+ return r.status;
238
+ }
239
+
240
+ test("anet-labelled and named API tokens → cli, everything else → login, node tokens untouched; idempotent", async () => {
241
+ const f = await fleet();
242
+ db.exec("ALTER TABLE api_tokens DROP COLUMN kind");
243
+ const ins = (id: string, net: string | null, scope: string, label: string | null) =>
244
+ db.run(
245
+ "INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope, client_label, node_identity_epoch) VALUES (?1, ?2, ?3, ?4, 'fixture', ?5, ?6, 1)",
246
+ [id, hashToken(`fixture-${id}`), f.userId, net, scope, label],
247
+ );
248
+ const id = (s: string) => `tok_fx_${seq}_${s}`;
249
+ ins(id("cli_label"), null, "user", "anet 2.4.0 · old-box · login");
250
+ ins(id("cli_demo"), null, "user", "anet dev · unknown-host · demo sci-team");
251
+ ins(id("browser"), null, "user", "Chrome on macOS");
252
+ ins(id("nolabel"), null, "user", null);
253
+ ins(id("lookalike"), null, "user", "planet-anet dashboard");
254
+ ins(id("upper"), null, "user", "ANET 2.4.0 · box · login");
255
+ ins(id("padded"), null, "user", " anet 2.4.0 · box · login");
256
+ ins(id("named"), null, "full", null);
257
+ ins(id("node"), f.net, "network", null);
258
+ ins(id("netfull"), f.net, "full", null);
259
+
260
+ expect(bootMigration()).toBe(0);
261
+ const k = (s: string) => db.get<any>("SELECT kind FROM api_tokens WHERE token_id = ?1", id(s))?.kind ?? null;
262
+ expect(k("cli_label")).toBe("cli");
263
+ expect(k("cli_demo")).toBe("cli");
264
+ expect(k("browser")).toBe("login");
265
+ expect(k("nolabel")).toBe("login");
266
+ expect(k("lookalike")).toBe("login");
267
+ expect(k("upper")).toBe("login"); // 区分大小写:SQLite 的 LIKE 会把它当 cli,PG 不会 —— 两边必须一致
268
+ expect(k("padded")).toBe("cli"); // 与运行时一样先 trim
269
+ expect(k("named")).toBe("cli");
270
+ expect(k("node")).toBe(null);
271
+ expect(k("netfull")).toBe(null);
272
+ // fleet() 的 live 令牌也被回填(列重建后原值丢了,按同一规则算回来)
273
+ expect(kindOf(f.cliByLabel)).toBe("cli");
274
+ expect(kindOf(f.apiToken)).toBe("cli");
275
+ expect(kindOf(f.app)).toBe("login");
276
+
277
+ // 幂等:已有值不被改写(手动改成 login 的 anet 行保持 login),再跑一次结果不变
278
+ db.run("UPDATE api_tokens SET kind = 'login' WHERE token_id = ?1", [id("cli_label")]);
279
+ expect(bootMigration()).toBe(0);
280
+ expect(k("cli_label")).toBe("login");
281
+ expect(k("named")).toBe("cli");
282
+ expect(k("browser")).toBe("login");
283
+
284
+ // 迁移后的库上,keep_cli_tokens 保留回填成 cli 的存量令牌
285
+ const cur = db.get<any>("SELECT token_id FROM api_tokens WHERE token_hash = ?1", hashToken(f.app)).token_id;
286
+ const r = changePassword(f.userId, PW, PW2, cur, { keepCliTokens: true });
287
+ expect(r.ok).toBe(true);
288
+ expect(k("named")).toBe("cli");
289
+ expect(k("cli_demo")).toBe("cli");
290
+ expect(k("browser")).toBe(null); // 被撤销(行已删除)
291
+ expect(k("node")).toBe(null);
292
+ expect(Number(db.get<any>("SELECT COUNT(*) AS n FROM api_tokens WHERE token_id = ?1", id("node")).n)).toBe(1);
293
+ }, 120_000);
294
+ });
295
+
296
+
297
+ describe("network password opt-in", () => {
298
+ const id = (token: string) => db.get<any>("SELECT token_id FROM api_tokens WHERE token_hash=?1", hashToken(token)).token_id;
299
+
300
+ test("default lists only the caller's active named network tokens across networks, without credentials", async () => {
301
+ const f = await fleet();
302
+ const second = createNetwork(f.userId, `pwk-second-${seq}`);
303
+ expect(second.ok).toBe(true);
304
+ const extra = createToken(f.userId, "second-network-script", second.network_id);
305
+ const other = await fleet();
306
+ const r = await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2 });
307
+ expect(r.status).toBe(200);
308
+ expect(r.body.revoked_network).toBe(0);
309
+ expect(r.body.revoked).toBe(5);
310
+ expect(r.body.kept_network_tokens.map((x: any) => x.token_id).sort()).toEqual([id(f.netToken), id(extra.token!)].sort());
311
+ for (const row of r.body.kept_network_tokens) {
312
+ expect(Object.keys(row).sort()).toEqual(["created_at", "last_used_at", "name", "network_id", "token_id"]);
313
+ }
314
+ const body = JSON.stringify(r.body);
315
+ expect(body.includes("token_hash")).toBe(false);
316
+ for (const t of [f.netToken, extra.token!, f.nodeToken, other.netToken]) expect(body.includes(t)).toBe(false);
317
+ expect(await works(f.netToken)).toBe(true);
318
+ expect(await works(extra.token!)).toBe(true);
319
+ expect(await works(other.netToken)).toBe(true);
320
+ expect(await works(r.body.token)).toBe(true);
321
+ });
322
+
323
+ test("explicit boolean opt-in revokes only own named network tokens, independently of keep_cli_tokens", async () => {
324
+ const f = await fleet();
325
+ const other = await fleet();
326
+ const second = createNetwork(f.userId, `pwk-revoke-second-${seq}`);
327
+ const extra = createToken(f.userId, "second-network-script", second.network_id);
328
+ const r = await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2, keep_cli_tokens: true, revoke_network_tokens: true });
329
+ expect(r.status).toBe(200);
330
+ expect(r.body.revoked_network).toBe(2);
331
+ expect(r.body.revoked_login).toBe(2);
332
+ expect(r.body.revoked_cli).toBe(0);
333
+ expect(r.body.revoked).toBe(4);
334
+ expect(r.body.kept_network_tokens).toEqual([]);
335
+ expect(await works(f.netToken)).toBe(false);
336
+ expect(await works(extra.token!)).toBe(false);
337
+ expect(await works(other.netToken)).toBe(true);
338
+ expect(await works(other.app)).toBe(true);
339
+ expect(await works(f.nodeToken)).toBe(true);
340
+ expect(await works(f.apiToken)).toBe(true);
341
+ expect(await works(r.body.token)).toBe(true);
342
+ expect(await works(f.app)).toBe(false);
343
+ });
344
+
345
+ for (const value of [false, "true", 1, null, {}]) {
346
+ test(`non-true option ${JSON.stringify(value)} cannot revoke network tokens`, async () => {
347
+ const f = await fleet();
348
+ const r = await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2, revoke_network_tokens: value });
349
+ expect(r.status).toBe(200);
350
+ expect(r.body.revoked_network).toBe(0);
351
+ expect(r.body.kept_network_tokens).toHaveLength(1);
352
+ expect(await works(f.netToken)).toBe(true);
353
+ });
354
+ }
355
+
356
+ test("scope and binding protect node credentials; inactive rows are neither listed nor counted", async () => {
357
+ const f = await fleet();
358
+ const legacy = createNetworkTokenForNode(f.userId, f.net, `legacy-pwk-${seq}`);
359
+ expect(legacy.ok).toBe(true);
360
+ const bound = createToken(f.userId, "mislabelled-bound-node", f.net);
361
+ db.run("UPDATE api_tokens SET bound_node_id=?1 WHERE token_id=?2", ["n-protected717", bound.token_id]);
362
+ const expired = createToken(f.userId, "expired-script", f.net);
363
+ db.run("UPDATE api_tokens SET expires_at=datetime('now','-1 day') WHERE token_id=?1", [expired.token_id]);
364
+ const revoked = createToken(f.userId, "revoked-script", f.net);
365
+ db.run("UPDATE api_tokens SET revoked_at=datetime('now') WHERE token_id=?1", [revoked.token_id]);
366
+ const r = await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2, revoke_network_tokens: true });
367
+ expect(r.status).toBe(200);
368
+ expect(r.body.revoked_network).toBe(1);
369
+ expect(await works(f.nodeToken)).toBe(true);
370
+ expect(await works(bound.token!)).toBe(true);
371
+ for (const t of [bound, expired, revoked, legacy]) expect(db.get("SELECT token_id FROM api_tokens WHERE token_id=?1", t.token_id)).not.toBeNull();
372
+ });
373
+
374
+ test("bad old password or weak new password has no token side effects", async () => {
375
+ for (const body of [{ old_password: "wrong", new_password: PW2 }, { old_password: PW, new_password: "123" }]) {
376
+ const f = await fleet();
377
+ const r = await call(f.app, "POST", "/api/auth/password", { ...body, revoke_network_tokens: true });
378
+ expect(r.status).toBe(400);
379
+ for (const t of [f.app, f.netToken, f.apiToken, f.nodeToken]) expect(await works(t)).toBe(true);
380
+ expect((await call("", "POST", "/api/auth/login", { username: f.user, password: PW })).status).toBe(200);
381
+ }
382
+ });
383
+
384
+ test("network-scoped and unauthenticated credentials cannot invoke password change", async () => {
385
+ const f = await fleet();
386
+ for (const t of [f.netToken, f.nodeToken, ""]) {
387
+ expect((await call(t, "POST", "/api/auth/password", { old_password: PW, new_password: PW2, revoke_network_tokens: true })).status).toBe(t ? 403 : 401);
388
+ }
389
+ expect(await works(f.netToken)).toBe(true);
390
+ expect(await works(f.app)).toBe(true);
391
+ });
392
+
393
+ test("a failed network revoke rolls back the password and earlier session deletions", async () => {
394
+ const f = await fleet();
395
+ const victim = id(f.netToken);
396
+ const fn = `pw717_failure_${seq}`;
397
+ const trigger = `pw717_trigger_${seq}`;
398
+ if (db.dialect === "postgres") db.exec(`CREATE FUNCTION ${fn}() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF OLD.token_id='${victim}' THEN RAISE EXCEPTION 'fixture717_delete_failure'; END IF; RETURN OLD; END; $$; CREATE TRIGGER ${trigger} BEFORE DELETE ON api_tokens FOR EACH ROW EXECUTE FUNCTION ${fn}()`);
399
+ else db.exec(`CREATE TRIGGER ${trigger} BEFORE DELETE ON api_tokens WHEN OLD.token_id='${victim}' BEGIN SELECT RAISE(ABORT,'fixture717_delete_failure'); END`);
400
+ try {
401
+ expect((await call(f.app, "POST", "/api/auth/password", { old_password: PW, new_password: PW2, revoke_network_tokens: true })).status).toBe(400);
402
+ for (const t of [f.app, f.appB, f.apiToken, f.netToken, f.nodeToken]) expect(await works(t)).toBe(true);
403
+ expect((await call("", "POST", "/api/auth/login", { username: f.user, password: PW })).status).toBe(200);
404
+ expect((await call("", "POST", "/api/auth/login", { username: f.user, password: PW2 })).status).toBe(401);
405
+ } finally {
406
+ db.exec(db.dialect === "postgres" ? `DROP TRIGGER ${trigger} ON api_tokens; DROP FUNCTION ${fn}()` : `DROP TRIGGER ${trigger}`);
407
+ }
408
+ });
409
+
410
+ test("direct changePassword keeps its current row and defaults to listing network tokens", async () => {
411
+ const f = await fleet();
412
+ const cur = id(f.app);
413
+ const r = changePassword(f.userId, PW, PW2, cur);
414
+ expect(r.ok).toBe(true);
415
+ expect(r.kept_network_tokens).toHaveLength(1);
416
+ expect(await works(f.app)).toBe(true);
417
+ const r2 = changePassword(f.userId, PW2, PW, cur, { revokeNetworkTokens: true, keepCliTokens: true });
418
+ expect(r2.ok).toBe(true);
419
+ expect(r2.revoked_network).toBe(1);
420
+ expect(await works(f.app)).toBe(true);
421
+ expect(await works(f.nodeToken)).toBe(true);
422
+ });
423
+ });