@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,227 @@
1
+ # Admin and operator recipes
2
+
3
+ Fortress is API-first by design — there is no reference UI shipped with
4
+ the library. The `admin` plugin (`@bajustone/fortress/plugins/admin`)
5
+ already exposes IAM CRUD as HTTP routes; this page collects the
6
+ **operator-workflow recipes** that combine those routes (and a few
7
+ service-layer helpers) into common admin tasks.
8
+
9
+ Drop these into your own admin app (Next.js / SvelteKit / Hono) or wire
10
+ them to a CLI. Every recipe is plain HTTP or `fortress.iam.*` /
11
+ `fortress.plugins.*` calls — no extra dependencies beyond what's
12
+ already installed.
13
+
14
+ ## Setup
15
+
16
+ Mount the admin plugin so the IAM endpoints exist:
17
+
18
+ ```ts
19
+ import { admin } from '@bajustone/fortress/plugins/admin';
20
+ import { apiKey } from '@bajustone/fortress/plugins/api-key';
21
+
22
+ createFortress({
23
+ // ...
24
+ plugins: [
25
+ admin({ apiKeyRoutes: true }), // also mounts admin api-key routes
26
+ apiKey({ prefix: 'fortress', routes: true }),
27
+ // ... rest of your plugins
28
+ ],
29
+ });
30
+ ```
31
+
32
+ Bootstrap the first admin user once:
33
+
34
+ ```sh
35
+ curl -X POST https://your-app/iam/admin/bootstrap \
36
+ -H 'Authorization: Bearer <a freshly-issued JWT for the user>' \
37
+ -H 'Content-Type: application/json' \
38
+ -d '{}'
39
+ ```
40
+
41
+ This creates the `fortress-admin` role with every `fortress:*`
42
+ permission and binds it to the calling user. Run once per deployment;
43
+ subsequent calls 409 unless the role is empty.
44
+
45
+ ## Users
46
+
47
+ Standard auth endpoints handle most user lifecycle:
48
+
49
+ | Task | API |
50
+ |---|---|
51
+ | Create user | `POST /auth/register` or `fortress.auth.createUser(...)` |
52
+ | Disable user | `fortress.auth.setUserActive(userId, false)` |
53
+ | Reset password (admin) | `fortress.auth.setUserPassword(userId, newPassword)` |
54
+ | Force email re-verification | clear `email_verified` row, call email-verification plugin's `send` method |
55
+ | Delete user | `fortress.auth.deleteUser(userId)` — cascades to sessions, role bindings, direct perms |
56
+
57
+ ## Roles, groups, permissions
58
+
59
+ `@bajustone/fortress/plugins/admin` routes:
60
+
61
+ ```
62
+ POST /iam/roles create role
63
+ DELETE /iam/roles/:id delete role
64
+ GET /iam/roles list roles
65
+ GET /iam/permissions list permissions
66
+ POST /iam/permissions/bind/user bind a permission to a user
67
+ POST /iam/permissions/bind/group bind to a group
68
+ POST /iam/permissions/bind/service-account bind to a service account
69
+ DELETE /iam/permissions/bind/* unbind variants
70
+ POST /iam/role-bindings/user bind a role to a user
71
+ POST /iam/role-bindings/group bind a role to a group
72
+ POST /iam/role-bindings/service-account bind a role to a service account
73
+ DELETE /iam/role-bindings/* unbind variants
74
+ POST /iam/groups create group
75
+ POST /iam/groups/:id/members add user to group
76
+ DELETE /iam/groups/:id/members/:userId remove user from group
77
+ ```
78
+
79
+ All require an authenticated user with the matching `fortress:*`
80
+ permission (e.g. `fortress:createRole`). The admin bootstrap binds them
81
+ to the first admin.
82
+
83
+ For declarative management (recommended for production), use
84
+ **policy-as-code** instead — see [docs/policy-as-code.md](./policy-as-code.md).
85
+
86
+ ## Sessions and revocation
87
+
88
+ ```ts
89
+ // List sessions for a user (callable from an admin route).
90
+ const sessions = await fortress.auth.listSessions(userId);
91
+
92
+ // Revoke a single session.
93
+ await fortress.auth.revokeSession(userId, sessionId);
94
+
95
+ // Revoke every refresh token for a user (forces logout everywhere on
96
+ // next access-token expiry; access tokens are stateless so they remain
97
+ // valid until they expire — keep TTL short).
98
+ await fortress.auth.revokeAllSessions(userId);
99
+ ```
100
+
101
+ To force-logout immediately even before access-token expiry, set the
102
+ user inactive (`setUserActive(userId, false)`); the auth pipeline
103
+ rejects requests whose JWT subject resolves to an inactive user.
104
+
105
+ ## Service accounts and API keys
106
+
107
+ Service accounts are first-class IAM subjects. They have no sessions —
108
+ they authenticate via API keys.
109
+
110
+ ```ts
111
+ // Create the service account.
112
+ const sa = await fortress.iam.createServiceAccount({
113
+ name: 'ci-bot',
114
+ displayName: 'CI Bot',
115
+ });
116
+
117
+ // Bind a role to it.
118
+ const role = (await fortress.iam.getRoles()).find(r => r.name === 'editor')!;
119
+ await fortress.iam.bindRoleToServiceAccount(sa.id, role.id);
120
+
121
+ // Mint an API key for the service account (admin plugin route, requires apiKey:manage).
122
+ const res = await fetch(`/admin/service-accounts/${sa.id}/api-keys`, {
123
+ method: 'POST',
124
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${adminJwt}` },
125
+ body: JSON.stringify({ name: 'ci-pipeline' }),
126
+ });
127
+ const { key } = await res.json(); // returned ONCE; store in secret manager
128
+ ```
129
+
130
+ API keys carry `subjectType: 'SERVICE_ACCOUNT'`; the resolver populates
131
+ the request principal so RBAC works the same as for users.
132
+
133
+ ## OAuth clients
134
+
135
+ OAuth clients have secrets; manage via the OAuth plugin's admin
136
+ methods (`fortress.plugins.oauth.createClient(...)`,
137
+ `updateClient(...)`, `deleteClient(...)`). Secrets are returned **once**
138
+ on create — re-issue with `rotateClientSecret(clientId)` and update RPs
139
+ out-of-band.
140
+
141
+ For policy-as-code: OAuth clients are intentionally NOT covered by the
142
+ policy file (secrets do not belong in a checked-in JSON file). Track
143
+ them in your secret manager and run create/update via a one-off script.
144
+
145
+ ## Audit logs
146
+
147
+ ```ts
148
+ // Read a page of audit log entries.
149
+ const page = await fortress.plugins['audit-log'].listEntries({
150
+ limit: 100,
151
+ offset: 0,
152
+ // optional filters:
153
+ eventType: 'LOGIN_FAILURE',
154
+ userId,
155
+ after: new Date(Date.now() - 7 * 24 * 3600 * 1000),
156
+ });
157
+
158
+ // Verify the hash chain (when hashChain: true was passed at plugin construction).
159
+ const verified = await fortress.plugins['audit-log'].verifyChain();
160
+ if (!verified.ok) {
161
+ console.error(`Chain broken at index ${verified.brokenAt}`);
162
+ }
163
+ ```
164
+
165
+ The verifier walks every row and recomputes the SHA-256 chain. Run as a
166
+ scheduled job; alert when `ok === false`.
167
+
168
+ ## Permission debugging — "why does user X have permission Y?"
169
+
170
+ Fortress ships `explainPermission` for this exact question:
171
+
172
+ ```ts
173
+ import { explainPermission } from '@bajustone/fortress';
174
+
175
+ const explain = await explainPermission(
176
+ fortress.config.database,
177
+ fortress.iam,
178
+ { type: 'USER', id: 42 },
179
+ 'article',
180
+ 'delete',
181
+ );
182
+
183
+ console.log(explain.allowed); // true | false
184
+ console.log(explain.sources); // every grant + its source (role / direct / group)
185
+ console.log(explain.roleBindings); // roles the user is bound to
186
+ console.log(explain.groupMemberships); // groups the user belongs to
187
+ ```
188
+
189
+ Each source carries enough context to point a human at the misconfig:
190
+
191
+ ```ts
192
+ explain.sources[0]
193
+ // {
194
+ // via: 'role',
195
+ // role: 'editor',
196
+ // group: { id: 7, name: 'eng' }, // when inherited via group
197
+ // permission: { id, resource, action, effect, conditions, description }
198
+ // }
199
+ ```
200
+
201
+ With `rbac.evaluationMode: 'deny-overrides'`, any matching `DENY` source
202
+ flips `allowed` to `false` regardless of matching `ALLOW` sources. In the
203
+ default `allow-only` mode, DENY entries are intentionally ignored. Wildcard permissions
204
+ (`resource: '*'`, `action: '*'`) match every check; you'll see them
205
+ listed as sources when they apply.
206
+
207
+ Wrap this in an admin route and you have a one-click "why does this
208
+ permission resolve the way it does" debugger.
209
+
210
+ ## Building an admin console UI
211
+
212
+ The library doesn't ship one. Wire the routes above to your framework
213
+ of choice; common starting points:
214
+
215
+ - **Hono + React/HTMX:** drop the existing admin plugin routes under
216
+ `/admin/*`, build a small SPA against them. The `mountFortress` call
217
+ already mounts every IAM route.
218
+ - **SvelteKit:** use the SvelteKit adapter; build admin pages under
219
+ `/routes/admin/+page.server.ts` that call `fortress.iam.*` directly.
220
+ No HTTP roundtrip needed.
221
+ - **CLI:** wrap the IAM methods in a thin commander/citty CLI; useful
222
+ for one-off ops and for environments without admin UI access.
223
+
224
+ In every case, gate the routes/pages with `permission('fortress', '...')`
225
+ declarations on the endpoint (`fortress` will RBAC-check before
226
+ dispatch) or `fortress.iam.checkPermission(subject, 'fortress', '...')`
227
+ in your handler.
@@ -0,0 +1,434 @@
1
+ # Architecture and plugin authoring
2
+
3
+ ## Request pipeline
4
+
5
+ Every framework adapter delegates Fortress-owned routes to:
6
+
7
+ ```typescript
8
+ const response = await fortress.handleRequest(request);
9
+ ```
10
+
11
+ The pipeline runs in this order:
12
+
13
+ 1. `before-auth` plugin middleware
14
+ 2. CSRF for unsafe cookie-authenticated requests
15
+ 3. route matching
16
+ 4. plugin principal resolvers, then JWT fallback
17
+ 5. `after-auth` plugin middleware
18
+ 6. endpoint permission check
19
+ 7. `after-rbac` plugin middleware
20
+ 8. body, query, and parameter validation
21
+ 9. endpoint dispatch
22
+ 10. auth-cookie serialization
23
+
24
+ Use the same pipeline without an HTTP server:
25
+
26
+ ```typescript
27
+ const result = await fortress.call.login({
28
+ identifier: 'alice@example.com',
29
+ password: 'correct-horse-battery-staple',
30
+ });
31
+ ```
32
+
33
+ Direct service calls skip HTTP middleware, CSRF, route permissions, and request schemas:
34
+
35
+ ```typescript
36
+ await fortress.auth.login(identifier, password);
37
+ await fortress.iam.checkPermission(subject, 'post', 'read');
38
+ ```
39
+
40
+ Auth and IAM service hooks and observers still run.
41
+
42
+ ## Instance composition
43
+
44
+ `createFortress()`:
45
+
46
+ 1. validates JWT keys and session options;
47
+ 2. wraps the database with observability;
48
+ 3. creates auth and IAM services;
49
+ 4. registers plugin methods, hooks, middleware, routes, and principal resolvers;
50
+ 5. merges core, plugin, and host endpoint metadata;
51
+ 6. builds `manifest`, `handleRequest`, and `call`.
52
+
53
+ ```typescript
54
+ const fortress = createFortress({ database, jwt: { key }, plugins });
55
+
56
+ fortress.auth;
57
+ fortress.iam;
58
+ fortress.plugins;
59
+ fortress.call;
60
+ fortress.endpoints;
61
+ fortress.manifest;
62
+ fortress.handleRequest;
63
+ fortress.migrate;
64
+ fortress.syncPermissionsFromManifest;
65
+ fortress.toOpenAPI;
66
+ ```
67
+
68
+ Startup rejects:
69
+
70
+ - JWT keys shorter than 32 UTF-8 bytes;
71
+ - non-positive `jwt.session` values;
72
+ - duplicate plugin names;
73
+ - duplicate routes or call keys between plugins;
74
+ - `security: ['none']` combined with a permission;
75
+ - use of the reserved `__host` plugin name when top-level `routes` are set.
76
+
77
+ A plugin may intentionally replace a core route. Two plugins cannot own the same route.
78
+
79
+ ## Endpoint metadata
80
+
81
+ An endpoint definition supplies request schemas, response schemas, security, IAM permission, OpenAPI metadata, and a handler name:
82
+
83
+ ```typescript
84
+ import { endpoint, obj, str } from '@bajustone/fortress';
85
+
86
+ const createPost = endpoint('POST', '/posts')
87
+ .summary('Create a post')
88
+ .security('bearer')
89
+ .permission('post', 'create')
90
+ .body(obj({ title: str() }, 'title'))
91
+ .response(201, 'Created', obj({ id: str() }, 'id'))
92
+ .handler('createPost')
93
+ .build();
94
+ ```
95
+
96
+ Register application-owned metadata at the top level:
97
+
98
+ ```typescript
99
+ const fortress = createFortress({
100
+ database,
101
+ jwt: { key },
102
+ routes: { createPost },
103
+ });
104
+ ```
105
+
106
+ Top-level routes appear in `fortress.endpoints`, the manifest, OpenAPI, and protection helpers. The host router still dispatches them. They do not create `fortress.call` methods.
107
+
108
+ To create a mounted and callable endpoint, define a plugin with matching `routes` and `methods` keys.
109
+
110
+ ## Write a route plugin
111
+
112
+ ```typescript
113
+ import {
114
+ endpoint,
115
+ obj,
116
+ str,
117
+ type FortressPlugin,
118
+ } from '@bajustone/fortress';
119
+
120
+ const greetingEndpoint = endpoint('POST', '/greetings')
121
+ .security('bearer')
122
+ .permission('greeting', 'create')
123
+ .body(obj({ name: str() }, 'name'))
124
+ .response(200, 'Greeting', obj({ message: str() }, 'message'))
125
+ .handler('createGreeting')
126
+ .build();
127
+
128
+ export function greetings() {
129
+ return {
130
+ name: 'greetings',
131
+ routes: { createGreeting: greetingEndpoint },
132
+ methods: () => ({
133
+ async createGreeting(input: { name: string }) {
134
+ return { message: `Hello ${input.name}` };
135
+ },
136
+ }),
137
+ } as const satisfies FortressPlugin;
138
+ }
139
+ ```
140
+
141
+ ```typescript
142
+ const fortress = createFortress({
143
+ database,
144
+ jwt: { key },
145
+ plugins: [greetings()] as const,
146
+ });
147
+
148
+ await fortress.call.createGreeting(
149
+ { name: 'Alice' },
150
+ { headers: { authorization: `Bearer ${token}` } },
151
+ );
152
+ ```
153
+
154
+ The route record key, endpoint handler, and method key must match.
155
+
156
+ ## Add lifecycle hooks
157
+
158
+ ```typescript
159
+ export function loginPolicy(): FortressPlugin {
160
+ return {
161
+ name: 'login-policy',
162
+ hooks: {
163
+ async beforeLogin(ctx) {
164
+ if (await isBlocked(ctx.email)) {
165
+ return {
166
+ stop: true,
167
+ response: { error: 'ACCOUNT_BLOCKED' },
168
+ };
169
+ }
170
+ },
171
+ async onLoginFailure(ctx) {
172
+ await recordFailure(ctx.identifier);
173
+ },
174
+ async afterLogin(ctx, result) {
175
+ await recordSuccess(result.user.id);
176
+ ctx.responseHeaders.set('X-Auth-Method', result.method);
177
+ return result;
178
+ },
179
+ },
180
+ };
181
+ }
182
+ ```
183
+
184
+ Hooks run in plugin registration order.
185
+
186
+ Use these before token issuance:
187
+
188
+ - `beforeLogin`
189
+ - `beforeRegister`
190
+ - `beforeTokenRefresh`
191
+ - `postAuthGate`
192
+
193
+ Use these for committed side effects:
194
+
195
+ - `afterLogin`
196
+ - `afterRegister`
197
+ - `afterTokenRefresh`
198
+ - `onLoginFailure`
199
+
200
+ A thrown `afterLogin`, `afterRegister`, or `afterTokenRefresh` hook is logged and skipped. It cannot roll back an issued session or created user. A plugin that must deny authentication belongs before token issuance.
201
+
202
+ ## Add a pending authentication gate
203
+
204
+ ```typescript
205
+ import { Errors, type FortressPlugin } from '@bajustone/fortress';
206
+
207
+ export function securityQuestion(): FortressPlugin {
208
+ return {
209
+ name: 'security-question',
210
+ hooks: {
211
+ postAuthGate: {
212
+ reason: 'two-factor',
213
+ maxAttempts: 5,
214
+ cooldownSeconds: 60,
215
+ async evaluate({ user }) {
216
+ if (await requiresQuestion(user.id))
217
+ return { pluginData: { prompt: 'First school?' } };
218
+ },
219
+ async verify({ user }, completion) {
220
+ if (!await checkAnswer(user.id, completion))
221
+ throw Errors.unauthorized('Invalid answer');
222
+ },
223
+ },
224
+ },
225
+ };
226
+ }
227
+ ```
228
+
229
+ A gate returns a pending auth result before tokens exist:
230
+
231
+ ```typescript
232
+ const result = await fortress.auth.login(email, password);
233
+
234
+ if (result.status === 'pending') {
235
+ const completed = await fortress.auth.completePendingAuth(
236
+ result.pending.continuationToken,
237
+ answer,
238
+ );
239
+ }
240
+ ```
241
+
242
+ Use an existing `PendingReason` or add a new reason to the core union before publishing a plugin.
243
+
244
+ ## Resolve a custom credential
245
+
246
+ Principal resolvers run in plugin order before JWT verification. Return `null` when the credential is absent so the next resolver can try.
247
+
248
+ ```typescript
249
+ export function signedRequest(): FortressPlugin {
250
+ return {
251
+ name: 'signed-request',
252
+ async resolvePrincipal(request) {
253
+ const signature = request.headers.get('X-Signature');
254
+ if (!signature)
255
+ return null;
256
+
257
+ const serviceAccountId = await verifySignature(signature, request);
258
+ return {
259
+ subject: { type: 'SERVICE_ACCOUNT', id: serviceAccountId },
260
+ scopes: ['orders:write'],
261
+ };
262
+ },
263
+ };
264
+ }
265
+ ```
266
+
267
+ Scopes narrow the subject's IAM permissions. They never grant a permission the subject does not already have.
268
+
269
+ ## Add request middleware
270
+
271
+ ```typescript
272
+ export function requestAudit(): FortressPlugin {
273
+ return {
274
+ name: 'request-audit',
275
+ middleware: [{
276
+ path: '/orders/*',
277
+ position: 'after-rbac',
278
+ async handler(_plugin, context, next) {
279
+ await audit(
280
+ context.fortressSubject,
281
+ new URL(context.request.url).pathname,
282
+ );
283
+ await next();
284
+ },
285
+ }],
286
+ };
287
+ }
288
+ ```
289
+
290
+ Positions:
291
+
292
+ | Position | Identity | Permission result | Use |
293
+ |---|---:|---:|---|
294
+ | `before-auth` | no | no | rate limits, request normalization |
295
+ | `after-auth` | yes | no | account state, identity-aware controls |
296
+ | `after-rbac` | yes | yes | authorized audit and request context |
297
+
298
+ ## Extend JWT claims
299
+
300
+ ```typescript
301
+ export function tenantClaims(): FortressPlugin {
302
+ return {
303
+ name: 'tenant-claims',
304
+ async enrichTokenClaims(userId, { db }) {
305
+ const membership = await findMembership(db, userId);
306
+ return {
307
+ tenantId: membership.id,
308
+ tenantCode: membership.code,
309
+ };
310
+ },
311
+ };
312
+ }
313
+ ```
314
+
315
+ Claims are shallow-merged in plugin order. Later plugins overwrite duplicate keys and emit a development warning.
316
+
317
+ ## Scope database access
318
+
319
+ `scopeRules` adds model filters. `wrapAdapter` replaces the request-scoped adapter when a plugin needs transaction or schema behavior.
320
+
321
+ ```typescript
322
+ export function organizationScope(): FortressPlugin {
323
+ return {
324
+ name: 'organization-scope',
325
+ async scopeRules(userId, model) {
326
+ if (model !== 'post')
327
+ return null;
328
+
329
+ const organizationId = await organizationFor(userId);
330
+ return {
331
+ filters: [{ field: 'organizationId', operator: '=', value: organizationId }],
332
+ defaults: { organizationId },
333
+ };
334
+ },
335
+ };
336
+ }
337
+ ```
338
+
339
+ Read the scoped adapter from framework context:
340
+
341
+ ```typescript
342
+ const db = await getScopedDb(c, 'post');
343
+ const posts = await db.findMany({ model: 'post' });
344
+ ```
345
+
346
+ Use the bundled [data-isolation plugin](plugins/data-isolation.md) for row-level filtering and [tenancy plugin](plugins/tenancy.md) for PostgreSQL schema isolation.
347
+
348
+ ## Database contract
349
+
350
+ Plugins use the same adapter as core:
351
+
352
+ ```typescript
353
+ interface DatabaseAdapter {
354
+ create(args): Promise<unknown>;
355
+ findOne(args): Promise<unknown | null>;
356
+ findMany(args): Promise<unknown[]>;
357
+ update(args): Promise<unknown | null>;
358
+ delete(args): Promise<void>;
359
+ count(args): Promise<number>;
360
+ transaction<T>(fn: (tx: DatabaseAdapter) => Promise<T>): Promise<T>;
361
+ rawQuery?<T>(sql: string, params?: unknown[]): Promise<T[]>;
362
+ dialect?: 'sqlite' | 'pg';
363
+ }
364
+ ```
365
+
366
+ Portable `rawQuery` SQL uses `?` placeholders. The adapter translates them for its driver.
367
+
368
+ Core operators:
369
+
370
+ ```typescript
371
+ '=' | '!=' | 'in' | 'gt' | 'lt' | 'gte' | 'lte' | 'isNull'
372
+ ```
373
+
374
+ Adapters may support additional operators. The Drizzle adapter also supports `like`.
375
+
376
+ ## Auth data flow
377
+
378
+ ### Login
379
+
380
+ 1. Run `beforeLogin` hooks.
381
+ 2. Resolve the login identifier.
382
+ 3. verify user state and password with timing-safe miss behavior.
383
+ 4. run post-auth gates.
384
+ 5. enrich claims.
385
+ 6. create the access token and refresh-token family.
386
+ 7. run failure-contained after hooks.
387
+ 8. emit auth observers.
388
+
389
+ ### Refresh
390
+
391
+ 1. hash and find the refresh token;
392
+ 2. reject expiry, inactivity, absolute lifetime, or fingerprint policy failures;
393
+ 3. detect reuse and revoke the family;
394
+ 4. rotate the token in the same family;
395
+ 5. refresh claims and session metadata;
396
+ 6. run failure-contained after hooks;
397
+ 7. return the new pair.
398
+
399
+ The database stores refresh-token hashes, not raw tokens.
400
+
401
+ ## IAM data flow
402
+
403
+ ```typescript
404
+ await fortress.iam.checkPermission(subject, resource, action, context);
405
+ ```
406
+
407
+ The check:
408
+
409
+ 1. verifies that a user subject still exists and is active;
410
+ 2. loads global permissions from the optional cache or tenant permissions from the database;
411
+ 3. collects role, group, and direct grants;
412
+ 4. narrows with credential scopes;
413
+ 5. evaluates resource/action wildcards and conditions;
414
+ 6. applies `allow-only` or `deny-overrides`;
415
+ 7. emits a synchronous permission-check event.
416
+
417
+ Tenant checks bypass the global permission cache. User activation is checked before cached grants, so deactivation denies immediately.
418
+
419
+ ## Package boundaries
420
+
421
+ | Entry point | Purpose |
422
+ |---|---|
423
+ | `@bajustone/fortress` | core, schemas, endpoints, policy, migrations |
424
+ | `@bajustone/fortress/hono` | Hono mount, middleware, validation, protection |
425
+ | `@bajustone/fortress/express` | Express mount, middleware, validation, protection |
426
+ | `@bajustone/fortress/sveltekit` | handle hook, actions, locals, validation, protection |
427
+ | `@bajustone/fortress/drizzle` | adapter, SQLite/PG schemas, DB error mapping |
428
+ | `@bajustone/fortress/drizzle/pg` | PostgreSQL schema only |
429
+ | `@bajustone/fortress/testing` | in-memory adapter and drift checks |
430
+ | `@bajustone/fortress/otel` | OpenTelemetry provider adapter |
431
+ | `@bajustone/fortress/fetcher` | outbound client and schema tooling |
432
+ | `@bajustone/fortress/plugins/*` | optional plugins |
433
+
434
+ Core does not import framework adapters. Framework adapters only translate their request/response APIs to the web-standard core.