@bajustone/fortress 1.0.0-rc.1

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 (465) hide show
  1. package/CHANGELOG.md +1011 -0
  2. package/LICENSE +21 -0
  3. package/README.md +760 -0
  4. package/SECURITY.md +197 -0
  5. package/bin/fortress.ts +667 -0
  6. package/dist/auth-service-BcyQ2PuZ.d.cts +307 -0
  7. package/dist/auth-service-BkzZkWfH.d.ts +307 -0
  8. package/dist/config-B2366s_G.d.ts +665 -0
  9. package/dist/config-GtlVt6ZD.d.cts +665 -0
  10. package/dist/convert-routes-BLCQT57f.d.ts +72 -0
  11. package/dist/convert-routes-BdMvP2_X.d.cts +72 -0
  12. package/dist/crypto.cjs +61 -0
  13. package/dist/crypto.d.cts +36 -0
  14. package/dist/crypto.d.ts +36 -0
  15. package/dist/crypto.js +35 -0
  16. package/dist/drift-BT1wtMDJ.d.cts +22 -0
  17. package/dist/drift-k9LiYpRP.d.ts +22 -0
  18. package/dist/drizzle/pg.cjs +481 -0
  19. package/dist/drizzle/pg.d.cts +15 -0
  20. package/dist/drizzle/pg.d.ts +15 -0
  21. package/dist/drizzle/pg.js +454 -0
  22. package/dist/drizzle.cjs +1442 -0
  23. package/dist/drizzle.d.cts +85 -0
  24. package/dist/drizzle.d.ts +85 -0
  25. package/dist/drizzle.js +1411 -0
  26. package/dist/express.cjs +1357 -0
  27. package/dist/express.d.cts +333 -0
  28. package/dist/express.d.ts +333 -0
  29. package/dist/express.js +1314 -0
  30. package/dist/fetcher.cjs +77 -0
  31. package/dist/fetcher.d.cts +7 -0
  32. package/dist/fetcher.d.ts +7 -0
  33. package/dist/fetcher.js +41 -0
  34. package/dist/fortress-BmSvFfqK.d.cts +1089 -0
  35. package/dist/fortress-uA-_cheR.d.ts +1089 -0
  36. package/dist/hono.cjs +1530 -0
  37. package/dist/hono.d.cts +514 -0
  38. package/dist/hono.d.ts +514 -0
  39. package/dist/hono.js +1483 -0
  40. package/dist/index-CxEkfCA0.d.cts +77 -0
  41. package/dist/index-CxEkfCA0.d.ts +77 -0
  42. package/dist/index-D054pcb-.d.cts +146 -0
  43. package/dist/index-jAMbbUAY.d.ts +146 -0
  44. package/dist/index.cjs +9046 -0
  45. package/dist/index.d.cts +717 -0
  46. package/dist/index.d.ts +717 -0
  47. package/dist/index.js +8935 -0
  48. package/dist/jwt.cjs +255 -0
  49. package/dist/jwt.d.cts +90 -0
  50. package/dist/jwt.d.ts +90 -0
  51. package/dist/jwt.js +227 -0
  52. package/dist/otel.cjs +108 -0
  53. package/dist/otel.d.cts +62 -0
  54. package/dist/otel.d.ts +62 -0
  55. package/dist/otel.js +73 -0
  56. package/dist/plugins/account-lockout.cjs +322 -0
  57. package/dist/plugins/account-lockout.d.cts +42 -0
  58. package/dist/plugins/account-lockout.d.ts +42 -0
  59. package/dist/plugins/account-lockout.js +295 -0
  60. package/dist/plugins/admin.cjs +2378 -0
  61. package/dist/plugins/admin.d.cts +72 -0
  62. package/dist/plugins/admin.d.ts +72 -0
  63. package/dist/plugins/admin.js +2351 -0
  64. package/dist/plugins/api-key.cjs +792 -0
  65. package/dist/plugins/api-key.d.cts +121 -0
  66. package/dist/plugins/api-key.d.ts +121 -0
  67. package/dist/plugins/api-key.js +765 -0
  68. package/dist/plugins/audit-log.cjs +493 -0
  69. package/dist/plugins/audit-log.d.cts +86 -0
  70. package/dist/plugins/audit-log.d.ts +86 -0
  71. package/dist/plugins/audit-log.js +468 -0
  72. package/dist/plugins/data-isolation.cjs +211 -0
  73. package/dist/plugins/data-isolation.d.cts +49 -0
  74. package/dist/plugins/data-isolation.d.ts +49 -0
  75. package/dist/plugins/data-isolation.js +186 -0
  76. package/dist/plugins/email-verification.cjs +273 -0
  77. package/dist/plugins/email-verification.d.cts +44 -0
  78. package/dist/plugins/email-verification.d.ts +44 -0
  79. package/dist/plugins/email-verification.js +246 -0
  80. package/dist/plugins/magic-link.cjs +231 -0
  81. package/dist/plugins/magic-link.d.cts +39 -0
  82. package/dist/plugins/magic-link.d.ts +39 -0
  83. package/dist/plugins/magic-link.js +204 -0
  84. package/dist/plugins/oauth.cjs +1696 -0
  85. package/dist/plugins/oauth.d.cts +406 -0
  86. package/dist/plugins/oauth.d.ts +406 -0
  87. package/dist/plugins/oauth.js +1669 -0
  88. package/dist/plugins/openapi.cjs +1185 -0
  89. package/dist/plugins/openapi.d.cts +6 -0
  90. package/dist/plugins/openapi.d.ts +6 -0
  91. package/dist/plugins/openapi.js +1158 -0
  92. package/dist/plugins/rate-limit/express.cjs +55 -0
  93. package/dist/plugins/rate-limit/express.d.cts +64 -0
  94. package/dist/plugins/rate-limit/express.d.ts +64 -0
  95. package/dist/plugins/rate-limit/express.js +30 -0
  96. package/dist/plugins/rate-limit/hono.cjs +51 -0
  97. package/dist/plugins/rate-limit/hono.d.cts +59 -0
  98. package/dist/plugins/rate-limit/hono.d.ts +59 -0
  99. package/dist/plugins/rate-limit/hono.js +26 -0
  100. package/dist/plugins/rate-limit/sveltekit.cjs +49 -0
  101. package/dist/plugins/rate-limit/sveltekit.d.cts +59 -0
  102. package/dist/plugins/rate-limit/sveltekit.d.ts +59 -0
  103. package/dist/plugins/rate-limit/sveltekit.js +24 -0
  104. package/dist/plugins/rate-limit.cjs +354 -0
  105. package/dist/plugins/rate-limit.d.cts +140 -0
  106. package/dist/plugins/rate-limit.d.ts +140 -0
  107. package/dist/plugins/rate-limit.js +325 -0
  108. package/dist/plugins/social-login.cjs +905 -0
  109. package/dist/plugins/social-login.d.cts +114 -0
  110. package/dist/plugins/social-login.d.ts +114 -0
  111. package/dist/plugins/social-login.js +880 -0
  112. package/dist/plugins/tenancy.cjs +780 -0
  113. package/dist/plugins/tenancy.d.cts +108 -0
  114. package/dist/plugins/tenancy.d.ts +108 -0
  115. package/dist/plugins/tenancy.js +753 -0
  116. package/dist/plugins/two-factor.cjs +555 -0
  117. package/dist/plugins/two-factor.d.cts +67 -0
  118. package/dist/plugins/two-factor.d.ts +67 -0
  119. package/dist/plugins/two-factor.js +526 -0
  120. package/dist/plugins/webauthn.cjs +786 -0
  121. package/dist/plugins/webauthn.d.cts +72 -0
  122. package/dist/plugins/webauthn.d.ts +72 -0
  123. package/dist/plugins/webauthn.js +766 -0
  124. package/dist/plugins/webhook.cjs +819 -0
  125. package/dist/plugins/webhook.d.cts +254 -0
  126. package/dist/plugins/webhook.d.ts +254 -0
  127. package/dist/plugins/webhook.js +789 -0
  128. package/dist/protect-D1v_0upK.d.cts +123 -0
  129. package/dist/protect-DsxV_-dJ.d.ts +123 -0
  130. package/dist/sveltekit.cjs +1258 -0
  131. package/dist/sveltekit.d.cts +420 -0
  132. package/dist/sveltekit.d.ts +420 -0
  133. package/dist/sveltekit.js +1207 -0
  134. package/dist/testing.cjs +4294 -0
  135. package/dist/testing.d.cts +197 -0
  136. package/dist/testing.d.ts +197 -0
  137. package/dist/testing.js +4264 -0
  138. package/dist/types-et0R8tsC.d.cts +217 -0
  139. package/dist/types-et0R8tsC.d.ts +217 -0
  140. package/docs/adapter-typed-helpers.md +192 -0
  141. package/docs/admin-recipes.md +227 -0
  142. package/docs/architecture.md +434 -0
  143. package/docs/ci/github-actions.yml +59 -0
  144. package/docs/ci.md +109 -0
  145. package/docs/compatibility.md +115 -0
  146. package/docs/deployment.md +305 -0
  147. package/docs/hardening.md +118 -0
  148. package/docs/host-owned-routes.md +154 -0
  149. package/docs/migrations/0001-schema-version.md +54 -0
  150. package/docs/migrations/0002-initial-schema.md +84 -0
  151. package/docs/migrations/upgrade-guide.md +160 -0
  152. package/docs/observability.md +394 -0
  153. package/docs/plugins/account-lockout.md +131 -0
  154. package/docs/plugins/admin.md +402 -0
  155. package/docs/plugins/api-key.md +327 -0
  156. package/docs/plugins/audit-log.md +261 -0
  157. package/docs/plugins/data-isolation.md +186 -0
  158. package/docs/plugins/email-verification.md +121 -0
  159. package/docs/plugins/magic-link.md +123 -0
  160. package/docs/plugins/oauth.md +544 -0
  161. package/docs/plugins/openapi.md +250 -0
  162. package/docs/plugins/rate-limit.md +223 -0
  163. package/docs/plugins/social-login.md +337 -0
  164. package/docs/plugins/tenancy.md +122 -0
  165. package/docs/plugins/two-factor.md +191 -0
  166. package/docs/plugins/webauthn.md +188 -0
  167. package/docs/plugins/webhook.md +242 -0
  168. package/docs/policy-as-code.md +156 -0
  169. package/docs/route-manifest.md +46 -0
  170. package/docs/security.md +261 -0
  171. package/docs/threat-model.md +161 -0
  172. package/examples/README.md +119 -0
  173. package/examples/express-app/index.ts +747 -0
  174. package/examples/hono-app/docker-compose.yml +14 -0
  175. package/examples/hono-app/index.ts +1009 -0
  176. package/examples/policy/fortress.policy.json +47 -0
  177. package/examples/sveltekit-app/README.md +125 -0
  178. package/examples/sveltekit-app/src/app.d.ts +16 -0
  179. package/examples/sveltekit-app/src/hooks.server.ts +23 -0
  180. package/examples/sveltekit-app/src/lib/server/fortress.ts +81 -0
  181. package/examples/sveltekit-app/src/routes/api/echo/[id]/+server.ts +32 -0
  182. package/examples/sveltekit-app/src/routes/api/fortress/[...path]/+server.ts +15 -0
  183. package/examples/sveltekit-app/src/routes/dashboard/+page.server.ts +20 -0
  184. package/examples/sveltekit-app/src/routes/login/+page.server.ts +48 -0
  185. package/examples/sveltekit-app/src/routes/oauth/consent/+page.server.ts +69 -0
  186. package/examples/sveltekit-app/src/routes/oauth/consent/+page.svelte +83 -0
  187. package/migrations/pg/0001_schema_version.down.sql +2 -0
  188. package/migrations/pg/0001_schema_version.sql +8 -0
  189. package/migrations/pg/0002_initial_schema.down.sql +36 -0
  190. package/migrations/pg/0002_initial_schema.sql +376 -0
  191. package/migrations/pg/0003_auth_continuation.down.sql +6 -0
  192. package/migrations/pg/0003_auth_continuation.sql +30 -0
  193. package/migrations/pg/0004_tenant_default_unique.down.sql +1 -0
  194. package/migrations/pg/0004_tenant_default_unique.sql +14 -0
  195. package/migrations/pg/0005_hot_indexes_timestamptz.down.sql +71 -0
  196. package/migrations/pg/0005_hot_indexes_timestamptz.sql +71 -0
  197. package/migrations/pg/0006_canonical_email.down.sql +3 -0
  198. package/migrations/pg/0006_canonical_email.sql +8 -0
  199. package/migrations/pg/0007_audit_chain_anchor.down.sql +1 -0
  200. package/migrations/pg/0007_audit_chain_anchor.sql +8 -0
  201. package/migrations/pg/0008_two_factor_hardening.down.sql +8 -0
  202. package/migrations/pg/0008_two_factor_hardening.sql +8 -0
  203. package/migrations/pg/0009_encrypt_totp_secrets.down.sql +2 -0
  204. package/migrations/pg/0009_encrypt_totp_secrets.sql +5 -0
  205. package/migrations/pg/0010_bigint_append_only_ids.down.sql +2 -0
  206. package/migrations/pg/0010_bigint_append_only_ids.sql +23 -0
  207. package/migrations/sqlite/0001_schema_version.down.sql +2 -0
  208. package/migrations/sqlite/0001_schema_version.sql +8 -0
  209. package/migrations/sqlite/0002_initial_schema.down.sql +36 -0
  210. package/migrations/sqlite/0002_initial_schema.sql +376 -0
  211. package/migrations/sqlite/0003_auth_continuation.down.sql +27 -0
  212. package/migrations/sqlite/0003_auth_continuation.sql +45 -0
  213. package/migrations/sqlite/0004_tenant_default_unique.down.sql +1 -0
  214. package/migrations/sqlite/0004_tenant_default_unique.sql +13 -0
  215. package/migrations/sqlite/0005_hot_indexes_timestamptz.down.sql +10 -0
  216. package/migrations/sqlite/0005_hot_indexes_timestamptz.sql +10 -0
  217. package/migrations/sqlite/0006_canonical_email.down.sql +3 -0
  218. package/migrations/sqlite/0006_canonical_email.sql +8 -0
  219. package/migrations/sqlite/0007_audit_chain_anchor.down.sql +1 -0
  220. package/migrations/sqlite/0007_audit_chain_anchor.sql +8 -0
  221. package/migrations/sqlite/0008_two_factor_hardening.down.sql +14 -0
  222. package/migrations/sqlite/0008_two_factor_hardening.sql +7 -0
  223. package/migrations/sqlite/0009_encrypt_totp_secrets.down.sql +2 -0
  224. package/migrations/sqlite/0009_encrypt_totp_secrets.sql +5 -0
  225. package/migrations/sqlite/0010_bigint_append_only_ids.down.sql +1 -0
  226. package/migrations/sqlite/0010_bigint_append_only_ids.sql +2 -0
  227. package/package.json +398 -0
  228. package/src/adapters/database/index.ts +63 -0
  229. package/src/adapters/database/types.ts +22 -0
  230. package/src/changelog-script.test.ts +21 -0
  231. package/src/cli.test.ts +79 -0
  232. package/src/core/auth/auth-admin.test.ts +247 -0
  233. package/src/core/auth/auth-endpoints.ts +558 -0
  234. package/src/core/auth/auth-observer.test.ts +191 -0
  235. package/src/core/auth/auth-service.ts +1464 -0
  236. package/src/core/auth/email.test.ts +17 -0
  237. package/src/core/auth/email.ts +11 -0
  238. package/src/core/auth/hooks.test.ts +251 -0
  239. package/src/core/auth/impersonation.test.ts +179 -0
  240. package/src/core/auth/jwt.test.ts +184 -0
  241. package/src/core/auth/jwt.ts +239 -0
  242. package/src/core/auth/login-timing.test.ts +77 -0
  243. package/src/core/auth/password-policy.test.ts +253 -0
  244. package/src/core/auth/password-policy.ts +220 -0
  245. package/src/core/auth/password.test.ts +42 -0
  246. package/src/core/auth/password.ts +62 -0
  247. package/src/core/auth/post-auth-gate.test.ts +258 -0
  248. package/src/core/auth/post-auth-gate.ts +228 -0
  249. package/src/core/auth/refresh-token.test.ts +86 -0
  250. package/src/core/auth/refresh-token.ts +105 -0
  251. package/src/core/auth/timing-safe.ts +23 -0
  252. package/src/core/config.ts +212 -0
  253. package/src/core/endpoint.ts +149 -0
  254. package/src/core/errors.ts +199 -0
  255. package/src/core/fetcher-interop.test.ts +106 -0
  256. package/src/core/fortress.test.ts +354 -0
  257. package/src/core/fortress.ts +550 -0
  258. package/src/core/http/call.test.ts +148 -0
  259. package/src/core/http/call.ts +149 -0
  260. package/src/core/http/cookie-serialize.test.ts +198 -0
  261. package/src/core/http/cookie-serialize.ts +107 -0
  262. package/src/core/http/csrf.test.ts +140 -0
  263. package/src/core/http/csrf.ts +173 -0
  264. package/src/core/http/dispatch.ts +652 -0
  265. package/src/core/http/error-response.test.ts +69 -0
  266. package/src/core/http/error-response.ts +93 -0
  267. package/src/core/http/fortress-rbac.test.ts +43 -0
  268. package/src/core/http/fortress-rbac.ts +127 -0
  269. package/src/core/http/handle-request.test.ts +441 -0
  270. package/src/core/http/handle-request.ts +354 -0
  271. package/src/core/http/match.test.ts +122 -0
  272. package/src/core/http/match.ts +126 -0
  273. package/src/core/http/outbound.test.ts +34 -0
  274. package/src/core/http/outbound.ts +105 -0
  275. package/src/core/http/plugin-middleware.ts +67 -0
  276. package/src/core/http/principal.test.ts +345 -0
  277. package/src/core/http/principal.ts +86 -0
  278. package/src/core/http/protect.test.ts +220 -0
  279. package/src/core/http/protect.ts +394 -0
  280. package/src/core/http/token-extraction.test.ts +59 -0
  281. package/src/core/http/token-extraction.ts +53 -0
  282. package/src/core/iam/explain.test.ts +177 -0
  283. package/src/core/iam/explain.ts +302 -0
  284. package/src/core/iam/iam-endpoints.ts +779 -0
  285. package/src/core/iam/iam-service.test.ts +829 -0
  286. package/src/core/iam/iam-service.ts +1137 -0
  287. package/src/core/iam/permission-cache.test.ts +115 -0
  288. package/src/core/iam/permission-cache.ts +71 -0
  289. package/src/core/iam/permission-check-observer.test.ts +90 -0
  290. package/src/core/iam/permission-evaluator.test.ts +291 -0
  291. package/src/core/iam/permission-evaluator.ts +198 -0
  292. package/src/core/iam/permission-sync.test.ts +166 -0
  293. package/src/core/iam/permission-sync.ts +192 -0
  294. package/src/core/iam/resource-sync.test.ts +70 -0
  295. package/src/core/iam/resource-sync.ts +182 -0
  296. package/src/core/iam/service-account-http.test.ts +529 -0
  297. package/src/core/iam/service-account.test.ts +282 -0
  298. package/src/core/internal-adapter.test.ts +389 -0
  299. package/src/core/internal-adapter.ts +435 -0
  300. package/src/core/json-schema.ts +50 -0
  301. package/src/core/manifest/__snapshots__/route-manifest.test.ts.snap +192 -0
  302. package/src/core/manifest/drift.ts +103 -0
  303. package/src/core/manifest/route-manifest.test.ts +142 -0
  304. package/src/core/manifest/route-manifest.ts +154 -0
  305. package/src/core/migrate.test.ts +72 -0
  306. package/src/core/migrations/engine.test.ts +308 -0
  307. package/src/core/migrations/engine.ts +548 -0
  308. package/src/core/migrations/migrations.ts +1771 -0
  309. package/src/core/migrations/pg-migration.integration-test.ts +353 -0
  310. package/src/core/migrations/upgrade-fixture.test.ts +378 -0
  311. package/src/core/observability/db-instrumentation.ts +205 -0
  312. package/src/core/observability/listener-list.test.ts +124 -0
  313. package/src/core/observability/listener-list.ts +73 -0
  314. package/src/core/observability/logger.test.ts +43 -0
  315. package/src/core/observability/logger.ts +49 -0
  316. package/src/core/observability/spans.test.ts +193 -0
  317. package/src/core/observability/types.ts +80 -0
  318. package/src/core/openapi-drift.test.ts +41 -0
  319. package/src/core/openapi.ts +47 -0
  320. package/src/core/plugin-methods-map.ts +75 -0
  321. package/src/core/plugin-runner.test.ts +233 -0
  322. package/src/core/plugin-runner.ts +276 -0
  323. package/src/core/plugin.ts +210 -0
  324. package/src/core/policy/apply.ts +272 -0
  325. package/src/core/policy/diff.ts +288 -0
  326. package/src/core/policy/loader.test.ts +63 -0
  327. package/src/core/policy/loader.ts +214 -0
  328. package/src/core/policy/policy.test.ts +232 -0
  329. package/src/core/policy/types.ts +105 -0
  330. package/src/core/schema-builder.test.ts +660 -0
  331. package/src/core/schema-builder.ts +881 -0
  332. package/src/core/standard-schema.ts +56 -0
  333. package/src/core/types.ts +258 -0
  334. package/src/core/validation.test.ts +195 -0
  335. package/src/core/validation.ts +192 -0
  336. package/src/drizzle/adapter.test.ts +425 -0
  337. package/src/drizzle/adapter.ts +474 -0
  338. package/src/drizzle/index.ts +31 -0
  339. package/src/drizzle/pg/adapter.integration-test.ts +456 -0
  340. package/src/drizzle/pg/index.ts +13 -0
  341. package/src/drizzle/pg/pg.integration-test.ts +1539 -0
  342. package/src/drizzle/pg/schema.ts +539 -0
  343. package/src/drizzle/pg-error-map.test.ts +218 -0
  344. package/src/drizzle/pg-error-map.ts +151 -0
  345. package/src/drizzle/schema.ts +544 -0
  346. package/src/drizzle/sqlite-serialization.test.ts +106 -0
  347. package/src/express/express-api-key-user-routes.test.ts +241 -0
  348. package/src/express/express.test.ts +442 -0
  349. package/src/express/handle.ts +191 -0
  350. package/src/express/index.ts +62 -0
  351. package/src/express/middleware.ts +551 -0
  352. package/src/express/protect.ts +154 -0
  353. package/src/express/validated.test.ts +86 -0
  354. package/src/express/validated.ts +99 -0
  355. package/src/fetcher/index.ts +81 -0
  356. package/src/hono/convert-routes.test.ts +220 -0
  357. package/src/hono/convert-routes.ts +196 -0
  358. package/src/hono/converters.test.ts +50 -0
  359. package/src/hono/converters.ts +43 -0
  360. package/src/hono/handle.ts +79 -0
  361. package/src/hono/helpers.ts +2 -0
  362. package/src/hono/hono-api-key-user-routes.test.ts +225 -0
  363. package/src/hono/hono-handlers.test.ts +861 -0
  364. package/src/hono/hono.test.ts +454 -0
  365. package/src/hono/index.ts +101 -0
  366. package/src/hono/middleware/auth.ts +236 -0
  367. package/src/hono/middleware/auth.types.test.ts +94 -0
  368. package/src/hono/middleware/csrf.test.ts +127 -0
  369. package/src/hono/middleware/csrf.ts +68 -0
  370. package/src/hono/middleware/error-handler.ts +12 -0
  371. package/src/hono/middleware/plugin-middleware.ts +35 -0
  372. package/src/hono/middleware/rbac.ts +131 -0
  373. package/src/hono/middleware/security-headers.test.ts +88 -0
  374. package/src/hono/middleware/security-headers.ts +68 -0
  375. package/src/hono/openapi.ts +237 -0
  376. package/src/hono/protect.ts +87 -0
  377. package/src/hono/validated.test.ts +123 -0
  378. package/src/hono/validated.ts +62 -0
  379. package/src/index.ts +309 -0
  380. package/src/integration.test.ts +718 -0
  381. package/src/internal/changelog.ts +56 -0
  382. package/src/otel/index.ts +158 -0
  383. package/src/otel/otel-types.test.ts +35 -0
  384. package/src/otel/otel.test.ts +235 -0
  385. package/src/plugins/account-lockout/account-lockout.test.ts +367 -0
  386. package/src/plugins/account-lockout/index.ts +267 -0
  387. package/src/plugins/admin/admin-api-key.test.ts +438 -0
  388. package/src/plugins/admin/index.ts +1612 -0
  389. package/src/plugins/api-key/api-key.test.ts +684 -0
  390. package/src/plugins/api-key/core.ts +336 -0
  391. package/src/plugins/api-key/index.ts +315 -0
  392. package/src/plugins/audit-log/audit-log.test.ts +749 -0
  393. package/src/plugins/audit-log/index.ts +680 -0
  394. package/src/plugins/data-isolation/data-isolation.test.ts +197 -0
  395. package/src/plugins/data-isolation/index.ts +157 -0
  396. package/src/plugins/email-verification/email-verification.test.ts +199 -0
  397. package/src/plugins/email-verification/index.ts +190 -0
  398. package/src/plugins/magic-link/index.ts +129 -0
  399. package/src/plugins/magic-link/magic-link.test.ts +156 -0
  400. package/src/plugins/oauth/id-token.ts +86 -0
  401. package/src/plugins/oauth/index.ts +2145 -0
  402. package/src/plugins/oauth/jwks.test.ts +32 -0
  403. package/src/plugins/oauth/jwks.ts +182 -0
  404. package/src/plugins/oauth/oauth.test.ts +2220 -0
  405. package/src/plugins/oauth/pkce.ts +58 -0
  406. package/src/plugins/openapi/index.ts +298 -0
  407. package/src/plugins/openapi/openapi.test.ts +425 -0
  408. package/src/plugins/openapi/spec-builder.ts +287 -0
  409. package/src/plugins/rate-limit/express.test.ts +89 -0
  410. package/src/plugins/rate-limit/express.ts +86 -0
  411. package/src/plugins/rate-limit/hono.test.ts +60 -0
  412. package/src/plugins/rate-limit/hono.ts +74 -0
  413. package/src/plugins/rate-limit/index.ts +383 -0
  414. package/src/plugins/rate-limit/memory-store.ts +61 -0
  415. package/src/plugins/rate-limit/rate-limit.test.ts +756 -0
  416. package/src/plugins/rate-limit/sveltekit.test.ts +58 -0
  417. package/src/plugins/rate-limit/sveltekit.ts +66 -0
  418. package/src/plugins/social-login/index.ts +715 -0
  419. package/src/plugins/social-login/providers/apple.ts +34 -0
  420. package/src/plugins/social-login/providers/discord.ts +26 -0
  421. package/src/plugins/social-login/providers/github.ts +54 -0
  422. package/src/plugins/social-login/providers/google.ts +23 -0
  423. package/src/plugins/social-login/providers/index.ts +22 -0
  424. package/src/plugins/social-login/providers/microsoft.ts +35 -0
  425. package/src/plugins/social-login/providers/oidc.ts +37 -0
  426. package/src/plugins/social-login/providers/providers.test.ts +211 -0
  427. package/src/plugins/social-login/social-login.test.ts +675 -0
  428. package/src/plugins/social-login/types.ts +80 -0
  429. package/src/plugins/tenancy/index.ts +544 -0
  430. package/src/plugins/tenancy/tenancy.test.ts +323 -0
  431. package/src/plugins/two-factor/index.ts +569 -0
  432. package/src/plugins/two-factor/two-factor.test.ts +235 -0
  433. package/src/plugins/webauthn/index.ts +554 -0
  434. package/src/plugins/webauthn/webauthn.test.ts +472 -0
  435. package/src/plugins/webhook/builtin-events.ts +29 -0
  436. package/src/plugins/webhook/delivery.test.ts +51 -0
  437. package/src/plugins/webhook/delivery.ts +154 -0
  438. package/src/plugins/webhook/hooks.ts +89 -0
  439. package/src/plugins/webhook/index.ts +445 -0
  440. package/src/plugins/webhook/queue/database.ts +67 -0
  441. package/src/plugins/webhook/queue/in-memory.test.ts +48 -0
  442. package/src/plugins/webhook/queue/in-memory.ts +71 -0
  443. package/src/plugins/webhook/queue/types.ts +51 -0
  444. package/src/plugins/webhook/registry.test.ts +48 -0
  445. package/src/plugins/webhook/registry.ts +64 -0
  446. package/src/plugins/webhook/signing.ts +45 -0
  447. package/src/plugins/webhook/ssrf.ts +198 -0
  448. package/src/plugins/webhook/types.ts +138 -0
  449. package/src/plugins/webhook/webhook.test.ts +324 -0
  450. package/src/sveltekit/actions.ts +233 -0
  451. package/src/sveltekit/catch-all.ts +43 -0
  452. package/src/sveltekit/cookies.ts +136 -0
  453. package/src/sveltekit/handle.ts +356 -0
  454. package/src/sveltekit/helpers.ts +69 -0
  455. package/src/sveltekit/index.ts +74 -0
  456. package/src/sveltekit/protect.ts +80 -0
  457. package/src/sveltekit/sveltekit-types.test.ts +31 -0
  458. package/src/sveltekit/sveltekit.test.ts +799 -0
  459. package/src/sveltekit/types.ts +134 -0
  460. package/src/sveltekit/validated.test.ts +117 -0
  461. package/src/sveltekit/validated.ts +78 -0
  462. package/src/testing/adapter-conformance.test.ts +408 -0
  463. package/src/testing/checks.test.ts +124 -0
  464. package/src/testing/checks.ts +304 -0
  465. package/src/testing/index.ts +107 -0
@@ -0,0 +1,2378 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/plugins/admin/index.ts
21
+ var admin_exports = {};
22
+ __export(admin_exports, {
23
+ admin: () => admin
24
+ });
25
+ module.exports = __toCommonJS(admin_exports);
26
+
27
+ // src/core/schema-builder.ts
28
+ var import_openapi = require("@bajustone/fetcher/openapi");
29
+ var import_schema = require("@bajustone/fetcher/schema");
30
+ function collectRefNames(schema, acc) {
31
+ if (!schema || typeof schema !== "object")
32
+ return acc;
33
+ if (typeof schema.$ref === "string") {
34
+ const i = schema.$ref.lastIndexOf("/");
35
+ acc.add(i >= 0 ? schema.$ref.slice(i + 1) : schema.$ref);
36
+ }
37
+ if (schema.properties) {
38
+ for (const key of Object.keys(schema.properties))
39
+ collectRefNames(schema.properties[key], acc);
40
+ }
41
+ collectRefNames(schema.items, acc);
42
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
43
+ const variants = schema[key];
44
+ if (variants) {
45
+ for (const variant of variants)
46
+ collectRefNames(variant, acc);
47
+ }
48
+ }
49
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object")
50
+ collectRefNames(schema.additionalProperties, acc);
51
+ return acc;
52
+ }
53
+ var refDefinitionContexts = /* @__PURE__ */ new WeakMap();
54
+ function refName(refPath) {
55
+ const i = refPath.lastIndexOf("/");
56
+ return i >= 0 ? refPath.slice(i + 1) : refPath;
57
+ }
58
+ var COMPONENT_NAME_RE = /^[\w.-]+$/;
59
+ function assertComponentName(name) {
60
+ if (!COMPONENT_NAME_RE.test(name))
61
+ throw new Error(`Invalid OpenAPI component name '${name}'`);
62
+ }
63
+ function resolveRefDefs(schema) {
64
+ if (collectRefNames(schema, /* @__PURE__ */ new Set()).size === 0)
65
+ return void 0;
66
+ const defs = /* @__PURE__ */ Object.create(null);
67
+ const unresolved = /* @__PURE__ */ new Set();
68
+ const hasOwn = (value, key) => Object.hasOwn(value, key);
69
+ const visit = (node, inherited) => {
70
+ if (!node || typeof node !== "object")
71
+ return;
72
+ const context = refDefinitionContexts.get(node) ?? inherited;
73
+ if (typeof node.$ref === "string") {
74
+ const name = refName(node.$ref);
75
+ const component = context && hasOwn(context, name) ? context[name] : void 0;
76
+ if (component) {
77
+ if (hasOwn(defs, name) && !unresolved.has(name) && defs[name] !== component) {
78
+ throw new Error(`Conflicting component definitions for $ref '${name}'`);
79
+ }
80
+ const firstResolution = !hasOwn(defs, name) || unresolved.delete(name);
81
+ defs[name] = component;
82
+ if (firstResolution)
83
+ visit(component, context);
84
+ } else if (!hasOwn(defs, name)) {
85
+ defs[name] = {};
86
+ unresolved.add(name);
87
+ }
88
+ }
89
+ if (node.properties) {
90
+ for (const child of Object.values(node.properties))
91
+ visit(child, context);
92
+ }
93
+ visit(node.items, context);
94
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
95
+ for (const child of node[key] ?? [])
96
+ visit(child, context);
97
+ }
98
+ if (node.additionalProperties && typeof node.additionalProperties === "object")
99
+ visit(node.additionalProperties, context);
100
+ };
101
+ visit(schema);
102
+ return defs;
103
+ }
104
+ function createStandardProps(schema) {
105
+ let validateFn;
106
+ return {
107
+ version: 1,
108
+ vendor: "fortress",
109
+ validate(value) {
110
+ if (!validateFn) {
111
+ const defs = resolveRefDefs(schema);
112
+ validateFn = (0, import_openapi.fromJSONSchema)(schema, defs)["~standard"].validate;
113
+ }
114
+ return validateFn(value);
115
+ }
116
+ };
117
+ }
118
+ function createExactOneOfProps(schemas) {
119
+ const validate = (value) => {
120
+ const results = schemas.map((schema) => schema["~standard"].validate(value));
121
+ if (results.some((result) => result instanceof Promise)) {
122
+ return Promise.all(results).then((resolved) => exactOneResult(resolved));
123
+ }
124
+ return exactOneResult(results);
125
+ };
126
+ return {
127
+ version: 1,
128
+ vendor: "fortress",
129
+ validate
130
+ };
131
+ }
132
+ function exactOneResult(results) {
133
+ const successes = results.filter((result) => !result.issues);
134
+ if (successes.length === 1)
135
+ return { value: successes[0].value };
136
+ return {
137
+ issues: [{
138
+ message: successes.length === 0 ? "Value must match exactly one oneOf variant" : "Value must match exactly one oneOf variant; multiple variants matched"
139
+ }]
140
+ };
141
+ }
142
+ function toFortressSchema(schema, refDefinitions, standardProps) {
143
+ if (refDefinitions)
144
+ refDefinitionContexts.set(schema, refDefinitions);
145
+ return Object.assign(schema, {
146
+ "~standard": standardProps ?? createStandardProps(schema)
147
+ });
148
+ }
149
+ function str(opts) {
150
+ const o = typeof opts === "string" ? { description: opts } : opts ?? {};
151
+ const s = { type: "string" };
152
+ if (o.description)
153
+ s.description = o.description;
154
+ if (o.min !== void 0)
155
+ s.minLength = o.min;
156
+ if (o.max !== void 0)
157
+ s.maxLength = o.max;
158
+ if (o.pattern !== void 0)
159
+ s.pattern = o.pattern;
160
+ if (o.format !== void 0)
161
+ s.format = o.format;
162
+ return toFortressSchema(s);
163
+ }
164
+ function int(opts) {
165
+ const o = typeof opts === "string" ? { description: opts } : opts ?? {};
166
+ const s = { type: "integer" };
167
+ if (o.description)
168
+ s.description = o.description;
169
+ if (o.min !== void 0)
170
+ s.minimum = o.min;
171
+ if (o.max !== void 0)
172
+ s.maximum = o.max;
173
+ return toFortressSchema(s);
174
+ }
175
+ function id(description) {
176
+ const s = { type: "string", minLength: 1 };
177
+ if (description)
178
+ s.description = description;
179
+ return toFortressSchema(s);
180
+ }
181
+ function bool(description) {
182
+ const s = { type: "boolean" };
183
+ if (description)
184
+ s.description = description;
185
+ return toFortressSchema(s);
186
+ }
187
+ function arr(items, description) {
188
+ const s = { type: "array", items };
189
+ if (description)
190
+ s.description = description;
191
+ return toFortressSchema(s);
192
+ }
193
+ function obj(properties, ...required) {
194
+ const s = { type: "object", properties };
195
+ if (required.length > 0)
196
+ s.required = required;
197
+ return toFortressSchema(s);
198
+ }
199
+ function nullable(schema) {
200
+ if (typeof schema.$ref === "string")
201
+ return toFortressSchema({ oneOf: [schema, { type: "null" }] });
202
+ const s = { ...schema, nullable: true };
203
+ return toFortressSchema(s, refDefinitionContexts.get(schema));
204
+ }
205
+ function oneOf(...schemas) {
206
+ const schema = { oneOf: schemas };
207
+ return toFortressSchema(
208
+ schema,
209
+ void 0,
210
+ createExactOneOfProps(schemas)
211
+ );
212
+ }
213
+ function defineComponents(components) {
214
+ for (const name of Object.keys(components))
215
+ assertComponentName(name);
216
+ const definitions = Object.assign(
217
+ /* @__PURE__ */ Object.create(null),
218
+ components
219
+ );
220
+ return {
221
+ components,
222
+ ref: (name) => {
223
+ assertComponentName(name);
224
+ return toFortressSchema(
225
+ { $ref: `#/components/schemas/${String(name)}` },
226
+ definitions
227
+ );
228
+ }
229
+ };
230
+ }
231
+ function enums(...values) {
232
+ return toFortressSchema({ enum: values });
233
+ }
234
+ function strFormat(format, description) {
235
+ const s = { type: "string", format };
236
+ if (description)
237
+ s.description = description;
238
+ return toFortressSchema(s);
239
+ }
240
+ function nullType() {
241
+ return toFortressSchema({ type: "null" });
242
+ }
243
+ function record(description) {
244
+ const s = { type: "object", additionalProperties: true };
245
+ if (description)
246
+ s.description = description;
247
+ return toFortressSchema(s);
248
+ }
249
+ function recordOf(valueSchema, description) {
250
+ const s = { type: "object", additionalProperties: valueSchema };
251
+ if (description)
252
+ s.description = description;
253
+ return toFortressSchema(s);
254
+ }
255
+ function makeFormatBuilder(factory) {
256
+ const f = factory();
257
+ return (description) => {
258
+ const s = { type: "string" };
259
+ if (f.format)
260
+ s.format = f.format;
261
+ if (f.pattern)
262
+ s.pattern = f.pattern;
263
+ if (description)
264
+ s.description = description;
265
+ return toFortressSchema(s);
266
+ };
267
+ }
268
+ var email = makeFormatBuilder(import_schema.email);
269
+ var uuid = makeFormatBuilder(import_schema.uuid);
270
+ var url = makeFormatBuilder(import_schema.url);
271
+ var datetime = makeFormatBuilder(import_schema.datetime);
272
+ var date = makeFormatBuilder(import_schema.date);
273
+ var time = makeFormatBuilder(import_schema.time);
274
+ function isStandardSchema(value) {
275
+ return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
276
+ }
277
+ function isFortressSchema(value) {
278
+ return isStandardSchema(value) && value["~standard"].vendor === "fortress" && ("type" in value || "$ref" in value || "oneOf" in value || "anyOf" in value);
279
+ }
280
+ var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
281
+ function hasJsonSchemaShape(value) {
282
+ if (typeof value !== "object" || value === null)
283
+ return false;
284
+ const v = value;
285
+ if ("$ref" in v || "oneOf" in v || "anyOf" in v || "allOf" in v)
286
+ return true;
287
+ if (typeof v.type === "string" && JSON_SCHEMA_TYPES.has(v.type))
288
+ return true;
289
+ if (Array.isArray(v.type) && v.type.every((t) => typeof t === "string" && JSON_SCHEMA_TYPES.has(t)))
290
+ return true;
291
+ return false;
292
+ }
293
+ function extractJsonSchema(schema) {
294
+ if (isFortressSchema(schema)) {
295
+ return schema;
296
+ }
297
+ if (isStandardSchema(schema)) {
298
+ const std = schema["~standard"];
299
+ if (std?.jsonSchema?.input) {
300
+ return std.jsonSchema.input({ target: "draft-2020-12" });
301
+ }
302
+ if (hasJsonSchemaShape(schema)) {
303
+ return schema;
304
+ }
305
+ }
306
+ return {};
307
+ }
308
+ var ErrorEnvelope = obj({
309
+ code: str("Machine-readable error code"),
310
+ message: str("Human-readable error message"),
311
+ statusCode: int("HTTP status code"),
312
+ details: toFortressSchema({ description: "Optional structured error details" })
313
+ }, "code", "message", "statusCode");
314
+ var EndpointBuilder = class {
315
+ _method;
316
+ _path;
317
+ _handler = "";
318
+ _summary = "";
319
+ _description;
320
+ _tags = [];
321
+ _security = [];
322
+ _deprecated = false;
323
+ _permission;
324
+ _body;
325
+ _query;
326
+ _params;
327
+ _responses = {};
328
+ constructor(method, path) {
329
+ this._method = method;
330
+ this._path = path;
331
+ }
332
+ summary(s) {
333
+ this._summary = s;
334
+ return this;
335
+ }
336
+ description(s) {
337
+ this._description = s;
338
+ return this;
339
+ }
340
+ tags(...t) {
341
+ this._tags.push(...t);
342
+ return this;
343
+ }
344
+ security(...s) {
345
+ this._security.push(...s);
346
+ return this;
347
+ }
348
+ deprecated() {
349
+ this._deprecated = true;
350
+ return this;
351
+ }
352
+ permission(resource, action) {
353
+ this._permission = { resource, action };
354
+ return this;
355
+ }
356
+ body(schema) {
357
+ this._body = schema;
358
+ return this;
359
+ }
360
+ query(schema) {
361
+ this._query = schema;
362
+ return this;
363
+ }
364
+ params(schema) {
365
+ this._params = schema;
366
+ return this;
367
+ }
368
+ response(status, description, schema) {
369
+ const stored = { description };
370
+ if (schema !== void 0) {
371
+ stored.schema = isStandardSchema(schema) ? extractJsonSchema(schema) : schema;
372
+ }
373
+ this._responses[status] = stored;
374
+ return this;
375
+ }
376
+ /**
377
+ * Declare an error response that returns the canonical {@link ErrorEnvelope}
378
+ * body. Shorthand for `.response(status, description, ErrorEnvelope)`.
379
+ *
380
+ * Aligns host endpoint error responses with what Fortress's own routes
381
+ * emit, so a single client-side error parser works everywhere.
382
+ *
383
+ * ```ts
384
+ * endpoint('GET', '/schools/:id')
385
+ * .summary('Get a school')
386
+ * .params(obj({ id: str() }, 'id'))
387
+ * .response(200, 'OK', SchoolEnvelope)
388
+ * .errorResponse(404, 'School not found')
389
+ * .errorResponse(403, 'Forbidden')
390
+ * .build();
391
+ * ```
392
+ */
393
+ errorResponse(status, description) {
394
+ return this.response(status, description, ErrorEnvelope);
395
+ }
396
+ handler(name) {
397
+ this._handler = name;
398
+ return this;
399
+ }
400
+ build() {
401
+ const def = {
402
+ method: this._method,
403
+ path: this._path,
404
+ handler: this._handler
405
+ };
406
+ if (this._summary || this._tags.length > 0 || this._security.length > 0 || this._description || this._deprecated || this._permission) {
407
+ def.meta = {
408
+ summary: this._summary,
409
+ ...this._description && { description: this._description },
410
+ ...this._tags.length > 0 && { tags: this._tags },
411
+ ...this._security.length > 0 && { security: this._security },
412
+ ...this._deprecated && { deprecated: true },
413
+ ...this._permission && { permission: this._permission }
414
+ };
415
+ }
416
+ if (this._body || this._query || this._params) {
417
+ def.input = {};
418
+ if (this._body) {
419
+ def.input.body = extractJsonSchema(this._body);
420
+ if (isStandardSchema(this._body))
421
+ def.input.bodySchema = this._body;
422
+ }
423
+ if (this._query) {
424
+ def.input.query = extractJsonSchema(this._query);
425
+ if (isStandardSchema(this._query))
426
+ def.input.querySchema = this._query;
427
+ }
428
+ if (this._params) {
429
+ def.input.params = extractJsonSchema(this._params);
430
+ if (isStandardSchema(this._params))
431
+ def.input.paramsSchema = this._params;
432
+ }
433
+ }
434
+ if (Object.keys(this._responses).length > 0) {
435
+ def.responses = this._responses;
436
+ }
437
+ return def;
438
+ }
439
+ };
440
+ function endpoint(method, path) {
441
+ return new EndpointBuilder(method, path);
442
+ }
443
+
444
+ // src/core/auth/auth-endpoints.ts
445
+ var User = obj(
446
+ {
447
+ id: id("User ID"),
448
+ email: strFormat("email", "User email"),
449
+ name: str("Display name"),
450
+ isActive: bool("Account active status"),
451
+ emailVerified: bool("Email verification status"),
452
+ createdAt: strFormat("date-time", "Creation timestamp"),
453
+ updatedAt: strFormat("date-time", "Last update timestamp")
454
+ },
455
+ "id",
456
+ "email",
457
+ "name",
458
+ "isActive",
459
+ "createdAt",
460
+ "updatedAt"
461
+ );
462
+ var AuthChallenge = obj(
463
+ {
464
+ reason: enums("two-factor", "webauthn", "email-verification", "magic-link"),
465
+ continuationToken: str("Single-use post-auth continuation token")
466
+ },
467
+ "reason",
468
+ "continuationToken"
469
+ );
470
+ var AuthResult = oneOf(
471
+ obj(
472
+ {
473
+ status: enums("success"),
474
+ user: User,
475
+ method: enums("password", "refresh", "two-factor", "webauthn", "magic-link", "impersonation"),
476
+ accessToken: str("JWT access token"),
477
+ refreshToken: str("Refresh token for rotation"),
478
+ pluginData: record()
479
+ },
480
+ "status",
481
+ "user",
482
+ "method",
483
+ "accessToken",
484
+ "refreshToken"
485
+ ),
486
+ obj(
487
+ {
488
+ status: enums("pending"),
489
+ user: User,
490
+ pending: AuthChallenge,
491
+ pluginData: record()
492
+ },
493
+ "status",
494
+ "user",
495
+ "pending"
496
+ ),
497
+ obj(
498
+ {
499
+ status: enums("impersonation"),
500
+ user: User,
501
+ accessToken: str("JWT access token"),
502
+ refreshToken: nullType(),
503
+ pluginData: record()
504
+ },
505
+ "status",
506
+ "user",
507
+ "accessToken",
508
+ "refreshToken"
509
+ )
510
+ );
511
+ var AuthTokenPair = obj(
512
+ {
513
+ accessToken: str("JWT access token"),
514
+ refreshToken: str("New refresh token")
515
+ },
516
+ "accessToken",
517
+ "refreshToken"
518
+ );
519
+ var SessionInfo = obj(
520
+ {
521
+ id: id("Token ID"),
522
+ ipAddress: nullable(str("Client IP address")),
523
+ userAgent: nullable(str("Client user agent")),
524
+ deviceName: nullable(str("Device name")),
525
+ createdAt: strFormat("date-time", "Session creation time"),
526
+ lastActiveAt: nullable(strFormat("date-time", "Last activity time"))
527
+ },
528
+ "id",
529
+ "createdAt"
530
+ );
531
+ var CreateUserInput = obj(
532
+ {
533
+ email: email("User email"),
534
+ // enforced format (ReDoS-safe pattern), not annotation-only
535
+ name: str("Display name"),
536
+ password: str("User password"),
537
+ isActive: bool("Set active status (default true)")
538
+ },
539
+ "email",
540
+ "name"
541
+ );
542
+ var ErrorResponse = obj(
543
+ {
544
+ code: str("Error code (e.g. UNAUTHORIZED)"),
545
+ message: str("Human-readable message"),
546
+ statusCode: int("HTTP status code")
547
+ },
548
+ "code",
549
+ "message",
550
+ "statusCode"
551
+ );
552
+ var LoginIdentifier = obj(
553
+ {
554
+ id: id("Identifier ID"),
555
+ userId: id("User ID"),
556
+ type: enums("email", "phone", "username"),
557
+ value: str("Identifier value")
558
+ },
559
+ "id",
560
+ "userId",
561
+ "type",
562
+ "value"
563
+ );
564
+ var authComponents = defineComponents({
565
+ User,
566
+ AuthChallenge,
567
+ AuthResult,
568
+ AuthTokenPair,
569
+ SessionInfo,
570
+ CreateUserInput,
571
+ ErrorResponse,
572
+ LoginIdentifier
573
+ });
574
+ var authComponentSchemas = authComponents.components;
575
+ var authRef = authComponents.ref;
576
+ var authEndpoints = {
577
+ login: endpoint("POST", "/auth/login").summary("Login with credentials").tags("Auth").security("none").body(obj(
578
+ {
579
+ identifier: str("Email, username, or phone"),
580
+ password: str("User password"),
581
+ trustedDeviceToken: str("Opaque trusted-device token previously returned after two-factor verification")
582
+ },
583
+ "identifier",
584
+ "password"
585
+ )).response(200, "Login successful", authRef("AuthResult")).response(401, "Invalid credentials", authRef("ErrorResponse")).handler("login").build(),
586
+ verifyTwoFactor: endpoint("POST", "/auth/2fa/verify").summary("Complete a pending two-factor challenge").tags("Auth", "Two-Factor").security("none").body(obj(
587
+ {
588
+ continuationToken: str("Single-use auth continuation token"),
589
+ code: str("TOTP or backup code"),
590
+ rememberDevice: bool("Issue an opaque trusted-device token after successful verification")
591
+ },
592
+ "continuationToken",
593
+ "code"
594
+ )).response(200, "Authentication result", authRef("AuthResult")).response(400, "Two-factor plugin unavailable", authRef("ErrorResponse")).response(401, "Invalid continuation or verification code", authRef("ErrorResponse")).handler("verifyTwoFactor").build(),
595
+ verifyMagicLink: endpoint("POST", "/auth/magic-link/verify").summary("Verify a magic-link token").tags("Auth", "Magic Link").security("none").body(obj({ token: str("Raw magic-link token") }, "token")).response(200, "Authentication result", authRef("AuthResult")).response(400, "Magic-link plugin unavailable", authRef("ErrorResponse")).response(404, "Invalid or expired magic link", authRef("ErrorResponse")).handler("verifyMagicLink").build(),
596
+ createUser: endpoint("POST", "/auth/register").summary("Create a new user").tags("Auth").security("none").body(CreateUserInput).response(201, "User created", authRef("User")).response(409, "Email already exists", authRef("ErrorResponse")).handler("createUser").build(),
597
+ refresh: endpoint("POST", "/auth/refresh").summary("Refresh access token").tags("Auth").security("none").body(obj({ refreshToken: str("Current refresh token") }, "refreshToken")).response(200, "Tokens refreshed", authRef("AuthTokenPair")).response(401, "Invalid or expired refresh token", authRef("ErrorResponse")).handler("refresh").build(),
598
+ logout: endpoint("POST", "/auth/logout").summary("Logout and revoke refresh token").tags("Auth").security("none").body(obj({ refreshToken: str("Refresh token to revoke") }, "refreshToken")).response(200, "Logged out", obj({ ok: bool() }, "ok")).handler("logout").build(),
599
+ me: endpoint("GET", "/auth/me").summary("Get current user profile").tags("Auth").security("bearer").response(200, "Current user", authRef("User")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("me").build(),
600
+ listSessions: endpoint("GET", "/auth/sessions").summary("List active sessions").tags("Auth").security("bearer").response(200, "Active sessions", arr(authRef("SessionInfo"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("listSessions").build(),
601
+ revokeSession: endpoint("DELETE", "/auth/sessions/:id").summary("Revoke a specific session").tags("Auth").security("bearer").params(obj({ id: id("Session/token ID") }, "id")).response(200, "Session revoked", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("revokeSession").build(),
602
+ revokeAllOtherSessions: endpoint("DELETE", "/auth/sessions").summary("Revoke all other sessions").description("Revokes all active sessions except the current one. Requires the current token ID.").tags("Auth").security("bearer").body(obj({ currentTokenId: id("Current token ID to keep") }, "currentTokenId")).response(200, "Other sessions revoked", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("revokeAllOtherSessions").build(),
603
+ addLoginIdentifier: endpoint("POST", "/auth/identifiers").summary("Add a login identifier").tags("Auth").security("bearer").body(obj(
604
+ {
605
+ type: enums("email", "phone", "username"),
606
+ value: str("Identifier value")
607
+ },
608
+ "type",
609
+ "value"
610
+ )).response(200, "Identifier added", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("addLoginIdentifier").build(),
611
+ removeLoginIdentifier: endpoint("DELETE", "/auth/identifiers").summary("Remove a login identifier").tags("Auth").security("bearer").body(obj(
612
+ {
613
+ type: enums("email", "phone", "username"),
614
+ value: str("Identifier value")
615
+ },
616
+ "type",
617
+ "value"
618
+ )).response(200, "Identifier removed", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("removeLoginIdentifier").build(),
619
+ getLoginIdentifiers: endpoint("GET", "/auth/identifiers").summary("List login identifiers").tags("Auth").security("bearer").response(200, "Login identifiers", arr(authRef("LoginIdentifier"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getLoginIdentifiers").build(),
620
+ impersonate: endpoint("POST", "/auth/impersonate").summary("Impersonate a user").description("Issue a short-lived, non-renewable token to act as another user. Requires fortress:impersonate permission.").tags("Auth").security("bearer").permission("fortress", "impersonate").body(obj(
621
+ {
622
+ targetUserId: id("User to impersonate"),
623
+ reason: str("Reason for impersonation"),
624
+ expirySeconds: int("Token expiry in seconds (default 3600)")
625
+ },
626
+ "targetUserId"
627
+ )).response(200, "Impersonation token issued", authRef("AuthResult")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Target user not found", authRef("ErrorResponse")).handler("impersonate").build()
628
+ };
629
+
630
+ // src/core/errors.ts
631
+ var FortressError = class extends Error {
632
+ code;
633
+ statusCode;
634
+ retryAfter;
635
+ details;
636
+ /** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
637
+ oauthError;
638
+ /** Human-readable description for the OAuth `error_description` field. */
639
+ oauthDescription;
640
+ /** Optional URI for the OAuth `error_uri` field (§5.2). */
641
+ oauthErrorUri;
642
+ constructor(code, message, statusCode, options) {
643
+ super(message, { cause: options?.cause });
644
+ this.code = code;
645
+ this.statusCode = statusCode;
646
+ this.retryAfter = options?.retryAfter;
647
+ this.details = options?.details;
648
+ this.oauthError = options?.oauthError;
649
+ this.oauthDescription = options?.oauthDescription;
650
+ this.oauthErrorUri = options?.oauthErrorUri;
651
+ }
652
+ toJSON() {
653
+ return {
654
+ code: this.code,
655
+ message: this.message,
656
+ statusCode: this.statusCode,
657
+ ...this.details !== void 0 && { details: this.details }
658
+ };
659
+ }
660
+ };
661
+ var Errors = {
662
+ unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
663
+ tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
664
+ sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
665
+ sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
666
+ forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
667
+ badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
668
+ notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
669
+ conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
670
+ unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
671
+ rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
672
+ database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
673
+ serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
674
+ validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
675
+ /**
676
+ * RFC 6749 §5.2 / §4.1.2.1 OAuth error.
677
+ *
678
+ * The HTTP error mapper detects `oauthError` and emits the OAuth-spec
679
+ * JSON body `{ error, error_description, error_uri? }` instead of the
680
+ * default fortress `{ code, message }` shape, so strict OAuth clients
681
+ * (Moodle, openid-client, Spring Security, etc.) can switch behaviour
682
+ * on the machine-readable `error` field.
683
+ *
684
+ * Status is inferred from the OAuth code per the spec table:
685
+ * - `invalid_client` → 401
686
+ * - everything else → 400
687
+ */
688
+ oauth: (error, description, options) => {
689
+ const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
690
+ const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
691
+ return new FortressError(code, description ?? error, status, {
692
+ oauthError: error,
693
+ oauthDescription: description,
694
+ oauthErrorUri: options?.errorUri,
695
+ cause: options?.cause
696
+ });
697
+ },
698
+ /**
699
+ * Reconstruct a {@link FortressError} from a JSON error body emitted by
700
+ * `errorToResponse`. Used by the in-process `fortress.call.*` client to
701
+ * convert non-2xx responses into typed throws that mirror what a direct
702
+ * service-method call would produce.
703
+ *
704
+ * Maps by HTTP status when the body doesn't carry a recognizable
705
+ * `{ code, message }` payload, so network errors and opaque upstream
706
+ * failures still become structured `FortressError`s.
707
+ */
708
+ fromHttpResponse: (status, body) => {
709
+ const payload = body && typeof body === "object" ? body : {};
710
+ const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
711
+ const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
712
+ return new FortressError(code, message, status, { details: payload.details });
713
+ }
714
+ };
715
+ function isFortressErrorCode(code) {
716
+ return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
717
+ }
718
+ function statusToErrorCode(status) {
719
+ if (status === 401)
720
+ return "UNAUTHORIZED";
721
+ if (status === 403)
722
+ return "FORBIDDEN";
723
+ if (status === 404)
724
+ return "NOT_FOUND";
725
+ if (status === 409)
726
+ return "CONFLICT";
727
+ if (status === 422)
728
+ return "UNPROCESSABLE_ENTITY";
729
+ if (status === 429)
730
+ return "RATE_LIMITED";
731
+ if (status === 503)
732
+ return "SERVICE_UNAVAILABLE";
733
+ if (status >= 500)
734
+ return "DATABASE_ERROR";
735
+ return "BAD_REQUEST";
736
+ }
737
+
738
+ // src/core/iam/iam-endpoints.ts
739
+ var Role = obj(
740
+ {
741
+ id: id("Role ID"),
742
+ name: str("Role name"),
743
+ description: nullable(str("Role description")),
744
+ isSystem: bool("Whether this is a system role")
745
+ },
746
+ "id",
747
+ "name"
748
+ );
749
+ var Group = obj(
750
+ {
751
+ id: id("Group ID"),
752
+ name: str("Group name"),
753
+ description: nullable(str("Group description"))
754
+ },
755
+ "id",
756
+ "name"
757
+ );
758
+ var Permission = obj(
759
+ {
760
+ id: id("Permission ID"),
761
+ resource: str("Resource name"),
762
+ action: str("Action name"),
763
+ effect: enums("ALLOW", "DENY"),
764
+ conditions: arr(obj({
765
+ field: str("Condition field"),
766
+ operator: enums("eq", "neq", "in", "startsWith"),
767
+ value: str("Condition value")
768
+ }), "Permission conditions"),
769
+ description: nullable(str("Permission description"))
770
+ },
771
+ "id",
772
+ "resource",
773
+ "action",
774
+ "effect"
775
+ );
776
+ var PermissionInput = obj(
777
+ {
778
+ resource: str("Resource name"),
779
+ action: str("Action name"),
780
+ effect: enums("ALLOW", "DENY"),
781
+ conditions: arr(obj({
782
+ field: str("Condition field"),
783
+ operator: enums("eq", "neq", "in", "startsWith"),
784
+ value: str("Condition value")
785
+ }))
786
+ },
787
+ "resource",
788
+ "action"
789
+ );
790
+ var ServiceAccount = obj(
791
+ {
792
+ id: id("Service account ID"),
793
+ name: str("Machine identifier \u2014 immutable after creation"),
794
+ displayName: nullable(str("Human-readable label")),
795
+ description: nullable(str("Free-form description")),
796
+ isActive: bool("Whether the account can authenticate and resolve permissions"),
797
+ createdAt: str("ISO 8601 creation timestamp"),
798
+ updatedAt: str("ISO 8601 update timestamp")
799
+ },
800
+ "id",
801
+ "name",
802
+ "isActive"
803
+ );
804
+ var iamComponents = defineComponents({
805
+ Role,
806
+ Group,
807
+ Permission,
808
+ PermissionInput,
809
+ ServiceAccount
810
+ });
811
+ var iamComponentSchemas = iamComponents.components;
812
+ var iamRef = iamComponents.ref;
813
+ var iamEndpoints = {
814
+ // ── Resources ──
815
+ getResources: endpoint("GET", "/iam/resources").summary("List all available resources").tags("IAM", "Resources").security("bearer").permission("fortress", "viewResources").response(200, "Available resources with their actions", obj({
816
+ resources: recordOf(obj({
817
+ actions: arr(str("Action name"), "Available actions"),
818
+ description: str("Resource description")
819
+ }), "Map of resource name to definition")
820
+ }, "resources")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getResources").build(),
821
+ // ── Roles ──
822
+ getRoles: endpoint("GET", "/iam/roles").summary("List all roles").tags("IAM", "Roles").security("bearer").permission("fortress", "viewRoles").response(200, "All roles", arr(iamRef("Role"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getRoles").build(),
823
+ createRole: endpoint("POST", "/iam/roles").summary("Create a role").tags("IAM", "Roles").security("bearer").permission("fortress", "createRole").body(obj(
824
+ {
825
+ name: str("Role name"),
826
+ permissions: arr(iamRef("PermissionInput"), "Permissions to assign"),
827
+ description: str("Role description")
828
+ },
829
+ "name",
830
+ "permissions"
831
+ )).response(201, "Role created", iamRef("Role")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createRole").build(),
832
+ deleteRole: endpoint("DELETE", "/iam/roles/:id").summary("Delete a role").tags("IAM", "Roles").security("bearer").permission("fortress", "deleteRole").params(obj({ id: id("Role ID") }, "id")).response(200, "Role deleted", obj({ ok: bool() }, "ok")).response(400, "Cannot delete system role", authRef("ErrorResponse")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("deleteRole").build(),
833
+ // ── Role Bindings ──
834
+ bindRoleToUser: endpoint("POST", "/iam/roles/:id/bind/user").summary("Bind role to a user").tags("IAM", "Roles").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
835
+ { userId: id("User ID"), tenantId: str("Tenant ID (optional)") },
836
+ "userId"
837
+ )).response(200, "Role bound to user", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToUser").build(),
838
+ bindRoleToGroup: endpoint("POST", "/iam/roles/:id/bind/group").summary("Bind role to a group").tags("IAM", "Roles").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
839
+ { groupId: id("Group ID"), tenantId: str("Tenant ID (optional)") },
840
+ "groupId"
841
+ )).response(200, "Role bound to group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToGroup").build(),
842
+ unbindRole: endpoint("DELETE", "/iam/roles/:id/bind").summary("Unbind a role").tags("IAM", "Roles").security("bearer").permission("fortress", "unbindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
843
+ {
844
+ subjectType: enums("USER", "GROUP", "SERVICE_ACCOUNT"),
845
+ subjectId: id("Subject ID"),
846
+ tenantId: str("Tenant ID (optional)")
847
+ },
848
+ "subjectType",
849
+ "subjectId"
850
+ )).response(200, "Role unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindRole").build(),
851
+ // ── Groups ──
852
+ createGroup: endpoint("POST", "/iam/groups").summary("Create a group").tags("IAM", "Groups").security("bearer").permission("fortress", "createGroup").body(obj(
853
+ { name: str("Group name"), description: str("Group description") },
854
+ "name"
855
+ )).response(201, "Group created", iamRef("Group")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createGroup").build(),
856
+ addUserToGroup: endpoint("POST", "/iam/groups/:id/users").summary("Add user to group").tags("IAM", "Groups").security("bearer").permission("fortress", "manageGroup").params(obj({ id: id("Group ID") }, "id")).body(obj({ userId: id("User ID") }, "userId")).response(200, "User added to group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("addUserToGroup").build(),
857
+ removeUserFromGroup: endpoint("DELETE", "/iam/groups/:id/users/:userId").summary("Remove user from group").tags("IAM", "Groups").security("bearer").permission("fortress", "manageGroup").params(obj({ id: id("Group ID"), userId: id("User ID") }, "id", "userId")).response(200, "User removed from group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("removeUserFromGroup").build(),
858
+ // ── Permissions ──
859
+ getUserPermissions: endpoint("GET", "/iam/users/:id/permissions").summary("Get user permissions").tags("IAM", "Permissions").security("bearer").permission("fortress", "viewPermissions").params(obj({ id: id("User ID") }, "id")).query(obj({ tenantId: str("Tenant ID (optional)") })).response(200, "User permissions", arr(iamRef("Permission"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getUserPermissions").build(),
860
+ checkPermission: endpoint("POST", "/iam/check").summary("Check if user has permission").tags("IAM", "Permissions").security("bearer").permission("fortress", "viewPermissions").body(obj(
861
+ {
862
+ userId: id("User ID"),
863
+ resource: str("Resource name"),
864
+ action: str("Action name"),
865
+ context: record("Permission context")
866
+ },
867
+ "userId",
868
+ "resource",
869
+ "action"
870
+ )).response(200, "Permission check result", obj({ allowed: bool("Whether permission is granted") }, "allowed")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("checkPermission").build(),
871
+ bindPermissionToUser: endpoint("POST", "/iam/permissions/bind/user").summary("Bind permission directly to a user").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
872
+ {
873
+ userId: id("User ID"),
874
+ permission: iamRef("PermissionInput"),
875
+ tenantId: str("Tenant ID (optional)")
876
+ },
877
+ "userId",
878
+ "permission"
879
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToUser").build(),
880
+ bindPermissionToGroup: endpoint("POST", "/iam/permissions/bind/group").summary("Bind permission directly to a group").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
881
+ {
882
+ groupId: id("Group ID"),
883
+ permission: iamRef("PermissionInput"),
884
+ tenantId: str("Tenant ID (optional)")
885
+ },
886
+ "groupId",
887
+ "permission"
888
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToGroup").build(),
889
+ unbindPermissionFromUser: endpoint("DELETE", "/iam/permissions/bind/user").summary("Unbind permission from a user").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
890
+ {
891
+ userId: id("User ID"),
892
+ permissionId: id("Permission ID"),
893
+ tenantId: str("Tenant ID (optional)")
894
+ },
895
+ "userId",
896
+ "permissionId"
897
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromUser").build(),
898
+ unbindPermissionFromGroup: endpoint("DELETE", "/iam/permissions/bind/group").summary("Unbind permission from a group").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
899
+ {
900
+ groupId: id("Group ID"),
901
+ permissionId: id("Permission ID"),
902
+ tenantId: str("Tenant ID (optional)")
903
+ },
904
+ "groupId",
905
+ "permissionId"
906
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromGroup").build(),
907
+ // ── Service Accounts ──
908
+ createServiceAccount: endpoint("POST", "/iam/service-accounts").summary("Create a service account").description("Create a non-human IAM principal. Service accounts hold roles and direct permissions just like users, but have no sessions, passwords, or group memberships. The `name` is immutable after creation.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "createServiceAccount").body(obj(
909
+ {
910
+ name: str("Machine identifier (immutable)"),
911
+ displayName: str("Human-readable label"),
912
+ description: str("Free-form description")
913
+ },
914
+ "name"
915
+ )).response(201, "Service account created", iamRef("ServiceAccount")).response(400, "Bad request", authRef("ErrorResponse")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createServiceAccount").build(),
916
+ listServiceAccounts: endpoint("GET", "/iam/service-accounts").summary("List service accounts").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "viewServiceAccounts").query(obj({
917
+ limit: int("Page size (default 50)"),
918
+ offset: int("Offset from the start of the list")
919
+ })).response(200, "Service accounts", obj({
920
+ serviceAccounts: arr(iamRef("ServiceAccount")),
921
+ total: int("Total service accounts")
922
+ }, "serviceAccounts", "total")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("listServiceAccounts").build(),
923
+ getServiceAccount: endpoint("GET", "/iam/service-accounts/:id").summary("Get a service account by ID").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "viewServiceAccounts").params(obj({ id: id("Service account ID") }, "id")).response(200, "Service account", iamRef("ServiceAccount")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("getServiceAccount").build(),
924
+ updateServiceAccount: endpoint("PATCH", "/iam/service-accounts/:id").summary("Update a service account").description("Update displayName, description, or isActive. The `name` field is immutable \u2014 to rename, delete and recreate.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "manageServiceAccount").params(obj({ id: id("Service account ID") }, "id")).body(obj({
925
+ displayName: nullable(str("New human-readable label")),
926
+ description: nullable(str("New description")),
927
+ isActive: bool("Whether the account is active")
928
+ })).response(200, "Service account updated", iamRef("ServiceAccount")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("updateServiceAccount").build(),
929
+ deleteServiceAccount: endpoint("DELETE", "/iam/service-accounts/:id").summary("Delete a service account").description("Hard-deletes the service account and cascades to role bindings, direct permission bindings, and (via plugin observer) api keys owned by the account.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "manageServiceAccount").params(obj({ id: id("Service account ID") }, "id")).response(200, "Service account deleted", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("deleteServiceAccount").build(),
930
+ getServiceAccountPermissions: endpoint("GET", "/iam/service-accounts/:id/permissions").summary("Get effective permissions for a service account").tags("IAM", "Service Accounts", "Permissions").security("bearer").permission("fortress", "viewPermissions").params(obj({ id: id("Service account ID") }, "id")).query(obj({ tenantId: str("Tenant ID (optional)") })).response(200, "Service account permissions", arr(iamRef("Permission"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getServiceAccountPermissions").build(),
931
+ bindRoleToServiceAccount: endpoint("POST", "/iam/roles/:id/bind/service-account").summary("Bind role to a service account").tags("IAM", "Roles", "Service Accounts").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
932
+ { serviceAccountId: id("Service account ID"), tenantId: str("Tenant ID (optional)") },
933
+ "serviceAccountId"
934
+ )).response(200, "Role bound to service account", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToServiceAccount").build(),
935
+ unbindRoleFromServiceAccount: endpoint("DELETE", "/iam/roles/:id/bind/service-account").summary("Unbind role from a service account").tags("IAM", "Roles", "Service Accounts").security("bearer").permission("fortress", "unbindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
936
+ { serviceAccountId: id("Service account ID"), tenantId: str("Tenant ID (optional)") },
937
+ "serviceAccountId"
938
+ )).response(200, "Role unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindRoleFromServiceAccount").build(),
939
+ bindPermissionToServiceAccount: endpoint("POST", "/iam/permissions/bind/service-account").summary("Bind permission directly to a service account").tags("IAM", "Permissions", "Service Accounts").security("bearer").permission("fortress", "managePermissions").body(obj(
940
+ {
941
+ serviceAccountId: id("Service account ID"),
942
+ permission: iamRef("PermissionInput"),
943
+ tenantId: str("Tenant ID (optional)")
944
+ },
945
+ "serviceAccountId",
946
+ "permission"
947
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToServiceAccount").build(),
948
+ unbindPermissionFromServiceAccount: endpoint("DELETE", "/iam/permissions/bind/service-account").summary("Unbind permission from a service account").tags("IAM", "Permissions", "Service Accounts").security("bearer").permission("fortress", "managePermissions").body(obj(
949
+ {
950
+ serviceAccountId: id("Service account ID"),
951
+ permissionId: id("Permission ID"),
952
+ tenantId: str("Tenant ID (optional)")
953
+ },
954
+ "serviceAccountId",
955
+ "permissionId"
956
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromServiceAccount").build()
957
+ };
958
+
959
+ // src/core/iam/resource-sync.ts
960
+ async function pullResources(db) {
961
+ const resources = await db.findMany({
962
+ model: "resource",
963
+ sortBy: { field: "name", direction: "asc" }
964
+ });
965
+ const result = { resources: {} };
966
+ for (const resource of resources) {
967
+ const permissions = await db.findMany({
968
+ model: "permission",
969
+ where: [{ field: "resource", operator: "=", value: resource.name }],
970
+ sortBy: { field: "id", direction: "asc" }
971
+ });
972
+ result.resources[resource.name] = {
973
+ actions: permissions.map((p) => p.action),
974
+ ...resource.description ? { description: resource.description } : {}
975
+ };
976
+ }
977
+ return result;
978
+ }
979
+
980
+ // src/plugins/api-key/core.ts
981
+ function toInfo(r) {
982
+ return {
983
+ id: r.id,
984
+ name: r.name,
985
+ keyPrefix: r.keyPrefix,
986
+ scopes: r.scopes ? JSON.parse(r.scopes) : null,
987
+ expiresAt: r.expiresAt,
988
+ lastUsedAt: r.lastUsedAt,
989
+ createdAt: r.createdAt
990
+ };
991
+ }
992
+ function subjectWhere(subject) {
993
+ return [
994
+ { field: "subjectType", operator: "=", value: subject.type },
995
+ { field: "subjectId", operator: "=", value: subject.id }
996
+ ];
997
+ }
998
+ async function listKeysForSubject(db, subject) {
999
+ const records = await db.findMany({
1000
+ model: "api_key",
1001
+ where: [
1002
+ ...subjectWhere(subject),
1003
+ { field: "isRevoked", operator: "=", value: false }
1004
+ ]
1005
+ });
1006
+ return records.map(toInfo);
1007
+ }
1008
+ async function revokeKeyAsAdmin(db, keyId) {
1009
+ const record2 = await db.findOne({
1010
+ model: "api_key",
1011
+ where: [{ field: "id", operator: "=", value: keyId }]
1012
+ });
1013
+ if (!record2)
1014
+ throw Errors.notFound("API key not found");
1015
+ await db.update({
1016
+ model: "api_key",
1017
+ where: [{ field: "id", operator: "=", value: keyId }],
1018
+ data: { isRevoked: true }
1019
+ });
1020
+ }
1021
+
1022
+ // src/plugins/admin/index.ts
1023
+ function collectPermissions(endpoints) {
1024
+ const seen = /* @__PURE__ */ new Set();
1025
+ const result = [];
1026
+ for (const ep of endpoints) {
1027
+ const perm = ep.meta?.permission;
1028
+ if (!perm)
1029
+ continue;
1030
+ const key = `${perm.resource}:${perm.action}`;
1031
+ if (seen.has(key))
1032
+ continue;
1033
+ seen.add(key);
1034
+ result.push(perm);
1035
+ }
1036
+ return result;
1037
+ }
1038
+ var userRef = { $ref: "#/components/schemas/User" };
1039
+ var permissionRef = { $ref: "#/components/schemas/Permission" };
1040
+ var permissionInputRef = { $ref: "#/components/schemas/PermissionInput" };
1041
+ var errorRef = { $ref: "#/components/schemas/ErrorResponse" };
1042
+ function intSchema(desc) {
1043
+ return { type: "integer", description: desc };
1044
+ }
1045
+ function strSchema(desc) {
1046
+ return { type: "string", description: desc };
1047
+ }
1048
+ function boolSchema(desc) {
1049
+ return { type: "boolean", description: desc };
1050
+ }
1051
+ var encoder = new TextEncoder();
1052
+ function timingSafeEqual(a, b) {
1053
+ const left = encoder.encode(a);
1054
+ const right = encoder.encode(b);
1055
+ const len = Math.max(left.length, right.length);
1056
+ let diff = left.length ^ right.length;
1057
+ for (let i = 0; i < len; i++)
1058
+ diff |= (left[i] ?? 0) ^ (right[i] ?? 0);
1059
+ return diff === 0;
1060
+ }
1061
+ function requireId(v, name) {
1062
+ if (v == null || v === "")
1063
+ throw Errors.badRequest(`${name} is required`);
1064
+ if (typeof v === "string")
1065
+ return v;
1066
+ if (typeof v === "number" && Number.isFinite(v))
1067
+ return String(v);
1068
+ throw Errors.badRequest(`${name} must be a string or number`);
1069
+ }
1070
+ var adminAuthEndpoints = [
1071
+ // GET /auth/users — list users (paginated, searchable)
1072
+ {
1073
+ method: "GET",
1074
+ path: "/auth/users",
1075
+ handler: "listUsers",
1076
+ meta: {
1077
+ summary: "List users",
1078
+ description: "List all users with pagination and optional search",
1079
+ tags: ["Auth", "Admin"],
1080
+ security: ["bearer"],
1081
+ permission: { resource: "fortress", action: "viewUsers" }
1082
+ },
1083
+ input: {
1084
+ query: {
1085
+ type: "object",
1086
+ properties: {
1087
+ limit: intSchema("Max results (default 50)"),
1088
+ offset: intSchema("Skip results (default 0)"),
1089
+ search: strSchema("Search by email"),
1090
+ sortBy: strSchema("Sort field (default: id)"),
1091
+ sortDirection: { type: "string", enum: ["asc", "desc"], description: "Sort direction" }
1092
+ }
1093
+ }
1094
+ },
1095
+ responses: {
1096
+ 200: {
1097
+ description: "User list",
1098
+ schema: {
1099
+ type: "object",
1100
+ properties: {
1101
+ users: { type: "array", items: userRef, description: "List of users" },
1102
+ total: intSchema("Total number of users")
1103
+ },
1104
+ required: ["users", "total"]
1105
+ }
1106
+ },
1107
+ 401: { description: "Not authenticated" },
1108
+ 403: { description: "Insufficient permissions" }
1109
+ }
1110
+ },
1111
+ // GET /auth/users/:id — get user by ID
1112
+ {
1113
+ method: "GET",
1114
+ path: "/auth/users/:id",
1115
+ handler: "getUserById",
1116
+ meta: {
1117
+ summary: "Get user by ID",
1118
+ tags: ["Auth", "Admin"],
1119
+ security: ["bearer"],
1120
+ permission: { resource: "fortress", action: "viewUsers" }
1121
+ },
1122
+ input: {
1123
+ params: {
1124
+ type: "object",
1125
+ properties: { id: intSchema("User ID") },
1126
+ required: ["id"]
1127
+ }
1128
+ },
1129
+ responses: {
1130
+ 200: { description: "User details", schema: userRef },
1131
+ 401: { description: "Not authenticated" },
1132
+ 403: { description: "Insufficient permissions" },
1133
+ 404: { description: "User not found", schema: errorRef }
1134
+ }
1135
+ },
1136
+ // PUT /auth/users/:id — update user
1137
+ {
1138
+ method: "PUT",
1139
+ path: "/auth/users/:id",
1140
+ handler: "updateUser",
1141
+ meta: {
1142
+ summary: "Update user",
1143
+ tags: ["Auth", "Admin"],
1144
+ security: ["bearer"],
1145
+ permission: { resource: "fortress", action: "manageUsers" }
1146
+ },
1147
+ input: {
1148
+ params: {
1149
+ type: "object",
1150
+ properties: { id: intSchema("User ID") },
1151
+ required: ["id"]
1152
+ },
1153
+ body: {
1154
+ type: "object",
1155
+ properties: {
1156
+ name: strSchema("Display name"),
1157
+ email: { type: "string", format: "email", description: "User email" },
1158
+ isActive: boolSchema("Account active status"),
1159
+ password: strSchema("New password (will be hashed)")
1160
+ }
1161
+ }
1162
+ },
1163
+ responses: {
1164
+ 200: { description: "Updated user", schema: userRef },
1165
+ 401: { description: "Not authenticated" },
1166
+ 403: { description: "Insufficient permissions" },
1167
+ 404: { description: "User not found", schema: errorRef },
1168
+ 409: { description: "Email already in use", schema: errorRef }
1169
+ }
1170
+ },
1171
+ // DELETE /auth/users/:id — delete user
1172
+ {
1173
+ method: "DELETE",
1174
+ path: "/auth/users/:id",
1175
+ handler: "deleteUser",
1176
+ meta: {
1177
+ summary: "Delete user",
1178
+ tags: ["Auth", "Admin"],
1179
+ security: ["bearer"],
1180
+ permission: { resource: "fortress", action: "manageUsers" }
1181
+ },
1182
+ input: {
1183
+ params: {
1184
+ type: "object",
1185
+ properties: { id: intSchema("User ID") },
1186
+ required: ["id"]
1187
+ }
1188
+ },
1189
+ responses: {
1190
+ 200: { description: "User deleted", schema: { type: "object", properties: { ok: boolSchema("Success") } } },
1191
+ 401: { description: "Not authenticated" },
1192
+ 403: { description: "Insufficient permissions" },
1193
+ 404: { description: "User not found", schema: errorRef }
1194
+ }
1195
+ },
1196
+ // POST /auth/users — admin-initiated user creation
1197
+ {
1198
+ method: "POST",
1199
+ path: "/auth/users",
1200
+ handler: "createUser",
1201
+ meta: {
1202
+ summary: "Create a user (admin)",
1203
+ description: "Admin-initiated user creation. Unlike /auth/register, requires authentication and manageUsers permission.",
1204
+ tags: ["Auth", "Admin"],
1205
+ security: ["bearer"],
1206
+ permission: { resource: "fortress", action: "manageUsers" }
1207
+ },
1208
+ input: {
1209
+ body: {
1210
+ type: "object",
1211
+ properties: {
1212
+ email: { type: "string", format: "email", description: "User email" },
1213
+ name: strSchema("Display name"),
1214
+ password: strSchema("User password"),
1215
+ isActive: boolSchema("Set active status (default true)")
1216
+ },
1217
+ required: ["email", "name"]
1218
+ }
1219
+ },
1220
+ responses: {
1221
+ 201: { description: "User created", schema: userRef },
1222
+ 401: { description: "Not authenticated" },
1223
+ 403: { description: "Insufficient permissions" },
1224
+ 409: { description: "Email already exists", schema: errorRef }
1225
+ }
1226
+ }
1227
+ ];
1228
+ var adminIamEndpoints = [
1229
+ // GET /iam/roles/:id — get role with permissions
1230
+ {
1231
+ method: "GET",
1232
+ path: "/iam/roles/:id",
1233
+ handler: "getRole",
1234
+ meta: {
1235
+ summary: "Get role with permissions",
1236
+ tags: ["IAM", "Admin"],
1237
+ security: ["bearer"],
1238
+ permission: { resource: "fortress", action: "viewRoles" }
1239
+ },
1240
+ input: {
1241
+ params: {
1242
+ type: "object",
1243
+ properties: { id: intSchema("Role ID") },
1244
+ required: ["id"]
1245
+ }
1246
+ },
1247
+ responses: {
1248
+ 200: {
1249
+ description: "Role with permissions",
1250
+ schema: {
1251
+ type: "object",
1252
+ properties: {
1253
+ id: intSchema("Role ID"),
1254
+ name: strSchema("Role name"),
1255
+ description: { type: "string", nullable: true, description: "Role description" },
1256
+ isSystem: boolSchema("Whether this is a system role"),
1257
+ permissions: { type: "array", items: permissionRef, description: "Role permissions" }
1258
+ },
1259
+ required: ["id", "name", "permissions"]
1260
+ }
1261
+ },
1262
+ 404: { description: "Role not found", schema: errorRef }
1263
+ }
1264
+ },
1265
+ // PUT /iam/roles/:id — update role
1266
+ {
1267
+ method: "PUT",
1268
+ path: "/iam/roles/:id",
1269
+ handler: "updateRole",
1270
+ meta: {
1271
+ summary: "Update role",
1272
+ tags: ["IAM", "Admin"],
1273
+ security: ["bearer"],
1274
+ permission: { resource: "fortress", action: "manageRoles" }
1275
+ },
1276
+ input: {
1277
+ params: {
1278
+ type: "object",
1279
+ properties: { id: intSchema("Role ID") },
1280
+ required: ["id"]
1281
+ },
1282
+ body: {
1283
+ type: "object",
1284
+ properties: {
1285
+ name: strSchema("Role name"),
1286
+ description: strSchema("Role description")
1287
+ }
1288
+ }
1289
+ },
1290
+ responses: {
1291
+ 200: { description: "Updated role", schema: { $ref: "#/components/schemas/Role" } },
1292
+ 400: { description: "Cannot update system role", schema: errorRef },
1293
+ 404: { description: "Role not found", schema: errorRef }
1294
+ }
1295
+ },
1296
+ // GET /iam/groups — list groups (paginated)
1297
+ {
1298
+ method: "GET",
1299
+ path: "/iam/groups",
1300
+ handler: "listGroups",
1301
+ meta: {
1302
+ summary: "List groups",
1303
+ tags: ["IAM", "Admin"],
1304
+ security: ["bearer"],
1305
+ permission: { resource: "fortress", action: "viewGroups" }
1306
+ },
1307
+ input: {
1308
+ query: {
1309
+ type: "object",
1310
+ properties: {
1311
+ limit: intSchema("Max results (default 50)"),
1312
+ offset: intSchema("Skip results (default 0)")
1313
+ }
1314
+ }
1315
+ },
1316
+ responses: {
1317
+ 200: {
1318
+ description: "Group list",
1319
+ schema: {
1320
+ type: "object",
1321
+ properties: {
1322
+ groups: { type: "array", items: { $ref: "#/components/schemas/Group" }, description: "List of groups" },
1323
+ total: intSchema("Total number of groups")
1324
+ },
1325
+ required: ["groups", "total"]
1326
+ }
1327
+ }
1328
+ }
1329
+ },
1330
+ // GET /iam/groups/:id — get group with members
1331
+ {
1332
+ method: "GET",
1333
+ path: "/iam/groups/:id",
1334
+ handler: "getGroup",
1335
+ meta: {
1336
+ summary: "Get group with members",
1337
+ tags: ["IAM", "Admin"],
1338
+ security: ["bearer"],
1339
+ permission: { resource: "fortress", action: "viewGroups" }
1340
+ },
1341
+ input: {
1342
+ params: {
1343
+ type: "object",
1344
+ properties: { id: intSchema("Group ID") },
1345
+ required: ["id"]
1346
+ }
1347
+ },
1348
+ responses: {
1349
+ 200: {
1350
+ description: "Group with members",
1351
+ schema: {
1352
+ type: "object",
1353
+ properties: {
1354
+ id: intSchema("Group ID"),
1355
+ name: strSchema("Group name"),
1356
+ description: { type: "string", nullable: true, description: "Group description" },
1357
+ users: { type: "array", items: userRef, description: "Group members" }
1358
+ },
1359
+ required: ["id", "name", "users"]
1360
+ }
1361
+ },
1362
+ 404: { description: "Group not found", schema: errorRef }
1363
+ }
1364
+ },
1365
+ // PUT /iam/groups/:id — update group
1366
+ {
1367
+ method: "PUT",
1368
+ path: "/iam/groups/:id",
1369
+ handler: "updateGroup",
1370
+ meta: {
1371
+ summary: "Update group",
1372
+ tags: ["IAM", "Admin"],
1373
+ security: ["bearer"],
1374
+ permission: { resource: "fortress", action: "manageGroup" }
1375
+ },
1376
+ input: {
1377
+ params: {
1378
+ type: "object",
1379
+ properties: { id: intSchema("Group ID") },
1380
+ required: ["id"]
1381
+ },
1382
+ body: {
1383
+ type: "object",
1384
+ properties: {
1385
+ name: strSchema("Group name"),
1386
+ description: strSchema("Group description")
1387
+ }
1388
+ }
1389
+ },
1390
+ responses: {
1391
+ 200: { description: "Updated group", schema: { $ref: "#/components/schemas/Group" } },
1392
+ 404: { description: "Group not found", schema: errorRef }
1393
+ }
1394
+ },
1395
+ // DELETE /iam/groups/:id — delete group
1396
+ {
1397
+ method: "DELETE",
1398
+ path: "/iam/groups/:id",
1399
+ handler: "deleteGroup",
1400
+ meta: {
1401
+ summary: "Delete group",
1402
+ tags: ["IAM", "Admin"],
1403
+ security: ["bearer"],
1404
+ permission: { resource: "fortress", action: "manageGroup" }
1405
+ },
1406
+ input: {
1407
+ params: {
1408
+ type: "object",
1409
+ properties: { id: intSchema("Group ID") },
1410
+ required: ["id"]
1411
+ }
1412
+ },
1413
+ responses: {
1414
+ 200: { description: "Group deleted", schema: { type: "object", properties: { ok: boolSchema("Success") } } },
1415
+ 404: { description: "Group not found", schema: errorRef }
1416
+ }
1417
+ },
1418
+ // GET /iam/groups/:id/users — list group members
1419
+ {
1420
+ method: "GET",
1421
+ path: "/iam/groups/:id/users",
1422
+ handler: "getGroupUsers",
1423
+ meta: {
1424
+ summary: "List group members",
1425
+ tags: ["IAM", "Admin"],
1426
+ security: ["bearer"],
1427
+ permission: { resource: "fortress", action: "viewGroups" }
1428
+ },
1429
+ input: {
1430
+ params: {
1431
+ type: "object",
1432
+ properties: { id: intSchema("Group ID") },
1433
+ required: ["id"]
1434
+ }
1435
+ },
1436
+ responses: {
1437
+ 200: {
1438
+ description: "Group members",
1439
+ schema: { type: "array", items: userRef }
1440
+ }
1441
+ }
1442
+ },
1443
+ // GET /iam/permissions — list all permissions
1444
+ {
1445
+ method: "GET",
1446
+ path: "/iam/permissions",
1447
+ handler: "listPermissions",
1448
+ meta: {
1449
+ summary: "List permissions",
1450
+ tags: ["IAM", "Admin"],
1451
+ security: ["bearer"],
1452
+ permission: { resource: "fortress", action: "viewPermissions" }
1453
+ },
1454
+ input: {
1455
+ query: {
1456
+ type: "object",
1457
+ properties: {
1458
+ resource: strSchema("Filter by resource name")
1459
+ }
1460
+ }
1461
+ },
1462
+ responses: {
1463
+ 200: {
1464
+ description: "Permission list",
1465
+ schema: { type: "array", items: permissionRef }
1466
+ }
1467
+ }
1468
+ },
1469
+ // POST /iam/permissions — create permission
1470
+ {
1471
+ method: "POST",
1472
+ path: "/iam/permissions",
1473
+ handler: "createPermission",
1474
+ meta: {
1475
+ summary: "Create permission",
1476
+ tags: ["IAM", "Admin"],
1477
+ security: ["bearer"],
1478
+ permission: { resource: "fortress", action: "managePermissions" }
1479
+ },
1480
+ input: {
1481
+ body: permissionInputRef
1482
+ },
1483
+ responses: {
1484
+ 200: { description: "Created permission", schema: permissionRef }
1485
+ }
1486
+ },
1487
+ // DELETE /iam/permissions/:id — delete permission
1488
+ {
1489
+ method: "DELETE",
1490
+ path: "/iam/permissions/:id",
1491
+ handler: "deletePermission",
1492
+ meta: {
1493
+ summary: "Delete permission",
1494
+ tags: ["IAM", "Admin"],
1495
+ security: ["bearer"],
1496
+ permission: { resource: "fortress", action: "managePermissions" }
1497
+ },
1498
+ input: {
1499
+ params: {
1500
+ type: "object",
1501
+ properties: { id: intSchema("Permission ID") },
1502
+ required: ["id"]
1503
+ }
1504
+ },
1505
+ responses: {
1506
+ 200: { description: "Permission deleted", schema: { type: "object", properties: { ok: boolSchema("Success") } } },
1507
+ 404: { description: "Permission not found", schema: errorRef }
1508
+ }
1509
+ },
1510
+ // POST /iam/roles/:id/permissions — add permission to role
1511
+ {
1512
+ method: "POST",
1513
+ path: "/iam/roles/:id/permissions",
1514
+ handler: "addPermissionToRole",
1515
+ meta: {
1516
+ summary: "Add permission to role",
1517
+ tags: ["IAM", "Admin"],
1518
+ security: ["bearer"],
1519
+ permission: { resource: "fortress", action: "manageRoles" }
1520
+ },
1521
+ input: {
1522
+ params: {
1523
+ type: "object",
1524
+ properties: { id: intSchema("Role ID") },
1525
+ required: ["id"]
1526
+ },
1527
+ body: permissionInputRef
1528
+ },
1529
+ responses: {
1530
+ 200: { description: "Permission added", schema: { type: "object", properties: { ok: boolSchema("Success") } } },
1531
+ 404: { description: "Role not found", schema: errorRef }
1532
+ }
1533
+ }
1534
+ ];
1535
+ var syncEndpoint = {
1536
+ method: "POST",
1537
+ path: "/iam/sync",
1538
+ handler: "syncResources",
1539
+ meta: {
1540
+ summary: "Sync IAM resources",
1541
+ description: "Push or pull resource definitions to/from the resource file",
1542
+ tags: ["IAM", "Admin"],
1543
+ security: ["bearer"],
1544
+ permission: { resource: "fortress", action: "managePermissions" }
1545
+ },
1546
+ input: {
1547
+ body: {
1548
+ type: "object",
1549
+ properties: {
1550
+ direction: { type: "string", enum: ["push", "pull"], description: "Sync direction" },
1551
+ filePath: strSchema("Resource file path (optional)")
1552
+ },
1553
+ required: ["direction"]
1554
+ }
1555
+ },
1556
+ responses: {
1557
+ 200: { description: "Sync complete", schema: { type: "object", properties: { ok: boolSchema("Success") } } },
1558
+ 401: { description: "Not authenticated" },
1559
+ 403: { description: "Insufficient permissions" }
1560
+ }
1561
+ };
1562
+ var bootstrapEndpoint = {
1563
+ method: "POST",
1564
+ path: "/iam/admin/bootstrap",
1565
+ handler: "bootstrap",
1566
+ meta: {
1567
+ summary: "Bootstrap admin user",
1568
+ description: "Opt-in emergency bootstrap. Assigns all fortress admin permissions only while zero fortress-admin bindings exist and only when the caller presents the one-time bootstrap secret. Creates the fortress resource and permissions if they do not exist.",
1569
+ tags: ["IAM", "Admin"],
1570
+ security: ["bearer"]
1571
+ },
1572
+ input: {
1573
+ body: {
1574
+ type: "object",
1575
+ properties: {
1576
+ userId: { type: "integer", description: "Target user ID. Defaults to the authenticated caller." },
1577
+ secret: { type: "string", description: "One-time bootstrap secret" }
1578
+ }
1579
+ }
1580
+ },
1581
+ responses: {
1582
+ 200: { description: "Admin bootstrapped", schema: { type: "object", properties: { ok: { type: "boolean" }, role: { type: "object" } } } },
1583
+ 401: { description: "Not authenticated" },
1584
+ 403: { description: "Bootstrap disabled, bad secret, or already bootstrapped" }
1585
+ }
1586
+ };
1587
+ var apiKeyInfoSchema = {
1588
+ type: "object",
1589
+ properties: {
1590
+ id: intSchema("Database id"),
1591
+ name: strSchema("Key label"),
1592
+ keyPrefix: strSchema("First 12 characters of the key, for identification"),
1593
+ scopes: { type: "array", items: { type: "string" }, nullable: true },
1594
+ expiresAt: { type: "string", format: "date-time", nullable: true },
1595
+ lastUsedAt: { type: "string", format: "date-time", nullable: true },
1596
+ createdAt: { type: "string", format: "date-time" }
1597
+ },
1598
+ required: ["id", "name", "keyPrefix", "createdAt"]
1599
+ };
1600
+ var createKeyBodySchema = {
1601
+ type: "object",
1602
+ properties: {
1603
+ name: strSchema("Human-readable key label"),
1604
+ scopes: { type: "array", items: { type: "string" }, description: "Optional permission scopes attached to the key" },
1605
+ expiresAt: { type: "string", description: "Optional expiry (ISO 8601 string)" }
1606
+ },
1607
+ required: ["name"]
1608
+ };
1609
+ var createKeyResponseSchema = {
1610
+ type: "object",
1611
+ properties: {
1612
+ key: strSchema("Raw API key \u2014 shown exactly once, store it immediately"),
1613
+ id: intSchema("Database id of the key")
1614
+ },
1615
+ required: ["key", "id"]
1616
+ };
1617
+ var adminApiKeyEndpoints = [
1618
+ // ── User-scoped admin api-key management ──────────────────────
1619
+ {
1620
+ method: "POST",
1621
+ path: "/admin/users/:userId/api-keys",
1622
+ handler: "adminCreateUserApiKey",
1623
+ meta: {
1624
+ summary: "Mint an API key for any user",
1625
+ description: "Create a new API key owned by any user, bypassing the self-service `maxKeysPerSubject` check is NOT done \u2014 the same limit applies. The raw key is returned exactly once and cannot be retrieved later. Requires the `apiKey:manage` permission. Typical use: provisioning keys during tenant onboarding or replacing a lost key on a user's behalf.",
1626
+ tags: ["Admin", "API Keys"],
1627
+ security: ["bearer"],
1628
+ permission: { resource: "apiKey", action: "manage" }
1629
+ },
1630
+ input: {
1631
+ params: {
1632
+ type: "object",
1633
+ properties: { userId: intSchema("Target user ID") },
1634
+ required: ["userId"]
1635
+ },
1636
+ body: createKeyBodySchema
1637
+ },
1638
+ responses: {
1639
+ 201: { description: "Key created", schema: createKeyResponseSchema },
1640
+ 400: { description: "Bad request", schema: errorRef },
1641
+ 401: { description: "Not authenticated", schema: errorRef },
1642
+ 403: { description: "Forbidden", schema: errorRef },
1643
+ 404: { description: "User not found", schema: errorRef }
1644
+ }
1645
+ },
1646
+ {
1647
+ method: "GET",
1648
+ path: "/admin/users/:userId/api-keys",
1649
+ handler: "adminListUserApiKeys",
1650
+ meta: {
1651
+ summary: "List a user's API keys",
1652
+ description: "Return the active (non-revoked) API keys belonging to any user. Raw keys and hashes are never returned. Requires the `apiKey:manage` permission.",
1653
+ tags: ["Admin", "API Keys"],
1654
+ security: ["bearer"],
1655
+ permission: { resource: "apiKey", action: "manage" }
1656
+ },
1657
+ input: {
1658
+ params: {
1659
+ type: "object",
1660
+ properties: { userId: intSchema("Target user ID") },
1661
+ required: ["userId"]
1662
+ }
1663
+ },
1664
+ responses: {
1665
+ 200: {
1666
+ description: "Keys",
1667
+ schema: {
1668
+ type: "object",
1669
+ properties: {
1670
+ keys: { type: "array", items: apiKeyInfoSchema }
1671
+ },
1672
+ required: ["keys"]
1673
+ }
1674
+ },
1675
+ 401: { description: "Not authenticated", schema: errorRef },
1676
+ 403: { description: "Forbidden", schema: errorRef }
1677
+ }
1678
+ },
1679
+ {
1680
+ method: "DELETE",
1681
+ path: "/admin/users/:userId/api-keys/:id",
1682
+ handler: "adminRevokeUserApiKey",
1683
+ meta: {
1684
+ summary: "Revoke a user's API key",
1685
+ description: "Revoke any API key by id, bypassing the self-service ownership check. Requires the `apiKey:manage` permission. Typically used to respond to leaked keys or compromised accounts.",
1686
+ tags: ["Admin", "API Keys"],
1687
+ security: ["bearer"],
1688
+ permission: { resource: "apiKey", action: "manage" }
1689
+ },
1690
+ input: {
1691
+ params: {
1692
+ type: "object",
1693
+ properties: {
1694
+ userId: intSchema("Target user ID (for URL namespacing; ownership is not enforced)"),
1695
+ id: intSchema("Key id to revoke")
1696
+ },
1697
+ required: ["userId", "id"]
1698
+ }
1699
+ },
1700
+ responses: {
1701
+ 200: {
1702
+ description: "Revoked",
1703
+ schema: {
1704
+ type: "object",
1705
+ properties: { ok: boolSchema("Success") },
1706
+ required: ["ok"]
1707
+ }
1708
+ },
1709
+ 401: { description: "Not authenticated", schema: errorRef },
1710
+ 403: { description: "Forbidden", schema: errorRef },
1711
+ 404: { description: "Not found", schema: errorRef }
1712
+ }
1713
+ },
1714
+ // ── Service-account-scoped admin api-key management ───────────
1715
+ //
1716
+ // Service accounts have no login path — they can't self-mint their
1717
+ // first key. These endpoints are the only supported way to bootstrap
1718
+ // a service account's credentials.
1719
+ {
1720
+ method: "POST",
1721
+ path: "/admin/service-accounts/:id/api-keys",
1722
+ handler: "adminCreateServiceAccountApiKey",
1723
+ meta: {
1724
+ summary: "Mint an API key for a service account",
1725
+ description: "Create a new API key owned by a service account. This is the primary entry point for bootstrapping a service account's credentials \u2014 a fresh service account has no way to authenticate until an admin mints its first key. The raw key is returned exactly once and cannot be retrieved later. Requires the `apiKey:manage` permission.",
1726
+ tags: ["Admin", "API Keys", "Service Accounts"],
1727
+ security: ["bearer"],
1728
+ permission: { resource: "apiKey", action: "manage" }
1729
+ },
1730
+ input: {
1731
+ params: {
1732
+ type: "object",
1733
+ properties: { id: intSchema("Target service account ID") },
1734
+ required: ["id"]
1735
+ },
1736
+ body: createKeyBodySchema
1737
+ },
1738
+ responses: {
1739
+ 201: { description: "Key created", schema: createKeyResponseSchema },
1740
+ 400: { description: "Bad request", schema: errorRef },
1741
+ 401: { description: "Not authenticated", schema: errorRef },
1742
+ 403: { description: "Forbidden", schema: errorRef },
1743
+ 404: { description: "Service account not found", schema: errorRef }
1744
+ }
1745
+ },
1746
+ {
1747
+ method: "GET",
1748
+ path: "/admin/service-accounts/:id/api-keys",
1749
+ handler: "adminListServiceAccountApiKeys",
1750
+ meta: {
1751
+ summary: "List a service account's API keys",
1752
+ description: "Return the active (non-revoked) API keys owned by a service account. Raw keys and hashes are never returned. Requires the `apiKey:manage` permission.",
1753
+ tags: ["Admin", "API Keys", "Service Accounts"],
1754
+ security: ["bearer"],
1755
+ permission: { resource: "apiKey", action: "manage" }
1756
+ },
1757
+ input: {
1758
+ params: {
1759
+ type: "object",
1760
+ properties: { id: intSchema("Target service account ID") },
1761
+ required: ["id"]
1762
+ }
1763
+ },
1764
+ responses: {
1765
+ 200: {
1766
+ description: "Keys",
1767
+ schema: {
1768
+ type: "object",
1769
+ properties: {
1770
+ keys: { type: "array", items: apiKeyInfoSchema }
1771
+ },
1772
+ required: ["keys"]
1773
+ }
1774
+ },
1775
+ 401: { description: "Not authenticated", schema: errorRef },
1776
+ 403: { description: "Forbidden", schema: errorRef }
1777
+ }
1778
+ },
1779
+ {
1780
+ method: "DELETE",
1781
+ path: "/admin/service-accounts/:id/api-keys/:keyId",
1782
+ handler: "adminRevokeServiceAccountApiKey",
1783
+ meta: {
1784
+ summary: "Revoke a service account's API key",
1785
+ description: "Revoke any API key by id, bypassing the self-service ownership check. Requires the `apiKey:manage` permission.",
1786
+ tags: ["Admin", "API Keys", "Service Accounts"],
1787
+ security: ["bearer"],
1788
+ permission: { resource: "apiKey", action: "manage" }
1789
+ },
1790
+ input: {
1791
+ params: {
1792
+ type: "object",
1793
+ properties: {
1794
+ id: intSchema("Target service account ID (for URL namespacing; ownership is not enforced)"),
1795
+ keyId: intSchema("Key id to revoke")
1796
+ },
1797
+ required: ["id", "keyId"]
1798
+ }
1799
+ },
1800
+ responses: {
1801
+ 200: {
1802
+ description: "Revoked",
1803
+ schema: {
1804
+ type: "object",
1805
+ properties: { ok: boolSchema("Success") },
1806
+ required: ["ok"]
1807
+ }
1808
+ },
1809
+ 401: { description: "Not authenticated", schema: errorRef },
1810
+ 403: { description: "Forbidden", schema: errorRef },
1811
+ 404: { description: "Not found", schema: errorRef }
1812
+ }
1813
+ }
1814
+ ];
1815
+ function admin(options = {}) {
1816
+ const mountApiKeyRoutes = options.apiKeyRoutes === true;
1817
+ const mountBootstrap = options.bootstrap?.enabled === true;
1818
+ const bootstrapSecret = options.bootstrap?.secret ?? process.env.FORTRESS_ADMIN_BOOTSTRAP_SECRET;
1819
+ return {
1820
+ name: "admin",
1821
+ // Admin routes are aggregated from several internal arrays into a
1822
+ // record keyed by `${method}_${path}` so collisions with core handler
1823
+ // names (e.g. `createUser` exists in both auth-endpoints.ts and admin)
1824
+ // are impossible. Admin routes exist for default-deny protection and
1825
+ // aren't exposed via the typed `fortress.call.*` surface.
1826
+ routes: Object.fromEntries(
1827
+ [
1828
+ ...mountBootstrap ? [bootstrapEndpoint] : [],
1829
+ syncEndpoint,
1830
+ ...adminAuthEndpoints,
1831
+ ...adminIamEndpoints,
1832
+ ...Object.values(iamEndpoints),
1833
+ ...mountApiKeyRoutes ? adminApiKeyEndpoints : []
1834
+ ].map((ep) => [`${ep.method}_${ep.path}`, ep])
1835
+ ),
1836
+ methods: (ctx) => ({
1837
+ async getResources() {
1838
+ return pullResources(ctx.db);
1839
+ },
1840
+ async bootstrap(body, routeCtx) {
1841
+ if (!mountBootstrap)
1842
+ throw Errors.notFound("Admin bootstrap route is disabled");
1843
+ if (!bootstrapSecret)
1844
+ throw Errors.forbidden("Admin bootstrap secret is not configured");
1845
+ if (!body.secret || !timingSafeEqual(body.secret, bootstrapSecret))
1846
+ throw Errors.forbidden("Invalid admin bootstrap secret");
1847
+ const callerId = routeCtx?.userId;
1848
+ const userId = body.userId ?? callerId;
1849
+ if (userId == null)
1850
+ throw Errors.unauthorized("User not authenticated");
1851
+ const db = ctx.db;
1852
+ return db.transaction(async (tx) => {
1853
+ if (tx.dialect === "pg" && tx.rawQuery)
1854
+ await tx.rawQuery("SELECT pg_advisory_xact_lock(hashtext(?))", ["fortress-admin-bootstrap"]);
1855
+ const user = await tx.findOne({
1856
+ model: "user",
1857
+ where: [{ field: "id", operator: "=", value: userId }]
1858
+ });
1859
+ if (!user) {
1860
+ throw Errors.notFound("User not found");
1861
+ }
1862
+ const existingRole = await tx.findOne({
1863
+ model: "role",
1864
+ where: [{ field: "name", operator: "=", value: "fortress-admin" }]
1865
+ });
1866
+ if (existingRole) {
1867
+ const existingAdmins = await tx.count({
1868
+ model: "role_binding",
1869
+ where: [{ field: "roleId", operator: "=", value: existingRole.id }]
1870
+ });
1871
+ if (existingAdmins > 0)
1872
+ throw Errors.forbidden("Admin already bootstrapped");
1873
+ }
1874
+ const plugins = ctx.config.plugins ?? [];
1875
+ const pluginEndpoints = [];
1876
+ for (const plugin of plugins) {
1877
+ if (plugin.routes)
1878
+ pluginEndpoints.push(...Object.values(plugin.routes));
1879
+ }
1880
+ const allEndpoints = [
1881
+ ...Object.values(authEndpoints),
1882
+ ...Object.values(iamEndpoints),
1883
+ ...pluginEndpoints
1884
+ ];
1885
+ const declaredPermissions = collectPermissions(allEndpoints);
1886
+ const resources = new Set(declaredPermissions.map((p) => p.resource));
1887
+ for (const resource of resources) {
1888
+ const existing = await tx.findOne({
1889
+ model: "resource",
1890
+ where: [{ field: "name", operator: "=", value: resource }]
1891
+ });
1892
+ if (!existing) {
1893
+ await tx.create({ model: "resource", data: { name: resource, description: `Auto-registered by admin plugin` } });
1894
+ }
1895
+ }
1896
+ const permissionIds = [];
1897
+ for (const perm of declaredPermissions) {
1898
+ let existing = await tx.findOne({
1899
+ model: "permission",
1900
+ where: [
1901
+ { field: "resource", operator: "=", value: perm.resource },
1902
+ { field: "action", operator: "=", value: perm.action }
1903
+ ]
1904
+ });
1905
+ if (!existing) {
1906
+ existing = await tx.create({
1907
+ model: "permission",
1908
+ data: {
1909
+ resource: perm.resource,
1910
+ action: perm.action,
1911
+ effect: "ALLOW",
1912
+ description: `${perm.action} ${perm.resource}`
1913
+ }
1914
+ });
1915
+ }
1916
+ permissionIds.push(existing.id);
1917
+ }
1918
+ let adminRole = existingRole;
1919
+ if (!adminRole) {
1920
+ adminRole = await tx.create({
1921
+ model: "role",
1922
+ data: { name: "fortress-admin", description: "Full fortress administration", isSystem: true }
1923
+ });
1924
+ }
1925
+ for (const permId of permissionIds) {
1926
+ const existingLink = await tx.findOne({
1927
+ model: "role_permission",
1928
+ where: [
1929
+ { field: "roleId", operator: "=", value: adminRole.id },
1930
+ { field: "permissionId", operator: "=", value: permId }
1931
+ ]
1932
+ });
1933
+ if (!existingLink) {
1934
+ await tx.create({ model: "role_permission", data: { roleId: adminRole.id, permissionId: permId } });
1935
+ }
1936
+ }
1937
+ const existingBinding = await tx.findOne({
1938
+ model: "role_binding",
1939
+ where: [
1940
+ { field: "roleId", operator: "=", value: adminRole.id },
1941
+ { field: "subjectType", operator: "=", value: "USER" },
1942
+ { field: "subjectId", operator: "=", value: userId }
1943
+ ]
1944
+ });
1945
+ if (!existingBinding) {
1946
+ await tx.create({
1947
+ model: "role_binding",
1948
+ data: { roleId: adminRole.id, subjectType: "USER", subjectId: userId, tenantId: null }
1949
+ });
1950
+ }
1951
+ return { ok: true, role: adminRole };
1952
+ });
1953
+ },
1954
+ // ── Auth admin — delegates to core auth service ──────────
1955
+ async listUsers(body) {
1956
+ if (!ctx.auth)
1957
+ throw Errors.database("Auth service not available");
1958
+ return ctx.auth.listUsers({
1959
+ limit: body.limit ? Number(body.limit) : void 0,
1960
+ offset: body.offset ? Number(body.offset) : void 0,
1961
+ search: body.search,
1962
+ sortBy: body.sortBy,
1963
+ sortDirection: body.sortDirection
1964
+ });
1965
+ },
1966
+ async getUserById(body) {
1967
+ if (!ctx.auth)
1968
+ throw Errors.database("Auth service not available");
1969
+ return ctx.auth.getUserById(requireId(body.id, "id"));
1970
+ },
1971
+ async createUser(body) {
1972
+ if (!ctx.auth)
1973
+ throw Errors.database("Auth service not available");
1974
+ return ctx.auth.createUser({
1975
+ email: body.email,
1976
+ name: body.name,
1977
+ password: body.password,
1978
+ isActive: body.isActive
1979
+ });
1980
+ },
1981
+ async updateUser(body) {
1982
+ if (!ctx.auth)
1983
+ throw Errors.database("Auth service not available");
1984
+ const { id: id2, ...data } = body;
1985
+ return ctx.auth.updateUser(requireId(id2, "id"), data);
1986
+ },
1987
+ async deleteUser(body) {
1988
+ if (!ctx.auth)
1989
+ throw Errors.database("Auth service not available");
1990
+ await ctx.auth.deleteUser(requireId(body.id, "id"));
1991
+ return { ok: true };
1992
+ },
1993
+ // ── IAM admin — delegates to core IAM service ────────────
1994
+ async getRole(body) {
1995
+ if (!ctx.iam)
1996
+ throw Errors.database("IAM service not available");
1997
+ return ctx.iam.getRole(requireId(body.id, "id"));
1998
+ },
1999
+ async updateRole(body) {
2000
+ if (!ctx.iam)
2001
+ throw Errors.database("IAM service not available");
2002
+ const { id: id2, ...data } = body;
2003
+ return ctx.iam.updateRole(requireId(id2, "id"), data);
2004
+ },
2005
+ async listGroups(body) {
2006
+ if (!ctx.iam)
2007
+ throw Errors.database("IAM service not available");
2008
+ return ctx.iam.listGroups({
2009
+ limit: body.limit ? Number(body.limit) : void 0,
2010
+ offset: body.offset ? Number(body.offset) : void 0
2011
+ });
2012
+ },
2013
+ async getGroup(body) {
2014
+ if (!ctx.iam)
2015
+ throw Errors.database("IAM service not available");
2016
+ return ctx.iam.getGroup(requireId(body.id, "id"));
2017
+ },
2018
+ async updateGroup(body) {
2019
+ if (!ctx.iam)
2020
+ throw Errors.database("IAM service not available");
2021
+ const { id: id2, ...data } = body;
2022
+ return ctx.iam.updateGroup(requireId(id2, "id"), data);
2023
+ },
2024
+ async deleteGroup(body) {
2025
+ if (!ctx.iam)
2026
+ throw Errors.database("IAM service not available");
2027
+ await ctx.iam.deleteGroup(requireId(body.id, "id"));
2028
+ return { ok: true };
2029
+ },
2030
+ async getGroupUsers(body) {
2031
+ if (!ctx.iam)
2032
+ throw Errors.database("IAM service not available");
2033
+ return ctx.iam.getGroupUsers(requireId(body.id, "id"));
2034
+ },
2035
+ async listPermissions(body) {
2036
+ if (!ctx.iam)
2037
+ throw Errors.database("IAM service not available");
2038
+ return ctx.iam.listPermissions({
2039
+ resource: body.resource || void 0
2040
+ });
2041
+ },
2042
+ async createPermission(body) {
2043
+ if (!ctx.iam)
2044
+ throw Errors.database("IAM service not available");
2045
+ return ctx.iam.createPermission(body);
2046
+ },
2047
+ async deletePermission(body) {
2048
+ if (!ctx.iam)
2049
+ throw Errors.database("IAM service not available");
2050
+ await ctx.iam.deletePermission(requireId(body.id, "id"));
2051
+ return { ok: true };
2052
+ },
2053
+ async addPermissionToRole(body) {
2054
+ if (!ctx.iam)
2055
+ throw Errors.database("IAM service not available");
2056
+ const { id: id2, ...permission } = body;
2057
+ await ctx.iam.addPermissionToRole(requireId(id2, "id"), permission);
2058
+ return { ok: true };
2059
+ },
2060
+ // ── Core IAM operations — previously spec-only ─────────────
2061
+ async getRoles() {
2062
+ if (!ctx.iam)
2063
+ throw Errors.database("IAM service not available");
2064
+ return ctx.iam.getRoles();
2065
+ },
2066
+ async createRole(body) {
2067
+ if (!ctx.iam)
2068
+ throw Errors.database("IAM service not available");
2069
+ return ctx.iam.createRole(
2070
+ body.name,
2071
+ body.permissions ?? [],
2072
+ body.description
2073
+ );
2074
+ },
2075
+ async deleteRole(body) {
2076
+ if (!ctx.iam)
2077
+ throw Errors.database("IAM service not available");
2078
+ await ctx.iam.deleteRole(requireId(body.id, "id"));
2079
+ return { ok: true };
2080
+ },
2081
+ async bindRoleToUser(body) {
2082
+ if (!ctx.iam)
2083
+ throw Errors.database("IAM service not available");
2084
+ await ctx.iam.bindRoleToUser(
2085
+ requireId(body.userId, "userId"),
2086
+ requireId(body.id, "id"),
2087
+ body.tenantId
2088
+ );
2089
+ return { ok: true };
2090
+ },
2091
+ async bindRoleToGroup(body) {
2092
+ if (!ctx.iam)
2093
+ throw Errors.database("IAM service not available");
2094
+ await ctx.iam.bindRoleToGroup(
2095
+ requireId(body.groupId, "groupId"),
2096
+ requireId(body.id, "id"),
2097
+ body.tenantId
2098
+ );
2099
+ return { ok: true };
2100
+ },
2101
+ async unbindRole(body) {
2102
+ if (!ctx.iam)
2103
+ throw Errors.database("IAM service not available");
2104
+ await ctx.iam.unbindRole(
2105
+ body.subjectType,
2106
+ requireId(body.subjectId, "subjectId"),
2107
+ requireId(body.id, "id"),
2108
+ body.tenantId
2109
+ );
2110
+ return { ok: true };
2111
+ },
2112
+ async createGroup(body) {
2113
+ if (!ctx.iam)
2114
+ throw Errors.database("IAM service not available");
2115
+ return ctx.iam.createGroup(
2116
+ body.name,
2117
+ body.description
2118
+ );
2119
+ },
2120
+ async addUserToGroup(body) {
2121
+ if (!ctx.iam)
2122
+ throw Errors.database("IAM service not available");
2123
+ await ctx.iam.addUserToGroup(
2124
+ requireId(body.id, "id"),
2125
+ requireId(body.userId, "userId")
2126
+ );
2127
+ return { ok: true };
2128
+ },
2129
+ async removeUserFromGroup(body) {
2130
+ if (!ctx.iam)
2131
+ throw Errors.database("IAM service not available");
2132
+ await ctx.iam.removeUserFromGroup(
2133
+ requireId(body.id, "id"),
2134
+ requireId(body.userId, "userId")
2135
+ );
2136
+ return { ok: true };
2137
+ },
2138
+ async getUserPermissions(body) {
2139
+ if (!ctx.iam)
2140
+ throw Errors.database("IAM service not available");
2141
+ return ctx.iam.getPermissionsForSubject(
2142
+ { type: "USER", id: requireId(body.id, "id") },
2143
+ body.tenantId
2144
+ );
2145
+ },
2146
+ async checkPermission(body) {
2147
+ if (!ctx.iam)
2148
+ throw Errors.database("IAM service not available");
2149
+ const allowed = await ctx.iam.checkPermission(
2150
+ { type: "USER", id: requireId(body.userId, "userId") },
2151
+ body.resource,
2152
+ body.action,
2153
+ body.context
2154
+ );
2155
+ return { allowed };
2156
+ },
2157
+ async bindPermissionToUser(body) {
2158
+ if (!ctx.iam)
2159
+ throw Errors.database("IAM service not available");
2160
+ await ctx.iam.bindPermissionToUser(
2161
+ requireId(body.userId, "userId"),
2162
+ body.permission,
2163
+ body.tenantId
2164
+ );
2165
+ return { ok: true };
2166
+ },
2167
+ async bindPermissionToGroup(body) {
2168
+ if (!ctx.iam)
2169
+ throw Errors.database("IAM service not available");
2170
+ await ctx.iam.bindPermissionToGroup(
2171
+ requireId(body.groupId, "groupId"),
2172
+ body.permission,
2173
+ body.tenantId
2174
+ );
2175
+ return { ok: true };
2176
+ },
2177
+ async unbindPermissionFromUser(body) {
2178
+ if (!ctx.iam)
2179
+ throw Errors.database("IAM service not available");
2180
+ await ctx.iam.unbindPermissionFromUser(
2181
+ requireId(body.userId, "userId"),
2182
+ requireId(body.permissionId, "permissionId"),
2183
+ body.tenantId
2184
+ );
2185
+ return { ok: true };
2186
+ },
2187
+ async unbindPermissionFromGroup(body) {
2188
+ if (!ctx.iam)
2189
+ throw Errors.database("IAM service not available");
2190
+ await ctx.iam.unbindPermissionFromGroup(
2191
+ requireId(body.groupId, "groupId"),
2192
+ requireId(body.permissionId, "permissionId"),
2193
+ body.tenantId
2194
+ );
2195
+ return { ok: true };
2196
+ },
2197
+ // ── Service-account IAM proxies ────────────────────────────
2198
+ //
2199
+ // The admin plugin re-exports `iamEndpoints` on its own `routes` so
2200
+ // that IAM routes get dispatched through `dispatchPlugin`. That
2201
+ // means every IAM handler referenced by those endpoint definitions
2202
+ // must exist on this plugin's methods object — thin proxies that
2203
+ // delegate to `ctx.iam`. Without these, hitting any
2204
+ // `/iam/service-accounts/*` route through an instance that has the
2205
+ // admin plugin registered returns a 404 from the dispatcher.
2206
+ async createServiceAccount(body) {
2207
+ if (!ctx.iam)
2208
+ throw Errors.database("IAM service not available");
2209
+ return ctx.iam.createServiceAccount({
2210
+ name: String(body.name ?? ""),
2211
+ displayName: body.displayName,
2212
+ description: body.description
2213
+ });
2214
+ },
2215
+ async listServiceAccounts(body) {
2216
+ if (!ctx.iam)
2217
+ throw Errors.database("IAM service not available");
2218
+ return ctx.iam.listServiceAccounts({
2219
+ limit: body.limit != null ? Number(body.limit) : void 0,
2220
+ offset: body.offset != null ? Number(body.offset) : void 0
2221
+ });
2222
+ },
2223
+ async getServiceAccount(body) {
2224
+ if (!ctx.iam)
2225
+ throw Errors.database("IAM service not available");
2226
+ return ctx.iam.getServiceAccount(requireId(body.id, "id"));
2227
+ },
2228
+ async updateServiceAccount(body) {
2229
+ if (!ctx.iam)
2230
+ throw Errors.database("IAM service not available");
2231
+ return ctx.iam.updateServiceAccount(requireId(body.id, "id"), {
2232
+ displayName: body.displayName,
2233
+ description: body.description,
2234
+ isActive: body.isActive
2235
+ });
2236
+ },
2237
+ async deleteServiceAccount(body) {
2238
+ if (!ctx.iam)
2239
+ throw Errors.database("IAM service not available");
2240
+ await ctx.iam.deleteServiceAccount(requireId(body.id, "id"));
2241
+ return { ok: true };
2242
+ },
2243
+ async getServiceAccountPermissions(body) {
2244
+ if (!ctx.iam)
2245
+ throw Errors.database("IAM service not available");
2246
+ return ctx.iam.getPermissionsForSubject(
2247
+ { type: "SERVICE_ACCOUNT", id: requireId(body.id, "id") },
2248
+ body.tenantId
2249
+ );
2250
+ },
2251
+ async bindRoleToServiceAccount(body) {
2252
+ if (!ctx.iam)
2253
+ throw Errors.database("IAM service not available");
2254
+ await ctx.iam.bindRoleToServiceAccount(
2255
+ requireId(body.serviceAccountId, "serviceAccountId"),
2256
+ requireId(body.id, "id"),
2257
+ body.tenantId
2258
+ );
2259
+ return { ok: true };
2260
+ },
2261
+ async unbindRoleFromServiceAccount(body) {
2262
+ if (!ctx.iam)
2263
+ throw Errors.database("IAM service not available");
2264
+ await ctx.iam.unbindRoleFromServiceAccount(
2265
+ requireId(body.serviceAccountId, "serviceAccountId"),
2266
+ requireId(body.id, "id"),
2267
+ body.tenantId
2268
+ );
2269
+ return { ok: true };
2270
+ },
2271
+ async bindPermissionToServiceAccount(body) {
2272
+ if (!ctx.iam)
2273
+ throw Errors.database("IAM service not available");
2274
+ await ctx.iam.bindPermissionToServiceAccount(
2275
+ requireId(body.serviceAccountId, "serviceAccountId"),
2276
+ body.permission,
2277
+ body.tenantId
2278
+ );
2279
+ return { ok: true };
2280
+ },
2281
+ async unbindPermissionFromServiceAccount(body) {
2282
+ if (!ctx.iam)
2283
+ throw Errors.database("IAM service not available");
2284
+ await ctx.iam.unbindPermissionFromServiceAccount(
2285
+ requireId(body.serviceAccountId, "serviceAccountId"),
2286
+ requireId(body.permissionId, "permissionId"),
2287
+ body.tenantId
2288
+ );
2289
+ return { ok: true };
2290
+ },
2291
+ // ── Admin api-key management ───────────────────────────────
2292
+ //
2293
+ // Read/delete delegate to the stateless helpers in `api-key/core.ts`.
2294
+ // Create re-enters the api-key plugin's `methods` factory so the
2295
+ // configured knobs (prefix, maxKeysPerSubject, defaultExpirySeconds)
2296
+ // apply to admin-minted keys exactly as they do to self-service
2297
+ // keys — no duplicate config. All of these are available as
2298
+ // programmatic methods regardless of the `apiKeyRoutes` flag — only
2299
+ // the HTTP mounting is gated. Requires the `api-key` plugin to be
2300
+ // registered (for the `api_key` model).
2301
+ async adminCreateUserApiKey(body) {
2302
+ const userId = requireId(body.userId, "userId");
2303
+ const user = await ctx.db.findOne({
2304
+ model: "user",
2305
+ where: [{ field: "id", operator: "=", value: userId }]
2306
+ });
2307
+ if (!user)
2308
+ throw Errors.notFound("User not found");
2309
+ const apiKeyMethods = getApiKeyMethods(ctx);
2310
+ return apiKeyMethods.createKey({
2311
+ subject: { type: "USER", id: userId },
2312
+ name: String(body.name ?? ""),
2313
+ scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : void 0,
2314
+ expiresAt: body.expiresAt
2315
+ });
2316
+ },
2317
+ async adminListUserApiKeys(body) {
2318
+ const keys = await listKeysForSubject(ctx.db, {
2319
+ type: "USER",
2320
+ id: requireId(body.userId, "userId")
2321
+ });
2322
+ return { keys };
2323
+ },
2324
+ async adminRevokeUserApiKey(body) {
2325
+ await revokeKeyAsAdmin(ctx.db, requireId(body.id, "id"));
2326
+ return { ok: true };
2327
+ },
2328
+ // ── Service-account-scoped admin api-key handlers ─────────
2329
+ async adminCreateServiceAccountApiKey(body) {
2330
+ const serviceAccountId = requireId(body.id, "id");
2331
+ const sa = await ctx.db.findOne({
2332
+ model: "service_account",
2333
+ where: [{ field: "id", operator: "=", value: serviceAccountId }]
2334
+ });
2335
+ if (!sa)
2336
+ throw Errors.notFound("Service account not found");
2337
+ const apiKeyMethods = getApiKeyMethods(ctx);
2338
+ return apiKeyMethods.createKey({
2339
+ subject: { type: "SERVICE_ACCOUNT", id: serviceAccountId },
2340
+ name: String(body.name ?? ""),
2341
+ scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : void 0,
2342
+ expiresAt: body.expiresAt
2343
+ });
2344
+ },
2345
+ async adminListServiceAccountApiKeys(body) {
2346
+ const keys = await listKeysForSubject(ctx.db, {
2347
+ type: "SERVICE_ACCOUNT",
2348
+ id: requireId(body.id, "id")
2349
+ });
2350
+ return { keys };
2351
+ },
2352
+ async adminRevokeServiceAccountApiKey(body) {
2353
+ await revokeKeyAsAdmin(ctx.db, requireId(body.keyId, "keyId"));
2354
+ return { ok: true };
2355
+ },
2356
+ // ── Resource sync ──────────────────────────────────────────
2357
+ async syncResources(body) {
2358
+ if (!ctx.iam)
2359
+ throw Errors.database("IAM service not available");
2360
+ await ctx.iam.syncResources(
2361
+ body.direction,
2362
+ body.filePath
2363
+ );
2364
+ return { ok: true };
2365
+ }
2366
+ })
2367
+ };
2368
+ }
2369
+ function getApiKeyMethods(ctx) {
2370
+ const apiKeyPlugin = (ctx.config.plugins ?? []).find((p) => p.name === "api-key");
2371
+ if (!apiKeyPlugin?.methods)
2372
+ throw Errors.database("api-key plugin is not registered");
2373
+ return apiKeyPlugin.methods(ctx);
2374
+ }
2375
+ // Annotate the CommonJS export names for ESM import in node:
2376
+ 0 && (module.exports = {
2377
+ admin
2378
+ });