@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,68 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { describeHubClockSkew, hubClockSkewMs } from '../../src/entitlements/clock-skew.js';
3
+
4
+ /**
5
+ * The hub clock probe (PRDCT-2644): one GET of the discovery document, the
6
+ * `Date` header read against the local clock; null on anything unreadable,
7
+ * never a throw; a sentence only past the hub's five-minute forward window.
8
+ */
9
+
10
+ const ISSUER = 'https://hub.test/';
11
+ const NOW = Date.parse('2026-09-23T12:00:00.000Z');
12
+ const MIN = 60_000;
13
+
14
+ function answering(headers: Record<string, string>) {
15
+ const calls: Array<{ url: string; method: string | undefined }> = [];
16
+ const fetchImpl = (async (input: string | URL | Request, init?: RequestInit) => {
17
+ calls.push({ url: String(input), method: init?.method });
18
+ return new Response('{}', { status: 200, headers });
19
+ }) as typeof fetch;
20
+ return { calls, fetchImpl };
21
+ }
22
+
23
+ describe('hubClockSkewMs', () => {
24
+ it('a hub seven minutes behind reads as +7 minutes, and the sentence says ahead', async () => {
25
+ const f = answering({ date: new Date(NOW - 7 * MIN).toUTCString() });
26
+ const skew = await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW });
27
+ expect(skew).toBe(7 * MIN);
28
+ expect(f.calls).toEqual([{ url: 'https://hub.test/.well-known/openid-configuration', method: 'GET' }]);
29
+ const sentence = describeHubClockSkew(skew!);
30
+ expect(sentence).toBe(
31
+ "this instance's clock is 7 minutes ahead of the hub's; usage events it stamps would be refused by the hub's window"
32
+ );
33
+ });
34
+
35
+ it('a hub seven minutes ahead reads as −7 minutes, and the sentence says behind', async () => {
36
+ const f = answering({ date: new Date(NOW + 7 * MIN).toUTCString() });
37
+ const skew = await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW });
38
+ expect(skew).toBe(-7 * MIN);
39
+ expect(describeHubClockSkew(skew!)).toMatch(/7 minutes behind the hub's/);
40
+ });
41
+
42
+ it('within five minutes either way there is no sentence (the bound is inclusive)', async () => {
43
+ const f = answering({ date: new Date(NOW - 3 * MIN).toUTCString() });
44
+ const skew = await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW });
45
+ expect(skew).toBe(3 * MIN);
46
+ expect(describeHubClockSkew(skew!)).toBeNull();
47
+ expect(describeHubClockSkew(5 * MIN)).toBeNull();
48
+ expect(describeHubClockSkew(-5 * MIN)).toBeNull();
49
+ expect(describeHubClockSkew(5 * MIN + 1)).not.toBeNull();
50
+ });
51
+
52
+ it('no Date header reads as null', async () => {
53
+ const f = answering({});
54
+ expect(await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW })).toBeNull();
55
+ });
56
+
57
+ it('an unparseable Date header reads as null', async () => {
58
+ const f = answering({ date: 'not a date' });
59
+ expect(await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW })).toBeNull();
60
+ });
61
+
62
+ it('a fetch that throws reads as null, never a throw', async () => {
63
+ const fetchImpl = (async () => {
64
+ throw new TypeError('fetch failed');
65
+ }) as unknown as typeof fetch;
66
+ await expect(hubClockSkewMs(ISSUER, { fetchImpl, now: () => NOW })).resolves.toBeNull();
67
+ });
68
+ });
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Hono } from 'hono';
3
+ import { crossSiteGuard } from '@antasphere/chassis-server/middleware';
4
+
5
+ /**
6
+ * BROW-2: nothing under /api/v1 checked Origin or Sec-Fetch metadata, so every
7
+ * session-authenticated business route was drivable from any page the victim
8
+ * had open. Better Auth's own check covers only its `/sign-in` paths.
9
+ */
10
+ function app(): Hono {
11
+ const api = new Hono();
12
+ api.use('*', crossSiteGuard({ publicBaseUrl: 'https://app.example.com' }));
13
+ api.get('/orgs', (c) => c.json({ read: true }));
14
+ api.post('/orgs', (c) => c.json({ created: true }, 201));
15
+ api.delete('/orgs/:id', (c) => c.json({ deleted: true }));
16
+ api.post('/auth/oauth2/token', (c) => c.json({ token: 'x' }));
17
+ api.post('/auth/sign-in/email', (c) => c.json({ signed: true }));
18
+ const root = new Hono();
19
+ root.route('/api/v1', api);
20
+ return root;
21
+ }
22
+
23
+ const post = (headers: Record<string, string>) => app().request('/api/v1/orgs', { method: 'POST', headers });
24
+
25
+ describe('crossSiteGuard', () => {
26
+ it('rejects a cross-site credentialed POST before the handler', async () => {
27
+ const res = await post({
28
+ 'sec-fetch-site': 'cross-site',
29
+ origin: 'https://evil.example',
30
+ cookie: 'session=abc'
31
+ });
32
+ expect(res.status).toBe(403);
33
+ expect(await res.json()).toMatchObject({ error: { code: 'cross_site_forbidden' } });
34
+ });
35
+
36
+ it('rejects an untrusted Origin even when Sec-Fetch metadata is absent (older browsers)', async () => {
37
+ const res = await post({ origin: 'https://evil.example', cookie: 'session=abc' });
38
+ expect(res.status).toBe(403);
39
+ });
40
+
41
+ it('rejects an opaque `Origin: null` (sandboxed iframe / redirected form post)', async () => {
42
+ const res = await post({ origin: 'null', cookie: 'session=abc' });
43
+ expect(res.status).toBe(403);
44
+ });
45
+
46
+ it('rejects a same-site-but-different-origin POST from a sibling subdomain', async () => {
47
+ // Sec-Fetch-Site says `same-site`, so the cross-site arm does not fire —
48
+ // the Origin arm must. A host-only session cookie IS sent on these.
49
+ const res = await post({
50
+ 'sec-fetch-site': 'same-site',
51
+ origin: 'https://marketing.example.com',
52
+ cookie: 'session=abc'
53
+ });
54
+ expect(res.status).toBe(403);
55
+ });
56
+
57
+ it('covers every unsafe method, not just POST', async () => {
58
+ const res = await app().request('/api/v1/orgs/1', {
59
+ method: 'DELETE',
60
+ headers: { 'sec-fetch-site': 'cross-site' }
61
+ });
62
+ expect(res.status).toBe(403);
63
+ });
64
+
65
+ it('allows the dashboard: same-origin POST with the app Origin', async () => {
66
+ const res = await app().request('https://app.example.com/api/v1/orgs', {
67
+ method: 'POST',
68
+ headers: {
69
+ 'sec-fetch-site': 'same-origin',
70
+ origin: 'https://app.example.com',
71
+ cookie: 'session=abc'
72
+ }
73
+ });
74
+ expect(res.status).toBe(201);
75
+ });
76
+
77
+ it('allows the serving origin even when it is not PUBLIC_BASE_URL (dev proxy, previews)', async () => {
78
+ const res = await app().request('http://localhost:5173/api/v1/orgs', {
79
+ method: 'POST',
80
+ headers: { origin: 'http://localhost:5173', 'sec-fetch-site': 'same-origin' }
81
+ });
82
+ expect(res.status).toBe(201);
83
+ });
84
+
85
+ it('leaves non-browser clients alone (no Origin, no Sec-Fetch)', async () => {
86
+ const res = await post({ 'content-type': 'application/json' });
87
+ expect(res.status).toBe(201);
88
+ });
89
+
90
+ it('never blocks bearer-authenticated calls — those are not ambient credentials', async () => {
91
+ const res = await post({
92
+ authorization: 'Bearer pk_live_something',
93
+ origin: 'https://some-mcp-client.example',
94
+ 'sec-fetch-site': 'cross-site'
95
+ });
96
+ expect(res.status).toBe(201);
97
+ });
98
+
99
+ it('exempts the deliberate wildcard-CORS OAuth endpoints', async () => {
100
+ const res = await app().request('/api/v1/auth/oauth2/token', {
101
+ method: 'POST',
102
+ headers: { origin: 'https://claude.ai', 'sec-fetch-site': 'cross-site' }
103
+ });
104
+ expect(res.status).toBe(200);
105
+ });
106
+
107
+ it('does NOT exempt the cookie-bearing auth endpoints', async () => {
108
+ const res = await app().request('/api/v1/auth/sign-in/email', {
109
+ method: 'POST',
110
+ headers: { origin: 'https://evil.example', 'sec-fetch-site': 'cross-site' }
111
+ });
112
+ expect(res.status).toBe(403);
113
+ });
114
+
115
+ it('leaves safe methods readable — CORS already stops the attacker seeing them', async () => {
116
+ const res = await app().request('/api/v1/orgs', {
117
+ headers: { 'sec-fetch-site': 'cross-site', origin: 'https://evil.example' }
118
+ });
119
+ expect(res.status).toBe(200);
120
+ });
121
+
122
+ describe('deniedOrigins — the viewer origin (PRDCT-1352)', () => {
123
+ const VIEWER = 'https://decks.example.net';
124
+ function gated(): Hono {
125
+ const api = new Hono();
126
+ api.use(
127
+ '*',
128
+ crossSiteGuard({
129
+ publicBaseUrl: 'https://app.example.com',
130
+ // Even listed as trusted, denial wins: an operator cannot re-open it by accident.
131
+ extraTrustedOrigins: [VIEWER],
132
+ deniedOrigins: [VIEWER]
133
+ })
134
+ );
135
+ api.post('/orgs', (c) => c.json({ created: true }, 201));
136
+ const root = new Hono();
137
+ root.route('/api/v1', api);
138
+ return root;
139
+ }
140
+
141
+ it('refuses the viewer origin even when it IS the serving origin', async () => {
142
+ const res = await gated().request(`${VIEWER}/api/v1/orgs`, {
143
+ method: 'POST',
144
+ headers: { origin: VIEWER, 'sec-fetch-site': 'same-origin', cookie: 'session=abc' }
145
+ });
146
+ expect(res.status).toBe(403);
147
+ expect(await res.json()).toMatchObject({ error: { code: 'cross_site_forbidden' } });
148
+ });
149
+
150
+ it('refuses the viewer origin arriving at the app origin', async () => {
151
+ const res = await gated().request('https://app.example.com/api/v1/orgs', {
152
+ method: 'POST',
153
+ headers: { origin: VIEWER, 'sec-fetch-site': 'cross-site', cookie: 'session=abc' }
154
+ });
155
+ expect(res.status).toBe(403);
156
+ });
157
+
158
+ it('still lets the app origin through', async () => {
159
+ const res = await gated().request('https://app.example.com/api/v1/orgs', {
160
+ method: 'POST',
161
+ headers: { origin: 'https://app.example.com', 'sec-fetch-site': 'same-origin' }
162
+ });
163
+ expect(res.status).toBe(201);
164
+ });
165
+ });
166
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { declaredCredits } from '@antasphere/chassis-contract';
3
+
4
+ /** The price an action declares is what a test can compute from the declaration (PRDCT-2627). */
5
+ describe('declaredCredits', () => {
6
+ it('prices a quantity from the declaration: credits per `per` units, exact', () => {
7
+ const MB = 1024 * 1024;
8
+ const upload = { creditsPerUnit: 5, unit: 'bytes', per: MB };
9
+ expect(declaredCredits(upload, 20 * MB)).toBe(100);
10
+ expect(declaredCredits(upload, 43)).toBeCloseTo(0.000205, 6);
11
+ expect(declaredCredits({ creditsPerUnit: 50 }, 1)).toBe(50);
12
+ });
13
+ });
@@ -0,0 +1,192 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ DEMO_PASS_DEFAULT_MINUTES,
4
+ DEMO_PASS_MAX_MINUTES,
5
+ DEMO_SESSION_REFUSED_AUTH_PATHS,
6
+ demoSessionAuthRefusal,
7
+ isDemoAddress,
8
+ isDemoSignInHost,
9
+ isSafeDemoPath,
10
+ parseDemoEmailDomains,
11
+ parseDemoHosts
12
+ } from '@antasphere/chassis-server/identity';
13
+ import { INSECURE_SETUP_ORIGINS, SECURE_SETUP_ORIGINS } from '@antasphere/chassis-server/testing';
14
+
15
+ /**
16
+ * The demo pass's pure rules (the demo pass spec, sections 1 to 3). A pass is
17
+ * a sign-in-equivalent link, so every one of these is a boundary: the host
18
+ * the switch may be on, the addresses a pass may open, the page it lands on.
19
+ */
20
+
21
+ const NONE: ReadonlySet<string> = new Set();
22
+
23
+ describe('the lifetime constants', () => {
24
+ it('default to one day and stop at one week', () => {
25
+ expect(DEMO_PASS_DEFAULT_MINUTES).toBe(1440);
26
+ expect(DEMO_PASS_MAX_MINUTES).toBe(10080);
27
+ });
28
+ });
29
+
30
+ describe('parseDemoHosts / parseDemoEmailDomains', () => {
31
+ it('are empty for unset and blank input', () => {
32
+ expect(parseDemoHosts(undefined).size).toBe(0);
33
+ expect(parseDemoHosts('').size).toBe(0);
34
+ expect(parseDemoHosts(' ').size).toBe(0);
35
+ expect(parseDemoEmailDomains(undefined).size).toBe(0);
36
+ });
37
+
38
+ it('trim, lowercase and dedupe the entries', () => {
39
+ const set = parseDemoHosts(' Demo.Example.io ,staging.acme.test, demo.example.io');
40
+ expect([...set].sort()).toEqual(['demo.example.io', 'staging.acme.test']);
41
+ });
42
+
43
+ it.each([
44
+ ['a scheme', 'https://demo.example.io', /scheme/],
45
+ ['a path', 'demo.example.io/app', /path/],
46
+ ['a port', 'demo.example.io:8443', /port/],
47
+ ['a space', 'demo example.io', /space/],
48
+ ['a wildcard', '*.example.io', /wildcard/],
49
+ ['an empty label', 'demo..example.io', /empty label/],
50
+ ['a leading dot', '.example.io', /empty label/],
51
+ ['a trailing dot', 'example.io.', /empty label/],
52
+ ['a character no hostname carries', 'demo_host.example.io', /not a hostname/]
53
+ ])('refuse an entry with %s, naming the variable and the entry', (_what, entry, reason) => {
54
+ expect(() => parseDemoHosts(`ok.example.io,${entry}`)).toThrow(reason);
55
+ expect(() => parseDemoHosts(entry)).toThrow(/DEMO_SIGN_IN_HOSTS/);
56
+ expect(() => parseDemoEmailDomains(entry)).toThrow(/DEMO_SIGN_IN_EMAIL_DOMAINS/);
57
+ });
58
+
59
+ it('refuse a list of separators only rather than arm an empty one', () => {
60
+ expect(() => parseDemoHosts(',,')).toThrow(/at least one hostname/);
61
+ });
62
+ });
63
+
64
+ describe('isDemoSignInHost: loopback, or listed exactly', () => {
65
+ // The loopback half of the setup-origin table: every plaintext origin the
66
+ // setup wizard accepts is loopback by construction (the https ones are
67
+ // accepted there for their transport, which says nothing about the host).
68
+ const LOOPBACK = SECURE_SETUP_ORIGINS.filter((url) => url.startsWith('http://'));
69
+ const PUBLIC = [
70
+ ...SECURE_SETUP_ORIGINS.filter((url) => url.startsWith('https://')),
71
+ ...INSECURE_SETUP_ORIGINS
72
+ ];
73
+
74
+ it.each(LOOPBACK)('accepts %s with an empty list', (url) => {
75
+ expect(isDemoSignInHost(url, NONE)).toBe(true);
76
+ });
77
+
78
+ it.each(PUBLIC)('REFUSES %s with an empty list (look-alike hosts included)', (url) => {
79
+ expect(isDemoSignInHost(url, NONE)).toBe(false);
80
+ });
81
+
82
+ it('accepts a public host once it is listed, and only that exact host', () => {
83
+ const hosts = parseDemoHosts('platform.example.com');
84
+ expect(isDemoSignInHost('https://platform.example.com', hosts)).toBe(true);
85
+ expect(isDemoSignInHost('https://PLATFORM.example.com/base', hosts)).toBe(true);
86
+ expect(isDemoSignInHost('https://app.platform.example.com', hosts)).toBe(false);
87
+ expect(isDemoSignInHost('https://platform.example.com.evil.test', hosts)).toBe(false);
88
+ });
89
+
90
+ it('fails closed on anything it cannot parse', () => {
91
+ expect(isDemoSignInHost('not a url', NONE)).toBe(false);
92
+ expect(isDemoSignInHost('', parseDemoHosts('localhost'))).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe('isDemoAddress: reserved example and test domains, or an operator-listed one', () => {
97
+ it.each([
98
+ 'a@example.com',
99
+ 'a@example.net',
100
+ 'a@example.org',
101
+ 'a@sub.example.org',
102
+ 'a@team.test',
103
+ 'a@x.localhost',
104
+ 'a@shop.example',
105
+ 'a@nowhere.invalid',
106
+ 'A@EXAMPLE.COM',
107
+ 'a@Team.Test'
108
+ ])('accepts %s', (email) => {
109
+ expect(isDemoAddress(email, NONE)).toBe(true);
110
+ });
111
+
112
+ it.each([
113
+ 'a@notexample.com',
114
+ 'a@example.com.evil.io',
115
+ 'a@gmail.com',
116
+ 'a@testing.io',
117
+ 'a@test.io',
118
+ '"a@example.com"@gmail.com',
119
+ 'a@example.com@gmail.com',
120
+ 'a@.test',
121
+ 'a@example..com',
122
+ 'a@',
123
+ '@example.com',
124
+ 'no-at-sign.example.com'
125
+ ])('refuses %s', (email) => {
126
+ expect(isDemoAddress(email, NONE)).toBe(false);
127
+ });
128
+
129
+ it('judges the part after the LAST @', () => {
130
+ expect(isDemoAddress('"a@gmail.com"@example.com', NONE)).toBe(true);
131
+ });
132
+
133
+ it('adds the operator domains and their subdomains, on a label boundary', () => {
134
+ const extra = parseDemoEmailDomains('demo.acme.io');
135
+ expect(isDemoAddress('a@demo.acme.io', extra)).toBe(true);
136
+ expect(isDemoAddress('a@eu.demo.acme.io', extra)).toBe(true);
137
+ expect(isDemoAddress('A@DEMO.ACME.IO', extra)).toBe(true);
138
+ expect(isDemoAddress('a@acme.io', extra)).toBe(false);
139
+ expect(isDemoAddress('a@xdemo.acme.io', extra)).toBe(false);
140
+ expect(isDemoAddress('a@demo.acme.io.evil.test.com', extra)).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe('isSafeDemoPath: one leading slash, no fragment, no control character', () => {
145
+ it.each(['/', '/decks/1?x=1', '/settings/members', '/a/b?c=d&e=%2F'])('accepts %s', (path) => {
146
+ expect(isSafeDemoPath(path)).toBe(true);
147
+ });
148
+
149
+ it('accepts a path of exactly 2048 characters', () => {
150
+ expect(isSafeDemoPath('/' + 'a'.repeat(2047))).toBe(true);
151
+ });
152
+
153
+ it.each([
154
+ ['empty', ''],
155
+ ['protocol-relative', '//evil'],
156
+ ['backslash-relative', '/\\evil'],
157
+ ['relative', 'decks'],
158
+ ['absolute URL', 'https://evil.test/'],
159
+ ['a fragment', '/a#b'],
160
+ ['a space', '/a b'],
161
+ ['a control character', '/a\u0001b'],
162
+ ['a newline', '/a\nb'],
163
+ ['DEL', '/a\u007fb'],
164
+ ['2049 characters', '/' + 'a'.repeat(2048)]
165
+ ])('refuses %s', (_what, path) => {
166
+ expect(isSafeDemoPath(path)).toBe(false);
167
+ });
168
+ });
169
+
170
+ describe('demoSessionAuthRefusal: what a pass’s session may not do at the sign-in library', () => {
171
+ it('refuses every listed path', () => {
172
+ expect(DEMO_SESSION_REFUSED_AUTH_PATHS.length).toBe(27);
173
+ for (const path of DEMO_SESSION_REFUSED_AUTH_PATHS) {
174
+ expect(demoSessionAuthRefusal(path), path).toBe(true);
175
+ }
176
+ // A session's dynamic client registration stores the client under the person.
177
+ expect(demoSessionAuthRefusal('/oauth2/register')).toBe(true);
178
+ });
179
+
180
+ it('refuses them whatever the case, and with one trailing slash', () => {
181
+ expect(demoSessionAuthRefusal('/Change-Password')).toBe(true);
182
+ expect(demoSessionAuthRefusal('/OAUTH2/AUTHORIZE')).toBe(true);
183
+ expect(demoSessionAuthRefusal('/change-password/')).toBe(true);
184
+ expect(demoSessionAuthRefusal('/Two-Factor/Enable/')).toBe(true);
185
+ });
186
+
187
+ it('allows reading the session, signing out, signing in and redeeming', () => {
188
+ for (const path of ['/get-session', '/sign-out', '/sign-in/email', '/demo/redeem']) {
189
+ expect(demoSessionAuthRefusal(path), path).toBe(false);
190
+ }
191
+ });
192
+ });
@@ -0,0 +1,127 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { createEmailDriver, type EmailMessage } from '@antasphere/chassis-server/email';
3
+ import type { Logger } from '@antasphere/chassis-server/logger';
4
+
5
+ const logger = { info: () => {}, warn: () => {}, error: () => {} } as unknown as Logger;
6
+
7
+ /** The factory takes a Pick of Env, so the tests build exactly that shape. */
8
+ type FactoryEnv = Parameters<typeof createEmailDriver>[0];
9
+ const env = (over: Partial<FactoryEnv> = {}): FactoryEnv => ({ EMAIL_DRIVER: 'none', ...over }) as FactoryEnv;
10
+
11
+ describe('createEmailDriver', () => {
12
+ it('defaults to the none driver, which never delivers', () => {
13
+ const driver = createEmailDriver(env(), logger);
14
+ expect(driver.name).toBe('none');
15
+ expect(driver.delivers).toBe(false);
16
+ });
17
+
18
+ it('builds each delivering driver when its credential and sender are present', () => {
19
+ const from = 'Things <noreply@example.com>';
20
+ expect(
21
+ createEmailDriver(env({ EMAIL_DRIVER: 'smtp', SMTP_URL: 'smtp://h:25', EMAIL_FROM: from }), logger).name
22
+ ).toBe('smtp');
23
+ expect(
24
+ createEmailDriver(env({ EMAIL_DRIVER: 'resend', RESEND_API_KEY: 're_x', EMAIL_FROM: from }), logger)
25
+ .name
26
+ ).toBe('resend');
27
+ const brevo = createEmailDriver(
28
+ env({ EMAIL_DRIVER: 'brevo', BREVO_API_KEY: 'xkeysib-x', EMAIL_FROM: from }),
29
+ logger
30
+ );
31
+ expect(brevo.name).toBe('brevo');
32
+ expect(brevo.delivers).toBe(true);
33
+ });
34
+
35
+ // A driver that boots without its credential would fail at the first send,
36
+ // which is the flow a user is waiting on; it must fail at boot instead.
37
+ it('refuses a delivering driver that is missing its credential or its sender', () => {
38
+ const from = 'Things <noreply@example.com>';
39
+ expect(() => createEmailDriver(env({ EMAIL_DRIVER: 'brevo', EMAIL_FROM: from }), logger)).toThrow(
40
+ 'EMAIL_DRIVER=brevo requires BREVO_API_KEY'
41
+ );
42
+ expect(() =>
43
+ createEmailDriver(env({ EMAIL_DRIVER: 'brevo', BREVO_API_KEY: 'xkeysib-x' }), logger)
44
+ ).toThrow('EMAIL_DRIVER=brevo requires EMAIL_FROM');
45
+ expect(() => createEmailDriver(env({ EMAIL_DRIVER: 'resend', EMAIL_FROM: from }), logger)).toThrow(
46
+ 'EMAIL_DRIVER=resend requires RESEND_API_KEY'
47
+ );
48
+ expect(() => createEmailDriver(env({ EMAIL_DRIVER: 'smtp', EMAIL_FROM: from }), logger)).toThrow(
49
+ 'EMAIL_DRIVER=smtp requires SMTP_URL'
50
+ );
51
+ });
52
+ });
53
+
54
+ describe('the brevo driver on the wire', () => {
55
+ afterEach(() => {
56
+ vi.unstubAllGlobals();
57
+ });
58
+
59
+ const send = async (
60
+ from: string,
61
+ response: Response,
62
+ message: EmailMessage = { to: 'her@example.com', subject: 'Subject', html: '<p>Body</p>' }
63
+ ) => {
64
+ const fetchMock = vi.fn(async () => response);
65
+ vi.stubGlobal('fetch', fetchMock);
66
+ const driver = createEmailDriver(
67
+ { EMAIL_DRIVER: 'brevo', BREVO_API_KEY: 'xkeysib-secret', EMAIL_FROM: from } as FactoryEnv,
68
+ logger
69
+ );
70
+ const result = await driver.send(message).then(
71
+ () => null,
72
+ (error: Error) => error
73
+ );
74
+ return { result, fetchMock };
75
+ };
76
+
77
+ it('posts the message to Brevo with the key in the api-key header', async () => {
78
+ const { result, fetchMock } = await send(
79
+ 'Things <noreply@example.com>',
80
+ new Response('{}', { status: 201 })
81
+ );
82
+ expect(result).toBeNull();
83
+ expect(fetchMock).toHaveBeenCalledTimes(1);
84
+ const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
85
+ expect(url).toBe('https://api.brevo.com/v3/smtp/email');
86
+ expect(init.method).toBe('POST');
87
+ expect((init.headers as Record<string, string>)['api-key']).toBe('xkeysib-secret');
88
+ const body = JSON.parse(init.body as string);
89
+ // `Name <addr>` is split into Brevo's own sender shape.
90
+ expect(body.sender).toEqual({ name: 'Things', email: 'noreply@example.com' });
91
+ expect(body.to).toEqual([{ email: 'her@example.com' }]);
92
+ expect(body.subject).toBe('Subject');
93
+ expect(body.htmlContent).toBe('<p>Body</p>');
94
+ // An absent text part is omitted, never sent as undefined/null.
95
+ expect('textContent' in body).toBe(false);
96
+ });
97
+
98
+ it('passes a bare sender address through and carries the text part when there is one', async () => {
99
+ const { fetchMock } = await send('noreply@example.com', new Response('{}', { status: 201 }), {
100
+ to: 'her@example.com',
101
+ subject: 'Subject',
102
+ html: '<p>Body</p>',
103
+ text: 'Body'
104
+ });
105
+ const body = JSON.parse((fetchMock.mock.calls[0] as unknown as [string, RequestInit])[1].body as string);
106
+ expect(body.sender).toEqual({ email: 'noreply@example.com' });
107
+ expect(body.textContent).toBe('Body');
108
+ });
109
+
110
+ // The response body names the reason and may echo the request, so it must not
111
+ // reach the thrown message a caller could relay onward.
112
+ it('maps a refusal to its cause without leaking the response body', async () => {
113
+ const leak = 'unverified sender her@example.com and xkeysib-secret';
114
+ const cases: [number, string][] = [
115
+ [401, 'brevo: authentication failed'],
116
+ [429, 'brevo: rate limited'],
117
+ [503, 'brevo: temporarily unavailable'],
118
+ [400, 'brevo: refused the message (400)']
119
+ ];
120
+ for (const [status, expected] of cases) {
121
+ const { result } = await send('noreply@example.com', new Response(leak, { status }));
122
+ expect(result?.message).toBe(expected);
123
+ expect(result?.message).not.toContain('xkeysib-secret');
124
+ expect(result?.message).not.toContain('her@example.com');
125
+ }
126
+ });
127
+ });