@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,118 @@
1
+ # Hardening
2
+
3
+ A focused checklist for operators running Fortress in production. Pair
4
+ with [deployment.md](./deployment.md) (configuration), [security.md](./security.md) (architecture),
5
+ and [threat-model.md](./threat-model.md) (attacker model).
6
+
7
+ Each item is "what to do" + "why" + "how to verify".
8
+
9
+ ## Identity and credentials
10
+
11
+ ### JWT secret
12
+
13
+ - **Do:** Generate via `fortress generate-secret` (64 bytes hex, exceeds the 32-byte HS256 minimum). Store in a secret manager. Rotate at least annually and immediately on suspected compromise.
14
+ - **Why:** A leaked HS256 secret lets an attacker forge any access token. Fortress validates secret length at boot — anything shorter throws.
15
+ - **Verify:** `createFortress` accepts an array for rotation (`jwt.key: [new, old]`) — make sure your rotation playbook adds `new` to position 0, redeploys, waits past `accessTokenExpirySeconds`, removes `old`.
16
+
17
+ ### Cookie posture
18
+
19
+ - **Do:** Keep the `__Host-` prefix defaults. Only set `cookies: { secure: false }` for localhost HTTP dev.
20
+ - **Why:** `__Host-` cookies are scoped to the origin, have no `Domain`, and refuse to be set without HTTPS — the strongest browser-enforced isolation.
21
+ - **Verify:** `curl -I https://your-domain/auth/login` returns `Set-Cookie: __Host-fortress_*` with `Secure; HttpOnly; SameSite=Lax; Path=/`.
22
+
23
+ ### Password policy
24
+
25
+ - **Do:** Keep the default `passwordPolicy.minLength` of 15 and enable `checkBreached`; choose `breachedFailureMode: 'closed'` when assurance is more important than registration availability.
26
+ - **Why:** Length plus breached-password screening provides useful resistance without brittle composition rules. Every HIBP outage emits `PASSWORD_BREACH_CHECK_DEGRADED` for alerting.
27
+ - **Verify:** Try registering with `"abc"` — it should reject with `BAD_REQUEST`; simulate an HIBP outage and verify your chosen open/closed behavior.
28
+
29
+ ### Refresh-token posture
30
+
31
+ - **Do:** Keep `refreshTokenExpirySeconds` ≤ 30 days. Enforce per-device refresh family rotation (Fortress default). Treat any `TOKEN_REUSE_DETECTED` event as a paging incident.
32
+ - **Why:** Refresh-token reuse detection turns a stolen refresh token into a same-day revocation event. Without rotation, the attacker has the full token lifetime.
33
+
34
+ ## Authorization
35
+
36
+ ### Default-deny everywhere
37
+
38
+ - **Do:** Every Fortress-managed route should carry either `.security('none')` (intentional public) or `.permission(resource, action)` (default-deny). The mutual-exclusion check at boot will throw if you mix them.
39
+ - **Why:** The plan's stated goal — operators should never have to remember to add a guard.
40
+ - **Verify:** `fortress check:public-routes` in CI catches a stray `.security('none')`; the bootstrap allow-list covers only Fortress's intentional public surface.
41
+
42
+ ### RBAC drift
43
+
44
+ - **Do:** Add `fortress manifest:check` to CI; also run `runFortressChecks({ fortress })` in a test that constructs your real `Fortress` instance.
45
+ - **Why:** Manifest drift catches "I added a route and forgot the `.permission` decorator" before it ships.
46
+
47
+ ### Policy as code
48
+
49
+ - **Do:** Manage roles/groups/service accounts via `fortress.policy.json` instead of imperative `iam.createRole(...)` from production code. Reconcile in CI with `applyPolicyPlan(...)`.
50
+ - **Why:** Reviewable Git history for permission grants; no "who gave Bob role X?" mystery.
51
+
52
+ ## Transport and edge
53
+
54
+ ### HTTPS
55
+
56
+ - **Do:** Terminate TLS at the edge; redirect HTTP to HTTPS. Set HSTS via your edge (`Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`).
57
+ - **Why:** All security guarantees in this guide assume HTTPS in the browser.
58
+
59
+ ### CSRF
60
+
61
+ - **Do:** Keep the default pipeline check on for cookie-authenticated browser sessions. Add `csrf: { rejectSameSite: true }` if your app is single-host (`example.com` only). Skip the check for routes that absolutely must accept third-party POSTs (webhooks) and replace it with signature verification per-route.
62
+ - **Why:** Cookies are ambient credentials; without a CSRF check, any malicious page can trigger a state change.
63
+ - **Verify:** A POST from a third-party origin (no `X-Fortress-CSRF` header, `Sec-Fetch-Site: cross-site`) returns 403.
64
+
65
+ ### Rate limiting
66
+
67
+ - **Do:** Mount the `rate-limit` plugin with `login`, `register`, `refresh`, `oauthToken`, and `apiKeyIssue` blocks. In multi-replica deployments, bring your own `RateLimitStore` (Redis, Memcached) — the in-memory default does not coordinate across replicas.
68
+ - **Why:** Per-IP and per-account limits stop credential-stuffing and account-enumeration before they reach the password hasher.
69
+
70
+ ## Data isolation
71
+
72
+ ### Tenancy
73
+
74
+ - **Do:** Use the verified `tenantId` JWT claim (set by `enrichTokenClaims`); never an `X-Tenant-Code` header. Use numeric tenant IDs in schema names (`tenant_<id>`). For PostgreSQL, the adapter pins `search_path` per transaction via `set_config('search_path', ?, true)`.
75
+ - **Why:** The hardening plan documented in [docs/plugins/tenancy.md](./plugins/tenancy.md) closes a class of cross-tenant data-leak bugs.
76
+
77
+ ### Multi-tenant scope rules
78
+
79
+ - **Do:** For row-level isolation across databases (not just PG), use the `data-isolation` plugin with explicit `scopes` per model.
80
+ - **Why:** Tenancy-via-schema only works on PG; everywhere else, scope rules are the safety net.
81
+
82
+ ## Audit and logging
83
+
84
+ ### Audit chain
85
+
86
+ - **Do:** Mount `auditLog({ hashChain: true })`. Schedule a nightly `verifyChain()` job. Mirror auth events into the audit log via an `addAuthObserver` (see [observability.md §6](./observability.md)).
87
+ - **Why:** Hash-chaining detects after-the-fact deletions; verification turns it into an enforced invariant.
88
+
89
+ ### Structured logging
90
+
91
+ - **Do:** Pass a structured logger to `FortressConfig.logger` (`pino`, `fastify.log`, etc.). Default is silent — production without a logger means you'll never see token reuse or RBAC denials.
92
+ - **Why:** Security events are useless if no one sees them.
93
+
94
+ ## Cryptographic posture
95
+
96
+ - **Do:** Stick with Argon2id (the default). Don't downgrade to PBKDF2/bcrypt without a measured reason.
97
+ - **Do:** Use the bundled OAuth signing key rotation (`oauth_signing_key` table); rotate quarterly.
98
+ - **Do:** When using social-login, pin provider issuer URLs to HTTPS; the bundled providers do this.
99
+
100
+ ## Deployment hygiene
101
+
102
+ - **Do:** Run `bun run lint` + `bun run typecheck` + `bun run test` + `fortress migrate:check` + `fortress manifest:check` + `fortress check:public-routes` on every CI build. The shipped GitHub Actions workflow does all six (see [docs/ci/github-actions.yml](./ci/github-actions.yml)).
103
+ - **Do:** Take a database backup before every deploy that bumps a migration version; test restores quarterly.
104
+ - **Do:** Pin to a minor version (`@bajustone/fortress@~0.1`). Pre-1.0 Fortress reserves the right to ship breaking changes in any minor.
105
+
106
+ ## Release notes discipline
107
+
108
+ The maintainer commits to:
109
+
110
+ - Every change that affects a security-relevant code path lands with a `### Security` or `### Changed` entry in the CHANGELOG noting the impact.
111
+ - Migration-bearing changes ship with a `docs/migrations/<version>.md` note (forward, rollback, backfill).
112
+ - Findings from external reviews land in `docs/security-review-<date>.md` with a remediation plan.
113
+
114
+ If a CHANGELOG entry for a security-affecting change is missing in a release, file an issue — it's a process bug.
115
+
116
+ ## Reporting a vulnerability
117
+
118
+ See [SECURITY.md](../SECURITY.md). Email `security@bajustone.dev`. Acknowledgement within 48 hours, fix targeted within 7 days for criticals.
@@ -0,0 +1,154 @@
1
+ # Host-owned routes
2
+
3
+ Fortress has two HTTP surfaces:
4
+
5
+ 1. **Fortress-managed routes** — endpoints registered in `fortress.endpoints` and served through `fortress.handleRequest()` by `mountFortress()` / `createSvelteKitHandle()`.
6
+ 2. **Host-owned routes** — routes your app registers in Hono, Express, SvelteKit, `Bun.serve`, etc. These often call `fortress.auth.*`, `fortress.iam.*`, or plugin methods directly.
7
+
8
+ Use `protect()` / `protectedRoute()` when a host-owned route should receive the same security controls as a Fortress-managed route.
9
+
10
+ ## What `protect()` runs
11
+
12
+ Given an endpoint definition (or handler name) present in the route manifest, Fortress applies:
13
+
14
+ - plugin middleware: `before-auth`, `after-auth`, `after-rbac`;
15
+ - pipeline CSRF protection for unsafe cookie-authenticated requests;
16
+ - principal resolution via plugin credentials first, then Fortress JWT bearer/cookie;
17
+ - default-deny RBAC from `endpoint.meta.permission`;
18
+ - request validation from endpoint body/query/params schemas;
19
+ - auth-cookie attachment when the handler returns `{ accessToken, refreshToken? }`.
20
+
21
+ The route's endpoint metadata remains the source of truth. Do not duplicate permissions in a separate route map unless you intentionally want a different policy.
22
+
23
+ ## Core API
24
+
25
+ ```ts
26
+ import { protect } from '@bajustone/fortress';
27
+
28
+ const handler = protect(fortress, endpointDefinition, async (ctx) => {
29
+ // ctx.subject, ctx.userId, ctx.input, ctx.params, ctx.query, ctx.body
30
+ return { ok: true };
31
+ });
32
+
33
+ const response = await handler(request);
34
+ ```
35
+
36
+ `target` may be an `EndpointDefinition` or a unique endpoint `handler` name. If a handler name maps to multiple routes, pass the definition directly or set `method`.
37
+
38
+ ### Typed input inference
39
+
40
+ `protect()` is generic over the endpoint you pass it. When `target` is the value returned by `endpoint(...).build()`, its phantom `<TBody, TQuery, TParams, TResponses>` generics flow into the `ProtectedRouteContext`:
41
+
42
+ ```ts
43
+ const createThing = endpoint('POST', '/things/:id')
44
+ .summary('Create thing')
45
+ .security('bearer')
46
+ .permission('thing', 'write')
47
+ .body(obj({ name: str() }, 'name'))
48
+ .params(obj({ id: int() }, 'id'))
49
+ .response(201, 'Created', obj({ ok: str() }, 'ok'))
50
+ .handler('createThing')
51
+ .build();
52
+
53
+ const handler = protect(fortress, createThing, async (ctx) => {
54
+ // ctx.body is { name: string } — non-optional, because a body schema is
55
+ // declared and validation already ran before the handler executes.
56
+ // ctx.params is { id: string }
57
+ // ctx.input is { name: string; id: string }
58
+ return { ok: ctx.body.name };
59
+ });
60
+ ```
61
+
62
+ When the endpoint declares a body schema, `ctx.body` is narrowed to a non-optional `T` — use it directly, no `!` and no detour through `ctx.input`. Endpoints with no declared body schema keep the loose `unknown` body.
63
+
64
+ Passing a string handler name keeps the looser `Record<string, unknown>` / `unknown` typing (no inference source available). Runtime validation and coercion are identical in both cases — this is purely a typing improvement.
65
+
66
+ ### Typed status returns with `ctx.respond`
67
+
68
+ For non-2xx returns, use `ctx.respond(status, body)` instead of hand-rolling a `Response`. When the target is a typed endpoint, both arguments narrow against the response declarations:
69
+
70
+ ```ts
71
+ const getThing = endpoint('GET', '/things/:id')
72
+ .summary('Get thing')
73
+ .security('bearer')
74
+ .params(obj({ id: int() }, 'id'))
75
+ .response(200, 'OK', obj({ data: str() }, 'data'))
76
+ .errorResponse(404, 'Not found')
77
+ .handler('getThing')
78
+ .build();
79
+
80
+ const handler = protect(fortress, getThing, async (ctx) => {
81
+ const thing = await load(ctx.params.id);
82
+ if (!thing)
83
+ return ctx.respond(404, { code: 'NOT_FOUND', message: 'no such thing', statusCode: 404 });
84
+ // ^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
85
+ // must be a declared status typed against responses[404].schema (ErrorEnvelope)
86
+ return { data: thing };
87
+ });
88
+ ```
89
+
90
+ String-target endpoints fall back to `(status: number, body: unknown) => Response` — same ergonomics as before, no narrowing.
91
+
92
+ ## Hono
93
+
94
+ ```ts
95
+ import { endpoint, obj, str } from '@bajustone/fortress';
96
+ import { protectedRoute } from '@bajustone/fortress/hono';
97
+
98
+ const statsEndpoint = endpoint('GET', '/api/stats')
99
+ .summary('Stats')
100
+ .security('bearer')
101
+ .permission('stats', 'read')
102
+ .response(200, 'OK', obj({ ok: str() }, 'ok'))
103
+ .handler('stats')
104
+ .build();
105
+
106
+ // Include the endpoint so it appears in
107
+ // fortress.endpoints / fortress.manifest / OpenAPI. Either pass it via the
108
+ // top-level `routes` shorthand (preferred) or as a one-field plugin.
109
+ const fortress = createFortress({
110
+ // ...
111
+ routes: { stats: statsEndpoint },
112
+ });
113
+
114
+ // Equivalent long-form (use this if you want to colocate host endpoints with
115
+ // other plugin metadata in the same record):
116
+ // plugins: [{ name: 'host-routes', routes: { stats: statsEndpoint } }],
117
+
118
+ app.get('/api/stats', protectedRoute(fortress, statsEndpoint, async (_c, ctx) => {
119
+ return { ok: ctx.subject?.type ?? 'unknown' };
120
+ }));
121
+ ```
122
+
123
+ Register the Hono route before `mountFortress(app, fortress)` so the host handler wins over the manifest-owned fallback route.
124
+
125
+ ## Express
126
+
127
+ ```ts
128
+ import { protectedRoute } from '@bajustone/fortress/express';
129
+
130
+ app.get('/api/stats', protectedRoute(fortress, statsEndpoint, async (_req, _res, ctx) => {
131
+ return { ok: ctx.subject?.type ?? 'unknown' };
132
+ }));
133
+ ```
134
+
135
+ ## SvelteKit
136
+
137
+ ```ts
138
+ import { protectedRoute } from '@bajustone/fortress/sveltekit';
139
+
140
+ const GET = protectedRoute(fortress, statsEndpoint, async (_event, ctx) => {
141
+ return new Response(JSON.stringify({ ok: ctx.subject?.type }), {
142
+ headers: { 'Content-Type': 'application/json' },
143
+ });
144
+ });
145
+
146
+ export { GET };
147
+ ```
148
+
149
+ ## Boundary checklist
150
+
151
+ - `fortress.handleRequest()` remains responsible for Fortress-managed routes.
152
+ - `protectedRoute()` is responsible for host-owned routes that opt in.
153
+ - Public host routes can remain unwrapped.
154
+ - If a host route is mounted at a different path than its endpoint metadata, pass `options.path` / `options.params` so plugin middleware and validation use the intended canonical route.
@@ -0,0 +1,54 @@
1
+ # Migration 0001 — Schema version tracking
2
+
3
+ Introduced: 2026-06-08
4
+
5
+ ## Summary
6
+
7
+ Adds the `fortress_schema_version` singleton table. Fortress migration tooling uses this table to compare a live database against bundled migrations.
8
+
9
+ ## Forward
10
+
11
+ SQLite:
12
+
13
+ ```sql
14
+ CREATE TABLE IF NOT EXISTS fortress_schema_version (
15
+ id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
16
+ version INTEGER NOT NULL,
17
+ applied_at INTEGER NOT NULL DEFAULT (unixepoch())
18
+ );
19
+ ```
20
+
21
+ PostgreSQL:
22
+
23
+ ```sql
24
+ CREATE TABLE IF NOT EXISTS fortress_schema_version (
25
+ id integer PRIMARY KEY DEFAULT 1 CHECK (id = 1),
26
+ version integer NOT NULL,
27
+ applied_at timestamp NOT NULL DEFAULT now()
28
+ );
29
+ ```
30
+
31
+ The singleton version row (`id = 1`) is **not** written by this SQL — the
32
+ migration runner (`migrateUp`) stamps it after each migration applies, so
33
+ version tracking has a single source of truth. Applying the raw SQL by
34
+ hand creates the table at version 0; run `migrateUp` (or the CLI) to stamp
35
+ the current version.
36
+
37
+ ## Rollback
38
+
39
+ ```sql
40
+ DROP TABLE IF EXISTS fortress_schema_version;
41
+ ```
42
+
43
+ ## Backfill / cleanup
44
+
45
+ No data backfill is required. Existing Fortress tables are not modified by this migration.
46
+
47
+ ## Validation
48
+
49
+ ```ts
50
+ import { getMigrationStatus } from '@bajustone/fortress';
51
+
52
+ const status = await getMigrationStatus(fortress.config.database);
53
+ console.log(status.currentVersion, status.latestVersion);
54
+ ```
@@ -0,0 +1,84 @@
1
+ # Migration 0002 — Initial Fortress schema
2
+
3
+ Introduced: 2026-06-08
4
+
5
+ ## Summary
6
+
7
+ Creates every Fortress-owned table, index, and constraint — everything
8
+ except the `fortress_schema_version` checkpoint installed by `0001`. This
9
+ is the baseline that lets `migrateUp` provision a brand-new database
10
+ (SQLite or PostgreSQL) end-to-end through the adapter's `rawQuery`, with
11
+ no Drizzle or `drizzle-kit` dependency at runtime.
12
+
13
+ The migration is the SQL-first source of truth for the schema:
14
+
15
+ - `src/testing/index.ts` builds the in-memory test adapter from this DDL.
16
+ - The column-drift checker (`detectMigrationDrift`) parses expected
17
+ columns out of this DDL.
18
+
19
+ So the test adapter and a production `migrateUp` cannot drift.
20
+
21
+ ## Forward
22
+
23
+ Run via the migration runner (recommended — it also stamps the version
24
+ row):
25
+
26
+ ```ts
27
+ import { migrateUp } from '@bajustone/fortress';
28
+
29
+ await migrateUp(adapter); // applies 0001 + 0002, stamps version 2
30
+ ```
31
+
32
+ Or apply the bundled SQL directly:
33
+
34
+ - SQLite: `migrations/sqlite/0002_initial_schema.sql`
35
+ - PostgreSQL: `migrations/pg/0002_initial_schema.sql`
36
+
37
+ All statements are `CREATE TABLE IF NOT EXISTS` / `CREATE [UNIQUE] INDEX
38
+ IF NOT EXISTS`, so applying it to a database that already has the Fortress
39
+ tables (e.g. provisioned from the Drizzle schema) is a no-op.
40
+
41
+ ## Rollback
42
+
43
+ ```ts
44
+ import { migrateDown } from '@bajustone/fortress';
45
+
46
+ await migrateDown(adapter, undefined, 1); // roll back to version 1
47
+ ```
48
+
49
+ Or apply `migrations/{sqlite,pg}/0002_initial_schema.down.sql`.
50
+
51
+ > **Destructive.** The down step drops every Fortress table (PostgreSQL
52
+ > uses `CASCADE`). It deletes all users, tokens, roles, and audit history.
53
+ > Back up first and only do this on a database you intend to re-provision.
54
+
55
+ ## Backfill / cleanup
56
+
57
+ None. On a fresh database this is the initial install. On a database whose
58
+ tables were already provisioned from the Drizzle schema, the forward step
59
+ is a no-op and only the version row changes.
60
+
61
+ ## Migrating from a pre-0002 database
62
+
63
+ If you deployed an earlier Fortress version and provisioned tables from
64
+ the Drizzle schema (the previous documented path):
65
+
66
+ 1. Pull this version.
67
+ 2. Run `migrateUp(adapter)`. The `IF NOT EXISTS` guards mean existing
68
+ tables are left untouched; the version row advances to 2.
69
+ 3. Run `detectMigrationDrift(adapter)` and confirm both `missingTables`
70
+ and `missingColumns` are empty. If `missingColumns` is non-empty, your
71
+ tables predate a column added in a later schema change — apply the
72
+ listed columns (the bundled DDL is the reference) and re-check.
73
+
74
+ ## Validation
75
+
76
+ ```ts
77
+ import { detectMigrationDrift, getMigrationStatus, hasMigrationDrift } from '@bajustone/fortress';
78
+
79
+ const status = await getMigrationStatus(adapter);
80
+ console.log(status.currentVersion, status.latestVersion); // 2 2
81
+
82
+ const drift = await detectMigrationDrift(adapter);
83
+ console.log(hasMigrationDrift(drift)); // false
84
+ ```
@@ -0,0 +1,160 @@
1
+ # Fortress migration upgrade guide
2
+
3
+ This guide is the operational counterpart to the per-release notes under
4
+ `docs/migrations/<version>.md`. It describes how to run, verify, and roll
5
+ back Fortress migrations across releases.
6
+
7
+ ## Migration sources of truth
8
+
9
+ - **Bundled migrations:** `migrations/{sqlite,pg}/<version>_<name>.sql`
10
+ and `<version>_<name>.down.sql`. Committed to the repo for review and
11
+ auditability. Mirrored as string constants in
12
+ `src/core/migrations/migrations.ts` so the runtime works without disk
13
+ access (serverless, bundled CLIs).
14
+ - **Expected table list:** `FORTRESS_TABLES` in
15
+ `src/core/migrations/migrations.ts`. Drives the live-DB drift checker.
16
+ Kept in sync with `src/drizzle/schema.ts`, `src/drizzle/pg/schema.ts`,
17
+ and `src/testing/index.ts`; the engine test fails if the test adapter
18
+ ever stops creating one of these tables.
19
+ - **Schema-version table:** `fortress_schema_version` (single row,
20
+ `id = 1`). The applied baseline is whatever `version` holds; missing
21
+ table means version 0.
22
+
23
+ ## Runtime API
24
+
25
+ ```ts
26
+ import {
27
+ detectMigrationDrift,
28
+ getMigrationStatus,
29
+ hasMigrationDrift,
30
+ migrateDown,
31
+ migrateUp,
32
+ } from '@bajustone/fortress';
33
+
34
+ const status = await getMigrationStatus(adapter);
35
+ // { dialect, currentVersion, latestVersion, pending, applied, upToDate, hasVersionTable }
36
+
37
+ const up = await migrateUp(adapter);
38
+ // { fromVersion, toVersion, applied: FortressMigration[] }
39
+
40
+ const down = await migrateDown(adapter, 'sqlite', 0);
41
+ // rolls back to version 0
42
+
43
+ const drift = await detectMigrationDrift(adapter);
44
+ // { currentVersion, latestVersion, missingVersionTable, unknownFutureVersion,
45
+ // pendingVersions, missingTables, missingColumns, missingIndexes }
46
+ if (hasMigrationDrift(drift)) throw new Error('Schema drift detected');
47
+ ```
48
+
49
+ Pass an explicit `dialect` when the adapter does not advertise one (most
50
+ non-Drizzle adapters). Both `migrateUp` and `migrateDown` are idempotent and safe to run on every
51
+ deploy. Mutation runs acquire a database-backed lock, re-read the checkpoint
52
+ only after locking, and execute transactionally. The engine maintains
53
+ `fortress_migration_journal` with one SHA-256 checksum per applied migration;
54
+ legacy checkpoints are backfilled once, while later missing or changed rows
55
+ fail closed before any schema mutation.
56
+
57
+ ## CLI
58
+
59
+ ```sh
60
+ fortress migrate:status
61
+ fortress migrate:down
62
+ fortress migrate:diff
63
+ fortress migrate:check # exits non-zero on drift; suitable for CI
64
+ ```
65
+
66
+ The CLI commands operate on the bundled migration catalog and do **not**
67
+ connect to a database. Migration v6 includes a Unicode-aware data step, so
68
+ `fortress migrate:up` intentionally refuses to emit SQL-only output that would
69
+ skip cleanup. Run the runtime API against your live database from application
70
+ code; it executes data steps and constraints atomically using the configured
71
+ adapter.
72
+
73
+ ## Drift signals (`detectMigrationDrift`)
74
+
75
+ | Field | Meaning | Recommended action |
76
+ |---|---|---|
77
+ | `missingVersionTable` | `fortress_schema_version` does not exist | Run `migrateUp` |
78
+ | `pendingVersions` | Migrations the runtime has but the DB has not applied | Run `migrateUp` |
79
+ | `unknownFutureVersion` | DB is at a higher version than the bundled migrations | Upgrade the Fortress version before deploying |
80
+ | `missingTables` | Any `FORTRESS_TABLES` entry not present in the DB | Run `migrateUp`, or finish the interrupted migration run |
81
+ | `missingColumns` | A table that exists but is missing a column the bundled DDL defines | Apply the migration that adds the column (or recreate the table from the bundled DDL) |
82
+ | `missingIndexes` | A required hot-path index is absent | Apply the owning migration or restore the named index |
83
+
84
+ `missingTables`, `missingColumns`, and `missingIndexes` are the deep checks — they catch the
85
+ case where the version table claims everything is applied but the schema
86
+ is genuinely incomplete (manual DROP, partial restore, a table created
87
+ from an older schema dump). Expected columns are parsed straight out of
88
+ the bundled migration DDL, so the check works for any adapter — not just
89
+ Drizzle. Wire `hasMigrationDrift()` into your deploy preflight and your CI
90
+ smoke tests.
91
+
92
+ ## Current migration catalog
93
+
94
+ Ten migrations ship today:
95
+
96
+ - **`0001_schema_version`** — installs the schema checkpoint.
97
+ - **`0002_initial_schema`** — creates the original Fortress schema.
98
+ - **`0003_auth_continuation`** — adds pending-auth/session metadata.
99
+ - **`0004_tenant_default_unique`** — enforces one default tenant membership.
100
+ - **`0005_hot_indexes_timestamptz`** — adds hot indexes and UTC-safe PostgreSQL timestamps.
101
+ - **`0006_canonical_email`** — performs runtime Unicode email cleanup and installs case-insensitive uniqueness.
102
+ - **`0007_audit_chain_anchor`** — installs the permanent zero-entry sentinel and persists the terminal audit hash/count so tail or full-chain deletion is detectable.
103
+ - **`0008_two_factor_hardening`** — adds durable continuation failure counters, invalidation/cooldown metadata, and the indexed per-account failure lookup used by MFA throttling.
104
+ - **`0009_encrypt_totp_secrets`** — removes legacy plaintext TOTP enrolments, backup codes, and trusted devices. SQL cannot encrypt legacy seeds with the application-held key, so affected users must re-enrol; new seeds are stored as AES-256-GCM envelopes.
105
+ - **`0010_bigint_append_only_ids`** — widens PostgreSQL identifiers and sequences on unbounded/churn-heavy token, audit, webhook-delivery, and WebAuthn-challenge tables to bigint. SQLite identifiers are already signed 64-bit. Narrowing rollback is intentionally unsupported.
106
+
107
+ If `fortress_audit_log` already contains rows written without hash chaining, migration 0007 intentionally leaves the anchor at zero; SQL cannot establish a trustworthy cryptographic history. Before serving auth traffic with `auditLog({ hashChain: true })`, archive and externally attest those rows, create the hash-chain-enabled Fortress instance during a maintenance window, and call `await fortress.plugins['audit-log'].rebaselineChain()`. The method transactionally accepts only an uninitialized zero anchor and fully unchained rows, links them deterministically, advances the anchor, and verifies the result. See [Audit Log Plugin](../plugins/audit-log.md#enabling-hash-chaining-on-an-existing-log).
108
+
109
+ The migrations are the SQL-first source of truth: `src/testing/index.ts`
110
+ derives the test adapter's schema from them, and the column-drift checker
111
+ parses expected columns from them, so the test adapter and a production
112
+ `migrateUp` cannot diverge.
113
+
114
+ ### Provisioning a new database
115
+
116
+ 1. Point Fortress at an empty database (any adapter with `rawQuery`).
117
+ 2. Call `migrateUp(adapter)` — this creates the checkpoint table, all
118
+ Fortress tables, and stamps `fortress_schema_version` at the latest
119
+ version.
120
+ 3. Call `detectMigrationDrift(adapter)` to confirm `missingTables` and
121
+ `missingColumns` are empty.
122
+
123
+ > Drizzle users may still provision tables directly from
124
+ > `src/drizzle/{,pg/}schema.ts` if they prefer to own schema management;
125
+ > `migrateUp` is idempotent (`CREATE TABLE IF NOT EXISTS`) and will simply
126
+ > stamp the version row in that case.
127
+
128
+ ### Upgrading an existing database
129
+
130
+ 1. Pull the new Fortress version.
131
+ 2. Read the version-specific notes in `docs/migrations/<version>.md` for
132
+ forward/rollback/backfill steps.
133
+ 3. Call `migrateUp(adapter)` on application start (or in a one-shot
134
+ deploy job).
135
+ 4. Confirm via `migrate:check` / `hasMigrationDrift()` that the
136
+ database is at the expected version with no missing tables.
137
+
138
+ ## Rollback
139
+
140
+ `migrateDown(adapter, dialect, targetVersion)` rolls the schema back to
141
+ the supplied version (default `0`). Each migration's `.down.sql` is
142
+ applied in reverse order. The version row is updated last, so a failed
143
+ rollback leaves the previous version recorded — re-run after fixing the
144
+ underlying issue.
145
+
146
+ Always take a backup before rolling back across an irreversible change
147
+ (table drops, column drops, type narrowing). The `0001_schema_version`
148
+ down step only removes the checkpoint table. **`0002_initial_schema`'s
149
+ down step drops every Fortress table** (PostgreSQL uses `CASCADE`), so
150
+ rolling back below version 2 is destructive — it deletes all users,
151
+ tokens, roles, and audit history. Only do this against a database you
152
+ intend to re-provision, and back up first.
153
+
154
+ ## Custom / additional migrations
155
+
156
+ The bundled catalog covers Fortress-owned tables. Application-owned
157
+ tables (e.g. tenant-specific business tables, audit-log partitions) are
158
+ out of scope — keep them in your own migration tool of choice. The
159
+ adapter's `rawQuery` is available if you need to call into the same DB
160
+ connection from your own scripts.