@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,352 @@
1
+ // Hub authorization for a native OpenCode V2 catalog read.
2
+ //
3
+ // The caller names a daemon and a child node. Identity, generation, and the
4
+ // project directory come from Hub rows. The queued payload has no URL,
5
+ // authorization, settings, headers, or body. The daemon reads its own
6
+ // already-running loopback and acks a whitelist snapshot.
7
+ //
8
+ // `status: "pending"` means the read was authorized and stored. It does not
9
+ // mean a daemon received the doorbell or produced a snapshot. Only
10
+ // `status: "ready"` is a snapshot, and that snapshot stays completeness
11
+ // unknown / invocation unverified.
12
+
13
+ import { posix, win32 } from "node:path";
14
+ import { z } from "zod/v4";
15
+ import { db, uuidv4 } from "./db.js";
16
+ import { getUserNetworkRole } from "./auth.js";
17
+
18
+ const TTL_MS = 60_000;
19
+ const MAX_PENDING = 100;
20
+ const SECRET = /^(sk-|gh[pousr]_|github_pat_|xox[baprs]-|AKIA|AIza|ya29\.|glpat-)/i;
21
+
22
+ export const CATALOG_ERROR_CODES = [
23
+ "invalid_target", "unauthorized", "unavailable", "invalid_response", "scope_mismatch", "too_large",
24
+ ] as const;
25
+ export type CatalogErrorCode = (typeof CATALOG_ERROR_CODES)[number];
26
+
27
+ const identifier = z.string().min(1).max(512).refine(value => !/[\s\u0000-\u001f\u007f]/.test(value) && !SECRET.test(value));
28
+ const CONTROL = /[\u0000-\u001f\u007f]/;
29
+ const WINDOWS_DRIVE = /^[A-Za-z]:[\\/]/;
30
+
31
+ /** Absolute in the path's own format. Hub `path.resolve` is not used: on a
32
+ * POSIX Hub it treats `C:\project` as a relative path under the process cwd. */
33
+ function windowsStrictAbsolute(value: string): boolean {
34
+ if (!WINDOWS_DRIVE.test(value)) return false;
35
+ const parts = value.slice(2).split(/[\\/]/);
36
+ if (parts[0] !== "" || parts.length < 2) return false;
37
+ return parts.slice(1).every(part => part !== "" && part !== "." && part !== "..");
38
+ }
39
+
40
+ export function strictNativeAbsoluteDirectory(value: string): boolean {
41
+ if (value.length < 1 || value.length > 1024 || CONTROL.test(value)) return false;
42
+ if (WINDOWS_DRIVE.test(value)) return windowsStrictAbsolute(value);
43
+ if (value.startsWith("/")) return posix.resolve(value) === value;
44
+ return false;
45
+ }
46
+
47
+ function tildeWorkdir(value: string): boolean {
48
+ const trimmed = value.trim();
49
+ return trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\");
50
+ }
51
+
52
+ /** Compare a stored create workdir with the directory the daemon reported.
53
+ * Windows keys ignore slash style and case. POSIX keys are lexical (`..` and
54
+ * a trailing slash collapse) and are not realpath'd — Hub does not see the
55
+ * daemon disk. A `~` / `~/…` workdir has no Hub-side home to expand; a
56
+ * native-absolute report is accepted because the daemon already refused the
57
+ * read unless that path is the endpoint it owns. A compromised daemon can
58
+ * name another absolute path for a tilde row, same as a null workdir. */
59
+ export function catalogDirectoriesMatch(stored: string, reported: string): boolean {
60
+ if (!strictNativeAbsoluteDirectory(reported)) return false;
61
+ const storedTrim = stored.trim();
62
+ if (tildeWorkdir(storedTrim)) return true;
63
+ const storedWindows = WINDOWS_DRIVE.test(storedTrim);
64
+ const reportedWindows = WINDOWS_DRIVE.test(reported);
65
+ if (storedWindows !== reportedWindows) return false;
66
+ if (storedWindows) {
67
+ const left = win32.resolve(storedTrim);
68
+ const right = win32.resolve(reported);
69
+ return /^[A-Za-z]:\\/.test(left) && left.toLowerCase() === right.toLowerCase();
70
+ }
71
+ if (storedTrim.startsWith("/") && reported.startsWith("/")) return posix.resolve(storedTrim) === posix.resolve(reported);
72
+ return false;
73
+ }
74
+
75
+ const directoryText = z.string().min(1).max(1024).refine(strictNativeAbsoluteDirectory);
76
+
77
+ export const opencodeCatalogSnapshotSchema = z.object({
78
+ scope: z.object({
79
+ networkId: z.string().min(1).max(200),
80
+ daemonId: z.string().min(1).max(200),
81
+ directory: directoryText,
82
+ }).strict(),
83
+ source: z.literal("opencode-v2-api"),
84
+ state: z.literal("snapshot"),
85
+ invocationVerified: z.literal(false),
86
+ completeness: z.literal("unknown"),
87
+ observedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/),
88
+ providers: z.array(z.object({
89
+ id: identifier,
90
+ activation: z.enum(["auto", "enabled", "disabled"]),
91
+ }).strict()).max(20_000),
92
+ models: z.array(z.object({
93
+ providerId: identifier,
94
+ id: identifier,
95
+ }).strict()).max(20_000),
96
+ defaultModel: z.object({ providerId: identifier, id: identifier }).strict().nullable(),
97
+ }).strict();
98
+
99
+ export type OpenCodeCatalogSnapshotWire = z.infer<typeof opencodeCatalogSnapshotSchema>;
100
+
101
+ export const opencodeCatalogAckSchema = z.object({
102
+ request_id: z.string().min(1).max(200),
103
+ error: z.enum(CATALOG_ERROR_CODES).optional(),
104
+ snapshot: opencodeCatalogSnapshotSchema.optional(),
105
+ }).strict();
106
+
107
+ interface PendingCatalogRead {
108
+ requestId: string;
109
+ daemonNodeId: string;
110
+ daemonAlias: string;
111
+ networkId: string;
112
+ nodeId: string;
113
+ alias: string;
114
+ directory: string | null;
115
+ expiresAt: number;
116
+ taken: boolean;
117
+ }
118
+
119
+ export interface StoredCatalogRead {
120
+ requestId: string;
121
+ networkId: string;
122
+ daemonNodeId: string;
123
+ nodeId: string;
124
+ status: "pending" | "ready" | "failed" | "expired";
125
+ error?: CatalogErrorCode;
126
+ snapshot?: OpenCodeCatalogSnapshotWire;
127
+ expiresAt: number;
128
+ }
129
+
130
+ const pending = new Map<string, PendingCatalogRead>();
131
+ const stored = new Map<string, StoredCatalogRead>();
132
+
133
+ export function resetOpenCodeCatalogForTest(): void {
134
+ pending.clear();
135
+ stored.clear();
136
+ }
137
+
138
+ export function expireOpenCodeCatalogForTest(requestId: string): void {
139
+ // Just past the read deadline, still inside the retention window. Setting
140
+ // the deadline to 0 would also drop the stored row and look like not_found.
141
+ const expiredAt = Date.now() - 1;
142
+ const row = pending.get(requestId);
143
+ if (row) row.expiresAt = expiredAt;
144
+ const result = stored.get(requestId);
145
+ if (result) result.expiresAt = expiredAt;
146
+ }
147
+
148
+ function childNodeId(requestId: string): string | null {
149
+ return requestId.startsWith("cr_") ? `node_${requestId.slice(3)}` : null;
150
+ }
151
+
152
+ function supervisorRole(snapshot: string | null): boolean {
153
+ if (!snapshot) return false;
154
+ try {
155
+ const parsed = JSON.parse(snapshot) as { role?: unknown };
156
+ return parsed.role === "host_supervisor";
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
162
+ function v2Flags(flagsJson: string): boolean {
163
+ try {
164
+ const flags = JSON.parse(flagsJson) as { opencodeGeneration?: unknown; opencodeUnsafeTools?: unknown };
165
+ return flags.opencodeGeneration === "v2" && flags.opencodeUnsafeTools === true;
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ export interface QueueCatalogInput {
172
+ callerUserId: string | null;
173
+ callerIsNode: boolean;
174
+ networkId: string | null;
175
+ daemonNodeId: string;
176
+ nodeId: string;
177
+ }
178
+
179
+ function evictExpired(now = Date.now()): void {
180
+ for (const [id, row] of pending) {
181
+ if (now > row.expiresAt) pending.delete(id);
182
+ }
183
+ for (const [id, row] of stored) {
184
+ if (row.status === "pending" && now > row.expiresAt) row.status = "expired";
185
+ if (now > row.expiresAt + TTL_MS) stored.delete(id);
186
+ }
187
+ }
188
+
189
+ export function queueOpenCodeCatalogRead(input: QueueCatalogInput):
190
+ | { ok: true; request_id: string; status: "pending"; daemon_alias: string; network_id: string }
191
+ | { ok: false; error: string } {
192
+ evictExpired();
193
+ if (input.callerIsNode) return { ok: false, error: "user_token_required" };
194
+ if (!input.callerUserId) return { ok: false, error: "auth_required" };
195
+ if (!input.networkId) return { ok: false, error: "network_id_required" };
196
+ const role = getUserNetworkRole(input.callerUserId, input.networkId);
197
+ if (!role) return { ok: false, error: "access_denied" };
198
+ if (pending.size >= MAX_PENDING) return { ok: false, error: "catalog_busy" };
199
+
200
+ const daemon = db.get<{ node_id: string; alias: string; config_snapshot: string | null }>(
201
+ `SELECT node_id, alias, config_snapshot FROM nodes WHERE node_id = ?1 AND network_id = ?2`,
202
+ input.daemonNodeId, input.networkId,
203
+ );
204
+ if (!daemon) return { ok: false, error: "daemon_not_found" };
205
+ if (!supervisorRole(daemon.config_snapshot)) return { ok: false, error: "daemon_not_supervisor" };
206
+
207
+ const child = db.get<{ node_id: string }>(
208
+ `SELECT node_id FROM nodes WHERE node_id = ?1 AND network_id = ?2`,
209
+ input.nodeId, input.networkId,
210
+ );
211
+ if (!child) return { ok: false, error: "node_not_found" };
212
+
213
+ const creates = db.all<{
214
+ request_id: string; daemon_node_id: string; child_name: string; runtime: string;
215
+ flags_json: string; workdir: string | null; status: string;
216
+ }>(
217
+ `SELECT request_id, daemon_node_id, child_name, runtime, flags_json, workdir, status
218
+ FROM node_create_requests
219
+ WHERE network_id = ?1 AND daemon_node_id = ?2 AND status IN ('succeeded', 'delivered')`,
220
+ input.networkId, input.daemonNodeId,
221
+ );
222
+ const owned = creates.filter(row => childNodeId(row.request_id) === input.nodeId && row.daemon_node_id === input.daemonNodeId);
223
+ if (owned.length !== 1) return { ok: false, error: "node_not_found" };
224
+ const row = owned[0]!;
225
+ if (row.runtime !== "opencode-cli" || !v2Flags(row.flags_json)) return { ok: false, error: "not_opencode_v2" };
226
+
227
+ const requestId = `cat_${uuidv4()}`;
228
+ const directory = typeof row.workdir === "string" && row.workdir.length > 0 ? row.workdir : null;
229
+ const record: PendingCatalogRead = {
230
+ requestId,
231
+ daemonNodeId: daemon.node_id,
232
+ daemonAlias: daemon.alias,
233
+ networkId: input.networkId,
234
+ nodeId: input.nodeId,
235
+ alias: row.child_name,
236
+ directory,
237
+ expiresAt: Date.now() + TTL_MS,
238
+ taken: false,
239
+ };
240
+ pending.set(requestId, record);
241
+ stored.set(requestId, {
242
+ requestId,
243
+ networkId: input.networkId,
244
+ daemonNodeId: daemon.node_id,
245
+ nodeId: input.nodeId,
246
+ status: "pending",
247
+ expiresAt: record.expiresAt,
248
+ });
249
+ return { ok: true, request_id: requestId, status: "pending", daemon_alias: daemon.alias, network_id: input.networkId };
250
+ }
251
+
252
+ export function takeOpenCodeCatalogRequest(requestId: string, daemonNodeId: string):
253
+ | { ok: true; request_id: string; node_id: string; alias: string; directory: string | null; scope: { networkId: string; daemonId: string } }
254
+ | { ok: false; error: "catalog_request_unavailable" } {
255
+ const row = pending.get(requestId);
256
+ if (!row || row.daemonNodeId !== daemonNodeId || row.taken) return { ok: false, error: "catalog_request_unavailable" };
257
+ if (Date.now() > row.expiresAt) {
258
+ row.taken = true;
259
+ const result = stored.get(requestId);
260
+ if (result && result.status === "pending") result.status = "expired";
261
+ return { ok: false, error: "catalog_request_unavailable" };
262
+ }
263
+ row.taken = true;
264
+ return {
265
+ ok: true,
266
+ request_id: row.requestId,
267
+ node_id: row.nodeId,
268
+ alias: row.alias,
269
+ directory: row.directory,
270
+ scope: { networkId: row.networkId, daemonId: row.daemonNodeId },
271
+ };
272
+ }
273
+
274
+ function consistent(snapshot: OpenCodeCatalogSnapshotWire): boolean {
275
+ const providers = new Set<string>();
276
+ for (const row of snapshot.providers) {
277
+ if (providers.has(row.id)) return false;
278
+ providers.add(row.id);
279
+ }
280
+ const models = new Set<string>();
281
+ for (const row of snapshot.models) {
282
+ if (!providers.has(row.providerId)) return false;
283
+ const key = JSON.stringify([row.providerId, row.id]);
284
+ if (models.has(key)) return false;
285
+ models.add(key);
286
+ }
287
+ if (snapshot.defaultModel && !models.has(JSON.stringify([snapshot.defaultModel.providerId, snapshot.defaultModel.id]))) return false;
288
+ return Buffer.byteLength(JSON.stringify(snapshot), "utf8") <= 2 * 1024 * 1024;
289
+ }
290
+
291
+ export function ackOpenCodeCatalog(daemonNodeId: string, body: unknown):
292
+ | { ok: true; status: "ready" | "failed"; error?: CatalogErrorCode }
293
+ | { ok: false; error: "catalog_request_unavailable" | "invalid_ack" } {
294
+ const parsed = opencodeCatalogAckSchema.safeParse(body);
295
+ if (!parsed.success) return { ok: false, error: "invalid_ack" };
296
+ const ack = parsed.data;
297
+ const hasSnapshot = ack.snapshot !== undefined;
298
+ const hasError = ack.error !== undefined;
299
+ if (hasSnapshot === hasError) return { ok: false, error: "invalid_ack" };
300
+ const row = pending.get(ack.request_id);
301
+ const result = stored.get(ack.request_id);
302
+ if (!row || !result || row.daemonNodeId !== daemonNodeId || !row.taken) {
303
+ return { ok: false, error: "catalog_request_unavailable" };
304
+ }
305
+ if (Date.now() > row.expiresAt || result.status !== "pending") {
306
+ if (result.status === "pending") result.status = "expired";
307
+ return { ok: false, error: "catalog_request_unavailable" };
308
+ }
309
+ if (ack.error) {
310
+ result.status = "failed";
311
+ result.error = ack.error;
312
+ return { ok: true, status: "failed", error: ack.error };
313
+ }
314
+ const snapshot = ack.snapshot!;
315
+ // A stored workdir must match. A null workdir (legacy create) has no
316
+ // hub-side path to compare; the daemon binds that read to the project
317
+ // directory it already owns. Hub does not see that filesystem.
318
+ const directoryOk = row.directory ? catalogDirectoriesMatch(row.directory, snapshot.scope.directory) : true;
319
+ if (snapshot.scope.networkId !== row.networkId || snapshot.scope.daemonId !== row.daemonNodeId || !directoryOk || !consistent(snapshot)) {
320
+ result.status = "failed";
321
+ result.error = "invalid_response";
322
+ return { ok: true, status: "failed", error: "invalid_response" };
323
+ }
324
+ result.status = "ready";
325
+ result.snapshot = snapshot;
326
+ return { ok: true, status: "ready" };
327
+ }
328
+
329
+ export function getOpenCodeCatalogResult(input: {
330
+ callerUserId: string | null;
331
+ callerIsNode: boolean;
332
+ networkId: string | null;
333
+ requestId: string;
334
+ }):
335
+ | { ok: true; status: "pending"; request_id: string }
336
+ | { ok: true; status: "ready"; request_id: string; snapshot: OpenCodeCatalogSnapshotWire }
337
+ | { ok: false; status?: "failed" | "expired"; error: string; request_id?: string } {
338
+ if (input.callerIsNode) return { ok: false, error: "user_token_required" };
339
+ if (!input.callerUserId) return { ok: false, error: "auth_required" };
340
+ if (!input.networkId) return { ok: false, error: "network_id_required" };
341
+ if (!getUserNetworkRole(input.callerUserId, input.networkId)) return { ok: false, error: "access_denied" };
342
+ evictExpired();
343
+ const result = stored.get(input.requestId);
344
+ if (!result || result.networkId !== input.networkId) return { ok: false, error: "not_found" };
345
+ if (result.status === "pending" && Date.now() > result.expiresAt) result.status = "expired";
346
+ if (result.status === "pending") return { ok: true, status: "pending", request_id: result.requestId };
347
+ if (result.status === "ready" && result.snapshot) {
348
+ return { ok: true, status: "ready", request_id: result.requestId, snapshot: result.snapshot };
349
+ }
350
+ if (result.status === "expired") return { ok: false, status: "expired", error: "unavailable", request_id: result.requestId };
351
+ return { ok: false, status: "failed", error: result.error || "unavailable", request_id: result.requestId };
352
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ type PackageManifest = { version: string };
6
+ type PackageLock = {
7
+ version: string;
8
+ packages: { "": { version: string } };
9
+ };
10
+
11
+ const readJson = <T>(name: string): T =>
12
+ JSON.parse(readFileSync(join(import.meta.dir, "..", name), "utf8")) as T;
13
+
14
+ describe("server package version consistency", () => {
15
+ const manifest = readJson<PackageManifest>("package.json");
16
+ const lock = readJson<PackageLock>("package-lock.json");
17
+
18
+ test("package-lock top-level version matches package.json", () => {
19
+ expect(lock.version).toBe(manifest.version);
20
+ });
21
+
22
+ test("package-lock root package version matches package.json", () => {
23
+ expect(lock.packages[""].version).toBe(manifest.version);
24
+ });
25
+ });
@@ -0,0 +1,25 @@
1
+ import { db } from "./db.js";
2
+
3
+ export type KeptNetworkToken = {
4
+ token_id: string;
5
+ name: string;
6
+ network_id: string;
7
+ created_at: string;
8
+ last_used_at: string | null;
9
+ };
10
+
11
+ /** Only user-created named network tokens. Node credentials have a different scope or binding. */
12
+ export function passwordNetworkTokens(userId: string, currentTokenId: string | null | undefined, revoke: boolean) {
13
+ const where = `user_id = ?1 AND network_id IS NOT NULL AND scope = 'full'
14
+ AND bound_node_id IS NULL AND revoked_at IS NULL
15
+ AND (expires_at IS NULL OR expires_at > datetime('now'))${currentTokenId ? " AND token_id != ?2" : ""}`;
16
+ const params = currentTokenId ? [userId, currentTokenId] : [userId];
17
+ if (revoke) {
18
+ return { revoked: db.run(`DELETE FROM api_tokens WHERE ${where}`, params).changes, kept: [] as KeptNetworkToken[] };
19
+ }
20
+ const kept = db.all<KeptNetworkToken>(
21
+ `SELECT token_id, name, network_id, created_at, last_used_at FROM api_tokens WHERE ${where} ORDER BY created_at DESC, token_id`,
22
+ ...params,
23
+ );
24
+ return { revoked: 0, kept };
25
+ }