@antasphere/chassis-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (798) hide show
  1. package/LICENSE +82 -0
  2. package/dist/accounts/deletion.d.ts +163 -0
  3. package/dist/accounts/deletion.d.ts.map +1 -0
  4. package/dist/accounts/deletion.js +360 -0
  5. package/dist/accounts/deletion.js.map +1 -0
  6. package/dist/accounts/erasure-log.d.ts +66 -0
  7. package/dist/accounts/erasure-log.d.ts.map +1 -0
  8. package/dist/accounts/erasure-log.js +117 -0
  9. package/dist/accounts/erasure-log.js.map +1 -0
  10. package/dist/accounts/index.d.ts +6 -0
  11. package/dist/accounts/index.d.ts.map +1 -0
  12. package/dist/accounts/index.js +6 -0
  13. package/dist/accounts/index.js.map +1 -0
  14. package/dist/accounts/mint-refusal.d.ts +47 -0
  15. package/dist/accounts/mint-refusal.d.ts.map +1 -0
  16. package/dist/accounts/mint-refusal.js +59 -0
  17. package/dist/accounts/mint-refusal.js.map +1 -0
  18. package/dist/accounts/signup-duplicate.d.ts +14 -0
  19. package/dist/accounts/signup-duplicate.d.ts.map +1 -0
  20. package/dist/accounts/signup-duplicate.js +25 -0
  21. package/dist/accounts/signup-duplicate.js.map +1 -0
  22. package/dist/accounts/superadmin.d.ts +27 -0
  23. package/dist/accounts/superadmin.d.ts.map +1 -0
  24. package/dist/accounts/superadmin.js +46 -0
  25. package/dist/accounts/superadmin.js.map +1 -0
  26. package/dist/api/apikeys.d.ts +14 -0
  27. package/dist/api/apikeys.d.ts.map +1 -0
  28. package/dist/api/apikeys.js +124 -0
  29. package/dist/api/apikeys.js.map +1 -0
  30. package/dist/api/audit.d.ts +4 -0
  31. package/dist/api/audit.d.ts.map +1 -0
  32. package/dist/api/audit.js +75 -0
  33. package/dist/api/audit.js.map +1 -0
  34. package/dist/api/break-glass.d.ts +48 -0
  35. package/dist/api/break-glass.d.ts.map +1 -0
  36. package/dist/api/break-glass.js +217 -0
  37. package/dist/api/break-glass.js.map +1 -0
  38. package/dist/api/cli-auth.d.ts +84 -0
  39. package/dist/api/cli-auth.d.ts.map +1 -0
  40. package/dist/api/cli-auth.js +177 -0
  41. package/dist/api/cli-auth.js.map +1 -0
  42. package/dist/api/create-api.d.ts +123 -0
  43. package/dist/api/create-api.d.ts.map +1 -0
  44. package/dist/api/create-api.js +1165 -0
  45. package/dist/api/create-api.js.map +1 -0
  46. package/dist/api/demo-passes.d.ts +45 -0
  47. package/dist/api/demo-passes.d.ts.map +1 -0
  48. package/dist/api/demo-passes.js +143 -0
  49. package/dist/api/demo-passes.js.map +1 -0
  50. package/dist/api/errors.d.ts +5 -0
  51. package/dist/api/errors.d.ts.map +1 -0
  52. package/dist/api/errors.js +5 -0
  53. package/dist/api/errors.js.map +1 -0
  54. package/dist/api/export.d.ts +63 -0
  55. package/dist/api/export.d.ts.map +1 -0
  56. package/dist/api/export.js +393 -0
  57. package/dist/api/export.js.map +1 -0
  58. package/dist/api/files.d.ts +40 -0
  59. package/dist/api/files.d.ts.map +1 -0
  60. package/dist/api/files.js +150 -0
  61. package/dist/api/files.js.map +1 -0
  62. package/dist/api/index.d.ts +17 -0
  63. package/dist/api/index.d.ts.map +1 -0
  64. package/dist/api/index.js +17 -0
  65. package/dist/api/index.js.map +1 -0
  66. package/dist/api/invitations.d.ts +50 -0
  67. package/dist/api/invitations.d.ts.map +1 -0
  68. package/dist/api/invitations.js +313 -0
  69. package/dist/api/invitations.js.map +1 -0
  70. package/dist/api/members.d.ts +25 -0
  71. package/dist/api/members.d.ts.map +1 -0
  72. package/dist/api/members.js +522 -0
  73. package/dist/api/members.js.map +1 -0
  74. package/dist/api/onboarding.d.ts +26 -0
  75. package/dist/api/onboarding.d.ts.map +1 -0
  76. package/dist/api/onboarding.js +23 -0
  77. package/dist/api/onboarding.js.map +1 -0
  78. package/dist/api/openapi-doc.d.ts +21 -0
  79. package/dist/api/openapi-doc.d.ts.map +1 -0
  80. package/dist/api/openapi-doc.js +25 -0
  81. package/dist/api/openapi-doc.js.map +1 -0
  82. package/dist/api/projects.d.ts +24 -0
  83. package/dist/api/projects.d.ts.map +1 -0
  84. package/dist/api/projects.js +624 -0
  85. package/dist/api/projects.js.map +1 -0
  86. package/dist/api/scope-routes.d.ts +12 -0
  87. package/dist/api/scope-routes.d.ts.map +1 -0
  88. package/dist/api/scope-routes.js +2 -0
  89. package/dist/api/scope-routes.js.map +1 -0
  90. package/dist/api/sso-connect.d.ts +70 -0
  91. package/dist/api/sso-connect.d.ts.map +1 -0
  92. package/dist/api/sso-connect.js +164 -0
  93. package/dist/api/sso-connect.js.map +1 -0
  94. package/dist/api/sso-logout.d.ts +41 -0
  95. package/dist/api/sso-logout.d.ts.map +1 -0
  96. package/dist/api/sso-logout.js +34 -0
  97. package/dist/api/sso-logout.js.map +1 -0
  98. package/dist/api/teams.d.ts +43 -0
  99. package/dist/api/teams.d.ts.map +1 -0
  100. package/dist/api/teams.js +447 -0
  101. package/dist/api/teams.js.map +1 -0
  102. package/dist/api/workspaces.d.ts +114 -0
  103. package/dist/api/workspaces.d.ts.map +1 -0
  104. package/dist/api/workspaces.js +462 -0
  105. package/dist/api/workspaces.js.map +1 -0
  106. package/dist/apikeys/index.d.ts +3 -0
  107. package/dist/apikeys/index.d.ts.map +1 -0
  108. package/dist/apikeys/index.js +3 -0
  109. package/dist/apikeys/index.js.map +1 -0
  110. package/dist/apikeys/peppers.d.ts +53 -0
  111. package/dist/apikeys/peppers.d.ts.map +1 -0
  112. package/dist/apikeys/peppers.js +97 -0
  113. package/dist/apikeys/peppers.js.map +1 -0
  114. package/dist/apikeys/service.d.ts +56 -0
  115. package/dist/apikeys/service.d.ts.map +1 -0
  116. package/dist/apikeys/service.js +196 -0
  117. package/dist/apikeys/service.js.map +1 -0
  118. package/dist/app.d.ts +65 -0
  119. package/dist/app.d.ts.map +1 -0
  120. package/dist/app.js +142 -0
  121. package/dist/app.js.map +1 -0
  122. package/dist/audit/filters.d.ts +38 -0
  123. package/dist/audit/filters.d.ts.map +1 -0
  124. package/dist/audit/filters.js +97 -0
  125. package/dist/audit/filters.js.map +1 -0
  126. package/dist/audit/index.d.ts +3 -0
  127. package/dist/audit/index.d.ts.map +1 -0
  128. package/dist/audit/index.js +3 -0
  129. package/dist/audit/index.js.map +1 -0
  130. package/dist/audit/service.d.ts +65 -0
  131. package/dist/audit/service.d.ts.map +1 -0
  132. package/dist/audit/service.js +103 -0
  133. package/dist/audit/service.js.map +1 -0
  134. package/dist/boot.d.ts +8 -0
  135. package/dist/boot.d.ts.map +1 -0
  136. package/dist/boot.js +822 -0
  137. package/dist/boot.js.map +1 -0
  138. package/dist/constant-time.d.ts +3 -0
  139. package/dist/constant-time.d.ts.map +1 -0
  140. package/dist/constant-time.js +10 -0
  141. package/dist/constant-time.js.map +1 -0
  142. package/dist/email/driver.d.ts +16 -0
  143. package/dist/email/driver.d.ts.map +1 -0
  144. package/dist/email/driver.js +136 -0
  145. package/dist/email/driver.js.map +1 -0
  146. package/dist/email/index.d.ts +4 -0
  147. package/dist/email/index.d.ts.map +1 -0
  148. package/dist/email/index.js +4 -0
  149. package/dist/email/index.js.map +1 -0
  150. package/dist/email/shell.d.ts +72 -0
  151. package/dist/email/shell.d.ts.map +1 -0
  152. package/dist/email/shell.js +146 -0
  153. package/dist/email/shell.js.map +1 -0
  154. package/dist/email/templates.d.ts +83 -0
  155. package/dist/email/templates.d.ts.map +1 -0
  156. package/dist/email/templates.js +96 -0
  157. package/dist/email/templates.js.map +1 -0
  158. package/dist/entitlements/check.d.ts +140 -0
  159. package/dist/entitlements/check.d.ts.map +1 -0
  160. package/dist/entitlements/check.js +276 -0
  161. package/dist/entitlements/check.js.map +1 -0
  162. package/dist/entitlements/clock-skew.d.ts +27 -0
  163. package/dist/entitlements/clock-skew.d.ts.map +1 -0
  164. package/dist/entitlements/clock-skew.js +58 -0
  165. package/dist/entitlements/clock-skew.js.map +1 -0
  166. package/dist/entitlements/gate.d.ts +113 -0
  167. package/dist/entitlements/gate.d.ts.map +1 -0
  168. package/dist/entitlements/gate.js +430 -0
  169. package/dist/entitlements/gate.js.map +1 -0
  170. package/dist/entitlements/hub-machine-token.d.ts +83 -0
  171. package/dist/entitlements/hub-machine-token.d.ts.map +1 -0
  172. package/dist/entitlements/hub-machine-token.js +175 -0
  173. package/dist/entitlements/hub-machine-token.js.map +1 -0
  174. package/dist/entitlements/hub-subject.d.ts +32 -0
  175. package/dist/entitlements/hub-subject.d.ts.map +1 -0
  176. package/dist/entitlements/hub-subject.js +68 -0
  177. package/dist/entitlements/hub-subject.js.map +1 -0
  178. package/dist/entitlements/index.d.ts +9 -0
  179. package/dist/entitlements/index.d.ts.map +1 -0
  180. package/dist/entitlements/index.js +9 -0
  181. package/dist/entitlements/index.js.map +1 -0
  182. package/dist/entitlements/poster.d.ts +81 -0
  183. package/dist/entitlements/poster.d.ts.map +1 -0
  184. package/dist/entitlements/poster.js +169 -0
  185. package/dist/entitlements/poster.js.map +1 -0
  186. package/dist/entitlements/profiles.d.ts +121 -0
  187. package/dist/entitlements/profiles.d.ts.map +1 -0
  188. package/dist/entitlements/profiles.js +212 -0
  189. package/dist/entitlements/profiles.js.map +1 -0
  190. package/dist/entitlements/slot.d.ts +33 -0
  191. package/dist/entitlements/slot.d.ts.map +1 -0
  192. package/dist/entitlements/slot.js +75 -0
  193. package/dist/entitlements/slot.js.map +1 -0
  194. package/dist/env.d.ts +293 -0
  195. package/dist/env.d.ts.map +1 -0
  196. package/dist/env.js +383 -0
  197. package/dist/env.js.map +1 -0
  198. package/dist/files/http.d.ts +23 -0
  199. package/dist/files/http.d.ts.map +1 -0
  200. package/dist/files/http.js +66 -0
  201. package/dist/files/http.js.map +1 -0
  202. package/dist/files/index.d.ts +5 -0
  203. package/dist/files/index.d.ts.map +1 -0
  204. package/dist/files/index.js +5 -0
  205. package/dist/files/index.js.map +1 -0
  206. package/dist/files/serve.d.ts +43 -0
  207. package/dist/files/serve.d.ts.map +1 -0
  208. package/dist/files/serve.js +71 -0
  209. package/dist/files/serve.js.map +1 -0
  210. package/dist/files/service.d.ts +69 -0
  211. package/dist/files/service.d.ts.map +1 -0
  212. package/dist/files/service.js +188 -0
  213. package/dist/files/service.js.map +1 -0
  214. package/dist/files/spool.d.ts +21 -0
  215. package/dist/files/spool.d.ts.map +1 -0
  216. package/dist/files/spool.js +47 -0
  217. package/dist/files/spool.js.map +1 -0
  218. package/dist/identity/better-auth.d.ts +3927 -0
  219. package/dist/identity/better-auth.d.ts.map +1 -0
  220. package/dist/identity/better-auth.js +896 -0
  221. package/dist/identity/better-auth.js.map +1 -0
  222. package/dist/identity/demo-pass-plugin.d.ts +29 -0
  223. package/dist/identity/demo-pass-plugin.d.ts.map +1 -0
  224. package/dist/identity/demo-pass-plugin.js +88 -0
  225. package/dist/identity/demo-pass-plugin.js.map +1 -0
  226. package/dist/identity/demo-pass-rules.d.ts +84 -0
  227. package/dist/identity/demo-pass-rules.d.ts.map +1 -0
  228. package/dist/identity/demo-pass-rules.js +223 -0
  229. package/dist/identity/demo-pass-rules.js.map +1 -0
  230. package/dist/identity/demo-pass.d.ts +159 -0
  231. package/dist/identity/demo-pass.d.ts.map +1 -0
  232. package/dist/identity/demo-pass.js +319 -0
  233. package/dist/identity/demo-pass.js.map +1 -0
  234. package/dist/identity/hub-grant.d.ts +182 -0
  235. package/dist/identity/hub-grant.d.ts.map +1 -0
  236. package/dist/identity/hub-grant.js +594 -0
  237. package/dist/identity/hub-grant.js.map +1 -0
  238. package/dist/identity/hub-jwt.d.ts +22 -0
  239. package/dist/identity/hub-jwt.d.ts.map +1 -0
  240. package/dist/identity/hub-jwt.js +77 -0
  241. package/dist/identity/hub-jwt.js.map +1 -0
  242. package/dist/identity/hub-live-gate.d.ts +17 -0
  243. package/dist/identity/hub-live-gate.d.ts.map +1 -0
  244. package/dist/identity/hub-live-gate.js +146 -0
  245. package/dist/identity/hub-live-gate.js.map +1 -0
  246. package/dist/identity/hub-logout.d.ts +88 -0
  247. package/dist/identity/hub-logout.d.ts.map +1 -0
  248. package/dist/identity/hub-logout.js +158 -0
  249. package/dist/identity/hub-logout.js.map +1 -0
  250. package/dist/identity/hub-projection.d.ts +103 -0
  251. package/dist/identity/hub-projection.d.ts.map +1 -0
  252. package/dist/identity/hub-projection.js +130 -0
  253. package/dist/identity/hub-projection.js.map +1 -0
  254. package/dist/identity/hub-reconcile.d.ts +151 -0
  255. package/dist/identity/hub-reconcile.d.ts.map +1 -0
  256. package/dist/identity/hub-reconcile.js +400 -0
  257. package/dist/identity/hub-reconcile.js.map +1 -0
  258. package/dist/identity/hub-sso.d.ts +363 -0
  259. package/dist/identity/hub-sso.d.ts.map +1 -0
  260. package/dist/identity/hub-sso.js +536 -0
  261. package/dist/identity/hub-sso.js.map +1 -0
  262. package/dist/identity/hub-user-client.d.ts +222 -0
  263. package/dist/identity/hub-user-client.d.ts.map +1 -0
  264. package/dist/identity/hub-user-client.js +406 -0
  265. package/dist/identity/hub-user-client.js.map +1 -0
  266. package/dist/identity/index.d.ts +13 -0
  267. package/dist/identity/index.d.ts.map +1 -0
  268. package/dist/identity/index.js +13 -0
  269. package/dist/identity/index.js.map +1 -0
  270. package/dist/identity/oauth-jwt.d.ts +32 -0
  271. package/dist/identity/oauth-jwt.d.ts.map +1 -0
  272. package/dist/identity/oauth-jwt.js +122 -0
  273. package/dist/identity/oauth-jwt.js.map +1 -0
  274. package/dist/identity/resolve-membership.d.ts +28 -0
  275. package/dist/identity/resolve-membership.d.ts.map +1 -0
  276. package/dist/identity/resolve-membership.js +69 -0
  277. package/dist/identity/resolve-membership.js.map +1 -0
  278. package/dist/identity/signing-key.d.ts +49 -0
  279. package/dist/identity/signing-key.d.ts.map +1 -0
  280. package/dist/identity/signing-key.js +144 -0
  281. package/dist/identity/signing-key.js.map +1 -0
  282. package/dist/index.d.ts +21 -0
  283. package/dist/index.d.ts.map +1 -0
  284. package/dist/index.js +11 -0
  285. package/dist/index.js.map +1 -0
  286. package/dist/invitations/index.d.ts +2 -0
  287. package/dist/invitations/index.d.ts.map +1 -0
  288. package/dist/invitations/index.js +2 -0
  289. package/dist/invitations/index.js.map +1 -0
  290. package/dist/invitations/service.d.ts +36 -0
  291. package/dist/invitations/service.d.ts.map +1 -0
  292. package/dist/invitations/service.js +122 -0
  293. package/dist/invitations/service.js.map +1 -0
  294. package/dist/jobs/index.d.ts +3 -0
  295. package/dist/jobs/index.d.ts.map +1 -0
  296. package/dist/jobs/index.js +3 -0
  297. package/dist/jobs/index.js.map +1 -0
  298. package/dist/jobs/pgboss.d.ts +119 -0
  299. package/dist/jobs/pgboss.d.ts.map +1 -0
  300. package/dist/jobs/pgboss.js +521 -0
  301. package/dist/jobs/pgboss.js.map +1 -0
  302. package/dist/jobs/timers.d.ts +127 -0
  303. package/dist/jobs/timers.d.ts.map +1 -0
  304. package/dist/jobs/timers.js +228 -0
  305. package/dist/jobs/timers.js.map +1 -0
  306. package/dist/logger.d.ts +17 -0
  307. package/dist/logger.d.ts.map +1 -0
  308. package/dist/logger.js +64 -0
  309. package/dist/logger.js.map +1 -0
  310. package/dist/mcp/errors.d.ts +37 -0
  311. package/dist/mcp/errors.d.ts.map +1 -0
  312. package/dist/mcp/errors.js +77 -0
  313. package/dist/mcp/errors.js.map +1 -0
  314. package/dist/mcp/http.d.ts +35 -0
  315. package/dist/mcp/http.d.ts.map +1 -0
  316. package/dist/mcp/http.js +80 -0
  317. package/dist/mcp/http.js.map +1 -0
  318. package/dist/mcp/index.d.ts +8 -0
  319. package/dist/mcp/index.d.ts.map +1 -0
  320. package/dist/mcp/index.js +8 -0
  321. package/dist/mcp/index.js.map +1 -0
  322. package/dist/mcp/inputs.d.ts +19 -0
  323. package/dist/mcp/inputs.d.ts.map +1 -0
  324. package/dist/mcp/inputs.js +28 -0
  325. package/dist/mcp/inputs.js.map +1 -0
  326. package/dist/mcp/projects.d.ts +31 -0
  327. package/dist/mcp/projects.d.ts.map +1 -0
  328. package/dist/mcp/projects.js +315 -0
  329. package/dist/mcp/projects.js.map +1 -0
  330. package/dist/mcp/server.d.ts +52 -0
  331. package/dist/mcp/server.d.ts.map +1 -0
  332. package/dist/mcp/server.js +93 -0
  333. package/dist/mcp/server.js.map +1 -0
  334. package/dist/mcp/teams.d.ts +7 -0
  335. package/dist/mcp/teams.d.ts.map +1 -0
  336. package/dist/mcp/teams.js +62 -0
  337. package/dist/mcp/teams.js.map +1 -0
  338. package/dist/mcp/tool-kit.d.ts +67 -0
  339. package/dist/mcp/tool-kit.d.ts.map +1 -0
  340. package/dist/mcp/tool-kit.js +74 -0
  341. package/dist/mcp/tool-kit.js.map +1 -0
  342. package/dist/members/removal.d.ts +95 -0
  343. package/dist/members/removal.d.ts.map +1 -0
  344. package/dist/members/removal.js +143 -0
  345. package/dist/members/removal.js.map +1 -0
  346. package/dist/middleware/auth-body.d.ts +25 -0
  347. package/dist/middleware/auth-body.d.ts.map +1 -0
  348. package/dist/middleware/auth-body.js +55 -0
  349. package/dist/middleware/auth-body.js.map +1 -0
  350. package/dist/middleware/auth-context.d.ts +170 -0
  351. package/dist/middleware/auth-context.d.ts.map +1 -0
  352. package/dist/middleware/auth-context.js +251 -0
  353. package/dist/middleware/auth-context.js.map +1 -0
  354. package/dist/middleware/body-cap.d.ts +17 -0
  355. package/dist/middleware/body-cap.d.ts.map +1 -0
  356. package/dist/middleware/body-cap.js +31 -0
  357. package/dist/middleware/body-cap.js.map +1 -0
  358. package/dist/middleware/body-refusal.d.ts +20 -0
  359. package/dist/middleware/body-refusal.d.ts.map +1 -0
  360. package/dist/middleware/body-refusal.js +5 -0
  361. package/dist/middleware/body-refusal.js.map +1 -0
  362. package/dist/middleware/cross-site.d.ts +61 -0
  363. package/dist/middleware/cross-site.d.ts.map +1 -0
  364. package/dist/middleware/cross-site.js +59 -0
  365. package/dist/middleware/cross-site.js.map +1 -0
  366. package/dist/middleware/hub-managed.d.ts +45 -0
  367. package/dist/middleware/hub-managed.d.ts.map +1 -0
  368. package/dist/middleware/hub-managed.js +56 -0
  369. package/dist/middleware/hub-managed.js.map +1 -0
  370. package/dist/middleware/idempotency.d.ts +14 -0
  371. package/dist/middleware/idempotency.d.ts.map +1 -0
  372. package/dist/middleware/idempotency.js +222 -0
  373. package/dist/middleware/idempotency.js.map +1 -0
  374. package/dist/middleware/index.d.ts +12 -0
  375. package/dist/middleware/index.d.ts.map +1 -0
  376. package/dist/middleware/index.js +12 -0
  377. package/dist/middleware/index.js.map +1 -0
  378. package/dist/middleware/json-depth.d.ts +32 -0
  379. package/dist/middleware/json-depth.d.ts.map +1 -0
  380. package/dist/middleware/json-depth.js +80 -0
  381. package/dist/middleware/json-depth.js.map +1 -0
  382. package/dist/middleware/no-store.d.ts +21 -0
  383. package/dist/middleware/no-store.d.ts.map +1 -0
  384. package/dist/middleware/no-store.js +32 -0
  385. package/dist/middleware/no-store.js.map +1 -0
  386. package/dist/middleware/oauth-public.d.ts +16 -0
  387. package/dist/middleware/oauth-public.d.ts.map +1 -0
  388. package/dist/middleware/oauth-public.js +50 -0
  389. package/dist/middleware/oauth-public.js.map +1 -0
  390. package/dist/middleware/rate-limit.d.ts +182 -0
  391. package/dist/middleware/rate-limit.d.ts.map +1 -0
  392. package/dist/middleware/rate-limit.js +277 -0
  393. package/dist/middleware/rate-limit.js.map +1 -0
  394. package/dist/middleware/request-id.d.ts +10 -0
  395. package/dist/middleware/request-id.d.ts.map +1 -0
  396. package/dist/middleware/request-id.js +42 -0
  397. package/dist/middleware/request-id.js.map +1 -0
  398. package/dist/middleware/scopes.d.ts +33 -0
  399. package/dist/middleware/scopes.d.ts.map +1 -0
  400. package/dist/middleware/scopes.js +139 -0
  401. package/dist/middleware/scopes.js.map +1 -0
  402. package/dist/middleware/security-headers.d.ts +53 -0
  403. package/dist/middleware/security-headers.d.ts.map +1 -0
  404. package/dist/middleware/security-headers.js +96 -0
  405. package/dist/middleware/security-headers.js.map +1 -0
  406. package/dist/observability/index.d.ts +3 -0
  407. package/dist/observability/index.d.ts.map +1 -0
  408. package/dist/observability/index.js +3 -0
  409. package/dist/observability/index.js.map +1 -0
  410. package/dist/observability/metrics.d.ts +17 -0
  411. package/dist/observability/metrics.d.ts.map +1 -0
  412. package/dist/observability/metrics.js +82 -0
  413. package/dist/observability/metrics.js.map +1 -0
  414. package/dist/observability/otel.d.ts +20 -0
  415. package/dist/observability/otel.d.ts.map +1 -0
  416. package/dist/observability/otel.js +54 -0
  417. package/dist/observability/otel.js.map +1 -0
  418. package/dist/pagination.d.ts +57 -0
  419. package/dist/pagination.d.ts.map +1 -0
  420. package/dist/pagination.js +90 -0
  421. package/dist/pagination.js.map +1 -0
  422. package/dist/platform/edition.d.ts +57 -0
  423. package/dist/platform/edition.d.ts.map +1 -0
  424. package/dist/platform/edition.js +90 -0
  425. package/dist/platform/edition.js.map +1 -0
  426. package/dist/platform/entitlements.d.ts +26 -0
  427. package/dist/platform/entitlements.d.ts.map +1 -0
  428. package/dist/platform/entitlements.js +28 -0
  429. package/dist/platform/entitlements.js.map +1 -0
  430. package/dist/platform/events.d.ts +87 -0
  431. package/dist/platform/events.d.ts.map +1 -0
  432. package/dist/platform/events.js +32 -0
  433. package/dist/platform/events.js.map +1 -0
  434. package/dist/platform/index.d.ts +8 -0
  435. package/dist/platform/index.d.ts.map +1 -0
  436. package/dist/platform/index.js +8 -0
  437. package/dist/platform/index.js.map +1 -0
  438. package/dist/platform/local-identity.d.ts +39 -0
  439. package/dist/platform/local-identity.d.ts.map +1 -0
  440. package/dist/platform/local-identity.js +87 -0
  441. package/dist/platform/local-identity.js.map +1 -0
  442. package/dist/platform/registry.d.ts +23 -0
  443. package/dist/platform/registry.d.ts.map +1 -0
  444. package/dist/platform/registry.js +5 -0
  445. package/dist/platform/registry.js.map +1 -0
  446. package/dist/platform/usage.d.ts +11 -0
  447. package/dist/platform/usage.d.ts.map +1 -0
  448. package/dist/platform/usage.js +12 -0
  449. package/dist/platform/usage.js.map +1 -0
  450. package/dist/platform/workspaces.d.ts +25 -0
  451. package/dist/platform/workspaces.d.ts.map +1 -0
  452. package/dist/platform/workspaces.js +44 -0
  453. package/dist/platform/workspaces.js.map +1 -0
  454. package/dist/projects/access.d.ts +40 -0
  455. package/dist/projects/access.d.ts.map +1 -0
  456. package/dist/projects/access.js +133 -0
  457. package/dist/projects/access.js.map +1 -0
  458. package/dist/projects/index.d.ts +2 -0
  459. package/dist/projects/index.d.ts.map +1 -0
  460. package/dist/projects/index.js +2 -0
  461. package/dist/projects/index.js.map +1 -0
  462. package/dist/route-label.d.ts +23 -0
  463. package/dist/route-label.d.ts.map +1 -0
  464. package/dist/route-label.js +29 -0
  465. package/dist/route-label.js.map +1 -0
  466. package/dist/routes/health.d.ts +30 -0
  467. package/dist/routes/health.d.ts.map +1 -0
  468. package/dist/routes/health.js +54 -0
  469. package/dist/routes/health.js.map +1 -0
  470. package/dist/routes/index.d.ts +3 -0
  471. package/dist/routes/index.d.ts.map +1 -0
  472. package/dist/routes/index.js +3 -0
  473. package/dist/routes/index.js.map +1 -0
  474. package/dist/routes/wellknown.d.ts +11 -0
  475. package/dist/routes/wellknown.d.ts.map +1 -0
  476. package/dist/routes/wellknown.js +45 -0
  477. package/dist/routes/wellknown.js.map +1 -0
  478. package/dist/secret.d.ts +30 -0
  479. package/dist/secret.d.ts.map +1 -0
  480. package/dist/secret.js +97 -0
  481. package/dist/secret.js.map +1 -0
  482. package/dist/setup-transport.d.ts +23 -0
  483. package/dist/setup-transport.d.ts.map +1 -0
  484. package/dist/setup-transport.js +52 -0
  485. package/dist/setup-transport.js.map +1 -0
  486. package/dist/state.d.ts +21 -0
  487. package/dist/state.d.ts.map +1 -0
  488. package/dist/state.js +4 -0
  489. package/dist/state.js.map +1 -0
  490. package/dist/storage/driver.d.ts +38 -0
  491. package/dist/storage/driver.d.ts.map +1 -0
  492. package/dist/storage/driver.js +14 -0
  493. package/dist/storage/driver.js.map +1 -0
  494. package/dist/storage/factory.d.ts +4 -0
  495. package/dist/storage/factory.d.ts.map +1 -0
  496. package/dist/storage/factory.js +20 -0
  497. package/dist/storage/factory.js.map +1 -0
  498. package/dist/storage/index.d.ts +4 -0
  499. package/dist/storage/index.d.ts.map +1 -0
  500. package/dist/storage/index.js +4 -0
  501. package/dist/storage/index.js.map +1 -0
  502. package/dist/storage/local.d.ts +21 -0
  503. package/dist/storage/local.d.ts.map +1 -0
  504. package/dist/storage/local.js +88 -0
  505. package/dist/storage/local.js.map +1 -0
  506. package/dist/storage/s3.d.ts +29 -0
  507. package/dist/storage/s3.d.ts.map +1 -0
  508. package/dist/storage/s3.js +82 -0
  509. package/dist/storage/s3.js.map +1 -0
  510. package/dist/testing/fake-hub.d.ts +425 -0
  511. package/dist/testing/fake-hub.d.ts.map +1 -0
  512. package/dist/testing/fake-hub.js +1074 -0
  513. package/dist/testing/fake-hub.js.map +1 -0
  514. package/dist/testing/helpers.d.ts +124 -0
  515. package/dist/testing/helpers.d.ts.map +1 -0
  516. package/dist/testing/helpers.js +174 -0
  517. package/dist/testing/helpers.js.map +1 -0
  518. package/dist/testing/index.d.ts +12 -0
  519. package/dist/testing/index.d.ts.map +1 -0
  520. package/dist/testing/index.js +12 -0
  521. package/dist/testing/index.js.map +1 -0
  522. package/dist/testing/setup-origins.d.ts +21 -0
  523. package/dist/testing/setup-origins.d.ts.map +1 -0
  524. package/dist/testing/setup-origins.js +57 -0
  525. package/dist/testing/setup-origins.js.map +1 -0
  526. package/dist/testing/sso-helpers.d.ts +43 -0
  527. package/dist/testing/sso-helpers.d.ts.map +1 -0
  528. package/dist/testing/sso-helpers.js +159 -0
  529. package/dist/testing/sso-helpers.js.map +1 -0
  530. package/dist/tool-definition.d.ts +382 -0
  531. package/dist/tool-definition.d.ts.map +1 -0
  532. package/dist/tool-definition.js +46 -0
  533. package/dist/tool-definition.js.map +1 -0
  534. package/dist/util/index.d.ts +8 -0
  535. package/dist/util/index.d.ts.map +1 -0
  536. package/dist/util/index.js +8 -0
  537. package/dist/util/index.js.map +1 -0
  538. package/package.json +161 -0
  539. package/src/accounts/deletion.ts +389 -0
  540. package/src/accounts/erasure-log.ts +140 -0
  541. package/src/accounts/index.ts +5 -0
  542. package/src/accounts/mint-refusal.ts +67 -0
  543. package/src/accounts/signup-duplicate.ts +21 -0
  544. package/src/accounts/superadmin.ts +46 -0
  545. package/src/api/apikeys.ts +147 -0
  546. package/src/api/audit.ts +83 -0
  547. package/src/api/break-glass.ts +294 -0
  548. package/src/api/cli-auth.ts +308 -0
  549. package/src/api/create-api.ts +1403 -0
  550. package/src/api/demo-passes.ts +207 -0
  551. package/src/api/errors.ts +13 -0
  552. package/src/api/export.ts +482 -0
  553. package/src/api/files.ts +200 -0
  554. package/src/api/index.ts +29 -0
  555. package/src/api/invitations.ts +396 -0
  556. package/src/api/members.ts +617 -0
  557. package/src/api/onboarding.ts +50 -0
  558. package/src/api/openapi-doc.ts +34 -0
  559. package/src/api/projects.ts +788 -0
  560. package/src/api/scope-routes.ts +12 -0
  561. package/src/api/sso-connect.ts +246 -0
  562. package/src/api/sso-logout.ts +76 -0
  563. package/src/api/teams.ts +575 -0
  564. package/src/api/workspaces.ts +649 -0
  565. package/src/apikeys/index.ts +2 -0
  566. package/src/apikeys/peppers.ts +118 -0
  567. package/src/apikeys/service.ts +223 -0
  568. package/src/app.ts +228 -0
  569. package/src/audit/filters.ts +101 -0
  570. package/src/audit/index.ts +9 -0
  571. package/src/audit/service.ts +154 -0
  572. package/src/boot.ts +953 -0
  573. package/src/constant-time.ts +9 -0
  574. package/src/email/driver.ts +139 -0
  575. package/src/email/index.ts +26 -0
  576. package/src/email/shell.ts +201 -0
  577. package/src/email/templates.ts +184 -0
  578. package/src/entitlements/check.ts +435 -0
  579. package/src/entitlements/clock-skew.ts +58 -0
  580. package/src/entitlements/gate.ts +636 -0
  581. package/src/entitlements/hub-machine-token.ts +205 -0
  582. package/src/entitlements/hub-subject.ts +77 -0
  583. package/src/entitlements/index.ts +37 -0
  584. package/src/entitlements/poster.ts +265 -0
  585. package/src/entitlements/profiles.ts +303 -0
  586. package/src/entitlements/slot.ts +105 -0
  587. package/src/env.ts +456 -0
  588. package/src/files/http.ts +77 -0
  589. package/src/files/index.ts +4 -0
  590. package/src/files/serve.ts +115 -0
  591. package/src/files/service.ts +209 -0
  592. package/src/files/spool.ts +58 -0
  593. package/src/identity/better-auth.ts +1103 -0
  594. package/src/identity/demo-pass-plugin.ts +121 -0
  595. package/src/identity/demo-pass-rules.ts +229 -0
  596. package/src/identity/demo-pass.ts +390 -0
  597. package/src/identity/hub-grant.ts +704 -0
  598. package/src/identity/hub-jwt.ts +78 -0
  599. package/src/identity/hub-live-gate.ts +167 -0
  600. package/src/identity/hub-logout.ts +174 -0
  601. package/src/identity/hub-projection.ts +251 -0
  602. package/src/identity/hub-reconcile.ts +572 -0
  603. package/src/identity/hub-sso.ts +696 -0
  604. package/src/identity/hub-user-client.ts +554 -0
  605. package/src/identity/index.ts +56 -0
  606. package/src/identity/oauth-jwt.ts +128 -0
  607. package/src/identity/resolve-membership.ts +104 -0
  608. package/src/identity/signing-key.ts +164 -0
  609. package/src/index.ts +65 -0
  610. package/src/invitations/index.ts +1 -0
  611. package/src/invitations/service.ts +171 -0
  612. package/src/jobs/index.ts +25 -0
  613. package/src/jobs/pgboss.ts +594 -0
  614. package/src/jobs/timers.ts +287 -0
  615. package/src/logger.ts +67 -0
  616. package/src/mcp/errors.ts +111 -0
  617. package/src/mcp/http.ts +129 -0
  618. package/src/mcp/index.ts +28 -0
  619. package/src/mcp/inputs.ts +33 -0
  620. package/src/mcp/projects.ts +480 -0
  621. package/src/mcp/server.ts +174 -0
  622. package/src/mcp/teams.ts +97 -0
  623. package/src/mcp/tool-kit.ts +148 -0
  624. package/src/members/removal.ts +216 -0
  625. package/src/middleware/auth-body.ts +58 -0
  626. package/src/middleware/auth-context.ts +384 -0
  627. package/src/middleware/body-cap.ts +33 -0
  628. package/src/middleware/body-refusal.ts +23 -0
  629. package/src/middleware/cross-site.ts +122 -0
  630. package/src/middleware/hub-managed.ts +61 -0
  631. package/src/middleware/idempotency.ts +264 -0
  632. package/src/middleware/index.ts +33 -0
  633. package/src/middleware/json-depth.ts +74 -0
  634. package/src/middleware/no-store.ts +30 -0
  635. package/src/middleware/oauth-public.ts +54 -0
  636. package/src/middleware/rate-limit.ts +432 -0
  637. package/src/middleware/request-id.ts +54 -0
  638. package/src/middleware/scopes.ts +150 -0
  639. package/src/middleware/security-headers.ts +129 -0
  640. package/src/observability/index.ts +2 -0
  641. package/src/observability/metrics.ts +102 -0
  642. package/src/observability/otel.ts +79 -0
  643. package/src/pagination.ts +118 -0
  644. package/src/platform/edition.ts +140 -0
  645. package/src/platform/entitlements.ts +47 -0
  646. package/src/platform/events.ts +71 -0
  647. package/src/platform/index.ts +7 -0
  648. package/src/platform/local-identity.ts +90 -0
  649. package/src/platform/registry.ts +28 -0
  650. package/src/platform/usage.ts +13 -0
  651. package/src/platform/workspaces.ts +41 -0
  652. package/src/projects/access.ts +160 -0
  653. package/src/projects/index.ts +7 -0
  654. package/src/route-label.ts +29 -0
  655. package/src/routes/health.ts +79 -0
  656. package/src/routes/index.ts +2 -0
  657. package/src/routes/wellknown.ts +67 -0
  658. package/src/secret.ts +115 -0
  659. package/src/setup-transport.ts +46 -0
  660. package/src/state.ts +23 -0
  661. package/src/storage/driver.ts +48 -0
  662. package/src/storage/factory.ts +34 -0
  663. package/src/storage/index.ts +3 -0
  664. package/src/storage/local.ts +93 -0
  665. package/src/storage/s3.ts +109 -0
  666. package/src/testing/fake-hub.ts +1316 -0
  667. package/src/testing/helpers.ts +254 -0
  668. package/src/testing/index.ts +11 -0
  669. package/src/testing/setup-origins.ts +58 -0
  670. package/src/testing/sso-helpers.ts +178 -0
  671. package/src/tool-definition.ts +463 -0
  672. package/src/util/index.ts +17 -0
  673. package/test/fixtures/onboarding-backfill.sql +13 -0
  674. package/test/fixtures/operator-user-id-backfill.sql +18 -0
  675. package/test/host/dr-lib.sh +253 -0
  676. package/test/host/drizzle/0000_chassis_baseline.sql +438 -0
  677. package/test/host/drizzle/0001_last_owner_guard.sql +42 -0
  678. package/test/host/drizzle/meta/0000_snapshot.json +3566 -0
  679. package/test/host/drizzle/meta/_journal.json +20 -0
  680. package/test/host/drizzle.config.ts +17 -0
  681. package/test/host/identity.ts +41 -0
  682. package/test/host/minimal-host.ts +35 -0
  683. package/test/host/minimal-tool.ts +123 -0
  684. package/test/integration/account-oracle-uniformity.test.ts +284 -0
  685. package/test/integration/account-recovery.test.ts +236 -0
  686. package/test/integration/account.test.ts +120 -0
  687. package/test/integration/api-quota.test.ts +208 -0
  688. package/test/integration/apikey-expiry.test.ts +178 -0
  689. package/test/integration/apikey-pepper-rotation.test.ts +190 -0
  690. package/test/integration/boot-posture.test.ts +169 -0
  691. package/test/integration/break-glass.test.ts +484 -0
  692. package/test/integration/cloud-lifecycle.test.ts +233 -0
  693. package/test/integration/credential-boundaries.test.ts +195 -0
  694. package/test/integration/default-workspace.test.ts +414 -0
  695. package/test/integration/demo-pass-rules.test.ts +332 -0
  696. package/test/integration/demo-pass.test.ts +1222 -0
  697. package/test/integration/dr-verify-real-dump.test.ts +154 -0
  698. package/test/integration/edition.test.ts +868 -0
  699. package/test/integration/email-change.test.ts +394 -0
  700. package/test/integration/empty-tool.test.ts +267 -0
  701. package/test/integration/entitlements-null-hook.test.ts +187 -0
  702. package/test/integration/entitlements.test.ts +693 -0
  703. package/test/integration/export-entries.test.ts +321 -0
  704. package/test/integration/files.test.ts +320 -0
  705. package/test/integration/first-boot-claim.test.ts +131 -0
  706. package/test/integration/gdpr.test.ts +532 -0
  707. package/test/integration/helpers.ts +29 -0
  708. package/test/integration/hub-grant.test.ts +505 -0
  709. package/test/integration/hub-live-gate.test.ts +612 -0
  710. package/test/integration/hub-managed-membership.test.ts +559 -0
  711. package/test/integration/hub-org-selector.test.ts +281 -0
  712. package/test/integration/hub-reconcile.test.ts +646 -0
  713. package/test/integration/hub-sso.test.ts +704 -0
  714. package/test/integration/hub-teams.test.ts +771 -0
  715. package/test/integration/idempotency.test.ts +261 -0
  716. package/test/integration/invite-verification.test.ts +210 -0
  717. package/test/integration/last-owner-race.test.ts +263 -0
  718. package/test/integration/m9-fixes.test.ts +290 -0
  719. package/test/integration/mcp-projects.test.ts +463 -0
  720. package/test/integration/member-removal-race.test.ts +466 -0
  721. package/test/integration/member-removal.test.ts +794 -0
  722. package/test/integration/oauth-mcp.test.ts +477 -0
  723. package/test/integration/oauth-workspace.test.ts +371 -0
  724. package/test/integration/observability.test.ts +183 -0
  725. package/test/integration/onboarding.test.ts +268 -0
  726. package/test/integration/orphan-purge.test.ts +451 -0
  727. package/test/integration/pagination.test.ts +344 -0
  728. package/test/integration/platform-core.test.ts +305 -0
  729. package/test/integration/platform.test.ts +246 -0
  730. package/test/integration/pool-survives-restart.test.ts +80 -0
  731. package/test/integration/projects-cloud.test.ts +193 -0
  732. package/test/integration/projects.test.ts +1310 -0
  733. package/test/integration/security-fixes.test.ts +190 -0
  734. package/test/integration/setup-transport.test.ts +118 -0
  735. package/test/integration/signin-wall.test.ts +103 -0
  736. package/test/integration/signing-key-rotation.test.ts +349 -0
  737. package/test/integration/sso-helpers.ts +15 -0
  738. package/test/integration/sso-logout.test.ts +283 -0
  739. package/test/integration/teams-cloud.test.ts +194 -0
  740. package/test/integration/teams.test.ts +551 -0
  741. package/test/integration/timers.test.ts +377 -0
  742. package/test/integration/two-factor.test.ts +301 -0
  743. package/test/integration/usage-window.test.ts +161 -0
  744. package/test/integration/workspace-create-cloud.test.ts +523 -0
  745. package/test/integration/workspace-create.test.ts +510 -0
  746. package/test/integration/workspace-scoping.test.ts +530 -0
  747. package/test/integration/zz-adv-replay-owner.test.ts +205 -0
  748. package/test/unit/apikey-peppers.test.ts +110 -0
  749. package/test/unit/apikey-prefix.test.ts +34 -0
  750. package/test/unit/audit-filters.test.ts +135 -0
  751. package/test/unit/auth-origins.test.ts +92 -0
  752. package/test/unit/body-cap.test.ts +84 -0
  753. package/test/unit/clock-skew.test.ts +68 -0
  754. package/test/unit/cross-site.test.ts +166 -0
  755. package/test/unit/declared-credits.test.ts +13 -0
  756. package/test/unit/demo-pass-rules.test.ts +192 -0
  757. package/test/unit/email-driver.test.ts +127 -0
  758. package/test/unit/end-pool.test.ts +181 -0
  759. package/test/unit/entitlement-check.test.ts +492 -0
  760. package/test/unit/entitlement-gate.test.ts +1333 -0
  761. package/test/unit/entitlement-profiles.test.ts +289 -0
  762. package/test/unit/erasure-log.test.ts +72 -0
  763. package/test/unit/error-mapping.test.ts +89 -0
  764. package/test/unit/export-entries.test.ts +85 -0
  765. package/test/unit/export-sanitize.test.ts +27 -0
  766. package/test/unit/files-head-route.test.ts +155 -0
  767. package/test/unit/files-http.test.ts +97 -0
  768. package/test/unit/head-rewrite.test.ts +72 -0
  769. package/test/unit/hub-create-org.test.ts +181 -0
  770. package/test/unit/hub-jwt.test.ts +93 -0
  771. package/test/unit/hub-sso-params.test.ts +39 -0
  772. package/test/unit/hub-subject.test.ts +83 -0
  773. package/test/unit/idempotency-crypto.test.ts +45 -0
  774. package/test/unit/json-depth.test.ts +88 -0
  775. package/test/unit/logger-redaction.test.ts +66 -0
  776. package/test/unit/mcp-errors.test.ts +110 -0
  777. package/test/unit/mcp-inputs.test.ts +106 -0
  778. package/test/unit/mcp-project-hints.test.ts +26 -0
  779. package/test/unit/no-store.test.ts +49 -0
  780. package/test/unit/no-tool-migration-filename.test.ts +70 -0
  781. package/test/unit/observability.test.ts +64 -0
  782. package/test/unit/openapi-doc.test.ts +93 -0
  783. package/test/unit/rate-limit-consume-on.test.ts +76 -0
  784. package/test/unit/rate-limit.test.ts +39 -0
  785. package/test/unit/readyz-storage.test.ts +157 -0
  786. package/test/unit/request-quota.test.ts +146 -0
  787. package/test/unit/scopes.test.ts +66 -0
  788. package/test/unit/security-headers.test.ts +131 -0
  789. package/test/unit/serve-content-type.test.ts +74 -0
  790. package/test/unit/setup-transport.test.ts +29 -0
  791. package/test/unit/storage-local.test.ts +53 -0
  792. package/test/unit/superadmin.test.ts +32 -0
  793. package/test/unit/timers.test.ts +287 -0
  794. package/test/unit/tool-definition-guards.test.ts +166 -0
  795. package/test/unit/tool-entitlements-guard.test.ts +168 -0
  796. package/test/unit/usage-poster.test.ts +542 -0
  797. package/test/unit/usage-window.test.ts +48 -0
  798. package/test/unit/workspace-create-audit.test.ts +48 -0
@@ -0,0 +1,1165 @@
1
+ import { OpenAPIHono } from '@hono/zod-openapi';
2
+ import { matchedRoutes } from 'hono/route';
3
+ import { createEmailVerificationToken } from 'better-auth/api';
4
+ import { jwtVerify } from 'jose';
5
+ import { registerOpenApiDoc } from './index.js';
6
+ import { ulid } from 'ulid';
7
+ import { and, asc, desc, eq } from 'drizzle-orm';
8
+ import { ACTIVE_WORKSPACE_HEADER } from '@antasphere/chassis-contract';
9
+ import { instanceRoute, setupRoute } from '@antasphere/chassis-contract/routes';
10
+ import { account, instanceSettings, user as userTable, userOnboarding, workspaceMembers, workspaces } from '@antasphere/chassis-db';
11
+ import { hubConfig } from '../env.js';
12
+ import { DEMO_SESSION_REFUSED_API_ROUTES, demoSessionApiRefusalMessage, demoSessionAuthRefusal } from '../identity/index.js';
13
+ import { DemoPassService } from '../identity/demo-pass.js';
14
+ import { auditMiddleware } from '../audit/index.js';
15
+ import { isDeferringGate, registerEntitlementGate } from '../entitlements/index.js';
16
+ import { constantTimeEquals } from '../util/index.js';
17
+ import { isSecureSetupOrigin } from '../util/index.js';
18
+ import { authBodyGuard } from '../middleware/index.js';
19
+ import { admitPrincipal, authContext, resolveSessionPrincipal } from '../middleware/index.js';
20
+ import { idempotency } from '../middleware/index.js';
21
+ import { crossSiteGuard } from '../middleware/index.js';
22
+ import { jsonDepthLimit } from '../middleware/index.js';
23
+ import { deferrableBodyCap } from '../middleware/body-cap.js';
24
+ import { noStoreAuthenticated } from '../middleware/index.js';
25
+ import { oauthPublicEndpoints } from '../middleware/index.js';
26
+ import { createRequestQuota, emailKeyOf, makeClientIp, rateLimit } from '../middleware/index.js';
27
+ import { HUB_SSO_PROVIDER_ID } from '../identity/index.js';
28
+ import { registerBreakGlassRoutes } from './index.js';
29
+ import { registerCliAuthRoutes } from './index.js';
30
+ import { registerOnboardingRoutes } from './index.js';
31
+ import { registerWorkspaceRoutes, workspaceCreationPolicy, workspaceCreationRefusal, workspaceCreateWallClosed } from './index.js';
32
+ import { registerSsoConnectRoutes } from './index.js';
33
+ import { registerSsoLogoutRoutes } from './index.js';
34
+ import { registerMemberRoutes } from './index.js';
35
+ import { demoSessionJudge, demoSignInOn, registerDemoPassRoutes } from './index.js';
36
+ import { registerProjectRoutes } from './index.js';
37
+ import { registerTeamRoutes } from './index.js';
38
+ import { registerApiKeyRoutes } from './index.js';
39
+ import { registerInvitationRoutes } from './index.js';
40
+ import { registerAuditRoutes } from './index.js';
41
+ import { registerFileRoutes } from './index.js';
42
+ import { registerExportRoutes } from './index.js';
43
+ /** Inline error body matching the wire shape; keeps openapi handlers typed. */
44
+ const err = (code, message) => ({ error: { code, message } });
45
+ /** Where the sign-in library is mounted: the full path a middleware on `api` sees starts with it. */
46
+ const AUTH_MOUNT = '/api/v1/auth';
47
+ /**
48
+ * PRDCT-1437 door 1: neutralize the change-email consume's account-existence
49
+ * oracle by rewriting the REQUEST before Better Auth reads the token.
50
+ *
51
+ * The consume of a `change-email-verification` link updates the target's email
52
+ * to the new address; when that address already belongs to ANOTHER user the
53
+ * update dies at the `user.email` UNIQUE constraint as a raw 500, while a free
54
+ * address answers a 302 — a mint-then-consume "does this email exist anywhere
55
+ * on the instance" for any workspace owner, and a robustness bug. Rather than
56
+ * reimplement the consume (LESSONS.md M6: never hand-roll the Better Auth
57
+ * flow), on a collision we re-sign the token so its target is the member's
58
+ * CURRENT address — a no-op change that runs Better Auth's ENTIRE success path
59
+ * (its own `callbackURL` origin check, the session mint, the set-cookie, the
60
+ * 302-or-JSON, the audit hook) with no unique violation. Redirect, cookie, and
61
+ * origin-check verdict are then byte-identical to the free case BY
62
+ * CONSTRUCTION, and no `callbackURL` handling is added here, so no open-redirect
63
+ * surface is introduced (the route's own originCheck stays the only judge).
64
+ *
65
+ * The one residual tell — the success JSON echoes the resulting email in its
66
+ * `user` record — is reachable only WITHOUT a `callbackURL` (a real consume
67
+ * always carries one and redirects) and is stripped to `user:null` by the
68
+ * wrapper below. The honest, inherent residual (email uniqueness is
69
+ * instance-global, so the change's outcome is readable one step later on the
70
+ * roster) is documented in ADR 013's amendment. It runs in a before-hook only
71
+ * as a query mutation, which the endpoint's own parsed query never sees, so it
72
+ * MUST rewrite the request here. Re-verify the token shape and the
73
+ * `change-email-verification` requestType on ANY Better Auth bump.
74
+ */
75
+ /**
76
+ * Constrain a verify-email redirect Location to the instance's own origin
77
+ * (Major 3, PRDCT-1437 hardening). DEFENCE-IN-DEPTH, not a hole being closed:
78
+ * on the pinned Better Auth, `/verify-email` DOES run its own callbackURL
79
+ * origin check in production (the `originCheck` factory has no GET skip; only
80
+ * `originCheckMiddleware` does) — but the test harness disables that guard
81
+ * (`NODE_ENV=test` sets `skipOriginCheck`), and a future bump could regress
82
+ * it, so we constrain the redirect ourselves and the no-op collision rewrite
83
+ * routes the taken branch through the same constraint. A same-origin (or
84
+ * public-origin) target is returned
85
+ * as a relative path; anything off-origin is neutralized to `/`. Legitimate
86
+ * consumes send a relative `callbackURL` and are untouched; applied to every
87
+ * response the wrapper sees, so the taken and free branches stay identical.
88
+ */
89
+ function safeRedirectLocation(location, requestUrl, publicBaseUrl) {
90
+ let resolved;
91
+ let servingOrigin;
92
+ try {
93
+ servingOrigin = new URL(requestUrl).origin;
94
+ resolved = new URL(location, requestUrl);
95
+ }
96
+ catch {
97
+ return '/';
98
+ }
99
+ let publicOrigin = null;
100
+ try {
101
+ publicOrigin = new URL(publicBaseUrl).origin;
102
+ }
103
+ catch {
104
+ publicOrigin = null;
105
+ }
106
+ if (resolved.origin === servingOrigin || resolved.origin === publicOrigin) {
107
+ return resolved.pathname + resolved.search + resolved.hash;
108
+ }
109
+ return '/';
110
+ }
111
+ async function rewriteChangeEmailCollision(auth, authSecret, request) {
112
+ const url = new URL(request.url);
113
+ const token = url.searchParams.get('token');
114
+ if (!token)
115
+ return request;
116
+ let payload;
117
+ try {
118
+ const verified = await jwtVerify(token, new TextEncoder().encode(authSecret), {
119
+ algorithms: ['HS256']
120
+ });
121
+ payload = verified.payload;
122
+ }
123
+ catch {
124
+ return request; // a bad token is Better Auth's to reject (uniform error redirect)
125
+ }
126
+ const email = payload.email;
127
+ const updateTo = payload.updateTo;
128
+ if (payload.requestType !== 'change-email-verification' ||
129
+ typeof email !== 'string' ||
130
+ typeof updateTo !== 'string' ||
131
+ email === updateTo) {
132
+ return request;
133
+ }
134
+ try {
135
+ const authCtx = await auth.$context;
136
+ const target = await authCtx.internalAdapter.findUserByEmail(email);
137
+ if (!target)
138
+ return request; // user_not_found — uniform on both branches
139
+ // A signed-in NON-target redirectOnErrors identically on both branches, so
140
+ // only the state that actually reaches the update needs neutralizing.
141
+ const session = await auth.api.getSession({ headers: request.headers }).catch(() => null);
142
+ if (session && session.user.email !== email)
143
+ return request;
144
+ const holder = await authCtx.internalAdapter.findUserByEmail(updateTo);
145
+ if (!holder || holder.user.id === target.user.id)
146
+ return request; // no collision
147
+ // Collision: re-sign as a no-op (change email → same email). Better Auth
148
+ // then updates the row to its own value (no 23505) and runs the full
149
+ // success path off the rewritten request.
150
+ const noopToken = await createEmailVerificationToken(authCtx.secret, email, email, 3600, {
151
+ requestType: 'change-email-verification'
152
+ });
153
+ url.searchParams.set('token', noopToken);
154
+ return new Request(url.toString(), request);
155
+ }
156
+ catch {
157
+ // A lookup failure must not turn into a distinguishing error — let Better
158
+ // Auth answer, and the wrapper's 5xx→uniform mapping backstops it.
159
+ return request;
160
+ }
161
+ }
162
+ /** Control-flow marker: the singleton claim lost (instance already set up). */
163
+ class SetupAlreadyDone extends Error {
164
+ }
165
+ /**
166
+ * The versioned API, mounted at /api/v1. Every route is defined by a contract
167
+ * in the tool's contract package (`<tool contract>/routes`); the OpenAPI document is generated from those
168
+ * contracts and served at /api/v1/openapi.json.
169
+ */
170
+ export function createApiApp(deps) {
171
+ const { db, env, auth, registry, logger, apiKeys: apiKeyService, audit, email, limiters, tool } = deps;
172
+ const { meRoute, apiKeysListRoute, apiKeyCreateRoute, apiKeyRevokeRoute, cliAuthCompleteRoute } = tool.scopes.contractRoutes;
173
+ const { ssoCliConnectRoute } = tool.scopes.contractRoutes;
174
+ // The registry as the chassis routers read it (the generic events only):
175
+ // the same seams, the same bus, seen through its chassis view.
176
+ const chassisRegistry = { ...registry, events: registry.events.chassisView() };
177
+ // What every positional hook of the tool receives.
178
+ const hookContext = {
179
+ db,
180
+ env,
181
+ auth,
182
+ registry,
183
+ logger,
184
+ audit,
185
+ email,
186
+ limiters,
187
+ storage: deps.storage,
188
+ fileService: deps.fileService,
189
+ authSecret: deps.authSecret,
190
+ hubSso: deps.hubSso
191
+ };
192
+ // P7 (internal/federation.md): on EDITION=cloud, membership of a hub-origin
193
+ // (projected) workspace is managed at the hub — this is the pointer the
194
+ // membership-mutation gates and /me carry. Same single `hubConfig` switch
195
+ // as every cloud seam: null on oss, so oss wires no gate and /me answers
196
+ // hubManageUrl null.
197
+ const hub = hubConfig(env);
198
+ const hubManaged = hub ? { manageUrl: hub.issuerUrl } : undefined;
199
+ // Demo sign-in (the demo pass spec): DEMO_SIGN_IN on the self-hosted
200
+ // edition only. Off, no demo route, wall or discovery key exists.
201
+ const demoSignIn = demoSignInOn(env, hub, logger);
202
+ const api = new OpenAPIHono({
203
+ // Validation failures use the same wire shape as every other error.
204
+ defaultHook: (result, c) => {
205
+ if (!result.success) {
206
+ return c.json({
207
+ error: {
208
+ code: 'validation_error',
209
+ message: 'Request validation failed',
210
+ details: result.error.issues
211
+ }
212
+ }, 400);
213
+ }
214
+ }
215
+ });
216
+ // NOTE (release-gate fix): malformed/empty JSON bodies used to 500 here —
217
+ // hono's json validator throws HTTPException(400) BEFORE zod runs, so the
218
+ // defaultHook above never fires. Hono routes thrown errors straight to the
219
+ // top-level errorHandler from the throwing frame (an upstream middleware
220
+ // try/catch never sees them), so the mapping to the 400 wire shape lives
221
+ // in app.ts app.onError — the one seam that covers every body route.
222
+ // ── Public OAuth endpoints first: wildcard CORS + OPTIONS 204 + no-store
223
+ // on token responses. Before the rate limits so preflights cost nothing.
224
+ api.use('/auth/*', oauthPublicEndpoints());
225
+ // ── Cross-site (CSRF) gate, before ANY other work: an unsafe method driven
226
+ // from another origin is refused before a body is buffered, before a
227
+ // limiter bucket is touched, and before a credential resolves. Sessions are
228
+ // ambient credentials — every business route below (the tool's resources,
229
+ // its tokens and collaborators, api-keys) would otherwise be drivable
230
+ // from any page the user happens to have open.
231
+ //
232
+ // SCOPED TO /api/v1 ON PURPOSE. The public viewer (`/v/:secret`) is mounted
233
+ // on the root app and is DELIBERATELY embeddable cross-origin (ADR 021 +
234
+ // /embed.js), including its password-form POST — mounting this guard there
235
+ // would break every legitimate embed. The viewer authenticates on the share
236
+ // secret and its own scoped cookie, not on the dashboard session, so it is
237
+ // not the ambient-credential surface this closes.
238
+ api.use('*', crossSiteGuard({
239
+ publicBaseUrl: env.PUBLIC_BASE_URL,
240
+ // The tool's never-trusted origins (e.g. a viewer origin, PRDCT-1352:
241
+ // the origin author-controlled script runs on): never a trust
242
+ // grant here, even if it is the serving origin.
243
+ deniedOrigins: deps.untrustedOrigins,
244
+ // `/api/v1/viewer/*` is the share-token annotation API, and it is a
245
+ // DELIBERATE wildcard-CORS surface (viewer/annotations-api.ts): it is
246
+ // called by the overlay client running inside the sandboxed viewer
247
+ // iframe, whose origin is the opaque `null`. Nothing there is
248
+ // cookie-authenticated — the share token in the path is the credential —
249
+ // so it is not the ambient-credential class this guard closes, and
250
+ // refusing `Origin: null` would break annotations outright.
251
+ isExempt: (path) => tool.api.csrfExempt?.(path) ?? false
252
+ }));
253
+ // ── Authenticated responses are never cached. Registered ABOVE the
254
+ // credential middleware so it wraps every route under /api/v1, and it reads
255
+ // the principal AFTER the inner chain has resolved it. Routes that set their
256
+ // own Cache-Control (public discovery, the OpenAPI document) keep it.
257
+ api.use('*', noStoreAuthenticated());
258
+ // The public viewer-token annotation surface (Phase 5): same posture — the
259
+ // overlay calls cross-origin from the sandboxed opaque origin (Origin:
260
+ // null), token-authed, never cookie-authed, so wildcard CORS is safe.
261
+ tool.api.early?.(api, hookContext);
262
+ // ── Body size caps, path-routed. 1 MiB is generous for every JSON/auth
263
+ // body. Exceptions:
264
+ // - /files: streamed uploads with their own mid-stream cap
265
+ // (MAX_FILE_SIZE_MB) plus a Content-Length entitlement check;
266
+ // - the tool's multipart asset uploads (its `bodyLimit` slot) — capped at
267
+ // MAX_FILE_SIZE_MB (+1 MiB multipart framing headroom) so an unbounded
268
+ // body can never balloon the buffering parse;
269
+ // - the rest of the tool's routes, which are JSON, but commit manifests are legal up to
270
+ // 5000 entries × 1 KiB paths — a 16 MiB cap fits any contract-valid
271
+ // manifest while still bounding abuse.
272
+ const jsonBodyCap = {
273
+ maxBytes: 1024 * 1024,
274
+ onError: (c) => c.json(err('payload_too_large', 'Request body exceeds the 1 MiB limit'), 413)
275
+ };
276
+ // The tool's own caps, declared ONCE (the slot's factory runs here, never
277
+ // per request) and built into middleware per cap object below.
278
+ const toolBodyLimit = tool.api.bodyLimit?.(hookContext);
279
+ // - the viewer's form file upload (PRDCT-2403): one raw streamed file per
280
+ // request, capped MID-STREAM by the form-upload ceiling in its handler.
281
+ //
282
+ // A declared Content-Length over a cap on a route whose gate judges a
283
+ // PLAN LIMIT is not refused here (PRDCT-2632): the entitlement gate must
284
+ // see the declared size first, so a metered account meets 403
285
+ // `plan_required` with its upgrade link at any size and the cap's 413
286
+ // answers only when the plan allows the size. `deferrableBodyCap`
287
+ // (middleware/body-cap.ts) parks the refusal on the context and DROPS the
288
+ // request's body, keyed on the gate itself being on the matched route;
289
+ // the gate fires it. Every cap goes through it, the tool's and the
290
+ // default JSON one alike, so a future declared-limit route left under
291
+ // the default cap is never shadowed either. An undeclared body is still
292
+ // counted and cut by the cap.
293
+ const deferring = (c) => matchedRoutes(c).some((route) => isDeferringGate(route.handler));
294
+ const capMiddleware = new WeakMap();
295
+ const capped = (cap) => {
296
+ let built = capMiddleware.get(cap);
297
+ if (!built) {
298
+ built = deferrableBodyCap(cap, deferring);
299
+ capMiddleware.set(cap, built);
300
+ }
301
+ return built;
302
+ };
303
+ api.use('*', (c, next) => {
304
+ const path = c.req.path;
305
+ if (path.startsWith('/api/v1/files'))
306
+ return next();
307
+ const verdict = toolBodyLimit?.(path);
308
+ if (verdict === 'exempt')
309
+ return next();
310
+ return capped(verdict ?? jsonBodyCap)(c, next);
311
+ });
312
+ // ── JSON nesting cap, right after the size cap (so the scan is bounded by
313
+ // it): `JSON.parse` accepts any depth but `JSON.stringify` is recursive, so
314
+ // a small body of nothing but `[` turns any echo/audit/log of it into a
315
+ // RangeError 500 inside the shipped node:22-alpine image.
316
+ // The form file upload is exempt: its body is a FILE, never parsed, and a
317
+ // respondent's `.json` sent with a JSON content type would otherwise be
318
+ // cloned into memory whole (up to the upload ceiling) by the scan.
319
+ const depthLimit = jsonDepthLimit();
320
+ api.use('*', (c, next) => (tool.api.jsonDepthExempt?.(c.req.path) ? next() : depthLimit(c, next)));
321
+ // ── Auth-surface rate limits: registered FIRST so they run before auth
322
+ // resolution — abusive traffic is rejected before it costs a DB query.
323
+ const clientIp = makeClientIp(env.TRUST_PROXY);
324
+ // Login keys by IP AND email: Docker NAT (or a rotating attacker) can
325
+ // collapse/expand the IP dimension, so per-account protection must not
326
+ // depend on it. emailKeyOf returns [] for bodyless social sign-in.
327
+ //
328
+ // consumeOn: 'failure' — see rate-limit.ts. The email key is derived from
329
+ // the REQUEST BODY, i.e. from an address anyone can type, so consuming it on
330
+ // arrival made the wall an account-lockout tool: ten POSTs naming a victim
331
+ // emptied that account's bucket for the window. Consuming only when the
332
+ // credential was actually refused keeps the brute-force budget identical and
333
+ // makes the owner's own successful sign-ins free.
334
+ //
335
+ // The federated start (`/auth/sign-in/oauth2`, the silent start toward the
336
+ // hub) presents no credential: it only answers the hub's authorize URL, and
337
+ // the hub checks the person with its own wall. It stays off this one, so a
338
+ // wall drained by wrong passwords behind a shared address does not also
339
+ // close the hub sign-in to everyone behind it (PRDCT-2830).
340
+ const loginWall = rateLimit(limiters.login, clientIp, emailKeyOf, { consumeOn: 'failure' });
341
+ api.use('/auth/sign-in/*', (c, next) => c.req.path.endsWith('/auth/sign-in/oauth2') ? next() : loginWall(c, next));
342
+ api.use('/auth/email-otp/*', rateLimit(limiters.otp, clientIp, emailKeyOf));
343
+ api.use('/auth/sign-up/*', rateLimit(limiters.login, clientIp));
344
+ // Password reset: request keys by IP AND email (tight); reset + change key
345
+ // by IP (brute-forcing a token / the current password). The deprecated
346
+ // emailOTP forget-password alias shares the reset wall.
347
+ api.use('/auth/request-password-reset', rateLimit(limiters.passwordReset, clientIp, emailKeyOf));
348
+ api.use('/auth/reset-password', rateLimit(limiters.passwordReset, clientIp));
349
+ api.use('/auth/reset-password/*', rateLimit(limiters.passwordReset, clientIp));
350
+ api.use('/auth/forget-password/*', rateLimit(limiters.passwordReset, clientIp, emailKeyOf));
351
+ // Email change mints sign-in-equivalent tokens and sends mail — same tight
352
+ // wall as the reset surface (per IP; the route is session-gated anyway).
353
+ api.use('/auth/change-email', rateLimit(limiters.passwordReset, clientIp));
354
+ api.use('/auth/change-password', rateLimit(limiters.login, clientIp));
355
+ // The 2FA surface: verify-totp / verify-backup-code guess a 6-digit code or
356
+ // a backup code behind the 10-minute pending-sign-in cookie; enable/disable
357
+ // present the current password. Same brute-force wall as login (per IP).
358
+ api.use('/auth/two-factor/*', rateLimit(limiters.login, clientIp));
359
+ // Account deletion presents the current password — same brute-force wall.
360
+ api.use('/auth/delete-user', rateLimit(limiters.login, clientIp));
361
+ api.use('/auth/oauth2/register', rateLimit(limiters.oauthRegister, clientIp));
362
+ api.use('/auth/oauth2/token', rateLimit(limiters.oauthToken, clientIp));
363
+ api.use('/setup', rateLimit(limiters.setup, clientIp));
364
+ api.use('/invitations/accept', rateLimit(limiters.invitationAccept, clientIp));
365
+ api.use('/invitations/lookup', rateLimit(limiters.invitationAccept, clientIp));
366
+ // Collaborator claims are invitation acceptances in per-resource clothing —
367
+ // the same public token-redemption surface, the same wall.
368
+ const rateLimitContext = { ...hookContext, clientIp };
369
+ tool.api.rateLimits?.(api, rateLimitContext);
370
+ // Break-glass: a rare superadmin recovery action — a tight per-IP wall
371
+ // bounds allowlist probing before the handlers' own session checks run.
372
+ api.use('/admin/break-glass/*', rateLimit(limiters.breakGlass, clientIp));
373
+ // CLI email-OTP sign-in (api/cli-auth.ts): request = an OTP send (the OTP
374
+ // wall, per IP + email); complete = a credential guess (the login wall,
375
+ // per IP + email) on top of better-auth's own 3-attempts-per-code limit.
376
+ api.use('/cli/auth/request', rateLimit(limiters.otp, clientIp, emailKeyOf));
377
+ api.use('/cli/auth/complete', rateLimit(limiters.login, clientIp, emailKeyOf, { consumeOn: 'failure' }));
378
+ // The demo pass redeem (identity/demo-pass-plugin.ts): a secret presented
379
+ // anonymously, so every arrival costs, per address only. In front of the
380
+ // sign-in handler, and only where the endpoint exists.
381
+ if (demoSignIn) {
382
+ // Only a REFUSED redeem costs: the wall is there against guessing, and a
383
+ // person playing a demonstration opens many valid links in a row from one
384
+ // address (found on the pair: the 21st valid click in fifteen minutes was
385
+ // refused). A drained wall still refuses everything until it refills.
386
+ api.use('/auth/demo/redeem', rateLimit(limiters.demoRedeem, clientIp, undefined, { consumeOn: 'failure' }));
387
+ }
388
+ // The OpenAPI document is unauthenticated (PUBLIC_API_PATHS) so it never
389
+ // reaches the per-principal quota; the buffer is generated once at boot
390
+ // (openapi-doc.ts) and this wall is the defence in depth on top.
391
+ api.use('/openapi.json', rateLimit(limiters.openapiDoc, clientIp));
392
+ // CLI cross-tool connect (cloud only, api/sso-connect.ts): presenting an
393
+ // exchange token is a credential presentation, walled per client address
394
+ // on its own bucket sized for a room (CLI_CONNECT_IP_LIMIT_PER_15_MIN): on
395
+ // the login wall (10 per 15 minutes) the 11th `<tool> login` behind one
396
+ // venue address was refused (PRDCT-2958, verifier F1). The token itself is
397
+ // one-time and the hub's walls bound each person. Conditional like the
398
+ // route itself: oss mounts zero hub surface.
399
+ if (deps.hubSso) {
400
+ api.use('/sso/cli-connect', rateLimit(limiters.cliConnect, clientIp));
401
+ }
402
+ // ── JSON pre-validation in front of the Better Auth mount (AF-1 + the
403
+ // update-user half of SL-B4, PRDCT-1358): Better Auth answers its own 500
404
+ // on a malformed/empty JSON body and on a NUL that reaches Postgres —
405
+ // responses app.onError never sees, so the app-level mappings cannot help.
406
+ // Registered here, after the size and depth caps, so the guard's body read
407
+ // is bounded.
408
+ api.use('/auth/*', authBodyGuard());
409
+ // Demo sign-in, the second door of "a session a pass opened lives only
410
+ // while its pass does": the sign-in library's mount runs BEFORE the
411
+ // credential resolver, and it is where a session authorizes an OAuth client
412
+ // and answers get-session. A dead pass's session is deleted here first, so
413
+ // the library sees none. A live pass's session is a visit: the library
414
+ // paths that would leave a credential, a grant or an account change behind
415
+ // it are refused here (identity/demo-pass-rules.ts
416
+ // `DEMO_SESSION_REFUSED_AUTH_PATHS`). And the library's own session deletes
417
+ // (a pass session's sign-out, a person's own revoke of one) delete the
418
+ // session row only: those sessions are ended the pass's way first
419
+ // (`endSessions`), so their OAuth tokens and link rows go with them.
420
+ // The revoking caller is verified by the library: a forged or dead cookie
421
+ // ends nothing.
422
+ if (demoSignIn) {
423
+ const judge = demoSessionJudge(db);
424
+ const demoPassService = new DemoPassService(db);
425
+ api.use('/auth/*', async (c, next) => {
426
+ if (!c.req.header('cookie'))
427
+ return next();
428
+ const verdict = await judge(c.req.raw.headers);
429
+ const authPath = c.req.path.startsWith(AUTH_MOUNT) ? c.req.path.slice(AUTH_MOUNT.length) : c.req.path;
430
+ if (verdict && verdict !== 'ended') {
431
+ if (demoSessionAuthRefusal(authPath)) {
432
+ return c.json(err('demo_session', 'A session opened by a demo link cannot do this: sign in with your password'), 403);
433
+ }
434
+ // A pass session that signs itself out ends the pass's way; the
435
+ // library then answers as usual.
436
+ if (authPath === '/sign-out')
437
+ await demoPassService.endSessions([verdict.sessionId]);
438
+ }
439
+ else if (authPath === '/revoke-session' ||
440
+ authPath === '/revoke-sessions' ||
441
+ authPath === '/revoke-other-sessions') {
442
+ // A person's OWN session revoking a pass session (a pass session is
443
+ // refused these paths above). The caller is verified by the library
444
+ // (`getSession`, the cookie's signature checked): a forged or dead
445
+ // cookie ends nothing. The body is read from a clone: the library
446
+ // gets the request untouched. The exact path compare loses nothing:
447
+ // the library's router answers 404 to any other spelling.
448
+ const verified = await auth.api.getSession({ headers: c.req.raw.headers }).catch(() => null);
449
+ if (verified?.session?.id && verified?.user?.id) {
450
+ let bodyToken = null;
451
+ try {
452
+ const body = await c.req.raw.clone().json();
453
+ if (body && typeof body === 'object' && typeof body.token === 'string') {
454
+ bodyToken = body.token;
455
+ }
456
+ }
457
+ catch {
458
+ // No JSON body: nothing names a session (the library answers as it does).
459
+ }
460
+ const passSessions = await demoPassService.passSessionsRevokedBy(authPath, { sessionId: verified.session.id, userId: verified.user.id }, bodyToken);
461
+ if (passSessions.length > 0)
462
+ await demoPassService.endSessions(passSessions);
463
+ }
464
+ }
465
+ return next();
466
+ });
467
+ }
468
+ // Better Auth owns /api/v1/auth/* (mounted before the credential middleware
469
+ // — it IS the credential machinery). On cloud the whole mount runs inside
470
+ // the hub-SSO login scope: an AsyncLocalStorage span carrying the verified
471
+ // hub assertion from the SSO callback's getUserInfo to its after-hook —
472
+ // request-scoped, so concurrent logins can never cross-wire (ADR 015).
473
+ const { hubSso } = deps;
474
+ // The change-email consume, wrapped (PRDCT-1437). The request is rewritten
475
+ // on a collision (rewriteChangeEmailCollision above), then two response
476
+ // normalizations keep the taken and free cases indistinguishable:
477
+ //
478
+ // - JSON success body: the change-email success echoes the resulting email
479
+ // in its `user` record — the one observable the no-op rewrite leaves
480
+ // differing between the taken and free cases. It is reachable ONLY without
481
+ // a `callbackURL` (a real consume always carries one and redirects, never
482
+ // hitting this JSON), so stripping `user` to null for every change-email
483
+ // consume touches no legitimate flow. Only the change/legacy branches
484
+ // return a non-null user; the plain verify-email success is already null.
485
+ // - a residual 5xx: the rewrite closes the DECIDABLE collision, but two
486
+ // concurrent mints claiming one FRESH address (no pre-existing account —
487
+ // so NOT an existence probe) can still lose at the DB. Map it to the same
488
+ // benign JSON so it never surfaces a raw 500. No redirect is issued here —
489
+ // every redirect stays Better Auth's, judged by its own originCheck, so
490
+ // this wrapper adds no open-redirect surface.
491
+ //
492
+ // Registered BEFORE the wildcard mount so the exact route wins.
493
+ api.on(['GET'], '/auth/verify-email', async (c) => {
494
+ const req = await rewriteChangeEmailCollision(auth, deps.authSecret, c.req.raw);
495
+ const res = await (hubSso ? hubSso.runWithLoginScope(() => auth.handler(req)) : auth.handler(req));
496
+ if (res.status === 200 && (res.headers.get('content-type') ?? '').includes('application/json')) {
497
+ const body = (await res
498
+ .clone()
499
+ .json()
500
+ .catch(() => null));
501
+ if (body && body.status === true && body.user != null) {
502
+ const headers = new Headers(res.headers);
503
+ headers.delete('content-length');
504
+ return new Response(JSON.stringify({ status: true, user: null }), { status: 200, headers });
505
+ }
506
+ return res;
507
+ }
508
+ // A 3xx: constrain the Location to this origin (Major 3). Better Auth's
509
+ // GET verify-email skips originCheck (no Origin header on a navigation) and
510
+ // redirects to any callbackURL — an open redirect for anyone holding a
511
+ // verify token, applied to BOTH branches so it stays uniform.
512
+ if (res.status >= 300 && res.status < 400) {
513
+ const location = res.headers.get('location');
514
+ if (location) {
515
+ const safe = safeRedirectLocation(location, c.req.url, env.PUBLIC_BASE_URL);
516
+ if (safe !== location) {
517
+ const headers = new Headers(res.headers);
518
+ headers.set('location', safe);
519
+ return new Response(null, { status: res.status, headers });
520
+ }
521
+ }
522
+ return res;
523
+ }
524
+ if (res.status < 500)
525
+ return res;
526
+ logger.warn({ status: res.status }, 'verify-email answered 5xx — mapped to the uniform non-revealing response');
527
+ return c.json({ status: true, user: null }, 200);
528
+ });
529
+ api.on(['GET', 'POST'], '/auth/*', (c) => hubSso ? hubSso.runWithLoginScope(() => auth.handler(c.req.raw)) : auth.handler(c.req.raw));
530
+ api.use('*', authContext({
531
+ registry: chassisRegistry,
532
+ isApiKeyToken: apiKeyService.isToken,
533
+ resolveApiKey: (token, requested) => apiKeyService.resolve(token, requested),
534
+ resolveOauthJwt: (token, requested) => deps.oauthJwt.resolve(token, requested),
535
+ keyFailureLimiter: limiters.apiKeyFailures,
536
+ clientIp,
537
+ // General per-principal quota (I3): runs inside authContext, after the
538
+ // credential resolves (the bucket key IS the principal identity) and
539
+ // before the scope gate. Public paths and /auth/* never reach it (no
540
+ // principal), so the auth-surface limiters above are not doubled.
541
+ // Limits ride the EntitlementService seam; buckets ride the same
542
+ // Redis-or-memory backend as every other limiter.
543
+ requestQuota: createRequestQuota({
544
+ entitlements: registry.entitlements,
545
+ make: limiters.make,
546
+ logger
547
+ }),
548
+ // Cloud edition's post-resolution veto (internal/federation.md P4);
549
+ // undefined on oss.
550
+ principalGate: deps.principalGate,
551
+ // Demo sign-in: a session a demo pass opened lives only while its pass
552
+ // does, and its audit rows carry the pass's id. Absent while the switch is off.
553
+ ...(demoSignIn ? { demoSession: demoSessionJudge(db) } : {}),
554
+ // The composed fail-closed allowlist: chassis rules, then the tool's rules.
555
+ requiredScopeFor: tool.scopes.requiredScopeFor,
556
+ guestForbiddenMessage: tool.copy.guestForbidden
557
+ }));
558
+ // A session a demo link opened is a visit (identity/demo-pass-rules.ts
559
+ // `DEMO_SESSION_REFUSED_API_ROUTES`): the routes that mint a credential or
560
+ // make something the person keeps refuse it here, at one place, right after
561
+ // the credential resolves and before anything claims an idempotency key.
562
+ // `demoPassId` is only ever set while the switch is on. The tool's own
563
+ // routes of the kind (its `demoSessionRefusedRoutes` slot) join the list here.
564
+ for (const rule of [...DEMO_SESSION_REFUSED_API_ROUTES, ...(tool.demoSessionRefusedRoutes ?? [])]) {
565
+ const gate = async (c, next) => {
566
+ if (c.get('demoPassId')) {
567
+ return c.json(err('demo_session', demoSessionApiRefusalMessage(rule.does)), 403);
568
+ }
569
+ return next();
570
+ };
571
+ if (rule.method === 'ALL')
572
+ api.use(rule.path, gate);
573
+ else
574
+ api.on(rule.method, rule.path, gate);
575
+ }
576
+ // Idempotency sits strictly BETWEEN authContext (it needs the resolved
577
+ // principal to scope claims) and auditMiddleware (a replayed short-circuit
578
+ // never reaches the audit layer, so a retry cannot land a second audit row).
579
+ api.use('*', idempotency({
580
+ db,
581
+ authSecret: deps.authSecret,
582
+ ...(tool.api.idempotencyTargets ? { toolTargets: tool.api.idempotencyTargets } : {})
583
+ }));
584
+ api.use('*', auditMiddleware(audit, clientIp, tool.api.auditExempt ? { exempt: tool.api.auditExempt } : {}));
585
+ // Instance id for usage-event sources, cached after first read.
586
+ let cachedInstanceId = null;
587
+ const instanceId = async () => {
588
+ if (cachedInstanceId)
589
+ return cachedInstanceId;
590
+ const [row] = await db.select({ id: instanceSettings.instanceId }).from(instanceSettings).limit(1);
591
+ cachedInstanceId = row?.id ?? 'unsetup';
592
+ return cachedInstanceId;
593
+ };
594
+ // ── The entitlement gate (the billing rail, §7) — AFTER the scope gate
595
+ // (inside authContext), the idempotency claim and the audit middleware,
596
+ // BEFORE every handler: one middleware per declared route, so a route
597
+ // that declares a meter, a limit or a feature is checked here and its
598
+ // usage event emitted here, and no handler checks or emits by hand. The
599
+ // dashboard, the CLI and the MCP tools all pass through it.
600
+ registerEntitlementGate(api, {
601
+ declarations: deps.entitlements.routes,
602
+ tool: deps.entitlements,
603
+ entitlements: registry.entitlements,
604
+ usage: registry.usage,
605
+ cloud: deps.entitlementCloud,
606
+ source: async () => ({ instanceId: await instanceId(), edition: env.EDITION, version: env.APP_VERSION }),
607
+ logger
608
+ });
609
+ // ── GET /instance — unauthenticated discovery ────────────────────────────
610
+ api.openapi(instanceRoute, async (c) => {
611
+ const [row] = await db.select().from(instanceSettings).limit(1);
612
+ c.header('Cache-Control', 'public, max-age=60');
613
+ return c.json({
614
+ name: row?.name ?? tool.identity.displayName,
615
+ instanceId: row?.instanceId ?? null,
616
+ edition: env.EDITION,
617
+ version: env.APP_VERSION,
618
+ apiVersion: 'v1',
619
+ setupRequired: !row,
620
+ auth: {
621
+ ...registry.identity.describe(),
622
+ // Cloud only (SL-1): the hint-cookie contract the dashboard's
623
+ // silent auto-connect reads — spread-gated on the same single
624
+ // `hub` switch as every cloud seam, so the oss discovery bytes
625
+ // never carry the key.
626
+ ...(hub
627
+ ? { sso: { hintCookieName: hub.hintCookieName, hintCookieDomain: hub.hintCookieDomain } }
628
+ : {})
629
+ },
630
+ features: { mcp: true, oauth: true, files: true },
631
+ // Present only while demo links sign people in here (the banner's cue).
632
+ ...(demoSignIn ? { demoSignIn: true } : {}),
633
+ // Present only on a cloud instance whose operator restricted the tool
634
+ // (PRDCT-2947): organizations are opened by Antasphere, not created here.
635
+ ...(hub && env.TOOL_RESTRICTED ? { restricted: true } : {}),
636
+ // What this version declares for the billing rail (§7): the hub seeds
637
+ // from it, staff read it, a client reads the limit it is held to.
638
+ entitlements: {
639
+ actions: deps.entitlements.actions,
640
+ limits: deps.entitlements.limits,
641
+ features: deps.entitlements.features
642
+ }
643
+ }, 200);
644
+ });
645
+ // ── POST /setup — one-shot first-boot wizard ─────────────────────────────
646
+ api.openapi(setupRoute, async (c) => {
647
+ const body = c.req.valid('json');
648
+ // BEFORE the token check: an instance reachable only over plaintext on a
649
+ // non-loopback origin must not accept the owner password at all, whether
650
+ // or not the caller holds the token (setup-transport.ts).
651
+ if (!env.ALLOW_INSECURE_SETUP && !isSecureSetupOrigin(env.PUBLIC_BASE_URL)) {
652
+ logger.warn({ publicBaseUrl: env.PUBLIC_BASE_URL }, 'refused setup over plaintext on a non-loopback origin');
653
+ return c.json(err('insecure_transport', 'Setup over plaintext HTTP on a non-loopback origin is refused. Put TLS in front, reach the instance through an SSH tunnel, or set ALLOW_INSECURE_SETUP=true.'), 403);
654
+ }
655
+ const [existing] = await db.select({ id: instanceSettings.id }).from(instanceSettings).limit(1);
656
+ if (existing) {
657
+ return c.json(err('already_setup', 'This instance has already been set up'), 410);
658
+ }
659
+ // ALWAYS required (PRDCT-1347): the claim decides who owns the instance.
660
+ // `deps.setupToken` is SETUP_TOKEN or the boot-generated token; it is
661
+ // null only for a set-up instance, which the 410 above already answered
662
+ // — so a null here is a claim with no credential to compare against and
663
+ // fails closed rather than falling through to a free claim.
664
+ if (!deps.setupToken || !constantTimeEquals(body.setupToken ?? '', deps.setupToken)) {
665
+ return c.json(err('invalid_setup_token', 'A valid setup token is required'), 403);
666
+ }
667
+ // Create the owner user through Better Auth so credential hashing and
668
+ // account rows follow its rules exactly. Better Auth cannot join our
669
+ // transaction, so this happens first; a setup that later loses the claim
670
+ // race leaves an orphaned user who can sign in but reaches nothing (401
671
+ // on every route — no membership).
672
+ let ownerUserId;
673
+ try {
674
+ const created = await auth.api.signUpEmail({
675
+ body: { email: body.owner.email, password: body.owner.password, name: body.owner.name }
676
+ });
677
+ ownerUserId = created.user.id;
678
+ }
679
+ catch (cause) {
680
+ // Retry after a crashed earlier attempt: the user may already exist.
681
+ // Reuse it ONLY if the presented credentials actually sign in — never
682
+ // attach a workspace to an account the caller cannot prove they own.
683
+ try {
684
+ const signedIn = await auth.api.signInEmail({
685
+ body: { email: body.owner.email, password: body.owner.password }
686
+ });
687
+ ownerUserId = signedIn.user.id;
688
+ }
689
+ catch {
690
+ logger.error({ err: cause }, 'setup: owner creation failed');
691
+ return c.json(err('owner_creation_failed', 'Could not create the owner user'), 400);
692
+ }
693
+ }
694
+ // The operator drove the wizard (holding the setup token when one is
695
+ // set) — their address needs no mailbox dance, and on the cloud edition
696
+ // it MUST be verified (D9, internal/federation.md): under hub-only login the
697
+ // operator's only entrance is the hub trusted-link, and Better Auth
698
+ // refuses to link a trusted provider onto an UNVERIFIED local email
699
+ // (requireLocalEmailVerified stays on). An unverified operator bricks a
700
+ // cloud instance at bootstrap. The hub's own setup does the same.
701
+ await db.update(userTable).set({ emailVerified: true }).where(eq(userTable.id, ownerUserId));
702
+ // Claim the singleton and — on the OSS edition — create the FIRST
703
+ // workspace + owner membership in ONE transaction: either the instance
704
+ // is fully set up or nothing persisted. The workspace goes through the
705
+ // registry's WorkspaceService (ADR 014) — the same path product flows
706
+ // use for every LATER workspace. On CLOUD (`hub` set — the same single
707
+ // switch as every cloud seam) setup creates NO workspace: every cloud
708
+ // workspace is a hub-org projection (user-scoped federation), so the
709
+ // operator bootstrap mints a verified, break-glass capable USER only —
710
+ // the singleton claim, the edition stamp, and the D9 emailVerified
711
+ // force above all stay.
712
+ const instanceId = ulid();
713
+ let workspaceId;
714
+ try {
715
+ workspaceId = await db.transaction(async (tx) => {
716
+ const claimed = await tx
717
+ .insert(instanceSettings)
718
+ // The edition stamp is what the R7 boot guard compares against
719
+ // (boot.ts, internal/federation.md): setup records the edition this
720
+ // instance was born under.
721
+ .values({
722
+ id: 'instance',
723
+ instanceId,
724
+ name: body.instanceName,
725
+ edition: env.EDITION,
726
+ // The durable operator record (CLOUD-3): survives the orphan purge.
727
+ operatorUserId: ownerUserId
728
+ })
729
+ .onConflictDoNothing()
730
+ .returning({ id: instanceSettings.id });
731
+ if (claimed.length === 0)
732
+ throw new SetupAlreadyDone();
733
+ if (hub)
734
+ return null;
735
+ const created = await registry.workspaces.create(body.instanceName, ownerUserId, tx);
736
+ return created.workspaceId;
737
+ });
738
+ }
739
+ catch (cause) {
740
+ if (cause instanceof SetupAlreadyDone) {
741
+ return c.json(err('already_setup', 'This instance has already been set up'), 410);
742
+ }
743
+ logger.error({ err: cause }, 'setup: transaction failed (nothing persisted)');
744
+ return c.json(err('internal', 'Setup failed; the instance remains uninitialized — retry'), 500);
745
+ }
746
+ chassisRegistry.events.emit('setup.completed', { workspaceId, instanceId });
747
+ logger.info({ workspaceId, instanceId }, 'first-boot setup completed');
748
+ // The generated token has done its one job; a leftover copy in the data
749
+ // volume is a secret with no purpose (it would ride in every backup).
750
+ await deps.clearGeneratedSetupToken().catch((cause) => {
751
+ logger.warn({ err: cause }, 'setup: could not remove the generated setup-token file');
752
+ });
753
+ // The genesis privileged action: audited directly (no principal in
754
+ // context yet — the middleware exempts /setup).
755
+ await audit.write({
756
+ workspaceId,
757
+ principal: null,
758
+ action: 'instance.setup',
759
+ resourceType: 'instance',
760
+ resourceId: instanceId,
761
+ requestId: c.get('requestId'),
762
+ metadata: { ownerUserId, ownerEmail: body.owner.email }
763
+ });
764
+ return c.json({ instanceId, workspaceId, ownerUserId }, 201);
765
+ });
766
+ /**
767
+ * SL-6 /me extras — CLOUD + SESSION callers only (both spread-gated
768
+ * below; machine credentials and every oss response never carry the
769
+ * keys):
770
+ *
771
+ * - `firstRunPending`: tool-local, retry-safe banner truth —
772
+ * `NOT EXISTS (user_onboarding row WHERE dismissed_at IS NOT NULL)`.
773
+ * A missing row (lost first-login insert) still shows the welcome;
774
+ * only an explicit dismissal (or the deploy backfill) hides it.
775
+ * - `ssoOnly`: the hint-watch discriminator — the user holds an
776
+ * `antasphere` account row AND no local credential (password) row.
777
+ * 'credential' is better-auth's password-account providerId (pinned
778
+ * 1.6.15; re-verify on bump). A break-glass-capable operator always
779
+ * has a credential row (setup mints it), so they are NEVER ssoOnly
780
+ * and the dashboard's hint-watch can never sign them out.
781
+ * - `hubDenied`: the organizations the person's last definitive hub
782
+ * list named as not opening this tool to them (a hint for the
783
+ * refusal page; the hub is the truth), and `hubNoAccessUrl`, the
784
+ * hub's page that lists whom to ask, ready-made with this tool's
785
+ * client id.
786
+ */
787
+ // `/me.canCreateWorkspace` and POST /workspaces judge through the SAME
788
+ // rule (api/workspaces.ts), so the flag never promises what the route
789
+ // refuses. Never throws into /me: a failed read answers false.
790
+ const creationPolicy = workspaceCreationPolicy({
791
+ maxPerUser: env.MAX_WORKSPACES_PER_USER,
792
+ restricted: env.TOOL_RESTRICTED,
793
+ cloud: deps.workspaceCloud
794
+ });
795
+ const creationWall = { person: limiters.workspaceCreate, address: limiters.workspaceCreateAddress };
796
+ const canCreateWorkspace = async (userId, via, address) => {
797
+ try {
798
+ if ((await workspaceCreationRefusal(db, creationPolicy, { userId, via })) !== null)
799
+ return false;
800
+ // Honest about the rate wall too (read-only, nothing is spent): a
801
+ // person the route would answer 429 is not offered the entry.
802
+ return !(await workspaceCreateWallClosed(creationWall, address, userId));
803
+ }
804
+ catch (cause) {
805
+ logger.warn({ err: cause }, '/me: canCreateWorkspace could not be computed — answering false');
806
+ return false;
807
+ }
808
+ };
809
+ const CREDENTIAL_PROVIDER_ID = 'credential';
810
+ // Cloud only (null on oss): the hub's no-access url, built once here where
811
+ // the hub is known and handed to the session extras by both callers.
812
+ const hubNoAccessUrl = hub
813
+ ? `${hub.issuerUrl.replace(/\/+$/, '')}/no-access?client_id=${encodeURIComponent(hub.clientId)}`
814
+ : null;
815
+ const cloudSessionExtras = async (userId, noAccessUrl) => {
816
+ const [onboardingRows, accountRows] = await Promise.all([
817
+ db
818
+ .select({ dismissedAt: userOnboarding.dismissedAt })
819
+ .from(userOnboarding)
820
+ .where(eq(userOnboarding.userId, userId))
821
+ .limit(1),
822
+ db.select({ providerId: account.providerId }).from(account).where(eq(account.userId, userId))
823
+ ]);
824
+ const providers = new Set(accountRows.map((r) => r.providerId));
825
+ return {
826
+ firstRunPending: onboardingRows[0]?.dismissedAt == null,
827
+ ssoOnly: providers.has(HUB_SSO_PROVIDER_ID) && !providers.has(CREDENTIAL_PROVIDER_ID),
828
+ hubDenied: deps.hubDeniedOrgs?.(userId) ?? [],
829
+ hubNoAccessUrl: noAccessUrl
830
+ };
831
+ };
832
+ // The zero-state re-admission: one cached reconcile pass, then the
833
+ // session is resolved again (the selection rule picks the workspace) and
834
+ // passed through the live gate exactly as authContext would. Any verdict
835
+ // other than a resolved, admitted principal keeps the zero state.
836
+ const readmitAfterReconcile = async (c, userId) => {
837
+ try {
838
+ await deps.reconcileForZeroState?.(userId);
839
+ const resolved = await resolveSessionPrincipal(c, chassisRegistry);
840
+ if (!resolved || resolved.userId !== userId)
841
+ return null;
842
+ const admitted = await admitPrincipal(c, resolved, deps.principalGate);
843
+ return admitted.ok ? admitted.principal : null;
844
+ }
845
+ catch (cause) {
846
+ logger.warn({ err: cause, userId }, '/me: zero-state re-admission failed — answering the zero state');
847
+ return null;
848
+ }
849
+ };
850
+ // ── GET /me — whoami across all credential paths ─────────────────────────
851
+ // No blanket requireAuth: the ZERO-MEMBERSHIP session state is route-local
852
+ // (user-scoped federation). A LIVE session whose user holds no active
853
+ // membership — the cloud operator before break-glass, a hub user whose
854
+ // last org was removed — resolves to a null principal (principal
855
+ // resolution requires a membership) but must NOT read as "signed out":
856
+ // /me answers 200 with `workspaces: [], workspace: null` so the dashboard
857
+ // renders the no-organization zero state instead of a login bounce.
858
+ // Machine credentials keep their 401: an invalid/zero-membership key or
859
+ // bearer dies inside authContext before this handler ever runs, so the
860
+ // session lookup below can only ever fire for cookie-authenticated
861
+ // requests.
862
+ api.openapi(meRoute, async (c) => {
863
+ let principal = c.get('principal');
864
+ let zeroStateUser = null;
865
+ if (!principal) {
866
+ // The zero state exists ONLY for the selector-less default request: a
867
+ // session that NAMED a workspace and failed its membership check must
868
+ // keep the fail-closed 401 (no oracle about the workspace, and the
869
+ // dashboard's stale-selection self-heal keys on that failure). With
870
+ // no selector, a live session resolving to null principal means
871
+ // exactly "zero active memberships" (the shared resolveMembership
872
+ // default rule).
873
+ if (c.req.header(ACTIVE_WORKSPACE_HEADER)?.trim()) {
874
+ return c.json(err('unauthenticated', 'Authentication required'), 401);
875
+ }
876
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
877
+ if (!session?.user) {
878
+ return c.json(err('unauthenticated', 'Authentication required'), 401);
879
+ }
880
+ // Re-admission (cloud): the live gate never runs for a principal-less
881
+ // session, so the zero-state read runs the reconciler's pass itself
882
+ // (cached ~10 s, single-flight, retry-throttled — a polling refusal
883
+ // page is not a hub call per request). When the pass re-projected a
884
+ // membership, the session resolves again through the SAME resolver
885
+ // and gate authContext uses, and the answer is the normal shape.
886
+ if (deps.reconcileForZeroState) {
887
+ principal = await readmitAfterReconcile(c, session.user.id);
888
+ }
889
+ if (!principal)
890
+ zeroStateUser = session.user;
891
+ }
892
+ if (!principal) {
893
+ const user = zeroStateUser;
894
+ if (!user)
895
+ return c.json(err('unauthenticated', 'Authentication required'), 401);
896
+ return c.json({
897
+ user: { id: user.id, email: user.email, name: user.name },
898
+ workspace: null,
899
+ role: null,
900
+ origin: null,
901
+ via: 'session',
902
+ scopes: null,
903
+ apiKeyExpiresAt: null,
904
+ workspaces: [],
905
+ activeWorkspaceId: null,
906
+ // The zero state's CTA target on cloud: organizations are created
907
+ // at the hub, never locally (internal/federation.md).
908
+ hubManageUrl: hubManaged?.manageUrl ?? null,
909
+ // oss: always false here — a zero-membership user is not an active
910
+ // member of this instance. cloud: true with a live hub link (their
911
+ // next organization is created from this very state).
912
+ canCreateWorkspace: hub ? await canCreateWorkspace(user.id, 'session', clientIp(c)) : false,
913
+ // Cloud + session extras (SL-6): the zero state is session-only
914
+ // by construction, so only the edition gate applies here.
915
+ ...(hubNoAccessUrl !== null ? await cloudSessionExtras(user.id, hubNoAccessUrl) : {})
916
+ }, 200);
917
+ }
918
+ // Workspaces this credential can name (user-scoped credential model):
919
+ // EVERY credential kind lists ALL of the user's active memberships — a
920
+ // credential is the user, and the org is a per-request parameter. The
921
+ // order mirrors the selection rule (default first, then oldest), but
922
+ // clients must read the `default` flag explicitly, never infer it from
923
+ // the index. (A PINNED key still lists everything: the pin restricts
924
+ // which workspace the key can REACH, not what its holder may see —
925
+ // /me answers about the user behind the credential.)
926
+ const memberships = await db
927
+ .select({
928
+ id: workspaces.id,
929
+ name: workspaces.name,
930
+ role: workspaceMembers.role,
931
+ centralAccountId: workspaces.centralAccountId,
932
+ hubStatus: workspaces.hubStatus,
933
+ isDefault: workspaceMembers.isDefault,
934
+ lookTheme: workspaces.lookTheme,
935
+ lookPattern: workspaces.lookPattern,
936
+ lookField: workspaces.lookField,
937
+ lookGrain: workspaces.lookGrain
938
+ })
939
+ .from(workspaceMembers)
940
+ .innerJoin(workspaces, eq(workspaceMembers.workspaceId, workspaces.id))
941
+ .where(and(eq(workspaceMembers.userId, principal.userId), eq(workspaceMembers.isActive, true)))
942
+ .orderBy(desc(workspaceMembers.isDefault), asc(workspaceMembers.createdAt), asc(workspaceMembers.id));
943
+ // hubOrigin is the BOOLEAN projection flag (P7); the hub org id rides
944
+ // beside it since PRDCT-2947 (the person's own organization ids, which
945
+ // `--org <name>` resolves against). Map explicitly, never spread the row.
946
+ const wireWorkspaces = memberships.map((m) => ({
947
+ id: m.id,
948
+ name: m.name,
949
+ role: m.role,
950
+ hubOrigin: m.centralAccountId !== null,
951
+ centralAccountId: m.centralAccountId,
952
+ look: { theme: m.lookTheme, pattern: m.lookPattern, field: m.lookField, grain: m.lookGrain },
953
+ suspended: m.hubStatus === 'suspended',
954
+ default: m.isDefault
955
+ }));
956
+ const active = wireWorkspaces.find((m) => m.id === principal.workspaceId);
957
+ return c.json({
958
+ user: { id: principal.userId, email: principal.email, name: principal.name },
959
+ workspace: {
960
+ id: principal.workspaceId,
961
+ name: active?.name ?? '',
962
+ // The ACTIVE workspace's flag comes from the principal itself —
963
+ // accountRef IS the request workspace's centralAccountId, read
964
+ // live by every credential resolver.
965
+ hubOrigin: Boolean(principal.accountRef),
966
+ look: active?.look ?? { theme: null, pattern: null, field: null, grain: null }
967
+ },
968
+ role: principal.role,
969
+ origin: principal.origin,
970
+ via: principal.via,
971
+ scopes: principal.scopes ? [...principal.scopes] : null,
972
+ apiKeyExpiresAt: principal.apiKeyExpiresAt ?? null,
973
+ workspaces: wireWorkspaces,
974
+ activeWorkspaceId: principal.workspaceId,
975
+ hubManageUrl: hubManaged && principal.accountRef ? hubManaged.manageUrl : null,
976
+ // A session a demo link opened creates no workspace (POST /workspaces
977
+ // refuses it before the rule), so it is not offered one.
978
+ canCreateWorkspace: c.get('demoPassId')
979
+ ? false
980
+ : await canCreateWorkspace(principal.userId, principal.via, clientIp(c)),
981
+ // Cloud + SESSION only (SL-6): machine credentials never carry the
982
+ // onboarding/hint-watch keys — the banner and the auto-sign-out are
983
+ // browser concerns.
984
+ ...(hubNoAccessUrl !== null && principal.via === 'session'
985
+ ? await cloudSessionExtras(principal.userId, hubNoAccessUrl)
986
+ : {})
987
+ }, 200);
988
+ });
989
+ // First-run onboarding dismiss (SL-6): cloud-only — an oss boot leaves
990
+ // POST /me/onboarding/dismiss to the JSON 404 terminator. Session-only
991
+ // (unlisted in the machine scope allowlist; route-local session check).
992
+ if (hub) {
993
+ registerOnboardingRoutes(api, { db, auth });
994
+ }
995
+ // Workspace creation (PRDCT-2444 / PRDCT-2443): one route, both editions.
996
+ // Session-only — unlisted in the machine scope allowlist, re-checked in
997
+ // the handler; self-audited into the NEW workspace (audit-exempt path).
998
+ registerWorkspaceRoutes(api, {
999
+ db,
1000
+ auth,
1001
+ audit,
1002
+ registry: chassisRegistry,
1003
+ logger,
1004
+ clientIp,
1005
+ wall: creationWall,
1006
+ maxPerUser: env.MAX_WORKSPACES_PER_USER,
1007
+ restricted: env.TOOL_RESTRICTED,
1008
+ cloud: deps.workspaceCloud,
1009
+ guestForbiddenMessage: tool.copy.guestForbidden,
1010
+ displayName: tool.identity.displayName
1011
+ });
1012
+ // ── Platform modules ─────────────────────────────────────────────────────
1013
+ // Break-glass first: it self-authenticates (superadmin sessions may carry
1014
+ // NO membership, so principal can be null) and is deliberately UNLISTED in
1015
+ // the machine scope allowlist — keys/tokens 403 fail-closed above.
1016
+ registerBreakGlassRoutes(api, {
1017
+ db,
1018
+ auth,
1019
+ audit,
1020
+ logger,
1021
+ superadminEmails: env.SUPERADMIN_EMAILS
1022
+ });
1023
+ // CLI email-OTP → API-key mint: PUBLIC pre-auth routes (listed in
1024
+ // PUBLIC_API_PATHS) riding the emailOTP plugin; rate-limited above. Also
1025
+ // registers DELETE /cli/auth/key — the authenticated self-revoke (CLI
1026
+ // logout), NOT public, open to machines under the tool's write scope. On
1027
+ // cloud (hubSso present) the mint pair refuses 403 cli_otp_disabled — the
1028
+ // D1 hub-only entrance closure; the self-revoke stays open.
1029
+ registerCliAuthRoutes(api, {
1030
+ db,
1031
+ auth,
1032
+ email,
1033
+ apiKeys: apiKeyService,
1034
+ audit,
1035
+ logger,
1036
+ hubSso,
1037
+ cliKeyScopes: tool.scopes.cliKey,
1038
+ routes: { cliAuthCompleteRoute },
1039
+ displayName: tool.identity.displayName
1040
+ });
1041
+ // CLI cross-tool connect (internal/federation.md P5): PUBLIC exchange of a
1042
+ // hub-minted 120 s JWT (+ its H3 offline grant) for a USER-scoped API
1043
+ // key. Registered ONLY on cloud — an oss boot leaves the path to the JSON
1044
+ // 404 terminator below, so the self-host edition provably carries zero
1045
+ // hub surface here.
1046
+ if (hubSso && deps.hubGrant) {
1047
+ registerSsoConnectRoutes(api, {
1048
+ db,
1049
+ auth,
1050
+ hubSso,
1051
+ grant: deps.hubGrant,
1052
+ apiKeys: apiKeyService,
1053
+ audit,
1054
+ logger,
1055
+ cliKeyScopes: tool.scopes.cliKey,
1056
+ routes: { ssoCliConnectRoute }
1057
+ });
1058
+ }
1059
+ // Single logout (SL-2): cloud-only like /sso/cli-connect — an oss boot
1060
+ // leaves POST /sso/logout to the JSON 404 terminator. Deliberately
1061
+ // UNLISTED in the machine scope allowlist: sessions only (incl.
1062
+ // zero-membership — the handler resolves the session route-locally).
1063
+ if (hubSso && deps.hubLogout && hub) {
1064
+ registerSsoLogoutRoutes(api, {
1065
+ auth,
1066
+ logout: deps.hubLogout,
1067
+ hint: {
1068
+ name: hub.hintCookieName,
1069
+ domain: hub.hintCookieDomain,
1070
+ secure: env.PUBLIC_BASE_URL.startsWith('https://')
1071
+ },
1072
+ logger
1073
+ });
1074
+ }
1075
+ registerMemberRoutes(api, {
1076
+ db,
1077
+ auth,
1078
+ publicBaseUrl: env.PUBLIC_BASE_URL,
1079
+ accountDeletion: deps.accountDeletion,
1080
+ hubManaged,
1081
+ guestTargetMessage: tool.copy.guestTarget,
1082
+ onMembershipRemoval: tool.membershipRemoval
1083
+ });
1084
+ // Teams: the tool's own on self-hosted (and in a cloud-local workspace), the
1085
+ // hub's projection in a hub-origin workspace, where the members' gate refuses the writes.
1086
+ registerTeamRoutes(api, { db, hubManaged });
1087
+ if (demoSignIn) {
1088
+ registerDemoPassRoutes(api, {
1089
+ db,
1090
+ publicBaseUrl: env.PUBLIC_BASE_URL,
1091
+ emailDomains: env.DEMO_SIGN_IN_EMAIL_DOMAINS,
1092
+ guestTargetMessage: tool.copy.guestTarget
1093
+ });
1094
+ }
1095
+ registerApiKeyRoutes(api, db, apiKeyService, { apiKeysListRoute, apiKeyCreateRoute, apiKeyRevokeRoute });
1096
+ registerInvitationRoutes(api, {
1097
+ db,
1098
+ env,
1099
+ auth,
1100
+ email,
1101
+ audit,
1102
+ registry: chassisRegistry,
1103
+ logger,
1104
+ mailBrand: { name: tool.identity.displayName, tagline: tool.copy.mail.tagline },
1105
+ inviteMail: { pitch: tool.copy.mail.invitePitch, preheader: tool.copy.mail.invitePreheader },
1106
+ hubManaged,
1107
+ // CLOUD-5: no local-password accounts minted through invitations on cloud.
1108
+ ssoOnly: Boolean(deps.hubSso),
1109
+ ...(demoSignIn ? { demoSession: demoSessionJudge(db) } : {})
1110
+ });
1111
+ // Projects: a subgroup of the workspace with its own members. Deliberately
1112
+ // handed NO `hubManaged`: project membership is the tool's own on both
1113
+ // editions, picked among the workspace's existing members.
1114
+ registerProjectRoutes(api, { db });
1115
+ registerAuditRoutes(api, db);
1116
+ registerExportRoutes(api, {
1117
+ db,
1118
+ env,
1119
+ storage: deps.storage,
1120
+ audit,
1121
+ limiters,
1122
+ clientIp,
1123
+ logger,
1124
+ workspaceExportRoute: tool.scopes.contractRoutes.workspaceExportRoute,
1125
+ // The tool's export entries, bound to its domain once, here. No slot: no field.
1126
+ ...(tool.api.exportEntries ? { exportEntries: tool.api.exportEntries(deps.domain) } : {})
1127
+ });
1128
+ registerFileRoutes(api, {
1129
+ service: deps.fileService,
1130
+ storage: deps.storage,
1131
+ registry: chassisRegistry,
1132
+ env,
1133
+ logger,
1134
+ // The tool's blob policy. ADR 011 sharp edge closed: a blob referenced
1135
+ // by a live version manifest of the tool's resource is not deletable
1136
+ // through the generic files surface; SL-B1: the generic files surface
1137
+ // authorizes per RESOURCE, not per workspace — the ADR 013 policy expressed
1138
+ // as a WHERE predicate.
1139
+ ...tool.api.filePolicy(deps.domain),
1140
+ fileDeleteRoute: tool.scopes.contractRoutes.fileDeleteRoute,
1141
+ fileInUse: tool.copy.fileInUse
1142
+ });
1143
+ // The tool's own routes: AFTER the files routes, BEFORE the OpenAPI
1144
+ // document and the JSON 404. The tool keeps its own internal order.
1145
+ const routesContext = { ...rateLimitContext, instanceId };
1146
+ tool.api.routes?.(api, routesContext, deps.domain);
1147
+ // Generated ONCE here, at the end of route registration — never per
1148
+ // request (api/openapi-doc.ts explains why that mattered).
1149
+ registerOpenApiDoc(api, {
1150
+ openapi: '3.1.0',
1151
+ info: {
1152
+ title: 'Platform API',
1153
+ version: env.APP_VERSION
1154
+ },
1155
+ servers: [{ url: '/api/v1' }]
1156
+ });
1157
+ // ── JSON 404 terminator — registered LAST ────────────────────────────────
1158
+ // An unmatched /api/v1 path answers the API wire shape instead of falling
1159
+ // through to the SPA catch-all (which would hand a browser session the
1160
+ // dashboard HTML with a 200). Machine principals never reach this: the
1161
+ // fail-closed scope gate already 403s every unlisted endpoint above.
1162
+ api.all('*', (c) => c.json(err('not_found', 'Not found'), 404));
1163
+ return api;
1164
+ }
1165
+ //# sourceMappingURL=create-api.js.map