@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,680 @@
1
+ /**
2
+ * Append-only audit log plugin for fortress.
3
+ *
4
+ * Captures auth and IAM lifecycle events into the `fortress_audit_log` table
5
+ * with a tamper-evident hash chain (each entry links to the previous entry's
6
+ * hash). Configurable event filtering and a query API for compliance reads.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { WhereClause } from '../../adapters/database/types';
12
+ import type { FortressPlugin } from '../../core/plugin';
13
+
14
+ export interface AuditLogConfig {
15
+ /** Events to capture. Default: all. */
16
+ events?: AuditEventType[];
17
+ /** Enable hash chain for tamper detection. Default: false. */
18
+ hashChain?: boolean;
19
+ }
20
+
21
+ export type AuditEventType
22
+ = | 'LOGIN_SUCCESS'
23
+ | 'LOGIN_FAILURE'
24
+ | 'LOGIN_PENDING'
25
+ | 'MFA_VERIFY_SUCCESS'
26
+ | 'MFA_VERIFY_FAILURE'
27
+ | 'LOGOUT'
28
+ | 'REGISTER'
29
+ | 'TOKEN_REFRESH'
30
+ | 'TOKEN_REUSE'
31
+ | 'ROLE_CREATED'
32
+ | 'ROLE_DELETED'
33
+ | 'ROLE_UPDATED'
34
+ | 'ROLE_BOUND'
35
+ | 'ROLE_UNBOUND'
36
+ | 'ROLE_PERMISSION_ADDED'
37
+ | 'ROLE_PERMISSION_REMOVED'
38
+ | 'PERMISSION_CHANGED'
39
+ | 'PERMISSION_CREATED'
40
+ | 'PERMISSION_DELETED'
41
+ | 'GROUP_CREATED'
42
+ | 'GROUP_UPDATED'
43
+ | 'GROUP_DELETED'
44
+ | 'GROUP_MEMBER_ADDED'
45
+ | 'GROUP_MEMBER_REMOVED'
46
+ | 'SERVICE_ACCOUNT_CREATED'
47
+ | 'SERVICE_ACCOUNT_UPDATED'
48
+ | 'SERVICE_ACCOUNT_DELETED';
49
+
50
+ export interface AuditLogEntry {
51
+ id: string;
52
+ timestamp: Date;
53
+ eventType: AuditEventType;
54
+ actorId: string | null;
55
+ actorType: string;
56
+ targetId: string | null;
57
+ targetType: string | null;
58
+ ipAddress: string | null;
59
+ userAgent: string | null;
60
+ outcome: string;
61
+ metadata: string | null;
62
+ previousHash: string | null;
63
+ createdAt: Date;
64
+ }
65
+
66
+ export interface AuditLogQueryOptions {
67
+ userId?: string;
68
+ eventType?: AuditEventType;
69
+ from?: Date;
70
+ to?: Date;
71
+ limit?: number;
72
+ offset?: number;
73
+ }
74
+
75
+ async function sha256Hex(input: string): Promise<string> {
76
+ const data = new TextEncoder().encode(input);
77
+ const buffer = await crypto.subtle.digest('SHA-256', data);
78
+ return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('');
79
+ }
80
+
81
+ // Synchronous SQLite drivers can deadlock the event loop while waiting on a
82
+ // second write on the same adapter. Queue those transactions per adapter, not
83
+ // process-wide. PostgreSQL relies solely on its transaction-scoped advisory
84
+ // lock, so unrelated databases and PG transactions never share this queue.
85
+ const auditWriteChains = new WeakMap<object, Promise<void>>();
86
+
87
+ function withAuditChainLock<T>(
88
+ db: import('../../adapters/database').DatabaseAdapter,
89
+ fn: (tx: import('../../adapters/database').DatabaseAdapter) => Promise<T>,
90
+ ): Promise<T> {
91
+ const run = (): Promise<T> => db.transaction(async (tx) => {
92
+ if (tx.dialect === 'pg') {
93
+ if (!tx.rawQuery)
94
+ throw new Error('PostgreSQL audit hash chains require rawQuery advisory-lock support');
95
+ await tx.rawQuery('SELECT pg_advisory_xact_lock(117993, 1)');
96
+ }
97
+ return fn(tx);
98
+ });
99
+
100
+ if (db.dialect === 'pg')
101
+ return run();
102
+
103
+ const previous = auditWriteChains.get(db) ?? Promise.resolve();
104
+ const result = previous.then(run, run);
105
+ auditWriteChains.set(db, result.then(() => undefined, () => undefined));
106
+ return result;
107
+ }
108
+
109
+ export interface CustomAuditEvent {
110
+ eventType: string;
111
+ actorId?: string | null;
112
+ actorType?: string;
113
+ targetId?: string | null;
114
+ targetType?: string | null;
115
+ outcome?: string;
116
+ metadata?: Record<string, unknown> | null;
117
+ ipAddress?: string | null;
118
+ userAgent?: string | null;
119
+ }
120
+
121
+ export interface ChainVerificationResult {
122
+ valid: boolean;
123
+ totalEntries: number;
124
+ brokenLinks: { entryId: string; expected: string; actual: string | null }[];
125
+ }
126
+
127
+ /** Serialization formats supported by {@link AuditLogMethods.exportEntries}. */
128
+ export type AuditLogExportFormat = 'json' | 'csv';
129
+
130
+ export interface AuditLogMethods {
131
+ getAuditLog: (options?: AuditLogQueryOptions) => Promise<AuditLogEntry[]>;
132
+ logCustomEvent: (event: CustomAuditEvent) => Promise<void>;
133
+ verifyChain: () => Promise<ChainVerificationResult>;
134
+ /** Transactionally convert an unchained legacy log into a hash chain. */
135
+ rebaselineChain: () => Promise<ChainVerificationResult>;
136
+ exportEntries: (format?: AuditLogExportFormat, options?: AuditLogQueryOptions) => Promise<string>;
137
+ }
138
+
139
+ /** Stable column order for the CSV export. */
140
+ const AUDIT_EXPORT_COLUMNS = [
141
+ 'id',
142
+ 'timestamp',
143
+ 'eventType',
144
+ 'actorId',
145
+ 'actorType',
146
+ 'targetId',
147
+ 'targetType',
148
+ 'ipAddress',
149
+ 'userAgent',
150
+ 'outcome',
151
+ 'metadata',
152
+ 'previousHash',
153
+ 'createdAt',
154
+ ] as const;
155
+
156
+ // RFC 4180: a cell must be quoted when it contains a comma, quote, or newline.
157
+ const CSV_SPECIAL_RE = /[",\n\r]/;
158
+ const CSV_FORMULA_PREFIX_RE = /^[=+\-@\t\r]/;
159
+
160
+ function toCsvCell(value: unknown): string {
161
+ if (value == null)
162
+ return '';
163
+ const raw = value instanceof Date ? value.toISOString() : String(value);
164
+ // Spreadsheet applications execute cells beginning with these characters.
165
+ // A leading apostrophe forces literal display without changing RFC 4180.
166
+ const str = CSV_FORMULA_PREFIX_RE.test(raw) ? `'${raw}` : raw;
167
+ // Quote and escape embedded quotes by doubling them.
168
+ if (CSV_SPECIAL_RE.test(str))
169
+ return `"${str.replace(/"/g, '""')}"`;
170
+ return str;
171
+ }
172
+
173
+ function entriesToCsv(entries: AuditLogEntry[]): string {
174
+ const header = AUDIT_EXPORT_COLUMNS.join(',');
175
+ const rows = entries.map((entry) => {
176
+ const record = entry as unknown as Record<string, unknown>;
177
+ return AUDIT_EXPORT_COLUMNS.map(column => toCsvCell(record[column])).join(',');
178
+ });
179
+ return [header, ...rows].join('\n');
180
+ }
181
+
182
+ async function computeEntryHash(entry: AuditLogEntry): Promise<string> {
183
+ const values = AUDIT_EXPORT_COLUMNS.map((column) => {
184
+ const value = entry[column];
185
+ return value instanceof Date ? value.toISOString() : value;
186
+ });
187
+ return sha256Hex(JSON.stringify(values));
188
+ }
189
+
190
+ interface AuditChainAnchor {
191
+ id: string;
192
+ lastHash: string | null;
193
+ entryCount: number;
194
+ updatedAt: Date;
195
+ }
196
+
197
+ interface ChainState {
198
+ tail: AuditLogEntry | null;
199
+ brokenLinks: ChainVerificationResult['brokenLinks'];
200
+ }
201
+
202
+ async function inspectChain(
203
+ entries: AuditLogEntry[],
204
+ anchor: AuditChainAnchor | null,
205
+ ): Promise<ChainState> {
206
+ if (entries.length === 0) {
207
+ const brokenLinks: ChainVerificationResult['brokenLinks'] = [];
208
+ if (!anchor) {
209
+ brokenLinks.push({
210
+ entryId: 'anchor',
211
+ expected: 'persistent zero-entry audit-chain anchor',
212
+ actual: null,
213
+ });
214
+ }
215
+ else if (anchor.entryCount !== 0 || anchor.lastHash !== null) {
216
+ brokenLinks.push({
217
+ entryId: 'anchor',
218
+ expected: 'entry_count=0 and last_hash=null',
219
+ actual: `${anchor.entryCount}:${anchor.lastHash ?? 'null'}`,
220
+ });
221
+ }
222
+ return { tail: null, brokenLinks };
223
+ }
224
+
225
+ const hashes = new Map<string, AuditLogEntry>();
226
+ const duplicateHashes = new Set<string>();
227
+ for (const entry of entries) {
228
+ const hash = await computeEntryHash(entry);
229
+ if (hashes.has(hash))
230
+ duplicateHashes.add(hash);
231
+ hashes.set(hash, entry);
232
+ }
233
+
234
+ const brokenLinks: ChainVerificationResult['brokenLinks'] = [];
235
+ const roots = entries.filter(entry => entry.previousHash == null);
236
+ for (const entry of entries) {
237
+ if (entry.previousHash != null && !hashes.has(entry.previousHash)) {
238
+ brokenLinks.push({
239
+ entryId: entry.id,
240
+ expected: 'hash of an existing predecessor',
241
+ actual: entry.previousHash,
242
+ });
243
+ }
244
+ }
245
+ for (const hash of duplicateHashes) {
246
+ brokenLinks.push({
247
+ entryId: hashes.get(hash)!.id,
248
+ expected: 'unique entry hash',
249
+ actual: hash,
250
+ });
251
+ }
252
+ if (roots.length !== 1) {
253
+ for (const root of roots.slice(1)) {
254
+ brokenLinks.push({
255
+ entryId: root.id,
256
+ expected: 'exactly one chain root',
257
+ actual: null,
258
+ });
259
+ }
260
+ if (roots.length === 0) {
261
+ brokenLinks.push({
262
+ entryId: entries[0].id,
263
+ expected: 'one entry with previousHash=null',
264
+ actual: entries[0].previousHash,
265
+ });
266
+ }
267
+ }
268
+
269
+ const byPreviousHash = new Map<string, AuditLogEntry[]>();
270
+ for (const entry of entries) {
271
+ if (entry.previousHash == null)
272
+ continue;
273
+ const successors = byPreviousHash.get(entry.previousHash) ?? [];
274
+ successors.push(entry);
275
+ byPreviousHash.set(entry.previousHash, successors);
276
+ }
277
+
278
+ const visited = new Set<AuditLogEntry>();
279
+ let current = roots.length === 1 ? roots[0] : null;
280
+ let tail: AuditLogEntry | null = null;
281
+ while (current && !visited.has(current)) {
282
+ visited.add(current);
283
+ tail = current;
284
+ const hash = await computeEntryHash(current);
285
+ const successors = byPreviousHash.get(hash) ?? [];
286
+ if (successors.length > 1) {
287
+ for (const successor of successors.slice(1)) {
288
+ brokenLinks.push({
289
+ entryId: successor.id,
290
+ expected: 'a predecessor with only one successor',
291
+ actual: successor.previousHash,
292
+ });
293
+ }
294
+ }
295
+ current = successors.length === 1 ? successors[0] : null;
296
+ }
297
+ for (const entry of entries) {
298
+ if (!visited.has(entry)) {
299
+ brokenLinks.push({
300
+ entryId: entry.id,
301
+ expected: 'entry reachable from the chain root',
302
+ actual: entry.previousHash,
303
+ });
304
+ }
305
+ }
306
+
307
+ if (!anchor) {
308
+ brokenLinks.push({
309
+ entryId: tail?.id ?? entries[0].id,
310
+ expected: 'persisted terminal audit-chain anchor',
311
+ actual: null,
312
+ });
313
+ }
314
+ else {
315
+ if (anchor.entryCount !== entries.length) {
316
+ brokenLinks.push({
317
+ entryId: tail?.id ?? entries[0].id,
318
+ expected: `anchor entry count ${anchor.entryCount}`,
319
+ actual: String(entries.length),
320
+ });
321
+ }
322
+ if (tail) {
323
+ const tailHash = await computeEntryHash(tail);
324
+ if (anchor.lastHash !== tailHash) {
325
+ brokenLinks.push({
326
+ entryId: tail.id,
327
+ expected: anchor.lastHash ?? 'non-null terminal hash',
328
+ actual: tailHash,
329
+ });
330
+ }
331
+ }
332
+ }
333
+
334
+ return { tail, brokenLinks };
335
+ }
336
+
337
+ async function queryAuditLog(
338
+ db: import('../../adapters/database').DatabaseAdapter,
339
+ options?: AuditLogQueryOptions,
340
+ ): Promise<AuditLogEntry[]> {
341
+ const where: WhereClause[] = [];
342
+
343
+ if (options?.userId != null)
344
+ where.push({ field: 'actorId', operator: '=', value: options.userId });
345
+ if (options?.eventType)
346
+ where.push({ field: 'eventType', operator: '=', value: options.eventType });
347
+ if (options?.from)
348
+ where.push({ field: 'timestamp', operator: '>=', value: options.from });
349
+ if (options?.to)
350
+ where.push({ field: 'timestamp', operator: '<=', value: options.to });
351
+
352
+ return db.findMany<AuditLogEntry>({
353
+ model: 'audit_log',
354
+ where: where.length > 0 ? where : undefined,
355
+ limit: options?.limit,
356
+ offset: options?.offset,
357
+ sortBy: { field: 'timestamp', direction: 'desc' },
358
+ });
359
+ }
360
+ /**
361
+ * Audit log plugin factory. Returns a {@link FortressPlugin} that records
362
+ * auth and IAM lifecycle events into an append-only table with a
363
+ * tamper-evident hash chain, plus a query API for compliance reads.
364
+ */
365
+ export function auditLog(config: AuditLogConfig = {}): FortressPlugin & { readonly name: 'audit-log' } {
366
+ const allowedEvents = config.events ?? null;
367
+ const hashChain = config.hashChain ?? false;
368
+
369
+ function shouldLog(eventType: AuditEventType): boolean {
370
+ if (!allowedEvents)
371
+ return true;
372
+ return allowedEvents.includes(eventType);
373
+ }
374
+
375
+ async function readChainAnchor(
376
+ db: import('../../adapters/database').DatabaseAdapter,
377
+ ): Promise<AuditChainAnchor | null> {
378
+ return db.findOne<AuditChainAnchor>({
379
+ model: 'audit_chain_state',
380
+ where: [{ field: 'id', operator: '=', value: '1' }],
381
+ });
382
+ }
383
+
384
+ async function writeEntry(
385
+ db: import('../../adapters/database').DatabaseAdapter,
386
+ entry: Omit<AuditLogEntry, 'id' | 'createdAt' | 'previousHash'>,
387
+ ): Promise<void> {
388
+ if (!hashChain) {
389
+ await db.create({ model: 'audit_log', data: { ...entry, previousHash: null } });
390
+ return;
391
+ }
392
+
393
+ await withAuditChainLock(db, async (tx) => {
394
+ const anchor = await readChainAnchor(tx);
395
+ if (
396
+ !anchor
397
+ || !Number.isInteger(anchor.entryCount)
398
+ || anchor.entryCount < 0
399
+ || (anchor.entryCount === 0) !== (anchor.lastHash === null)
400
+ ) {
401
+ throw new Error('Cannot append without a valid audit hash-chain anchor');
402
+ }
403
+
404
+ if (anchor.entryCount === 0) {
405
+ const existing = await tx.findMany<Pick<AuditLogEntry, 'id'>>({
406
+ model: 'audit_log',
407
+ limit: 1,
408
+ });
409
+ if (existing.length > 0) {
410
+ throw new Error(
411
+ 'Cannot append to an unchained legacy audit log; call rebaselineChain() first',
412
+ );
413
+ }
414
+ }
415
+
416
+ const created = await tx.create<AuditLogEntry>({
417
+ model: 'audit_log',
418
+ data: { ...entry, previousHash: anchor.lastHash },
419
+ });
420
+ const lastHash = await computeEntryHash(created);
421
+ const updated = await tx.update({
422
+ model: 'audit_chain_state',
423
+ where: [{ field: 'id', operator: '=', value: anchor.id }],
424
+ data: { lastHash, entryCount: anchor.entryCount + 1, updatedAt: new Date() },
425
+ });
426
+ if (!updated)
427
+ throw new Error('Audit hash-chain anchor disappeared during append');
428
+ });
429
+ }
430
+
431
+ async function writeAuthEntry(
432
+ db: import('../../adapters/database').DatabaseAdapter,
433
+ entry: Omit<AuditLogEntry, 'id' | 'createdAt' | 'previousHash'>,
434
+ logger: import('../../core/observability/logger').FortressLogger | undefined,
435
+ ): Promise<void> {
436
+ try {
437
+ await writeEntry(db, entry);
438
+ }
439
+ catch (error) {
440
+ // Auth state may already be committed when after-hooks run. Audit
441
+ // availability must not turn a successful auth operation into a reported
442
+ // failure; explicit logCustomEvent calls still surface write failures.
443
+ try {
444
+ logger?.error({ plugin: 'audit-log', error }, 'audit log write failed');
445
+ }
446
+ catch {
447
+ // A custom observability sink must never alter an already-completed
448
+ // authentication operation.
449
+ }
450
+ }
451
+ }
452
+
453
+ return {
454
+ name: 'audit-log',
455
+
456
+ models: [{
457
+ name: 'audit_log',
458
+ fields: {
459
+ id: { type: 'string', required: true },
460
+ timestamp: { type: 'date', required: true },
461
+ eventType: { type: 'string', required: true },
462
+ actorId: { type: 'string' },
463
+ actorType: { type: 'string', required: true },
464
+ targetId: { type: 'string' },
465
+ targetType: { type: 'string' },
466
+ ipAddress: { type: 'string' },
467
+ userAgent: { type: 'string' },
468
+ outcome: { type: 'string', required: true },
469
+ metadata: { type: 'string' },
470
+ previousHash: { type: 'string' },
471
+ createdAt: { type: 'date', required: true },
472
+ },
473
+ }, {
474
+ name: 'audit_chain_state',
475
+ fields: {
476
+ id: { type: 'string', required: true },
477
+ lastHash: { type: 'string' },
478
+ entryCount: { type: 'number', required: true },
479
+ updatedAt: { type: 'date', required: true },
480
+ },
481
+ }],
482
+
483
+ hooks: {
484
+ async afterLogin(ctx, result) {
485
+ if (!shouldLog('LOGIN_SUCCESS'))
486
+ return result;
487
+
488
+ await writeAuthEntry(ctx.db, {
489
+ timestamp: new Date(),
490
+ eventType: 'LOGIN_SUCCESS',
491
+ actorId: result.user.id,
492
+ actorType: 'user',
493
+ targetId: null,
494
+ targetType: null,
495
+ ipAddress: ctx.meta?.ipAddress ?? null,
496
+ userAgent: ctx.meta?.userAgent ?? null,
497
+ outcome: 'success',
498
+ metadata: null,
499
+ }, ctx.config.logger);
500
+
501
+ return result;
502
+ },
503
+
504
+ async onLoginFailure(ctx) {
505
+ if (!shouldLog('LOGIN_FAILURE'))
506
+ return;
507
+
508
+ await writeAuthEntry(ctx.db, {
509
+ timestamp: new Date(),
510
+ eventType: 'LOGIN_FAILURE',
511
+ actorId: null,
512
+ actorType: 'anonymous',
513
+ targetId: null,
514
+ targetType: null,
515
+ ipAddress: null,
516
+ userAgent: null,
517
+ outcome: 'failure',
518
+ metadata: JSON.stringify({ identifier: ctx.identifier, error: ctx.error.message }),
519
+ }, ctx.config.logger);
520
+ },
521
+
522
+ async beforeLogout(ctx) {
523
+ if (!shouldLog('LOGOUT'))
524
+ return;
525
+
526
+ await writeAuthEntry(ctx.db, {
527
+ timestamp: new Date(),
528
+ eventType: 'LOGOUT',
529
+ actorId: null,
530
+ actorType: 'user',
531
+ targetId: null,
532
+ targetType: null,
533
+ ipAddress: ctx.meta?.ipAddress ?? null,
534
+ userAgent: ctx.meta?.userAgent ?? null,
535
+ outcome: 'success',
536
+ metadata: null,
537
+ }, ctx.config.logger);
538
+ },
539
+
540
+ async afterRegister(ctx, user) {
541
+ if (!shouldLog('REGISTER'))
542
+ return;
543
+
544
+ await writeAuthEntry(ctx.db, {
545
+ timestamp: new Date(),
546
+ eventType: 'REGISTER',
547
+ actorId: user.id,
548
+ actorType: 'user',
549
+ targetId: user.id,
550
+ targetType: 'user',
551
+ ipAddress: ctx.meta?.ipAddress ?? null,
552
+ userAgent: ctx.meta?.userAgent ?? null,
553
+ outcome: 'success',
554
+ metadata: null,
555
+ }, ctx.config.logger);
556
+ },
557
+
558
+ async afterTokenRefresh(ctx, result) {
559
+ if (!shouldLog('TOKEN_REFRESH'))
560
+ return result;
561
+
562
+ await writeAuthEntry(ctx.db, {
563
+ timestamp: new Date(),
564
+ eventType: 'TOKEN_REFRESH',
565
+ actorId: null,
566
+ actorType: 'user',
567
+ targetId: null,
568
+ targetType: null,
569
+ ipAddress: ctx.meta?.ipAddress ?? null,
570
+ userAgent: ctx.meta?.userAgent ?? null,
571
+ outcome: 'success',
572
+ metadata: null,
573
+ }, ctx.config.logger);
574
+
575
+ return result;
576
+ },
577
+ },
578
+
579
+ methods: ctx => ({
580
+ async getAuditLog(options?: AuditLogQueryOptions): Promise<AuditLogEntry[]> {
581
+ return queryAuditLog(ctx.db, options);
582
+ },
583
+
584
+ async logCustomEvent(event: CustomAuditEvent): Promise<void> {
585
+ const eventType = event.eventType as AuditEventType;
586
+ if (allowedEvents && !allowedEvents.includes(eventType))
587
+ return;
588
+
589
+ await writeEntry(ctx.db, {
590
+ timestamp: new Date(),
591
+ eventType,
592
+ actorId: event.actorId ?? null,
593
+ actorType: event.actorType ?? 'system',
594
+ targetId: event.targetId ?? null,
595
+ targetType: event.targetType ?? null,
596
+ ipAddress: event.ipAddress ?? null,
597
+ userAgent: event.userAgent ?? null,
598
+ outcome: event.outcome ?? 'success',
599
+ metadata: event.metadata ? JSON.stringify(event.metadata) : null,
600
+ });
601
+ },
602
+
603
+ async rebaselineChain(): Promise<ChainVerificationResult> {
604
+ if (!hashChain)
605
+ throw new Error('Enable hashChain before rebaselining the audit log');
606
+
607
+ return withAuditChainLock(ctx.db, async (tx) => {
608
+ const anchor = await readChainAnchor(tx);
609
+ if (!anchor)
610
+ throw new Error('Cannot rebaseline without the persistent audit hash-chain anchor');
611
+ if (anchor.entryCount !== 0 || anchor.lastHash !== null) {
612
+ throw new Error('Cannot rebaseline an audit hash chain that has already been initialized');
613
+ }
614
+
615
+ const entries = await tx.findMany<AuditLogEntry>({ model: 'audit_log' });
616
+ if (entries.some(entry => entry.previousHash !== null)) {
617
+ throw new Error('Cannot rebaseline a partially or previously chained audit log');
618
+ }
619
+ entries.sort((left, right) => {
620
+ const timeDifference = left.createdAt.getTime() - right.createdAt.getTime();
621
+ if (timeDifference !== 0)
622
+ return timeDifference;
623
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
624
+ });
625
+
626
+ let previousHash: string | null = null;
627
+ const rebaselined: AuditLogEntry[] = [];
628
+ for (const entry of entries) {
629
+ const updated = await tx.update<AuditLogEntry>({
630
+ model: 'audit_log',
631
+ where: [{ field: 'id', operator: '=', value: entry.id }],
632
+ data: { previousHash },
633
+ });
634
+ if (!updated)
635
+ throw new Error(`Audit entry ${entry.id} disappeared during rebaseline`);
636
+ rebaselined.push(updated);
637
+ previousHash = await computeEntryHash(updated);
638
+ }
639
+
640
+ const updatedAt = new Date();
641
+ const updatedAnchor = await tx.update<AuditChainAnchor>({
642
+ model: 'audit_chain_state',
643
+ where: [{ field: 'id', operator: '=', value: anchor.id }],
644
+ data: { lastHash: previousHash, entryCount: entries.length, updatedAt },
645
+ });
646
+ if (!updatedAnchor)
647
+ throw new Error('Audit hash-chain anchor disappeared during rebaseline');
648
+
649
+ const { brokenLinks } = await inspectChain(rebaselined, updatedAnchor);
650
+ if (brokenLinks.length > 0)
651
+ throw new Error('Rebaselined audit hash chain failed verification');
652
+ return { valid: true, totalEntries: entries.length, brokenLinks: [] };
653
+ });
654
+ },
655
+
656
+ async exportEntries(
657
+ format: AuditLogExportFormat = 'json',
658
+ options?: AuditLogQueryOptions,
659
+ ): Promise<string> {
660
+ const entries = await queryAuditLog(ctx.db, options);
661
+ if (format === 'csv')
662
+ return entriesToCsv(entries);
663
+ return JSON.stringify(entries, null, 2);
664
+ },
665
+
666
+ async verifyChain(): Promise<ChainVerificationResult> {
667
+ return withAuditChainLock(ctx.db, async (tx) => {
668
+ const entries = await tx.findMany<AuditLogEntry>({ model: 'audit_log' });
669
+ const anchor = await readChainAnchor(tx);
670
+ const { brokenLinks } = await inspectChain(entries, anchor);
671
+ return {
672
+ valid: brokenLinks.length === 0,
673
+ totalEntries: entries.length,
674
+ brokenLinks,
675
+ };
676
+ });
677
+ },
678
+ }),
679
+ };
680
+ }