@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,868 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import {
4
+ createDatabase,
5
+ createTestApp,
6
+ expectBootRefusal,
7
+ extractCookie,
8
+ readJson,
9
+ RecordingEmailDriver,
10
+ startPostgres,
11
+ type TestApp,
12
+ host
13
+ } from './helpers.js';
14
+ import * as sso from './sso-helpers.js';
15
+
16
+ /**
17
+ * The edition split, Phase 2 (internal/federation.md):
18
+ * - setup stamps the instance's edition; the R7 boot guard refuses an env
19
+ * EDITION that differs on an already-set-up instance, and
20
+ * EDITION_CHANGE_ALLOWED=true is the one-boot acknowledgement that
21
+ * re-stamps it;
22
+ * - a fresh database boots under any edition (cloud starts from a fresh DB);
23
+ * - EDITION=oss keeps today's behavior byte-identical — no hub config read.
24
+ *
25
+ * The SSO wiring itself is Phase 3; these tests pin the scaffolding only.
26
+ */
27
+
28
+ const OWNER = { email: 'owner@edition.test', name: 'Ed Owner', password: 'ed-owner-password-123' };
29
+
30
+ /** The full hub block a cloud boot requires (dev-shaped values). */
31
+ const HUB_ENV = {
32
+ EDITION: 'cloud',
33
+ HUB_ISSUER_URL: 'http://hub.localhost:3300',
34
+ HUB_CLIENT_ID: host.hubClientId,
35
+ HUB_CLIENT_SECRET: 'integration-test-hub-secret-0001'
36
+ };
37
+
38
+ const json = (body: unknown) => ({
39
+ method: 'POST',
40
+ headers: { 'content-type': 'application/json' },
41
+ body: JSON.stringify(body)
42
+ });
43
+
44
+ let container: StartedPostgreSqlContainer;
45
+
46
+ beforeAll(async () => {
47
+ container = await startPostgres();
48
+ });
49
+
50
+ afterAll(async () => {
51
+ await container?.stop();
52
+ });
53
+
54
+ describe('R7 edition-flip boot guard', () => {
55
+ let connectionString: string;
56
+
57
+ beforeAll(async () => {
58
+ connectionString = await createDatabase(container, 'edition_guard');
59
+ const app = await createTestApp(connectionString);
60
+ const res = await app.app.request(
61
+ '/api/v1/setup',
62
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Ed', owner: OWNER })
63
+ );
64
+ expect(res.status).toBe(201);
65
+ await app.stop();
66
+ });
67
+
68
+ it('setup stamped the instance as oss (the test boot default)', async () => {
69
+ const app = await createTestApp(connectionString);
70
+ const { rows } = await app.db.pool.query<{ edition: string }>(`SELECT edition FROM instance_settings`);
71
+ expect(rows).toEqual([{ edition: 'oss' }]);
72
+ await app.stop();
73
+ });
74
+
75
+ it('refuses to boot EDITION=cloud on the populated oss instance', async () => {
76
+ await expectBootRefusal(
77
+ createTestApp(connectionString, HUB_ENV),
78
+ /refusing to boot: EDITION=cloud but this instance was set up as 'oss'/
79
+ );
80
+ });
81
+
82
+ it('EDITION_CHANGE_ALLOWED=true re-stamps and boots; the flip then holds without the flag', async () => {
83
+ // The flip pre-flights (PRDCT-1356 EDIT-1): an unprojected workspace
84
+ // refuses it. Project the setup workspace first — cloud-lifecycle.test.ts
85
+ // pins the refusal itself.
86
+ const prep = await createTestApp(connectionString);
87
+ await prep.db.pool.query(
88
+ `UPDATE workspaces SET central_account_id = '44444444-aaaa-4bbb-8ccc-0000000000ed'`
89
+ );
90
+ await prep.stop();
91
+ const flipped = await createTestApp(connectionString, { ...HUB_ENV, EDITION_CHANGE_ALLOWED: 'true' });
92
+ const { rows } = await flipped.db.pool.query<{ edition: string }>(
93
+ `SELECT edition FROM instance_settings`
94
+ );
95
+ expect(rows).toEqual([{ edition: 'cloud' }]);
96
+ await flipped.stop();
97
+
98
+ // The stamp moved: a plain cloud boot now succeeds, and an oss boot is
99
+ // now the refused flip (the guard is symmetric) — and the REVERSE flip
100
+ // is refused even with the flag (EDIT-4, cloud-lifecycle.test.ts).
101
+ const cloudAgain = await createTestApp(connectionString, HUB_ENV);
102
+ await cloudAgain.stop();
103
+ await expectBootRefusal(
104
+ createTestApp(connectionString),
105
+ /refusing to boot: EDITION=oss but this instance was set up as 'cloud'/
106
+ );
107
+ });
108
+ });
109
+
110
+ describe('cloud edition on a fresh database', () => {
111
+ let app: TestApp;
112
+
113
+ beforeAll(async () => {
114
+ app = await createTestApp(await createDatabase(container, 'edition_cloud'), HUB_ENV);
115
+ });
116
+
117
+ afterAll(async () => {
118
+ await app.stop();
119
+ });
120
+
121
+ it('boots (fresh DB — nothing to guard) and setup stamps edition=cloud', async () => {
122
+ const res = await app.app.request(
123
+ '/api/v1/setup',
124
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'EdCloud', owner: OWNER })
125
+ );
126
+ expect(res.status).toBe(201);
127
+ const body = await readJson(res);
128
+ // Cloud setup creates NO workspace (user-scoped federation): every
129
+ // cloud workspace is a hub-org projection — the operator bootstrap
130
+ // mints a verified USER only, and the response says so honestly.
131
+ expect(body.workspaceId).toBeNull();
132
+ expect(body.ownerUserId).toBeTruthy();
133
+ const workspaces = await app.db.pool.query(`SELECT id FROM workspaces`);
134
+ expect(workspaces.rows).toHaveLength(0);
135
+ const memberships = await app.db.pool.query(`SELECT id FROM workspace_members`);
136
+ expect(memberships.rows).toHaveLength(0);
137
+ const { rows } = await app.db.pool.query<{ edition: string }>(`SELECT edition FROM instance_settings`);
138
+ expect(rows).toEqual([{ edition: 'cloud' }]);
139
+ });
140
+
141
+ it('the zero-membership operator session gets the /me zero state (200), not a login bounce', async () => {
142
+ const signIn = await app.app.request(
143
+ '/api/v1/auth/sign-in/email',
144
+ json({ email: OWNER.email, password: OWNER.password })
145
+ );
146
+ expect(signIn.status).toBe(200);
147
+ const cookie = extractCookie(signIn);
148
+ const me = await app.app.request('/api/v1/me', { headers: { cookie } });
149
+ expect(me.status).toBe(200);
150
+ const body = await readJson(me);
151
+ expect(body.user.email).toBe(OWNER.email);
152
+ expect(body.workspaces).toEqual([]);
153
+ expect(body.workspace).toBeNull();
154
+ expect(body.activeWorkspaceId).toBeNull();
155
+ expect(body.role).toBeNull();
156
+ expect(body.origin).toBeNull();
157
+ expect(body.via).toBe('session');
158
+ // The zero state's CTA target: orgs are created at the hub.
159
+ expect(body.hubManageUrl).toBe(HUB_ENV.HUB_ISSUER_URL);
160
+ });
161
+
162
+ it('unauthenticated and machine callers keep their 401 on /me (zero state is session-only)', async () => {
163
+ const anon = await app.app.request('/api/v1/me');
164
+ expect(anon.status).toBe(401);
165
+ // A syntactically valid but unknown API key still dies inside
166
+ // authContext — the route-local session path never runs for bearers.
167
+ const fakeKey = `${host.identity.apiKeyPrefix}_AAAAAAAA_` + 'a'.repeat(43);
168
+ const withKey = await app.app.request('/api/v1/me', {
169
+ headers: { authorization: `Bearer ${fakeKey}`, 'x-forwarded-for': '10.77.0.1' }
170
+ });
171
+ expect(withKey.status).toBe(401);
172
+ });
173
+
174
+ it('a session NAMING a workspace keeps the fail-closed 401 — the zero state is selector-less only', async () => {
175
+ // A failed explicit selection must never soften into the 200 zero state:
176
+ // it is the no-oracle posture AND the dashboard's stale-selection
177
+ // self-heal signal (it drops the stored selector only on failure).
178
+ const signIn = await app.app.request(
179
+ '/api/v1/auth/sign-in/email',
180
+ json({ email: OWNER.email, password: OWNER.password })
181
+ );
182
+ const cookie = extractCookie(signIn);
183
+ const res = await app.app.request('/api/v1/me', {
184
+ headers: { cookie, 'x-workspace-id': '99999999-9999-4999-8999-999999999999' }
185
+ });
186
+ expect(res.status).toBe(401);
187
+ });
188
+
189
+ it('reports edition=cloud in discovery', async () => {
190
+ const info = await readJson(await app.app.request('/api/v1/instance'));
191
+ expect(info.edition).toBe('cloud');
192
+ });
193
+
194
+ it('exposes the hub SSO hint-cookie contract in discovery (SL-1)', async () => {
195
+ const info = await readJson(await app.app.request('/api/v1/instance'));
196
+ // Defaults: the cross-repo cookie name; domain = issuer host minus its
197
+ // first label (hub.localhost → localhost).
198
+ expect(info.auth.sso).toEqual({ hintCookieName: 'ant_sso_hint', hintCookieDomain: 'localhost' });
199
+ });
200
+
201
+ it('lands OAuth-callback AS errors on /login (cloud onAPIError.errorURL, SL-1)', async () => {
202
+ // A prompt=none authorize against a hub with no session answers
203
+ // ?error=login_required at the callback; the genericOAuth callback
204
+ // redirects AS errors to onAPIError.errorURL BEFORE parseState (1.6.15),
205
+ // so no state/cookie is needed to pin the landing.
206
+ const res = await app.app.request(
207
+ '/api/v1/auth/oauth2/callback/antasphere?error=login_required&error_description=Login%20required'
208
+ );
209
+ expect(res.status).toBe(302);
210
+ const location = res.headers.get('location')!;
211
+ expect(location.startsWith('http://localhost:3000/login?error=login_required')).toBe(true);
212
+ // The error exit mints no session.
213
+ expect(res.headers.get('set-cookie')).toBeNull();
214
+ });
215
+
216
+ it('setup minted the operator emailVerified=true (D9 — the trusted-link bootstrap)', async () => {
217
+ // Under hub-only login (D1, Phase 3) the operator can only enter via the
218
+ // hub trusted-link, which Better Auth refuses onto an unverified local
219
+ // email. An unverified operator = a bricked cloud instance. Pinned here
220
+ // so no auth refactor can silently reopen the trap.
221
+ const { rows } = await app.db.pool.query<{ email_verified: boolean }>(
222
+ `SELECT email_verified FROM "user" WHERE email = $1`,
223
+ [OWNER.email]
224
+ );
225
+ expect(rows).toEqual([{ email_verified: true }]);
226
+ });
227
+
228
+ it('advertises the hub-only human entrance (D1): antasphere + machine methods, no password/OTP', async () => {
229
+ const info = await readJson(await app.app.request('/api/v1/instance'));
230
+ expect(info.auth.methods).toContain('antasphere');
231
+ // Hub-only posture (Phase 3): the login page renders ONLY the SSO
232
+ // button; password/OTP/google are hidden (the machinery stays wired for
233
+ // the break-glass CLI, verified in the next test). Machine entrances
234
+ // are edition-independent.
235
+ expect(info.auth.methods).not.toContain('password');
236
+ expect(info.auth.methods).not.toContain('email-otp');
237
+ expect(info.auth.methods).not.toContain('google');
238
+ expect(info.auth.methods).toContain('api-key');
239
+ expect(info.auth.methods).toContain('oauth');
240
+ // Credentials and email are the hub's to manage; local 2FA guards
241
+ // sign-in surfaces the cloud page no longer offers.
242
+ expect(info.auth.passwordReset).toBe(false);
243
+ expect(info.auth.emailChange).toBe(false);
244
+ expect(info.auth.twoFactor).toBe(false);
245
+ });
246
+
247
+ it('the provider-grant routes stay closed on CLOUD too (PRDCT-1354 pin; the oss pin lives in minted-credentials)', async () => {
248
+ // On cloud the stored provider grant IS the hub grant (ADR 019): handing
249
+ // it to a caller — or rotating it outside HubGrantService's lock — is
250
+ // the seam PRDCT-1370 hardens. The closure is unconditional in code;
251
+ // this pins it on a cloud boot so an edition-gated regression cannot
252
+ // pass on the oss-only pin alone.
253
+ const signIn = await app.app.request(
254
+ '/api/v1/auth/sign-in/email',
255
+ json({ email: OWNER.email, password: OWNER.password })
256
+ );
257
+ expect(signIn.status).toBe(200);
258
+ const cookie = signIn.headers.get('set-cookie')!.split(';')[0]!;
259
+ for (const path of ['/api/v1/auth/get-access-token', '/api/v1/auth/refresh-token']) {
260
+ for (const headers of [{ cookie }, {}]) {
261
+ const res = await app.app.request(path, {
262
+ ...json({ providerId: 'antasphere' }),
263
+ headers: { 'content-type': 'application/json', ...headers }
264
+ });
265
+ expect(res.status, `${path} with ${Object.keys(headers).join(',') || 'no'} cookie`).toBe(403);
266
+ const body = await res.text();
267
+ expect(body).toContain('provider_grant_forbidden');
268
+ expect(body).not.toMatch(/accessToken|idToken|refreshToken|access_token/);
269
+ }
270
+ }
271
+ });
272
+
273
+ it('local password sign-in stays WIRED though hidden (the break-glass door, D1)', async () => {
274
+ const signIn = await app.app.request(
275
+ '/api/v1/auth/sign-in/email',
276
+ json({ email: OWNER.email, password: OWNER.password })
277
+ );
278
+ expect(signIn.status).toBe(200);
279
+ const cookie = signIn.headers.get('set-cookie')!.split(';')[0]!;
280
+ const me = await app.app.request('/api/v1/me', { headers: { cookie } });
281
+ expect(me.status).toBe(200);
282
+ expect((await readJson(me)).user.email).toBe(OWNER.email);
283
+ });
284
+ });
285
+
286
+ describe('cloud edition closes the local password-reset surface (P8, ADR 017)', () => {
287
+ // The P3 residual: on cloud, a hub-JIT user (no credential account) could
288
+ // use the reset surface to SET a local password, then /sign-in/email to
289
+ // mint sessions that skip the per-login SSO re-sync. D1's hub-only posture
290
+ // closes the WHOLE reset class (core + emailOTP alias routes) while the
291
+ // break-glass operator door — /sign-in/email with the SETUP password plus
292
+ // the break-glass endpoints — stays fully independent of it.
293
+ let app: TestApp;
294
+ let email: RecordingEmailDriver;
295
+
296
+ // The reset wall is 5/10min per IP (rate-limit.ts) and several reset paths
297
+ // share one limiter — a unique forwarded address per request (TRUST_PROXY
298
+ // is on in tests) keeps this suite off one shared 'unknown' bucket, so it
299
+ // pins the 403 REFUSAL, never a 429.
300
+ let ipCounter = 0;
301
+ const jsonIp = (body: unknown, headers: Record<string, string> = {}) => ({
302
+ method: 'POST',
303
+ headers: {
304
+ 'content-type': 'application/json',
305
+ 'x-forwarded-for': `198.51.100.${++ipCounter % 250}`,
306
+ ...headers
307
+ },
308
+ body: JSON.stringify(body)
309
+ });
310
+
311
+ beforeAll(async () => {
312
+ email = new RecordingEmailDriver();
313
+ // A DELIVERING mailer + superadmin: the strongest configuration — on oss
314
+ // this would open self-serve reset; on cloud every reset door must still
315
+ // refuse while break-glass keeps working.
316
+ app = await createTestApp(
317
+ await createDatabase(container, 'edition_cloud_reset'),
318
+ { ...HUB_ENV, SUPERADMIN_EMAILS: OWNER.email },
319
+ { email }
320
+ );
321
+ const res = await app.app.request(
322
+ '/api/v1/setup',
323
+ jsonIp({ setupToken: 'integration-test-setup-token', instanceName: 'EdReset', owner: OWNER })
324
+ );
325
+ expect(res.status).toBe(201);
326
+ // Cloud setup mints no workspace — the cloud-LOCAL workspace this
327
+ // suite's reset-link + break-glass fixtures need is seeded directly.
328
+ await sso.seedLocalWorkspace(app, 'EdReset', OWNER.email);
329
+ });
330
+
331
+ afterAll(async () => {
332
+ await app.stop();
333
+ });
334
+
335
+ async function resetTokenCount(): Promise<number> {
336
+ const { rows } = await app.db.pool.query<{ n: number }>(
337
+ `SELECT count(*)::int AS n FROM verification WHERE identifier LIKE 'reset-password:%'`
338
+ );
339
+ return rows[0]!.n;
340
+ }
341
+
342
+ it('refuses every reset-shaped route (core + emailOTP) despite the delivering mailer', async () => {
343
+ const attempts: Array<[string, Record<string, unknown>]> = [
344
+ [
345
+ '/api/v1/auth/request-password-reset',
346
+ { email: OWNER.email, redirectTo: 'http://localhost:3000/reset-password' }
347
+ ],
348
+ ['/api/v1/auth/reset-password', { newPassword: 'sso-bypass-password-99', token: 'whatever' }],
349
+ ['/api/v1/auth/email-otp/request-password-reset', { email: OWNER.email }],
350
+ ['/api/v1/auth/forget-password/email-otp', { email: OWNER.email }],
351
+ [
352
+ '/api/v1/auth/email-otp/reset-password',
353
+ { email: OWNER.email, otp: '123456', password: 'sso-bypass-password-99' }
354
+ ]
355
+ ];
356
+ for (const [path, body] of attempts) {
357
+ const res = await app.app.request(path, jsonIp(body));
358
+ expect(res.status, path).toBe(403);
359
+ }
360
+ // The tokened GET callback (the mailed-link landing) refuses too.
361
+ const cb = await app.app.request('/api/v1/auth/reset-password/some-token', {
362
+ headers: { 'x-forwarded-for': `198.51.100.${++ipCounter % 250}` }
363
+ });
364
+ expect(cb.status).toBe(403);
365
+ // Nothing was mailed and no reset token was minted.
366
+ expect(email.sent).toHaveLength(0);
367
+ expect(await resetTokenCount()).toBe(0);
368
+ });
369
+
370
+ it('refuses to CONSUME a valid reset token — even one minted straight into the store', async () => {
371
+ // An attacker (or a leftover admin link) holding a well-formed token in
372
+ // Better Auth's own verification table still cannot set a password: the
373
+ // refusal is on the route, not on token acquisition.
374
+ const { rows } = await app.db.pool.query<{ id: string }>(`SELECT id FROM "user" WHERE email = $1`, [
375
+ OWNER.email
376
+ ]);
377
+ await app.db.pool.query(
378
+ `INSERT INTO verification (id, identifier, value, expires_at) VALUES ($1, $2, $3, $4)`,
379
+ [crypto.randomUUID(), 'reset-password:direct-mint-token', rows[0]!.id, new Date(Date.now() + 3600_000)]
380
+ );
381
+ const res = await app.app.request(
382
+ '/api/v1/auth/reset-password',
383
+ jsonIp({ newPassword: 'sso-bypass-password-99', token: 'direct-mint-token' })
384
+ );
385
+ expect(res.status).toBe(403);
386
+ // The password did not change: the setup password still signs in, the
387
+ // attempted one does not.
388
+ const bypass = await app.app.request(
389
+ '/api/v1/auth/sign-in/email',
390
+ jsonIp({ email: OWNER.email, password: 'sso-bypass-password-99' })
391
+ );
392
+ expect(bypass.status).toBe(401);
393
+ // Remove the hand-minted row so the next test's "the mint added nothing"
394
+ // count starts from zero again.
395
+ await app.db.pool.query(`DELETE FROM verification WHERE identifier = 'reset-password:direct-mint-token'`);
396
+ });
397
+
398
+ it('refuses the admin reset-link mint on a cloud-LOCAL workspace (password_reset_disabled)', async () => {
399
+ // Hub-origin workspaces already refuse at the P7 subtree gate
400
+ // (hub_managed); this pins the cloud-LOCAL half — otherwise an admin
401
+ // would mint links that dead-end on the refused /reset-password.
402
+ const signIn = await app.app.request(
403
+ '/api/v1/auth/sign-in/email',
404
+ jsonIp({ email: OWNER.email, password: OWNER.password })
405
+ );
406
+ expect(signIn.status).toBe(200);
407
+ const cookie = signIn.headers.get('set-cookie')!.split(';')[0]!;
408
+ const { members } = await readJson(await app.app.request('/api/v1/members', { headers: { cookie } }));
409
+ const minted = await app.app.request(`/api/v1/members/${members[0].id}/reset-link`, {
410
+ method: 'POST',
411
+ headers: { cookie }
412
+ });
413
+ expect(minted.status).toBe(403);
414
+ expect((await readJson(minted)).error.code).toBe('password_reset_disabled');
415
+ expect(await resetTokenCount()).toBe(0);
416
+ });
417
+
418
+ it('refuses the admin change-email-link mint on a cloud-LOCAL workspace (email_change_disabled)', async () => {
419
+ // AUTH-1 (PRDCT-1354): the sibling mint had NO cloud closure at all, yet
420
+ // its token is the STRONGER of the two — consuming it signs the target in
421
+ // AND rewrites the address the hub identity is keyed on, while on cloud
422
+ // the email is the hub's to own (D10 re-syncs it every login). Same shape
423
+ // as the reset-link closure above, distinct code.
424
+ const signIn = await app.app.request(
425
+ '/api/v1/auth/sign-in/email',
426
+ jsonIp({ email: OWNER.email, password: OWNER.password })
427
+ );
428
+ expect(signIn.status).toBe(200);
429
+ const cookie = signIn.headers.get('set-cookie')!.split(';')[0]!;
430
+ const { members } = await readJson(await app.app.request('/api/v1/members', { headers: { cookie } }));
431
+ const minted = await app.app.request(`/api/v1/members/${members[0].id}/change-email-link`, {
432
+ ...jsonIp({ newEmail: 'hijacked@edreset.test' }, { cookie })
433
+ });
434
+ expect(minted.status).toBe(403);
435
+ const body = await readJson(minted);
436
+ expect(body.error.code).toBe('email_change_disabled');
437
+ expect(body.verifyUrl).toBeUndefined();
438
+ });
439
+
440
+ it('refuses the self-serve email change (403 email_change_disabled, naming the account site), the address unchanged and nothing mailed', async () => {
441
+ // PRDCT-2818: the dashboard hides the form on cloud, the route was open.
442
+ // A signed-in caller with a DELIVERING mailer is the strongest case: on
443
+ // oss this very call mails the verification link.
444
+ const signIn = await app.app.request(
445
+ '/api/v1/auth/sign-in/email',
446
+ jsonIp({ email: OWNER.email, password: OWNER.password })
447
+ );
448
+ expect(signIn.status).toBe(200);
449
+ const cookie = signIn.headers.get('set-cookie')!.split(';')[0]!;
450
+ const mailsBefore = email.sent.length;
451
+ const attempts: Array<[string, Record<string, unknown>]> = [
452
+ ['/api/v1/auth/change-email', { newEmail: 'moved@edreset.test', callbackURL: '/account' }],
453
+ ['/api/v1/auth/email-otp/request-email-change', { newEmail: 'moved@edreset.test' }],
454
+ ['/api/v1/auth/email-otp/change-email', { newEmail: 'moved@edreset.test', otp: '000000' }]
455
+ ];
456
+ for (const [path, body] of attempts) {
457
+ const res = await app.app.request(path, jsonIp(body, { cookie }));
458
+ expect(res.status, path).toBe(403);
459
+ const answer = await readJson(res);
460
+ expect(JSON.stringify(answer), path).toContain('email_change_disabled');
461
+ expect(JSON.stringify(answer), path).toContain(HUB_ENV.HUB_ISSUER_URL.replace(/\/+$/, ''));
462
+ }
463
+ expect(email.sent.length).toBe(mailsBefore);
464
+ const { rows } = await app.db.pool.query<{ n: number }>(
465
+ `SELECT count(*)::int AS n FROM "user" WHERE email = $1`,
466
+ [OWNER.email]
467
+ );
468
+ expect(rows[0]!.n).toBe(1);
469
+ });
470
+
471
+ it('the operator door survives the closure: /sign-in/email + break-glass need no reset route', async () => {
472
+ // The full recovery chain, reset-free: sign in with the SETUP password
473
+ // (D9 minted the operator emailVerified=true), then claim-ownership and
474
+ // reset-2fa — the ADR 010 surface — both work.
475
+ const signIn = await app.app.request(
476
+ '/api/v1/auth/sign-in/email',
477
+ jsonIp({ email: OWNER.email, password: OWNER.password })
478
+ );
479
+ expect(signIn.status).toBe(200);
480
+ const cookie = signIn.headers.get('set-cookie')!.split(';')[0]!;
481
+
482
+ const claim = await app.app.request('/api/v1/admin/break-glass/claim-ownership', {
483
+ ...jsonIp({}),
484
+ headers: { 'content-type': 'application/json', cookie }
485
+ });
486
+ expect(claim.status).toBe(200);
487
+ expect((await readJson(claim)).role).toBe('owner');
488
+
489
+ const { rows } = await app.db.pool.query<{ id: string }>(`SELECT id FROM "user" WHERE email = $1`, [
490
+ OWNER.email
491
+ ]);
492
+ const reset2fa = await app.app.request('/api/v1/admin/break-glass/reset-2fa', {
493
+ ...jsonIp({ userId: rows[0]!.id }),
494
+ headers: { 'content-type': 'application/json', cookie }
495
+ });
496
+ expect(reset2fa.status).toBe(200);
497
+ });
498
+ });
499
+
500
+ describe('cloud edition closes the OTP entrances (D1 hub-only credentials)', () => {
501
+ // The ADR 017 §7 charter call, now taken: beyond the P8 reset closure,
502
+ // the two remaining non-SSO credential-MINTING entrances refuse on cloud
503
+ // so every human session AND CLI key traces through the hub (its audit
504
+ // log is the complete access record). Both were hub-gated per request by
505
+ // the P4 re-assertion already — posture, not a hole. The self-revoke
506
+ // (DELETE /cli/auth/key) is the deliberate exception: revocation narrows
507
+ // access, and the CLI's `logout --org` needs it.
508
+ let app: TestApp;
509
+ let email: RecordingEmailDriver;
510
+
511
+ let ipCounter = 0;
512
+ const jsonIp = (body: unknown, headers: Record<string, string> = {}) => ({
513
+ method: 'POST',
514
+ headers: {
515
+ 'content-type': 'application/json',
516
+ 'x-forwarded-for': `198.51.101.${++ipCounter % 250}`,
517
+ ...headers
518
+ },
519
+ body: JSON.stringify(body)
520
+ });
521
+
522
+ beforeAll(async () => {
523
+ email = new RecordingEmailDriver();
524
+ // A DELIVERING mailer AND Google credentials set: the strongest
525
+ // misconfiguration — on oss both would open OTP login and Google social
526
+ // sign-in; on cloud every one of them must be closed by construction.
527
+ app = await createTestApp(
528
+ await createDatabase(container, 'edition_cloud_otp'),
529
+ { ...HUB_ENV, GOOGLE_CLIENT_ID: 'cloud-google-id', GOOGLE_CLIENT_SECRET: 'cloud-google-secret' },
530
+ { email }
531
+ );
532
+ const res = await app.app.request(
533
+ '/api/v1/setup',
534
+ jsonIp({ setupToken: 'integration-test-setup-token', instanceName: 'EdOtp', owner: OWNER })
535
+ );
536
+ expect(res.status).toBe(201);
537
+ // Cloud setup mints no workspace — the key-mint fixture below needs the
538
+ // operator to hold a membership; seed a cloud-LOCAL one.
539
+ await sso.seedLocalWorkspace(app, 'EdOtp', OWNER.email);
540
+ });
541
+
542
+ afterAll(async () => {
543
+ await app.stop();
544
+ });
545
+
546
+ it('refuses the emailOTP session surface (send / sign-in / verify-email) despite the mailer', async () => {
547
+ // The enumerated 1.6.15 session-minting surface (isOtpSignInPath): the
548
+ // sign-in mint, the verify-email conditional mint (config insurance),
549
+ // and the send leg (no dead codes get mailed).
550
+ const attempts: Array<[string, Record<string, unknown>]> = [
551
+ ['/api/v1/auth/email-otp/send-verification-otp', { email: OWNER.email, type: 'sign-in' }],
552
+ ['/api/v1/auth/sign-in/email-otp', { email: OWNER.email, otp: '123456' }],
553
+ ['/api/v1/auth/email-otp/verify-email', { email: OWNER.email, otp: '123456' }]
554
+ ];
555
+ for (const [path, body] of attempts) {
556
+ const res = await app.app.request(path, jsonIp(body));
557
+ expect(res.status, path).toBe(403);
558
+ expect(res.headers.get('set-cookie'), path).toBeNull(); // no session minted
559
+ }
560
+ // The refusal carries the steering code, and nothing was mailed.
561
+ const refusal = await app.app.request(
562
+ '/api/v1/auth/sign-in/email-otp',
563
+ jsonIp({ email: OWNER.email, otp: '123456' })
564
+ );
565
+ expect((await readJson(refusal)).code).toBe('otp_signin_disabled');
566
+ expect(email.sent).toHaveLength(0);
567
+ });
568
+
569
+ it('does NOT register the Google social provider despite GOOGLE_CLIENT_ID/SECRET set', async () => {
570
+ // Google social is a self-host option only: on cloud the provider is
571
+ // not registered at all, so /sign-in/social mints no session even with
572
+ // the credentials configured (defense in depth — the audit-completeness
573
+ // guarantee holds by construction, not by "operator didn't set the env
574
+ // var"). 1.6.15 answers PROVIDER_NOT_FOUND (404) for an unregistered
575
+ // provider, and no session cookie is set.
576
+ const social = await app.app.request('/api/v1/auth/sign-in/social', jsonIp({ provider: 'google' }));
577
+ expect(social.status).toBe(404);
578
+ expect(social.headers.get('set-cookie')).toBeNull();
579
+ // Discovery never advertises google on cloud either (the hub-only
580
+ // descriptor), the credentials notwithstanding.
581
+ const info = await readJson(await app.app.request('/api/v1/instance'));
582
+ expect(info.auth.methods).not.toContain('google');
583
+ });
584
+
585
+ it('refuses the CLI OTP mint pair (cli_otp_disabled → `antasphere login`)', async () => {
586
+ const request = await app.app.request('/api/v1/cli/auth/request', jsonIp({ email: OWNER.email }));
587
+ expect(request.status).toBe(403);
588
+ const requestBody = await readJson(request);
589
+ expect(requestBody.error.code).toBe('cli_otp_disabled');
590
+ expect(requestBody.error.message).toContain('antasphere login');
591
+
592
+ const complete = await app.app.request(
593
+ '/api/v1/cli/auth/complete',
594
+ jsonIp({ email: OWNER.email, otp: '123456' })
595
+ );
596
+ expect(complete.status).toBe(403);
597
+ expect((await readJson(complete)).error.code).toBe('cli_otp_disabled');
598
+ expect(email.sent).toHaveLength(0); // still nothing mailed
599
+ });
600
+
601
+ it('the self-revoke stays open: a key kills exactly ITSELF; the operator door survives', async () => {
602
+ // /sign-in/email still works AFTER the OTP close (the break-glass door).
603
+ const signIn = await app.app.request(
604
+ '/api/v1/auth/sign-in/email',
605
+ jsonIp({ email: OWNER.email, password: OWNER.password })
606
+ );
607
+ expect(signIn.status).toBe(200);
608
+ const cookie = signIn.headers.get('set-cookie')!.split(';')[0]!;
609
+
610
+ const mint = async (name: string) => {
611
+ const res = await app.app.request('/api/v1/api-keys', {
612
+ ...jsonIp({ name, scopes: [host.scopes.read, host.scopes.write] }),
613
+ headers: { 'content-type': 'application/json', cookie }
614
+ });
615
+ expect(res.status).toBe(201);
616
+ return readJson(res);
617
+ };
618
+ const keyA = await mint('cloud-cli-a');
619
+ const keyB = await mint('cloud-cli-b');
620
+
621
+ const revoke = await app.app.request('/api/v1/cli/auth/key', {
622
+ method: 'DELETE',
623
+ headers: { authorization: `Bearer ${keyA.key}` }
624
+ });
625
+ expect(revoke.status).toBe(200);
626
+ expect(await readJson(revoke)).toEqual({ revoked: true, id: keyA.apiKey.id });
627
+
628
+ // The presenting key is dead; the OTHER key is untouched — the route
629
+ // names no key, so nothing else was revocable.
630
+ const deadMe = await app.app.request('/api/v1/me', {
631
+ headers: { authorization: `Bearer ${keyA.key}` }
632
+ });
633
+ expect(deadMe.status).toBe(401);
634
+ const liveMe = await app.app.request('/api/v1/me', {
635
+ headers: { authorization: `Bearer ${keyB.key}` }
636
+ });
637
+ expect(liveMe.status).toBe(200);
638
+ });
639
+ });
640
+
641
+ describe('session TTL split (SL-5): cloud 30d FIXED, oss 365d sliding', () => {
642
+ // The one-login-concept mechanics: a cloud tool session is a PROJECTION of
643
+ // the hub anchor — sliding it would let the anchor die under an active
644
+ // user, so cloud sessions are fixed-duration (disableSessionRefresh, the
645
+ // 1.6.15 knob) and re-derive silently from the anchor. oss keeps the
646
+ // template's sliding sessions byte-identically.
647
+
648
+ interface SessionRow {
649
+ id: string;
650
+ expires_at: Date;
651
+ created_at: Date;
652
+ }
653
+
654
+ async function newestSession(app: TestApp, email: string): Promise<SessionRow> {
655
+ const { rows } = await app.db.pool.query<SessionRow>(
656
+ `SELECT s.id, s.expires_at, s.created_at FROM session s
657
+ JOIN "user" u ON u.id = s.user_id WHERE u.email = $1
658
+ ORDER BY s.created_at DESC LIMIT 1`,
659
+ [email]
660
+ );
661
+ expect(rows).toHaveLength(1);
662
+ return rows[0]!;
663
+ }
664
+
665
+ /** Backdate a session so the sliding rule (expiresAt − expiresIn + updateAge ≤ now) fires. */
666
+ async function backdate(app: TestApp, id: string): Promise<Date> {
667
+ const { rows } = await app.db.pool.query<{ expires_at: Date }>(
668
+ `UPDATE session SET expires_at = now() + interval '1 hour' WHERE id = $1 RETURNING expires_at`,
669
+ [id]
670
+ );
671
+ return rows[0]!.expires_at;
672
+ }
673
+
674
+ it('cloud: sessions mint at 30 days and expiresAt does NOT advance on use', async () => {
675
+ const app = await createTestApp(await createDatabase(container, 'session_ttl_cloud'), HUB_ENV);
676
+ try {
677
+ expect(
678
+ (
679
+ await app.app.request(
680
+ '/api/v1/setup',
681
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Ttl', owner: OWNER })
682
+ )
683
+ ).status
684
+ ).toBe(201);
685
+ const signIn = await app.app.request(
686
+ '/api/v1/auth/sign-in/email',
687
+ json({ email: OWNER.email, password: OWNER.password })
688
+ );
689
+ expect(signIn.status).toBe(200);
690
+ const cookie = extractCookie(signIn);
691
+
692
+ const minted = await newestSession(app, OWNER.email);
693
+ const ttlDays = (minted.expires_at.getTime() - minted.created_at.getTime()) / 86_400_000;
694
+ expect(ttlDays).toBeGreaterThan(29.9);
695
+ expect(ttlDays).toBeLessThan(30.1);
696
+
697
+ // Trip the sliding rule deliberately, then USE the session: with
698
+ // disableSessionRefresh the row must not move — the hard expiry is
699
+ // what forces the silent re-derivation from the hub anchor.
700
+ const backdated = await backdate(app, minted.id);
701
+ const use = await app.app.request('/api/v1/auth/get-session', { headers: { cookie } });
702
+ expect(use.status).toBe(200);
703
+ expect((await readJson(use))?.session).toBeTruthy();
704
+ const after = await newestSession(app, OWNER.email);
705
+ expect(after.id).toBe(minted.id);
706
+ expect(after.expires_at.toISOString()).toBe(backdated.toISOString());
707
+ } finally {
708
+ await app.stop();
709
+ }
710
+ });
711
+
712
+ it('oss: the template sliding behavior is unchanged — the same use DOES advance expiresAt', async () => {
713
+ const app = await createTestApp(await createDatabase(container, 'session_ttl_oss'));
714
+ try {
715
+ expect(
716
+ (
717
+ await app.app.request(
718
+ '/api/v1/setup',
719
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'TtlOss', owner: OWNER })
720
+ )
721
+ ).status
722
+ ).toBe(201);
723
+ const signIn = await app.app.request(
724
+ '/api/v1/auth/sign-in/email',
725
+ json({ email: OWNER.email, password: OWNER.password })
726
+ );
727
+ expect(signIn.status).toBe(200);
728
+ const cookie = extractCookie(signIn);
729
+
730
+ const minted = await newestSession(app, OWNER.email);
731
+ const ttlDays = (minted.expires_at.getTime() - minted.created_at.getTime()) / 86_400_000;
732
+ expect(ttlDays).toBeGreaterThan(364);
733
+ expect(ttlDays).toBeLessThan(366);
734
+
735
+ const backdated = await backdate(app, minted.id);
736
+ const use = await app.app.request('/api/v1/auth/get-session', { headers: { cookie } });
737
+ expect(use.status).toBe(200);
738
+ const after = await newestSession(app, OWNER.email);
739
+ expect(after.id).toBe(minted.id);
740
+ // Slid forward: a fresh ~365d horizon, far past the backdated hour.
741
+ expect(after.expires_at.getTime()).toBeGreaterThan(backdated.getTime() + 300 * 86_400_000);
742
+ } finally {
743
+ await app.stop();
744
+ }
745
+ });
746
+ });
747
+
748
+ describe('oss stays dark: zero hub-shaped calls across boot + a request matrix (fetch-spy)', () => {
749
+ it('an oss boot with HUB_* vars present makes NO outbound fetch at all', async () => {
750
+ const fetched: string[] = [];
751
+ const realFetch = globalThis.fetch;
752
+ const spy: typeof fetch = (input, init) => {
753
+ fetched.push(String(input instanceof Request ? input.url : input));
754
+ return realFetch(input as Parameters<typeof fetch>[0], init);
755
+ };
756
+ globalThis.fetch = spy;
757
+ try {
758
+ // Positive control first: the spy really intercepts global fetch.
759
+ await spy('http://127.0.0.1:1/spy-probe').catch(() => {});
760
+ expect(fetched).toEqual(['http://127.0.0.1:1/spy-probe']);
761
+ fetched.length = 0;
762
+
763
+ // Adversarial config: every HUB_* var set — EDITION=oss must leave
764
+ // them completely unread (hubConfig() is the single switch).
765
+ const app = await createTestApp(await createDatabase(container, 'edition_oss_dark'), {
766
+ HUB_ISSUER_URL: 'http://hub.localhost:3300',
767
+ HUB_CLIENT_ID: host.hubClientId,
768
+ HUB_CLIENT_SECRET: 'integration-test-hub-secret-0001'
769
+ });
770
+ try {
771
+ // The request matrix: setup, session login, whoami, key mint, key
772
+ // use, a domain read — every credential kind exercised.
773
+ const setup = await app.app.request(
774
+ '/api/v1/setup',
775
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Dark', owner: OWNER })
776
+ );
777
+ expect(setup.status).toBe(201);
778
+ const signIn = await app.app.request(
779
+ '/api/v1/auth/sign-in/email',
780
+ json({ email: OWNER.email, password: OWNER.password })
781
+ );
782
+ expect(signIn.status).toBe(200);
783
+ const cookie = signIn.headers.get('set-cookie')!.split(';')[0]!;
784
+ expect((await app.app.request('/api/v1/me', { headers: { cookie } })).status).toBe(200);
785
+ const mint = await app.app.request('/api/v1/api-keys', {
786
+ method: 'POST',
787
+ headers: { 'content-type': 'application/json', cookie },
788
+ body: JSON.stringify({ name: 'dark key', scopes: [host.scopes.read] })
789
+ });
790
+ expect(mint.status).toBe(201);
791
+ const { key } = await readJson(mint);
792
+ expect(
793
+ (await app.app.request('/api/v1/me', { headers: { authorization: `Bearer ${key}` } })).status
794
+ ).toBe(200);
795
+ expect((await app.app.request(host.probeRoute, { headers: { cookie } })).status).toBe(200);
796
+ // The cloud-only surfaces stay dark 404s on oss — probed INSIDE the
797
+ // spy window so a regression that mounts them (or makes them phone
798
+ // the hub) fails the zero-fetch assertion below too.
799
+ const ssoLogout = await app.app.request('/api/v1/sso/logout', {
800
+ method: 'POST',
801
+ headers: { cookie }
802
+ });
803
+ expect(ssoLogout.status).toBe(404);
804
+ const dismiss = await app.app.request('/api/v1/me/onboarding/dismiss', {
805
+ method: 'POST',
806
+ headers: { cookie }
807
+ });
808
+ expect(dismiss.status).toBe(404);
809
+ } finally {
810
+ await app.stop();
811
+ }
812
+ // The whole boot + matrix performed ZERO outbound fetches — no hub
813
+ // grant, no reconcile, no token endpoint, nothing. Byte-identity is
814
+ // not "no hub calls", it is "no network surface constructed at all".
815
+ expect(fetched).toEqual([]);
816
+ } finally {
817
+ globalThis.fetch = realFetch;
818
+ }
819
+ });
820
+ });
821
+
822
+ describe('oss edition discovery (unchanged)', () => {
823
+ it('never advertises the hub SSO method, and never carries auth.sso (SL-1 pin)', async () => {
824
+ const app = await createTestApp(await createDatabase(container, 'edition_oss_disc'));
825
+ const info = await readJson(await app.app.request('/api/v1/instance'));
826
+ expect(info.edition).toBe('oss');
827
+ expect(info.auth.methods).not.toContain('antasphere');
828
+ expect(info.auth.methods).toContain('password');
829
+ // The discovery wire shape stays byte-identical on oss: no sso block —
830
+ // key ABSENT, not null.
831
+ expect('sso' in info.auth).toBe(false);
832
+ // And the error surfaces keep their pre-change defaults: the antasphere
833
+ // callback path does not exist (no genericOAuth plugin registered) and
834
+ // the better-auth error route renders its own page — no /login redirect
835
+ // (onAPIError.errorURL is cloud-gated).
836
+ const cb = await app.app.request('/api/v1/auth/oauth2/callback/antasphere?error=login_required');
837
+ expect(cb.status).toBe(404);
838
+ const errRoute = await app.app.request('/api/v1/auth/error?error=probe');
839
+ expect(errRoute.status).toBe(200);
840
+ expect(errRoute.headers.get('content-type')).toContain('text/html');
841
+ await app.stop();
842
+ });
843
+
844
+ it('keeps Google social sign-in when configured (the cloud gate is cloud-only)', async () => {
845
+ // The mirror of the cloud closure above: on oss the same GOOGLE_CLIENT_ID/
846
+ // SECRET REGISTER the provider — /sign-in/social builds a real Google
847
+ // authorization URL (200 + url, never PROVIDER_NOT_FOUND) and discovery
848
+ // lists 'google'. Proves the gate touches cloud alone.
849
+ const app = await createTestApp(await createDatabase(container, 'edition_oss_google'), {
850
+ GOOGLE_CLIENT_ID: 'oss-google-id',
851
+ GOOGLE_CLIENT_SECRET: 'oss-google-secret'
852
+ });
853
+ const info = await readJson(await app.app.request('/api/v1/instance'));
854
+ expect(info.edition).toBe('oss');
855
+ expect(info.auth.methods).toContain('google');
856
+
857
+ const social = await app.app.request('/api/v1/auth/sign-in/social', {
858
+ method: 'POST',
859
+ headers: { 'content-type': 'application/json' },
860
+ body: JSON.stringify({ provider: 'google' })
861
+ });
862
+ expect(social.status).toBe(200);
863
+ const body = await readJson(social);
864
+ expect(typeof body.url).toBe('string');
865
+ expect(body.url).toContain('accounts.google.com');
866
+ await app.stop();
867
+ });
868
+ });