@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
package/README.md ADDED
@@ -0,0 +1,760 @@
1
+ # Fortress
2
+
3
+ Authentication, sessions, IAM, and security plugins for TypeScript.
4
+
5
+ - Web-standard core: `Request → Response`
6
+ - Hono, Express, and SvelteKit adapters
7
+ - PostgreSQL and SQLite through Drizzle
8
+ - JWT access tokens and rotating refresh-token families
9
+ - Roles, groups, direct permissions, conditions, and service accounts
10
+ - Typed endpoint schemas, in-process calls, OpenAPI, migrations, and CI checks
11
+ - 15 optional plugins
12
+
13
+ Fortress runs on Bun, Deno, Node.js 20.19+, and edge runtimes. File-based helpers and the Bun CLI require a filesystem runtime.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ # npm
19
+ npx jsr add @bajustone/fortress
20
+
21
+ # Bun
22
+ bunx jsr add @bajustone/fortress
23
+
24
+ # Deno
25
+ deno add jsr:@bajustone/fortress
26
+ ```
27
+
28
+ Install the integrations you use:
29
+
30
+ ```bash
31
+ bun add drizzle-orm hono # Hono + Drizzle
32
+ bun add drizzle-orm express # Express + Drizzle
33
+ bun add drizzle-orm @sveltejs/kit # SvelteKit + Drizzle
34
+ ```
35
+
36
+ ## Start with Hono and SQLite
37
+
38
+ ```typescript
39
+ import { Hono } from 'hono';
40
+ import { drizzle } from 'drizzle-orm/bun-sqlite';
41
+ import { createFortress } from '@bajustone/fortress';
42
+ import { createDrizzleAdapter } from '@bajustone/fortress/drizzle';
43
+ import { mountFortress } from '@bajustone/fortress/hono';
44
+
45
+ const fortress = createFortress({
46
+ database: createDrizzleAdapter(drizzle('app.db')),
47
+ jwt: {
48
+ key: process.env.FORTRESS_JWT_SECRET!, // at least 32 UTF-8 bytes
49
+ issuer: 'my-app',
50
+ },
51
+ // Required only for plain-HTTP local development.
52
+ cookies: { secure: false },
53
+ });
54
+
55
+ await fortress.migrate();
56
+
57
+ const app = new Hono();
58
+ mountFortress(app, fortress);
59
+
60
+ export default app;
61
+ ```
62
+
63
+ `mountFortress()` serves core auth, IAM, and registered plugin routes. Login and refresh responses set access and refresh cookies.
64
+
65
+ Generate a key:
66
+
67
+ ```bash
68
+ fortress generate-secret
69
+ ```
70
+
71
+ The `fortress` CLI uses Bun. Run database-bound operations from application code with your configured adapter.
72
+
73
+ ## Use the service API
74
+
75
+ Direct service calls are useful in jobs, tests, scripts, and application services. They run auth/IAM hooks and observers but skip the HTTP middleware, CSRF, route RBAC, and request-validation pipeline.
76
+
77
+ ### Register and sign in
78
+
79
+ ```typescript
80
+ const user = await fortress.auth.createUser({
81
+ email: 'alice@example.com',
82
+ name: 'Alice',
83
+ password: 'correct-horse-battery-staple',
84
+ });
85
+
86
+ const result = await fortress.auth.login(
87
+ 'alice@example.com',
88
+ 'correct-horse-battery-staple',
89
+ {
90
+ ipAddress: requestIp,
91
+ userAgent: request.headers.get('user-agent') ?? undefined,
92
+ deviceName: 'Alice laptop',
93
+ },
94
+ );
95
+
96
+ if (result.status === 'success') {
97
+ result.user;
98
+ result.accessToken;
99
+ result.refreshToken;
100
+ }
101
+
102
+ if (result.status === 'pending') {
103
+ // Complete the factor through the plugin that requested it. Two-factor:
104
+ const completed = await fortress.plugins['two-factor'].verify(
105
+ result.pending.continuationToken,
106
+ code,
107
+ );
108
+ }
109
+ ```
110
+
111
+ `AuthResult` has three variants:
112
+
113
+ ```typescript
114
+ switch (result.status) {
115
+ case 'success':
116
+ result.refreshToken; // string
117
+ break;
118
+ case 'pending':
119
+ result.pending.reason;
120
+ result.pending.continuationToken;
121
+ break;
122
+ case 'impersonation':
123
+ result.refreshToken; // null
124
+ break;
125
+ }
126
+ ```
127
+
128
+ ### Rotate and revoke sessions
129
+
130
+ ```typescript
131
+ const next = await fortress.auth.refresh(refreshToken, {
132
+ ipAddress: requestIp,
133
+ userAgent: request.headers.get('user-agent') ?? undefined,
134
+ });
135
+
136
+ await fortress.auth.logout(next.refreshToken);
137
+
138
+ const sessions = await fortress.auth.listSessions(user.id);
139
+ await fortress.auth.revokeSession(user.id, sessions[0].id);
140
+ await fortress.auth.revokeAllOtherSessions(user.id, currentSessionId);
141
+ ```
142
+
143
+ Every refresh rotates the token. Reuse of a revoked token invalidates its family. Configure retry grace, inactivity limits, absolute lifetime, and per-user caps under `jwt.session`.
144
+
145
+ Add phone or username sign-in without replacing the primary email:
146
+
147
+ ```typescript
148
+ await fortress.auth.addLoginIdentifier(user.id, 'phone', '+15551234567');
149
+ await fortress.auth.addLoginIdentifier(user.id, 'username', 'alice');
150
+ await fortress.auth.login('alice', password);
151
+ ```
152
+
153
+ Issue a short-lived, access-only impersonation token. The caller must hold `fortress:impersonate`:
154
+
155
+ ```typescript
156
+ const impersonation = await fortress.auth.impersonate(admin.id, user.id, {
157
+ reason: 'Support case 1234',
158
+ expirySeconds: 5 * 60,
159
+ });
160
+ // impersonation.status === 'impersonation'
161
+ // JWT `sub` is user.id; `act.sub` is admin.id; refreshToken is null.
162
+ ```
163
+
164
+ ### Grant and check permissions
165
+
166
+ ```typescript
167
+ const editor = await fortress.iam.createRole('editor', [
168
+ { resource: 'post', action: 'read' },
169
+ { resource: 'post', action: 'update' },
170
+ ]);
171
+
172
+ await fortress.iam.bindRoleToUser(user.id, editor.id);
173
+
174
+ const allowed = await fortress.iam.checkPermission(
175
+ { type: 'USER', id: user.id },
176
+ 'post',
177
+ 'update',
178
+ { resource: { ownerId: user.id } },
179
+ );
180
+ ```
181
+
182
+ Permissions can come from roles, groups, or direct bindings:
183
+
184
+ ```typescript
185
+ const team = await fortress.iam.createGroup('engineering');
186
+ await fortress.iam.addUserToGroup(team.id, user.id);
187
+ await fortress.iam.bindRoleToGroup(team.id, editor.id);
188
+
189
+ await fortress.iam.bindPermissionToUser(user.id, {
190
+ resource: 'report',
191
+ action: 'export',
192
+ });
193
+ ```
194
+
195
+ Use `deny-overrides` to enforce explicit denies:
196
+
197
+ ```typescript
198
+ const fortress = createFortress({
199
+ database,
200
+ jwt: { key },
201
+ rbac: { evaluationMode: 'deny-overrides' },
202
+ });
203
+ ```
204
+
205
+ Add ABAC conditions to permissions:
206
+
207
+ ```typescript
208
+ await fortress.iam.createRole('owner', [{
209
+ resource: 'post',
210
+ action: 'update',
211
+ conditions: [{
212
+ field: 'resource.ownerId',
213
+ operator: 'eq',
214
+ value: { ref: 'user.id' },
215
+ }],
216
+ }]);
217
+ ```
218
+
219
+ Credential scopes only narrow IAM access. An empty `credentialScopes` array denies every permission:
220
+
221
+ ```typescript
222
+ await fortress.iam.checkPermission(subject, 'post', 'read', {
223
+ credentialScopes: ['post:read'],
224
+ });
225
+ ```
226
+
227
+ ### Use service accounts
228
+
229
+ ```typescript
230
+ const ci = await fortress.iam.createServiceAccount({
231
+ name: 'ci-deploy',
232
+ displayName: 'CI deployer',
233
+ });
234
+
235
+ await fortress.iam.bindRoleToServiceAccount(ci.id, editor.id);
236
+ ```
237
+
238
+ Register the API-key plugin to authenticate the account:
239
+
240
+ ```typescript
241
+ import { apiKey } from '@bajustone/fortress/plugins/api-key';
242
+
243
+ const fortress = createFortress({
244
+ database,
245
+ jwt: { key },
246
+ plugins: [apiKey()] as const,
247
+ });
248
+
249
+ const credential = await fortress.plugins['api-key'].createKey({
250
+ subject: { type: 'SERVICE_ACCOUNT', id: ci.id },
251
+ name: 'production',
252
+ scopes: ['post:read'],
253
+ });
254
+
255
+ console.log(credential.key); // returned once; only its hash is stored
256
+ ```
257
+
258
+ Clients send `Authorization: ApiKey <key>` or `X-API-Key: <key>`.
259
+
260
+ ## Call routes in process
261
+
262
+ `fortress.call` infers request and success-response types from endpoint definitions. It enters the same HTTP pipeline as a network request.
263
+
264
+ ```typescript
265
+ const login = await fortress.call.login({
266
+ identifier: 'alice@example.com',
267
+ password: 'correct-horse-battery-staple',
268
+ });
269
+
270
+ await fortress.call.revokeSession(
271
+ { id: sessionId },
272
+ { headers: { authorization: `Bearer ${login.accessToken}` } },
273
+ );
274
+ ```
275
+
276
+ Non-2xx responses throw `FortressError`:
277
+
278
+ ```typescript
279
+ import { FortressError } from '@bajustone/fortress';
280
+
281
+ try {
282
+ await fortress.call.login({ identifier: 'alice@example.com', password: 'wrong' });
283
+ }
284
+ catch (error) {
285
+ if (error instanceof FortressError && error.code === 'UNAUTHORIZED') {
286
+ // Return your application's login error.
287
+ }
288
+ }
289
+ ```
290
+
291
+ ## Mount a framework adapter
292
+
293
+ ### Hono
294
+
295
+ ```typescript
296
+ import { Hono } from 'hono';
297
+ import {
298
+ createHonoMiddleware,
299
+ getSubject,
300
+ mountFortress,
301
+ } from '@bajustone/fortress/hono';
302
+
303
+ const app = new Hono();
304
+ mountFortress(app, fortress, { prefix: '/api' });
305
+
306
+ const { authMiddleware, rbacMiddleware, errorHandler } = createHonoMiddleware(fortress, {
307
+ routeMap: {
308
+ 'GET /posts': { resource: 'post', action: 'read' },
309
+ 'POST /posts': { resource: 'post', action: 'create' },
310
+ },
311
+ defaultDeny: true,
312
+ skipPaths: ['/health'],
313
+ });
314
+
315
+ app.onError(errorHandler);
316
+ app.use('/posts/*', authMiddleware, rbacMiddleware);
317
+ app.get('/posts', (c) => c.json({ subject: getSubject(c) }));
318
+ ```
319
+
320
+ Set HSTS, CSP, frame, content-type, referrer, and cross-domain-policy headers:
321
+
322
+ ```typescript
323
+ import { createSecurityHeadersMiddleware } from '@bajustone/fortress/hono';
324
+
325
+ app.use('*', createSecurityHeadersMiddleware());
326
+ ```
327
+
328
+ ### Express
329
+
330
+ ```typescript
331
+ import express from 'express';
332
+ import {
333
+ createExpressMiddleware,
334
+ getSubject,
335
+ mountFortress,
336
+ } from '@bajustone/fortress/express';
337
+
338
+ const app = express();
339
+ app.use(express.json());
340
+ app.use(express.urlencoded({ extended: false })); // OAuth form endpoints
341
+ mountFortress(app, fortress, { prefix: '/api' });
342
+
343
+ const { authMiddleware, rbacMiddleware, errorHandler } = createExpressMiddleware(fortress, {
344
+ routeMap: { 'GET /posts': { resource: 'post', action: 'read' } },
345
+ unmappedRoutes: 'deny',
346
+ });
347
+
348
+ app.use('/posts', authMiddleware, rbacMiddleware);
349
+ app.get('/posts', (req, res) => res.json({ subject: getSubject(req) }));
350
+ app.use(errorHandler);
351
+ ```
352
+
353
+ ### SvelteKit
354
+
355
+ ```typescript
356
+ // src/hooks.server.ts
357
+ import { createSvelteKitHandle } from '@bajustone/fortress/sveltekit';
358
+ import { fortress } from '$lib/server/fortress';
359
+
360
+ export const handle = createSvelteKitHandle(fortress, { basePath: '/api' });
361
+ ```
362
+
363
+ ```typescript
364
+ // src/app.d.ts
365
+ import type { FortressLocals } from '@bajustone/fortress/sveltekit';
366
+
367
+ declare global {
368
+ namespace App {
369
+ interface Locals extends FortressLocals {}
370
+ }
371
+ }
372
+
373
+ export {};
374
+ ```
375
+
376
+ ```typescript
377
+ // src/routes/login/+page.server.ts
378
+ import { fortressActions } from '@bajustone/fortress/sveltekit';
379
+ import { fortress } from '$lib/server/fortress';
380
+
381
+ export const actions = {
382
+ default: fortressActions.login(fortress, { redirectTo: '/dashboard' }),
383
+ };
384
+ ```
385
+
386
+ The handle hook mounts Fortress routes, resolves `event.locals.fortress`, and refreshes expired access cookies on safe requests.
387
+
388
+ ### Any `Request`/`Response` runtime
389
+
390
+ ```typescript
391
+ export default {
392
+ fetch(request: Request) {
393
+ return fortress.handleRequest(request);
394
+ },
395
+ };
396
+ ```
397
+
398
+ ## Protect application-owned routes
399
+
400
+ Define route metadata once, then use the adapter-specific `protectedRoute()` wrapper. The metadata drives authentication, CSRF, RBAC, validation, the route manifest, and OpenAPI.
401
+
402
+ ```typescript
403
+ import { endpoint, obj, str } from '@bajustone/fortress';
404
+ import { protectedRoute } from '@bajustone/fortress/hono';
405
+
406
+ const createPost = endpoint('POST', '/posts')
407
+ .security('bearer')
408
+ .permission('post', 'create')
409
+ .body(obj({ title: str({ min: 1 }) }, 'title'))
410
+ .response(201, 'Created', obj({ id: str() }, 'id'))
411
+ .handler('createPost')
412
+ .build();
413
+
414
+ const fortress = createFortress({
415
+ database,
416
+ jwt: { key },
417
+ routes: { createPost }, // manifest/OpenAPI metadata; host router owns dispatch
418
+ });
419
+
420
+ app.post('/posts', protectedRoute(fortress, createPost, async (_c, ctx) => {
421
+ const post = await savePost(ctx.body.title, ctx.subject!);
422
+ return ctx.respond(201, { id: post.id });
423
+ }));
424
+ ```
425
+
426
+ See [Host-owned routes](docs/host-owned-routes.md).
427
+
428
+ ## Validate custom handlers
429
+
430
+ Fortress schemas provide JSON Schema, Standard Schema V1 validation, and inferred TypeScript types:
431
+
432
+ ```typescript
433
+ import { email, int, obj, strict, type Infer } from '@bajustone/fortress';
434
+
435
+ const CreateUser = strict(obj({
436
+ email: email(),
437
+ age: int({ min: 18 }),
438
+ }, 'email', 'age'));
439
+
440
+ type CreateUser = Infer<typeof CreateUser>;
441
+ ```
442
+
443
+ Use `vBody`, `vParam`, and `vQuery` in framework-owned handlers:
444
+
445
+ ```typescript
446
+ import { vBody } from '@bajustone/fortress/hono';
447
+
448
+ app.post('/users', async (c) => {
449
+ const input = await vBody(c, CreateUser);
450
+ return c.json(await createUser(input), 201);
451
+ });
452
+ ```
453
+
454
+ For other runtimes, validate all endpoint inputs at once:
455
+
456
+ ```typescript
457
+ import { validateRequest } from '@bajustone/fortress';
458
+
459
+ await validateRequest(endpoint.input, {
460
+ body: await request.json(),
461
+ query: Object.fromEntries(new URL(request.url).searchParams),
462
+ params,
463
+ });
464
+
465
+ // Continue with the original data after validation.
466
+ ```
467
+
468
+ `endpoint()` accepts any Standard Schema V1 implementation, including Zod, Valibot, and ArkType. Fetcher's richer schema builder is available at `@bajustone/fortress/fetcher`.
469
+
470
+ ## Configure Fortress
471
+
472
+ ```typescript
473
+ const fortress = createFortress({
474
+ database,
475
+ jwt: {
476
+ key: [currentSecret, previousSecret], // first signs; all verify
477
+ issuer: 'my-app',
478
+ audience: 'my-api',
479
+ accessTokenExpirySeconds: 15 * 60,
480
+ refreshTokenExpirySeconds: 7 * 24 * 60 * 60,
481
+ validateRefreshFingerprint: 'warn', // false | 'warn' | true
482
+ session: {
483
+ refreshGraceSeconds: 5,
484
+ idleTimeoutSeconds: 7 * 24 * 60 * 60,
485
+ absoluteTimeoutSeconds: 30 * 24 * 60 * 60,
486
+ maxSessionsPerUser: 10,
487
+ },
488
+ },
489
+ rbac: {
490
+ evaluationMode: 'deny-overrides',
491
+ resourceFile: './fortress.resources.json',
492
+ cache: { ttlSeconds: 30, maxEntries: 1000 }, // opt in
493
+ },
494
+ passwordPolicy: {
495
+ minLength: 15,
496
+ maxLength: 128,
497
+ checkBreached: true,
498
+ breachedFailureMode: 'open',
499
+ },
500
+ impersonation: { maxTtlSeconds: 15 * 60 },
501
+ cookies: {
502
+ secure: true,
503
+ sameSite: 'lax',
504
+ path: '/',
505
+ },
506
+ csrf: {
507
+ enabled: true,
508
+ skipPaths: ['/webhooks/incoming'],
509
+ },
510
+ plugins: [],
511
+ routes: {},
512
+ logger,
513
+ observability,
514
+ });
515
+ ```
516
+
517
+ Defaults:
518
+
519
+ - JWT issuer: `fortress`
520
+ - Access token: 15 minutes
521
+ - Refresh token: 7 days
522
+ - Permission evaluation: `allow-only`
523
+ - Permission cache: disabled until `rbac.cache` is supplied
524
+ - Password length: 15–128 characters; breach lookup disabled
525
+ - Cookies: `Secure`, `HttpOnly`, `SameSite=Lax`, `Path=/`, with `__Host-` names when possible
526
+ - CSRF: enabled for unsafe browser requests; cookie-authenticated requests require `X-Fortress-CSRF`
527
+ - Logger and telemetry: silent/no-op
528
+
529
+ Set `cookies: { secure: false }` only for plain-HTTP local development. `SameSite=None` requires `Secure`.
530
+
531
+ ### Replace password hashing
532
+
533
+ The default hasher is Argon2id through WASM. Supply a native implementation when needed:
534
+
535
+ ```typescript
536
+ import type { PasswordHasher } from '@bajustone/fortress';
537
+
538
+ const passwordHasher: PasswordHasher = {
539
+ hash: password => Bun.password.hash(password, { algorithm: 'argon2id' }),
540
+ verify: (hash, password) => Bun.password.verify(password, hash),
541
+ };
542
+ ```
543
+
544
+ ## Add plugins
545
+
546
+ ```typescript
547
+ import { accountLockout } from '@bajustone/fortress/plugins/account-lockout';
548
+ import { auditLog } from '@bajustone/fortress/plugins/audit-log';
549
+ import { rateLimit } from '@bajustone/fortress/plugins/rate-limit';
550
+
551
+ const fortress = createFortress({
552
+ database,
553
+ jwt: { key },
554
+ plugins: [
555
+ rateLimit(),
556
+ accountLockout(),
557
+ auditLog({ hashChain: true }),
558
+ ] as const,
559
+ });
560
+
561
+ await fortress.plugins['account-lockout'].resetLockout('alice@example.com');
562
+ const events = await fortress.plugins['audit-log'].getAuditLog({ userId: user.id });
563
+ ```
564
+
565
+ Hooks run in registration order. Put rejection/gate plugins before side-effect plugins. A failing `afterLogin`, `afterRegister`, or `afterTokenRefresh` side-effect is logged and does not invalidate an already committed auth result. Use `beforeLogin` or `postAuthGate` to block token issuance.
566
+
567
+ | Need | Plugin | Guide |
568
+ |---|---|---|
569
+ | Protected admin CRUD and first-admin bootstrap | `admin` | [Admin](docs/plugins/admin.md) |
570
+ | API keys for users and service accounts | `api-key` | [API key](docs/plugins/api-key.md) |
571
+ | Progressive login lockouts | `account-lockout` | [Account lockout](docs/plugins/account-lockout.md) |
572
+ | Sliding-window limits for auth and app routes | `rate-limit` | [Rate limit](docs/plugins/rate-limit.md) |
573
+ | Verify email before issuing a session | `email-verification` | [Email verification](docs/plugins/email-verification.md) |
574
+ | TOTP, backup codes, and trusted devices | `two-factor` | [Two-factor](docs/plugins/two-factor.md) |
575
+ | Passkeys or WebAuthn as a second factor | `webauthn` | [WebAuthn](docs/plugins/webauthn.md) |
576
+ | Passwordless email login | `magic-link` | [Magic link](docs/plugins/magic-link.md) |
577
+ | Google, GitHub, Apple, Microsoft, Discord, or OIDC login | `social-login` | [Social login](docs/plugins/social-login.md) |
578
+ | OAuth 2.0/OIDC authorization server | `oauth` | [OAuth server](docs/plugins/oauth.md) |
579
+ | PostgreSQL schema-per-tenant isolation | `tenancy` | [Tenancy](docs/plugins/tenancy.md) |
580
+ | Database-independent row-level scoping | `data-isolation` | [Data isolation](docs/plugins/data-isolation.md) |
581
+ | Queryable or hash-chained audit events | `audit-log` | [Audit log](docs/plugins/audit-log.md) |
582
+ | Signed, queued outbound events | `webhook` | [Webhook](docs/plugins/webhook.md) |
583
+ | OpenAPI 3.1 JSON and Scalar UI | `openapi` | [OpenAPI](docs/plugins/openapi.md) |
584
+
585
+ ## Migrate and bootstrap
586
+
587
+ ```typescript
588
+ await fortress.migrate({
589
+ migrateApp: () => migrate(appDb, { migrationsFolder: './drizzle' }),
590
+ });
591
+
592
+ await fortress.syncPermissionsFromManifest({
593
+ defaultRoles: {
594
+ admin: '*',
595
+ member: ['post:read'],
596
+ },
597
+ });
598
+ ```
599
+
600
+ Fortress migrations run before `migrateApp`. Permission sync is idempotent and only adds missing manifest permissions and bindings.
601
+
602
+ Check live schema drift:
603
+
604
+ ```typescript
605
+ import { detectMigrationDrift, hasMigrationDrift } from '@bajustone/fortress';
606
+
607
+ const drift = await detectMigrationDrift(fortress.config.database);
608
+ if (hasMigrationDrift(drift))
609
+ throw new Error(JSON.stringify(drift));
610
+ ```
611
+
612
+ See the [migration upgrade guide](docs/migrations/upgrade-guide.md).
613
+
614
+ ## Use PostgreSQL
615
+
616
+ ```typescript
617
+ import { drizzle } from 'drizzle-orm/node-postgres';
618
+ import { createDrizzleAdapter } from '@bajustone/fortress/drizzle';
619
+ import { fortressPgSchema } from '@bajustone/fortress/drizzle/pg';
620
+
621
+ const drizzleDb = drizzle(connectionString, { schema: fortressPgSchema });
622
+ const database = createDrizzleAdapter(drizzleDb, { dialect: 'pg' });
623
+ ```
624
+
625
+ `@bajustone/fortress/drizzle/pg` exports the schema. Import `createDrizzleAdapter` from `@bajustone/fortress/drizzle`.
626
+
627
+ To use another datastore, implement `DatabaseAdapter`:
628
+
629
+ ```typescript
630
+ import type { DatabaseAdapter } from '@bajustone/fortress';
631
+
632
+ const database: DatabaseAdapter = {
633
+ create: async ({ model, data }) => insert(model, data),
634
+ findOne: async ({ model, where }) => findOne(model, where),
635
+ findMany: async ({ model, where, limit, offset, sortBy }) => findMany(model, { where, limit, offset, sortBy }),
636
+ update: async ({ model, where, data }) => update(model, where, data),
637
+ delete: async ({ model, where }) => remove(model, where),
638
+ count: async ({ model, where }) => count(model, where),
639
+ transaction: async fn => runTransaction(fn),
640
+ rawQuery: async (sql, params) => query(sql, params), // optional
641
+ dialect: 'pg', // optional
642
+ };
643
+ ```
644
+
645
+ ## Generate OpenAPI
646
+
647
+ ```typescript
648
+ const spec = fortress.toOpenAPI({
649
+ title: 'My API',
650
+ version: '1.0.0',
651
+ servers: [{ url: 'https://api.example.com' }],
652
+ });
653
+
654
+ await Bun.write('openapi.json', JSON.stringify(spec, null, 2));
655
+ ```
656
+
657
+ Or mount the plugin:
658
+
659
+ ```typescript
660
+ import { openapi } from '@bajustone/fortress/plugins/openapi';
661
+
662
+ const fortress = createFortress({
663
+ database,
664
+ jwt: { key },
665
+ plugins: [openapi({ title: 'My API', version: '1.0.0' })] as const,
666
+ });
667
+ // GET /openapi.json
668
+ // GET /openapi
669
+ ```
670
+
671
+ ## Test and check drift
672
+
673
+ ```bash
674
+ # Node tests need the optional SQLite driver.
675
+ npm install --save-dev better-sqlite3
676
+ ```
677
+
678
+ ```typescript
679
+ import { createFortress } from '@bajustone/fortress';
680
+ import { createTestAdapter, runFortressChecks } from '@bajustone/fortress/testing';
681
+
682
+ const fortress = createFortress({
683
+ database: createTestAdapter(),
684
+ jwt: { key: 'test-secret-that-is-at-least-32-bytes' },
685
+ plugins,
686
+ });
687
+
688
+ const result = await runFortressChecks({ fortress });
689
+ if (!result.ok)
690
+ throw new Error(result.messages.join('\n'));
691
+ ```
692
+
693
+ The testing entrypoint also exports `smokeTestAuth`, `checkMigrationDrift`, `checkRouteManifestDrift`, and `checkPublicRoutes`. See [CI checks](docs/ci.md).
694
+
695
+ ## Observe auth and IAM
696
+
697
+ ```typescript
698
+ const stopAuth = fortress.auth.addAuthObserver(async (event) => {
699
+ await securityEvents.write(event);
700
+ });
701
+
702
+ const stopIam = fortress.iam.addIamObserver(async (event) => {
703
+ await audit.write(event);
704
+ });
705
+
706
+ const stopChecks = fortress.iam.addPermissionCheckObserver((event) => {
707
+ permissionLatency.record(event.durationSeconds);
708
+ });
709
+ ```
710
+
711
+ Auth and IAM observers may return promises; failures are logged and contained. Permission-check observers are synchronous and run on the authorization hot path.
712
+
713
+ Use OpenTelemetry through the optional entrypoint:
714
+
715
+ ```typescript
716
+ import { createOtelTelemetry } from '@bajustone/fortress/otel';
717
+
718
+ const observability = await createOtelTelemetry({ name: 'my-app' });
719
+ const fortress = createFortress({ database, jwt: { key }, observability });
720
+ ```
721
+
722
+ See [Observability](docs/observability.md).
723
+
724
+ ## CLI
725
+
726
+ ```bash
727
+ fortress init
728
+ fortress generate-secret
729
+ fortress sync:types
730
+ fortress openapi --out openapi.json
731
+ fortress schemas --format json-schema --out schemas.json
732
+ fortress manifest --out route-manifest.json
733
+ fortress manifest:check
734
+ fortress check:public-routes
735
+ fortress migrate:status --dialect pg
736
+ fortress migrate:check --dialect pg
737
+ fortress policy:summary
738
+ ```
739
+
740
+ `sync:push`, `sync:pull`, `policy:diff`, `policy:apply`, `policy:check`, and live migration checks print code for the database-bound operation. Run that code inside your application so Fortress can use its configured adapter.
741
+
742
+ ## Guides
743
+
744
+ - [Examples](examples/README.md)
745
+ - [Host-owned routes](docs/host-owned-routes.md)
746
+ - [Typed framework helpers](docs/adapter-typed-helpers.md)
747
+ - [Policy as code](docs/policy-as-code.md)
748
+ - [Admin recipes](docs/admin-recipes.md)
749
+ - [Deployment](docs/deployment.md)
750
+ - [Security](docs/security.md) and [hardening](docs/hardening.md)
751
+ - [Threat model](docs/threat-model.md)
752
+ - [Observability](docs/observability.md)
753
+ - [Route manifest](docs/route-manifest.md)
754
+ - [CI checks](docs/ci.md)
755
+ - [Compatibility](docs/compatibility.md)
756
+ - [Architecture and plugin authoring](docs/architecture.md)
757
+
758
+ ## License
759
+
760
+ MIT