@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,526 @@
1
+ // src/core/auth/refresh-token.ts
2
+ async function generateRefreshToken() {
3
+ const bytes = new Uint8Array(32);
4
+ crypto.getRandomValues(bytes);
5
+ const raw = base64UrlEncode(bytes);
6
+ const hash = await hashToken(raw);
7
+ return { raw, hash };
8
+ }
9
+ async function hashToken(raw) {
10
+ const data = new TextEncoder().encode(raw);
11
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
12
+ return hexEncode(new Uint8Array(hashBuffer));
13
+ }
14
+ function base64UrlEncode(bytes) {
15
+ const binString = Array.from(bytes, (b) => String.fromCodePoint(b)).join("");
16
+ return btoa(binString).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
17
+ }
18
+ function hexEncode(bytes) {
19
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
20
+ }
21
+
22
+ // src/core/errors.ts
23
+ var FortressError = class extends Error {
24
+ code;
25
+ statusCode;
26
+ retryAfter;
27
+ details;
28
+ /** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
29
+ oauthError;
30
+ /** Human-readable description for the OAuth `error_description` field. */
31
+ oauthDescription;
32
+ /** Optional URI for the OAuth `error_uri` field (§5.2). */
33
+ oauthErrorUri;
34
+ constructor(code, message, statusCode, options) {
35
+ super(message, { cause: options?.cause });
36
+ this.code = code;
37
+ this.statusCode = statusCode;
38
+ this.retryAfter = options?.retryAfter;
39
+ this.details = options?.details;
40
+ this.oauthError = options?.oauthError;
41
+ this.oauthDescription = options?.oauthDescription;
42
+ this.oauthErrorUri = options?.oauthErrorUri;
43
+ }
44
+ toJSON() {
45
+ return {
46
+ code: this.code,
47
+ message: this.message,
48
+ statusCode: this.statusCode,
49
+ ...this.details !== void 0 && { details: this.details }
50
+ };
51
+ }
52
+ };
53
+ var Errors = {
54
+ unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
55
+ tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
56
+ sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
57
+ sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
58
+ forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
59
+ badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
60
+ notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
61
+ conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
62
+ unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
63
+ rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
64
+ database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
65
+ serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
66
+ validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
67
+ /**
68
+ * RFC 6749 §5.2 / §4.1.2.1 OAuth error.
69
+ *
70
+ * The HTTP error mapper detects `oauthError` and emits the OAuth-spec
71
+ * JSON body `{ error, error_description, error_uri? }` instead of the
72
+ * default fortress `{ code, message }` shape, so strict OAuth clients
73
+ * (Moodle, openid-client, Spring Security, etc.) can switch behaviour
74
+ * on the machine-readable `error` field.
75
+ *
76
+ * Status is inferred from the OAuth code per the spec table:
77
+ * - `invalid_client` → 401
78
+ * - everything else → 400
79
+ */
80
+ oauth: (error, description, options) => {
81
+ const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
82
+ const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
83
+ return new FortressError(code, description ?? error, status, {
84
+ oauthError: error,
85
+ oauthDescription: description,
86
+ oauthErrorUri: options?.errorUri,
87
+ cause: options?.cause
88
+ });
89
+ },
90
+ /**
91
+ * Reconstruct a {@link FortressError} from a JSON error body emitted by
92
+ * `errorToResponse`. Used by the in-process `fortress.call.*` client to
93
+ * convert non-2xx responses into typed throws that mirror what a direct
94
+ * service-method call would produce.
95
+ *
96
+ * Maps by HTTP status when the body doesn't carry a recognizable
97
+ * `{ code, message }` payload, so network errors and opaque upstream
98
+ * failures still become structured `FortressError`s.
99
+ */
100
+ fromHttpResponse: (status, body) => {
101
+ const payload = body && typeof body === "object" ? body : {};
102
+ const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
103
+ const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
104
+ return new FortressError(code, message, status, { details: payload.details });
105
+ }
106
+ };
107
+ function isFortressErrorCode(code) {
108
+ 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";
109
+ }
110
+ function statusToErrorCode(status) {
111
+ if (status === 401)
112
+ return "UNAUTHORIZED";
113
+ if (status === 403)
114
+ return "FORBIDDEN";
115
+ if (status === 404)
116
+ return "NOT_FOUND";
117
+ if (status === 409)
118
+ return "CONFLICT";
119
+ if (status === 422)
120
+ return "UNPROCESSABLE_ENTITY";
121
+ if (status === 429)
122
+ return "RATE_LIMITED";
123
+ if (status === 503)
124
+ return "SERVICE_UNAVAILABLE";
125
+ if (status >= 500)
126
+ return "DATABASE_ERROR";
127
+ return "BAD_REQUEST";
128
+ }
129
+
130
+ // src/plugins/two-factor/index.ts
131
+ var ENCODER = new TextEncoder();
132
+ var DECODER = new TextDecoder();
133
+ var HEX_32_BYTE_KEY = /^[0-9a-f]{64}$/i;
134
+ function decodeEncryptionKey(value) {
135
+ const trimmed = value.trim();
136
+ if (HEX_32_BYTE_KEY.test(trimmed))
137
+ return new Uint8Array(trimmed.match(/.{2}/g).map((byte) => Number.parseInt(byte, 16)));
138
+ try {
139
+ const normalized = trimmed.replace(/-/g, "+").replace(/_/g, "/");
140
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
141
+ const raw = atob(padded);
142
+ const decoded = Uint8Array.from(raw, (char) => char.charCodeAt(0));
143
+ if (decoded.length === 32)
144
+ return decoded;
145
+ } catch {
146
+ }
147
+ return ENCODER.encode(value);
148
+ }
149
+ function base64UrlEncode2(value) {
150
+ return btoa(String.fromCharCode(...value)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
151
+ }
152
+ function base64UrlDecode(value) {
153
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
154
+ const raw = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
155
+ return Uint8Array.from(raw, (char) => char.charCodeAt(0));
156
+ }
157
+ function toArrayBuffer(value) {
158
+ return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
159
+ }
160
+ async function encryptSecret(keyPromise, secret, userId) {
161
+ const key = await keyPromise;
162
+ const iv = crypto.getRandomValues(new Uint8Array(12));
163
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt(
164
+ { name: "AES-GCM", iv, additionalData: ENCODER.encode(userId) },
165
+ key,
166
+ ENCODER.encode(secret)
167
+ ));
168
+ return `v1.${base64UrlEncode2(iv)}.${base64UrlEncode2(ciphertext)}`;
169
+ }
170
+ async function decryptSecret(keyPromise, encrypted, userId) {
171
+ const [version, encodedIv, encodedCiphertext] = encrypted.split(".");
172
+ if (version !== "v1" || !encodedIv || !encodedCiphertext)
173
+ throw Errors.unauthorized("Two-factor enrolment uses an unsupported secret format; re-enrolment is required");
174
+ try {
175
+ const key = await keyPromise;
176
+ const iv = base64UrlDecode(encodedIv);
177
+ const ciphertext = base64UrlDecode(encodedCiphertext);
178
+ const plaintext = await crypto.subtle.decrypt(
179
+ { name: "AES-GCM", iv: toArrayBuffer(iv), additionalData: ENCODER.encode(userId) },
180
+ key,
181
+ toArrayBuffer(ciphertext)
182
+ );
183
+ return DECODER.decode(plaintext);
184
+ } catch {
185
+ throw Errors.unauthorized("Unable to decrypt two-factor secret");
186
+ }
187
+ }
188
+ function generateSecret() {
189
+ const bytes = new Uint8Array(20);
190
+ crypto.getRandomValues(bytes);
191
+ return base32Encode(bytes);
192
+ }
193
+ function base32Encode(bytes) {
194
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
195
+ let bits = 0;
196
+ let value = 0;
197
+ let result = "";
198
+ for (const byte of bytes) {
199
+ value = value << 8 | byte;
200
+ bits += 8;
201
+ while (bits >= 5) {
202
+ result += alphabet[value >>> bits - 5 & 31];
203
+ bits -= 5;
204
+ }
205
+ }
206
+ if (bits > 0) {
207
+ result += alphabet[value << 5 - bits & 31];
208
+ }
209
+ return result;
210
+ }
211
+ var BASE32_PADDING = /=+$/;
212
+ function base32Decode(encoded) {
213
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
214
+ const cleanInput = encoded.replace(BASE32_PADDING, "").toUpperCase();
215
+ let bits = 0;
216
+ let value = 0;
217
+ const output = [];
218
+ for (const char of cleanInput) {
219
+ const idx = alphabet.indexOf(char);
220
+ if (idx === -1)
221
+ continue;
222
+ value = value << 5 | idx;
223
+ bits += 5;
224
+ if (bits >= 8) {
225
+ output.push(value >>> bits - 8 & 255);
226
+ bits -= 8;
227
+ }
228
+ }
229
+ return new Uint8Array(output);
230
+ }
231
+ async function generateTOTPForCounter(secret, digits, counter) {
232
+ const counterBytes = new ArrayBuffer(8);
233
+ const view = new DataView(counterBytes);
234
+ view.setBigUint64(0, BigInt(counter));
235
+ const keyBytes = base32Decode(secret);
236
+ const key = await crypto.subtle.importKey(
237
+ "raw",
238
+ keyBytes.buffer,
239
+ { name: "HMAC", hash: "SHA-1" },
240
+ false,
241
+ ["sign"]
242
+ );
243
+ const hmac = new Uint8Array(await crypto.subtle.sign("HMAC", key, counterBytes));
244
+ const offset = hmac.at(-1) & 15;
245
+ const code = ((hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255) % 10 ** digits;
246
+ return String(code).padStart(digits, "0");
247
+ }
248
+ async function generateTOTP(secret, period, digits, timeOffset = 0) {
249
+ const counter = Math.floor((Date.now() / 1e3 + timeOffset) / period);
250
+ return generateTOTPForCounter(secret, digits, counter);
251
+ }
252
+ async function verifyTOTPWithCounter(secret, code, period, digits) {
253
+ const currentCounter = Math.floor(Date.now() / 1e3 / period);
254
+ for (const counter of [currentCounter, currentCounter - 1, currentCounter + 1]) {
255
+ const expected = await generateTOTPForCounter(secret, digits, counter);
256
+ if (expected === code)
257
+ return counter;
258
+ }
259
+ return null;
260
+ }
261
+ async function verifyTOTP(secret, code, period, digits) {
262
+ return await verifyTOTPWithCounter(secret, code, period, digits) !== null;
263
+ }
264
+ function buildOtpauthUrl(secret, issuer, email, period, digits) {
265
+ const label = `${issuer}:${email}`;
266
+ const params = new URLSearchParams({
267
+ secret,
268
+ issuer,
269
+ algorithm: "SHA1",
270
+ digits: String(digits),
271
+ period: String(period)
272
+ });
273
+ return `otpauth://totp/${encodeURIComponent(label)}?${params.toString()}`;
274
+ }
275
+ async function generateBackupCodes(count) {
276
+ const raw = [];
277
+ const hashes = [];
278
+ for (let i = 0; i < count; i++) {
279
+ const bytes = new Uint8Array(16);
280
+ crypto.getRandomValues(bytes);
281
+ const code = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
282
+ raw.push(code);
283
+ hashes.push(await hashToken(code));
284
+ }
285
+ return { raw, hashes };
286
+ }
287
+ function twoFactor(config) {
288
+ if (!config?.secretEncryptionKey)
289
+ throw Errors.badRequest("twoFactor requires secretEncryptionKey");
290
+ const keyBytes = decodeEncryptionKey(config.secretEncryptionKey);
291
+ if (keyBytes.length !== 32)
292
+ throw Errors.badRequest("twoFactor secretEncryptionKey must decode to exactly 32 bytes for AES-256-GCM");
293
+ const keyBuffer = keyBytes.buffer.slice(keyBytes.byteOffset, keyBytes.byteOffset + keyBytes.byteLength);
294
+ const encryptionKey = crypto.subtle.importKey("raw", keyBuffer, "AES-GCM", false, ["encrypt", "decrypt"]);
295
+ const issuer = config.totp?.issuer ?? "Fortress";
296
+ const period = config.totp?.period ?? 30;
297
+ const digits = config.totp?.digits ?? 6;
298
+ const backupCodeCount = config.backupCodes?.count ?? 10;
299
+ const trustedDeviceDays = config.trustedDeviceDays ?? 30;
300
+ const maxAttempts = config.maxAttempts ?? 5;
301
+ const failedAttemptCooldownSeconds = config.failedAttemptCooldownSeconds ?? 1;
302
+ async function verifyCode(db, userId, code, meta) {
303
+ const secretRecord = await db.findOne({
304
+ model: "two_factor_secret",
305
+ where: [{ field: "userId", operator: "=", value: userId }]
306
+ });
307
+ if (!secretRecord)
308
+ throw Errors.badRequest("Two-factor authentication is not set up");
309
+ const secret = await decryptSecret(encryptionKey, secretRecord.secret, userId);
310
+ const matchedCounter = await verifyTOTPWithCounter(secret, code, period, digits);
311
+ if (matchedCounter !== null) {
312
+ const previousCounter = secretRecord.lastUsedCounter;
313
+ if (previousCounter !== null && matchedCounter <= previousCounter)
314
+ throw Errors.unauthorized("Two-factor code has already been used");
315
+ const claimed2 = await db.update({
316
+ model: "two_factor_secret",
317
+ where: [
318
+ { field: "id", operator: "=", value: secretRecord.id },
319
+ previousCounter === null ? { field: "lastUsedCounter", operator: "isNull", value: null } : { field: "lastUsedCounter", operator: "=", value: previousCounter }
320
+ ],
321
+ data: { isEnabled: true, lastUsedCounter: matchedCounter }
322
+ });
323
+ if (!claimed2)
324
+ throw Errors.unauthorized("Two-factor code has already been used");
325
+ if (meta?.rememberDevice) {
326
+ const trusted = await generateRefreshToken();
327
+ const expiresAt = new Date(Date.now() + trustedDeviceDays * 24 * 60 * 60 * 1e3);
328
+ await db.create({
329
+ model: "trusted_device",
330
+ data: { userId, deviceHash: trusted.hash, expiresAt, lastUsedAt: /* @__PURE__ */ new Date() }
331
+ });
332
+ return trusted.raw;
333
+ }
334
+ return void 0;
335
+ }
336
+ const backupCodes = await db.findMany({
337
+ model: "backup_code",
338
+ where: [
339
+ { field: "userId", operator: "=", value: userId },
340
+ { field: "isUsed", operator: "=", value: false }
341
+ ]
342
+ });
343
+ const codeHash = await hashToken(code);
344
+ const matchingCode = backupCodes.find((backupCode) => backupCode.codeHash === codeHash);
345
+ if (!matchingCode)
346
+ throw Errors.unauthorized("Invalid two-factor code");
347
+ const claimed = await db.update({
348
+ model: "backup_code",
349
+ where: [
350
+ { field: "id", operator: "=", value: matchingCode.id },
351
+ { field: "isUsed", operator: "=", value: false }
352
+ ],
353
+ data: { isUsed: true }
354
+ });
355
+ if (!claimed)
356
+ throw Errors.unauthorized("Invalid two-factor code");
357
+ if (!secretRecord.isEnabled) {
358
+ await db.update({
359
+ model: "two_factor_secret",
360
+ where: [{ field: "id", operator: "=", value: secretRecord.id }],
361
+ data: { isEnabled: true }
362
+ });
363
+ }
364
+ if (meta?.rememberDevice) {
365
+ const trusted = await generateRefreshToken();
366
+ const expiresAt = new Date(Date.now() + trustedDeviceDays * 24 * 60 * 60 * 1e3);
367
+ await db.create({
368
+ model: "trusted_device",
369
+ data: { userId, deviceHash: trusted.hash, expiresAt, lastUsedAt: /* @__PURE__ */ new Date() }
370
+ });
371
+ return trusted.raw;
372
+ }
373
+ return void 0;
374
+ }
375
+ return {
376
+ name: "two-factor",
377
+ models: [
378
+ {
379
+ name: "two_factor_secret",
380
+ fields: {
381
+ id: { type: "number", required: true },
382
+ userId: { type: "number", required: true, unique: true, references: { model: "user", field: "id" } },
383
+ secret: { type: "string", required: true },
384
+ isEnabled: { type: "boolean", required: true },
385
+ lastUsedCounter: { type: "number" },
386
+ createdAt: { type: "date", required: true }
387
+ }
388
+ },
389
+ {
390
+ name: "backup_code",
391
+ fields: {
392
+ id: { type: "number", required: true },
393
+ userId: { type: "number", required: true, references: { model: "user", field: "id" } },
394
+ codeHash: { type: "string", required: true },
395
+ isUsed: { type: "boolean", required: true },
396
+ createdAt: { type: "date", required: true }
397
+ }
398
+ },
399
+ {
400
+ name: "trusted_device",
401
+ fields: {
402
+ id: { type: "number", required: true },
403
+ userId: { type: "number", required: true, references: { model: "user", field: "id" } },
404
+ deviceHash: { type: "string", required: true },
405
+ expiresAt: { type: "date", required: true },
406
+ lastUsedAt: { type: "date", required: true },
407
+ createdAt: { type: "date", required: true }
408
+ }
409
+ }
410
+ ],
411
+ hooks: {
412
+ postAuthGate: {
413
+ reason: "two-factor",
414
+ maxAttempts,
415
+ cooldownSeconds: failedAttemptCooldownSeconds,
416
+ async evaluate(ctx) {
417
+ const secret = await ctx.db.findOne({
418
+ model: "two_factor_secret",
419
+ where: [
420
+ { field: "userId", operator: "=", value: ctx.user.id },
421
+ { field: "isEnabled", operator: "=", value: true }
422
+ ]
423
+ });
424
+ if (!secret)
425
+ return;
426
+ if (ctx.meta?.trustedDeviceToken) {
427
+ const deviceHash = await hashToken(ctx.meta.trustedDeviceToken);
428
+ const trusted = await ctx.db.findOne({
429
+ model: "trusted_device",
430
+ where: [
431
+ { field: "userId", operator: "=", value: ctx.user.id },
432
+ { field: "deviceHash", operator: "=", value: deviceHash }
433
+ ]
434
+ });
435
+ if (trusted && trusted.expiresAt > /* @__PURE__ */ new Date()) {
436
+ await ctx.db.update({
437
+ model: "trusted_device",
438
+ where: [{ field: "id", operator: "=", value: trusted.id }],
439
+ data: { lastUsedAt: /* @__PURE__ */ new Date() }
440
+ });
441
+ return;
442
+ }
443
+ }
444
+ return { pluginData: { requires2FA: true } };
445
+ },
446
+ async verify(ctx, completion) {
447
+ if (typeof completion !== "string")
448
+ throw Errors.unauthorized("Invalid two-factor code");
449
+ const trustedDeviceToken = await verifyCode(ctx.db, ctx.user.id, completion, ctx.meta);
450
+ return trustedDeviceToken ? { trustedDeviceToken } : void 0;
451
+ }
452
+ }
453
+ },
454
+ methods: (ctx) => {
455
+ return {
456
+ async enable(userId) {
457
+ const user = await ctx.db.findOne({
458
+ model: "user",
459
+ where: [{ field: "id", operator: "=", value: userId }]
460
+ });
461
+ if (!user)
462
+ throw Errors.notFound("User not found");
463
+ const existing = await ctx.db.findOne({
464
+ model: "two_factor_secret",
465
+ where: [{ field: "userId", operator: "=", value: userId }]
466
+ });
467
+ if (existing?.isEnabled)
468
+ throw Errors.badRequest("Two-factor authentication is already enabled");
469
+ if (existing) {
470
+ await ctx.db.delete({
471
+ model: "two_factor_secret",
472
+ where: [{ field: "userId", operator: "=", value: userId }]
473
+ });
474
+ await ctx.db.delete({
475
+ model: "backup_code",
476
+ where: [{ field: "userId", operator: "=", value: userId }]
477
+ });
478
+ }
479
+ const secret = generateSecret();
480
+ const otpauthUrl = buildOtpauthUrl(secret, issuer, user.email, period, digits);
481
+ const { raw: backupCodesRaw, hashes } = await generateBackupCodes(backupCodeCount);
482
+ const encryptedSecret = await encryptSecret(encryptionKey, secret, userId);
483
+ await ctx.db.create({
484
+ model: "two_factor_secret",
485
+ data: { userId, secret: encryptedSecret, isEnabled: false, lastUsedCounter: null }
486
+ });
487
+ for (const hash of hashes) {
488
+ await ctx.db.create({
489
+ model: "backup_code",
490
+ data: { userId, codeHash: hash, isUsed: false }
491
+ });
492
+ }
493
+ return { secret, otpauthUrl, backupCodes: backupCodesRaw };
494
+ },
495
+ async confirmSetup(userId, code, meta) {
496
+ const trustedDeviceToken = await verifyCode(ctx.db, userId, code, meta);
497
+ return { verified: true, ...trustedDeviceToken ? { trustedDeviceToken } : {} };
498
+ },
499
+ async verify(continuationToken, code, meta) {
500
+ if (!ctx.auth)
501
+ throw Errors.badRequest("Auth service is unavailable");
502
+ return ctx.auth.completePendingAuth(continuationToken, code, meta);
503
+ },
504
+ async disable(userId) {
505
+ await ctx.db.delete({
506
+ model: "two_factor_secret",
507
+ where: [{ field: "userId", operator: "=", value: userId }]
508
+ });
509
+ await ctx.db.delete({
510
+ model: "backup_code",
511
+ where: [{ field: "userId", operator: "=", value: userId }]
512
+ });
513
+ await ctx.db.delete({
514
+ model: "trusted_device",
515
+ where: [{ field: "userId", operator: "=", value: userId }]
516
+ });
517
+ }
518
+ };
519
+ }
520
+ };
521
+ }
522
+ export {
523
+ generateTOTP,
524
+ twoFactor,
525
+ verifyTOTP
526
+ };