@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,4264 @@
1
+ // src/testing/index.ts
2
+ import { createRequire } from "module";
3
+ import { resolve } from "path";
4
+
5
+ // src/core/auth/email.ts
6
+ function normalizeEmail(email2) {
7
+ return email2.toLowerCase().normalize("NFC");
8
+ }
9
+
10
+ // src/core/migrations/migrations.ts
11
+ var SQLITE_0001_UP = `
12
+ CREATE TABLE IF NOT EXISTS fortress_schema_version (
13
+ id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
14
+ version INTEGER NOT NULL,
15
+ applied_at INTEGER NOT NULL DEFAULT (unixepoch())
16
+ );
17
+ `.trim();
18
+ var SQLITE_0001_DOWN = `
19
+ DROP TABLE IF EXISTS fortress_schema_version;
20
+ `.trim();
21
+ var PG_0001_UP = `
22
+ CREATE TABLE IF NOT EXISTS fortress_schema_version (
23
+ id integer PRIMARY KEY DEFAULT 1 CHECK (id = 1),
24
+ version integer NOT NULL,
25
+ applied_at timestamp NOT NULL DEFAULT now()
26
+ );
27
+ `.trim();
28
+ var PG_0001_DOWN = `
29
+ DROP TABLE IF EXISTS fortress_schema_version;
30
+ `.trim();
31
+ var SQLITE_0002_UP = `
32
+ CREATE TABLE IF NOT EXISTS fortress_user (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ email TEXT NOT NULL UNIQUE,
35
+ name TEXT NOT NULL,
36
+ password_hash TEXT,
37
+ is_active INTEGER NOT NULL DEFAULT 1,
38
+ email_verified INTEGER NOT NULL DEFAULT 0,
39
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
40
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS fortress_login_identifier (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
46
+ type TEXT NOT NULL,
47
+ value TEXT NOT NULL UNIQUE
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS fortress_refresh_token (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
53
+ token_hash TEXT NOT NULL UNIQUE,
54
+ token_family TEXT NOT NULL,
55
+ is_revoked INTEGER NOT NULL DEFAULT 0,
56
+ expires_at INTEGER NOT NULL,
57
+ ip_address TEXT,
58
+ user_agent TEXT,
59
+ device_name TEXT,
60
+ last_active_at INTEGER,
61
+ fingerprint_hash TEXT,
62
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS fortress_group (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ name TEXT NOT NULL UNIQUE,
68
+ description TEXT
69
+ );
70
+
71
+ CREATE TABLE IF NOT EXISTS fortress_group_user (
72
+ group_id INTEGER NOT NULL REFERENCES fortress_group(id) ON DELETE CASCADE,
73
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
74
+ PRIMARY KEY (group_id, user_id)
75
+ );
76
+
77
+ CREATE TABLE IF NOT EXISTS fortress_service_account (
78
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
79
+ name TEXT NOT NULL UNIQUE,
80
+ display_name TEXT,
81
+ description TEXT,
82
+ is_active INTEGER NOT NULL DEFAULT 1,
83
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
84
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
85
+ );
86
+
87
+ CREATE TABLE IF NOT EXISTS fortress_resource (
88
+ name TEXT PRIMARY KEY,
89
+ description TEXT
90
+ );
91
+
92
+ CREATE TABLE IF NOT EXISTS fortress_permission (
93
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
94
+ resource TEXT NOT NULL REFERENCES fortress_resource(name) ON DELETE CASCADE,
95
+ action TEXT NOT NULL,
96
+ effect TEXT NOT NULL DEFAULT 'ALLOW',
97
+ conditions TEXT,
98
+ description TEXT
99
+ );
100
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_permission_no_conditions
101
+ ON fortress_permission (resource, action, effect)
102
+ WHERE conditions IS NULL;
103
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_permission_with_conditions
104
+ ON fortress_permission (resource, action, effect, conditions)
105
+ WHERE conditions IS NOT NULL;
106
+
107
+ CREATE TABLE IF NOT EXISTS fortress_role (
108
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
109
+ name TEXT NOT NULL UNIQUE,
110
+ description TEXT,
111
+ is_system INTEGER NOT NULL DEFAULT 0
112
+ );
113
+
114
+ CREATE TABLE IF NOT EXISTS fortress_role_permission (
115
+ role_id INTEGER NOT NULL REFERENCES fortress_role(id) ON DELETE CASCADE,
116
+ permission_id INTEGER NOT NULL REFERENCES fortress_permission(id) ON DELETE CASCADE,
117
+ PRIMARY KEY (role_id, permission_id)
118
+ );
119
+
120
+ CREATE TABLE IF NOT EXISTS fortress_role_binding (
121
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ role_id INTEGER NOT NULL REFERENCES fortress_role(id) ON DELETE CASCADE,
123
+ subject_type TEXT NOT NULL,
124
+ subject_id INTEGER NOT NULL,
125
+ tenant_id TEXT,
126
+ UNIQUE (role_id, subject_type, subject_id, tenant_id)
127
+ );
128
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_role_binding_global
129
+ ON fortress_role_binding (role_id, subject_type, subject_id)
130
+ WHERE tenant_id IS NULL;
131
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_role_binding_tenant
132
+ ON fortress_role_binding (role_id, subject_type, subject_id, tenant_id)
133
+ WHERE tenant_id IS NOT NULL;
134
+
135
+ CREATE TABLE IF NOT EXISTS fortress_direct_permission_binding (
136
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
137
+ permission_id INTEGER NOT NULL REFERENCES fortress_permission(id) ON DELETE CASCADE,
138
+ subject_type TEXT NOT NULL,
139
+ subject_id INTEGER NOT NULL,
140
+ tenant_id TEXT,
141
+ UNIQUE (permission_id, subject_type, subject_id, tenant_id)
142
+ );
143
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_direct_permission_binding_global
144
+ ON fortress_direct_permission_binding (permission_id, subject_type, subject_id)
145
+ WHERE tenant_id IS NULL;
146
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_direct_permission_binding_tenant
147
+ ON fortress_direct_permission_binding (permission_id, subject_type, subject_id, tenant_id)
148
+ WHERE tenant_id IS NOT NULL;
149
+
150
+ CREATE TABLE IF NOT EXISTS fortress_email_verification_token (
151
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
152
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
153
+ token TEXT NOT NULL,
154
+ email TEXT NOT NULL,
155
+ expires_at INTEGER NOT NULL,
156
+ used_at INTEGER,
157
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
158
+ );
159
+
160
+ CREATE TABLE IF NOT EXISTS fortress_magic_link_token (
161
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
162
+ email TEXT NOT NULL,
163
+ token TEXT NOT NULL,
164
+ expires_at INTEGER NOT NULL,
165
+ used_at INTEGER,
166
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
167
+ );
168
+
169
+ CREATE TABLE IF NOT EXISTS fortress_api_key (
170
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
171
+ subject_type TEXT NOT NULL,
172
+ subject_id INTEGER NOT NULL,
173
+ name TEXT NOT NULL,
174
+ key_hash TEXT NOT NULL UNIQUE,
175
+ key_prefix TEXT NOT NULL,
176
+ scopes TEXT,
177
+ expires_at INTEGER,
178
+ last_used_at INTEGER,
179
+ is_revoked INTEGER NOT NULL DEFAULT 0,
180
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
181
+ );
182
+ CREATE INDEX IF NOT EXISTS api_key_subject_idx ON fortress_api_key (subject_type, subject_id);
183
+
184
+ CREATE TABLE IF NOT EXISTS fortress_two_factor_secret (
185
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
186
+ user_id INTEGER NOT NULL UNIQUE REFERENCES fortress_user(id) ON DELETE CASCADE,
187
+ secret TEXT NOT NULL,
188
+ is_enabled INTEGER NOT NULL DEFAULT 0,
189
+ last_used_counter INTEGER,
190
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
191
+ );
192
+
193
+ CREATE TABLE IF NOT EXISTS fortress_backup_code (
194
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
195
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
196
+ code_hash TEXT NOT NULL,
197
+ is_used INTEGER NOT NULL DEFAULT 0,
198
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
199
+ );
200
+
201
+ CREATE TABLE IF NOT EXISTS fortress_trusted_device (
202
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
203
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
204
+ device_hash TEXT NOT NULL,
205
+ expires_at INTEGER NOT NULL,
206
+ last_used_at INTEGER NOT NULL,
207
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
208
+ );
209
+
210
+ CREATE TABLE IF NOT EXISTS fortress_social_account (
211
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
212
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
213
+ provider TEXT NOT NULL,
214
+ provider_account_id TEXT NOT NULL,
215
+ email TEXT,
216
+ access_token TEXT,
217
+ refresh_token TEXT,
218
+ token_expires_at INTEGER,
219
+ profile TEXT,
220
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
221
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
222
+ UNIQUE (user_id, provider),
223
+ UNIQUE (provider, provider_account_id)
224
+ );
225
+
226
+ CREATE TABLE IF NOT EXISTS fortress_tenant (
227
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
228
+ name TEXT NOT NULL,
229
+ tax_id TEXT NOT NULL UNIQUE,
230
+ description TEXT,
231
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
232
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
233
+ );
234
+
235
+ CREATE TABLE IF NOT EXISTS fortress_tenant_user (
236
+ tenant_id INTEGER NOT NULL REFERENCES fortress_tenant(id) ON DELETE CASCADE,
237
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
238
+ is_default INTEGER NOT NULL DEFAULT 0,
239
+ PRIMARY KEY (tenant_id, user_id)
240
+ );
241
+ CREATE UNIQUE INDEX IF NOT EXISTS fortress_tenant_user_one_default_idx
242
+ ON fortress_tenant_user (user_id) WHERE is_default = 1;
243
+
244
+ CREATE TABLE IF NOT EXISTS fortress_oauth_client (
245
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
246
+ client_id TEXT NOT NULL UNIQUE,
247
+ client_secret_hash TEXT NOT NULL,
248
+ name TEXT NOT NULL,
249
+ redirect_uris TEXT NOT NULL,
250
+ grant_types TEXT NOT NULL,
251
+ allowed_scopes TEXT,
252
+ token_endpoint_auth_method TEXT,
253
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
254
+ );
255
+
256
+ CREATE TABLE IF NOT EXISTS fortress_oauth_authorization_code (
257
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
258
+ code TEXT NOT NULL UNIQUE,
259
+ client_id TEXT NOT NULL,
260
+ user_id INTEGER NOT NULL,
261
+ redirect_uri TEXT NOT NULL,
262
+ scope TEXT,
263
+ code_challenge TEXT,
264
+ code_challenge_method TEXT,
265
+ nonce TEXT,
266
+ auth_time INTEGER,
267
+ expires_at INTEGER NOT NULL,
268
+ used_at INTEGER,
269
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
270
+ );
271
+
272
+ CREATE TABLE IF NOT EXISTS fortress_oauth_access_token (
273
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
274
+ token TEXT NOT NULL UNIQUE,
275
+ client_id TEXT NOT NULL,
276
+ user_id INTEGER,
277
+ scope TEXT,
278
+ expires_at INTEGER NOT NULL,
279
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
280
+ );
281
+
282
+ CREATE TABLE IF NOT EXISTS fortress_oauth_refresh_token (
283
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
284
+ token TEXT NOT NULL UNIQUE,
285
+ family_id TEXT NOT NULL,
286
+ client_id TEXT NOT NULL,
287
+ user_id INTEGER NOT NULL,
288
+ scope TEXT,
289
+ issued_at INTEGER NOT NULL,
290
+ expires_at INTEGER NOT NULL,
291
+ used_at INTEGER,
292
+ parent_id INTEGER,
293
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
294
+ );
295
+
296
+ CREATE TABLE IF NOT EXISTS fortress_oauth_pending_flow (
297
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
298
+ flow_id TEXT NOT NULL UNIQUE,
299
+ client_id TEXT NOT NULL,
300
+ redirect_uri TEXT NOT NULL,
301
+ scope TEXT,
302
+ state TEXT NOT NULL,
303
+ code_challenge TEXT,
304
+ code_challenge_method TEXT,
305
+ nonce TEXT,
306
+ user_id INTEGER,
307
+ used_at INTEGER,
308
+ expires_at INTEGER NOT NULL,
309
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
310
+ );
311
+
312
+ CREATE TABLE IF NOT EXISTS fortress_oauth_signing_key (
313
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
314
+ kid TEXT NOT NULL UNIQUE,
315
+ alg TEXT NOT NULL,
316
+ public_jwk TEXT NOT NULL,
317
+ private_jwk TEXT NOT NULL,
318
+ rotated_at INTEGER,
319
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
320
+ );
321
+
322
+ CREATE TABLE IF NOT EXISTS fortress_user_scope_assignment (
323
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
324
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
325
+ scope_name TEXT NOT NULL,
326
+ scope_value TEXT NOT NULL,
327
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
328
+ );
329
+
330
+ CREATE TABLE IF NOT EXISTS fortress_account_lockout (
331
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
332
+ identifier TEXT NOT NULL UNIQUE,
333
+ failed_attempts INTEGER NOT NULL DEFAULT 0,
334
+ last_failed_at INTEGER,
335
+ locked_until INTEGER,
336
+ lockout_count INTEGER NOT NULL DEFAULT 0,
337
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
338
+ );
339
+
340
+ CREATE TABLE IF NOT EXISTS fortress_audit_log (
341
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
342
+ timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
343
+ event_type TEXT NOT NULL,
344
+ actor_id INTEGER,
345
+ actor_type TEXT NOT NULL DEFAULT 'USER',
346
+ target_id INTEGER,
347
+ target_type TEXT,
348
+ ip_address TEXT,
349
+ user_agent TEXT,
350
+ outcome TEXT NOT NULL DEFAULT 'SUCCESS',
351
+ metadata TEXT,
352
+ previous_hash TEXT,
353
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
354
+ );
355
+
356
+ CREATE TABLE IF NOT EXISTS fortress_webhook_endpoint (
357
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
358
+ url TEXT NOT NULL,
359
+ events TEXT NOT NULL,
360
+ secret TEXT NOT NULL,
361
+ is_active INTEGER NOT NULL DEFAULT 1,
362
+ deactivated_reason TEXT,
363
+ consecutive_failures INTEGER NOT NULL DEFAULT 0,
364
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
365
+ );
366
+
367
+ CREATE TABLE IF NOT EXISTS fortress_webhook_delivery (
368
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
369
+ endpoint_id INTEGER NOT NULL REFERENCES fortress_webhook_endpoint(id) ON DELETE CASCADE,
370
+ event_type TEXT NOT NULL,
371
+ payload TEXT NOT NULL,
372
+ status TEXT NOT NULL DEFAULT 'pending',
373
+ attempts INTEGER NOT NULL DEFAULT 0,
374
+ idempotency_key TEXT,
375
+ last_attempt_at INTEGER,
376
+ next_retry_at INTEGER,
377
+ response_status INTEGER,
378
+ response_body TEXT,
379
+ error_kind TEXT,
380
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
381
+ );
382
+
383
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_webhook_delivery_idempotency
384
+ ON fortress_webhook_delivery (endpoint_id, idempotency_key)
385
+ WHERE idempotency_key IS NOT NULL;
386
+
387
+ CREATE TABLE IF NOT EXISTS fortress_webauthn_credential (
388
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
389
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
390
+ credential_id TEXT NOT NULL UNIQUE,
391
+ public_key TEXT NOT NULL,
392
+ counter INTEGER NOT NULL DEFAULT 0,
393
+ device_type TEXT NOT NULL,
394
+ backed_up INTEGER NOT NULL DEFAULT 0,
395
+ transports TEXT,
396
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
397
+ );
398
+
399
+ CREATE TABLE IF NOT EXISTS fortress_webauthn_challenge (
400
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
401
+ challenge TEXT NOT NULL UNIQUE,
402
+ user_id INTEGER,
403
+ expires_at INTEGER NOT NULL,
404
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
405
+ );
406
+ `.trim();
407
+ var SQLITE_0002_DOWN = `
408
+ DROP TABLE IF EXISTS fortress_webauthn_challenge;
409
+ DROP TABLE IF EXISTS fortress_webauthn_credential;
410
+ DROP TABLE IF EXISTS fortress_webhook_delivery;
411
+ DROP TABLE IF EXISTS fortress_webhook_endpoint;
412
+ DROP TABLE IF EXISTS fortress_audit_log;
413
+ DROP TABLE IF EXISTS fortress_account_lockout;
414
+ DROP TABLE IF EXISTS fortress_user_scope_assignment;
415
+ DROP TABLE IF EXISTS fortress_oauth_signing_key;
416
+ DROP TABLE IF EXISTS fortress_oauth_pending_flow;
417
+ DROP TABLE IF EXISTS fortress_oauth_refresh_token;
418
+ DROP TABLE IF EXISTS fortress_oauth_access_token;
419
+ DROP TABLE IF EXISTS fortress_oauth_authorization_code;
420
+ DROP TABLE IF EXISTS fortress_oauth_client;
421
+ DROP TABLE IF EXISTS fortress_tenant_user;
422
+ DROP TABLE IF EXISTS fortress_tenant;
423
+ DROP TABLE IF EXISTS fortress_social_account;
424
+ DROP TABLE IF EXISTS fortress_trusted_device;
425
+ DROP TABLE IF EXISTS fortress_backup_code;
426
+ DROP TABLE IF EXISTS fortress_two_factor_secret;
427
+ DROP TABLE IF EXISTS fortress_api_key;
428
+ DROP TABLE IF EXISTS fortress_magic_link_token;
429
+ DROP TABLE IF EXISTS fortress_email_verification_token;
430
+ DROP TABLE IF EXISTS fortress_direct_permission_binding;
431
+ DROP TABLE IF EXISTS fortress_role_binding;
432
+ DROP TABLE IF EXISTS fortress_role_permission;
433
+ DROP TABLE IF EXISTS fortress_role;
434
+ DROP TABLE IF EXISTS fortress_permission;
435
+ DROP TABLE IF EXISTS fortress_resource;
436
+ DROP TABLE IF EXISTS fortress_service_account;
437
+ DROP TABLE IF EXISTS fortress_group_user;
438
+ DROP TABLE IF EXISTS fortress_group;
439
+ DROP TABLE IF EXISTS fortress_refresh_token;
440
+ DROP TABLE IF EXISTS fortress_login_identifier;
441
+ DROP TABLE IF EXISTS fortress_user;
442
+ `.trim();
443
+ var PG_0002_UP = `
444
+ CREATE TABLE IF NOT EXISTS fortress_user (
445
+ id SERIAL PRIMARY KEY,
446
+ email VARCHAR(255) NOT NULL UNIQUE,
447
+ name VARCHAR(255) NOT NULL,
448
+ password_hash TEXT,
449
+ is_active BOOLEAN NOT NULL DEFAULT true,
450
+ email_verified BOOLEAN NOT NULL DEFAULT false,
451
+ created_at TIMESTAMP NOT NULL DEFAULT now(),
452
+ updated_at TIMESTAMP NOT NULL DEFAULT now()
453
+ );
454
+
455
+ CREATE TABLE IF NOT EXISTS fortress_login_identifier (
456
+ id SERIAL PRIMARY KEY,
457
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
458
+ type VARCHAR(20) NOT NULL,
459
+ value VARCHAR(255) NOT NULL UNIQUE
460
+ );
461
+
462
+ CREATE TABLE IF NOT EXISTS fortress_refresh_token (
463
+ id BIGSERIAL PRIMARY KEY,
464
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
465
+ token_hash VARCHAR(64) NOT NULL UNIQUE,
466
+ token_family VARCHAR(64) NOT NULL,
467
+ is_revoked BOOLEAN NOT NULL DEFAULT false,
468
+ expires_at TIMESTAMP NOT NULL,
469
+ ip_address VARCHAR(45),
470
+ user_agent TEXT,
471
+ device_name TEXT,
472
+ last_active_at TIMESTAMP,
473
+ fingerprint_hash VARCHAR(64),
474
+ created_at TIMESTAMP NOT NULL DEFAULT now()
475
+ );
476
+
477
+ CREATE TABLE IF NOT EXISTS fortress_group (
478
+ id SERIAL PRIMARY KEY,
479
+ name VARCHAR(100) NOT NULL UNIQUE,
480
+ description TEXT
481
+ );
482
+
483
+ CREATE TABLE IF NOT EXISTS fortress_group_user (
484
+ group_id INTEGER NOT NULL REFERENCES fortress_group(id) ON DELETE CASCADE,
485
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
486
+ PRIMARY KEY (group_id, user_id)
487
+ );
488
+
489
+ CREATE TABLE IF NOT EXISTS fortress_service_account (
490
+ id SERIAL PRIMARY KEY,
491
+ name VARCHAR(100) NOT NULL UNIQUE,
492
+ display_name VARCHAR(255),
493
+ description TEXT,
494
+ is_active BOOLEAN NOT NULL DEFAULT true,
495
+ created_at TIMESTAMP NOT NULL DEFAULT now(),
496
+ updated_at TIMESTAMP NOT NULL DEFAULT now()
497
+ );
498
+
499
+ CREATE TABLE IF NOT EXISTS fortress_resource (
500
+ name VARCHAR(100) PRIMARY KEY,
501
+ description TEXT
502
+ );
503
+
504
+ CREATE TABLE IF NOT EXISTS fortress_permission (
505
+ id SERIAL PRIMARY KEY,
506
+ resource VARCHAR(100) NOT NULL REFERENCES fortress_resource(name) ON DELETE CASCADE,
507
+ action VARCHAR(100) NOT NULL,
508
+ effect VARCHAR(10) NOT NULL DEFAULT 'ALLOW',
509
+ conditions JSONB,
510
+ description TEXT
511
+ );
512
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_permission_no_conditions
513
+ ON fortress_permission (resource, action, effect)
514
+ WHERE conditions IS NULL;
515
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_permission_with_conditions
516
+ ON fortress_permission (resource, action, effect, conditions)
517
+ WHERE conditions IS NOT NULL;
518
+
519
+ CREATE TABLE IF NOT EXISTS fortress_role (
520
+ id SERIAL PRIMARY KEY,
521
+ name VARCHAR(100) NOT NULL UNIQUE,
522
+ description TEXT,
523
+ is_system BOOLEAN NOT NULL DEFAULT false
524
+ );
525
+
526
+ CREATE TABLE IF NOT EXISTS fortress_role_permission (
527
+ role_id INTEGER NOT NULL REFERENCES fortress_role(id) ON DELETE CASCADE,
528
+ permission_id INTEGER NOT NULL REFERENCES fortress_permission(id) ON DELETE CASCADE,
529
+ PRIMARY KEY (role_id, permission_id)
530
+ );
531
+
532
+ CREATE TABLE IF NOT EXISTS fortress_role_binding (
533
+ id SERIAL PRIMARY KEY,
534
+ role_id INTEGER NOT NULL REFERENCES fortress_role(id) ON DELETE CASCADE,
535
+ subject_type VARCHAR(20) NOT NULL,
536
+ subject_id INTEGER NOT NULL,
537
+ tenant_id VARCHAR(100),
538
+ UNIQUE (role_id, subject_type, subject_id, tenant_id)
539
+ );
540
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_role_binding_global
541
+ ON fortress_role_binding (role_id, subject_type, subject_id)
542
+ WHERE tenant_id IS NULL;
543
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_role_binding_tenant
544
+ ON fortress_role_binding (role_id, subject_type, subject_id, tenant_id)
545
+ WHERE tenant_id IS NOT NULL;
546
+
547
+ CREATE TABLE IF NOT EXISTS fortress_direct_permission_binding (
548
+ id SERIAL PRIMARY KEY,
549
+ permission_id INTEGER NOT NULL REFERENCES fortress_permission(id) ON DELETE CASCADE,
550
+ subject_type VARCHAR(20) NOT NULL,
551
+ subject_id INTEGER NOT NULL,
552
+ tenant_id VARCHAR(100),
553
+ UNIQUE (permission_id, subject_type, subject_id, tenant_id)
554
+ );
555
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_direct_permission_binding_global
556
+ ON fortress_direct_permission_binding (permission_id, subject_type, subject_id)
557
+ WHERE tenant_id IS NULL;
558
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_direct_permission_binding_tenant
559
+ ON fortress_direct_permission_binding (permission_id, subject_type, subject_id, tenant_id)
560
+ WHERE tenant_id IS NOT NULL;
561
+
562
+ CREATE TABLE IF NOT EXISTS fortress_email_verification_token (
563
+ id BIGSERIAL PRIMARY KEY,
564
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
565
+ token TEXT NOT NULL,
566
+ email VARCHAR(255) NOT NULL,
567
+ expires_at TIMESTAMP NOT NULL,
568
+ used_at TIMESTAMP,
569
+ created_at TIMESTAMP NOT NULL DEFAULT now()
570
+ );
571
+
572
+ CREATE TABLE IF NOT EXISTS fortress_magic_link_token (
573
+ id BIGSERIAL PRIMARY KEY,
574
+ email VARCHAR(255) NOT NULL,
575
+ token VARCHAR(64) NOT NULL,
576
+ expires_at TIMESTAMP NOT NULL,
577
+ used_at TIMESTAMP,
578
+ created_at TIMESTAMP NOT NULL DEFAULT now()
579
+ );
580
+
581
+ CREATE TABLE IF NOT EXISTS fortress_api_key (
582
+ id SERIAL PRIMARY KEY,
583
+ subject_type VARCHAR(20) NOT NULL,
584
+ subject_id INTEGER NOT NULL,
585
+ name VARCHAR(255) NOT NULL,
586
+ key_hash VARCHAR(64) NOT NULL UNIQUE,
587
+ key_prefix VARCHAR(20) NOT NULL,
588
+ scopes TEXT,
589
+ expires_at TIMESTAMP,
590
+ last_used_at TIMESTAMP,
591
+ is_revoked BOOLEAN NOT NULL DEFAULT false,
592
+ created_at TIMESTAMP NOT NULL DEFAULT now()
593
+ );
594
+ CREATE INDEX IF NOT EXISTS api_key_subject_idx ON fortress_api_key (subject_type, subject_id);
595
+
596
+ CREATE TABLE IF NOT EXISTS fortress_two_factor_secret (
597
+ id SERIAL PRIMARY KEY,
598
+ user_id INTEGER NOT NULL UNIQUE REFERENCES fortress_user(id) ON DELETE CASCADE,
599
+ secret TEXT NOT NULL,
600
+ is_enabled BOOLEAN NOT NULL DEFAULT false,
601
+ last_used_counter INTEGER,
602
+ created_at TIMESTAMP NOT NULL DEFAULT now()
603
+ );
604
+
605
+ CREATE TABLE IF NOT EXISTS fortress_backup_code (
606
+ id SERIAL PRIMARY KEY,
607
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
608
+ code_hash TEXT NOT NULL,
609
+ is_used BOOLEAN NOT NULL DEFAULT false,
610
+ created_at TIMESTAMP NOT NULL DEFAULT now()
611
+ );
612
+
613
+ CREATE TABLE IF NOT EXISTS fortress_trusted_device (
614
+ id SERIAL PRIMARY KEY,
615
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
616
+ device_hash VARCHAR(64) NOT NULL,
617
+ expires_at TIMESTAMP NOT NULL,
618
+ last_used_at TIMESTAMP NOT NULL,
619
+ created_at TIMESTAMP NOT NULL DEFAULT now()
620
+ );
621
+
622
+ CREATE TABLE IF NOT EXISTS fortress_social_account (
623
+ id SERIAL PRIMARY KEY,
624
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
625
+ provider VARCHAR(50) NOT NULL,
626
+ provider_account_id VARCHAR(255) NOT NULL,
627
+ email VARCHAR(255),
628
+ access_token TEXT,
629
+ refresh_token TEXT,
630
+ token_expires_at TIMESTAMP,
631
+ profile JSONB,
632
+ created_at TIMESTAMP NOT NULL DEFAULT now(),
633
+ updated_at TIMESTAMP NOT NULL DEFAULT now(),
634
+ UNIQUE (user_id, provider),
635
+ UNIQUE (provider, provider_account_id)
636
+ );
637
+
638
+ CREATE TABLE IF NOT EXISTS fortress_tenant (
639
+ id SERIAL PRIMARY KEY,
640
+ name VARCHAR(255) NOT NULL,
641
+ tax_id VARCHAR(100) NOT NULL UNIQUE,
642
+ description TEXT,
643
+ created_at TIMESTAMP NOT NULL DEFAULT now(),
644
+ updated_at TIMESTAMP NOT NULL DEFAULT now()
645
+ );
646
+
647
+ CREATE TABLE IF NOT EXISTS fortress_tenant_user (
648
+ tenant_id INTEGER NOT NULL REFERENCES fortress_tenant(id) ON DELETE CASCADE,
649
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
650
+ is_default BOOLEAN NOT NULL DEFAULT false,
651
+ PRIMARY KEY (tenant_id, user_id)
652
+ );
653
+ CREATE UNIQUE INDEX IF NOT EXISTS fortress_tenant_user_one_default_idx
654
+ ON fortress_tenant_user (user_id) WHERE is_default = true;
655
+
656
+ CREATE TABLE IF NOT EXISTS fortress_oauth_client (
657
+ id SERIAL PRIMARY KEY,
658
+ client_id VARCHAR(255) NOT NULL UNIQUE,
659
+ client_secret_hash TEXT NOT NULL,
660
+ name VARCHAR(255) NOT NULL,
661
+ redirect_uris TEXT NOT NULL,
662
+ grant_types TEXT NOT NULL,
663
+ allowed_scopes TEXT,
664
+ token_endpoint_auth_method TEXT,
665
+ created_at TIMESTAMP NOT NULL DEFAULT now()
666
+ );
667
+
668
+ CREATE TABLE IF NOT EXISTS fortress_oauth_authorization_code (
669
+ id BIGSERIAL PRIMARY KEY,
670
+ code VARCHAR(255) NOT NULL UNIQUE,
671
+ client_id VARCHAR(255) NOT NULL,
672
+ user_id INTEGER NOT NULL,
673
+ redirect_uri TEXT NOT NULL,
674
+ scope TEXT,
675
+ code_challenge TEXT,
676
+ code_challenge_method VARCHAR(10),
677
+ nonce TEXT,
678
+ auth_time INTEGER,
679
+ expires_at TIMESTAMP NOT NULL,
680
+ used_at TIMESTAMP,
681
+ created_at TIMESTAMP NOT NULL DEFAULT now()
682
+ );
683
+
684
+ CREATE TABLE IF NOT EXISTS fortress_oauth_access_token (
685
+ id BIGSERIAL PRIMARY KEY,
686
+ token VARCHAR(255) NOT NULL UNIQUE,
687
+ client_id VARCHAR(255) NOT NULL,
688
+ user_id INTEGER,
689
+ scope TEXT,
690
+ expires_at TIMESTAMP NOT NULL,
691
+ created_at TIMESTAMP NOT NULL DEFAULT now()
692
+ );
693
+
694
+ CREATE TABLE IF NOT EXISTS fortress_oauth_refresh_token (
695
+ id BIGSERIAL PRIMARY KEY,
696
+ token VARCHAR(255) NOT NULL UNIQUE,
697
+ family_id VARCHAR(64) NOT NULL,
698
+ client_id VARCHAR(255) NOT NULL,
699
+ user_id INTEGER NOT NULL,
700
+ scope TEXT,
701
+ issued_at TIMESTAMP NOT NULL,
702
+ expires_at TIMESTAMP NOT NULL,
703
+ used_at TIMESTAMP,
704
+ parent_id INTEGER,
705
+ created_at TIMESTAMP NOT NULL DEFAULT now()
706
+ );
707
+
708
+ CREATE TABLE IF NOT EXISTS fortress_oauth_pending_flow (
709
+ id BIGSERIAL PRIMARY KEY,
710
+ flow_id TEXT NOT NULL UNIQUE,
711
+ client_id VARCHAR(255) NOT NULL,
712
+ redirect_uri TEXT NOT NULL,
713
+ scope TEXT,
714
+ state VARCHAR(255) NOT NULL,
715
+ code_challenge TEXT,
716
+ code_challenge_method VARCHAR(10),
717
+ nonce TEXT,
718
+ user_id INTEGER,
719
+ used_at TIMESTAMP,
720
+ expires_at TIMESTAMP NOT NULL,
721
+ created_at TIMESTAMP NOT NULL DEFAULT now()
722
+ );
723
+
724
+ CREATE TABLE IF NOT EXISTS fortress_oauth_signing_key (
725
+ id SERIAL PRIMARY KEY,
726
+ kid VARCHAR(64) NOT NULL UNIQUE,
727
+ alg VARCHAR(16) NOT NULL,
728
+ public_jwk TEXT NOT NULL,
729
+ private_jwk TEXT NOT NULL,
730
+ rotated_at TIMESTAMP,
731
+ created_at TIMESTAMP NOT NULL DEFAULT now()
732
+ );
733
+
734
+ CREATE TABLE IF NOT EXISTS fortress_user_scope_assignment (
735
+ id SERIAL PRIMARY KEY,
736
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
737
+ scope_name VARCHAR(100) NOT NULL,
738
+ scope_value VARCHAR(255) NOT NULL,
739
+ created_at TIMESTAMP NOT NULL DEFAULT now()
740
+ );
741
+
742
+ CREATE TABLE IF NOT EXISTS fortress_account_lockout (
743
+ id SERIAL PRIMARY KEY,
744
+ identifier VARCHAR(255) NOT NULL UNIQUE,
745
+ failed_attempts INTEGER NOT NULL DEFAULT 0,
746
+ last_failed_at TIMESTAMP,
747
+ locked_until TIMESTAMP,
748
+ lockout_count INTEGER NOT NULL DEFAULT 0,
749
+ created_at TIMESTAMP NOT NULL DEFAULT now()
750
+ );
751
+
752
+ CREATE TABLE IF NOT EXISTS fortress_audit_log (
753
+ id BIGSERIAL PRIMARY KEY,
754
+ timestamp TIMESTAMP NOT NULL DEFAULT now(),
755
+ event_type VARCHAR(100) NOT NULL,
756
+ actor_id INTEGER,
757
+ actor_type VARCHAR(20) NOT NULL DEFAULT 'USER',
758
+ target_id INTEGER,
759
+ target_type VARCHAR(50),
760
+ ip_address VARCHAR(45),
761
+ user_agent TEXT,
762
+ outcome VARCHAR(20) NOT NULL DEFAULT 'SUCCESS',
763
+ metadata JSONB,
764
+ previous_hash TEXT,
765
+ created_at TIMESTAMP NOT NULL DEFAULT now()
766
+ );
767
+
768
+ CREATE TABLE IF NOT EXISTS fortress_webhook_endpoint (
769
+ id SERIAL PRIMARY KEY,
770
+ url TEXT NOT NULL,
771
+ events TEXT NOT NULL,
772
+ secret TEXT NOT NULL,
773
+ is_active BOOLEAN NOT NULL DEFAULT true,
774
+ deactivated_reason TEXT,
775
+ consecutive_failures INTEGER NOT NULL DEFAULT 0,
776
+ created_at TIMESTAMP NOT NULL DEFAULT now()
777
+ );
778
+
779
+ CREATE TABLE IF NOT EXISTS fortress_webhook_delivery (
780
+ id BIGSERIAL PRIMARY KEY,
781
+ endpoint_id INTEGER NOT NULL REFERENCES fortress_webhook_endpoint(id) ON DELETE CASCADE,
782
+ event_type VARCHAR(100) NOT NULL,
783
+ payload TEXT NOT NULL,
784
+ status VARCHAR(20) NOT NULL DEFAULT 'pending',
785
+ attempts INTEGER NOT NULL DEFAULT 0,
786
+ idempotency_key TEXT,
787
+ last_attempt_at TIMESTAMP,
788
+ next_retry_at TIMESTAMP,
789
+ response_status INTEGER,
790
+ response_body TEXT,
791
+ error_kind TEXT,
792
+ created_at TIMESTAMP NOT NULL DEFAULT now()
793
+ );
794
+
795
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_webhook_delivery_idempotency
796
+ ON fortress_webhook_delivery (endpoint_id, idempotency_key)
797
+ WHERE idempotency_key IS NOT NULL;
798
+
799
+ CREATE TABLE IF NOT EXISTS fortress_webauthn_credential (
800
+ id SERIAL PRIMARY KEY,
801
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
802
+ credential_id TEXT NOT NULL UNIQUE,
803
+ public_key TEXT NOT NULL,
804
+ counter INTEGER NOT NULL DEFAULT 0,
805
+ device_type VARCHAR(20) NOT NULL,
806
+ backed_up BOOLEAN NOT NULL DEFAULT false,
807
+ transports TEXT,
808
+ created_at TIMESTAMP NOT NULL DEFAULT now()
809
+ );
810
+
811
+ CREATE TABLE IF NOT EXISTS fortress_webauthn_challenge (
812
+ id BIGSERIAL PRIMARY KEY,
813
+ challenge TEXT NOT NULL UNIQUE,
814
+ user_id INTEGER,
815
+ expires_at TIMESTAMP NOT NULL,
816
+ created_at TIMESTAMP NOT NULL DEFAULT now()
817
+ );
818
+ `.trim();
819
+ var PG_0002_DOWN = `
820
+ DROP TABLE IF EXISTS fortress_webauthn_challenge CASCADE;
821
+ DROP TABLE IF EXISTS fortress_webauthn_credential CASCADE;
822
+ DROP TABLE IF EXISTS fortress_webhook_delivery CASCADE;
823
+ DROP TABLE IF EXISTS fortress_webhook_endpoint CASCADE;
824
+ DROP TABLE IF EXISTS fortress_audit_log CASCADE;
825
+ DROP TABLE IF EXISTS fortress_account_lockout CASCADE;
826
+ DROP TABLE IF EXISTS fortress_user_scope_assignment CASCADE;
827
+ DROP TABLE IF EXISTS fortress_oauth_signing_key CASCADE;
828
+ DROP TABLE IF EXISTS fortress_oauth_pending_flow CASCADE;
829
+ DROP TABLE IF EXISTS fortress_oauth_refresh_token CASCADE;
830
+ DROP TABLE IF EXISTS fortress_oauth_access_token CASCADE;
831
+ DROP TABLE IF EXISTS fortress_oauth_authorization_code CASCADE;
832
+ DROP TABLE IF EXISTS fortress_oauth_client CASCADE;
833
+ DROP TABLE IF EXISTS fortress_tenant_user CASCADE;
834
+ DROP TABLE IF EXISTS fortress_tenant CASCADE;
835
+ DROP TABLE IF EXISTS fortress_social_account CASCADE;
836
+ DROP TABLE IF EXISTS fortress_trusted_device CASCADE;
837
+ DROP TABLE IF EXISTS fortress_backup_code CASCADE;
838
+ DROP TABLE IF EXISTS fortress_two_factor_secret CASCADE;
839
+ DROP TABLE IF EXISTS fortress_api_key CASCADE;
840
+ DROP TABLE IF EXISTS fortress_magic_link_token CASCADE;
841
+ DROP TABLE IF EXISTS fortress_email_verification_token CASCADE;
842
+ DROP TABLE IF EXISTS fortress_direct_permission_binding CASCADE;
843
+ DROP TABLE IF EXISTS fortress_role_binding CASCADE;
844
+ DROP TABLE IF EXISTS fortress_role_permission CASCADE;
845
+ DROP TABLE IF EXISTS fortress_role CASCADE;
846
+ DROP TABLE IF EXISTS fortress_permission CASCADE;
847
+ DROP TABLE IF EXISTS fortress_resource CASCADE;
848
+ DROP TABLE IF EXISTS fortress_service_account CASCADE;
849
+ DROP TABLE IF EXISTS fortress_group_user CASCADE;
850
+ DROP TABLE IF EXISTS fortress_group CASCADE;
851
+ DROP TABLE IF EXISTS fortress_refresh_token CASCADE;
852
+ DROP TABLE IF EXISTS fortress_login_identifier CASCADE;
853
+ DROP TABLE IF EXISTS fortress_user CASCADE;
854
+ `.trim();
855
+ var SQLITE_0003_UP = `
856
+ CREATE TABLE fortress_refresh_token_v3 (
857
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
858
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
859
+ token_hash TEXT NOT NULL UNIQUE,
860
+ token_family TEXT NOT NULL,
861
+ family_created_at INTEGER NOT NULL DEFAULT (unixepoch()),
862
+ successor_token_hash TEXT,
863
+ rotated_at INTEGER,
864
+ is_revoked INTEGER NOT NULL DEFAULT 0,
865
+ expires_at INTEGER NOT NULL,
866
+ ip_address TEXT,
867
+ user_agent TEXT,
868
+ device_name TEXT,
869
+ last_active_at INTEGER,
870
+ fingerprint_hash TEXT,
871
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
872
+ );
873
+ INSERT INTO fortress_refresh_token_v3 (
874
+ id, user_id, token_hash, token_family, family_created_at,
875
+ successor_token_hash, rotated_at, is_revoked, expires_at, ip_address,
876
+ user_agent, device_name, last_active_at, fingerprint_hash, created_at
877
+ )
878
+ SELECT
879
+ token.id, token.user_id, token.token_hash, token.token_family,
880
+ (SELECT MIN(family.created_at)
881
+ FROM fortress_refresh_token AS family
882
+ WHERE family.token_family = token.token_family),
883
+ NULL, NULL, token.is_revoked, token.expires_at, token.ip_address,
884
+ token.user_agent, token.device_name, token.last_active_at,
885
+ token.fingerprint_hash, token.created_at
886
+ FROM fortress_refresh_token AS token;
887
+ DROP TABLE fortress_refresh_token;
888
+ ALTER TABLE fortress_refresh_token_v3 RENAME TO fortress_refresh_token;
889
+
890
+ CREATE TABLE fortress_auth_continuation (
891
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
892
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
893
+ token_hash TEXT NOT NULL UNIQUE,
894
+ reason TEXT NOT NULL,
895
+ expires_at INTEGER NOT NULL,
896
+ consumed_at INTEGER,
897
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
898
+ );
899
+ `.trim();
900
+ var SQLITE_0003_DOWN = `
901
+ DROP TABLE IF EXISTS fortress_auth_continuation;
902
+ CREATE TABLE fortress_refresh_token_v2 (
903
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
904
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
905
+ token_hash TEXT NOT NULL UNIQUE,
906
+ token_family TEXT NOT NULL,
907
+ is_revoked INTEGER NOT NULL DEFAULT 0,
908
+ expires_at INTEGER NOT NULL,
909
+ ip_address TEXT,
910
+ user_agent TEXT,
911
+ device_name TEXT,
912
+ last_active_at INTEGER,
913
+ fingerprint_hash TEXT,
914
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
915
+ );
916
+ INSERT INTO fortress_refresh_token_v2 (
917
+ id, user_id, token_hash, token_family, is_revoked, expires_at,
918
+ ip_address, user_agent, device_name, last_active_at, fingerprint_hash, created_at
919
+ )
920
+ SELECT
921
+ id, user_id, token_hash, token_family, is_revoked, expires_at,
922
+ ip_address, user_agent, device_name, last_active_at, fingerprint_hash, created_at
923
+ FROM fortress_refresh_token;
924
+ DROP TABLE fortress_refresh_token;
925
+ ALTER TABLE fortress_refresh_token_v2 RENAME TO fortress_refresh_token;
926
+ `.trim();
927
+ var PG_0003_UP = `
928
+ ALTER TABLE fortress_refresh_token
929
+ ADD COLUMN family_created_at TIMESTAMP;
930
+ UPDATE fortress_refresh_token AS token
931
+ SET family_created_at = family.created_at
932
+ FROM (
933
+ SELECT token_family, MIN(created_at) AS created_at
934
+ FROM fortress_refresh_token
935
+ GROUP BY token_family
936
+ ) AS family
937
+ WHERE token.token_family = family.token_family;
938
+ ALTER TABLE fortress_refresh_token
939
+ ALTER COLUMN family_created_at SET NOT NULL;
940
+ ALTER TABLE fortress_refresh_token
941
+ ALTER COLUMN family_created_at SET DEFAULT now();
942
+ ALTER TABLE fortress_refresh_token
943
+ ADD COLUMN successor_token_hash VARCHAR(64);
944
+ ALTER TABLE fortress_refresh_token
945
+ ADD COLUMN rotated_at TIMESTAMP;
946
+
947
+ CREATE TABLE fortress_auth_continuation (
948
+ id BIGSERIAL PRIMARY KEY,
949
+ user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
950
+ token_hash VARCHAR(64) NOT NULL UNIQUE,
951
+ reason VARCHAR(32) NOT NULL,
952
+ expires_at TIMESTAMP NOT NULL,
953
+ consumed_at TIMESTAMP,
954
+ created_at TIMESTAMP NOT NULL DEFAULT now()
955
+ );
956
+ `.trim();
957
+ var PG_0003_DOWN = `
958
+ DROP TABLE IF EXISTS fortress_auth_continuation CASCADE;
959
+ ALTER TABLE fortress_refresh_token DROP COLUMN rotated_at;
960
+ ALTER TABLE fortress_refresh_token DROP COLUMN successor_token_hash;
961
+ ALTER TABLE fortress_refresh_token DROP COLUMN family_created_at;
962
+ `.trim();
963
+ var SQLITE_0004_UP = `
964
+ UPDATE fortress_tenant_user
965
+ SET is_default = 0
966
+ WHERE is_default = 1
967
+ AND rowid NOT IN (
968
+ SELECT MIN(rowid)
969
+ FROM fortress_tenant_user
970
+ WHERE is_default = 1
971
+ GROUP BY user_id
972
+ );
973
+ CREATE UNIQUE INDEX IF NOT EXISTS fortress_tenant_user_one_default_idx
974
+ ON fortress_tenant_user (user_id)
975
+ WHERE is_default = 1;
976
+ `.trim();
977
+ var SQLITE_0004_DOWN = `
978
+ DROP INDEX IF EXISTS fortress_tenant_user_one_default_idx;
979
+ `.trim();
980
+ var PG_0004_UP = `
981
+ WITH ranked_defaults AS (
982
+ SELECT ctid, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY tenant_id) AS position
983
+ FROM fortress_tenant_user
984
+ WHERE is_default = true
985
+ )
986
+ UPDATE fortress_tenant_user AS membership
987
+ SET is_default = false
988
+ FROM ranked_defaults
989
+ WHERE membership.ctid = ranked_defaults.ctid
990
+ AND ranked_defaults.position > 1;
991
+ CREATE UNIQUE INDEX IF NOT EXISTS fortress_tenant_user_one_default_idx
992
+ ON fortress_tenant_user (user_id)
993
+ WHERE is_default = true;
994
+ `.trim();
995
+ var PG_0004_DOWN = `
996
+ DROP INDEX IF EXISTS fortress_tenant_user_one_default_idx;
997
+ `.trim();
998
+ var SQLITE_0005_UP = `
999
+ CREATE INDEX refresh_token_family_idx ON fortress_refresh_token (token_family);
1000
+ CREATE INDEX refresh_token_user_idx ON fortress_refresh_token (user_id);
1001
+ CREATE INDEX email_verification_token_token_idx ON fortress_email_verification_token (token);
1002
+ CREATE INDEX magic_link_token_token_idx ON fortress_magic_link_token (token);
1003
+ CREATE INDEX role_binding_subject_idx ON fortress_role_binding (subject_type, subject_id);
1004
+ CREATE INDEX direct_permission_binding_subject_idx ON fortress_direct_permission_binding (subject_type, subject_id);
1005
+ CREATE INDEX backup_code_user_idx ON fortress_backup_code (user_id);
1006
+ CREATE INDEX trusted_device_user_idx ON fortress_trusted_device (user_id);
1007
+ CREATE INDEX webhook_delivery_retry_idx ON fortress_webhook_delivery (status, next_retry_at);
1008
+ CREATE INDEX audit_log_timestamp_idx ON fortress_audit_log (timestamp);
1009
+ `.trim();
1010
+ var SQLITE_0005_DOWN = `
1011
+ DROP INDEX IF EXISTS audit_log_timestamp_idx;
1012
+ DROP INDEX IF EXISTS webhook_delivery_retry_idx;
1013
+ DROP INDEX IF EXISTS trusted_device_user_idx;
1014
+ DROP INDEX IF EXISTS backup_code_user_idx;
1015
+ DROP INDEX IF EXISTS direct_permission_binding_subject_idx;
1016
+ DROP INDEX IF EXISTS role_binding_subject_idx;
1017
+ DROP INDEX IF EXISTS magic_link_token_token_idx;
1018
+ DROP INDEX IF EXISTS email_verification_token_token_idx;
1019
+ DROP INDEX IF EXISTS refresh_token_user_idx;
1020
+ DROP INDEX IF EXISTS refresh_token_family_idx;
1021
+ `.trim();
1022
+ var PG_0005_UP = `
1023
+ CREATE INDEX refresh_token_family_idx ON fortress_refresh_token (token_family);
1024
+ CREATE INDEX refresh_token_user_idx ON fortress_refresh_token (user_id);
1025
+ CREATE INDEX email_verification_token_token_idx ON fortress_email_verification_token (token);
1026
+ CREATE INDEX magic_link_token_token_idx ON fortress_magic_link_token (token);
1027
+ CREATE INDEX role_binding_subject_idx ON fortress_role_binding (subject_type, subject_id);
1028
+ CREATE INDEX direct_permission_binding_subject_idx ON fortress_direct_permission_binding (subject_type, subject_id);
1029
+ CREATE INDEX backup_code_user_idx ON fortress_backup_code (user_id);
1030
+ CREATE INDEX trusted_device_user_idx ON fortress_trusted_device (user_id);
1031
+ CREATE INDEX webhook_delivery_retry_idx ON fortress_webhook_delivery (status, next_retry_at);
1032
+ CREATE INDEX audit_log_timestamp_idx ON fortress_audit_log (timestamp);
1033
+
1034
+ ALTER TABLE fortress_schema_version ALTER COLUMN applied_at TYPE TIMESTAMPTZ USING applied_at AT TIME ZONE 'UTC';
1035
+ ALTER TABLE fortress_user ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1036
+ ALTER TABLE fortress_user ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
1037
+ ALTER TABLE fortress_refresh_token ALTER COLUMN family_created_at TYPE TIMESTAMPTZ USING family_created_at AT TIME ZONE 'UTC';
1038
+ ALTER TABLE fortress_refresh_token ALTER COLUMN rotated_at TYPE TIMESTAMPTZ USING rotated_at AT TIME ZONE 'UTC';
1039
+ ALTER TABLE fortress_refresh_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1040
+ ALTER TABLE fortress_refresh_token ALTER COLUMN last_active_at TYPE TIMESTAMPTZ USING last_active_at AT TIME ZONE 'UTC';
1041
+ ALTER TABLE fortress_refresh_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1042
+ ALTER TABLE fortress_auth_continuation ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1043
+ ALTER TABLE fortress_auth_continuation ALTER COLUMN consumed_at TYPE TIMESTAMPTZ USING consumed_at AT TIME ZONE 'UTC';
1044
+ ALTER TABLE fortress_auth_continuation ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1045
+ ALTER TABLE fortress_service_account ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1046
+ ALTER TABLE fortress_service_account ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
1047
+ ALTER TABLE fortress_email_verification_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1048
+ ALTER TABLE fortress_email_verification_token ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
1049
+ ALTER TABLE fortress_email_verification_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1050
+ ALTER TABLE fortress_magic_link_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1051
+ ALTER TABLE fortress_magic_link_token ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
1052
+ ALTER TABLE fortress_magic_link_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1053
+ ALTER TABLE fortress_api_key ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1054
+ ALTER TABLE fortress_api_key ALTER COLUMN last_used_at TYPE TIMESTAMPTZ USING last_used_at AT TIME ZONE 'UTC';
1055
+ ALTER TABLE fortress_api_key ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1056
+ ALTER TABLE fortress_two_factor_secret ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1057
+ ALTER TABLE fortress_backup_code ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1058
+ ALTER TABLE fortress_trusted_device ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1059
+ ALTER TABLE fortress_trusted_device ALTER COLUMN last_used_at TYPE TIMESTAMPTZ USING last_used_at AT TIME ZONE 'UTC';
1060
+ ALTER TABLE fortress_trusted_device ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1061
+ ALTER TABLE fortress_social_account ALTER COLUMN token_expires_at TYPE TIMESTAMPTZ USING token_expires_at AT TIME ZONE 'UTC';
1062
+ ALTER TABLE fortress_social_account ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1063
+ ALTER TABLE fortress_social_account ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
1064
+ ALTER TABLE fortress_tenant ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1065
+ ALTER TABLE fortress_tenant ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
1066
+ ALTER TABLE fortress_oauth_client ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1067
+ ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1068
+ ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
1069
+ ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1070
+ ALTER TABLE fortress_oauth_access_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1071
+ ALTER TABLE fortress_oauth_access_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1072
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN issued_at TYPE TIMESTAMPTZ USING issued_at AT TIME ZONE 'UTC';
1073
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1074
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
1075
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1076
+ ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
1077
+ ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1078
+ ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1079
+ ALTER TABLE fortress_oauth_signing_key ALTER COLUMN rotated_at TYPE TIMESTAMPTZ USING rotated_at AT TIME ZONE 'UTC';
1080
+ ALTER TABLE fortress_oauth_signing_key ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1081
+ ALTER TABLE fortress_user_scope_assignment ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1082
+ ALTER TABLE fortress_account_lockout ALTER COLUMN last_failed_at TYPE TIMESTAMPTZ USING last_failed_at AT TIME ZONE 'UTC';
1083
+ ALTER TABLE fortress_account_lockout ALTER COLUMN locked_until TYPE TIMESTAMPTZ USING locked_until AT TIME ZONE 'UTC';
1084
+ ALTER TABLE fortress_account_lockout ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1085
+ ALTER TABLE fortress_audit_log ALTER COLUMN timestamp TYPE TIMESTAMPTZ USING timestamp AT TIME ZONE 'UTC';
1086
+ ALTER TABLE fortress_audit_log ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1087
+ ALTER TABLE fortress_webhook_endpoint ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1088
+ ALTER TABLE fortress_webhook_delivery ALTER COLUMN last_attempt_at TYPE TIMESTAMPTZ USING last_attempt_at AT TIME ZONE 'UTC';
1089
+ ALTER TABLE fortress_webhook_delivery ALTER COLUMN next_retry_at TYPE TIMESTAMPTZ USING next_retry_at AT TIME ZONE 'UTC';
1090
+ ALTER TABLE fortress_webhook_delivery ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1091
+ ALTER TABLE fortress_webauthn_credential ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1092
+ ALTER TABLE fortress_webauthn_challenge ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
1093
+ ALTER TABLE fortress_webauthn_challenge ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
1094
+ `.trim();
1095
+ var PG_0005_DOWN = `
1096
+ ALTER TABLE fortress_schema_version ALTER COLUMN applied_at TYPE TIMESTAMP USING applied_at AT TIME ZONE 'UTC';
1097
+ ALTER TABLE fortress_user ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1098
+ ALTER TABLE fortress_user ALTER COLUMN updated_at TYPE TIMESTAMP USING updated_at AT TIME ZONE 'UTC';
1099
+ ALTER TABLE fortress_refresh_token ALTER COLUMN family_created_at TYPE TIMESTAMP USING family_created_at AT TIME ZONE 'UTC';
1100
+ ALTER TABLE fortress_refresh_token ALTER COLUMN rotated_at TYPE TIMESTAMP USING rotated_at AT TIME ZONE 'UTC';
1101
+ ALTER TABLE fortress_refresh_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1102
+ ALTER TABLE fortress_refresh_token ALTER COLUMN last_active_at TYPE TIMESTAMP USING last_active_at AT TIME ZONE 'UTC';
1103
+ ALTER TABLE fortress_refresh_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1104
+ ALTER TABLE fortress_auth_continuation ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1105
+ ALTER TABLE fortress_auth_continuation ALTER COLUMN consumed_at TYPE TIMESTAMP USING consumed_at AT TIME ZONE 'UTC';
1106
+ ALTER TABLE fortress_auth_continuation ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1107
+ ALTER TABLE fortress_service_account ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1108
+ ALTER TABLE fortress_service_account ALTER COLUMN updated_at TYPE TIMESTAMP USING updated_at AT TIME ZONE 'UTC';
1109
+ ALTER TABLE fortress_email_verification_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1110
+ ALTER TABLE fortress_email_verification_token ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
1111
+ ALTER TABLE fortress_email_verification_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1112
+ ALTER TABLE fortress_magic_link_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1113
+ ALTER TABLE fortress_magic_link_token ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
1114
+ ALTER TABLE fortress_magic_link_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1115
+ ALTER TABLE fortress_api_key ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1116
+ ALTER TABLE fortress_api_key ALTER COLUMN last_used_at TYPE TIMESTAMP USING last_used_at AT TIME ZONE 'UTC';
1117
+ ALTER TABLE fortress_api_key ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1118
+ ALTER TABLE fortress_two_factor_secret ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1119
+ ALTER TABLE fortress_backup_code ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1120
+ ALTER TABLE fortress_trusted_device ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1121
+ ALTER TABLE fortress_trusted_device ALTER COLUMN last_used_at TYPE TIMESTAMP USING last_used_at AT TIME ZONE 'UTC';
1122
+ ALTER TABLE fortress_trusted_device ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1123
+ ALTER TABLE fortress_social_account ALTER COLUMN token_expires_at TYPE TIMESTAMP USING token_expires_at AT TIME ZONE 'UTC';
1124
+ ALTER TABLE fortress_social_account ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1125
+ ALTER TABLE fortress_social_account ALTER COLUMN updated_at TYPE TIMESTAMP USING updated_at AT TIME ZONE 'UTC';
1126
+ ALTER TABLE fortress_tenant ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1127
+ ALTER TABLE fortress_tenant ALTER COLUMN updated_at TYPE TIMESTAMP USING updated_at AT TIME ZONE 'UTC';
1128
+ ALTER TABLE fortress_oauth_client ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1129
+ ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1130
+ ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
1131
+ ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1132
+ ALTER TABLE fortress_oauth_access_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1133
+ ALTER TABLE fortress_oauth_access_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1134
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN issued_at TYPE TIMESTAMP USING issued_at AT TIME ZONE 'UTC';
1135
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1136
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
1137
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1138
+ ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
1139
+ ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1140
+ ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1141
+ ALTER TABLE fortress_oauth_signing_key ALTER COLUMN rotated_at TYPE TIMESTAMP USING rotated_at AT TIME ZONE 'UTC';
1142
+ ALTER TABLE fortress_oauth_signing_key ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1143
+ ALTER TABLE fortress_user_scope_assignment ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1144
+ ALTER TABLE fortress_account_lockout ALTER COLUMN last_failed_at TYPE TIMESTAMP USING last_failed_at AT TIME ZONE 'UTC';
1145
+ ALTER TABLE fortress_account_lockout ALTER COLUMN locked_until TYPE TIMESTAMP USING locked_until AT TIME ZONE 'UTC';
1146
+ ALTER TABLE fortress_account_lockout ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1147
+ ALTER TABLE fortress_audit_log ALTER COLUMN timestamp TYPE TIMESTAMP USING timestamp AT TIME ZONE 'UTC';
1148
+ ALTER TABLE fortress_audit_log ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1149
+ ALTER TABLE fortress_webhook_endpoint ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1150
+ ALTER TABLE fortress_webhook_delivery ALTER COLUMN last_attempt_at TYPE TIMESTAMP USING last_attempt_at AT TIME ZONE 'UTC';
1151
+ ALTER TABLE fortress_webhook_delivery ALTER COLUMN next_retry_at TYPE TIMESTAMP USING next_retry_at AT TIME ZONE 'UTC';
1152
+ ALTER TABLE fortress_webhook_delivery ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1153
+ ALTER TABLE fortress_webauthn_credential ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1154
+ ALTER TABLE fortress_webauthn_challenge ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
1155
+ ALTER TABLE fortress_webauthn_challenge ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
1156
+
1157
+ DROP INDEX IF EXISTS audit_log_timestamp_idx;
1158
+ DROP INDEX IF EXISTS webhook_delivery_retry_idx;
1159
+ DROP INDEX IF EXISTS trusted_device_user_idx;
1160
+ DROP INDEX IF EXISTS backup_code_user_idx;
1161
+ DROP INDEX IF EXISTS direct_permission_binding_subject_idx;
1162
+ DROP INDEX IF EXISTS role_binding_subject_idx;
1163
+ DROP INDEX IF EXISTS magic_link_token_token_idx;
1164
+ DROP INDEX IF EXISTS email_verification_token_token_idx;
1165
+ DROP INDEX IF EXISTS refresh_token_user_idx;
1166
+ DROP INDEX IF EXISTS refresh_token_family_idx;
1167
+ `.trim();
1168
+ async function normalizeExistingUserEmails(db) {
1169
+ const users3 = await db.findMany({
1170
+ model: "user",
1171
+ sortBy: { field: "id", direction: "asc" }
1172
+ });
1173
+ const identifiers = await db.findMany({
1174
+ model: "login_identifier",
1175
+ sortBy: { field: "id", direction: "asc" }
1176
+ });
1177
+ const groups3 = /* @__PURE__ */ new Map();
1178
+ const usedCanonicalEmails = /* @__PURE__ */ new Set([
1179
+ ...users3.map((user) => normalizeEmail(user.email)),
1180
+ ...identifiers.map((identifier) => normalizeEmail(identifier.value))
1181
+ ]);
1182
+ for (const user of users3) {
1183
+ const canonical = normalizeEmail(user.email);
1184
+ const group = groups3.get(canonical) ?? [];
1185
+ group.push(user);
1186
+ groups3.set(canonical, group);
1187
+ }
1188
+ const tombstoneFor = (id2) => {
1189
+ let attempt = 0;
1190
+ let value = `fortress-duplicate-${id2}@invalid`;
1191
+ while (usedCanonicalEmails.has(normalizeEmail(value))) {
1192
+ attempt++;
1193
+ value = `fortress-duplicate-${id2}-${attempt}@invalid`;
1194
+ }
1195
+ usedCanonicalEmails.add(normalizeEmail(value));
1196
+ return value;
1197
+ };
1198
+ const duplicateUserIds = /* @__PURE__ */ new Set();
1199
+ const primaryOwnerByCanonical = /* @__PURE__ */ new Map();
1200
+ for (const [canonical, matches] of groups3) {
1201
+ const [survivor, ...duplicates] = matches;
1202
+ primaryOwnerByCanonical.set(canonical, survivor.id);
1203
+ for (const duplicate of duplicates) {
1204
+ duplicateUserIds.add(duplicate.id);
1205
+ const tombstone = tombstoneFor(duplicate.id);
1206
+ await db.update({
1207
+ model: "refresh_token",
1208
+ where: [{ field: "userId", operator: "=", value: duplicate.id }],
1209
+ data: { isRevoked: true }
1210
+ });
1211
+ for (const model of [
1212
+ "oauth_refresh_token",
1213
+ "oauth_access_token",
1214
+ "oauth_authorization_code",
1215
+ "oauth_pending_flow",
1216
+ "auth_continuation"
1217
+ ]) {
1218
+ await db.delete({
1219
+ model,
1220
+ where: [{ field: "userId", operator: "=", value: duplicate.id }]
1221
+ });
1222
+ }
1223
+ await db.update({
1224
+ model: "user",
1225
+ where: [{ field: "id", operator: "=", value: duplicate.id }],
1226
+ data: { email: tombstone, isActive: false }
1227
+ });
1228
+ }
1229
+ if (survivor.email !== canonical) {
1230
+ await db.update({
1231
+ model: "user",
1232
+ where: [{ field: "id", operator: "=", value: survivor.id }],
1233
+ data: { email: canonical }
1234
+ });
1235
+ }
1236
+ }
1237
+ const identifierGroups = /* @__PURE__ */ new Map();
1238
+ const occupiedNonEmailValues = /* @__PURE__ */ new Set();
1239
+ for (const identifier of identifiers) {
1240
+ if (duplicateUserIds.has(identifier.userId)) {
1241
+ await db.delete({
1242
+ model: "login_identifier",
1243
+ where: [{ field: "id", operator: "=", value: identifier.id }]
1244
+ });
1245
+ continue;
1246
+ }
1247
+ if (identifier.type !== "email") {
1248
+ if (primaryOwnerByCanonical.has(identifier.value)) {
1249
+ await db.delete({
1250
+ model: "login_identifier",
1251
+ where: [{ field: "id", operator: "=", value: identifier.id }]
1252
+ });
1253
+ } else {
1254
+ occupiedNonEmailValues.add(identifier.value);
1255
+ }
1256
+ continue;
1257
+ }
1258
+ const canonical = normalizeEmail(identifier.value);
1259
+ const group = identifierGroups.get(canonical) ?? [];
1260
+ group.push(identifier);
1261
+ identifierGroups.set(canonical, group);
1262
+ }
1263
+ for (const [canonical, matches] of identifierGroups) {
1264
+ const primaryOwner = primaryOwnerByCanonical.get(canonical);
1265
+ const survivor = occupiedNonEmailValues.has(canonical) ? void 0 : primaryOwner ? matches.find((identifier) => identifier.userId === primaryOwner) : matches[0];
1266
+ for (const duplicate of matches) {
1267
+ if (duplicate.id === survivor?.id)
1268
+ continue;
1269
+ await db.delete({
1270
+ model: "login_identifier",
1271
+ where: [{ field: "id", operator: "=", value: duplicate.id }]
1272
+ });
1273
+ }
1274
+ if (survivor && survivor.value !== canonical) {
1275
+ await db.update({
1276
+ model: "login_identifier",
1277
+ where: [{ field: "id", operator: "=", value: survivor.id }],
1278
+ data: { value: canonical }
1279
+ });
1280
+ }
1281
+ }
1282
+ if (!db.rawQuery)
1283
+ throw new Error("Email data migration requires rawQuery support");
1284
+ await db.rawQuery("CREATE TABLE fortress_email_migration_ready (id INTEGER PRIMARY KEY)");
1285
+ }
1286
+ var SQLITE_0006_UP = `
1287
+ -- This sentinel is created only by the Fortress migration engine after its
1288
+ -- Unicode-aware data step. Applying this SQL directly fails closed.
1289
+ INSERT INTO fortress_email_migration_ready (id) VALUES (1);
1290
+ CREATE UNIQUE INDEX user_email_ci_unique ON fortress_user (email COLLATE NOCASE);
1291
+ CREATE UNIQUE INDEX login_identifier_email_ci_unique
1292
+ ON fortress_login_identifier (value COLLATE NOCASE)
1293
+ WHERE type = 'email';
1294
+ DROP TABLE fortress_email_migration_ready;
1295
+ `.trim();
1296
+ var SQLITE_0006_FRESH_UP = `
1297
+ CREATE UNIQUE INDEX user_email_ci_unique ON fortress_user (email COLLATE NOCASE);
1298
+ CREATE UNIQUE INDEX login_identifier_email_ci_unique
1299
+ ON fortress_login_identifier (value COLLATE NOCASE)
1300
+ WHERE type = 'email';
1301
+ `.trim();
1302
+ var SQLITE_0006_DOWN = `
1303
+ -- Canonicalization and duplicate-account quarantine are intentionally irreversible.
1304
+ DROP INDEX IF EXISTS login_identifier_email_ci_unique;
1305
+ DROP INDEX IF EXISTS user_email_ci_unique;
1306
+ `.trim();
1307
+ var PG_0006_UP = `
1308
+ -- This sentinel is created only by the Fortress migration engine after its
1309
+ -- Unicode-aware data step. Applying this SQL directly fails closed.
1310
+ INSERT INTO fortress_email_migration_ready (id) VALUES (1);
1311
+ CREATE UNIQUE INDEX user_email_ci_unique ON fortress_user (lower(email));
1312
+ CREATE UNIQUE INDEX login_identifier_email_ci_unique
1313
+ ON fortress_login_identifier (lower(value))
1314
+ WHERE type = 'email';
1315
+ DROP TABLE fortress_email_migration_ready;
1316
+ `.trim();
1317
+ var PG_0006_FRESH_UP = `
1318
+ CREATE UNIQUE INDEX user_email_ci_unique ON fortress_user (lower(email));
1319
+ CREATE UNIQUE INDEX login_identifier_email_ci_unique
1320
+ ON fortress_login_identifier (lower(value))
1321
+ WHERE type = 'email';
1322
+ `.trim();
1323
+ var PG_0006_DOWN = `
1324
+ -- Canonicalization and duplicate-account quarantine are intentionally irreversible.
1325
+ DROP INDEX IF EXISTS login_identifier_email_ci_unique;
1326
+ DROP INDEX IF EXISTS user_email_ci_unique;
1327
+ `.trim();
1328
+ var SQLITE_0007_UP = `
1329
+ CREATE TABLE fortress_audit_chain_state (
1330
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1331
+ last_hash TEXT,
1332
+ entry_count INTEGER NOT NULL CHECK (entry_count >= 0),
1333
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
1334
+ CHECK ((entry_count = 0 AND last_hash IS NULL) OR (entry_count > 0 AND last_hash IS NOT NULL))
1335
+ );
1336
+ INSERT INTO fortress_audit_chain_state (id, last_hash, entry_count) VALUES (1, NULL, 0);
1337
+ `.trim();
1338
+ var SQLITE_0007_DOWN = "DROP TABLE IF EXISTS fortress_audit_chain_state;";
1339
+ var PG_0007_UP = `
1340
+ CREATE TABLE fortress_audit_chain_state (
1341
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1342
+ last_hash VARCHAR(64),
1343
+ entry_count INTEGER NOT NULL CHECK (entry_count >= 0),
1344
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
1345
+ CHECK ((entry_count = 0 AND last_hash IS NULL) OR (entry_count > 0 AND last_hash IS NOT NULL))
1346
+ );
1347
+ INSERT INTO fortress_audit_chain_state (id, last_hash, entry_count) VALUES (1, NULL, 0);
1348
+ `.trim();
1349
+ var PG_0007_DOWN = "DROP TABLE IF EXISTS fortress_audit_chain_state;";
1350
+ var SQLITE_0008_UP = `
1351
+ ALTER TABLE fortress_auth_continuation ADD COLUMN failed_attempts INTEGER NOT NULL DEFAULT 0;
1352
+ ALTER TABLE fortress_auth_continuation ADD COLUMN last_failed_at INTEGER;
1353
+ ALTER TABLE fortress_auth_continuation ADD COLUMN invalidated_at INTEGER;
1354
+ ALTER TABLE fortress_auth_continuation ADD COLUMN max_attempts INTEGER NOT NULL DEFAULT 5;
1355
+ ALTER TABLE fortress_auth_continuation ADD COLUMN cooldown_seconds INTEGER NOT NULL DEFAULT 1;
1356
+ CREATE INDEX auth_continuation_failure_idx ON fortress_auth_continuation (user_id, reason, last_failed_at);
1357
+ `.trim();
1358
+ var SQLITE_0008_DOWN = `
1359
+ CREATE TABLE fortress_auth_continuation_old AS SELECT id, user_id, token_hash, reason, expires_at, consumed_at, created_at FROM fortress_auth_continuation;
1360
+ DROP TABLE fortress_auth_continuation;
1361
+ CREATE TABLE fortress_auth_continuation (
1362
+ id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
1363
+ token_hash TEXT NOT NULL UNIQUE, reason TEXT NOT NULL, expires_at INTEGER NOT NULL, consumed_at INTEGER,
1364
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
1365
+ );
1366
+ INSERT INTO fortress_auth_continuation SELECT * FROM fortress_auth_continuation_old;
1367
+ DROP TABLE fortress_auth_continuation_old;
1368
+ `.trim();
1369
+ var PG_0008_UP = `
1370
+ ALTER TABLE fortress_auth_continuation
1371
+ ADD COLUMN failed_attempts INTEGER NOT NULL DEFAULT 0,
1372
+ ADD COLUMN last_failed_at TIMESTAMPTZ,
1373
+ ADD COLUMN invalidated_at TIMESTAMPTZ,
1374
+ ADD COLUMN max_attempts INTEGER NOT NULL DEFAULT 5,
1375
+ ADD COLUMN cooldown_seconds INTEGER NOT NULL DEFAULT 1;
1376
+ CREATE INDEX auth_continuation_failure_idx ON fortress_auth_continuation (user_id, reason, last_failed_at);
1377
+ `.trim();
1378
+ var PG_0008_DOWN = `
1379
+ DROP INDEX IF EXISTS auth_continuation_failure_idx;
1380
+ ALTER TABLE fortress_auth_continuation DROP COLUMN cooldown_seconds, DROP COLUMN max_attempts, DROP COLUMN invalidated_at, DROP COLUMN last_failed_at, DROP COLUMN failed_attempts;
1381
+ `.trim();
1382
+ var SQLITE_0009_UP = `
1383
+ DELETE FROM fortress_backup_code;
1384
+ DELETE FROM fortress_trusted_device;
1385
+ DELETE FROM fortress_two_factor_secret;
1386
+ `.trim();
1387
+ var SQLITE_0009_DOWN = "SELECT 1;";
1388
+ var PG_0009_UP = SQLITE_0009_UP;
1389
+ var PG_0009_DOWN = SQLITE_0009_DOWN;
1390
+ var SQLITE_0010_UP = "SELECT 1;";
1391
+ var SQLITE_0010_DOWN = "SELECT 1;";
1392
+ var PG_0010_UP = `
1393
+ ALTER SEQUENCE fortress_refresh_token_id_seq AS BIGINT;
1394
+ ALTER TABLE fortress_refresh_token ALTER COLUMN id TYPE BIGINT;
1395
+ ALTER SEQUENCE fortress_auth_continuation_id_seq AS BIGINT;
1396
+ ALTER TABLE fortress_auth_continuation ALTER COLUMN id TYPE BIGINT;
1397
+ ALTER SEQUENCE fortress_email_verification_token_id_seq AS BIGINT;
1398
+ ALTER TABLE fortress_email_verification_token ALTER COLUMN id TYPE BIGINT;
1399
+ ALTER SEQUENCE fortress_magic_link_token_id_seq AS BIGINT;
1400
+ ALTER TABLE fortress_magic_link_token ALTER COLUMN id TYPE BIGINT;
1401
+ ALTER SEQUENCE fortress_oauth_authorization_code_id_seq AS BIGINT;
1402
+ ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN id TYPE BIGINT;
1403
+ ALTER SEQUENCE fortress_oauth_access_token_id_seq AS BIGINT;
1404
+ ALTER TABLE fortress_oauth_access_token ALTER COLUMN id TYPE BIGINT;
1405
+ ALTER SEQUENCE fortress_oauth_refresh_token_id_seq AS BIGINT;
1406
+ ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN id TYPE BIGINT;
1407
+ ALTER SEQUENCE fortress_oauth_pending_flow_id_seq AS BIGINT;
1408
+ ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN id TYPE BIGINT;
1409
+ ALTER SEQUENCE fortress_audit_log_id_seq AS BIGINT;
1410
+ ALTER TABLE fortress_audit_log ALTER COLUMN id TYPE BIGINT;
1411
+ ALTER SEQUENCE fortress_webhook_delivery_id_seq AS BIGINT;
1412
+ ALTER TABLE fortress_webhook_delivery ALTER COLUMN id TYPE BIGINT;
1413
+ ALTER SEQUENCE fortress_webauthn_challenge_id_seq AS BIGINT;
1414
+ ALTER TABLE fortress_webauthn_challenge ALTER COLUMN id TYPE BIGINT;
1415
+ `.trim();
1416
+ var PG_0010_DOWN = "SELECT 1;";
1417
+ var fortressMigrations = {
1418
+ sqlite: [
1419
+ { version: 1, name: "schema_version", dialect: "sqlite", up: SQLITE_0001_UP, down: SQLITE_0001_DOWN },
1420
+ { version: 2, name: "initial_schema", dialect: "sqlite", up: SQLITE_0002_UP, down: SQLITE_0002_DOWN },
1421
+ { version: 3, name: "auth_continuation", dialect: "sqlite", up: SQLITE_0003_UP, down: SQLITE_0003_DOWN },
1422
+ { version: 4, name: "tenant_default_unique", dialect: "sqlite", up: SQLITE_0004_UP, down: SQLITE_0004_DOWN },
1423
+ { version: 5, name: "hot_indexes_timestamptz", dialect: "sqlite", up: SQLITE_0005_UP, down: SQLITE_0005_DOWN },
1424
+ { version: 6, name: "canonical_email", dialect: "sqlite", up: SQLITE_0006_UP, down: SQLITE_0006_DOWN, freshUp: SQLITE_0006_FRESH_UP, dataStep: "normalize-email-v2", beforeUp: normalizeExistingUserEmails },
1425
+ { version: 7, name: "audit_chain_anchor", dialect: "sqlite", up: SQLITE_0007_UP, down: SQLITE_0007_DOWN },
1426
+ { version: 8, name: "two_factor_hardening", dialect: "sqlite", up: SQLITE_0008_UP, down: SQLITE_0008_DOWN },
1427
+ { version: 9, name: "encrypt_totp_secrets", dialect: "sqlite", up: SQLITE_0009_UP, down: SQLITE_0009_DOWN },
1428
+ { version: 10, name: "bigint_append_only_ids", dialect: "sqlite", up: SQLITE_0010_UP, down: SQLITE_0010_DOWN }
1429
+ ],
1430
+ pg: [
1431
+ { version: 1, name: "schema_version", dialect: "pg", up: PG_0001_UP, down: PG_0001_DOWN },
1432
+ { version: 2, name: "initial_schema", dialect: "pg", up: PG_0002_UP, down: PG_0002_DOWN },
1433
+ { version: 3, name: "auth_continuation", dialect: "pg", up: PG_0003_UP, down: PG_0003_DOWN },
1434
+ { version: 4, name: "tenant_default_unique", dialect: "pg", up: PG_0004_UP, down: PG_0004_DOWN },
1435
+ { version: 5, name: "hot_indexes_timestamptz", dialect: "pg", up: PG_0005_UP, down: PG_0005_DOWN },
1436
+ { version: 6, name: "canonical_email", dialect: "pg", up: PG_0006_UP, down: PG_0006_DOWN, freshUp: PG_0006_FRESH_UP, dataStep: "normalize-email-v2", beforeUp: normalizeExistingUserEmails },
1437
+ { version: 7, name: "audit_chain_anchor", dialect: "pg", up: PG_0007_UP, down: PG_0007_DOWN },
1438
+ { version: 8, name: "two_factor_hardening", dialect: "pg", up: PG_0008_UP, down: PG_0008_DOWN },
1439
+ { version: 9, name: "encrypt_totp_secrets", dialect: "pg", up: PG_0009_UP, down: PG_0009_DOWN },
1440
+ { version: 10, name: "bigint_append_only_ids", dialect: "pg", up: PG_0010_UP, down: PG_0010_DOWN }
1441
+ ]
1442
+ };
1443
+ function getFortressMigrations(dialect) {
1444
+ return [...fortressMigrations[dialect]].sort((a, b) => a.version - b.version);
1445
+ }
1446
+ function getLatestMigrationVersion(dialect) {
1447
+ return getFortressMigrations(dialect).at(-1)?.version ?? 0;
1448
+ }
1449
+ function getMigrationUpSql(dialect) {
1450
+ return getFortressMigrations(dialect).map((migration) => migration.freshUp ?? migration.up).join("\n\n");
1451
+ }
1452
+ var FORTRESS_TABLES = [
1453
+ "fortress_schema_version",
1454
+ "fortress_user",
1455
+ "fortress_login_identifier",
1456
+ "fortress_refresh_token",
1457
+ "fortress_auth_continuation",
1458
+ "fortress_group",
1459
+ "fortress_group_user",
1460
+ "fortress_service_account",
1461
+ "fortress_resource",
1462
+ "fortress_permission",
1463
+ "fortress_role",
1464
+ "fortress_role_permission",
1465
+ "fortress_role_binding",
1466
+ "fortress_direct_permission_binding",
1467
+ "fortress_email_verification_token",
1468
+ "fortress_magic_link_token",
1469
+ "fortress_api_key",
1470
+ "fortress_two_factor_secret",
1471
+ "fortress_backup_code",
1472
+ "fortress_trusted_device",
1473
+ "fortress_social_account",
1474
+ "fortress_tenant",
1475
+ "fortress_tenant_user",
1476
+ "fortress_oauth_client",
1477
+ "fortress_oauth_authorization_code",
1478
+ "fortress_oauth_access_token",
1479
+ "fortress_oauth_refresh_token",
1480
+ "fortress_oauth_pending_flow",
1481
+ "fortress_oauth_signing_key",
1482
+ "fortress_user_scope_assignment",
1483
+ "fortress_account_lockout",
1484
+ "fortress_audit_log",
1485
+ "fortress_audit_chain_state",
1486
+ "fortress_webhook_endpoint",
1487
+ "fortress_webhook_delivery",
1488
+ "fortress_webauthn_credential",
1489
+ "fortress_webauthn_challenge"
1490
+ ];
1491
+ var FORTRESS_INDEXES = [
1492
+ { name: "refresh_token_family_idx", table: "fortress_refresh_token", columns: ["token_family"] },
1493
+ { name: "refresh_token_user_idx", table: "fortress_refresh_token", columns: ["user_id"] },
1494
+ { name: "email_verification_token_token_idx", table: "fortress_email_verification_token", columns: ["token"] },
1495
+ { name: "magic_link_token_token_idx", table: "fortress_magic_link_token", columns: ["token"] },
1496
+ { name: "role_binding_subject_idx", table: "fortress_role_binding", columns: ["subject_type", "subject_id"] },
1497
+ { name: "direct_permission_binding_subject_idx", table: "fortress_direct_permission_binding", columns: ["subject_type", "subject_id"] },
1498
+ { name: "backup_code_user_idx", table: "fortress_backup_code", columns: ["user_id"] },
1499
+ { name: "trusted_device_user_idx", table: "fortress_trusted_device", columns: ["user_id"] },
1500
+ { name: "webhook_delivery_retry_idx", table: "fortress_webhook_delivery", columns: ["status", "next_retry_at"] },
1501
+ { name: "audit_log_timestamp_idx", table: "fortress_audit_log", columns: ["timestamp"] }
1502
+ ];
1503
+ var CONSTRAINT_KEYWORDS = /* @__PURE__ */ new Set(["primary", "unique", "foreign", "constraint", "check"]);
1504
+ var CREATE_TABLE_RE = /create\s+table\s+(?:if\s+not\s+exists\s+)?(\w+)\s*\(/gi;
1505
+ var ADD_COLUMN_RE = /alter\s+table\s+(\w+)\s+add\s+column\s+(\w+)/gi;
1506
+ var RENAME_TABLE_RE = /alter\s+table\s+(\w+)\s+rename\s+to\s+(\w+)/gi;
1507
+ var WHITESPACE_RE = /\s+/;
1508
+ var QUOTE_CHARS_RE = /["'`]/g;
1509
+ function getExpectedColumns(dialect) {
1510
+ const sql4 = getMigrationUpSql(dialect).split("\n").filter((line) => !line.trimStart().startsWith("--")).join("\n");
1511
+ const result = {};
1512
+ CREATE_TABLE_RE.lastIndex = 0;
1513
+ let match = CREATE_TABLE_RE.exec(sql4);
1514
+ while (match !== null) {
1515
+ const tableName = match[1];
1516
+ const bodyStart = match.index + match[0].length;
1517
+ let depth = 1;
1518
+ let i = bodyStart;
1519
+ for (; i < sql4.length && depth > 0; i++) {
1520
+ if (sql4[i] === "(")
1521
+ depth++;
1522
+ else if (sql4[i] === ")")
1523
+ depth--;
1524
+ }
1525
+ const body = sql4.slice(bodyStart, i - 1);
1526
+ result[tableName] = extractColumnNames(body);
1527
+ CREATE_TABLE_RE.lastIndex = i;
1528
+ match = CREATE_TABLE_RE.exec(sql4);
1529
+ }
1530
+ ADD_COLUMN_RE.lastIndex = 0;
1531
+ let addMatch = ADD_COLUMN_RE.exec(sql4);
1532
+ while (addMatch !== null) {
1533
+ const [, tableName, columnName] = addMatch;
1534
+ const columns = result[tableName] ?? (result[tableName] = []);
1535
+ const normalized = columnName.toLowerCase();
1536
+ if (!columns.includes(normalized))
1537
+ columns.push(normalized);
1538
+ addMatch = ADD_COLUMN_RE.exec(sql4);
1539
+ }
1540
+ RENAME_TABLE_RE.lastIndex = 0;
1541
+ let renameMatch = RENAME_TABLE_RE.exec(sql4);
1542
+ while (renameMatch !== null) {
1543
+ const [, fromName, toName] = renameMatch;
1544
+ if (result[fromName]) {
1545
+ result[toName] = result[fromName];
1546
+ delete result[fromName];
1547
+ }
1548
+ renameMatch = RENAME_TABLE_RE.exec(sql4);
1549
+ }
1550
+ return result;
1551
+ }
1552
+ function extractColumnNames(body) {
1553
+ const columns = [];
1554
+ let depth = 0;
1555
+ let current = "";
1556
+ const flush = () => {
1557
+ const trimmed = current.trim();
1558
+ current = "";
1559
+ if (!trimmed)
1560
+ return;
1561
+ const firstWord = trimmed.split(WHITESPACE_RE)[0].replace(QUOTE_CHARS_RE, "").toLowerCase();
1562
+ if (!firstWord || CONSTRAINT_KEYWORDS.has(firstWord))
1563
+ return;
1564
+ columns.push(firstWord);
1565
+ };
1566
+ for (const char of body) {
1567
+ if (char === "(") {
1568
+ depth++;
1569
+ current += char;
1570
+ } else if (char === ")") {
1571
+ depth--;
1572
+ current += char;
1573
+ } else if (char === "," && depth === 0) {
1574
+ flush();
1575
+ } else {
1576
+ current += char;
1577
+ }
1578
+ }
1579
+ flush();
1580
+ return columns;
1581
+ }
1582
+
1583
+ // src/drizzle/adapter.ts
1584
+ import { AsyncLocalStorage } from "async_hooks";
1585
+ import { and, eq, getTableColumns, gt, gte, inArray, isNull, like, lt, lte, ne, sql as sql3 } from "drizzle-orm";
1586
+
1587
+ // src/core/errors.ts
1588
+ var FortressError = class extends Error {
1589
+ code;
1590
+ statusCode;
1591
+ retryAfter;
1592
+ details;
1593
+ /** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
1594
+ oauthError;
1595
+ /** Human-readable description for the OAuth `error_description` field. */
1596
+ oauthDescription;
1597
+ /** Optional URI for the OAuth `error_uri` field (§5.2). */
1598
+ oauthErrorUri;
1599
+ constructor(code, message, statusCode, options) {
1600
+ super(message, { cause: options?.cause });
1601
+ this.code = code;
1602
+ this.statusCode = statusCode;
1603
+ this.retryAfter = options?.retryAfter;
1604
+ this.details = options?.details;
1605
+ this.oauthError = options?.oauthError;
1606
+ this.oauthDescription = options?.oauthDescription;
1607
+ this.oauthErrorUri = options?.oauthErrorUri;
1608
+ }
1609
+ toJSON() {
1610
+ return {
1611
+ code: this.code,
1612
+ message: this.message,
1613
+ statusCode: this.statusCode,
1614
+ ...this.details !== void 0 && { details: this.details }
1615
+ };
1616
+ }
1617
+ };
1618
+ var Errors = {
1619
+ unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
1620
+ tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
1621
+ sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
1622
+ sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
1623
+ forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
1624
+ badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
1625
+ notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
1626
+ conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
1627
+ unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
1628
+ rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
1629
+ database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
1630
+ serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
1631
+ validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
1632
+ /**
1633
+ * RFC 6749 §5.2 / §4.1.2.1 OAuth error.
1634
+ *
1635
+ * The HTTP error mapper detects `oauthError` and emits the OAuth-spec
1636
+ * JSON body `{ error, error_description, error_uri? }` instead of the
1637
+ * default fortress `{ code, message }` shape, so strict OAuth clients
1638
+ * (Moodle, openid-client, Spring Security, etc.) can switch behaviour
1639
+ * on the machine-readable `error` field.
1640
+ *
1641
+ * Status is inferred from the OAuth code per the spec table:
1642
+ * - `invalid_client` → 401
1643
+ * - everything else → 400
1644
+ */
1645
+ oauth: (error, description, options) => {
1646
+ const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
1647
+ const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
1648
+ return new FortressError(code, description ?? error, status, {
1649
+ oauthError: error,
1650
+ oauthDescription: description,
1651
+ oauthErrorUri: options?.errorUri,
1652
+ cause: options?.cause
1653
+ });
1654
+ },
1655
+ /**
1656
+ * Reconstruct a {@link FortressError} from a JSON error body emitted by
1657
+ * `errorToResponse`. Used by the in-process `fortress.call.*` client to
1658
+ * convert non-2xx responses into typed throws that mirror what a direct
1659
+ * service-method call would produce.
1660
+ *
1661
+ * Maps by HTTP status when the body doesn't carry a recognizable
1662
+ * `{ code, message }` payload, so network errors and opaque upstream
1663
+ * failures still become structured `FortressError`s.
1664
+ */
1665
+ fromHttpResponse: (status, body) => {
1666
+ const payload = body && typeof body === "object" ? body : {};
1667
+ const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
1668
+ const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
1669
+ return new FortressError(code, message, status, { details: payload.details });
1670
+ }
1671
+ };
1672
+ function isFortressErrorCode(code) {
1673
+ return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
1674
+ }
1675
+ function statusToErrorCode(status) {
1676
+ if (status === 401)
1677
+ return "UNAUTHORIZED";
1678
+ if (status === 403)
1679
+ return "FORBIDDEN";
1680
+ if (status === 404)
1681
+ return "NOT_FOUND";
1682
+ if (status === 409)
1683
+ return "CONFLICT";
1684
+ if (status === 422)
1685
+ return "UNPROCESSABLE_ENTITY";
1686
+ if (status === 429)
1687
+ return "RATE_LIMITED";
1688
+ if (status === 503)
1689
+ return "SERVICE_UNAVAILABLE";
1690
+ if (status >= 500)
1691
+ return "DATABASE_ERROR";
1692
+ return "BAD_REQUEST";
1693
+ }
1694
+
1695
+ // src/drizzle/pg-error-map.ts
1696
+ var MAX_CAUSE_DEPTH = 6;
1697
+ var SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
1698
+ var PG_SQLSTATE_MAP = {
1699
+ "23505": (cause) => Errors.conflict("Resource already exists", { cause }),
1700
+ "23503": (cause) => Errors.unprocessable("Referenced resource does not exist", { cause }),
1701
+ "23502": (_cause) => Errors.badRequest("Required field is missing"),
1702
+ "23514": (cause) => Errors.unprocessable("Constraint check failed", { cause }),
1703
+ "40001": (cause) => Errors.conflict("Serialization failure, retry the request", { cause }),
1704
+ "40P01": (cause) => Errors.conflict("Deadlock detected, retry the request", { cause }),
1705
+ "57014": (cause) => Errors.serviceUnavailable("Query canceled", { cause })
1706
+ };
1707
+ var SQLITE_CONSTRAINT_MAP = [
1708
+ {
1709
+ match: /SQLITE_CONSTRAINT_(?:UNIQUE|PRIMARYKEY)|UNIQUE constraint failed/i,
1710
+ factory: (cause) => Errors.conflict("Resource already exists", { cause })
1711
+ },
1712
+ {
1713
+ match: /SQLITE_CONSTRAINT_FOREIGNKEY|FOREIGN KEY constraint failed/i,
1714
+ factory: (cause) => Errors.unprocessable("Referenced resource does not exist", { cause })
1715
+ },
1716
+ {
1717
+ match: /SQLITE_CONSTRAINT_NOTNULL|NOT NULL constraint failed/i,
1718
+ factory: (_cause) => Errors.badRequest("Required field is missing")
1719
+ }
1720
+ ];
1721
+ function mapSqliteConstraint(err) {
1722
+ let current = err;
1723
+ for (let i = 0; i < MAX_CAUSE_DEPTH; i++) {
1724
+ if (current === null || typeof current !== "object")
1725
+ return null;
1726
+ const coded = current;
1727
+ const code = typeof coded.code === "string" ? coded.code : "";
1728
+ const message = typeof coded.message === "string" ? coded.message : "";
1729
+ const haystack = `${code} ${message}`;
1730
+ for (const { match, factory } of SQLITE_CONSTRAINT_MAP) {
1731
+ if (match.test(haystack))
1732
+ return factory(err);
1733
+ }
1734
+ current = coded.cause;
1735
+ }
1736
+ return null;
1737
+ }
1738
+ function findSqlstate(err) {
1739
+ let current = err;
1740
+ for (let i = 0; i < MAX_CAUSE_DEPTH; i++) {
1741
+ if (current === null || typeof current !== "object")
1742
+ return null;
1743
+ const candidate = current.code;
1744
+ if (typeof candidate === "string" && SQLSTATE_PATTERN.test(candidate))
1745
+ return candidate;
1746
+ current = current.cause;
1747
+ }
1748
+ return null;
1749
+ }
1750
+ function rethrowDbError(err, dialect) {
1751
+ if (dialect === "pg") {
1752
+ const sqlstate = findSqlstate(err);
1753
+ const factory = sqlstate ? PG_SQLSTATE_MAP[sqlstate] : void 0;
1754
+ if (factory)
1755
+ throw factory(err);
1756
+ throw err;
1757
+ }
1758
+ if (dialect === "sqlite") {
1759
+ const mapped = mapSqliteConstraint(err);
1760
+ if (mapped)
1761
+ throw mapped;
1762
+ throw err;
1763
+ }
1764
+ throw err;
1765
+ }
1766
+
1767
+ // src/drizzle/pg/schema.ts
1768
+ import { sql } from "drizzle-orm";
1769
+ import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex, varchar } from "drizzle-orm/pg-core";
1770
+ var schemaVersion = pgTable("fortress_schema_version", {
1771
+ id: integer("id").primaryKey().default(1),
1772
+ version: integer("version").notNull(),
1773
+ appliedAt: timestamp("applied_at", { withTimezone: true }).notNull().defaultNow()
1774
+ });
1775
+ var users = pgTable("fortress_user", {
1776
+ id: serial("id").primaryKey(),
1777
+ email: varchar("email", { length: 255 }).notNull().unique(),
1778
+ name: varchar("name", { length: 255 }).notNull(),
1779
+ passwordHash: text("password_hash"),
1780
+ isActive: boolean("is_active").notNull().default(true),
1781
+ emailVerified: boolean("email_verified").notNull().default(false),
1782
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1783
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
1784
+ }, (table) => [
1785
+ uniqueIndex("user_email_ci_unique").on(sql`lower(${table.email})`)
1786
+ ]);
1787
+ var loginIdentifiers = pgTable("fortress_login_identifier", {
1788
+ id: serial("id").primaryKey(),
1789
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1790
+ type: varchar("type", { length: 20 }).notNull(),
1791
+ // 'email' | 'phone' | 'username'
1792
+ value: varchar("value", { length: 255 }).notNull().unique()
1793
+ }, (table) => [
1794
+ uniqueIndex("login_identifier_email_ci_unique").on(sql`lower(${table.value})`).where(sql`${table.type} = 'email'`)
1795
+ ]);
1796
+ var refreshTokens = pgTable("fortress_refresh_token", {
1797
+ id: bigserial("id", { mode: "number" }).primaryKey(),
1798
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1799
+ tokenHash: varchar("token_hash", { length: 64 }).notNull().unique(),
1800
+ tokenFamily: varchar("token_family", { length: 64 }).notNull(),
1801
+ familyCreatedAt: timestamp("family_created_at", { withTimezone: true }).notNull().defaultNow(),
1802
+ successorTokenHash: varchar("successor_token_hash", { length: 64 }),
1803
+ rotatedAt: timestamp("rotated_at", { withTimezone: true }),
1804
+ isRevoked: boolean("is_revoked").notNull().default(false),
1805
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
1806
+ ipAddress: varchar("ip_address", { length: 45 }),
1807
+ userAgent: text("user_agent"),
1808
+ deviceName: text("device_name"),
1809
+ lastActiveAt: timestamp("last_active_at", { withTimezone: true }),
1810
+ fingerprintHash: varchar("fingerprint_hash", { length: 64 }),
1811
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
1812
+ }, (table) => [
1813
+ index("refresh_token_family_idx").on(table.tokenFamily),
1814
+ index("refresh_token_user_idx").on(table.userId)
1815
+ ]);
1816
+ var authContinuations = pgTable("fortress_auth_continuation", {
1817
+ id: bigserial("id", { mode: "number" }).primaryKey(),
1818
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1819
+ tokenHash: varchar("token_hash", { length: 64 }).notNull().unique(),
1820
+ reason: varchar("reason", { length: 32 }).notNull(),
1821
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
1822
+ consumedAt: timestamp("consumed_at", { withTimezone: true }),
1823
+ failedAttempts: integer("failed_attempts").notNull().default(0),
1824
+ lastFailedAt: timestamp("last_failed_at", { withTimezone: true }),
1825
+ invalidatedAt: timestamp("invalidated_at", { withTimezone: true }),
1826
+ maxAttempts: integer("max_attempts").notNull().default(5),
1827
+ cooldownSeconds: integer("cooldown_seconds").notNull().default(1),
1828
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
1829
+ }, (table) => [
1830
+ index("auth_continuation_failure_idx").on(table.userId, table.reason, table.lastFailedAt)
1831
+ ]);
1832
+ var groups = pgTable("fortress_group", {
1833
+ id: serial("id").primaryKey(),
1834
+ name: varchar("name", { length: 100 }).notNull().unique(),
1835
+ description: text("description")
1836
+ });
1837
+ var groupUsers = pgTable(
1838
+ "fortress_group_user",
1839
+ {
1840
+ groupId: integer("group_id").notNull().references(() => groups.id, { onDelete: "cascade" }),
1841
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" })
1842
+ },
1843
+ (table) => [primaryKey({ columns: [table.groupId, table.userId] })]
1844
+ );
1845
+ var serviceAccounts = pgTable("fortress_service_account", {
1846
+ id: serial("id").primaryKey(),
1847
+ name: varchar("name", { length: 100 }).notNull().unique(),
1848
+ displayName: varchar("display_name", { length: 255 }),
1849
+ description: text("description"),
1850
+ isActive: boolean("is_active").notNull().default(true),
1851
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1852
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
1853
+ });
1854
+ var resources = pgTable("fortress_resource", {
1855
+ name: varchar("name", { length: 100 }).primaryKey(),
1856
+ description: text("description")
1857
+ });
1858
+ var permissions = pgTable("fortress_permission", {
1859
+ id: serial("id").primaryKey(),
1860
+ resource: varchar("resource", { length: 100 }).notNull().references(() => resources.name, { onDelete: "cascade" }),
1861
+ action: varchar("action", { length: 100 }).notNull(),
1862
+ effect: varchar("effect", { length: 10 }).notNull().default("ALLOW"),
1863
+ // 'ALLOW' | 'DENY'
1864
+ conditions: jsonb("conditions"),
1865
+ // PermissionCondition[] as JSONB
1866
+ description: text("description")
1867
+ }, (table) => [
1868
+ // M8 fix — same split-index pattern as role/direct-permission bindings
1869
+ // so concurrent findOrCreatePermission can't insert duplicate rows when
1870
+ // conditions is NULL (a plain UNIQUE treats NULLs as distinct).
1871
+ uniqueIndex("uniq_permission_no_conditions").on(table.resource, table.action, table.effect).where(sql`${table.conditions} is null`),
1872
+ uniqueIndex("uniq_permission_with_conditions").on(table.resource, table.action, table.effect, table.conditions).where(sql`${table.conditions} is not null`)
1873
+ ]);
1874
+ var roles = pgTable("fortress_role", {
1875
+ id: serial("id").primaryKey(),
1876
+ name: varchar("name", { length: 100 }).notNull().unique(),
1877
+ description: text("description"),
1878
+ isSystem: boolean("is_system").notNull().default(false)
1879
+ });
1880
+ var rolePermissions = pgTable(
1881
+ "fortress_role_permission",
1882
+ {
1883
+ roleId: integer("role_id").notNull().references(() => roles.id, { onDelete: "cascade" }),
1884
+ permissionId: integer("permission_id").notNull().references(() => permissions.id, { onDelete: "cascade" })
1885
+ },
1886
+ (table) => [primaryKey({ columns: [table.roleId, table.permissionId] })]
1887
+ );
1888
+ var roleBindings = pgTable("fortress_role_binding", {
1889
+ id: serial("id").primaryKey(),
1890
+ roleId: integer("role_id").notNull().references(() => roles.id, { onDelete: "cascade" }),
1891
+ subjectType: varchar("subject_type", { length: 20 }).notNull(),
1892
+ // 'USER' | 'GROUP' | 'SERVICE_ACCOUNT'
1893
+ subjectId: integer("subject_id").notNull(),
1894
+ tenantId: varchar("tenant_id", { length: 100 })
1895
+ }, (table) => [
1896
+ unique().on(table.roleId, table.subjectType, table.subjectId, table.tenantId),
1897
+ uniqueIndex("uniq_role_binding_global").on(table.roleId, table.subjectType, table.subjectId).where(sql`${table.tenantId} is null`),
1898
+ uniqueIndex("uniq_role_binding_tenant").on(table.roleId, table.subjectType, table.subjectId, table.tenantId).where(sql`${table.tenantId} is not null`),
1899
+ index("role_binding_subject_idx").on(table.subjectType, table.subjectId)
1900
+ ]);
1901
+ var directPermissionBindings = pgTable("fortress_direct_permission_binding", {
1902
+ id: serial("id").primaryKey(),
1903
+ permissionId: integer("permission_id").notNull().references(() => permissions.id, { onDelete: "cascade" }),
1904
+ subjectType: varchar("subject_type", { length: 20 }).notNull(),
1905
+ // 'USER' | 'GROUP' | 'SERVICE_ACCOUNT'
1906
+ subjectId: integer("subject_id").notNull(),
1907
+ tenantId: varchar("tenant_id", { length: 100 })
1908
+ }, (table) => [
1909
+ unique().on(table.permissionId, table.subjectType, table.subjectId, table.tenantId),
1910
+ uniqueIndex("uniq_direct_permission_binding_global").on(table.permissionId, table.subjectType, table.subjectId).where(sql`${table.tenantId} is null`),
1911
+ uniqueIndex("uniq_direct_permission_binding_tenant").on(table.permissionId, table.subjectType, table.subjectId, table.tenantId).where(sql`${table.tenantId} is not null`),
1912
+ index("direct_permission_binding_subject_idx").on(table.subjectType, table.subjectId)
1913
+ ]);
1914
+ var emailVerificationTokens = pgTable("fortress_email_verification_token", {
1915
+ id: bigserial("id", { mode: "number" }).primaryKey(),
1916
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1917
+ token: text("token").notNull(),
1918
+ email: varchar("email", { length: 255 }).notNull(),
1919
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
1920
+ usedAt: timestamp("used_at", { withTimezone: true }),
1921
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
1922
+ }, (table) => [index("email_verification_token_token_idx").on(table.token)]);
1923
+ var magicLinkTokens = pgTable("fortress_magic_link_token", {
1924
+ id: bigserial("id", { mode: "number" }).primaryKey(),
1925
+ email: varchar("email", { length: 255 }).notNull(),
1926
+ token: varchar("token", { length: 64 }).notNull(),
1927
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
1928
+ usedAt: timestamp("used_at", { withTimezone: true }),
1929
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
1930
+ }, (table) => [index("magic_link_token_token_idx").on(table.token)]);
1931
+ var apiKeys = pgTable("fortress_api_key", {
1932
+ id: serial("id").primaryKey(),
1933
+ subjectType: varchar("subject_type", { length: 20 }).notNull(),
1934
+ // 'USER' | 'SERVICE_ACCOUNT' — polymorphic owner
1935
+ subjectId: integer("subject_id").notNull(),
1936
+ name: varchar("name", { length: 255 }).notNull(),
1937
+ keyHash: varchar("key_hash", { length: 64 }).notNull().unique(),
1938
+ keyPrefix: varchar("key_prefix", { length: 20 }).notNull(),
1939
+ scopes: text("scopes"),
1940
+ // JSON array of "resource:action" strings
1941
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
1942
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
1943
+ isRevoked: boolean("is_revoked").notNull().default(false),
1944
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
1945
+ }, (table) => [index("api_key_subject_idx").on(table.subjectType, table.subjectId)]);
1946
+ var twoFactorSecrets = pgTable("fortress_two_factor_secret", {
1947
+ id: serial("id").primaryKey(),
1948
+ userId: integer("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
1949
+ secret: text("secret").notNull(),
1950
+ // Versioned AES-GCM envelope containing the TOTP seed
1951
+ isEnabled: boolean("is_enabled").notNull().default(false),
1952
+ lastUsedCounter: integer("last_used_counter"),
1953
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
1954
+ });
1955
+ var backupCodes = pgTable("fortress_backup_code", {
1956
+ id: serial("id").primaryKey(),
1957
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1958
+ codeHash: text("code_hash").notNull(),
1959
+ isUsed: boolean("is_used").notNull().default(false),
1960
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
1961
+ }, (table) => [index("backup_code_user_idx").on(table.userId)]);
1962
+ var trustedDevices = pgTable("fortress_trusted_device", {
1963
+ id: serial("id").primaryKey(),
1964
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1965
+ /** SHA-256 hash of the server-issued trusted-device secret (never raw token). */
1966
+ deviceHash: varchar("device_hash", { length: 64 }).notNull(),
1967
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
1968
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }).notNull(),
1969
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
1970
+ }, (table) => [index("trusted_device_user_idx").on(table.userId)]);
1971
+ var socialAccounts = pgTable("fortress_social_account", {
1972
+ id: serial("id").primaryKey(),
1973
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1974
+ provider: varchar("provider", { length: 50 }).notNull(),
1975
+ providerAccountId: varchar("provider_account_id", { length: 255 }).notNull(),
1976
+ email: varchar("email", { length: 255 }),
1977
+ accessToken: text("access_token"),
1978
+ // Encrypted
1979
+ refreshToken: text("refresh_token"),
1980
+ // Encrypted
1981
+ tokenExpiresAt: timestamp("token_expires_at", { withTimezone: true }),
1982
+ profile: jsonb("profile"),
1983
+ // JSON
1984
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1985
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
1986
+ }, (table) => [
1987
+ unique().on(table.userId, table.provider),
1988
+ unique().on(table.provider, table.providerAccountId)
1989
+ ]);
1990
+ var tenants = pgTable("fortress_tenant", {
1991
+ id: serial("id").primaryKey(),
1992
+ name: varchar("name", { length: 255 }).notNull(),
1993
+ taxId: varchar("tax_id", { length: 100 }).notNull().unique(),
1994
+ description: text("description"),
1995
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1996
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
1997
+ });
1998
+ var tenantUsers = pgTable(
1999
+ "fortress_tenant_user",
2000
+ {
2001
+ tenantId: integer("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" }),
2002
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
2003
+ isDefault: boolean("is_default").notNull().default(false)
2004
+ },
2005
+ (table) => [
2006
+ primaryKey({ columns: [table.tenantId, table.userId] }),
2007
+ uniqueIndex("fortress_tenant_user_one_default_idx").on(table.userId).where(sql`${table.isDefault} = true`)
2008
+ ]
2009
+ );
2010
+ var oauthClients = pgTable("fortress_oauth_client", {
2011
+ id: serial("id").primaryKey(),
2012
+ clientId: varchar("client_id", { length: 255 }).notNull().unique(),
2013
+ clientSecretHash: text("client_secret_hash").notNull(),
2014
+ name: varchar("name", { length: 255 }).notNull(),
2015
+ redirectUris: text("redirect_uris").notNull(),
2016
+ // JSON array
2017
+ grantTypes: text("grant_types").notNull(),
2018
+ // JSON array
2019
+ // RFC 6749 §3.3 / RFC 9700 §2.2.1 per-client scope allow-list (JSON array,
2020
+ // nullable for legacy v0 clients).
2021
+ allowedScopes: text("allowed_scopes"),
2022
+ // RFC 6749 §2.1 client authentication method ('client_secret_basic' |
2023
+ // 'client_secret_post' | 'none' for RFC 8252 public clients).
2024
+ tokenEndpointAuthMethod: text("token_endpoint_auth_method"),
2025
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2026
+ });
2027
+ var oauthAuthorizationCodes = pgTable("fortress_oauth_authorization_code", {
2028
+ id: bigserial("id", { mode: "number" }).primaryKey(),
2029
+ code: varchar("code", { length: 255 }).notNull().unique(),
2030
+ clientId: varchar("client_id", { length: 255 }).notNull(),
2031
+ userId: integer("user_id").notNull(),
2032
+ redirectUri: text("redirect_uri").notNull(),
2033
+ scope: text("scope"),
2034
+ codeChallenge: text("code_challenge"),
2035
+ codeChallengeMethod: varchar("code_challenge_method", { length: 10 }),
2036
+ // OIDC Core §3.1.2.1 / §2 — echoed into the id_token if present.
2037
+ nonce: text("nonce"),
2038
+ authTime: integer("auth_time"),
2039
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2040
+ usedAt: timestamp("used_at", { withTimezone: true }),
2041
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2042
+ });
2043
+ var oauthAccessTokens = pgTable("fortress_oauth_access_token", {
2044
+ id: bigserial("id", { mode: "number" }).primaryKey(),
2045
+ token: varchar("token", { length: 255 }).notNull().unique(),
2046
+ clientId: varchar("client_id", { length: 255 }).notNull(),
2047
+ userId: integer("user_id"),
2048
+ scope: text("scope"),
2049
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2050
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2051
+ });
2052
+ var oauthRefreshTokens = pgTable("fortress_oauth_refresh_token", {
2053
+ id: bigserial("id", { mode: "number" }).primaryKey(),
2054
+ token: varchar("token", { length: 255 }).notNull().unique(),
2055
+ familyId: varchar("family_id", { length: 64 }).notNull(),
2056
+ clientId: varchar("client_id", { length: 255 }).notNull(),
2057
+ userId: integer("user_id").notNull(),
2058
+ scope: text("scope"),
2059
+ issuedAt: timestamp("issued_at", { withTimezone: true }).notNull(),
2060
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2061
+ usedAt: timestamp("used_at", { withTimezone: true }),
2062
+ parentId: integer("parent_id"),
2063
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2064
+ });
2065
+ var oauthPendingFlows = pgTable("fortress_oauth_pending_flow", {
2066
+ id: bigserial("id", { mode: "number" }).primaryKey(),
2067
+ flowId: text("flow_id").notNull().unique(),
2068
+ clientId: varchar("client_id", { length: 255 }).notNull(),
2069
+ redirectUri: text("redirect_uri").notNull(),
2070
+ scope: text("scope"),
2071
+ state: varchar("state", { length: 255 }).notNull(),
2072
+ codeChallenge: text("code_challenge"),
2073
+ codeChallengeMethod: varchar("code_challenge_method", { length: 10 }),
2074
+ nonce: text("nonce"),
2075
+ // H6 fix: subject the flow is bound to (TOFU-claimed on first
2076
+ // authenticated touch).
2077
+ userId: integer("user_id"),
2078
+ // Single-use approval/denial claim used by the OAuth consent API.
2079
+ usedAt: timestamp("used_at", { withTimezone: true }),
2080
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2081
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2082
+ });
2083
+ var oauthSigningKeys = pgTable("fortress_oauth_signing_key", {
2084
+ id: serial("id").primaryKey(),
2085
+ kid: varchar("kid", { length: 64 }).notNull().unique(),
2086
+ alg: varchar("alg", { length: 16 }).notNull(),
2087
+ publicJwk: text("public_jwk").notNull(),
2088
+ privateJwk: text("private_jwk").notNull(),
2089
+ rotatedAt: timestamp("rotated_at", { withTimezone: true }),
2090
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2091
+ });
2092
+ var userScopeAssignments = pgTable("fortress_user_scope_assignment", {
2093
+ id: serial("id").primaryKey(),
2094
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
2095
+ scopeName: varchar("scope_name", { length: 100 }).notNull(),
2096
+ scopeValue: varchar("scope_value", { length: 255 }).notNull(),
2097
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2098
+ });
2099
+ var accountLockouts = pgTable("fortress_account_lockout", {
2100
+ id: serial("id").primaryKey(),
2101
+ identifier: varchar("identifier", { length: 255 }).notNull().unique(),
2102
+ failedAttempts: integer("failed_attempts").notNull().default(0),
2103
+ lastFailedAt: timestamp("last_failed_at", { withTimezone: true }),
2104
+ lockedUntil: timestamp("locked_until", { withTimezone: true }),
2105
+ lockoutCount: integer("lockout_count").notNull().default(0),
2106
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2107
+ });
2108
+ var auditLogs = pgTable("fortress_audit_log", {
2109
+ id: bigserial("id", { mode: "number" }).primaryKey(),
2110
+ timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(),
2111
+ eventType: varchar("event_type", { length: 100 }).notNull(),
2112
+ actorId: integer("actor_id"),
2113
+ actorType: varchar("actor_type", { length: 20 }).notNull().default("USER"),
2114
+ targetId: integer("target_id"),
2115
+ targetType: varchar("target_type", { length: 50 }),
2116
+ ipAddress: varchar("ip_address", { length: 45 }),
2117
+ userAgent: text("user_agent"),
2118
+ outcome: varchar("outcome", { length: 20 }).notNull().default("SUCCESS"),
2119
+ metadata: jsonb("metadata"),
2120
+ previousHash: text("previous_hash"),
2121
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2122
+ }, (table) => [index("audit_log_timestamp_idx").on(table.timestamp)]);
2123
+ var auditChainState = pgTable("fortress_audit_chain_state", {
2124
+ id: integer("id").primaryKey(),
2125
+ lastHash: varchar("last_hash", { length: 64 }),
2126
+ entryCount: integer("entry_count").notNull(),
2127
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
2128
+ });
2129
+ var webhookEndpoints = pgTable("fortress_webhook_endpoint", {
2130
+ id: serial("id").primaryKey(),
2131
+ url: text("url").notNull(),
2132
+ events: text("events").notNull(),
2133
+ // JSON array
2134
+ secret: text("secret").notNull(),
2135
+ isActive: boolean("is_active").notNull().default(true),
2136
+ deactivatedReason: text("deactivated_reason"),
2137
+ consecutiveFailures: integer("consecutive_failures").notNull().default(0),
2138
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2139
+ });
2140
+ var webhookDeliveries = pgTable("fortress_webhook_delivery", {
2141
+ id: bigserial("id", { mode: "number" }).primaryKey(),
2142
+ endpointId: integer("endpoint_id").notNull().references(() => webhookEndpoints.id, { onDelete: "cascade" }),
2143
+ eventType: varchar("event_type", { length: 100 }).notNull(),
2144
+ payload: text("payload").notNull(),
2145
+ // JSON
2146
+ status: varchar("status", { length: 20 }).notNull().default("pending"),
2147
+ // pending | success | failed
2148
+ attempts: integer("attempts").notNull().default(0),
2149
+ idempotencyKey: text("idempotency_key"),
2150
+ lastAttemptAt: timestamp("last_attempt_at", { withTimezone: true }),
2151
+ nextRetryAt: timestamp("next_retry_at", { withTimezone: true }),
2152
+ responseStatus: integer("response_status"),
2153
+ responseBody: text("response_body"),
2154
+ errorKind: text("error_kind"),
2155
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2156
+ }, (table) => [
2157
+ uniqueIndex("uniq_webhook_delivery_idempotency").on(table.endpointId, table.idempotencyKey).where(sql`${table.idempotencyKey} is not null`),
2158
+ index("webhook_delivery_retry_idx").on(table.status, table.nextRetryAt)
2159
+ ]);
2160
+ var webauthnCredentials = pgTable("fortress_webauthn_credential", {
2161
+ id: serial("id").primaryKey(),
2162
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
2163
+ credentialId: text("credential_id").notNull().unique(),
2164
+ publicKey: text("public_key").notNull(),
2165
+ counter: integer("counter").notNull().default(0),
2166
+ deviceType: varchar("device_type", { length: 20 }).notNull(),
2167
+ backedUp: boolean("backed_up").notNull().default(false),
2168
+ transports: text("transports"),
2169
+ // JSON array
2170
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2171
+ });
2172
+ var webauthnChallenges = pgTable("fortress_webauthn_challenge", {
2173
+ id: bigserial("id", { mode: "number" }).primaryKey(),
2174
+ challenge: text("challenge").notNull().unique(),
2175
+ userId: integer("user_id"),
2176
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2177
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
2178
+ });
2179
+ var fortressPgSchema = {
2180
+ schemaVersion,
2181
+ users,
2182
+ loginIdentifiers,
2183
+ refreshTokens,
2184
+ authContinuations,
2185
+ groups,
2186
+ groupUsers,
2187
+ serviceAccounts,
2188
+ resources,
2189
+ permissions,
2190
+ roles,
2191
+ rolePermissions,
2192
+ roleBindings,
2193
+ directPermissionBindings,
2194
+ emailVerificationTokens,
2195
+ magicLinkTokens,
2196
+ apiKeys,
2197
+ twoFactorSecrets,
2198
+ backupCodes,
2199
+ trustedDevices,
2200
+ socialAccounts,
2201
+ tenants,
2202
+ tenantUsers,
2203
+ oauthClients,
2204
+ oauthAuthorizationCodes,
2205
+ oauthAccessTokens,
2206
+ oauthRefreshTokens,
2207
+ oauthPendingFlows,
2208
+ oauthSigningKeys,
2209
+ userScopeAssignments,
2210
+ accountLockouts,
2211
+ auditLogs,
2212
+ auditChainState,
2213
+ webhookEndpoints,
2214
+ webhookDeliveries,
2215
+ webauthnCredentials,
2216
+ webauthnChallenges
2217
+ };
2218
+
2219
+ // src/drizzle/schema.ts
2220
+ import { sql as sql2 } from "drizzle-orm";
2221
+ import { index as index2, integer as integer2, primaryKey as primaryKey2, sqliteTable, text as text2, unique as unique2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
2222
+ var schemaVersion2 = sqliteTable("fortress_schema_version", {
2223
+ id: integer2("id").primaryKey().default(1),
2224
+ version: integer2("version").notNull(),
2225
+ appliedAt: integer2("applied_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2226
+ });
2227
+ var users2 = sqliteTable("fortress_user", {
2228
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2229
+ email: text2("email").notNull().unique(),
2230
+ name: text2("name").notNull(),
2231
+ passwordHash: text2("password_hash"),
2232
+ isActive: integer2("is_active", { mode: "boolean" }).notNull().default(true),
2233
+ emailVerified: integer2("email_verified", { mode: "boolean" }).notNull().default(false),
2234
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
2235
+ updatedAt: integer2("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2236
+ }, (table) => [
2237
+ uniqueIndex2("user_email_ci_unique").on(sql2`${table.email} COLLATE NOCASE`)
2238
+ ]);
2239
+ var loginIdentifiers2 = sqliteTable("fortress_login_identifier", {
2240
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2241
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2242
+ type: text2("type").notNull(),
2243
+ // 'email' | 'phone' | 'username'
2244
+ value: text2("value").notNull().unique()
2245
+ }, (table) => [
2246
+ uniqueIndex2("login_identifier_email_ci_unique").on(sql2`${table.value} COLLATE NOCASE`).where(sql2`${table.type} = 'email'`)
2247
+ ]);
2248
+ var refreshTokens2 = sqliteTable("fortress_refresh_token", {
2249
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2250
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2251
+ tokenHash: text2("token_hash").notNull().unique(),
2252
+ tokenFamily: text2("token_family").notNull(),
2253
+ familyCreatedAt: integer2("family_created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
2254
+ successorTokenHash: text2("successor_token_hash"),
2255
+ rotatedAt: integer2("rotated_at", { mode: "timestamp" }),
2256
+ isRevoked: integer2("is_revoked", { mode: "boolean" }).notNull().default(false),
2257
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2258
+ ipAddress: text2("ip_address"),
2259
+ userAgent: text2("user_agent"),
2260
+ deviceName: text2("device_name"),
2261
+ lastActiveAt: integer2("last_active_at", { mode: "timestamp" }),
2262
+ fingerprintHash: text2("fingerprint_hash"),
2263
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2264
+ }, (table) => [
2265
+ index2("refresh_token_family_idx").on(table.tokenFamily),
2266
+ index2("refresh_token_user_idx").on(table.userId)
2267
+ ]);
2268
+ var authContinuations2 = sqliteTable("fortress_auth_continuation", {
2269
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2270
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2271
+ tokenHash: text2("token_hash").notNull().unique(),
2272
+ reason: text2("reason").notNull(),
2273
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2274
+ consumedAt: integer2("consumed_at", { mode: "timestamp" }),
2275
+ failedAttempts: integer2("failed_attempts").notNull().default(0),
2276
+ lastFailedAt: integer2("last_failed_at", { mode: "timestamp" }),
2277
+ invalidatedAt: integer2("invalidated_at", { mode: "timestamp" }),
2278
+ maxAttempts: integer2("max_attempts").notNull().default(5),
2279
+ cooldownSeconds: integer2("cooldown_seconds").notNull().default(1),
2280
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2281
+ }, (table) => [
2282
+ index2("auth_continuation_failure_idx").on(table.userId, table.reason, table.lastFailedAt)
2283
+ ]);
2284
+ var groups2 = sqliteTable("fortress_group", {
2285
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2286
+ name: text2("name").notNull().unique(),
2287
+ description: text2("description")
2288
+ });
2289
+ var groupUsers2 = sqliteTable(
2290
+ "fortress_group_user",
2291
+ {
2292
+ groupId: integer2("group_id").notNull().references(() => groups2.id, { onDelete: "cascade" }),
2293
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" })
2294
+ },
2295
+ (table) => [primaryKey2({ columns: [table.groupId, table.userId] })]
2296
+ );
2297
+ var serviceAccounts2 = sqliteTable("fortress_service_account", {
2298
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2299
+ name: text2("name").notNull().unique(),
2300
+ displayName: text2("display_name"),
2301
+ description: text2("description"),
2302
+ isActive: integer2("is_active", { mode: "boolean" }).notNull().default(true),
2303
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
2304
+ updatedAt: integer2("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2305
+ });
2306
+ var resources2 = sqliteTable("fortress_resource", {
2307
+ name: text2("name").primaryKey(),
2308
+ description: text2("description")
2309
+ });
2310
+ var permissions2 = sqliteTable("fortress_permission", {
2311
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2312
+ resource: text2("resource").notNull().references(() => resources2.name, { onDelete: "cascade" }),
2313
+ action: text2("action").notNull(),
2314
+ effect: text2("effect").notNull().default("ALLOW"),
2315
+ // 'ALLOW' | 'DENY'
2316
+ conditions: text2("conditions"),
2317
+ // JSON string of PermissionCondition[]
2318
+ description: text2("description")
2319
+ }, (table) => [
2320
+ // M8 fix: SQL UNIQUE treats two NULL `conditions` as distinct, so the
2321
+ // plain unique() above lets `findOrCreatePermission` insert duplicate
2322
+ // rows for the same (resource, action, effect, conditions=NULL) tuple
2323
+ // under concurrency. Mirror the split-index pattern used for role /
2324
+ // direct-permission bindings.
2325
+ uniqueIndex2("uniq_permission_no_conditions").on(table.resource, table.action, table.effect).where(sql2`${table.conditions} is null`),
2326
+ uniqueIndex2("uniq_permission_with_conditions").on(table.resource, table.action, table.effect, table.conditions).where(sql2`${table.conditions} is not null`)
2327
+ ]);
2328
+ var roles2 = sqliteTable("fortress_role", {
2329
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2330
+ name: text2("name").notNull().unique(),
2331
+ description: text2("description"),
2332
+ isSystem: integer2("is_system", { mode: "boolean" }).notNull().default(false)
2333
+ });
2334
+ var rolePermissions2 = sqliteTable(
2335
+ "fortress_role_permission",
2336
+ {
2337
+ roleId: integer2("role_id").notNull().references(() => roles2.id, { onDelete: "cascade" }),
2338
+ permissionId: integer2("permission_id").notNull().references(() => permissions2.id, { onDelete: "cascade" })
2339
+ },
2340
+ (table) => [primaryKey2({ columns: [table.roleId, table.permissionId] })]
2341
+ );
2342
+ var roleBindings2 = sqliteTable("fortress_role_binding", {
2343
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2344
+ roleId: integer2("role_id").notNull().references(() => roles2.id, { onDelete: "cascade" }),
2345
+ subjectType: text2("subject_type").notNull(),
2346
+ // 'USER' | 'GROUP' | 'SERVICE_ACCOUNT'
2347
+ subjectId: integer2("subject_id").notNull(),
2348
+ tenantId: text2("tenant_id")
2349
+ }, (table) => [
2350
+ unique2().on(table.roleId, table.subjectType, table.subjectId, table.tenantId),
2351
+ uniqueIndex2("uniq_role_binding_global").on(table.roleId, table.subjectType, table.subjectId).where(sql2`${table.tenantId} is null`),
2352
+ uniqueIndex2("uniq_role_binding_tenant").on(table.roleId, table.subjectType, table.subjectId, table.tenantId).where(sql2`${table.tenantId} is not null`),
2353
+ index2("role_binding_subject_idx").on(table.subjectType, table.subjectId)
2354
+ ]);
2355
+ var directPermissionBindings2 = sqliteTable("fortress_direct_permission_binding", {
2356
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2357
+ permissionId: integer2("permission_id").notNull().references(() => permissions2.id, { onDelete: "cascade" }),
2358
+ subjectType: text2("subject_type").notNull(),
2359
+ // 'USER' | 'GROUP' | 'SERVICE_ACCOUNT'
2360
+ subjectId: integer2("subject_id").notNull(),
2361
+ tenantId: text2("tenant_id")
2362
+ }, (table) => [
2363
+ unique2().on(table.permissionId, table.subjectType, table.subjectId, table.tenantId),
2364
+ uniqueIndex2("uniq_direct_permission_binding_global").on(table.permissionId, table.subjectType, table.subjectId).where(sql2`${table.tenantId} is null`),
2365
+ uniqueIndex2("uniq_direct_permission_binding_tenant").on(table.permissionId, table.subjectType, table.subjectId, table.tenantId).where(sql2`${table.tenantId} is not null`),
2366
+ index2("direct_permission_binding_subject_idx").on(table.subjectType, table.subjectId)
2367
+ ]);
2368
+ var emailVerificationTokens2 = sqliteTable("fortress_email_verification_token", {
2369
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2370
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2371
+ token: text2("token").notNull(),
2372
+ email: text2("email").notNull(),
2373
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2374
+ usedAt: integer2("used_at", { mode: "timestamp" }),
2375
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2376
+ }, (table) => [index2("email_verification_token_token_idx").on(table.token)]);
2377
+ var magicLinkTokens2 = sqliteTable("fortress_magic_link_token", {
2378
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2379
+ email: text2("email").notNull(),
2380
+ token: text2("token").notNull(),
2381
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2382
+ usedAt: integer2("used_at", { mode: "timestamp" }),
2383
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2384
+ }, (table) => [index2("magic_link_token_token_idx").on(table.token)]);
2385
+ var apiKeys2 = sqliteTable("fortress_api_key", {
2386
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2387
+ subjectType: text2("subject_type").notNull(),
2388
+ // 'USER' | 'SERVICE_ACCOUNT' — polymorphic owner
2389
+ subjectId: integer2("subject_id").notNull(),
2390
+ name: text2("name").notNull(),
2391
+ keyHash: text2("key_hash").notNull().unique(),
2392
+ keyPrefix: text2("key_prefix").notNull(),
2393
+ scopes: text2("scopes"),
2394
+ // JSON array of "resource:action" strings
2395
+ expiresAt: integer2("expires_at", { mode: "timestamp" }),
2396
+ lastUsedAt: integer2("last_used_at", { mode: "timestamp" }),
2397
+ isRevoked: integer2("is_revoked", { mode: "boolean" }).notNull().default(false),
2398
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2399
+ }, (table) => [index2("api_key_subject_idx").on(table.subjectType, table.subjectId)]);
2400
+ var twoFactorSecrets2 = sqliteTable("fortress_two_factor_secret", {
2401
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2402
+ userId: integer2("user_id").notNull().unique().references(() => users2.id, { onDelete: "cascade" }),
2403
+ secret: text2("secret").notNull(),
2404
+ // Versioned AES-GCM envelope containing the TOTP seed
2405
+ isEnabled: integer2("is_enabled", { mode: "boolean" }).notNull().default(false),
2406
+ lastUsedCounter: integer2("last_used_counter"),
2407
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2408
+ });
2409
+ var backupCodes2 = sqliteTable("fortress_backup_code", {
2410
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2411
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2412
+ codeHash: text2("code_hash").notNull(),
2413
+ isUsed: integer2("is_used", { mode: "boolean" }).notNull().default(false),
2414
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2415
+ }, (table) => [index2("backup_code_user_idx").on(table.userId)]);
2416
+ var trustedDevices2 = sqliteTable("fortress_trusted_device", {
2417
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2418
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2419
+ /** SHA-256 hash of the server-issued trusted-device secret (never raw token). */
2420
+ deviceHash: text2("device_hash").notNull(),
2421
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2422
+ lastUsedAt: integer2("last_used_at", { mode: "timestamp" }).notNull(),
2423
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2424
+ }, (table) => [index2("trusted_device_user_idx").on(table.userId)]);
2425
+ var socialAccounts2 = sqliteTable("fortress_social_account", {
2426
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2427
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2428
+ provider: text2("provider").notNull(),
2429
+ providerAccountId: text2("provider_account_id").notNull(),
2430
+ email: text2("email"),
2431
+ accessToken: text2("access_token"),
2432
+ // Encrypted
2433
+ refreshToken: text2("refresh_token"),
2434
+ // Encrypted
2435
+ tokenExpiresAt: integer2("token_expires_at", { mode: "timestamp" }),
2436
+ profile: text2("profile"),
2437
+ // JSON
2438
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
2439
+ updatedAt: integer2("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2440
+ }, (table) => [
2441
+ unique2().on(table.userId, table.provider),
2442
+ unique2().on(table.provider, table.providerAccountId)
2443
+ ]);
2444
+ var tenants2 = sqliteTable("fortress_tenant", {
2445
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2446
+ name: text2("name").notNull(),
2447
+ taxId: text2("tax_id").notNull().unique(),
2448
+ description: text2("description"),
2449
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
2450
+ updatedAt: integer2("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2451
+ });
2452
+ var tenantUsers2 = sqliteTable(
2453
+ "fortress_tenant_user",
2454
+ {
2455
+ tenantId: integer2("tenant_id").notNull().references(() => tenants2.id, { onDelete: "cascade" }),
2456
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2457
+ isDefault: integer2("is_default", { mode: "boolean" }).notNull().default(false)
2458
+ },
2459
+ (table) => [
2460
+ primaryKey2({ columns: [table.tenantId, table.userId] }),
2461
+ uniqueIndex2("fortress_tenant_user_one_default_idx").on(table.userId).where(sql2`${table.isDefault} = true`)
2462
+ ]
2463
+ );
2464
+ var oauthClients2 = sqliteTable("fortress_oauth_client", {
2465
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2466
+ clientId: text2("client_id").notNull().unique(),
2467
+ clientSecretHash: text2("client_secret_hash").notNull(),
2468
+ name: text2("name").notNull(),
2469
+ redirectUris: text2("redirect_uris").notNull(),
2470
+ // JSON array
2471
+ grantTypes: text2("grant_types").notNull(),
2472
+ // JSON array
2473
+ // RFC 6749 §3.3 / RFC 9700 §2.2.1 per-client scope allow-list (JSON array,
2474
+ // nullable for legacy v0 clients).
2475
+ allowedScopes: text2("allowed_scopes"),
2476
+ // RFC 6749 §2.1 client authentication method ('client_secret_basic' |
2477
+ // 'client_secret_post' | 'none' for RFC 8252 public clients).
2478
+ tokenEndpointAuthMethod: text2("token_endpoint_auth_method"),
2479
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2480
+ });
2481
+ var oauthAuthorizationCodes2 = sqliteTable("fortress_oauth_authorization_code", {
2482
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2483
+ code: text2("code").notNull().unique(),
2484
+ clientId: text2("client_id").notNull(),
2485
+ userId: integer2("user_id").notNull(),
2486
+ redirectUri: text2("redirect_uri").notNull(),
2487
+ scope: text2("scope"),
2488
+ codeChallenge: text2("code_challenge"),
2489
+ codeChallengeMethod: text2("code_challenge_method"),
2490
+ // OIDC Core §3.1.2.1 / §2 — echoed into the id_token if present.
2491
+ nonce: text2("nonce"),
2492
+ authTime: integer2("auth_time"),
2493
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2494
+ usedAt: integer2("used_at", { mode: "timestamp" }),
2495
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2496
+ });
2497
+ var oauthAccessTokens2 = sqliteTable("fortress_oauth_access_token", {
2498
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2499
+ token: text2("token").notNull().unique(),
2500
+ clientId: text2("client_id").notNull(),
2501
+ userId: integer2("user_id"),
2502
+ scope: text2("scope"),
2503
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2504
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2505
+ });
2506
+ var oauthRefreshTokens2 = sqliteTable("fortress_oauth_refresh_token", {
2507
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2508
+ token: text2("token").notNull().unique(),
2509
+ familyId: text2("family_id").notNull(),
2510
+ clientId: text2("client_id").notNull(),
2511
+ userId: integer2("user_id").notNull(),
2512
+ scope: text2("scope"),
2513
+ issuedAt: integer2("issued_at", { mode: "timestamp" }).notNull(),
2514
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2515
+ usedAt: integer2("used_at", { mode: "timestamp" }),
2516
+ parentId: integer2("parent_id"),
2517
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2518
+ });
2519
+ var oauthPendingFlows2 = sqliteTable("fortress_oauth_pending_flow", {
2520
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2521
+ flowId: text2("flow_id").notNull().unique(),
2522
+ clientId: text2("client_id").notNull(),
2523
+ redirectUri: text2("redirect_uri").notNull(),
2524
+ scope: text2("scope"),
2525
+ state: text2("state").notNull(),
2526
+ codeChallenge: text2("code_challenge"),
2527
+ codeChallengeMethod: text2("code_challenge_method"),
2528
+ nonce: text2("nonce"),
2529
+ // H6 fix: subject the flow is bound to. Nullable for the
2530
+ // login-redirect path which can't know the user up front — the
2531
+ // first authenticated GetFlow / Approve claims it.
2532
+ userId: integer2("user_id"),
2533
+ // Single-use approval/denial claim used by the OAuth consent API.
2534
+ usedAt: integer2("used_at", { mode: "timestamp" }),
2535
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2536
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2537
+ });
2538
+ var oauthSigningKeys2 = sqliteTable("fortress_oauth_signing_key", {
2539
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2540
+ kid: text2("kid").notNull().unique(),
2541
+ alg: text2("alg").notNull(),
2542
+ publicJwk: text2("public_jwk").notNull(),
2543
+ privateJwk: text2("private_jwk").notNull(),
2544
+ rotatedAt: integer2("rotated_at", { mode: "timestamp" }),
2545
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2546
+ });
2547
+ var userScopeAssignments2 = sqliteTable("fortress_user_scope_assignment", {
2548
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2549
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2550
+ scopeName: text2("scope_name").notNull(),
2551
+ scopeValue: text2("scope_value").notNull(),
2552
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2553
+ });
2554
+ var accountLockouts2 = sqliteTable("fortress_account_lockout", {
2555
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2556
+ identifier: text2("identifier").notNull().unique(),
2557
+ failedAttempts: integer2("failed_attempts").notNull().default(0),
2558
+ lastFailedAt: integer2("last_failed_at", { mode: "timestamp" }),
2559
+ lockedUntil: integer2("locked_until", { mode: "timestamp" }),
2560
+ lockoutCount: integer2("lockout_count").notNull().default(0),
2561
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2562
+ });
2563
+ var auditLogs2 = sqliteTable("fortress_audit_log", {
2564
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2565
+ timestamp: integer2("timestamp", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
2566
+ eventType: text2("event_type").notNull(),
2567
+ actorId: integer2("actor_id"),
2568
+ actorType: text2("actor_type").notNull().default("USER"),
2569
+ targetId: integer2("target_id"),
2570
+ targetType: text2("target_type"),
2571
+ ipAddress: text2("ip_address"),
2572
+ userAgent: text2("user_agent"),
2573
+ outcome: text2("outcome").notNull().default("SUCCESS"),
2574
+ metadata: text2("metadata"),
2575
+ previousHash: text2("previous_hash"),
2576
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2577
+ }, (table) => [index2("audit_log_timestamp_idx").on(table.timestamp)]);
2578
+ var auditChainState2 = sqliteTable("fortress_audit_chain_state", {
2579
+ id: integer2("id").primaryKey(),
2580
+ lastHash: text2("last_hash"),
2581
+ entryCount: integer2("entry_count").notNull(),
2582
+ updatedAt: integer2("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2583
+ });
2584
+ var webhookEndpoints2 = sqliteTable("fortress_webhook_endpoint", {
2585
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2586
+ url: text2("url").notNull(),
2587
+ events: text2("events").notNull(),
2588
+ // JSON array
2589
+ secret: text2("secret").notNull(),
2590
+ isActive: integer2("is_active", { mode: "boolean" }).notNull().default(true),
2591
+ deactivatedReason: text2("deactivated_reason"),
2592
+ consecutiveFailures: integer2("consecutive_failures").notNull().default(0),
2593
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2594
+ });
2595
+ var webhookDeliveries2 = sqliteTable("fortress_webhook_delivery", {
2596
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2597
+ endpointId: integer2("endpoint_id").notNull().references(() => webhookEndpoints2.id, { onDelete: "cascade" }),
2598
+ eventType: text2("event_type").notNull(),
2599
+ payload: text2("payload").notNull(),
2600
+ // JSON
2601
+ status: text2("status").notNull().default("pending"),
2602
+ // pending | success | failed
2603
+ attempts: integer2("attempts").notNull().default(0),
2604
+ idempotencyKey: text2("idempotency_key"),
2605
+ lastAttemptAt: integer2("last_attempt_at", { mode: "timestamp" }),
2606
+ nextRetryAt: integer2("next_retry_at", { mode: "timestamp" }),
2607
+ responseStatus: integer2("response_status"),
2608
+ responseBody: text2("response_body"),
2609
+ errorKind: text2("error_kind"),
2610
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2611
+ }, (table) => [
2612
+ uniqueIndex2("uniq_webhook_delivery_idempotency").on(table.endpointId, table.idempotencyKey).where(sql2`${table.idempotencyKey} is not null`),
2613
+ index2("webhook_delivery_retry_idx").on(table.status, table.nextRetryAt)
2614
+ ]);
2615
+ var webauthnCredentials2 = sqliteTable("fortress_webauthn_credential", {
2616
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2617
+ userId: integer2("user_id").notNull().references(() => users2.id, { onDelete: "cascade" }),
2618
+ credentialId: text2("credential_id").notNull().unique(),
2619
+ publicKey: text2("public_key").notNull(),
2620
+ counter: integer2("counter").notNull().default(0),
2621
+ deviceType: text2("device_type").notNull(),
2622
+ backedUp: integer2("backed_up", { mode: "boolean" }).notNull().default(false),
2623
+ transports: text2("transports"),
2624
+ // JSON array
2625
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2626
+ });
2627
+ var webauthnChallenges2 = sqliteTable("fortress_webauthn_challenge", {
2628
+ id: integer2("id").primaryKey({ autoIncrement: true }),
2629
+ challenge: text2("challenge").notNull().unique(),
2630
+ userId: integer2("user_id"),
2631
+ expiresAt: integer2("expires_at", { mode: "timestamp" }).notNull(),
2632
+ createdAt: integer2("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
2633
+ });
2634
+ var fortressSchema = {
2635
+ schemaVersion: schemaVersion2,
2636
+ users: users2,
2637
+ loginIdentifiers: loginIdentifiers2,
2638
+ refreshTokens: refreshTokens2,
2639
+ authContinuations: authContinuations2,
2640
+ groups: groups2,
2641
+ groupUsers: groupUsers2,
2642
+ serviceAccounts: serviceAccounts2,
2643
+ resources: resources2,
2644
+ permissions: permissions2,
2645
+ roles: roles2,
2646
+ rolePermissions: rolePermissions2,
2647
+ roleBindings: roleBindings2,
2648
+ directPermissionBindings: directPermissionBindings2,
2649
+ emailVerificationTokens: emailVerificationTokens2,
2650
+ magicLinkTokens: magicLinkTokens2,
2651
+ apiKeys: apiKeys2,
2652
+ twoFactorSecrets: twoFactorSecrets2,
2653
+ backupCodes: backupCodes2,
2654
+ trustedDevices: trustedDevices2,
2655
+ socialAccounts: socialAccounts2,
2656
+ tenants: tenants2,
2657
+ tenantUsers: tenantUsers2,
2658
+ oauthClients: oauthClients2,
2659
+ oauthAuthorizationCodes: oauthAuthorizationCodes2,
2660
+ oauthAccessTokens: oauthAccessTokens2,
2661
+ oauthRefreshTokens: oauthRefreshTokens2,
2662
+ oauthPendingFlows: oauthPendingFlows2,
2663
+ oauthSigningKeys: oauthSigningKeys2,
2664
+ userScopeAssignments: userScopeAssignments2,
2665
+ accountLockouts: accountLockouts2,
2666
+ auditLogs: auditLogs2,
2667
+ auditChainState: auditChainState2,
2668
+ webhookEndpoints: webhookEndpoints2,
2669
+ webhookDeliveries: webhookDeliveries2,
2670
+ webauthnCredentials: webauthnCredentials2,
2671
+ webauthnChallenges: webauthnChallenges2
2672
+ };
2673
+
2674
+ // src/drizzle/adapter.ts
2675
+ function buildTableMap(schema) {
2676
+ return {
2677
+ schema_version: schema.schemaVersion,
2678
+ user: schema.users,
2679
+ login_identifier: schema.loginIdentifiers,
2680
+ refresh_token: schema.refreshTokens,
2681
+ auth_continuation: schema.authContinuations,
2682
+ group: schema.groups,
2683
+ group_user: schema.groupUsers,
2684
+ service_account: schema.serviceAccounts,
2685
+ resource: schema.resources,
2686
+ permission: schema.permissions,
2687
+ role: schema.roles,
2688
+ role_permission: schema.rolePermissions,
2689
+ role_binding: schema.roleBindings,
2690
+ direct_permission_binding: schema.directPermissionBindings,
2691
+ email_verification_token: schema.emailVerificationTokens,
2692
+ magic_link_token: schema.magicLinkTokens,
2693
+ api_key: schema.apiKeys,
2694
+ two_factor_secret: schema.twoFactorSecrets,
2695
+ backup_code: schema.backupCodes,
2696
+ trusted_device: schema.trustedDevices,
2697
+ social_account: schema.socialAccounts,
2698
+ tenant: schema.tenants,
2699
+ tenant_user: schema.tenantUsers,
2700
+ oauth_client: schema.oauthClients,
2701
+ oauth_authorization_code: schema.oauthAuthorizationCodes,
2702
+ oauth_access_token: schema.oauthAccessTokens,
2703
+ oauth_refresh_token: schema.oauthRefreshTokens,
2704
+ oauth_pending_flow: schema.oauthPendingFlows,
2705
+ oauth_signing_key: schema.oauthSigningKeys,
2706
+ user_scope_assignment: schema.userScopeAssignments,
2707
+ account_lockout: schema.accountLockouts,
2708
+ audit_log: schema.auditLogs,
2709
+ audit_chain_state: schema.auditChainState,
2710
+ webhook_endpoint: schema.webhookEndpoints,
2711
+ webhook_delivery: schema.webhookDeliveries,
2712
+ webauthn_credential: schema.webauthnCredentials,
2713
+ webauthn_challenge: schema.webauthnChallenges
2714
+ };
2715
+ }
2716
+ var SQLITE_DEFAULT_TABLE_MAP = buildTableMap(fortressSchema);
2717
+ var PG_DEFAULT_TABLE_MAP = buildTableMap(fortressPgSchema);
2718
+ function getColumn(table, field) {
2719
+ const columns = getTableColumns(table);
2720
+ if (columns[field])
2721
+ return columns[field];
2722
+ const camelCase = field.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
2723
+ if (columns[camelCase])
2724
+ return columns[camelCase];
2725
+ throw Errors.badRequest(`Unknown field: ${field} on table`);
2726
+ }
2727
+ function buildWhereCondition(table, where) {
2728
+ if (where.length === 0)
2729
+ throw Errors.badRequest("A non-empty where clause is required (empty where would match all rows)");
2730
+ const conditions = where.map((clause) => {
2731
+ const column = getColumn(table, clause.field);
2732
+ switch (clause.operator) {
2733
+ case "=":
2734
+ return eq(column, clause.value);
2735
+ case "!=":
2736
+ return ne(column, clause.value);
2737
+ case "in":
2738
+ return inArray(column, clause.value);
2739
+ case "gt":
2740
+ return gt(column, clause.value);
2741
+ case "lt":
2742
+ return lt(column, clause.value);
2743
+ case "gte":
2744
+ return gte(column, clause.value);
2745
+ case "lte":
2746
+ return lte(column, clause.value);
2747
+ case "like":
2748
+ return like(column, clause.value);
2749
+ case "isNull":
2750
+ return isNull(column);
2751
+ default:
2752
+ throw Errors.badRequest(`Unsupported operator: ${clause.operator}`);
2753
+ }
2754
+ });
2755
+ return conditions.length === 1 ? conditions[0] : and(...conditions);
2756
+ }
2757
+ function sanitizeData(data) {
2758
+ const result = {};
2759
+ for (const [key, value] of Object.entries(data)) {
2760
+ result[key] = value === void 0 ? null : value;
2761
+ }
2762
+ return result;
2763
+ }
2764
+ var ID_FIELD_RE = /[a-z]Id$/;
2765
+ function isIdField(key) {
2766
+ return key === "id" || ID_FIELD_RE.test(key);
2767
+ }
2768
+ function stringifyIds(row) {
2769
+ if (row == null || typeof row !== "object")
2770
+ return row;
2771
+ if (Array.isArray(row))
2772
+ return row.map(stringifyIds);
2773
+ const out = {};
2774
+ for (const [key, value] of Object.entries(row)) {
2775
+ if (isIdField(key) && (typeof value === "number" || typeof value === "bigint")) {
2776
+ out[key] = String(value);
2777
+ } else if (value && typeof value === "object" && !(value instanceof Date)) {
2778
+ out[key] = stringifyIds(value);
2779
+ } else {
2780
+ out[key] = value;
2781
+ }
2782
+ }
2783
+ return out;
2784
+ }
2785
+ function buildRawSql(sqlText, params = []) {
2786
+ if (params.length === 0)
2787
+ return sql3.raw(sqlText);
2788
+ if (sqlText.includes("?")) {
2789
+ const parts = sqlText.split("?");
2790
+ if (parts.length - 1 !== params.length) {
2791
+ throw Errors.badRequest(`rawQuery placeholder count (${parts.length - 1}) does not match params (${params.length})`);
2792
+ }
2793
+ let query = sql3.raw(parts[0]);
2794
+ for (let i = 0; i < params.length; i++) {
2795
+ query = sql3`${query}${params[i]}${sql3.raw(parts[i + 1])}`;
2796
+ }
2797
+ return query;
2798
+ }
2799
+ throw Errors.badRequest(
2800
+ "rawQuery uses ? positional placeholders on every dialect; params were provided but no ? placeholders were found"
2801
+ );
2802
+ }
2803
+ function normalizeRawRows(result) {
2804
+ if (Array.isArray(result))
2805
+ return result;
2806
+ const rows = result?.rows;
2807
+ if (Array.isArray(rows))
2808
+ return rows;
2809
+ return [];
2810
+ }
2811
+ function sqliteStatementReturnsRows(sqlText) {
2812
+ const normalized = sqlText.trimStart().toLowerCase();
2813
+ return normalized.startsWith("select") || normalized.startsWith("with") || normalized.startsWith("pragma") || normalized.includes(" returning ");
2814
+ }
2815
+ function createDrizzleAdapter(db, options) {
2816
+ const dialect = options?.dialect ?? "sqlite";
2817
+ const defaults = dialect === "pg" ? PG_DEFAULT_TABLE_MAP : SQLITE_DEFAULT_TABLE_MAP;
2818
+ const tableMap = { ...defaults, ...options?.tables };
2819
+ const isSqlite = dialect === "sqlite";
2820
+ let sqliteTxChain = Promise.resolve();
2821
+ const sqliteTxContext = new AsyncLocalStorage();
2822
+ function serializeSqlite(op) {
2823
+ if (!isSqlite || sqliteTxContext.getStore())
2824
+ return op();
2825
+ const result = sqliteTxChain.then(op, op);
2826
+ sqliteTxChain = result.catch(() => void 0);
2827
+ return result;
2828
+ }
2829
+ async function execOne(query) {
2830
+ const row = isSqlite ? query.get() : (await query)[0];
2831
+ return row === void 0 ? void 0 : stringifyIds(row);
2832
+ }
2833
+ async function execMany(query) {
2834
+ const rows = isSqlite ? query.all() : await query;
2835
+ return rows.map(stringifyIds);
2836
+ }
2837
+ async function execRun(query) {
2838
+ if (isSqlite) {
2839
+ query.run();
2840
+ return;
2841
+ }
2842
+ await query;
2843
+ }
2844
+ function getTable(model) {
2845
+ const table = tableMap[model];
2846
+ if (!table) {
2847
+ throw Errors.badRequest(`Unknown model: ${model}`);
2848
+ }
2849
+ return table;
2850
+ }
2851
+ function buildAdapter(drizzle) {
2852
+ const self = {
2853
+ get dialect() {
2854
+ return dialect;
2855
+ },
2856
+ async create(params) {
2857
+ return serializeSqlite(async () => {
2858
+ const table = getTable(params.model);
2859
+ try {
2860
+ const result = await execOne(drizzle.insert(table).values(sanitizeData(params.data)).returning());
2861
+ return result;
2862
+ } catch (err) {
2863
+ rethrowDbError(err, dialect);
2864
+ }
2865
+ });
2866
+ },
2867
+ async findOne(params) {
2868
+ return serializeSqlite(async () => {
2869
+ const table = getTable(params.model);
2870
+ const condition = buildWhereCondition(table, params.where);
2871
+ const result = await execOne(drizzle.select().from(table).where(condition).limit(1));
2872
+ return result ?? null;
2873
+ });
2874
+ },
2875
+ async findMany(params) {
2876
+ return serializeSqlite(async () => {
2877
+ const table = getTable(params.model);
2878
+ let query = drizzle.select().from(table).$dynamic();
2879
+ if (params.where && params.where.length > 0) {
2880
+ const condition = buildWhereCondition(table, params.where);
2881
+ query = query.where(condition);
2882
+ }
2883
+ if (params.sortBy) {
2884
+ const column = getColumn(table, params.sortBy.field);
2885
+ query = query.orderBy(
2886
+ params.sortBy.direction === "desc" ? sql3`${column} desc` : sql3`${column} asc`
2887
+ );
2888
+ }
2889
+ if (params.limit) {
2890
+ query = query.limit(params.limit);
2891
+ }
2892
+ if (params.offset) {
2893
+ query = query.offset(params.offset);
2894
+ }
2895
+ return execMany(query);
2896
+ });
2897
+ },
2898
+ async update(params) {
2899
+ return serializeSqlite(async () => {
2900
+ const table = getTable(params.model);
2901
+ const condition = buildWhereCondition(table, params.where);
2902
+ const query = drizzle.update(table).set(sanitizeData(params.data)).where(condition).returning();
2903
+ try {
2904
+ if (isSqlite) {
2905
+ const rows = query.all().map(stringifyIds);
2906
+ return rows[0] ?? null;
2907
+ }
2908
+ const result = await execOne(query);
2909
+ return result ?? null;
2910
+ } catch (err) {
2911
+ rethrowDbError(err, dialect);
2912
+ }
2913
+ });
2914
+ },
2915
+ async delete(params) {
2916
+ return serializeSqlite(async () => {
2917
+ const table = getTable(params.model);
2918
+ const condition = buildWhereCondition(table, params.where);
2919
+ try {
2920
+ await execRun(drizzle.delete(table).where(condition));
2921
+ } catch (err) {
2922
+ rethrowDbError(err, dialect);
2923
+ }
2924
+ });
2925
+ },
2926
+ async count(params) {
2927
+ return serializeSqlite(async () => {
2928
+ const table = getTable(params.model);
2929
+ let query = drizzle.select({ count: sql3`count(*)` }).from(table).$dynamic();
2930
+ if (params.where && params.where.length > 0) {
2931
+ const condition = buildWhereCondition(table, params.where);
2932
+ query = query.where(condition);
2933
+ }
2934
+ const result = await execOne(query);
2935
+ return Number(result?.count) || 0;
2936
+ });
2937
+ },
2938
+ async rawQuery(sqlText, params) {
2939
+ return serializeSqlite(async () => {
2940
+ const query = buildRawSql(sqlText, params ?? []);
2941
+ if (isSqlite) {
2942
+ if (typeof drizzle.all === "function" && sqliteStatementReturnsRows(sqlText))
2943
+ return normalizeRawRows(drizzle.all(query)).map(stringifyIds);
2944
+ if (typeof drizzle.run === "function") {
2945
+ drizzle.run(query);
2946
+ return [];
2947
+ }
2948
+ if (typeof drizzle.execute === "function")
2949
+ return normalizeRawRows(await drizzle.execute(query)).map(stringifyIds);
2950
+ throw Errors.badRequest("rawQuery is not supported by this SQLite Drizzle driver");
2951
+ }
2952
+ if (typeof drizzle.execute !== "function")
2953
+ throw Errors.badRequest("rawQuery is not supported by this Drizzle driver");
2954
+ return normalizeRawRows(await drizzle.execute(query)).map(stringifyIds);
2955
+ });
2956
+ },
2957
+ async transaction(fn) {
2958
+ if (dialect === "sqlite") {
2959
+ if (sqliteTxContext.getStore()) {
2960
+ throw Errors.badRequest("Nested transactions are not supported by the SQLite Drizzle adapter");
2961
+ }
2962
+ const run = async () => {
2963
+ drizzle.run(sql3`BEGIN IMMEDIATE`);
2964
+ try {
2965
+ const result2 = await sqliteTxContext.run(true, () => fn(self));
2966
+ drizzle.run(sql3`COMMIT`);
2967
+ return result2;
2968
+ } catch (error) {
2969
+ drizzle.run(sql3`ROLLBACK`);
2970
+ throw error;
2971
+ }
2972
+ };
2973
+ const result = sqliteTxChain.then(run, run);
2974
+ sqliteTxChain = result.catch(() => void 0);
2975
+ return result;
2976
+ }
2977
+ return drizzle.transaction(async (tx) => {
2978
+ const txAdapter = buildAdapter(tx);
2979
+ return fn(txAdapter);
2980
+ });
2981
+ }
2982
+ };
2983
+ return self;
2984
+ }
2985
+ return buildAdapter(db);
2986
+ }
2987
+
2988
+ // src/core/schema-builder.ts
2989
+ import { fromJSONSchema } from "@bajustone/fetcher/openapi";
2990
+ import { date as fDate, datetime as fDatetime, email as fEmail, time as fTime, url as fUrl, uuid as fUuid } from "@bajustone/fetcher/schema";
2991
+ function collectRefNames(schema, acc) {
2992
+ if (!schema || typeof schema !== "object")
2993
+ return acc;
2994
+ if (typeof schema.$ref === "string") {
2995
+ const i = schema.$ref.lastIndexOf("/");
2996
+ acc.add(i >= 0 ? schema.$ref.slice(i + 1) : schema.$ref);
2997
+ }
2998
+ if (schema.properties) {
2999
+ for (const key of Object.keys(schema.properties))
3000
+ collectRefNames(schema.properties[key], acc);
3001
+ }
3002
+ collectRefNames(schema.items, acc);
3003
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
3004
+ const variants = schema[key];
3005
+ if (variants) {
3006
+ for (const variant of variants)
3007
+ collectRefNames(variant, acc);
3008
+ }
3009
+ }
3010
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object")
3011
+ collectRefNames(schema.additionalProperties, acc);
3012
+ return acc;
3013
+ }
3014
+ var refDefinitionContexts = /* @__PURE__ */ new WeakMap();
3015
+ function refName(refPath) {
3016
+ const i = refPath.lastIndexOf("/");
3017
+ return i >= 0 ? refPath.slice(i + 1) : refPath;
3018
+ }
3019
+ var COMPONENT_NAME_RE = /^[\w.-]+$/;
3020
+ function assertComponentName(name) {
3021
+ if (!COMPONENT_NAME_RE.test(name))
3022
+ throw new Error(`Invalid OpenAPI component name '${name}'`);
3023
+ }
3024
+ function resolveRefDefs(schema) {
3025
+ if (collectRefNames(schema, /* @__PURE__ */ new Set()).size === 0)
3026
+ return void 0;
3027
+ const defs = /* @__PURE__ */ Object.create(null);
3028
+ const unresolved = /* @__PURE__ */ new Set();
3029
+ const hasOwn = (value, key) => Object.hasOwn(value, key);
3030
+ const visit = (node, inherited) => {
3031
+ if (!node || typeof node !== "object")
3032
+ return;
3033
+ const context = refDefinitionContexts.get(node) ?? inherited;
3034
+ if (typeof node.$ref === "string") {
3035
+ const name = refName(node.$ref);
3036
+ const component = context && hasOwn(context, name) ? context[name] : void 0;
3037
+ if (component) {
3038
+ if (hasOwn(defs, name) && !unresolved.has(name) && defs[name] !== component) {
3039
+ throw new Error(`Conflicting component definitions for $ref '${name}'`);
3040
+ }
3041
+ const firstResolution = !hasOwn(defs, name) || unresolved.delete(name);
3042
+ defs[name] = component;
3043
+ if (firstResolution)
3044
+ visit(component, context);
3045
+ } else if (!hasOwn(defs, name)) {
3046
+ defs[name] = {};
3047
+ unresolved.add(name);
3048
+ }
3049
+ }
3050
+ if (node.properties) {
3051
+ for (const child of Object.values(node.properties))
3052
+ visit(child, context);
3053
+ }
3054
+ visit(node.items, context);
3055
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
3056
+ for (const child of node[key] ?? [])
3057
+ visit(child, context);
3058
+ }
3059
+ if (node.additionalProperties && typeof node.additionalProperties === "object")
3060
+ visit(node.additionalProperties, context);
3061
+ };
3062
+ visit(schema);
3063
+ return defs;
3064
+ }
3065
+ function createStandardProps(schema) {
3066
+ let validateFn;
3067
+ return {
3068
+ version: 1,
3069
+ vendor: "fortress",
3070
+ validate(value) {
3071
+ if (!validateFn) {
3072
+ const defs = resolveRefDefs(schema);
3073
+ validateFn = fromJSONSchema(schema, defs)["~standard"].validate;
3074
+ }
3075
+ return validateFn(value);
3076
+ }
3077
+ };
3078
+ }
3079
+ function createExactOneOfProps(schemas) {
3080
+ const validate = (value) => {
3081
+ const results = schemas.map((schema) => schema["~standard"].validate(value));
3082
+ if (results.some((result) => result instanceof Promise)) {
3083
+ return Promise.all(results).then((resolved) => exactOneResult(resolved));
3084
+ }
3085
+ return exactOneResult(results);
3086
+ };
3087
+ return {
3088
+ version: 1,
3089
+ vendor: "fortress",
3090
+ validate
3091
+ };
3092
+ }
3093
+ function exactOneResult(results) {
3094
+ const successes = results.filter((result) => !result.issues);
3095
+ if (successes.length === 1)
3096
+ return { value: successes[0].value };
3097
+ return {
3098
+ issues: [{
3099
+ message: successes.length === 0 ? "Value must match exactly one oneOf variant" : "Value must match exactly one oneOf variant; multiple variants matched"
3100
+ }]
3101
+ };
3102
+ }
3103
+ function toFortressSchema(schema, refDefinitions, standardProps) {
3104
+ if (refDefinitions)
3105
+ refDefinitionContexts.set(schema, refDefinitions);
3106
+ return Object.assign(schema, {
3107
+ "~standard": standardProps ?? createStandardProps(schema)
3108
+ });
3109
+ }
3110
+ function str(opts) {
3111
+ const o = typeof opts === "string" ? { description: opts } : opts ?? {};
3112
+ const s = { type: "string" };
3113
+ if (o.description)
3114
+ s.description = o.description;
3115
+ if (o.min !== void 0)
3116
+ s.minLength = o.min;
3117
+ if (o.max !== void 0)
3118
+ s.maxLength = o.max;
3119
+ if (o.pattern !== void 0)
3120
+ s.pattern = o.pattern;
3121
+ if (o.format !== void 0)
3122
+ s.format = o.format;
3123
+ return toFortressSchema(s);
3124
+ }
3125
+ function int(opts) {
3126
+ const o = typeof opts === "string" ? { description: opts } : opts ?? {};
3127
+ const s = { type: "integer" };
3128
+ if (o.description)
3129
+ s.description = o.description;
3130
+ if (o.min !== void 0)
3131
+ s.minimum = o.min;
3132
+ if (o.max !== void 0)
3133
+ s.maximum = o.max;
3134
+ return toFortressSchema(s);
3135
+ }
3136
+ function id(description) {
3137
+ const s = { type: "string", minLength: 1 };
3138
+ if (description)
3139
+ s.description = description;
3140
+ return toFortressSchema(s);
3141
+ }
3142
+ function bool(description) {
3143
+ const s = { type: "boolean" };
3144
+ if (description)
3145
+ s.description = description;
3146
+ return toFortressSchema(s);
3147
+ }
3148
+ function arr(items, description) {
3149
+ const s = { type: "array", items };
3150
+ if (description)
3151
+ s.description = description;
3152
+ return toFortressSchema(s);
3153
+ }
3154
+ function obj(properties, ...required) {
3155
+ const s = { type: "object", properties };
3156
+ if (required.length > 0)
3157
+ s.required = required;
3158
+ return toFortressSchema(s);
3159
+ }
3160
+ function nullable(schema) {
3161
+ if (typeof schema.$ref === "string")
3162
+ return toFortressSchema({ oneOf: [schema, { type: "null" }] });
3163
+ const s = { ...schema, nullable: true };
3164
+ return toFortressSchema(s, refDefinitionContexts.get(schema));
3165
+ }
3166
+ function oneOf(...schemas) {
3167
+ const schema = { oneOf: schemas };
3168
+ return toFortressSchema(
3169
+ schema,
3170
+ void 0,
3171
+ createExactOneOfProps(schemas)
3172
+ );
3173
+ }
3174
+ function defineComponents(components) {
3175
+ for (const name of Object.keys(components))
3176
+ assertComponentName(name);
3177
+ const definitions = Object.assign(
3178
+ /* @__PURE__ */ Object.create(null),
3179
+ components
3180
+ );
3181
+ return {
3182
+ components,
3183
+ ref: (name) => {
3184
+ assertComponentName(name);
3185
+ return toFortressSchema(
3186
+ { $ref: `#/components/schemas/${String(name)}` },
3187
+ definitions
3188
+ );
3189
+ }
3190
+ };
3191
+ }
3192
+ function enums(...values) {
3193
+ return toFortressSchema({ enum: values });
3194
+ }
3195
+ function strFormat(format, description) {
3196
+ const s = { type: "string", format };
3197
+ if (description)
3198
+ s.description = description;
3199
+ return toFortressSchema(s);
3200
+ }
3201
+ function nullType() {
3202
+ return toFortressSchema({ type: "null" });
3203
+ }
3204
+ function record(description) {
3205
+ const s = { type: "object", additionalProperties: true };
3206
+ if (description)
3207
+ s.description = description;
3208
+ return toFortressSchema(s);
3209
+ }
3210
+ function recordOf(valueSchema, description) {
3211
+ const s = { type: "object", additionalProperties: valueSchema };
3212
+ if (description)
3213
+ s.description = description;
3214
+ return toFortressSchema(s);
3215
+ }
3216
+ function makeFormatBuilder(factory) {
3217
+ const f = factory();
3218
+ return (description) => {
3219
+ const s = { type: "string" };
3220
+ if (f.format)
3221
+ s.format = f.format;
3222
+ if (f.pattern)
3223
+ s.pattern = f.pattern;
3224
+ if (description)
3225
+ s.description = description;
3226
+ return toFortressSchema(s);
3227
+ };
3228
+ }
3229
+ var email = makeFormatBuilder(fEmail);
3230
+ var uuid = makeFormatBuilder(fUuid);
3231
+ var url = makeFormatBuilder(fUrl);
3232
+ var datetime = makeFormatBuilder(fDatetime);
3233
+ var date = makeFormatBuilder(fDate);
3234
+ var time = makeFormatBuilder(fTime);
3235
+ function isStandardSchema(value) {
3236
+ return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
3237
+ }
3238
+ function isFortressSchema(value) {
3239
+ return isStandardSchema(value) && value["~standard"].vendor === "fortress" && ("type" in value || "$ref" in value || "oneOf" in value || "anyOf" in value);
3240
+ }
3241
+ var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
3242
+ function hasJsonSchemaShape(value) {
3243
+ if (typeof value !== "object" || value === null)
3244
+ return false;
3245
+ const v = value;
3246
+ if ("$ref" in v || "oneOf" in v || "anyOf" in v || "allOf" in v)
3247
+ return true;
3248
+ if (typeof v.type === "string" && JSON_SCHEMA_TYPES.has(v.type))
3249
+ return true;
3250
+ if (Array.isArray(v.type) && v.type.every((t) => typeof t === "string" && JSON_SCHEMA_TYPES.has(t)))
3251
+ return true;
3252
+ return false;
3253
+ }
3254
+ function extractJsonSchema(schema) {
3255
+ if (isFortressSchema(schema)) {
3256
+ return schema;
3257
+ }
3258
+ if (isStandardSchema(schema)) {
3259
+ const std = schema["~standard"];
3260
+ if (std?.jsonSchema?.input) {
3261
+ return std.jsonSchema.input({ target: "draft-2020-12" });
3262
+ }
3263
+ if (hasJsonSchemaShape(schema)) {
3264
+ return schema;
3265
+ }
3266
+ }
3267
+ return {};
3268
+ }
3269
+ var ErrorEnvelope = obj({
3270
+ code: str("Machine-readable error code"),
3271
+ message: str("Human-readable error message"),
3272
+ statusCode: int("HTTP status code"),
3273
+ details: toFortressSchema({ description: "Optional structured error details" })
3274
+ }, "code", "message", "statusCode");
3275
+ var EndpointBuilder = class {
3276
+ _method;
3277
+ _path;
3278
+ _handler = "";
3279
+ _summary = "";
3280
+ _description;
3281
+ _tags = [];
3282
+ _security = [];
3283
+ _deprecated = false;
3284
+ _permission;
3285
+ _body;
3286
+ _query;
3287
+ _params;
3288
+ _responses = {};
3289
+ constructor(method, path) {
3290
+ this._method = method;
3291
+ this._path = path;
3292
+ }
3293
+ summary(s) {
3294
+ this._summary = s;
3295
+ return this;
3296
+ }
3297
+ description(s) {
3298
+ this._description = s;
3299
+ return this;
3300
+ }
3301
+ tags(...t) {
3302
+ this._tags.push(...t);
3303
+ return this;
3304
+ }
3305
+ security(...s) {
3306
+ this._security.push(...s);
3307
+ return this;
3308
+ }
3309
+ deprecated() {
3310
+ this._deprecated = true;
3311
+ return this;
3312
+ }
3313
+ permission(resource, action) {
3314
+ this._permission = { resource, action };
3315
+ return this;
3316
+ }
3317
+ body(schema) {
3318
+ this._body = schema;
3319
+ return this;
3320
+ }
3321
+ query(schema) {
3322
+ this._query = schema;
3323
+ return this;
3324
+ }
3325
+ params(schema) {
3326
+ this._params = schema;
3327
+ return this;
3328
+ }
3329
+ response(status, description, schema) {
3330
+ const stored = { description };
3331
+ if (schema !== void 0) {
3332
+ stored.schema = isStandardSchema(schema) ? extractJsonSchema(schema) : schema;
3333
+ }
3334
+ this._responses[status] = stored;
3335
+ return this;
3336
+ }
3337
+ /**
3338
+ * Declare an error response that returns the canonical {@link ErrorEnvelope}
3339
+ * body. Shorthand for `.response(status, description, ErrorEnvelope)`.
3340
+ *
3341
+ * Aligns host endpoint error responses with what Fortress's own routes
3342
+ * emit, so a single client-side error parser works everywhere.
3343
+ *
3344
+ * ```ts
3345
+ * endpoint('GET', '/schools/:id')
3346
+ * .summary('Get a school')
3347
+ * .params(obj({ id: str() }, 'id'))
3348
+ * .response(200, 'OK', SchoolEnvelope)
3349
+ * .errorResponse(404, 'School not found')
3350
+ * .errorResponse(403, 'Forbidden')
3351
+ * .build();
3352
+ * ```
3353
+ */
3354
+ errorResponse(status, description) {
3355
+ return this.response(status, description, ErrorEnvelope);
3356
+ }
3357
+ handler(name) {
3358
+ this._handler = name;
3359
+ return this;
3360
+ }
3361
+ build() {
3362
+ const def = {
3363
+ method: this._method,
3364
+ path: this._path,
3365
+ handler: this._handler
3366
+ };
3367
+ if (this._summary || this._tags.length > 0 || this._security.length > 0 || this._description || this._deprecated || this._permission) {
3368
+ def.meta = {
3369
+ summary: this._summary,
3370
+ ...this._description && { description: this._description },
3371
+ ...this._tags.length > 0 && { tags: this._tags },
3372
+ ...this._security.length > 0 && { security: this._security },
3373
+ ...this._deprecated && { deprecated: true },
3374
+ ...this._permission && { permission: this._permission }
3375
+ };
3376
+ }
3377
+ if (this._body || this._query || this._params) {
3378
+ def.input = {};
3379
+ if (this._body) {
3380
+ def.input.body = extractJsonSchema(this._body);
3381
+ if (isStandardSchema(this._body))
3382
+ def.input.bodySchema = this._body;
3383
+ }
3384
+ if (this._query) {
3385
+ def.input.query = extractJsonSchema(this._query);
3386
+ if (isStandardSchema(this._query))
3387
+ def.input.querySchema = this._query;
3388
+ }
3389
+ if (this._params) {
3390
+ def.input.params = extractJsonSchema(this._params);
3391
+ if (isStandardSchema(this._params))
3392
+ def.input.paramsSchema = this._params;
3393
+ }
3394
+ }
3395
+ if (Object.keys(this._responses).length > 0) {
3396
+ def.responses = this._responses;
3397
+ }
3398
+ return def;
3399
+ }
3400
+ };
3401
+ function endpoint(method, path) {
3402
+ return new EndpointBuilder(method, path);
3403
+ }
3404
+
3405
+ // src/core/auth/auth-endpoints.ts
3406
+ var User = obj(
3407
+ {
3408
+ id: id("User ID"),
3409
+ email: strFormat("email", "User email"),
3410
+ name: str("Display name"),
3411
+ isActive: bool("Account active status"),
3412
+ emailVerified: bool("Email verification status"),
3413
+ createdAt: strFormat("date-time", "Creation timestamp"),
3414
+ updatedAt: strFormat("date-time", "Last update timestamp")
3415
+ },
3416
+ "id",
3417
+ "email",
3418
+ "name",
3419
+ "isActive",
3420
+ "createdAt",
3421
+ "updatedAt"
3422
+ );
3423
+ var AuthChallenge = obj(
3424
+ {
3425
+ reason: enums("two-factor", "webauthn", "email-verification", "magic-link"),
3426
+ continuationToken: str("Single-use post-auth continuation token")
3427
+ },
3428
+ "reason",
3429
+ "continuationToken"
3430
+ );
3431
+ var AuthResult = oneOf(
3432
+ obj(
3433
+ {
3434
+ status: enums("success"),
3435
+ user: User,
3436
+ method: enums("password", "refresh", "two-factor", "webauthn", "magic-link", "impersonation"),
3437
+ accessToken: str("JWT access token"),
3438
+ refreshToken: str("Refresh token for rotation"),
3439
+ pluginData: record()
3440
+ },
3441
+ "status",
3442
+ "user",
3443
+ "method",
3444
+ "accessToken",
3445
+ "refreshToken"
3446
+ ),
3447
+ obj(
3448
+ {
3449
+ status: enums("pending"),
3450
+ user: User,
3451
+ pending: AuthChallenge,
3452
+ pluginData: record()
3453
+ },
3454
+ "status",
3455
+ "user",
3456
+ "pending"
3457
+ ),
3458
+ obj(
3459
+ {
3460
+ status: enums("impersonation"),
3461
+ user: User,
3462
+ accessToken: str("JWT access token"),
3463
+ refreshToken: nullType(),
3464
+ pluginData: record()
3465
+ },
3466
+ "status",
3467
+ "user",
3468
+ "accessToken",
3469
+ "refreshToken"
3470
+ )
3471
+ );
3472
+ var AuthTokenPair = obj(
3473
+ {
3474
+ accessToken: str("JWT access token"),
3475
+ refreshToken: str("New refresh token")
3476
+ },
3477
+ "accessToken",
3478
+ "refreshToken"
3479
+ );
3480
+ var SessionInfo = obj(
3481
+ {
3482
+ id: id("Token ID"),
3483
+ ipAddress: nullable(str("Client IP address")),
3484
+ userAgent: nullable(str("Client user agent")),
3485
+ deviceName: nullable(str("Device name")),
3486
+ createdAt: strFormat("date-time", "Session creation time"),
3487
+ lastActiveAt: nullable(strFormat("date-time", "Last activity time"))
3488
+ },
3489
+ "id",
3490
+ "createdAt"
3491
+ );
3492
+ var CreateUserInput = obj(
3493
+ {
3494
+ email: email("User email"),
3495
+ // enforced format (ReDoS-safe pattern), not annotation-only
3496
+ name: str("Display name"),
3497
+ password: str("User password"),
3498
+ isActive: bool("Set active status (default true)")
3499
+ },
3500
+ "email",
3501
+ "name"
3502
+ );
3503
+ var ErrorResponse = obj(
3504
+ {
3505
+ code: str("Error code (e.g. UNAUTHORIZED)"),
3506
+ message: str("Human-readable message"),
3507
+ statusCode: int("HTTP status code")
3508
+ },
3509
+ "code",
3510
+ "message",
3511
+ "statusCode"
3512
+ );
3513
+ var LoginIdentifier = obj(
3514
+ {
3515
+ id: id("Identifier ID"),
3516
+ userId: id("User ID"),
3517
+ type: enums("email", "phone", "username"),
3518
+ value: str("Identifier value")
3519
+ },
3520
+ "id",
3521
+ "userId",
3522
+ "type",
3523
+ "value"
3524
+ );
3525
+ var authComponents = defineComponents({
3526
+ User,
3527
+ AuthChallenge,
3528
+ AuthResult,
3529
+ AuthTokenPair,
3530
+ SessionInfo,
3531
+ CreateUserInput,
3532
+ ErrorResponse,
3533
+ LoginIdentifier
3534
+ });
3535
+ var authComponentSchemas = authComponents.components;
3536
+ var authRef = authComponents.ref;
3537
+ var authEndpoints = {
3538
+ login: endpoint("POST", "/auth/login").summary("Login with credentials").tags("Auth").security("none").body(obj(
3539
+ {
3540
+ identifier: str("Email, username, or phone"),
3541
+ password: str("User password"),
3542
+ trustedDeviceToken: str("Opaque trusted-device token previously returned after two-factor verification")
3543
+ },
3544
+ "identifier",
3545
+ "password"
3546
+ )).response(200, "Login successful", authRef("AuthResult")).response(401, "Invalid credentials", authRef("ErrorResponse")).handler("login").build(),
3547
+ verifyTwoFactor: endpoint("POST", "/auth/2fa/verify").summary("Complete a pending two-factor challenge").tags("Auth", "Two-Factor").security("none").body(obj(
3548
+ {
3549
+ continuationToken: str("Single-use auth continuation token"),
3550
+ code: str("TOTP or backup code"),
3551
+ rememberDevice: bool("Issue an opaque trusted-device token after successful verification")
3552
+ },
3553
+ "continuationToken",
3554
+ "code"
3555
+ )).response(200, "Authentication result", authRef("AuthResult")).response(400, "Two-factor plugin unavailable", authRef("ErrorResponse")).response(401, "Invalid continuation or verification code", authRef("ErrorResponse")).handler("verifyTwoFactor").build(),
3556
+ verifyMagicLink: endpoint("POST", "/auth/magic-link/verify").summary("Verify a magic-link token").tags("Auth", "Magic Link").security("none").body(obj({ token: str("Raw magic-link token") }, "token")).response(200, "Authentication result", authRef("AuthResult")).response(400, "Magic-link plugin unavailable", authRef("ErrorResponse")).response(404, "Invalid or expired magic link", authRef("ErrorResponse")).handler("verifyMagicLink").build(),
3557
+ createUser: endpoint("POST", "/auth/register").summary("Create a new user").tags("Auth").security("none").body(CreateUserInput).response(201, "User created", authRef("User")).response(409, "Email already exists", authRef("ErrorResponse")).handler("createUser").build(),
3558
+ refresh: endpoint("POST", "/auth/refresh").summary("Refresh access token").tags("Auth").security("none").body(obj({ refreshToken: str("Current refresh token") }, "refreshToken")).response(200, "Tokens refreshed", authRef("AuthTokenPair")).response(401, "Invalid or expired refresh token", authRef("ErrorResponse")).handler("refresh").build(),
3559
+ logout: endpoint("POST", "/auth/logout").summary("Logout and revoke refresh token").tags("Auth").security("none").body(obj({ refreshToken: str("Refresh token to revoke") }, "refreshToken")).response(200, "Logged out", obj({ ok: bool() }, "ok")).handler("logout").build(),
3560
+ me: endpoint("GET", "/auth/me").summary("Get current user profile").tags("Auth").security("bearer").response(200, "Current user", authRef("User")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("me").build(),
3561
+ listSessions: endpoint("GET", "/auth/sessions").summary("List active sessions").tags("Auth").security("bearer").response(200, "Active sessions", arr(authRef("SessionInfo"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("listSessions").build(),
3562
+ revokeSession: endpoint("DELETE", "/auth/sessions/:id").summary("Revoke a specific session").tags("Auth").security("bearer").params(obj({ id: id("Session/token ID") }, "id")).response(200, "Session revoked", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("revokeSession").build(),
3563
+ revokeAllOtherSessions: endpoint("DELETE", "/auth/sessions").summary("Revoke all other sessions").description("Revokes all active sessions except the current one. Requires the current token ID.").tags("Auth").security("bearer").body(obj({ currentTokenId: id("Current token ID to keep") }, "currentTokenId")).response(200, "Other sessions revoked", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("revokeAllOtherSessions").build(),
3564
+ addLoginIdentifier: endpoint("POST", "/auth/identifiers").summary("Add a login identifier").tags("Auth").security("bearer").body(obj(
3565
+ {
3566
+ type: enums("email", "phone", "username"),
3567
+ value: str("Identifier value")
3568
+ },
3569
+ "type",
3570
+ "value"
3571
+ )).response(200, "Identifier added", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("addLoginIdentifier").build(),
3572
+ removeLoginIdentifier: endpoint("DELETE", "/auth/identifiers").summary("Remove a login identifier").tags("Auth").security("bearer").body(obj(
3573
+ {
3574
+ type: enums("email", "phone", "username"),
3575
+ value: str("Identifier value")
3576
+ },
3577
+ "type",
3578
+ "value"
3579
+ )).response(200, "Identifier removed", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("removeLoginIdentifier").build(),
3580
+ getLoginIdentifiers: endpoint("GET", "/auth/identifiers").summary("List login identifiers").tags("Auth").security("bearer").response(200, "Login identifiers", arr(authRef("LoginIdentifier"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getLoginIdentifiers").build(),
3581
+ impersonate: endpoint("POST", "/auth/impersonate").summary("Impersonate a user").description("Issue a short-lived, non-renewable token to act as another user. Requires fortress:impersonate permission.").tags("Auth").security("bearer").permission("fortress", "impersonate").body(obj(
3582
+ {
3583
+ targetUserId: id("User to impersonate"),
3584
+ reason: str("Reason for impersonation"),
3585
+ expirySeconds: int("Token expiry in seconds (default 3600)")
3586
+ },
3587
+ "targetUserId"
3588
+ )).response(200, "Impersonation token issued", authRef("AuthResult")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Target user not found", authRef("ErrorResponse")).handler("impersonate").build()
3589
+ };
3590
+
3591
+ // src/core/http/csrf.ts
3592
+ function resolveCsrfConfig(config) {
3593
+ return {
3594
+ enabled: config?.enabled ?? true,
3595
+ headerName: config?.headerName ?? "X-Fortress-CSRF",
3596
+ skipPaths: config?.skipPaths ?? [],
3597
+ rejectSameSite: config?.rejectSameSite ?? false
3598
+ };
3599
+ }
3600
+
3601
+ // src/core/iam/iam-endpoints.ts
3602
+ var Role = obj(
3603
+ {
3604
+ id: id("Role ID"),
3605
+ name: str("Role name"),
3606
+ description: nullable(str("Role description")),
3607
+ isSystem: bool("Whether this is a system role")
3608
+ },
3609
+ "id",
3610
+ "name"
3611
+ );
3612
+ var Group = obj(
3613
+ {
3614
+ id: id("Group ID"),
3615
+ name: str("Group name"),
3616
+ description: nullable(str("Group description"))
3617
+ },
3618
+ "id",
3619
+ "name"
3620
+ );
3621
+ var Permission = obj(
3622
+ {
3623
+ id: id("Permission ID"),
3624
+ resource: str("Resource name"),
3625
+ action: str("Action name"),
3626
+ effect: enums("ALLOW", "DENY"),
3627
+ conditions: arr(obj({
3628
+ field: str("Condition field"),
3629
+ operator: enums("eq", "neq", "in", "startsWith"),
3630
+ value: str("Condition value")
3631
+ }), "Permission conditions"),
3632
+ description: nullable(str("Permission description"))
3633
+ },
3634
+ "id",
3635
+ "resource",
3636
+ "action",
3637
+ "effect"
3638
+ );
3639
+ var PermissionInput = obj(
3640
+ {
3641
+ resource: str("Resource name"),
3642
+ action: str("Action name"),
3643
+ effect: enums("ALLOW", "DENY"),
3644
+ conditions: arr(obj({
3645
+ field: str("Condition field"),
3646
+ operator: enums("eq", "neq", "in", "startsWith"),
3647
+ value: str("Condition value")
3648
+ }))
3649
+ },
3650
+ "resource",
3651
+ "action"
3652
+ );
3653
+ var ServiceAccount = obj(
3654
+ {
3655
+ id: id("Service account ID"),
3656
+ name: str("Machine identifier \u2014 immutable after creation"),
3657
+ displayName: nullable(str("Human-readable label")),
3658
+ description: nullable(str("Free-form description")),
3659
+ isActive: bool("Whether the account can authenticate and resolve permissions"),
3660
+ createdAt: str("ISO 8601 creation timestamp"),
3661
+ updatedAt: str("ISO 8601 update timestamp")
3662
+ },
3663
+ "id",
3664
+ "name",
3665
+ "isActive"
3666
+ );
3667
+ var iamComponents = defineComponents({
3668
+ Role,
3669
+ Group,
3670
+ Permission,
3671
+ PermissionInput,
3672
+ ServiceAccount
3673
+ });
3674
+ var iamComponentSchemas = iamComponents.components;
3675
+ var iamRef = iamComponents.ref;
3676
+ var iamEndpoints = {
3677
+ // ── Resources ──
3678
+ getResources: endpoint("GET", "/iam/resources").summary("List all available resources").tags("IAM", "Resources").security("bearer").permission("fortress", "viewResources").response(200, "Available resources with their actions", obj({
3679
+ resources: recordOf(obj({
3680
+ actions: arr(str("Action name"), "Available actions"),
3681
+ description: str("Resource description")
3682
+ }), "Map of resource name to definition")
3683
+ }, "resources")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getResources").build(),
3684
+ // ── Roles ──
3685
+ getRoles: endpoint("GET", "/iam/roles").summary("List all roles").tags("IAM", "Roles").security("bearer").permission("fortress", "viewRoles").response(200, "All roles", arr(iamRef("Role"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getRoles").build(),
3686
+ createRole: endpoint("POST", "/iam/roles").summary("Create a role").tags("IAM", "Roles").security("bearer").permission("fortress", "createRole").body(obj(
3687
+ {
3688
+ name: str("Role name"),
3689
+ permissions: arr(iamRef("PermissionInput"), "Permissions to assign"),
3690
+ description: str("Role description")
3691
+ },
3692
+ "name",
3693
+ "permissions"
3694
+ )).response(201, "Role created", iamRef("Role")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createRole").build(),
3695
+ deleteRole: endpoint("DELETE", "/iam/roles/:id").summary("Delete a role").tags("IAM", "Roles").security("bearer").permission("fortress", "deleteRole").params(obj({ id: id("Role ID") }, "id")).response(200, "Role deleted", obj({ ok: bool() }, "ok")).response(400, "Cannot delete system role", authRef("ErrorResponse")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("deleteRole").build(),
3696
+ // ── Role Bindings ──
3697
+ bindRoleToUser: endpoint("POST", "/iam/roles/:id/bind/user").summary("Bind role to a user").tags("IAM", "Roles").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
3698
+ { userId: id("User ID"), tenantId: str("Tenant ID (optional)") },
3699
+ "userId"
3700
+ )).response(200, "Role bound to user", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToUser").build(),
3701
+ bindRoleToGroup: endpoint("POST", "/iam/roles/:id/bind/group").summary("Bind role to a group").tags("IAM", "Roles").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
3702
+ { groupId: id("Group ID"), tenantId: str("Tenant ID (optional)") },
3703
+ "groupId"
3704
+ )).response(200, "Role bound to group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToGroup").build(),
3705
+ unbindRole: endpoint("DELETE", "/iam/roles/:id/bind").summary("Unbind a role").tags("IAM", "Roles").security("bearer").permission("fortress", "unbindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
3706
+ {
3707
+ subjectType: enums("USER", "GROUP", "SERVICE_ACCOUNT"),
3708
+ subjectId: id("Subject ID"),
3709
+ tenantId: str("Tenant ID (optional)")
3710
+ },
3711
+ "subjectType",
3712
+ "subjectId"
3713
+ )).response(200, "Role unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindRole").build(),
3714
+ // ── Groups ──
3715
+ createGroup: endpoint("POST", "/iam/groups").summary("Create a group").tags("IAM", "Groups").security("bearer").permission("fortress", "createGroup").body(obj(
3716
+ { name: str("Group name"), description: str("Group description") },
3717
+ "name"
3718
+ )).response(201, "Group created", iamRef("Group")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createGroup").build(),
3719
+ addUserToGroup: endpoint("POST", "/iam/groups/:id/users").summary("Add user to group").tags("IAM", "Groups").security("bearer").permission("fortress", "manageGroup").params(obj({ id: id("Group ID") }, "id")).body(obj({ userId: id("User ID") }, "userId")).response(200, "User added to group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("addUserToGroup").build(),
3720
+ removeUserFromGroup: endpoint("DELETE", "/iam/groups/:id/users/:userId").summary("Remove user from group").tags("IAM", "Groups").security("bearer").permission("fortress", "manageGroup").params(obj({ id: id("Group ID"), userId: id("User ID") }, "id", "userId")).response(200, "User removed from group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("removeUserFromGroup").build(),
3721
+ // ── Permissions ──
3722
+ getUserPermissions: endpoint("GET", "/iam/users/:id/permissions").summary("Get user permissions").tags("IAM", "Permissions").security("bearer").permission("fortress", "viewPermissions").params(obj({ id: id("User ID") }, "id")).query(obj({ tenantId: str("Tenant ID (optional)") })).response(200, "User permissions", arr(iamRef("Permission"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getUserPermissions").build(),
3723
+ checkPermission: endpoint("POST", "/iam/check").summary("Check if user has permission").tags("IAM", "Permissions").security("bearer").permission("fortress", "viewPermissions").body(obj(
3724
+ {
3725
+ userId: id("User ID"),
3726
+ resource: str("Resource name"),
3727
+ action: str("Action name"),
3728
+ context: record("Permission context")
3729
+ },
3730
+ "userId",
3731
+ "resource",
3732
+ "action"
3733
+ )).response(200, "Permission check result", obj({ allowed: bool("Whether permission is granted") }, "allowed")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("checkPermission").build(),
3734
+ bindPermissionToUser: endpoint("POST", "/iam/permissions/bind/user").summary("Bind permission directly to a user").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
3735
+ {
3736
+ userId: id("User ID"),
3737
+ permission: iamRef("PermissionInput"),
3738
+ tenantId: str("Tenant ID (optional)")
3739
+ },
3740
+ "userId",
3741
+ "permission"
3742
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToUser").build(),
3743
+ bindPermissionToGroup: endpoint("POST", "/iam/permissions/bind/group").summary("Bind permission directly to a group").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
3744
+ {
3745
+ groupId: id("Group ID"),
3746
+ permission: iamRef("PermissionInput"),
3747
+ tenantId: str("Tenant ID (optional)")
3748
+ },
3749
+ "groupId",
3750
+ "permission"
3751
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToGroup").build(),
3752
+ unbindPermissionFromUser: endpoint("DELETE", "/iam/permissions/bind/user").summary("Unbind permission from a user").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
3753
+ {
3754
+ userId: id("User ID"),
3755
+ permissionId: id("Permission ID"),
3756
+ tenantId: str("Tenant ID (optional)")
3757
+ },
3758
+ "userId",
3759
+ "permissionId"
3760
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromUser").build(),
3761
+ unbindPermissionFromGroup: endpoint("DELETE", "/iam/permissions/bind/group").summary("Unbind permission from a group").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
3762
+ {
3763
+ groupId: id("Group ID"),
3764
+ permissionId: id("Permission ID"),
3765
+ tenantId: str("Tenant ID (optional)")
3766
+ },
3767
+ "groupId",
3768
+ "permissionId"
3769
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromGroup").build(),
3770
+ // ── Service Accounts ──
3771
+ createServiceAccount: endpoint("POST", "/iam/service-accounts").summary("Create a service account").description("Create a non-human IAM principal. Service accounts hold roles and direct permissions just like users, but have no sessions, passwords, or group memberships. The `name` is immutable after creation.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "createServiceAccount").body(obj(
3772
+ {
3773
+ name: str("Machine identifier (immutable)"),
3774
+ displayName: str("Human-readable label"),
3775
+ description: str("Free-form description")
3776
+ },
3777
+ "name"
3778
+ )).response(201, "Service account created", iamRef("ServiceAccount")).response(400, "Bad request", authRef("ErrorResponse")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createServiceAccount").build(),
3779
+ listServiceAccounts: endpoint("GET", "/iam/service-accounts").summary("List service accounts").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "viewServiceAccounts").query(obj({
3780
+ limit: int("Page size (default 50)"),
3781
+ offset: int("Offset from the start of the list")
3782
+ })).response(200, "Service accounts", obj({
3783
+ serviceAccounts: arr(iamRef("ServiceAccount")),
3784
+ total: int("Total service accounts")
3785
+ }, "serviceAccounts", "total")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("listServiceAccounts").build(),
3786
+ getServiceAccount: endpoint("GET", "/iam/service-accounts/:id").summary("Get a service account by ID").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "viewServiceAccounts").params(obj({ id: id("Service account ID") }, "id")).response(200, "Service account", iamRef("ServiceAccount")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("getServiceAccount").build(),
3787
+ updateServiceAccount: endpoint("PATCH", "/iam/service-accounts/:id").summary("Update a service account").description("Update displayName, description, or isActive. The `name` field is immutable \u2014 to rename, delete and recreate.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "manageServiceAccount").params(obj({ id: id("Service account ID") }, "id")).body(obj({
3788
+ displayName: nullable(str("New human-readable label")),
3789
+ description: nullable(str("New description")),
3790
+ isActive: bool("Whether the account is active")
3791
+ })).response(200, "Service account updated", iamRef("ServiceAccount")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("updateServiceAccount").build(),
3792
+ deleteServiceAccount: endpoint("DELETE", "/iam/service-accounts/:id").summary("Delete a service account").description("Hard-deletes the service account and cascades to role bindings, direct permission bindings, and (via plugin observer) api keys owned by the account.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "manageServiceAccount").params(obj({ id: id("Service account ID") }, "id")).response(200, "Service account deleted", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("deleteServiceAccount").build(),
3793
+ getServiceAccountPermissions: endpoint("GET", "/iam/service-accounts/:id/permissions").summary("Get effective permissions for a service account").tags("IAM", "Service Accounts", "Permissions").security("bearer").permission("fortress", "viewPermissions").params(obj({ id: id("Service account ID") }, "id")).query(obj({ tenantId: str("Tenant ID (optional)") })).response(200, "Service account permissions", arr(iamRef("Permission"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getServiceAccountPermissions").build(),
3794
+ bindRoleToServiceAccount: endpoint("POST", "/iam/roles/:id/bind/service-account").summary("Bind role to a service account").tags("IAM", "Roles", "Service Accounts").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
3795
+ { serviceAccountId: id("Service account ID"), tenantId: str("Tenant ID (optional)") },
3796
+ "serviceAccountId"
3797
+ )).response(200, "Role bound to service account", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToServiceAccount").build(),
3798
+ unbindRoleFromServiceAccount: endpoint("DELETE", "/iam/roles/:id/bind/service-account").summary("Unbind role from a service account").tags("IAM", "Roles", "Service Accounts").security("bearer").permission("fortress", "unbindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
3799
+ { serviceAccountId: id("Service account ID"), tenantId: str("Tenant ID (optional)") },
3800
+ "serviceAccountId"
3801
+ )).response(200, "Role unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindRoleFromServiceAccount").build(),
3802
+ bindPermissionToServiceAccount: endpoint("POST", "/iam/permissions/bind/service-account").summary("Bind permission directly to a service account").tags("IAM", "Permissions", "Service Accounts").security("bearer").permission("fortress", "managePermissions").body(obj(
3803
+ {
3804
+ serviceAccountId: id("Service account ID"),
3805
+ permission: iamRef("PermissionInput"),
3806
+ tenantId: str("Tenant ID (optional)")
3807
+ },
3808
+ "serviceAccountId",
3809
+ "permission"
3810
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToServiceAccount").build(),
3811
+ unbindPermissionFromServiceAccount: endpoint("DELETE", "/iam/permissions/bind/service-account").summary("Unbind permission from a service account").tags("IAM", "Permissions", "Service Accounts").security("bearer").permission("fortress", "managePermissions").body(obj(
3812
+ {
3813
+ serviceAccountId: id("Service account ID"),
3814
+ permissionId: id("Permission ID"),
3815
+ tenantId: str("Tenant ID (optional)")
3816
+ },
3817
+ "serviceAccountId",
3818
+ "permissionId"
3819
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromServiceAccount").build()
3820
+ };
3821
+
3822
+ // src/core/manifest/route-manifest.ts
3823
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
3824
+ function endpointKey(endpoint2) {
3825
+ return `${endpoint2.method.toUpperCase()} ${endpoint2.path}`;
3826
+ }
3827
+ function classifyEndpoint(endpoint2) {
3828
+ const security = endpoint2.meta?.security ?? [];
3829
+ if (endpoint2.meta?.bearerKind === "oauth")
3830
+ return "oauth-protocol";
3831
+ if (security.includes("none"))
3832
+ return "public";
3833
+ if (endpoint2.meta?.permission)
3834
+ return "rbac";
3835
+ if (security.includes("bearer") || security.includes("apiKey") || security.includes("basic"))
3836
+ return "authenticated";
3837
+ return "default-deny";
3838
+ }
3839
+ function pathPatternToRegex(pattern) {
3840
+ const regexStr = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+").replace(/\\\*/g, ".*").replace(/\//g, "\\/");
3841
+ return new RegExp(`^${regexStr}$`);
3842
+ }
3843
+ function middlewareMatchesEndpoint(middleware, endpoint2) {
3844
+ if (!pathPatternToRegex(middleware.path).test(endpoint2.path))
3845
+ return false;
3846
+ const methods = middleware.methods;
3847
+ if (methods && !methods.map((m) => m.toUpperCase()).includes(endpoint2.method.toUpperCase()))
3848
+ return false;
3849
+ return true;
3850
+ }
3851
+ function isRateLimited(endpoint2, plugins) {
3852
+ for (const plugin of plugins) {
3853
+ if (plugin.name !== "rate-limit")
3854
+ continue;
3855
+ for (const middleware of plugin.middleware ?? []) {
3856
+ if (middlewareMatchesEndpoint(middleware, endpoint2))
3857
+ return true;
3858
+ }
3859
+ }
3860
+ if (plugins.some((plugin) => plugin.name === "rate-limit")) {
3861
+ if (endpoint2.method === "POST" && endpoint2.path === "/auth/login")
3862
+ return true;
3863
+ if (endpoint2.method === "POST" && endpoint2.path === "/auth/register")
3864
+ return true;
3865
+ if (endpoint2.method === "POST" && endpoint2.path === "/auth/refresh")
3866
+ return true;
3867
+ }
3868
+ return false;
3869
+ }
3870
+ function csrfApplies(endpoint2, fortress) {
3871
+ const csrf = resolveCsrfConfig(fortress.config.csrf);
3872
+ if (!csrf.enabled)
3873
+ return false;
3874
+ if (SAFE_METHODS.has(endpoint2.method.toUpperCase()))
3875
+ return false;
3876
+ for (const skip of csrf.skipPaths) {
3877
+ if (endpoint2.path === skip || endpoint2.path.startsWith(`${skip}/`))
3878
+ return false;
3879
+ }
3880
+ return true;
3881
+ }
3882
+ function collectEndpointOrigins(fortress) {
3883
+ const origins = /* @__PURE__ */ new Map();
3884
+ const coreAuth = Object.values(authEndpoints);
3885
+ const coreIam = Object.values(iamEndpoints);
3886
+ for (const endpoint2 of coreAuth)
3887
+ origins.set(endpointKey(endpoint2), { endpoint: endpoint2, plugin: "auth" });
3888
+ for (const endpoint2 of coreIam)
3889
+ origins.set(endpointKey(endpoint2), { endpoint: endpoint2, plugin: "iam" });
3890
+ for (const endpoint2 of Object.values(fortress.config.routes ?? {})) {
3891
+ origins.set(endpointKey(endpoint2), { endpoint: endpoint2, plugin: null });
3892
+ }
3893
+ for (const plugin of fortress.config.plugins ?? []) {
3894
+ for (const endpoint2 of Object.values(plugin.routes ?? {})) {
3895
+ origins.set(endpointKey(endpoint2), { endpoint: endpoint2, plugin: plugin.name });
3896
+ }
3897
+ }
3898
+ const byKey = /* @__PURE__ */ new Map();
3899
+ for (const endpoint2 of fortress.endpoints) {
3900
+ const key = endpointKey(endpoint2);
3901
+ byKey.set(key, origins.get(key) ?? { endpoint: endpoint2, plugin: null });
3902
+ }
3903
+ return [...byKey.values()];
3904
+ }
3905
+ function buildRouteManifest(fortress) {
3906
+ const plugins = fortress.config.plugins ?? [];
3907
+ return collectEndpointOrigins(fortress).map(({ endpoint: endpoint2, plugin }) => {
3908
+ const security = endpoint2.meta?.security ?? [];
3909
+ return {
3910
+ method: endpoint2.method,
3911
+ path: endpoint2.path,
3912
+ handler: endpoint2.handler,
3913
+ plugin,
3914
+ classification: classifyEndpoint(endpoint2),
3915
+ ...endpoint2.meta?.permission ? { permission: endpoint2.meta.permission } : {},
3916
+ security,
3917
+ ...endpoint2.meta?.bearerKind ? { bearerKind: endpoint2.meta.bearerKind } : {},
3918
+ csrfApplicable: csrfApplies(endpoint2, fortress),
3919
+ rateLimited: isRateLimited(endpoint2, plugins),
3920
+ // Top-level host routes are metadata/protection declarations only.
3921
+ // Framework adapters must fall through to the host's own handler.
3922
+ mounted: plugin !== null
3923
+ };
3924
+ }).sort((a, b) => `${a.method} ${a.path}`.localeCompare(`${b.method} ${b.path}`));
3925
+ }
3926
+
3927
+ // src/core/manifest/drift.ts
3928
+ function endpointKey2(route) {
3929
+ return `${route.method.toUpperCase()} ${route.path}`;
3930
+ }
3931
+ function manifestKey(route) {
3932
+ return `${route.method.toUpperCase()} ${route.path}`;
3933
+ }
3934
+ function toOpenAPIPath(path) {
3935
+ return path.replace(/:(\w+)/g, "{$1}");
3936
+ }
3937
+ function fromOpenAPIPath(path) {
3938
+ return path.replace(/\{(\w+)\}/g, ":$1");
3939
+ }
3940
+ function permissionKey(permission) {
3941
+ return permission ? `${permission.resource}:${permission.action}` : void 0;
3942
+ }
3943
+ function diffSets(expected, actual) {
3944
+ return [...expected].filter((item) => !actual.has(item)).sort();
3945
+ }
3946
+ function detectRouteManifestDrift(fortress, options = {}) {
3947
+ const canonicalManifest = buildRouteManifest(fortress);
3948
+ const manifest = options.manifest ?? fortress.manifest ?? canonicalManifest;
3949
+ const mounted = new Set(canonicalManifest.filter((entry) => entry.mounted).map(manifestKey));
3950
+ const manifestMountedRoutes = new Set(manifest.filter((entry) => entry.mounted).map(manifestKey));
3951
+ const manifestRoutes = new Set(manifest.map(manifestKey));
3952
+ let openapiRoutes = /* @__PURE__ */ new Set();
3953
+ if (options.openapi) {
3954
+ for (const [path, operations] of Object.entries(options.openapi.paths)) {
3955
+ for (const method of Object.keys(operations))
3956
+ openapiRoutes.add(`${method.toUpperCase()} ${fromOpenAPIPath(path)}`);
3957
+ }
3958
+ } else {
3959
+ openapiRoutes = new Set(fortress.endpoints.map((ep) => `${ep.method.toUpperCase()} ${toOpenAPIPath(ep.path)}`).map((key) => key.replace(/\{(\w+)\}/g, ":$1")));
3960
+ }
3961
+ const endpointPermissions = /* @__PURE__ */ new Map();
3962
+ for (const endpoint2 of fortress.endpoints)
3963
+ endpointPermissions.set(endpointKey2(endpoint2), permissionKey(endpoint2.meta?.permission));
3964
+ const rbacPermissionMismatches = [];
3965
+ for (const entry of manifest) {
3966
+ const key = manifestKey(entry);
3967
+ const expected = endpointPermissions.get(key);
3968
+ const actual = permissionKey(entry.permission);
3969
+ if (expected !== actual) {
3970
+ rbacPermissionMismatches.push({ route: key, expected, actual });
3971
+ continue;
3972
+ }
3973
+ if (entry.classification === "rbac" && !actual) {
3974
+ rbacPermissionMismatches.push({ route: key, expected, actual });
3975
+ }
3976
+ }
3977
+ return {
3978
+ mountedMissingFromManifest: diffSets(mounted, manifestMountedRoutes),
3979
+ manifestMissingFromMounted: diffSets(manifestMountedRoutes, mounted),
3980
+ openapiMissingFromManifest: diffSets(openapiRoutes, manifestRoutes),
3981
+ manifestMissingFromOpenapi: diffSets(manifestRoutes, openapiRoutes),
3982
+ rbacPermissionMismatches
3983
+ };
3984
+ }
3985
+ function hasRouteManifestDrift(drift) {
3986
+ return drift.mountedMissingFromManifest.length > 0 || drift.manifestMissingFromMounted.length > 0 || drift.openapiMissingFromManifest.length > 0 || drift.manifestMissingFromOpenapi.length > 0 || drift.rbacPermissionMismatches.length > 0;
3987
+ }
3988
+
3989
+ // src/core/migrations/engine.ts
3990
+ function assertRawQuery(db) {
3991
+ if (!db.rawQuery) {
3992
+ throw Errors.badRequest("Migration tooling requires a database adapter with rawQuery support");
3993
+ }
3994
+ return db.rawQuery.bind(db);
3995
+ }
3996
+ async function tableExists(db, dialect) {
3997
+ const rawQuery = assertRawQuery(db);
3998
+ if (dialect === "sqlite") {
3999
+ const rows2 = await rawQuery(
4000
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'fortress_schema_version'"
4001
+ );
4002
+ return rows2.length > 0;
4003
+ }
4004
+ const rows = await rawQuery(
4005
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'fortress_schema_version'"
4006
+ );
4007
+ return rows.length > 0;
4008
+ }
4009
+ async function listFortressTables(db, dialect) {
4010
+ const rawQuery = assertRawQuery(db);
4011
+ if (dialect === "sqlite") {
4012
+ const rows2 = await rawQuery(
4013
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'fortress_%'"
4014
+ );
4015
+ return new Set(rows2.map((row) => row.name));
4016
+ }
4017
+ const rows = await rawQuery(
4018
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name LIKE 'fortress_%'"
4019
+ );
4020
+ return new Set(rows.map((row) => row.table_name));
4021
+ }
4022
+ async function listColumns(db, dialect, table) {
4023
+ const rawQuery = assertRawQuery(db);
4024
+ if (dialect === "sqlite") {
4025
+ const rows2 = await rawQuery(`PRAGMA table_info(${table})`);
4026
+ return new Set(rows2.map((row) => row.name.toLowerCase()));
4027
+ }
4028
+ const rows = await rawQuery(
4029
+ "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?",
4030
+ [table]
4031
+ );
4032
+ return new Set(rows.map((row) => row.column_name.toLowerCase()));
4033
+ }
4034
+ async function listFortressIndexes(db, dialect) {
4035
+ const rawQuery = assertRawQuery(db);
4036
+ if (dialect === "sqlite") {
4037
+ const rows2 = await rawQuery(
4038
+ "SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex_%'"
4039
+ );
4040
+ return new Set(rows2.map((row) => row.name));
4041
+ }
4042
+ const rows = await rawQuery(
4043
+ "SELECT indexname FROM pg_indexes WHERE schemaname = 'public' AND tablename LIKE 'fortress_%'"
4044
+ );
4045
+ return new Set(rows.map((row) => row.indexname));
4046
+ }
4047
+ async function readCurrentVersion(db, dialect) {
4048
+ const hasVersionTable = await tableExists(db, dialect);
4049
+ if (!hasVersionTable)
4050
+ return { version: 0, hasVersionTable };
4051
+ const rawQuery = assertRawQuery(db);
4052
+ const rows = await rawQuery("SELECT version FROM fortress_schema_version WHERE id = 1");
4053
+ return { version: Number(rows[0]?.version ?? 0), hasVersionTable };
4054
+ }
4055
+ var sqliteMigrationChain = Promise.resolve();
4056
+ async function getMigrationStatus(db, dialect = db.dialect === "pg" ? "pg" : "sqlite") {
4057
+ const migrations = getFortressMigrations(dialect);
4058
+ const latestVersion = getLatestMigrationVersion(dialect);
4059
+ const { version: currentVersion, hasVersionTable } = await readCurrentVersion(db, dialect);
4060
+ return {
4061
+ dialect,
4062
+ currentVersion,
4063
+ latestVersion,
4064
+ pending: migrations.filter((migration) => migration.version > currentVersion),
4065
+ applied: migrations.filter((migration) => migration.version <= currentVersion),
4066
+ upToDate: currentVersion >= latestVersion,
4067
+ hasVersionTable
4068
+ };
4069
+ }
4070
+ async function detectMigrationDrift(db, dialect = db.dialect === "pg" ? "pg" : "sqlite") {
4071
+ const status = await getMigrationStatus(db, dialect);
4072
+ const present = await listFortressTables(db, dialect);
4073
+ const missingTables = FORTRESS_TABLES.filter((name) => !present.has(name));
4074
+ const expectedColumns = getExpectedColumns(dialect);
4075
+ const missingColumns = [];
4076
+ for (const [table, columns] of Object.entries(expectedColumns)) {
4077
+ if (!present.has(table))
4078
+ continue;
4079
+ const live = await listColumns(db, dialect, table);
4080
+ const missing = columns.filter((column) => !live.has(column));
4081
+ if (missing.length > 0)
4082
+ missingColumns.push({ table, columns: missing });
4083
+ }
4084
+ const presentIndexes = await listFortressIndexes(db, dialect);
4085
+ const missingIndexes = FORTRESS_INDEXES.map((index3) => index3.name).filter((name) => !presentIndexes.has(name));
4086
+ return {
4087
+ dialect,
4088
+ currentVersion: status.currentVersion,
4089
+ latestVersion: status.latestVersion,
4090
+ missingVersionTable: !status.hasVersionTable,
4091
+ unknownFutureVersion: status.currentVersion > status.latestVersion,
4092
+ pendingVersions: status.pending.map((migration) => migration.version),
4093
+ missingTables,
4094
+ missingColumns,
4095
+ missingIndexes
4096
+ };
4097
+ }
4098
+ function hasMigrationDrift(drift) {
4099
+ return drift.missingVersionTable || drift.unknownFutureVersion || drift.pendingVersions.length > 0 || drift.missingTables.length > 0 || drift.missingColumns.length > 0 || drift.missingIndexes.length > 0;
4100
+ }
4101
+
4102
+ // src/testing/checks.ts
4103
+ function checkRouteManifestDrift(fortress) {
4104
+ const drift = detectRouteManifestDrift(fortress);
4105
+ const messages = [];
4106
+ if (drift.mountedMissingFromManifest.length > 0)
4107
+ messages.push(`Mounted routes missing from manifest: ${drift.mountedMissingFromManifest.join(", ")}`);
4108
+ if (drift.manifestMissingFromMounted.length > 0)
4109
+ messages.push(`Manifest routes missing from mounted set: ${drift.manifestMissingFromMounted.join(", ")}`);
4110
+ if (drift.rbacPermissionMismatches.length > 0) {
4111
+ messages.push(
4112
+ `RBAC permission mismatches:
4113
+ ${drift.rbacPermissionMismatches.map((m) => `${m.route}: expected=${m.expected ?? "(none)"} actual=${m.actual ?? "(none)"}`).join("\n ")}`
4114
+ );
4115
+ }
4116
+ if (drift.openapiMissingFromManifest.length > 0)
4117
+ messages.push(`Routes in OpenAPI but missing from manifest: ${drift.openapiMissingFromManifest.join(", ")}`);
4118
+ if (drift.manifestMissingFromOpenapi.length > 0)
4119
+ messages.push(`Routes in manifest but missing from OpenAPI: ${drift.manifestMissingFromOpenapi.join(", ")}`);
4120
+ return { ok: !hasRouteManifestDrift(drift), drift, messages };
4121
+ }
4122
+ var DEFAULT_PUBLIC_ALLOW = [
4123
+ "POST /auth/login",
4124
+ "POST /auth/register",
4125
+ "POST /auth/refresh",
4126
+ "POST /auth/logout",
4127
+ "POST /auth/2fa/verify",
4128
+ "POST /auth/magic-link/verify",
4129
+ "GET /oauth/authorize",
4130
+ "POST /oauth/token",
4131
+ "POST /oauth/introspect",
4132
+ "POST /oauth/revoke",
4133
+ "GET /oauth/userinfo",
4134
+ "GET /oauth/.well-known/openid-configuration",
4135
+ "GET /oauth/.well-known/jwks.json"
4136
+ ];
4137
+ function checkPublicRoutes(fortress, options = {}) {
4138
+ const allow = /* @__PURE__ */ new Set([...options.allow ?? [], ...DEFAULT_PUBLIC_ALLOW]);
4139
+ const classifications = new Set(
4140
+ options.classifications ?? ["public", "oauth-protocol"]
4141
+ );
4142
+ const manifest = buildRouteManifest(fortress);
4143
+ const unexpected = manifest.filter(
4144
+ (entry) => classifications.has(entry.classification) && !allow.has(`${entry.method} ${entry.path}`)
4145
+ );
4146
+ const messages = unexpected.map(
4147
+ (entry) => `Unexpected ${entry.classification} route: ${entry.method} ${entry.path} (plugin=${entry.plugin ?? "core"})`
4148
+ );
4149
+ return { ok: unexpected.length === 0, unexpected, messages };
4150
+ }
4151
+ async function checkMigrationDrift(db, dialect) {
4152
+ const drift = await detectMigrationDrift(db, dialect);
4153
+ const messages = [];
4154
+ if (drift.missingVersionTable)
4155
+ messages.push("Schema version table is missing \u2014 run fortress.migrate()");
4156
+ if (drift.pendingVersions.length > 0)
4157
+ messages.push(`Pending migrations: ${drift.pendingVersions.join(", ")}`);
4158
+ if (drift.unknownFutureVersion)
4159
+ messages.push(`Database is at version ${drift.currentVersion} but bundled catalog stops at ${drift.latestVersion}`);
4160
+ if (drift.missingTables.length > 0)
4161
+ messages.push(`Missing Fortress tables: ${drift.missingTables.join(", ")}`);
4162
+ if (drift.missingColumns.length > 0) {
4163
+ messages.push(
4164
+ `Stale Fortress tables missing columns: ${drift.missingColumns.map((entry) => `${entry.table} (${entry.columns.join(", ")})`).join("; ")}`
4165
+ );
4166
+ }
4167
+ return { ok: !hasMigrationDrift(drift), drift, messages };
4168
+ }
4169
+ async function smokeTestAuth(fortress, options = {}) {
4170
+ const email2 = options.email ?? `smoke+${Date.now()}@fortress.test`;
4171
+ const password = options.password ?? "smoke-test-password-1234564!";
4172
+ const name = options.name ?? "Smoke Test";
4173
+ const messages = [];
4174
+ try {
4175
+ await fortress.auth.createUser({ email: email2, password, name });
4176
+ const login = await fortress.auth.login(email2, password);
4177
+ if (login.status !== "success" || !login.accessToken)
4178
+ throw new Error(`login returned status=${login.status}`);
4179
+ const claims = await fortress.auth.verifyToken(login.accessToken);
4180
+ if (claims.sub !== login.user.id)
4181
+ throw new Error(`token sub mismatch: ${claims.sub} vs user.id ${login.user.id}`);
4182
+ if (!login.refreshToken)
4183
+ throw new Error("login did not return a refresh token");
4184
+ const refreshed = await fortress.auth.refresh(login.refreshToken);
4185
+ if (!refreshed.accessToken)
4186
+ throw new Error("refresh did not return a new access token");
4187
+ await fortress.auth.logout(refreshed.refreshToken);
4188
+ return { ok: true, messages };
4189
+ } catch (err) {
4190
+ messages.push(`Auth smoke test failed: ${err instanceof Error ? err.message : String(err)}`);
4191
+ return { ok: false, messages };
4192
+ }
4193
+ }
4194
+ async function runFortressChecks(options) {
4195
+ const manifest = checkRouteManifestDrift(options.fortress);
4196
+ const publicRoutes = checkPublicRoutes(options.fortress, options.publicRoutes);
4197
+ const migrations = options.skipMigrations ? void 0 : await checkMigrationDrift(options.db ?? options.fortress.config.database);
4198
+ const authSmokeTest = options.skipAuthSmokeTest ? void 0 : await smokeTestAuth(options.fortress, options.smokeTest);
4199
+ const messages = [];
4200
+ if (!manifest.ok)
4201
+ messages.push(...manifest.messages.map((m) => `[manifest] ${m}`));
4202
+ if (!publicRoutes.ok)
4203
+ messages.push(...publicRoutes.messages.map((m) => `[public-routes] ${m}`));
4204
+ if (migrations && !migrations.ok)
4205
+ messages.push(...migrations.messages.map((m) => `[migrations] ${m}`));
4206
+ if (authSmokeTest && !authSmokeTest.ok)
4207
+ messages.push(...authSmokeTest.messages.map((m) => `[auth-smoke] ${m}`));
4208
+ return {
4209
+ ok: manifest.ok && publicRoutes.ok && (migrations?.ok ?? true) && (authSmokeTest?.ok ?? true),
4210
+ manifest,
4211
+ publicRoutes,
4212
+ ...migrations ? { migrations } : {},
4213
+ ...authSmokeTest ? { authSmokeTest } : {},
4214
+ messages
4215
+ };
4216
+ }
4217
+
4218
+ // src/testing/index.ts
4219
+ var CREATE_TABLES_SQL = getMigrationUpSql("sqlite");
4220
+ var STAMP_SCHEMA_VERSION_SQL = `
4221
+ INSERT INTO fortress_schema_version (id, version, applied_at)
4222
+ VALUES (1, ${getLatestMigrationVersion("sqlite")}, unixepoch())
4223
+ ON CONFLICT(id) DO UPDATE SET
4224
+ version = excluded.version,
4225
+ applied_at = excluded.applied_at;
4226
+ `;
4227
+ var isBun = typeof globalThis.Bun !== "undefined";
4228
+ var runtimeRequire = globalThis.require ?? createRequire(resolve(process.argv[1] ?? "__fortress_testing__.mjs"));
4229
+ function createTestAdapter() {
4230
+ if (isBun) {
4231
+ return createBunAdapter();
4232
+ }
4233
+ return createNodeAdapter();
4234
+ }
4235
+ function createBunAdapter() {
4236
+ const { Database } = runtimeRequire("bun:sqlite");
4237
+ const { drizzle } = runtimeRequire("drizzle-orm/bun-sqlite");
4238
+ const sqlite = new Database(":memory:");
4239
+ sqlite.exec("PRAGMA journal_mode = WAL;");
4240
+ sqlite.exec("PRAGMA foreign_keys = ON;");
4241
+ sqlite.exec(CREATE_TABLES_SQL);
4242
+ sqlite.exec(STAMP_SCHEMA_VERSION_SQL);
4243
+ const db = drizzle(sqlite);
4244
+ return createDrizzleAdapter(db);
4245
+ }
4246
+ function createNodeAdapter() {
4247
+ const BetterSqlite3 = runtimeRequire("better-sqlite3");
4248
+ const { drizzle } = runtimeRequire("drizzle-orm/better-sqlite3");
4249
+ const sqlite = new BetterSqlite3(":memory:");
4250
+ sqlite.pragma("journal_mode = WAL");
4251
+ sqlite.pragma("foreign_keys = ON");
4252
+ sqlite.exec(CREATE_TABLES_SQL);
4253
+ sqlite.exec(STAMP_SCHEMA_VERSION_SQL);
4254
+ const db = drizzle(sqlite);
4255
+ return createDrizzleAdapter(db);
4256
+ }
4257
+ export {
4258
+ checkMigrationDrift,
4259
+ checkPublicRoutes,
4260
+ checkRouteManifestDrift,
4261
+ createTestAdapter,
4262
+ runFortressChecks,
4263
+ smokeTestAuth
4264
+ };