@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,696 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { JWTPayload } from 'jose';
3
+ import { and, eq, ne } from 'drizzle-orm';
4
+ import type { GenericOAuthConfig } from 'better-auth/plugins';
5
+ import { account, user as userTable, userOnboarding, type Db } from '@antasphere/chassis-db';
6
+ import type { Logger } from '../logger.js';
7
+ import { HubJwtVerifier } from './hub-jwt.js';
8
+ import type { ReconcilePassOutcome } from './hub-reconcile.js';
9
+ import type { LoginAccessToken } from './hub-user-client.js';
10
+
11
+ /**
12
+ * "Sign in with Antasphere" — the cloud edition's SSO entrance
13
+ * (internal/federation.md, user-scoped federation). This module owns everything
14
+ * hub-SSO:
15
+ *
16
+ * - the relying-party provider config for better-auth's `genericOAuth`
17
+ * plugin (registered by identity/better-auth.ts ONLY when the instance
18
+ * boots EDITION=cloud — an oss boot never constructs this class);
19
+ * - `HubJwtVerifier`: remote-JWKS verification of hub-minted JWTs (hard
20
+ * iss/aud pinning, RS256 allowlist, cached keys + one forced refresh);
21
+ * - the per-login ASSERTION handoff (an AsyncLocalStorage scope spanning
22
+ * the callback request — see `runWithLoginScope`);
23
+ * - the per-login work better-auth does not know about: identity guard,
24
+ * D10 email re-sync, and the FAIL-CLOSED login reconcile (the org
25
+ * projection) — a cloud login whose reconcile pass fails must not exist.
26
+ *
27
+ * Identity comes from the ID TOKEN ONLY (aud = HUB_CLIENT_ID). The access
28
+ * token is never claim-bearing for the tool: the SSO code exchange sends
29
+ * RFC 8707 `resource = <hub>/mcp` (the `tokenResource` seam constant shared
30
+ * with hub-grant.ts), so the callback access token is HUB-audienced — a
31
+ * bearer for the hub's own `/api/v1`, which the login-time reconcile uses
32
+ * to read the user's org list AS THE USER. Org claims are gone from the
33
+ * login path entirely; the hub's caller-scoped `GET /orgs` is the ONE
34
+ * source of org/membership truth. Refresh + between-logins reads live in
35
+ * hub-grant.ts / hub-user-client.ts; SSO scopes carry `offline_access
36
+ * account:read` so the grant persists on the account row, plus
37
+ * `orgs:create` (HUB_SSO_SCOPES below) for the one write the tool makes at
38
+ * the hub as the user.
39
+ */
40
+
41
+ /**
42
+ * providerId of the hub relying party — the discovery method name in
43
+ * `/api/v1/instance`, the better-auth callback path segment
44
+ * (`/api/v1/auth/oauth2/callback/antasphere` — the redirect URI in the
45
+ * hub's TOOL_REGISTRY entry), and the D9 trusted-provider key.
46
+ */
47
+ export const HUB_SSO_PROVIDER_ID = 'antasphere';
48
+
49
+ /**
50
+ * What one hub login asserts about the USER, extracted from the VERIFIED
51
+ * id_token (aud = our client id). No org half: org truth is read live from
52
+ * the hub, never from token claims.
53
+ */
54
+ export interface HubSsoAssertion {
55
+ /** Hub user id (`sub`) — matched against `account.accountId`, never a local user id. */
56
+ sub: string;
57
+ email: string;
58
+ emailVerified: boolean;
59
+ name: string;
60
+ }
61
+
62
+ /**
63
+ * What one VERIFIED hub exchange JWT asserts (`POST /sso/cli-connect`,
64
+ * internal/federation.md P5): the USER only. The H3 token still carries ONE
65
+ * org's transitional claims through the hub's compat window, but the tool
66
+ * reads none of them — org truth comes from the connect-time reconcile
67
+ * (as-the-user `GET /orgs` with the grant the SAME H3 response delivered),
68
+ * exactly like the browser login path.
69
+ */
70
+ export interface HubConnectAssertion extends HubSsoAssertion {
71
+ /** Unique per-token id — consumed one-time-use by the replay ledger. */
72
+ jti: string;
73
+ /** The token's own `exp` — how long the consumed jti must stay claimed. */
74
+ expiresAt: Date;
75
+ }
76
+
77
+ /**
78
+ * The minimal slice of better-auth's server API `provisionConnect` needs
79
+ * (structural — `Auth` from identity/better-auth.ts satisfies it; typing it
80
+ * here keeps hub-sso.ts free of an import cycle with that module). Going
81
+ * through the INTERNAL ADAPTER is the point: `createOAuthUser` is the exact
82
+ * call the genericOAuth callback makes for a browser-SSO JIT, so the
83
+ * databaseHooks.user.create.after seam (`user.created` → the collaborator
84
+ * grant sweep) fires identically, and the rows are byte-identical.
85
+ */
86
+ export interface ConnectAuthSeam {
87
+ $context: Promise<{
88
+ internalAdapter: {
89
+ createOAuthUser: (
90
+ user: { email: string; name: string; emailVerified: boolean },
91
+ account: { providerId: string; accountId: string }
92
+ ) => Promise<{ user: { id: string } }>;
93
+ linkAccount: (account: { providerId: string; accountId: string; userId: string }) => Promise<unknown>;
94
+ };
95
+ }>;
96
+ }
97
+
98
+ /**
99
+ * The login-time face of the org reconciler (hub-reconcile.ts) — bound by
100
+ * boot AFTER construction (the reconciler needs the auth context's secret,
101
+ * which needs createAuth, which needs this service). Structural to avoid a
102
+ * hard cycle.
103
+ */
104
+ export interface LoginReconciler {
105
+ forceReconcile(localUserId: string, login?: LoginAccessToken): Promise<ReconcilePassOutcome>;
106
+ }
107
+
108
+ /**
109
+ * A login failure the after-hook converts into a clean, session-less
110
+ * redirect back to /login?error=<code>. Codes are stable (the dashboard
111
+ * maps them to copy); anything unexpected becomes `sso_login_failed`.
112
+ * `/sso/cli-connect` reuses the same errors as 403 bodies —
113
+ * `sso_link_refused` is its trusted-link refusal (never thrown on the
114
+ * browser login path, where better-auth's own linking gate answers first).
115
+ */
116
+ export class HubSsoLoginError extends Error {
117
+ constructor(
118
+ readonly code:
119
+ | 'sso_assertion_missing'
120
+ | 'sso_identity_conflict'
121
+ | 'sso_email_conflict'
122
+ | 'sso_link_refused'
123
+ | 'sso_projection_failed'
124
+ | 'sso_login_failed',
125
+ message: string
126
+ ) {
127
+ super(message);
128
+ this.name = 'HubSsoLoginError';
129
+ }
130
+ }
131
+
132
+ /**
133
+ * The assertion handoff (ADR 015, reshaped): the verified USER identity —
134
+ * plus the raw hub-audienced access token the login reconcile presents —
135
+ * exists in `getUserInfo` (PRE user-create); the per-login work runs in the
136
+ * callback's after-hook (POST create). An AsyncLocalStorage scope
137
+ * established around the whole auth request (api/index.ts wraps
138
+ * `auth.handler`) carries it across: getUserInfo writes, the after-hook
139
+ * takes. Being request-scoped, two concurrent logins can NEVER read each
140
+ * other's assertion or token.
141
+ */
142
+ export interface LoginScopeAssertion extends HubSsoAssertion {
143
+ /** The callback access token (hub-audienced) — the login reconcile's bearer. */
144
+ accessToken: string;
145
+ accessTokenExpiresAt: Date | null;
146
+ }
147
+
148
+ interface LoginScope {
149
+ assertion?: LoginScopeAssertion;
150
+ }
151
+
152
+ const loginScope = new AsyncLocalStorage<LoginScope>();
153
+
154
+ /**
155
+ * The scopes the tool requests at hub sign-in — the ONE statement of the
156
+ * list (the provider entry below spreads it; tests import it).
157
+ *
158
+ * - `openid profile email`: the id_token identity.
159
+ * - `offline_access account:read`: the stored grant and its as-the-user
160
+ * `GET /orgs` reads (hub-grant.ts, hub-user-client.ts).
161
+ * - `orgs:create` (PRDCT-2443): the hub's DEDICATED scope for `POST /orgs`
162
+ * — it opens that one route, to a tool-registry client only.
163
+ * `account:write` does NOT open it and is deliberately not requested: a
164
+ * tool holds the narrowest write it needs, nothing account-wide.
165
+ *
166
+ * DEPLOY ORDER — THE HUB FIRST. The hub's authorize endpoint
167
+ * (@better-auth/oauth-provider) validates every requested scope against the
168
+ * CLIENT's registered scopes (its tool registry seeds them) and answers
169
+ * `error=invalid_scope` for an unknown one — the WHOLE sign-in fails, for
170
+ * every user, not just workspace creation. A hub that does not yet list
171
+ * `orgs:create` for this client must never meet a tool that requests
172
+ * it. A grant minted BEFORE this shipped simply lacks the scope: the hub
173
+ * answers 403 `insufficient_scope` on create, which the route maps to 401
174
+ * `hub_reauth_required` — one sign-in heals it.
175
+ */
176
+ export const HUB_SSO_SCOPES = [
177
+ 'openid',
178
+ 'profile',
179
+ 'email',
180
+ 'offline_access',
181
+ 'account:read',
182
+ 'orgs:create'
183
+ ] as const;
184
+
185
+ /**
186
+ * Strict whitelist for per-call authorize params (the SL-1 silent-connect
187
+ * seam): the dashboard's silent auto-connect passes
188
+ * `additionalData: { prompt: 'none' }` on `POST /sign-in/oauth2`, and the
189
+ * ONLY thing that may ever reach the hub's authorize URL from that
190
+ * caller-controlled blob is the literal `prompt=none` pair — anything else
191
+ * (other prompt values, extra keys) is dropped. Verified on better-auth
192
+ * 1.6.15 (re-verify on ANY bump): `authorizationUrlParams` may be a
193
+ * FUNCTION of the endpoint ctx (generic-oauth/types.d.mts), the sign-in
194
+ * body carries `additionalData` (generic-oauth/routes.mjs — also threaded
195
+ * into the state blob), and the function's return is applied to the
196
+ * authorization URL via `createAuthorizationURL`'s `additionalParams`.
197
+ */
198
+ export function ssoAuthorizationUrlParams(body: unknown): Record<string, string> {
199
+ const additionalData = (body as { additionalData?: unknown } | null | undefined)?.additionalData;
200
+ const prompt = (additionalData as { prompt?: unknown } | null | undefined)?.prompt;
201
+ return prompt === 'none' ? { prompt: 'none' } : {};
202
+ }
203
+
204
+ export interface HubSsoOptions {
205
+ db: Db;
206
+ logger: Logger;
207
+ /** The hub OIDC issuer (HUB_ISSUER_URL) — iss pin + discovery root. */
208
+ issuerUrl: string;
209
+ clientId: string;
210
+ clientSecret: string;
211
+ /**
212
+ * This instance's own OAuth resource identifier (`<PUBLIC_BASE_URL>/mcp`,
213
+ * always derived, never configured) — the aud pin for H3 CONNECT tokens
214
+ * (which the hub mints FOR this tool). Not used on the login path.
215
+ */
216
+ resourceUrl: string;
217
+ /**
218
+ * RFC 8707 `resource` for the SSO CODE EXCHANGE — `<hub>/mcp`
219
+ * (hubApiResource), THE seam constant shared with hub-grant.ts: the
220
+ * callback access token and every refreshed token are HUB-audienced and
221
+ * callable at hub /api/v1. Null = omit the param (flips the whole flow
222
+ * to opaque tokens in one line if the hub ever accepts them).
223
+ */
224
+ tokenResource: string | null;
225
+ /** Dashboard origin for the failure redirects (PUBLIC_BASE_URL). */
226
+ publicBaseUrl: string;
227
+ }
228
+
229
+ export class HubSsoService {
230
+ private readonly verifier: HubJwtVerifier;
231
+ private reconciler: LoginReconciler | null = null;
232
+
233
+ constructor(private readonly opts: HubSsoOptions) {
234
+ this.verifier = new HubJwtVerifier(opts.issuerUrl);
235
+ }
236
+
237
+ /** Boot binds the reconciler once it exists (see LoginReconciler). */
238
+ bindReconciler(reconciler: LoginReconciler): void {
239
+ this.reconciler = reconciler;
240
+ }
241
+
242
+ /**
243
+ * Establish the per-request login scope. api/index.ts wraps EVERY
244
+ * /api/v1/auth/* request on cloud — an empty object store is free, and
245
+ * wrapping unconditionally keeps the seam independent of better-auth's
246
+ * internal routing.
247
+ */
248
+ runWithLoginScope<T>(fn: () => T): T {
249
+ return loginScope.run({}, fn);
250
+ }
251
+
252
+ /** One-shot read of the assertion the current request's getUserInfo verified. */
253
+ takeAssertion(): LoginScopeAssertion | undefined {
254
+ const scope = loginScope.getStore();
255
+ const assertion = scope?.assertion;
256
+ if (scope) delete scope.assertion;
257
+ return assertion;
258
+ }
259
+
260
+ /** The account site's address (the hub issuer, no trailing slash): where a refusal sends the person. */
261
+ accountUrl(): string {
262
+ return this.opts.issuerUrl.replace(/\/+$/, '');
263
+ }
264
+
265
+ /** Where a failed SSO login lands — the login page with a stable error code. */
266
+ loginErrorUrl(code: string): string {
267
+ return `${this.opts.publicBaseUrl.replace(/\/+$/, '')}/login?error=${encodeURIComponent(code)}`;
268
+ }
269
+
270
+ logLoginFailure(err: unknown, localUserId: string): void {
271
+ this.opts.logger.warn(
272
+ { err, localUserId },
273
+ 'hub SSO login failed after callback — session revoked, redirecting with error'
274
+ );
275
+ }
276
+
277
+ /**
278
+ * The genericOAuth provider entry for the hub. Notes that are POSTURE,
279
+ * not accident:
280
+ *
281
+ * - `scopes` (HUB_SSO_SCOPES) carry `offline_access account:read`: the
282
+ * grant IS the between-logins credential — better-auth persists the
283
+ * refresh token on the account row (encrypted; identity/better-auth.ts),
284
+ * and hub-grant.ts keeps it alive — and `orgs:create`, the hub's
285
+ * DEDICATED scope for `POST /orgs` (PRDCT-2443).
286
+ * - `tokenUrlParams.resource` (RFC 8707) is `tokenResource` — `<hub>/mcp`
287
+ * — so the minted access token is a bearer for the HUB's own API (the
288
+ * login reconcile + every between-logins read), never for ours.
289
+ * - `disableSignUp` stays UNSET: SSO IS the sanctioned signup entrance
290
+ * on cloud — the deliberate FOURTH switch next to the three
291
+ * closed-signup switches (CLAUDE.md invariant, internal/federation.md).
292
+ * - `getUserInfo` replaces the plugin's default entirely, so IT owns the
293
+ * emailVerified mapping — set from the hub's verified `email_verified`
294
+ * claim, never defaulted.
295
+ */
296
+ providerConfig(): GenericOAuthConfig {
297
+ return {
298
+ providerId: HUB_SSO_PROVIDER_ID,
299
+ discoveryUrl: this.opts.issuerUrl.replace(/\/+$/, '') + '/.well-known/openid-configuration',
300
+ clientId: this.opts.clientId,
301
+ clientSecret: this.opts.clientSecret,
302
+ scopes: [...HUB_SSO_SCOPES],
303
+ pkce: true,
304
+ ...(this.opts.tokenResource ? { tokenUrlParams: { resource: this.opts.tokenResource } } : {}),
305
+ // Per-call prompt=none passthrough for the silent auto-connect — the
306
+ // strict whitelist above; nothing else from the caller's
307
+ // additionalData ever reaches the hub authorize URL.
308
+ authorizationUrlParams: (ctx) => ssoAuthorizationUrlParams(ctx.body),
309
+ getUserInfo: (tokens) => this.getUserInfo(tokens)
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Verify the callback id_token and extract the USER assertion. Returning
315
+ * null makes the plugin fail the login with a clean error redirect — the
316
+ * fail-closed path for every bad token (wrong iss/aud, expired, bad
317
+ * signature, malformed claims). The access token is NOT verified here —
318
+ * it is hub-audienced, never claim-bearing for us; it rides the login
319
+ * scope as the reconcile's bearer, and the HUB verifies it on use.
320
+ */
321
+ private async getUserInfo(tokens: {
322
+ accessToken?: string | undefined;
323
+ idToken?: string | undefined;
324
+ accessTokenExpiresAt?: Date | undefined;
325
+ }): Promise<{ id: string; email: string; name: string; emailVerified: boolean } | null> {
326
+ try {
327
+ if (!tokens.idToken) throw new Error('token response carried no id_token (openid is always scoped)');
328
+ if (!tokens.accessToken) throw new Error('token response carried no access token');
329
+ const idClaims = await this.verifier.verify(tokens.idToken, this.opts.clientId);
330
+ const assertion = buildUserAssertion(idClaims);
331
+ const scope = loginScope.getStore();
332
+ if (scope) {
333
+ scope.assertion = {
334
+ ...assertion,
335
+ accessToken: tokens.accessToken,
336
+ accessTokenExpiresAt: tokens.accessTokenExpiresAt ?? null
337
+ };
338
+ } else {
339
+ // No scope means the auth mount was not wrapped — the after-hook
340
+ // will fail this login closed (sso_assertion_missing). Say why.
341
+ this.opts.logger.error(
342
+ 'hub SSO: getUserInfo ran outside a login scope — the callback after-hook will refuse this login'
343
+ );
344
+ }
345
+ return {
346
+ id: assertion.sub,
347
+ email: assertion.email,
348
+ name: assertion.name,
349
+ emailVerified: assertion.emailVerified
350
+ };
351
+ } catch (err) {
352
+ this.opts.logger.warn({ err }, 'hub SSO: callback token rejected');
353
+ return null;
354
+ }
355
+ }
356
+
357
+ /**
358
+ * The per-login work (after-hook, newSession-gated, JIT and returning
359
+ * users alike), in fail-closed order:
360
+ *
361
+ * 1. single-hub-identity guard — undo an email-match link that would
362
+ * merge two hub subjects into one local user;
363
+ * 2. email re-sync (D10) with the explicit duplicate-email collision
364
+ * path (fail the login, corrupt nothing);
365
+ * 3. the FAIL-CLOSED login reconcile: one forced pass reading the user's
366
+ * org list from the hub with the callback access token — reconcile IS
367
+ * the projection now. A pass that does not definitively succeed
368
+ * throws `sso_projection_failed` (session revoked, error redirect):
369
+ * a cloud login without its projection must not exist.
370
+ *
371
+ * Throws HubSsoLoginError; the caller revokes the just-minted session and
372
+ * redirects. Any other throw is mapped to the generic failure code there.
373
+ */
374
+ async assertLogin(localUserId: string, assertion: LoginScopeAssertion): Promise<void> {
375
+ await this.guardSingleHubIdentity(localUserId, assertion.sub);
376
+ await this.syncEmail(localUserId, assertion);
377
+ if (!this.reconciler) {
378
+ // Boot always binds on cloud; a missing binding is a wiring bug and
379
+ // must fail the login closed, never mint an unprojected session.
380
+ throw new HubSsoLoginError('sso_projection_failed', 'no login reconciler bound (boot wiring bug)');
381
+ }
382
+ const outcome = await this.reconciler.forceReconcile(localUserId, {
383
+ accessToken: assertion.accessToken,
384
+ expiresAt: assertion.accessTokenExpiresAt
385
+ });
386
+ if (outcome !== 'ok') {
387
+ throw new HubSsoLoginError('sso_projection_failed', `login reconcile pass failed (${outcome})`);
388
+ }
389
+ // SL-6: lazily mark the first login for the tool-local onboarding seam.
390
+ // AFTER the reconcile 'ok' (a revoked login must not leave a row) and
391
+ // strictly BEST-EFFORT: this write is NEVER a login failure mode — a
392
+ // failed insert just means the row appears on the NEXT login, and the
393
+ // banner semantics (`NOT EXISTS dismissed row` = welcome owed) already
394
+ // show the welcome without any row at all. ON CONFLICT DO NOTHING keeps
395
+ // returning logins from touching an existing (possibly dismissed) row.
396
+ try {
397
+ await this.opts.db.insert(userOnboarding).values({ userId: localUserId }).onConflictDoNothing();
398
+ } catch (err) {
399
+ this.opts.logger.warn(
400
+ { err, localUserId },
401
+ 'onboarding first-login insert failed — retry-safe (the welcome stays owed), login unaffected'
402
+ );
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Verify a hub-minted exchange JWT (`POST /sso/cli-connect`, the H3
408
+ * counterpart — internal/federation.md P5) and extract its assertion. The
409
+ * chain, every link fail-closed:
410
+ *
411
+ * 1. `HubJwtVerifier.verify` — hub JWKS signature, `iss` pinned to the
412
+ * hub, `aud` pinned to OUR resource URL (a token minted for another
413
+ * tool dies here; a tool-minted MCP token dies on `iss`),
414
+ * RS256 allowlist, expiry (5 s tolerance).
415
+ * 2. `exp` REQUIRED — jose only validates expiry when the claim exists,
416
+ * and the replay ledger needs a bound; a token without one is a
417
+ * contract break, refused.
418
+ * 3. `purpose === 'sso-connect'` REQUIRED — a hub ACCESS token carries
419
+ * no purpose claim and dies here even if iss/aud ever matched.
420
+ * 4. `jti` REQUIRED (bounded) — the caller consumes it one-time-use.
421
+ *
422
+ * The token's TRANSITIONAL org claims (workspace_id/role/workspace_name,
423
+ * kept by the hub through the compat window) are deliberately NOT read —
424
+ * org truth comes from the connect-time reconcile, never a claim.
425
+ *
426
+ * Throws on any failure; the route maps every throw to one uniform 401
427
+ * (no oracle distinguishing replay from expiry from a foreign audience).
428
+ */
429
+ async verifyConnectToken(token: string): Promise<HubConnectAssertion> {
430
+ const claims = await this.verifier.verify(token, this.opts.resourceUrl);
431
+ if (typeof claims.exp !== 'number') throw new Error('exchange token carries no exp');
432
+ if (claims.purpose !== 'sso-connect') {
433
+ throw new Error('token is not an sso-connect exchange token (purpose claim)');
434
+ }
435
+ const jti = claims.jti;
436
+ if (typeof jti !== 'string' || !jti || jti.length > 256) {
437
+ throw new Error('exchange token carries no usable jti');
438
+ }
439
+ const sub = claims.sub;
440
+ if (typeof sub !== 'string' || !sub) throw new Error('exchange token carries no sub');
441
+ const email = claims.email;
442
+ if (typeof email !== 'string' || !email.includes('@')) {
443
+ throw new Error('exchange token carries no usable email claim');
444
+ }
445
+ const normalizedEmail = email.toLowerCase();
446
+ return {
447
+ sub,
448
+ email: normalizedEmail,
449
+ // The hub mints exchange tokens only for a live, authenticated hub
450
+ // user — an identity whose email the hub itself verified (H3 re-reads
451
+ // emailVerified LIVE at mint). The H3 contract carries no
452
+ // email_verified or name claim, so the honest mirror of the SSO
453
+ // id_token's `email_verified: true` is asserted here, and the display
454
+ // name falls back exactly like an SSO login with a blank profile name.
455
+ emailVerified: true,
456
+ name: normalizedEmail.split('@')[0]!,
457
+ jti,
458
+ expiresAt: new Date(claims.exp * 1000)
459
+ };
460
+ }
461
+
462
+ /**
463
+ * JIT-provision for `/sso/cli-connect`: resolve-or-create the local user
464
+ * for a VERIFIED connect assertion, then run the USER half of the
465
+ * per-login work — identity guard, D10 email sync. NO org projection
466
+ * happens here: the route stores the H3 grant (`acquireFromConnect`) and
467
+ * runs the same fail-closed reconcile as a browser login — the reconcile
468
+ * IS the projection, on the connect path too. User resolution mirrors
469
+ * better-auth's own OAuth linking exactly (oauth2/link-account.mjs on the
470
+ * pinned 1.6.15):
471
+ *
472
+ * 1. account row (providerId='antasphere', accountId=sub) → that user;
473
+ * 2. else a local user holding the asserted email → TRUSTED LINK (D9),
474
+ * but only onto a VERIFIED local email (requireLocalEmailVerified —
475
+ * an attacker-parked unverified account can never be taken over);
476
+ * 3. else create user + account through the internal adapter's
477
+ * `createOAuthUser` — the genericOAuth JIT call, so the
478
+ * databaseHooks.user.create.after seam (`user.created` → collaborator
479
+ * grant sweep) fires exactly as it would for a browser SSO signup.
480
+ *
481
+ * Two concurrent first-connects race the create; the loser's unique-email
482
+ * violation is caught and resolved by re-running the lookup (the Phase 5
483
+ * duplicate-account lesson).
484
+ */
485
+ async provisionConnect(
486
+ auth: ConnectAuthSeam,
487
+ assertion: HubConnectAssertion
488
+ ): Promise<{ user: { id: string; email: string; name: string } }> {
489
+ let userId = await this.resolveConnectUser(auth, assertion, true);
490
+ if (userId === null) {
491
+ // Lost the create race: the winner's rows are committed now (the
492
+ // adapter's transaction passthrough) — one retry must resolve.
493
+ userId = await this.resolveConnectUser(auth, assertion, false);
494
+ if (userId === null) {
495
+ throw new HubSsoLoginError('sso_login_failed', 'user provisioning raced and re-lookup failed');
496
+ }
497
+ }
498
+ await this.guardSingleHubIdentity(userId, assertion.sub);
499
+ await this.syncEmail(userId, assertion);
500
+ // Re-read AFTER the D10 sync may have just rewritten the email; the
501
+ // response must carry what the database now holds.
502
+ const [row] = await this.opts.db
503
+ .select({ id: userTable.id, email: userTable.email, name: userTable.name })
504
+ .from(userTable)
505
+ .where(eq(userTable.id, userId))
506
+ .limit(1);
507
+ if (!row) throw new HubSsoLoginError('sso_login_failed', 'user row missing after provisioning');
508
+ return { user: row };
509
+ }
510
+
511
+ /**
512
+ * The connect path's fail-closed projection pass: one forced reconcile
513
+ * reading the user's org list AS THE USER with their freshly stored (or
514
+ * pre-existing) grant. Returns the pass's own outcome — the route mints a
515
+ * key ONLY on 'ok' (never a born-dead key).
516
+ */
517
+ async reconcileConnect(localUserId: string): Promise<ReconcilePassOutcome> {
518
+ if (!this.reconciler) {
519
+ // Boot always binds on cloud; a missing binding is a wiring bug and
520
+ // must refuse the connect, never mint an unprojected key.
521
+ throw new HubSsoLoginError('sso_projection_failed', 'no login reconciler bound (boot wiring bug)');
522
+ }
523
+ return this.reconciler.forceReconcile(localUserId);
524
+ }
525
+
526
+ /**
527
+ * One resolution pass (see provisionConnect). Returns the local user id,
528
+ * or null when `mayCreate` and the create lost a uniqueness race —
529
+ * signalling the caller to re-run with the winner's rows visible.
530
+ */
531
+ private async resolveConnectUser(
532
+ auth: ConnectAuthSeam,
533
+ assertion: HubConnectAssertion,
534
+ mayCreate: boolean
535
+ ): Promise<string | null> {
536
+ const db = this.opts.db;
537
+ const [linked] = await db
538
+ .select({ userId: account.userId })
539
+ .from(account)
540
+ .where(and(eq(account.providerId, HUB_SSO_PROVIDER_ID), eq(account.accountId, assertion.sub)))
541
+ .limit(1);
542
+ if (linked) return linked.userId;
543
+
544
+ const [existing] = await db
545
+ .select({ id: userTable.id, emailVerified: userTable.emailVerified })
546
+ .from(userTable)
547
+ .where(eq(userTable.email, assertion.email))
548
+ .limit(1);
549
+ const authCtx = await auth.$context;
550
+ if (existing) {
551
+ if (!existing.emailVerified) {
552
+ // requireLocalEmailVerified, mirrored: linking a trusted provider
553
+ // onto an UNVERIFIED local account is the classic pre-registration
554
+ // takeover — refuse, exactly like the browser SSO path does.
555
+ throw new HubSsoLoginError(
556
+ 'sso_link_refused',
557
+ 'a local account holds this email but its address is unverified — refusing the trusted link'
558
+ );
559
+ }
560
+ await authCtx.internalAdapter.linkAccount({
561
+ providerId: HUB_SSO_PROVIDER_ID,
562
+ accountId: assertion.sub,
563
+ userId: existing.id
564
+ });
565
+ return existing.id;
566
+ }
567
+ if (!mayCreate) return null;
568
+ try {
569
+ const created = await authCtx.internalAdapter.createOAuthUser(
570
+ { email: assertion.email, name: assertion.name, emailVerified: assertion.emailVerified },
571
+ { providerId: HUB_SSO_PROVIDER_ID, accountId: assertion.sub }
572
+ );
573
+ return created.user.id;
574
+ } catch (err) {
575
+ // The only expected failure is the unique-email (or duplicate-link)
576
+ // violation from a concurrent first-connect — resolvable by retry.
577
+ this.opts.logger.warn({ err }, 'sso cli-connect: JIT create raced — retrying resolution');
578
+ return null;
579
+ }
580
+ }
581
+
582
+ /**
583
+ * A local user may hold AT MOST ONE hub identity. The dangerous path:
584
+ * hub user B's email was changed (hub-side, verified there) to an address
585
+ * some local user A still carries because A's projection is stale — B's
586
+ * first login then finds no account row, matches A by email, and the D9
587
+ * trusted link merges B onto A's local user (and A's resources). Detect the
588
+ * second `antasphere` accountId, DELETE the NEWEST link row, and fail the
589
+ * login. The conflict clears when A next logs in (their email re-syncs
590
+ * away) or an operator intervenes.
591
+ *
592
+ * The undo keys on RECENCY, not on the current login's `sub`. On the
593
+ * normal conflict they are the same row (the trusted link just inserted
594
+ * it). They differ only in residue states where TWO rows pre-exist — a
595
+ * crash between better-auth's link and this hook, or a second identity
596
+ * linked via the explicit /oauth2/link flow (which mints no session, so
597
+ * this guard never saw it). Deleting the current-sub row there would let
598
+ * the ESTABLISHED identity's own login destroy itself (and two
599
+ * concurrent conflicting logins destroy BOTH rows, stranding the user);
600
+ * the newest row is always the intruding link, never the long-standing
601
+ * identity, so the user self-heals on retry.
602
+ */
603
+ private async guardSingleHubIdentity(localUserId: string, sub: string): Promise<void> {
604
+ const rows = await this.opts.db
605
+ .select({ accountId: account.accountId, createdAt: account.createdAt })
606
+ .from(account)
607
+ .where(and(eq(account.userId, localUserId), eq(account.providerId, HUB_SSO_PROVIDER_ID)));
608
+ if (!rows.some((r) => r.accountId !== sub)) return;
609
+ const newest = [...rows].sort(
610
+ (a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.accountId.localeCompare(a.accountId)
611
+ )[0]!;
612
+ await this.opts.db
613
+ .delete(account)
614
+ .where(
615
+ and(
616
+ eq(account.userId, localUserId),
617
+ eq(account.providerId, HUB_SSO_PROVIDER_ID),
618
+ eq(account.accountId, newest.accountId)
619
+ )
620
+ );
621
+ throw new HubSsoLoginError(
622
+ 'sso_identity_conflict',
623
+ `hub subject ${sub} maps to a local user already linked to a different hub subject — newest link undone`
624
+ );
625
+ }
626
+
627
+ /**
628
+ * D10: the hub is the identity source on cloud, so the local email FOLLOWS
629
+ * the (verified) hub email at every login — email-keyed features (the
630
+ * collaborator grant sweep) must see the current address. A collision with
631
+ * another local user's email fails the login cleanly; the unique
632
+ * constraint on user.email backstops the read-then-write race.
633
+ */
634
+ private async syncEmail(localUserId: string, assertion: HubSsoAssertion): Promise<void> {
635
+ const email = assertion.email.toLowerCase();
636
+ const [row] = await this.opts.db
637
+ .select({ email: userTable.email, emailVerified: userTable.emailVerified })
638
+ .from(userTable)
639
+ .where(eq(userTable.id, localUserId))
640
+ .limit(1);
641
+ if (!row) throw new HubSsoLoginError('sso_login_failed', 'user row missing after callback');
642
+ const emailChanged = row.email !== email;
643
+ // For an UNCHANGED address, emailVerified is a LATCH: once this instance
644
+ // holds the address as verified, a weaker hub assertion never flips it
645
+ // back — break-glass (api/break-glass.ts) refuses unverified users, so a
646
+ // downward sync could close the operator door if the hub's posture ever
647
+ // allowed an unverified login. A CHANGED address takes the hub's
648
+ // asserted state honestly (the new mailbox is unproven). Mirrors
649
+ // better-auth's own overrideUserInfo semantics.
650
+ const emailVerified = emailChanged
651
+ ? assertion.emailVerified
652
+ : row.emailVerified || assertion.emailVerified;
653
+ if (!emailChanged && row.emailVerified === emailVerified) return;
654
+ if (emailChanged) {
655
+ const [holder] = await this.opts.db
656
+ .select({ id: userTable.id })
657
+ .from(userTable)
658
+ .where(and(eq(userTable.email, email), ne(userTable.id, localUserId)))
659
+ .limit(1);
660
+ if (holder) {
661
+ throw new HubSsoLoginError(
662
+ 'sso_email_conflict',
663
+ `hub-asserted email is already held by another local user — refusing to sync`
664
+ );
665
+ }
666
+ }
667
+ try {
668
+ await this.opts.db
669
+ .update(userTable)
670
+ .set({ email, emailVerified, updatedAt: new Date() })
671
+ .where(eq(userTable.id, localUserId));
672
+ } catch {
673
+ // Concurrent claim of the same address between check and write.
674
+ throw new HubSsoLoginError('sso_email_conflict', 'email sync lost a uniqueness race');
675
+ }
676
+ }
677
+ }
678
+
679
+ /** The SSO-login user assertion, from the VERIFIED id_token ONLY. */
680
+ function buildUserAssertion(idClaims: JWTPayload): HubSsoAssertion {
681
+ const sub = idClaims.sub;
682
+ if (typeof sub !== 'string' || !sub) throw new Error('id_token carries no sub');
683
+ const email = idClaims.email;
684
+ if (typeof email !== 'string' || !email.includes('@')) {
685
+ throw new Error('id_token carries no usable email claim');
686
+ }
687
+ const normalizedEmail = email.toLowerCase();
688
+ const rawName = idClaims.name;
689
+ const name = typeof rawName === 'string' && rawName.trim() ? rawName : normalizedEmail.split('@')[0]!;
690
+ return {
691
+ sub,
692
+ email: normalizedEmail,
693
+ emailVerified: idClaims.email_verified === true,
694
+ name
695
+ };
696
+ }