@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,779 @@
1
+ import type { ErrorResponseWire, OkResponseWire } from '../auth/auth-endpoints';
2
+ import type { EndpointDefinition } from '../endpoint';
3
+ import type { FortressSchema } from '../json-schema';
4
+ import { authRef } from '../auth/auth-endpoints';
5
+ import { arr, bool, defineComponents, endpoint, enums, id, int, nullable, obj, record, recordOf, str } from '../schema-builder';
6
+
7
+ // Sentinel for "no body / query / params" matching EndpointDefinition's default.
8
+
9
+ interface EmptyInput {}
10
+
11
+ // ── Wire-format shapes (what endpoint handlers serialize to JSON) ──────
12
+
13
+ /** Wire shape of a persisted role. `description` and `isSystem` are optional nullable on the wire. */
14
+ export interface RoleWire {
15
+ id: string;
16
+ name: string;
17
+ description?: string | null;
18
+ isSystem?: boolean;
19
+ }
20
+
21
+ /** Wire shape of a persisted group. */
22
+ export interface GroupWire {
23
+ id: string;
24
+ name: string;
25
+ description?: string | null;
26
+ }
27
+
28
+ /** Wire shape of a permission condition. `operator` is restricted to the supported set. */
29
+ export interface PermissionConditionWire {
30
+ field: string;
31
+ operator: 'eq' | 'neq' | 'in' | 'startsWith';
32
+ value: string;
33
+ }
34
+
35
+ /** Wire shape of a persisted permission. */
36
+ export interface PermissionWire {
37
+ id: string;
38
+ resource: string;
39
+ action: string;
40
+ effect: 'ALLOW' | 'DENY';
41
+ conditions?: PermissionConditionWire[];
42
+ description?: string | null;
43
+ }
44
+
45
+ /** Wire shape of a permission-creation input. */
46
+ export interface PermissionInputWire {
47
+ resource: string;
48
+ action: string;
49
+ effect?: 'ALLOW' | 'DENY';
50
+ conditions?: PermissionConditionWire[];
51
+ }
52
+
53
+ /** Wire shape of a persisted service account. Date fields are ISO strings on the wire. */
54
+ export interface ServiceAccountWire {
55
+ id: string;
56
+ name: string;
57
+ displayName?: string | null;
58
+ description?: string | null;
59
+ isActive: boolean;
60
+ createdAt?: string;
61
+ updatedAt?: string;
62
+ }
63
+
64
+ // ── Component schemas (typed registry) ──────────────────────────────────
65
+
66
+ const Role: FortressSchema<RoleWire> = obj(
67
+ {
68
+ id: id('Role ID'),
69
+ name: str('Role name'),
70
+ description: nullable(str('Role description')),
71
+ isSystem: bool('Whether this is a system role'),
72
+ },
73
+ 'id',
74
+ 'name',
75
+ ) as FortressSchema<RoleWire>;
76
+
77
+ const Group: FortressSchema<GroupWire> = obj(
78
+ {
79
+ id: id('Group ID'),
80
+ name: str('Group name'),
81
+ description: nullable(str('Group description')),
82
+ },
83
+ 'id',
84
+ 'name',
85
+ ) as FortressSchema<GroupWire>;
86
+
87
+ const Permission: FortressSchema<PermissionWire> = obj(
88
+ {
89
+ id: id('Permission ID'),
90
+ resource: str('Resource name'),
91
+ action: str('Action name'),
92
+ effect: enums('ALLOW', 'DENY'),
93
+ conditions: arr(obj({
94
+ field: str('Condition field'),
95
+ operator: enums('eq', 'neq', 'in', 'startsWith'),
96
+ value: str('Condition value'),
97
+ }), 'Permission conditions'),
98
+ description: nullable(str('Permission description')),
99
+ },
100
+ 'id',
101
+ 'resource',
102
+ 'action',
103
+ 'effect',
104
+ ) as FortressSchema<PermissionWire>;
105
+
106
+ const PermissionInput: FortressSchema<PermissionInputWire> = obj(
107
+ {
108
+ resource: str('Resource name'),
109
+ action: str('Action name'),
110
+ effect: enums('ALLOW', 'DENY'),
111
+ conditions: arr(obj({
112
+ field: str('Condition field'),
113
+ operator: enums('eq', 'neq', 'in', 'startsWith'),
114
+ value: str('Condition value'),
115
+ })),
116
+ },
117
+ 'resource',
118
+ 'action',
119
+ ) as FortressSchema<PermissionInputWire>;
120
+
121
+ const ServiceAccount: FortressSchema<ServiceAccountWire> = obj(
122
+ {
123
+ id: id('Service account ID'),
124
+ name: str('Machine identifier — immutable after creation'),
125
+ displayName: nullable(str('Human-readable label')),
126
+ description: nullable(str('Free-form description')),
127
+ isActive: bool('Whether the account can authenticate and resolve permissions'),
128
+ createdAt: str('ISO 8601 creation timestamp'),
129
+ updatedAt: str('ISO 8601 update timestamp'),
130
+ },
131
+ 'id',
132
+ 'name',
133
+ 'isActive',
134
+ ) as FortressSchema<ServiceAccountWire>;
135
+
136
+ /** Explicit registry type so JSR's fast-check doesn't walk into the deeply-inferred `defineComponents` return. */
137
+ interface IamComponents {
138
+ readonly components: {
139
+ readonly Role: FortressSchema<RoleWire>;
140
+ readonly Group: FortressSchema<GroupWire>;
141
+ readonly Permission: FortressSchema<PermissionWire>;
142
+ readonly PermissionInput: FortressSchema<PermissionInputWire>;
143
+ readonly ServiceAccount: FortressSchema<ServiceAccountWire>;
144
+ };
145
+ readonly ref: <K extends keyof IamComponents['components']>(
146
+ name: K,
147
+ ) => FortressSchema<IamComponents['components'][K] extends FortressSchema<infer U> ? U : never>;
148
+ }
149
+
150
+ const iamComponents: IamComponents = defineComponents({
151
+ Role,
152
+ Group,
153
+ Permission,
154
+ PermissionInput,
155
+ ServiceAccount,
156
+ }) as IamComponents;
157
+
158
+ /** Reusable OpenAPI component schemas referenced by the core IAM endpoints. */
159
+ export const iamComponentSchemas: IamComponents['components'] = iamComponents.components;
160
+
161
+ /** Typed `$ref` helper bound to {@link iamComponentSchemas}. */
162
+ export const iamRef: IamComponents['ref'] = iamComponents.ref;
163
+
164
+ // ── IAM endpoint definitions (keyed by handler name) ───────────────
165
+
166
+ /** Resource catalog response shape. */
167
+ interface ResourceCatalogWire {
168
+ resources: Record<string, { actions: string[]; description?: string }>;
169
+ }
170
+
171
+ /** Paged service-account list response shape. */
172
+ interface ServiceAccountsListWire {
173
+ serviceAccounts: ServiceAccountWire[];
174
+ total: number;
175
+ }
176
+
177
+ /**
178
+ * Typed record of every core IAM endpoint. Declared explicitly so JSR's
179
+ * fast-check passes without `--allow-slow-types`, while
180
+ * `InferEndpointCallInput<typeof iamEndpoints.X>` still resolves per-handler.
181
+ */
182
+ export interface IamEndpointsMap {
183
+ getResources: EndpointDefinition<
184
+ EmptyInput,
185
+ EmptyInput,
186
+ EmptyInput,
187
+ { 200: ResourceCatalogWire; 401: ErrorResponseWire }
188
+ >;
189
+ getRoles: EndpointDefinition<
190
+ EmptyInput,
191
+ EmptyInput,
192
+ EmptyInput,
193
+ { 200: RoleWire[]; 401: ErrorResponseWire }
194
+ >;
195
+ createRole: EndpointDefinition<
196
+ { name: string; permissions: PermissionInputWire[]; description?: string },
197
+ EmptyInput,
198
+ EmptyInput,
199
+ { 201: RoleWire; 401: ErrorResponseWire }
200
+ >;
201
+ deleteRole: EndpointDefinition<
202
+ EmptyInput,
203
+ EmptyInput,
204
+ { id: string },
205
+ { 200: OkResponseWire; 400: ErrorResponseWire; 401: ErrorResponseWire }
206
+ >;
207
+ bindRoleToUser: EndpointDefinition<
208
+ { userId: string; tenantId?: string },
209
+ EmptyInput,
210
+ { id: string },
211
+ { 200: OkResponseWire; 401: ErrorResponseWire }
212
+ >;
213
+ bindRoleToGroup: EndpointDefinition<
214
+ { groupId: string; tenantId?: string },
215
+ EmptyInput,
216
+ { id: string },
217
+ { 200: OkResponseWire; 401: ErrorResponseWire }
218
+ >;
219
+ unbindRole: EndpointDefinition<
220
+ {
221
+ subjectType: 'USER' | 'GROUP' | 'SERVICE_ACCOUNT';
222
+ subjectId: string;
223
+ tenantId?: string;
224
+ },
225
+ EmptyInput,
226
+ { id: string },
227
+ { 200: OkResponseWire; 401: ErrorResponseWire }
228
+ >;
229
+ createGroup: EndpointDefinition<
230
+ { name: string; description?: string },
231
+ EmptyInput,
232
+ EmptyInput,
233
+ { 201: GroupWire; 401: ErrorResponseWire }
234
+ >;
235
+ addUserToGroup: EndpointDefinition<
236
+ { userId: string },
237
+ EmptyInput,
238
+ { id: string },
239
+ { 200: OkResponseWire; 401: ErrorResponseWire }
240
+ >;
241
+ removeUserFromGroup: EndpointDefinition<
242
+ EmptyInput,
243
+ EmptyInput,
244
+ { id: string; userId: string },
245
+ { 200: OkResponseWire; 401: ErrorResponseWire }
246
+ >;
247
+ getUserPermissions: EndpointDefinition<
248
+ EmptyInput,
249
+ { tenantId?: string },
250
+ { id: string },
251
+ { 200: PermissionWire[]; 401: ErrorResponseWire }
252
+ >;
253
+ checkPermission: EndpointDefinition<
254
+ {
255
+ userId: string;
256
+ resource: string;
257
+ action: string;
258
+ context?: Record<string, unknown>;
259
+ },
260
+ EmptyInput,
261
+ EmptyInput,
262
+ { 200: { allowed: boolean }; 401: ErrorResponseWire }
263
+ >;
264
+ bindPermissionToUser: EndpointDefinition<
265
+ { userId: string; permission: PermissionInputWire; tenantId?: string },
266
+ EmptyInput,
267
+ EmptyInput,
268
+ { 200: OkResponseWire; 401: ErrorResponseWire }
269
+ >;
270
+ bindPermissionToGroup: EndpointDefinition<
271
+ { groupId: string; permission: PermissionInputWire; tenantId?: string },
272
+ EmptyInput,
273
+ EmptyInput,
274
+ { 200: OkResponseWire; 401: ErrorResponseWire }
275
+ >;
276
+ unbindPermissionFromUser: EndpointDefinition<
277
+ { userId: string; permissionId: string; tenantId?: string },
278
+ EmptyInput,
279
+ EmptyInput,
280
+ { 200: OkResponseWire; 401: ErrorResponseWire }
281
+ >;
282
+ unbindPermissionFromGroup: EndpointDefinition<
283
+ { groupId: string; permissionId: string; tenantId?: string },
284
+ EmptyInput,
285
+ EmptyInput,
286
+ { 200: OkResponseWire; 401: ErrorResponseWire }
287
+ >;
288
+ createServiceAccount: EndpointDefinition<
289
+ { name: string; displayName?: string; description?: string },
290
+ EmptyInput,
291
+ EmptyInput,
292
+ { 201: ServiceAccountWire; 400: ErrorResponseWire; 401: ErrorResponseWire }
293
+ >;
294
+ listServiceAccounts: EndpointDefinition<
295
+ EmptyInput,
296
+ { limit?: number; offset?: number },
297
+ EmptyInput,
298
+ { 200: ServiceAccountsListWire; 401: ErrorResponseWire }
299
+ >;
300
+ getServiceAccount: EndpointDefinition<
301
+ EmptyInput,
302
+ EmptyInput,
303
+ { id: string },
304
+ { 200: ServiceAccountWire; 401: ErrorResponseWire; 404: ErrorResponseWire }
305
+ >;
306
+ updateServiceAccount: EndpointDefinition<
307
+ { displayName?: string | null; description?: string | null; isActive?: boolean },
308
+ EmptyInput,
309
+ { id: string },
310
+ { 200: ServiceAccountWire; 401: ErrorResponseWire; 404: ErrorResponseWire }
311
+ >;
312
+ deleteServiceAccount: EndpointDefinition<
313
+ EmptyInput,
314
+ EmptyInput,
315
+ { id: string },
316
+ { 200: OkResponseWire; 401: ErrorResponseWire; 404: ErrorResponseWire }
317
+ >;
318
+ getServiceAccountPermissions: EndpointDefinition<
319
+ EmptyInput,
320
+ { tenantId?: string },
321
+ { id: string },
322
+ { 200: PermissionWire[]; 401: ErrorResponseWire }
323
+ >;
324
+ bindRoleToServiceAccount: EndpointDefinition<
325
+ { serviceAccountId: string; tenantId?: string },
326
+ EmptyInput,
327
+ { id: string },
328
+ { 200: OkResponseWire; 401: ErrorResponseWire }
329
+ >;
330
+ unbindRoleFromServiceAccount: EndpointDefinition<
331
+ { serviceAccountId: string; tenantId?: string },
332
+ EmptyInput,
333
+ { id: string },
334
+ { 200: OkResponseWire; 401: ErrorResponseWire }
335
+ >;
336
+ bindPermissionToServiceAccount: EndpointDefinition<
337
+ { serviceAccountId: string; permission: PermissionInputWire; tenantId?: string },
338
+ EmptyInput,
339
+ EmptyInput,
340
+ { 200: OkResponseWire; 401: ErrorResponseWire }
341
+ >;
342
+ unbindPermissionFromServiceAccount: EndpointDefinition<
343
+ { serviceAccountId: string; permissionId: string; tenantId?: string },
344
+ EmptyInput,
345
+ EmptyInput,
346
+ { 200: OkResponseWire; 401: ErrorResponseWire }
347
+ >;
348
+ }
349
+
350
+ /**
351
+ * Declarative endpoint definitions for fortress's built-in IAM admin routes.
352
+ * The explicit `IamEndpointsMap` annotation keeps JSR fast-check happy
353
+ * without sacrificing per-handler inference for `fortress.call.*`.
354
+ */
355
+ export const iamEndpoints: IamEndpointsMap = {
356
+ // ── Resources ──
357
+
358
+ getResources: endpoint('GET', '/iam/resources')
359
+ .summary('List all available resources')
360
+ .tags('IAM', 'Resources')
361
+ .security('bearer')
362
+ .permission('fortress', 'viewResources')
363
+ .response(200, 'Available resources with their actions', obj({
364
+ resources: recordOf(obj({
365
+ actions: arr(str('Action name'), 'Available actions'),
366
+ description: str('Resource description'),
367
+ }), 'Map of resource name to definition'),
368
+ }, 'resources'))
369
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
370
+ .handler('getResources')
371
+ .build() as IamEndpointsMap['getResources'],
372
+
373
+ // ── Roles ──
374
+
375
+ getRoles: endpoint('GET', '/iam/roles')
376
+ .summary('List all roles')
377
+ .tags('IAM', 'Roles')
378
+ .security('bearer')
379
+ .permission('fortress', 'viewRoles')
380
+ .response(200, 'All roles', arr(iamRef('Role')))
381
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
382
+ .handler('getRoles')
383
+ .build() as IamEndpointsMap['getRoles'],
384
+
385
+ createRole: endpoint('POST', '/iam/roles')
386
+ .summary('Create a role')
387
+ .tags('IAM', 'Roles')
388
+ .security('bearer')
389
+ .permission('fortress', 'createRole')
390
+ .body(obj(
391
+ {
392
+ name: str('Role name'),
393
+ permissions: arr(iamRef('PermissionInput'), 'Permissions to assign'),
394
+ description: str('Role description'),
395
+ },
396
+ 'name',
397
+ 'permissions',
398
+ ))
399
+ .response(201, 'Role created', iamRef('Role'))
400
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
401
+ .handler('createRole')
402
+ .build() as IamEndpointsMap['createRole'],
403
+
404
+ deleteRole: endpoint('DELETE', '/iam/roles/:id')
405
+ .summary('Delete a role')
406
+ .tags('IAM', 'Roles')
407
+ .security('bearer')
408
+ .permission('fortress', 'deleteRole')
409
+ .params(obj({ id: id('Role ID') }, 'id'))
410
+ .response(200, 'Role deleted', obj({ ok: bool() }, 'ok'))
411
+ .response(400, 'Cannot delete system role', authRef('ErrorResponse'))
412
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
413
+ .handler('deleteRole')
414
+ .build() as IamEndpointsMap['deleteRole'],
415
+
416
+ // ── Role Bindings ──
417
+
418
+ bindRoleToUser: endpoint('POST', '/iam/roles/:id/bind/user')
419
+ .summary('Bind role to a user')
420
+ .tags('IAM', 'Roles')
421
+ .security('bearer')
422
+ .permission('fortress', 'bindRole')
423
+ .params(obj({ id: id('Role ID') }, 'id'))
424
+ .body(obj(
425
+ { userId: id('User ID'), tenantId: str('Tenant ID (optional)') },
426
+ 'userId',
427
+ ))
428
+ .response(200, 'Role bound to user', obj({ ok: bool() }, 'ok'))
429
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
430
+ .handler('bindRoleToUser')
431
+ .build() as IamEndpointsMap['bindRoleToUser'],
432
+
433
+ bindRoleToGroup: endpoint('POST', '/iam/roles/:id/bind/group')
434
+ .summary('Bind role to a group')
435
+ .tags('IAM', 'Roles')
436
+ .security('bearer')
437
+ .permission('fortress', 'bindRole')
438
+ .params(obj({ id: id('Role ID') }, 'id'))
439
+ .body(obj(
440
+ { groupId: id('Group ID'), tenantId: str('Tenant ID (optional)') },
441
+ 'groupId',
442
+ ))
443
+ .response(200, 'Role bound to group', obj({ ok: bool() }, 'ok'))
444
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
445
+ .handler('bindRoleToGroup')
446
+ .build() as IamEndpointsMap['bindRoleToGroup'],
447
+
448
+ unbindRole: endpoint('DELETE', '/iam/roles/:id/bind')
449
+ .summary('Unbind a role')
450
+ .tags('IAM', 'Roles')
451
+ .security('bearer')
452
+ .permission('fortress', 'unbindRole')
453
+ .params(obj({ id: id('Role ID') }, 'id'))
454
+ .body(obj(
455
+ {
456
+ subjectType: enums('USER', 'GROUP', 'SERVICE_ACCOUNT'),
457
+ subjectId: id('Subject ID'),
458
+ tenantId: str('Tenant ID (optional)'),
459
+ },
460
+ 'subjectType',
461
+ 'subjectId',
462
+ ))
463
+ .response(200, 'Role unbound', obj({ ok: bool() }, 'ok'))
464
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
465
+ .handler('unbindRole')
466
+ .build() as IamEndpointsMap['unbindRole'],
467
+
468
+ // ── Groups ──
469
+
470
+ createGroup: endpoint('POST', '/iam/groups')
471
+ .summary('Create a group')
472
+ .tags('IAM', 'Groups')
473
+ .security('bearer')
474
+ .permission('fortress', 'createGroup')
475
+ .body(obj(
476
+ { name: str('Group name'), description: str('Group description') },
477
+ 'name',
478
+ ))
479
+ .response(201, 'Group created', iamRef('Group'))
480
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
481
+ .handler('createGroup')
482
+ .build() as IamEndpointsMap['createGroup'],
483
+
484
+ addUserToGroup: endpoint('POST', '/iam/groups/:id/users')
485
+ .summary('Add user to group')
486
+ .tags('IAM', 'Groups')
487
+ .security('bearer')
488
+ .permission('fortress', 'manageGroup')
489
+ .params(obj({ id: id('Group ID') }, 'id'))
490
+ .body(obj({ userId: id('User ID') }, 'userId'))
491
+ .response(200, 'User added to group', obj({ ok: bool() }, 'ok'))
492
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
493
+ .handler('addUserToGroup')
494
+ .build() as IamEndpointsMap['addUserToGroup'],
495
+
496
+ removeUserFromGroup: endpoint('DELETE', '/iam/groups/:id/users/:userId')
497
+ .summary('Remove user from group')
498
+ .tags('IAM', 'Groups')
499
+ .security('bearer')
500
+ .permission('fortress', 'manageGroup')
501
+ .params(obj({ id: id('Group ID'), userId: id('User ID') }, 'id', 'userId'))
502
+ .response(200, 'User removed from group', obj({ ok: bool() }, 'ok'))
503
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
504
+ .handler('removeUserFromGroup')
505
+ .build() as IamEndpointsMap['removeUserFromGroup'],
506
+
507
+ // ── Permissions ──
508
+
509
+ getUserPermissions: endpoint('GET', '/iam/users/:id/permissions')
510
+ .summary('Get user permissions')
511
+ .tags('IAM', 'Permissions')
512
+ .security('bearer')
513
+ .permission('fortress', 'viewPermissions')
514
+ .params(obj({ id: id('User ID') }, 'id'))
515
+ .query(obj({ tenantId: str('Tenant ID (optional)') }))
516
+ .response(200, 'User permissions', arr(iamRef('Permission')))
517
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
518
+ .handler('getUserPermissions')
519
+ .build() as IamEndpointsMap['getUserPermissions'],
520
+
521
+ checkPermission: endpoint('POST', '/iam/check')
522
+ .summary('Check if user has permission')
523
+ .tags('IAM', 'Permissions')
524
+ .security('bearer')
525
+ .permission('fortress', 'viewPermissions')
526
+ .body(obj(
527
+ {
528
+ userId: id('User ID'),
529
+ resource: str('Resource name'),
530
+ action: str('Action name'),
531
+ context: record('Permission context'),
532
+ },
533
+ 'userId',
534
+ 'resource',
535
+ 'action',
536
+ ))
537
+ .response(200, 'Permission check result', obj({ allowed: bool('Whether permission is granted') }, 'allowed'))
538
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
539
+ .handler('checkPermission')
540
+ .build() as IamEndpointsMap['checkPermission'],
541
+
542
+ bindPermissionToUser: endpoint('POST', '/iam/permissions/bind/user')
543
+ .summary('Bind permission directly to a user')
544
+ .tags('IAM', 'Permissions')
545
+ .security('bearer')
546
+ .permission('fortress', 'managePermissions')
547
+ .body(obj(
548
+ {
549
+ userId: id('User ID'),
550
+ permission: iamRef('PermissionInput'),
551
+ tenantId: str('Tenant ID (optional)'),
552
+ },
553
+ 'userId',
554
+ 'permission',
555
+ ))
556
+ .response(200, 'Permission bound', obj({ ok: bool() }, 'ok'))
557
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
558
+ .handler('bindPermissionToUser')
559
+ .build() as IamEndpointsMap['bindPermissionToUser'],
560
+
561
+ bindPermissionToGroup: endpoint('POST', '/iam/permissions/bind/group')
562
+ .summary('Bind permission directly to a group')
563
+ .tags('IAM', 'Permissions')
564
+ .security('bearer')
565
+ .permission('fortress', 'managePermissions')
566
+ .body(obj(
567
+ {
568
+ groupId: id('Group ID'),
569
+ permission: iamRef('PermissionInput'),
570
+ tenantId: str('Tenant ID (optional)'),
571
+ },
572
+ 'groupId',
573
+ 'permission',
574
+ ))
575
+ .response(200, 'Permission bound', obj({ ok: bool() }, 'ok'))
576
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
577
+ .handler('bindPermissionToGroup')
578
+ .build() as IamEndpointsMap['bindPermissionToGroup'],
579
+
580
+ unbindPermissionFromUser: endpoint('DELETE', '/iam/permissions/bind/user')
581
+ .summary('Unbind permission from a user')
582
+ .tags('IAM', 'Permissions')
583
+ .security('bearer')
584
+ .permission('fortress', 'managePermissions')
585
+ .body(obj(
586
+ {
587
+ userId: id('User ID'),
588
+ permissionId: id('Permission ID'),
589
+ tenantId: str('Tenant ID (optional)'),
590
+ },
591
+ 'userId',
592
+ 'permissionId',
593
+ ))
594
+ .response(200, 'Permission unbound', obj({ ok: bool() }, 'ok'))
595
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
596
+ .handler('unbindPermissionFromUser')
597
+ .build() as IamEndpointsMap['unbindPermissionFromUser'],
598
+
599
+ unbindPermissionFromGroup: endpoint('DELETE', '/iam/permissions/bind/group')
600
+ .summary('Unbind permission from a group')
601
+ .tags('IAM', 'Permissions')
602
+ .security('bearer')
603
+ .permission('fortress', 'managePermissions')
604
+ .body(obj(
605
+ {
606
+ groupId: id('Group ID'),
607
+ permissionId: id('Permission ID'),
608
+ tenantId: str('Tenant ID (optional)'),
609
+ },
610
+ 'groupId',
611
+ 'permissionId',
612
+ ))
613
+ .response(200, 'Permission unbound', obj({ ok: bool() }, 'ok'))
614
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
615
+ .handler('unbindPermissionFromGroup')
616
+ .build() as IamEndpointsMap['unbindPermissionFromGroup'],
617
+
618
+ // ── Service Accounts ──
619
+
620
+ createServiceAccount: endpoint('POST', '/iam/service-accounts')
621
+ .summary('Create a service account')
622
+ .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.')
623
+ .tags('IAM', 'Service Accounts')
624
+ .security('bearer')
625
+ .permission('fortress', 'createServiceAccount')
626
+ .body(obj(
627
+ {
628
+ name: str('Machine identifier (immutable)'),
629
+ displayName: str('Human-readable label'),
630
+ description: str('Free-form description'),
631
+ },
632
+ 'name',
633
+ ))
634
+ .response(201, 'Service account created', iamRef('ServiceAccount'))
635
+ .response(400, 'Bad request', authRef('ErrorResponse'))
636
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
637
+ .handler('createServiceAccount')
638
+ .build() as IamEndpointsMap['createServiceAccount'],
639
+
640
+ listServiceAccounts: endpoint('GET', '/iam/service-accounts')
641
+ .summary('List service accounts')
642
+ .tags('IAM', 'Service Accounts')
643
+ .security('bearer')
644
+ .permission('fortress', 'viewServiceAccounts')
645
+ .query(obj({
646
+ limit: int('Page size (default 50)'),
647
+ offset: int('Offset from the start of the list'),
648
+ }))
649
+ .response(200, 'Service accounts', obj({
650
+ serviceAccounts: arr(iamRef('ServiceAccount')),
651
+ total: int('Total service accounts'),
652
+ }, 'serviceAccounts', 'total'))
653
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
654
+ .handler('listServiceAccounts')
655
+ .build() as IamEndpointsMap['listServiceAccounts'],
656
+
657
+ getServiceAccount: endpoint('GET', '/iam/service-accounts/:id')
658
+ .summary('Get a service account by ID')
659
+ .tags('IAM', 'Service Accounts')
660
+ .security('bearer')
661
+ .permission('fortress', 'viewServiceAccounts')
662
+ .params(obj({ id: id('Service account ID') }, 'id'))
663
+ .response(200, 'Service account', iamRef('ServiceAccount'))
664
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
665
+ .response(404, 'Not found', authRef('ErrorResponse'))
666
+ .handler('getServiceAccount')
667
+ .build() as IamEndpointsMap['getServiceAccount'],
668
+
669
+ updateServiceAccount: endpoint('PATCH', '/iam/service-accounts/:id')
670
+ .summary('Update a service account')
671
+ .description('Update displayName, description, or isActive. The `name` field is immutable — to rename, delete and recreate.')
672
+ .tags('IAM', 'Service Accounts')
673
+ .security('bearer')
674
+ .permission('fortress', 'manageServiceAccount')
675
+ .params(obj({ id: id('Service account ID') }, 'id'))
676
+ .body(obj({
677
+ displayName: nullable(str('New human-readable label')),
678
+ description: nullable(str('New description')),
679
+ isActive: bool('Whether the account is active'),
680
+ }))
681
+ .response(200, 'Service account updated', iamRef('ServiceAccount'))
682
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
683
+ .response(404, 'Not found', authRef('ErrorResponse'))
684
+ .handler('updateServiceAccount')
685
+ .build() as IamEndpointsMap['updateServiceAccount'],
686
+
687
+ deleteServiceAccount: endpoint('DELETE', '/iam/service-accounts/:id')
688
+ .summary('Delete a service account')
689
+ .description('Hard-deletes the service account and cascades to role bindings, direct permission bindings, and (via plugin observer) api keys owned by the account.')
690
+ .tags('IAM', 'Service Accounts')
691
+ .security('bearer')
692
+ .permission('fortress', 'manageServiceAccount')
693
+ .params(obj({ id: id('Service account ID') }, 'id'))
694
+ .response(200, 'Service account deleted', obj({ ok: bool() }, 'ok'))
695
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
696
+ .response(404, 'Not found', authRef('ErrorResponse'))
697
+ .handler('deleteServiceAccount')
698
+ .build() as IamEndpointsMap['deleteServiceAccount'],
699
+
700
+ getServiceAccountPermissions: endpoint('GET', '/iam/service-accounts/:id/permissions')
701
+ .summary('Get effective permissions for a service account')
702
+ .tags('IAM', 'Service Accounts', 'Permissions')
703
+ .security('bearer')
704
+ .permission('fortress', 'viewPermissions')
705
+ .params(obj({ id: id('Service account ID') }, 'id'))
706
+ .query(obj({ tenantId: str('Tenant ID (optional)') }))
707
+ .response(200, 'Service account permissions', arr(iamRef('Permission')))
708
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
709
+ .handler('getServiceAccountPermissions')
710
+ .build() as IamEndpointsMap['getServiceAccountPermissions'],
711
+
712
+ bindRoleToServiceAccount: endpoint('POST', '/iam/roles/:id/bind/service-account')
713
+ .summary('Bind role to a service account')
714
+ .tags('IAM', 'Roles', 'Service Accounts')
715
+ .security('bearer')
716
+ .permission('fortress', 'bindRole')
717
+ .params(obj({ id: id('Role ID') }, 'id'))
718
+ .body(obj(
719
+ { serviceAccountId: id('Service account ID'), tenantId: str('Tenant ID (optional)') },
720
+ 'serviceAccountId',
721
+ ))
722
+ .response(200, 'Role bound to service account', obj({ ok: bool() }, 'ok'))
723
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
724
+ .handler('bindRoleToServiceAccount')
725
+ .build() as IamEndpointsMap['bindRoleToServiceAccount'],
726
+
727
+ unbindRoleFromServiceAccount: endpoint('DELETE', '/iam/roles/:id/bind/service-account')
728
+ .summary('Unbind role from a service account')
729
+ .tags('IAM', 'Roles', 'Service Accounts')
730
+ .security('bearer')
731
+ .permission('fortress', 'unbindRole')
732
+ .params(obj({ id: id('Role ID') }, 'id'))
733
+ .body(obj(
734
+ { serviceAccountId: id('Service account ID'), tenantId: str('Tenant ID (optional)') },
735
+ 'serviceAccountId',
736
+ ))
737
+ .response(200, 'Role unbound', obj({ ok: bool() }, 'ok'))
738
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
739
+ .handler('unbindRoleFromServiceAccount')
740
+ .build() as IamEndpointsMap['unbindRoleFromServiceAccount'],
741
+
742
+ bindPermissionToServiceAccount: endpoint('POST', '/iam/permissions/bind/service-account')
743
+ .summary('Bind permission directly to a service account')
744
+ .tags('IAM', 'Permissions', 'Service Accounts')
745
+ .security('bearer')
746
+ .permission('fortress', 'managePermissions')
747
+ .body(obj(
748
+ {
749
+ serviceAccountId: id('Service account ID'),
750
+ permission: iamRef('PermissionInput'),
751
+ tenantId: str('Tenant ID (optional)'),
752
+ },
753
+ 'serviceAccountId',
754
+ 'permission',
755
+ ))
756
+ .response(200, 'Permission bound', obj({ ok: bool() }, 'ok'))
757
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
758
+ .handler('bindPermissionToServiceAccount')
759
+ .build() as IamEndpointsMap['bindPermissionToServiceAccount'],
760
+
761
+ unbindPermissionFromServiceAccount: endpoint('DELETE', '/iam/permissions/bind/service-account')
762
+ .summary('Unbind permission from a service account')
763
+ .tags('IAM', 'Permissions', 'Service Accounts')
764
+ .security('bearer')
765
+ .permission('fortress', 'managePermissions')
766
+ .body(obj(
767
+ {
768
+ serviceAccountId: id('Service account ID'),
769
+ permissionId: id('Permission ID'),
770
+ tenantId: str('Tenant ID (optional)'),
771
+ },
772
+ 'serviceAccountId',
773
+ 'permissionId',
774
+ ))
775
+ .response(200, 'Permission unbound', obj({ ok: bool() }, 'ok'))
776
+ .response(401, 'Not authenticated', authRef('ErrorResponse'))
777
+ .handler('unbindPermissionFromServiceAccount')
778
+ .build() as IamEndpointsMap['unbindPermissionFromServiceAccount'],
779
+ };