@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,394 @@
1
+ # Observability
2
+
3
+ Fortress has three layered observability surfaces. All three are zero-default — Fortress never writes to stderr, never imports `@opentelemetry/api`, and never allocates metric objects unless you explicitly opt in.
4
+
5
+ ## 1. Pluggable Logger (`config.logger`)
6
+
7
+ The `FortressLogger` interface is structurally compatible with `pino`'s `BaseLogger` and Fastify's `FastifyBaseLogger`, so a `pino()` instance or `fastify.log` drops in directly with zero adapter code.
8
+
9
+ ```typescript
10
+ export interface FortressLogger {
11
+ level: LogLevel | 'silent' | string;
12
+ fatal: LogFn;
13
+ error: LogFn;
14
+ warn: LogFn;
15
+ info: LogFn;
16
+ debug: LogFn;
17
+ trace: LogFn;
18
+ silent: LogFn;
19
+ child?: (bindings: Record<string, unknown>) => FortressLogger;
20
+ isLevelEnabled?: (level: LogLevel) => boolean;
21
+ }
22
+ ```
23
+
24
+ Where `LogFn` accepts any of three shapes:
25
+ ```typescript
26
+ logger.info('a string');
27
+ logger.info({ key: 'value' }, 'string with meta');
28
+ logger.info('format %s', 'arg');
29
+ ```
30
+
31
+ ### Default: `SILENT_LOGGER`
32
+
33
+ If you don't pass `config.logger`, Fortress uses a no-op logger where every method is a shared arrow function. Zero allocation per call.
34
+
35
+ ### Pino example
36
+
37
+ ```typescript
38
+ import pino from 'pino';
39
+ import { createFortress } from '@bajustone/fortress';
40
+
41
+ const fortress = createFortress({
42
+ jwt: { key: process.env.JWT_SECRET! },
43
+ database: db,
44
+ logger: pino({ level: 'info' }),
45
+ });
46
+ ```
47
+
48
+ ### Fastify example
49
+
50
+ ```typescript
51
+ import Fastify from 'fastify';
52
+ import { createFortress } from '@bajustone/fortress';
53
+
54
+ const app = Fastify({ logger: { level: 'info' } });
55
+ const fortress = createFortress({
56
+ jwt: { key: process.env.JWT_SECRET! },
57
+ database: db,
58
+ logger: app.log,
59
+ });
60
+ ```
61
+
62
+ ### Console wrapper
63
+
64
+ ```typescript
65
+ const consoleLogger = {
66
+ level: 'info',
67
+ fatal: (...a) => console.error('[fatal]', ...a),
68
+ error: (...a) => console.error('[error]', ...a),
69
+ warn: (...a) => console.warn('[warn]', ...a),
70
+ info: (...a) => console.info('[info]', ...a),
71
+ debug: (...a) => console.debug('[debug]', ...a),
72
+ trace: (...a) => console.debug('[trace]', ...a),
73
+ silent: () => {},
74
+ };
75
+
76
+ const fortress = createFortress({
77
+ jwt: { key: process.env.JWT_SECRET! },
78
+ database: db,
79
+ logger: consoleLogger,
80
+ });
81
+ ```
82
+
83
+ ### What Fortress logs
84
+
85
+ - **Plugin token-claim overwrite** (dev only, `warn` level) — when one plugin's `enrichTokenClaims` overwrites a claim set by an earlier plugin.
86
+ - **Refresh token fingerprint mismatch** (`warn` level) — when `config.jwt.validateRefreshFingerprint === 'warn'` detects a UA change on refresh without rejecting.
87
+ - **Unhandled errors in `fortress.handleRequest`** (`error` level) — anything that reaches the outer catch that isn't a `FortressError`.
88
+ - **Unhandled errors in the Express error handler** (`error` level) — same, for the Express adapter.
89
+ - **Observer failures** (`error` level) — when an auth/IAM/permission-check listener throws or rejects.
90
+
91
+ Fortress does **not** call `logger.debug` or `logger.info` on hot paths like `checkPermission`. Hot-path observability goes through metrics and the sync `PermissionCheckEvent` observer, both of which are bounded and allocation-free when nobody subscribes. If you want per-check logs, attach a permission-check observer and call your logger explicitly — you'll pay the cost you opt into.
92
+
93
+ ## 2. Observer Lists
94
+
95
+ Three independent listener lists on the `Fortress` instance. Each `add…Observer` call returns an `() => void` unsubscribe function. Listener exceptions are routed to `logger.error` and the remaining listeners continue.
96
+
97
+ ### `auth.addAuthObserver`
98
+
99
+ Async, cold-path. Fires on every auth lifecycle event.
100
+
101
+ ```typescript
102
+ import type { AuthEvent } from '@bajustone/fortress';
103
+
104
+ const off = fortress.auth.addAuthObserver(async (event: AuthEvent) => {
105
+ switch (event.eventType) {
106
+ case 'LOGIN_SUCCESS':
107
+ // actor, identifier, ipAddress, userAgent available
108
+ break;
109
+ case 'LOGIN_FAILURE':
110
+ // identifier + error.code + error.message available
111
+ break;
112
+ case 'TOKEN_REUSE_DETECTED':
113
+ // metadata.tokenFamily available; send security alert
114
+ break;
115
+ // LOGOUT | REGISTER | TOKEN_REFRESH | TOKEN_FINGERPRINT_MISMATCH
116
+ }
117
+ });
118
+
119
+ // Later...
120
+ off(); // unsubscribe
121
+ ```
122
+
123
+ ### `iam.addIamObserver`
124
+
125
+ Async, cold-path. Fires on IAM mutation events (`ROLE_CREATED`, `ROLE_BOUND`, `PERMISSION_CHANGED`, `GROUP_MEMBER_ADDED`, etc.). Used by the audit-log plugin and the api-key plugin for cascade deletes.
126
+
127
+ ```typescript
128
+ fortress.iam.addIamObserver(async (event) => {
129
+ if (event.eventType === 'ROLE_BOUND') {
130
+ await notifySlack(`Role ${event.targetId} bound to subject ${event.metadata?.subjectId}`);
131
+ }
132
+ });
133
+ ```
134
+
135
+ ### `iam.addPermissionCheckObserver`
136
+
137
+ **Synchronous, hot-path.** Fires on every `checkPermission` call with latency and cache-hit information.
138
+
139
+ ```typescript
140
+ fortress.iam.addPermissionCheckObserver((event) => {
141
+ // This runs on the hot path — keep it bounded.
142
+ if (!event.allowed) {
143
+ metrics.increment('auth.denies', { resource: event.resource });
144
+ }
145
+ });
146
+ ```
147
+
148
+ **Why synchronous?** The listener signature returns `void`, not `Promise<void>`, to make it impossible to accidentally await expensive work inline. If you need async work, fire-and-forget: `void asyncWork()`.
149
+
150
+ **Why separate from `addIamObserver`?** Permission checks fire thousands of times per minute in a busy app. Mixing them with mutation events would spam the audit-log plugin.
151
+
152
+ ### Error-routing gotcha (async observers only)
153
+
154
+ The auth and IAM observer lists route listener failures to `logger.error` so one bad listener can't break the caller. But the safety net only engages when the listener **returns** the promise. Consider the two shapes:
155
+
156
+ ```typescript
157
+ // ✅ Routed — the listener returns the promise, the list attaches .catch
158
+ fortress.auth.addAuthObserver(async (event) => {
159
+ await siem.log(event); // a rejection here ends up at logger.error
160
+ });
161
+
162
+ // ✅ Routed — equivalent; the async function returns an implicit promise
163
+ fortress.auth.addAuthObserver(event => siem.log(event));
164
+
165
+ // ❌ Not routed — the listener returns `undefined`, the list has no handle
166
+ // on the inflight promise, and a rejection escapes to the runtime's
167
+ // unhandled-rejection handler.
168
+ fortress.auth.addAuthObserver((event) => {
169
+ void siem.log(event);
170
+ });
171
+ ```
172
+
173
+ `void asyncWork()` is an explicit opt-out of the safety net. It's sometimes what you want — you accept the risk because you don't want the listener to wait on a cross-network call — but if you want rejections surfaced in your logger, return the promise. Fortress does not install a process-level `unhandledRejection` handler on your behalf.
174
+
175
+ The permission-check observer is synchronous (returns `void`, not `Promise<void>`), so this gotcha doesn't apply to it — there's no async work to lose track of.
176
+
177
+ ## 3. OpenTelemetry Adapter (opt-in)
178
+
179
+ The `@bajustone/fortress/otel` sub-path ships a single factory: `createOtelTelemetry()`. It's the **only file in the repository that imports `@opentelemetry/api`**, and the import is dynamic (`await import('@opentelemetry/api')`). Runtimes that don't import the `/otel` sub-path never resolve the peer dep.
180
+
181
+ ### Install
182
+
183
+ ```bash
184
+ bun add @opentelemetry/api
185
+ # Plus the SDK you want (this example uses Node SDK with OTLP):
186
+ bun add @opentelemetry/sdk-node \
187
+ @opentelemetry/exporter-trace-otlp-http \
188
+ @opentelemetry/exporter-metrics-otlp-http
189
+ ```
190
+
191
+ `@opentelemetry/api` is declared as an **optional peer dependency** on the Fortress `package.json`, so it's not installed automatically — you explicitly add it when you opt in.
192
+
193
+ ### Wire it up
194
+
195
+ ```typescript
196
+ import { createFortress } from '@bajustone/fortress';
197
+ import { createOtelTelemetry } from '@bajustone/fortress/otel';
198
+ import { NodeSDK } from '@opentelemetry/sdk-node';
199
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
200
+ import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
201
+
202
+ // 1. Start an OTel SDK (registers global Tracer/Meter providers).
203
+ const sdk = new NodeSDK({
204
+ metricReader: new PeriodicExportingMetricReader({
205
+ exporter: new OTLPMetricExporter({ url: process.env.OTEL_ENDPOINT! }),
206
+ }),
207
+ });
208
+ sdk.start();
209
+
210
+ // 2. Wire the Fortress adapter.
211
+ const observability = await createOtelTelemetry({ name: 'my-app-auth' });
212
+ const fortress = createFortress({
213
+ jwt: { key: process.env.JWT_SECRET! },
214
+ database: db,
215
+ observability,
216
+ });
217
+ ```
218
+
219
+ ### Metric catalog
220
+
221
+ | Metric | Type | Unit | Attributes |
222
+ |---|---|---|---|
223
+ | `fortress.auth.events.total` | counter | — | `event`, `outcome`, `method` |
224
+ | `fortress.iam.events.total` | counter | — | `event` |
225
+ | `fortress.iam.permission_check.duration` | histogram | s | `subject_type`, `result`, `cached` |
226
+ | `fortress.iam.permission_check.cache.hits` | counter | — | — |
227
+ | `fortress.iam.permission_check.cache.misses` | counter | — | — |
228
+ | `db.client.operation.duration` | histogram | s | `db.system.name`, `db.operation.name`, `db.collection.name` |
229
+
230
+ **Histogram bucket boundaries:**
231
+
232
+ - `fortress.iam.permission_check.duration`: `[0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1]`
233
+ - `db.client.operation.duration`: `[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10]` (OTel advisory default for DB clients)
234
+
235
+ ### Span catalog
236
+
237
+ - `fortress.iam.permission_check.deny` — fired only on denied permission checks. Attributes: `subject.type`, `subject.id`, `resource`, `action`, `cached`. Allowed checks are metric-only — no span — to keep the hot path cheap.
238
+
239
+ ### Cardinality rules (important)
240
+
241
+ Fortress **never** places user IDs, emails, tenant IDs, session IDs, or raw resource IDs on metric attributes. Those would cause cardinality explosions — every monitoring vendor charges you for high-cardinality metrics, and Prometheus will eat itself.
242
+
243
+ Allowed metric attributes: `result`, `method`, `outcome`, `event`, `subject_type`, `cached`, and the three standard `db.*` semconv attributes. Everything else goes on spans (pivotable per trace) or logs (grep-able).
244
+
245
+ ### What Fortress does **not** emit
246
+
247
+ - **`http.server.request.duration`** — that's the host framework's job via `@opentelemetry/instrumentation-http`, `@opentelemetry/instrumentation-hono`, `@opentelemetry/instrumentation-express`, etc. Emitting it here would double-count.
248
+ - **Structured logs via `@opentelemetry/api-logs`** — that package is explicitly not for library authors and is alpha. Fortress uses its own `FortressLogger` contract, which is pino-compatible.
249
+
250
+ ## Provider contract (advanced)
251
+
252
+ If you want to plug a non-OTel metric backend (custom Prometheus client, DataDog StatsD client, etc.), you can implement the `TelemetryProvider` interface directly instead of using the `/otel` adapter:
253
+
254
+ ```typescript
255
+ import type { TelemetryProvider } from '@bajustone/fortress';
256
+
257
+ const customTelemetry: TelemetryProvider = {
258
+ tracer: {
259
+ startSpan: (name, attrs) => makeSpan(name, attrs),
260
+ startActiveSpan: async (name, attrs, callback) => {
261
+ // Your backend must keep this span active across the async callback so
262
+ // nested database/IAM spans inherit it.
263
+ return myBackend.withActiveSpan(makeSpan(name, attrs), callback);
264
+ },
265
+ },
266
+ meter: {
267
+ createCounter: (name, opts) => ({ add: (v, a) => myBackend.counter(name).add(v, a) }),
268
+ createHistogram: (name, opts) => ({ record: (v, a) => myBackend.histogram(name).observe(v, a) }),
269
+ },
270
+ };
271
+
272
+ createFortress({ /* ... */ observability: customTelemetry });
273
+ ```
274
+
275
+ Optional `startActiveSpan` is part of the frozen provider contract and is used at the request boundary for async parent propagation. Existing providers that implement only `startSpan` remain valid (Fortress falls back to it, without parent propagation). This is the escape hatch—most users should stick with `createOtelTelemetry()` because the OpenTelemetry SDK ecosystem already has exporters for every major backend.
276
+
277
+ ## 4. Event catalog
278
+
279
+ Fortress emits structured events through three observer surfaces:
280
+
281
+ - `fortress.auth.addAuthObserver(listener)` — user-lifecycle and token events.
282
+ - `fortress.iam.addIamObserver(listener)` — IAM mutation events.
283
+ - `fortress.iam.addPermissionCheckObserver(listener)` — every allow/deny check.
284
+
285
+ The `audit-log` plugin subscribes to `addIamObserver` automatically so IAM events flow into the audit-log table without extra wiring. Wire your own `addAuthObserver` if you want auth events in the same log.
286
+
287
+ ### Auth events (`AuthEvent.eventType`)
288
+
289
+ | Event | Outcome | Security weight | Recommended alert |
290
+ |---|---|---|---|
291
+ | `LOGIN_SUCCESS` | `success` | low | none |
292
+ | `LOGIN_FAILURE` | `invalid_credentials` \| `account_locked` \| `account_inactive` \| `2fa_required` \| ... | medium | spike alert (>N per window per IP) |
293
+ | `REGISTER` | `success` | low | spike alert (>N per window per IP) |
294
+ | `LOGOUT` | `success` | low | none |
295
+ | `TOKEN_REFRESH` | `success` \| `invalid` \| `expired` | low | none |
296
+ | `TOKEN_REUSE_DETECTED` | `success` (revocation succeeded) | **high** | page on-call — indicates stolen refresh token |
297
+ | `TOKEN_FINGERPRINT_MISMATCH` | `success` (revocation succeeded) | **high** | page on-call — indicates session hijack |
298
+ | `IMPERSONATE` | `success` | medium | log to immutable trail |
299
+
300
+ ### IAM events (`IamEvent.eventType`)
301
+
302
+ | Event | Where | Recommended alert |
303
+ |---|---|---|
304
+ | `ROLE_CREATED` / `ROLE_DELETED` / `ROLE_UPDATED` | `fortress.iam.createRole / deleteRole / updateRole` | log only |
305
+ | `ROLE_PERMISSION_ADDED` / `ROLE_PERMISSION_REMOVED` | role-permission diff | log only |
306
+ | `ROLE_BOUND` / `ROLE_UNBOUND` | role-binding mutations | log only |
307
+ | `PERMISSION_CREATED` / `PERMISSION_DELETED` | permission catalog mutations | log only |
308
+ | `PERMISSION_CHANGED` | direct-permission binding mutations | log only |
309
+ | `GROUP_CREATED` / `GROUP_UPDATED` / `GROUP_DELETED` | group lifecycle mutations | log only |
310
+ | `GROUP_MEMBER_ADDED` / `GROUP_MEMBER_REMOVED` | group membership mutations | log only |
311
+ | `SERVICE_ACCOUNT_CREATED` / `SERVICE_ACCOUNT_UPDATED` / `SERVICE_ACCOUNT_DELETED` | service-account mutations | log only |
312
+
313
+ All IAM mutation events carry `targetType`, `targetId`, and a `metadata` object suitable for audit-log persistence.
314
+
315
+ ### Permission-check events (`PermissionCheckEvent`)
316
+
317
+ | Field | Description |
318
+ |---|---|
319
+ | `subjectType` | `'USER'` \| `'GROUP'` \| `'SERVICE_ACCOUNT'` |
320
+ | `subjectId` | string id (kept off metric attributes by Fortress — stays on the in-process event) |
321
+ | `resource`, `action` | what was checked |
322
+ | `allowed` | final decision |
323
+ | `cached` | whether the result was served from the permission cache |
324
+ | `durationSeconds` | check latency |
325
+
326
+ The handler is **synchronous and hot-path**. Keep it bounded — update a counter, push to an in-memory channel, never await a network call.
327
+
328
+ ## 5. Recommended dashboards and alerts
329
+
330
+ ### Auth dashboard
331
+
332
+ - **Login success rate** = `rate(fortress.auth.events.total{event="LOGIN_SUCCESS"}[5m]) / rate(fortress.auth.events.total{event=~"LOGIN_.*"}[5m])`.
333
+ - **Failure breakdown** by `outcome` attribute. A sudden spike in `invalid_credentials` from one IP range is credential-stuffing.
334
+ - **Refresh churn** = `rate(fortress.auth.events.total{event="TOKEN_REFRESH",outcome="success"}[5m])`. Higher than expected (multiple per minute per user) indicates a misbehaving client.
335
+ - **Token reuse** = `increase(fortress.auth.events.total{event="TOKEN_REUSE_DETECTED"}[1h])`. Any non-zero value is a paging incident — a refresh token was used after rotation.
336
+ - **Impersonation** = `increase(fortress.auth.events.total{event="IMPERSONATE"}[1d])`. Audit weekly.
337
+
338
+ ### IAM dashboard
339
+
340
+ - **Permission check rate**, separated by `result` (`allow` vs `deny`). A deny ratio over ~5% sustained indicates either a misconfig or active probing.
341
+ - **Cache hit ratio** = `rate(fortress.iam.permission_check.cache.hits[5m]) / (rate(fortress.iam.permission_check.cache.hits[5m]) + rate(fortress.iam.permission_check.cache.misses[5m]))`. Healthy services see >90% after warm-up; a drop indicates churn (frequent role changes, cache invalidations).
342
+ - **p99 check latency** = `histogram_quantile(0.99, sum by (le)(rate(fortress.iam.permission_check.duration_bucket[5m])))`. Target <10ms with cache, <100ms cache-miss.
343
+
344
+ ### OAuth dashboard (if mounted)
345
+
346
+ - **Token-endpoint error rate** by `error` label from the response body shape; spike alert on `invalid_client`, `invalid_grant`.
347
+ - **PKCE rejections** = audit-log `OAUTH_PKCE_REQUIRED` rows (if you mirror them into a counter via an `addAuthObserver`).
348
+ - **Refresh-token family revocations** = `TOKEN_REUSE_DETECTED` events scoped to OAuth refresh tokens.
349
+
350
+ ### Database dashboard
351
+
352
+ - **p99 query latency** = `histogram_quantile(0.99, sum by (le, db_operation_name)(rate(db_client_operation_duration_bucket[5m])))`. Triage slow queries by `db.operation.name` attribute.
353
+ - **Operation rate** by `db.collection.name`. Useful when adding indexes or tuning the IAM cache.
354
+
355
+ ### Suggested alerts (production)
356
+
357
+ | Alert | Expression / rule | Severity |
358
+ |---|---|---|
359
+ | Token-reuse detected | any `TOKEN_REUSE_DETECTED` event in 5m | **page** |
360
+ | Token-fingerprint mismatch | any `TOKEN_FINGERPRINT_MISMATCH` event in 5m | **page** |
361
+ | Login failure surge (per IP) | >100 failures from one IP in 10m | warn |
362
+ | Permission-check deny ratio | `deny / total > 0.20` over 15m | warn |
363
+ | Permission-check p99 latency | `p99 > 100ms` over 15m | warn |
364
+ | Audit-log chain broken | `verifyChain().ok === false` from a scheduled job | **page** |
365
+ | OAuth `invalid_client` surge | `>20/m` over 10m | warn |
366
+ | Schema-version drift | `fortress migrate:check` non-zero exit in deploy preflight | **page** |
367
+
368
+ All thresholds are starting points — tune to your traffic shape after a baseline week.
369
+
370
+ ## 6. Audit-log integration
371
+
372
+ The `audit-log` plugin persists every `IamEvent` to `fortress_audit_log` automatically and can be configured to hash-chain entries (`auditLog({ hashChain: true })`) so deletions are detectable. Recommended pattern:
373
+
374
+ ```ts
375
+ import { auditLog } from '@bajustone/fortress/plugins/audit-log';
376
+
377
+ createFortress({
378
+ // ...
379
+ plugins: [auditLog({ hashChain: true })],
380
+ });
381
+
382
+ // Mirror auth events into the same log:
383
+ fortress.auth.addAuthObserver((event) => {
384
+ fortress.plugins['audit-log'].logCustomEvent(event).catch((err) => {
385
+ fortress.logger.error({ err, event }, 'failed to persist auth event');
386
+ });
387
+ });
388
+
389
+ // Schedule a verifier (e.g. nightly cron):
390
+ const result = await fortress.plugins['audit-log'].verifyChain();
391
+ if (!result.ok) {
392
+ // Page on-call — someone tampered with the audit table.
393
+ }
394
+ ```
@@ -0,0 +1,131 @@
1
+ # Account Lockout Plugin
2
+
3
+ ## Overview
4
+
5
+ The `account-lockout` plugin adds progressive account lockout to Fortress. After a configurable number of failed login attempts, the account is temporarily locked. With escalation enabled, each successive lockout doubles the duration up to a configurable cap.
6
+
7
+ Lockout sequence with defaults:
8
+ - 1st lockout: 15 minutes
9
+ - 2nd lockout: 30 minutes
10
+ - 3rd lockout: 60 minutes (capped at `maxLockoutSeconds`)
11
+
12
+ On successful login the failed attempt counter resets to zero.
13
+
14
+ ## Installation
15
+
16
+ Import the `accountLockout` factory and pass it in the `plugins` array when creating a Fortress instance:
17
+
18
+ ```ts
19
+ import { createFortress } from '@bajustone/fortress';
20
+ import { accountLockout } from '@bajustone/fortress/plugins/account-lockout';
21
+
22
+ const fortress = createFortress({
23
+ jwt: { key: 'your-secret-at-least-32-bytes!!' },
24
+ database: adapter,
25
+ plugins: [
26
+ accountLockout({
27
+ maxFailedAttempts: 5,
28
+ lockoutDurationSeconds: 900,
29
+ escalation: true,
30
+ maxLockoutSeconds: 3600,
31
+ }),
32
+ ],
33
+ });
34
+ ```
35
+
36
+ Once registered, methods are available at `fortress.plugins['account-lockout']` with full type safety.
37
+
38
+ ## Configuration
39
+
40
+ All fields on `AccountLockoutConfig` are optional:
41
+
42
+ | Option | Type | Default | Description |
43
+ |---|---|---|---|
44
+ | `maxFailedAttempts` | `number` | `5` | Number of consecutive failed login attempts before the account is locked. |
45
+ | `lockoutDurationSeconds` | `number` | `900` (15 min) | Duration of the first lockout in seconds. |
46
+ | `escalation` | `boolean` | `true` | When `true`, each successive lockout doubles the duration (exponential backoff). |
47
+ | `maxLockoutSeconds` | `number` | `3600` (1 hour) | Maximum lockout duration in seconds. Escalation never exceeds this cap. |
48
+
49
+ ## How It Works
50
+
51
+ The plugin uses three lifecycle hooks -- no manual calls are needed for the lockout logic itself:
52
+
53
+ 1. **`beforeLogin`** -- Checks if the identifier (email) is currently locked. If `lockedUntil` is in the future, a `FortressError` with code `UNAUTHORIZED` and message "Account temporarily locked. Try again later." is thrown.
54
+ 2. **`onLoginFailure`** -- Increments the failed attempt counter. When the counter reaches `maxFailedAttempts`, the account is locked for the calculated duration and `lockoutCount` is incremented.
55
+ 3. **`afterLogin`** -- Resets the failed attempt counter and clears the lock on successful login.
56
+
57
+ ## API Reference
58
+
59
+ | Method | Signature | Returns |
60
+ |---|---|---|
61
+ | `getLockoutStatus` | `(identifier: string)` | `Promise<LockoutStatus>` |
62
+ | `resetLockout` | `(identifier: string)` | `Promise<void>` |
63
+
64
+ ### getLockoutStatus
65
+
66
+ Returns the current lockout status for an identifier (typically the user's email):
67
+
68
+ ```ts
69
+ const status = await fortress.plugins['account-lockout'].getLockoutStatus('alice@example.com');
70
+
71
+ console.log(status.isLocked); // boolean
72
+ console.log(status.failedAttempts); // number
73
+ console.log(status.lockoutCount); // number
74
+ console.log(status.lockedUntil); // Date | null
75
+ console.log(status.lastFailedAt); // Date | null
76
+ ```
77
+
78
+ The `LockoutStatus` type:
79
+
80
+ ```ts
81
+ interface LockoutStatus {
82
+ identifier: string;
83
+ failedAttempts: number;
84
+ lockoutCount: number;
85
+ lockedUntil: Date | null;
86
+ lastFailedAt: Date | null;
87
+ isLocked: boolean;
88
+ }
89
+ ```
90
+
91
+ If no lockout record exists for the identifier, all counters are `0` and `isLocked` is `false`.
92
+
93
+ ### resetLockout
94
+
95
+ Manually resets all lockout state for an identifier. Useful for admin-initiated unlocks:
96
+
97
+ ```ts
98
+ await fortress.plugins['account-lockout'].resetLockout('alice@example.com');
99
+ ```
100
+
101
+ This clears `failedAttempts`, `lockedUntil`, `lockoutCount`, and `lastFailedAt`.
102
+
103
+ ## Example
104
+
105
+ ```ts
106
+ import { createFortress } from '@bajustone/fortress';
107
+ import { accountLockout } from '@bajustone/fortress/plugins/account-lockout';
108
+
109
+ const fortress = createFortress({
110
+ jwt: { key: 'your-secret-at-least-32-bytes!!' },
111
+ database: adapter,
112
+ plugins: [
113
+ accountLockout({
114
+ maxFailedAttempts: 3,
115
+ lockoutDurationSeconds: 60,
116
+ escalation: true,
117
+ maxLockoutSeconds: 600,
118
+ }),
119
+ ],
120
+ });
121
+
122
+ // After 3 failed logins, the account is locked for 60 seconds.
123
+ // If the user fails again after unlock, the next lockout is 120 seconds.
124
+ // The maximum lockout is capped at 600 seconds (10 minutes).
125
+
126
+ // Admin can check status or manually unlock:
127
+ const status = await fortress.plugins['account-lockout'].getLockoutStatus('alice@example.com');
128
+ if (status.isLocked) {
129
+ await fortress.plugins['account-lockout'].resetLockout('alice@example.com');
130
+ }
131
+ ```