@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,1011 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [1.0.0-rc.1] - 2026-07-14
6
+
7
+ ### Added
8
+ - **Release verification is now a hard publish prerequisite.** Tag publishing waits for lint, source and example typechecks, the full unit/OpenAPI-drift suite, PostgreSQL/Testcontainers integration tests, build/export parity, and a pure-Node-ESM `dist/testing` smoke test. PostgreSQL integration now runs on every CI push as well as pull requests and nightly schedules. The same checks are available locally through `bun run check:release`.
9
+ - **`@bajustone/fetcher` is now a runtime dependency, re-exported at `@bajustone/fortress/fetcher`.** The zero-dependency, Standard-Schema-V1-native fetch client + schema builder that fortress uses internally is surfaced so consumers can author endpoint schemas with the same toolkit and build their own validated outbound clients without a separate install. The subpath re-exports fetcher's root (`createFetch`, middleware, errors, types) flat, plus `schema`, `openapi`, and `specTools` namespaces.
10
+ - **Richer schema-builder DSL.** `str()`, `num()`, and `int()` now accept an options object as well as a bare description: `str({ min, max, pattern, format })`, `int({ min, max })` — the constraints are **enforced at runtime**. New builders: `literal(value)` (`const`), `intersect(...schemas)` (`allOf`), `strict(objSchema)` (`additionalProperties: false`, to reject over-posting/mass-assignment), and `discriminatedUnion(propertyName, ...variants)` (`oneOf` + `discriminator`).
11
+ - **Enforced string formats.** `email()`, `uuid()`, `url()`, `datetime()`, `date()`, and `time()` emit both a `format` annotation (for OpenAPI) and a ReDoS-safe `pattern` lifted from `@bajustone/fetcher`, so the value is now validated at runtime — previously `strFormat('email')` was annotation-only and never checked.
12
+ - **Author endpoint schemas with fetcher's builder.** `endpoint().body()/.query()/.params()/.response()` (and `vBody`/`vParam`/`vQuery`) accept schemas from `@bajustone/fortress/fetcher`'s `schema` namespace directly — fetcher's `object`/`optional`/`discriminatedUnion`/`transform`/`refined`/`brand`/`tuple`/… . They validate at runtime via fetcher's engine and serialize to clean OpenAPI (internal `~`-prefixed keys are stripped by the spec builder), unlocking combinators fortress's own DSL doesn't expose.
13
+ - **Outbound HTTP hardened via `@bajustone/fetcher`.** OAuth token exchange, OIDC discovery, provider userinfo, the GitHub profile fetch, and the HIBP breach check now route through a shared fetcher client (`src/core/http/outbound.ts`) that adds a request **timeout** — native `fetch` has none, so a hung upstream previously blocked login/registration indefinitely — and replaces unchecked `as` casts with schema-validated parsing (`access_token` presence on token exchange; JSON object/array shape on discovery/userinfo/GitHub). The HIBP check keeps its **fail-open**, plaintext, k-anonymity-prefix-only contract (6 s timeout, body read as text, never schema-parsed). No public API change.
14
+ - **Webhook plugin v1 — custom events + Bring-Your-Own-Queue.** Full rewrite of `@bajustone/fortress/plugins/webhook`:
15
+ - **Custom events**: declare your own events (`{ name, schema?, description? }`) alongside the built-ins via `events`, and `emit(name, payload, { idempotencyKey? })` them through the same path. `emit()` validates the payload against the event's Standard Schema and a `maxPayloadBytes` cap, throwing a tagged `WebhookEmitError` (`unknown_event` / `invalid_payload` / `payload_too_large`).
16
+ - **Bring-Your-Own-Queue**: delivery is always queued behind a `WebhookQueue` interface. Bundled `inMemoryQueue()` (default, dev-only — loses scheduled retries on restart) and crash-safe `databaseQueue({ pollMs })`; the `webhook_delivery` table is the transactional outbox.
17
+ - **Delivery over `@bajustone/fetcher`** with an SSRF-safe transport that pins the connection to the resolved IP (now also blocking `64:ff9b::/96` NAT64 forms). Failure classification (404/410/421 → deactivate; other 4xx → fail-fast; 408/425/429/5xx/network → retry), jittered backoff, a per-endpoint **circuit breaker** (`maxConsecutiveFailures`), and `onDeliveryFailed`/`onEndpointDeactivated` DLQ hooks.
18
+ - **Secrets** are CSPRNG-generated (returned once at `registerEndpoint`/`rotateSecret`) and **redacted** from `listEndpoints`/`updateEndpoint`. New `updateEndpoint`, `rotateSecret`, `listEventTypes`, `stop` methods. Stable per-delivery `webhook-id` (`msg_<deliveryId>`) for receiver dedup.
19
+ - **Observability types and trace parenting are now public and complete.** The package root and `/otel` export `FortressLogger`, `TelemetryProvider` and supporting tracer/meter/span types, Auth/IAM event and listener types, permission-check observer types, and `Unsubscribe`. Optional `Tracer.startActiveSpan` is part of the provider contract; `fortress.handleRequest` uses it across the async pipeline so nested database/IAM spans inherit the request span, while existing providers with only `startSpan` retain their prior runtime behavior. An SDK-backed regression verifies the parent span ID.
20
+ - **IAM mutation events now cover the full admin lifecycle.** `createPermission`/`deletePermission`, `updateGroup`, and `deleteGroup` emit `PERMISSION_CREATED`, `PERMISSION_DELETED`, `GROUP_UPDATED`, and `GROUP_DELETED`; audit-log filtering accepts those names plus the previously emitted role-update/role-permission events. Permission creation emits exactly once only for the concurrent caller that inserts the row. Group deletion transactionally removes the group identity and all memberships/GROUP role/direct-permission bindings under one serialized mutation lock, so non-cascading adapters cannot leave authorization orphans.
21
+ - **OpenAPI: Zod-free schema converters + a spec-drift CI gate.** `@bajustone/fortress/hono` now exports `identitySchemaConverter`, `fetcherSchemaConverter`, and `toJSONSchemaConverter` so you can `mountFortressOpenAPI` / `convertRoutes` using fortress's or fetcher's builder **without installing Zod** (existing Zod callers unaffected). A new gate (`src/core/openapi-drift.test.ts`) runs `@bajustone/fetcher/spec-tools`' `lintSpec` over the emitted spec and fails on any unenforced request-schema keyword beyond the intentional `format`/`additionalProperties`. The `/auth/register` body's `email` field now uses the **enforced** `email()` builder (ReDoS-safe pattern) instead of annotation-only `strFormat('email')`, so malformed emails are rejected at registration.
22
+
23
+ ### Fixed
24
+ - **Refresh fingerprints are now keyed and network-bound.** The optional consistency check uses a domain-separated HMAC over User-Agent plus observed source IP instead of a reversible bare User-Agent SHA-256. Existing fingerprinted sessions must authenticate again after upgrading; hard mode should be enabled only where source IP stability is acceptable.
25
+ - **OAuth signing keys now have an operational rotation lifecycle.** `rotateSigningKey()` atomically retires the active RS256 key, creates a replacement, publishes retired keys for `signingKeyGraceSeconds`, and prunes expired keys. JWKS filtering prevents stale verification keys from remaining public indefinitely.
26
+ - **PostgreSQL append-only identifiers are bigint-safe.** Migration `0010_bigint_append_only_ids` widens token, audit, webhook-delivery, and WebAuthn-challenge identifiers and backing sequences before int4 exhaustion; fresh schemas use `BIGSERIAL`. A real two-process SQLite regression now exercises the database migration lock rather than relying on same-process serialization.
27
+ - **TOTP seeds are encrypted at rest.** `twoFactor()` now requires a 32-byte application-held `secretEncryptionKey` and stores only AES-256-GCM envelopes bound to the user id. Migration `0009_encrypt_totp_secrets` securely invalidates legacy plaintext enrolments and their recovery/trusted-device state, requiring affected users to re-enrol.
28
+ - **Outbound deadlines cover the complete response body.** OAuth/OIDC, social-login, GitHub, and HIBP requests remain abortable after headers, preventing slow-body peers from stalling authentication or password writes indefinitely.
29
+ - **OIDC refresh no longer fabricates authentication recency.** Refresh-issued id_tokens omit `auth_time` because the original event was not persisted, rather than resetting it to the refresh time and bypassing RP `max_age` policies.
30
+ - **SvelteKit helpers now accept Fortress instances with strongly typed plugin maps.** Adapter signatures previously instantiated the default `Fortress` generic, rejecting instances whose inferred plugin methods lacked a string index signature; the accepted instance type is now generic-map agnostic while preserving the real SvelteKit `Handle`/`Action` return contracts.
31
+ - **The `openapi` and `schemas --format zod` CLI commands no longer crash.** Endpoint definition maps are now converted with `Object.values(...)`; a subprocess smoke suite invokes every documented command and verifies controlled failures do not surface runtime exceptions.
32
+ - **Audit hash chains now protect every stored field, retain a terminal anchor, and cannot fork under concurrent writes.** The audit plugin hashes an unambiguous serialization of all 13 entry fields, including `previousHash`, and migration v7 adds a permanent zero-entry sentinel/terminal hash-count anchor so deletion or mutation of the tail, the full chain, or the anchor is detectable. It traverses links without assuming numerically ordered string IDs, updates the entry and anchor in one serialized transaction (with a PostgreSQL advisory lock), and refuses to append to an invalid chain. Audit `id`, `actorId`, and `targetId` model metadata now match their public string types. CSV exports neutralize spreadsheet-formula prefixes in addition to RFC 4180 escaping.
33
+ - **Migrations are serialized, atomic, and checksummed.** SQLite migrators acquire a database writer lock (plus a same-process queue for synchronous drivers); PostgreSQL uses a transaction-scoped advisory lock. Both re-read the schema checkpoint only after locking and apply the selected run transactionally. `migrateDown(target > current)` is now a true no-op and never stamps a fictitious future version. `fortress_migration_journal` records a SHA-256 checksum for every applied migration—including data-step identity—backfills legacy checkpoints once, rejects missing/edited/unexpected rows before any DDL, removes rows on rollback, and is cleaned up at version zero.
34
+ - **Email identity is lowercase, NFC-normalized, and case-insensitively unique.** Registration, admin updates, email login identifiers, mixed-case login lookup, and social-login linking/JIT provisioning now use one canonical email representation. Migration `0006_canonical_email` normalizes legacy rows before adding SQLite `NOCASE` / PostgreSQL `lower(...)` unique indexes for users and email identifiers. If legacy accounts collapse to the same identity, the oldest account remains authoritative; later accounts are disabled, moved to deterministic non-routable tombstones, and have all refresh tokens revoked rather than being silently merged. This data cleanup is intentionally irreversible when rolling the index migration down. Because Unicode NFC normalization requires the runtime data step, SQL-only `migrate:up` export now refuses to emit an incomplete migration; use `fortress.migrate()` with the configured adapter.
35
+ - **Database hot paths are indexed and PostgreSQL timestamps are timezone-safe.** Migration `0005_hot_indexes_timestamptz` adds ten cross-dialect indexes for refresh families/users, verification and magic-link tokens, IAM subjects, backup/trusted-device users, webhook retries, and audit chronology. PostgreSQL upgrades explicitly interpret historical timestamp-without-time-zone values as UTC before converting all Fortress timestamp columns to `TIMESTAMPTZ`; rollback uses the inverse explicit UTC conversion. Migration drift now reports missing required indexes.
36
+ - **Component `$ref` request schemas are enforced at runtime.** Refs created by `defineComponents()` or typed `ref(name, schema)` now carry their real component definitions into the fetcher-backed validator, including nested, transitive, and recursive refs. Independent component registries remain isolated even when they reuse the same component name. A bare `ref(name)` remains permissive when no definition is available; when composed with a bound ref of that same OpenAPI component name, both resolve to the shared component definition, matching OpenAPI's global component semantics.
37
+ - **Non-transactional SQLite writes can no longer be lost to a concurrent transaction's rollback.** The Drizzle adapter's single-connection SQLite path now serializes every standalone op (`create`/`findOne`/`findMany`/`update`/`delete`/`count`/`rawQuery`) on the same async chain that orders `transaction()` calls, so a plain write issued while another request's transaction is mid-`BEGIN…COMMIT` can no longer interleave into that open transaction and be swept away by its `ROLLBACK`. Ops issued inside a transaction callback still run directly on the open transaction (no self-deadlock), and PostgreSQL (native async transactions) is unaffected.
38
+ - **IAM evaluation and cache behavior now fail closed.** Missing condition fields/references never satisfy any operator (including `neq`), authoritative subject identity overwrites caller context, and `explainPermission` delegates its verdict to the configured evaluator with condition context. Permission-cache generations reject stale async writes after per-subject or global invalidation.
39
+ - **Rate-limit account keys and long windows are stable.** Login account identifiers are trimmed, NFC-normalized, and lowercased before rate-limit keying, matching lockout behavior and preventing case/Unicode bypass. The memory store retains each key for its configured window instead of dropping all counters after one hour.
40
+ - **URL coercion and middleware paths are canonicalized safely.** Query/path numerics accept only decimal notation—empty, hex, exponent, signed-plus, and non-finite forms remain strings for validation to reject. Route and plugin-middleware matching now share slash canonicalization, so double/trailing slashes cannot reach an endpoint while bypassing path-scoped controls.
41
+ - **OAuth signing-key lookup now uses SQL `IS NULL`.** Repeated OIDC id-token issuance reuses the active RS256 key instead of generating a new keypair on every lookup under SQL adapters.
42
+ - **Microsoft social login now fails closed on an absent `email_verified` claim (security).** The Microsoft provider previously mapped a missing `email_verified` to `true`, so a Microsoft login could auto-link by email to an existing account with no verified-email signal — an account-takeover vector, since Graph `/me` and most Entra id_tokens omit the claim. It now matches the other providers: absent ⇒ unverified ⇒ no by-email auto-link (the account is JIT-provisioned instead). Tenants that trust their directory can opt back in via a custom `mapProfile`.
43
+ - **All documented subpaths now resolve for npm consumers.** `package.json` `"exports"` covered only 9 of the 29 subpaths declared in `jsr.json`, so `@bajustone/fortress/express`, `@bajustone/fortress/drizzle/pg`, and every `@bajustone/fortress/plugins/*` returned a bare module-not-found on npm even though they resolved on JSR. Added the 20 missing `exports` entries (and the matching `tsup` build entries for `drizzle/pg` + all `plugins/*`) so the build emits each artifact, plus a `check:exports` guard (run in CI and before publish) that fails if `jsr.json`, `package.json`, and `tsup.config.ts` ever drift again.
44
+
45
+ ### Changed (breaking)
46
+ - **The adapter conformance suite now enforces the full frozen database contract.** It runtime-checks string IDs, every core comparison/null operator, ascending and descending sorting, update return values, multi-row update/delete behavior, boolean round-trips, unknown-operator rejection, portable raw-query placeholders, and typed duplicate conflicts on both SQLite and PostgreSQL. Node consumers of `@bajustone/fortress/testing` must install the newly declared optional peer `better-sqlite3`; the built testing artifact now loads through pure Node ESM.
47
+ - **Auth and post-auth verification now share one frozen result contract.** `AuthResponse*`/wire names are replaced by `AuthResult`, `AuthSuccess`, `AuthPending`, and `AuthImpersonation`; a pending result has a required `{ reason, continuationToken }` challenge and no token properties, while success includes `method`. Two-factor completion is `verify(continuationToken, code, meta?)`, setup activation is `confirmSetup(userId, code)`, and magic-link completion is `verify(token, meta?)`. Core HTTP now exposes `POST /auth/2fa/verify` and `POST /auth/magic-link/verify`, both returning `AuthResult` and setting cookies only on success.
48
+ - **Password and cookie defaults are now freeze-ready.** New passwords default to a 15-character minimum (existing hashes and logins are unaffected). HIBP checks now use a bounded LRU cache (`breachedCacheMaxEntries`, default 1000) and support `breachedFailureMode: 'open' | 'closed'` (default `'open'`); every upstream outage logs and emits `PASSWORD_BREACH_CHECK_DEGRADED`. `isPasswordBreached` now accepts an options object as its second argument. Cookie configuration now fails at startup when `SameSite=None` is combined with `Secure: false` or caller-supplied `__Host-`/`__Secure-` names violate their required attributes.
49
+ - **Webhook plugin API replaced and frozen (v1).** Event names are now dot-cased (`auth.login.success`, `auth.user.registered`, … — was the `LOGIN_SUCCESS`/`REGISTER` enum). `registerEndpoint(url, events, secret)` → `registerEndpoint(url, events, opts?)` (secret is generated when omitted). The `deliver` config option is replaced by `delivery.fetch` (a fetcher `FetchFn`). `processRetries()` is removed — retries are the queue's job now. New `webhook_endpoint` columns (`deactivated_reason`, `consecutive_failures`) and `webhook_delivery` columns (`idempotency_key`, `response_body`, `error_kind`) plus a unique index on `webhook_delivery (endpoint_id, idempotency_key)`. The frozen breaker defaults are `maxConsecutiveFailures=15`, `permanentStatuses=[404,410,421]`, and `timeoutMs=10000`; exported `WebhookErrorKind` is `'http' | 'network'` and `WebhookDeactivatedReason` is `permanent_${number} | too_many_failures`. The plugin had not shipped a stable release, so there is no migration path. See [docs/plugins/webhook.md](docs/plugins/webhook.md).
50
+ - **SvelteKit adapter contracts now match the real peer runtime.** Public handle/action types are anchored to `@sveltejs/kit` and compile under strict mode against `Handle`, `Action`, and `RequestHandler`. `redirectTo` now throws SvelteKit's recognized `Redirect` and failures use real `ActionFailure`s instead of raw lookalikes. Action success is discriminated as `{ pending: false }` or `{ pending: true, challenge }`. Silent SSR refreshes are single-flighted per refresh token and forward IP/user-agent `RequestMeta`, preventing concurrent family revocation and hard fingerprint mismatches.
51
+ - **Express host-route middleware is complete and OAuth form bodies are preserved.** `@bajustone/fortress/express` now exports `createCsrfMiddleware` and the factory returns `csrfMiddleware`, with configurable custom header, safe methods, segment-safe skip paths, and cross-site rejection. Express requests parsed by `express.urlencoded()` are re-encoded as form data—not JSON—before OAuth token/introspection/revocation dispatch. The standalone Hono CSRF middleware now also matches skips at segment boundaries (`/foo` covers `/foo/bar`, never `/foobar`; trailing `/*` is supported).
52
+ - **Plugin middleware now receives one cross-adapter request contract.** Core, Hono, and Express all pass the exported `PluginRequestContext` (`{ request: Request, fortressSubject?, fortressUserId?, fortressClaims?, fortressScopes? }`) to `MiddlewareDefinition.handler`. Framework-native Hono `Context` / Express `Request` objects are no longer passed directly. This fixes path-bound plugin middleware silently failing under adapters—notably generic rate-limit `paths` bindings, which require the web-standard `request` field.
53
+ - **Route-map RBAC can now fail closed.** The Express and Hono `createRbacMiddleware` still default to treating an unmapped user route as public, but you can now opt into fail-closed enforcement: `unmappedRoutes: 'deny'` (Express) / `defaultDeny: true` (Hono) refuses any non-skipped route without a `routeMap`/`mapRequest` entry with a 403, so a forgotten mapping can't silently expose a route. Genuinely public routes go in `skipPaths`. Express matches `routeMap` against `originalUrl`, so mounting middleware under `app.use('/api', ...)` no longer strips the prefix and silently misses full-path mappings. Fortress-managed routes are unaffected—they are protected inside `fortress.handleRequest`.
54
+ - **Policy/resource loaders now validate frozen shapes and destructive intent.** `fortress.resources.json` is canonically a map keyed by resource name; legacy arrays and malformed actions/descriptions are rejected, and CLI `init`/`sync:types` use that same map. `loadPolicy` validates all collection/item primitives, effects, and duplicate names. `diffPolicy(..., { prune: true })` refuses an empty policy unless `allowEmptyPrune: true` explicitly acknowledges deleting all managed IAM state.
55
+ - **Policy apply/diff now converges through authoritative IAM state.** `applyPolicyPlan` applies resource operations directly without `node:fs`; `IamService.pushResources` exposes the in-memory path. Service-account role diffs use `listRoleBindingsForSubject` rows instead of permission heuristics, covering shared permissions and zero-permission roles. Role descriptions can clear to `null`, and prune ordering unbinds retained service accounts before deleting roles. The legacy `applyResourceOps` path argument remains accepted but no file is written. `loadPolicy` and `resolvePolicyPath` are now async so their `node:fs`/`node:path` dependencies can be dynamically loaded only when file helpers run; callers must `await` them.
56
+ - **Tenancy responses and default switching now match their frozen contract.** `getMyTenants` and `GET /tenancy/tenants/mine` return `{ tenants: [...] }` as declared by OpenAPI (rather than a bare array). `switchTenant` serializes per user and performs a two-phase clear/set inside one transaction; migration `0004_tenant_default_unique` deterministically repairs duplicate historical defaults and adds a partial unique index enforcing at most one default membership per user.
57
+ - **Core host dispatch and collision behavior are now fail-fast.** Top-level `routes` are explicitly metadata-only (`manifest.mounted: false`), so framework adapters fall through to host handlers and direct `handleRequest()` returns 404 instead of fabricating `{ok:true}`. Duplicate plugin method/path declarations and duplicate `fortress.call` keys now fail at startup; intentional plugin-over-core overrides remain supported.
58
+ - **Core logout and permission-only routes have consistent wire behavior.** Successful `POST /auth/logout` expires both configured auth cookies. An endpoint with `meta.permission` now attempts normal JWT authentication even when it omits redundant `security:['bearer']`, so valid callers reach RBAC instead of receiving a credential-dependent 401.
59
+ - **DatabaseAdapter contract hardened for freeze.** `DrizzleDialect` is now `'sqlite' | 'pg'` — `mysql` is dropped (it was only smoke-tested and had no constraint-error mapping or CI lane; it can be re-added once there's a real consumer). `update`, `delete`, and `findOne` now **throw** on an empty `where` clause instead of silently matching every row (a full-table wipe footgun); unfiltered `findMany`/`count` reads are still allowed. The Drizzle adapter now maps **SQLite** constraint errors to typed `FortressError`s the same way it already did for Postgres (`UNIQUE`/`PRIMARYKEY` → `CONFLICT/409`, `FOREIGN KEY` → `UNPROCESSABLE_ENTITY/422`, `NOT NULL` → `BAD_REQUEST/400`), and the exported helper `rethrowPgError` is renamed `rethrowDbError(err, dialect)`. The `rawQuery` contract now uses portable `?` positional placeholders on every dialect (the adapter translates to driver syntax), and the conformance suite asserts placeholder behavior, empty-`where` rejection, and unique-violation mapping.
60
+
61
+ ### Changed (breaking)
62
+ - **Validation migration window and dependency are frozen for the next release.** Consumers must migrate issue-path readers from `[{key}]` to bare property keys. Fortress explicitly re-exports its reviewed fetcher API and accepts compatible `@bajustone/fetcher` 1.x security patches via `^1.0.0`; no dual-shape compatibility shim is provided during the 0.x breaking-change window.
63
+ - **Request/response validation now runs on `@bajustone/fetcher`'s `fromJSONSchema`.** Fortress's hand-rolled `validateJsonSchema` (`src/core/json-schema-validator.ts`) is deleted; `FortressSchema` objects keep `vendor: 'fortress'` and stay plain JSON Schema objects, but their `~standard.validate()` delegates to fetcher's compiled validator. Consequences:
64
+ - The `422 VALIDATION_ERROR` body's issue `path` segments are now fetcher's bare `PropertyKey`s (e.g. `["email"]`) instead of fortress's `{ key }` objects (e.g. `[{ key: "email" }]`). The `location`/`message`/`code` envelope is unchanged.
65
+ - `oneOf` enforces JSON Schema's exactly-one semantics; overlapping branches are rejected. `anyOf` remains the at-least-one combinator.
66
+ - More keywords are now actually enforced (`minimum`/`maximum`, `const`, `allOf`, `additionalProperties: false`, `discriminator`) — schemas that emitted these previously passed them through unchecked.
67
+ - `$ref` fields in request bodies resolve against the component definitions and are enforced transitively at runtime.
68
+ - **Node `>=20.19.0` is now required** (matches `@bajustone/fetcher`'s engine floor), declared in `package.json#engines`.
69
+
70
+ ### Changed (breaking)
71
+ - **Subject identifiers are strings everywhere.** `FortressUser.id`, `TokenClaims.sub`, `Session.userId`, every IAM `id`/`*Id` field (`Role`, `Group`, `Permission`, `RoleBinding`, `LoginIdentifier`, `ServiceAccount`, `Subject`), every plugin `id` field (`api-key.id`, `webhook.id`, `webauthn.userId`, audit `actorId`/`targetId`, etc.), and every HTTP endpoint path-param `:id` are now typed `string` at the fortress API surface. Closes the lock-in identified in the 2026-06-13 audit: `TokenClaims.sub: number` plus the `Number()`/`String()` round-trip in `verifyAccessToken` made string/UUID/ULID-keyed adapters structurally impossible. This was the only lock-in blocking UUID adapters that fortress couldn't lift later without a second breaking change.
72
+
73
+ **Why string and not `string | number`.** RFC 7519 §4.1.2 already mandates `sub` is a string on the wire; the only reason the round-trip existed was that the internal type disagreed with the wire type. A union pushed the coercion hazard to every consumer. Making the internal type match the wire type deletes the round-trip and the bug class in one cut. (See the rejected-options note under the [lock-in audit memory](memory/project_lockin_audit.md).)
74
+
75
+ **What didn't change.** The Drizzle adapter still uses `bigserial`/`integer` PKs by default — the new `stringifyIds` helper at the adapter boundary transparently translates rows on read. No migrations are required for existing numeric-keyed installations. JWTs signed by v0.2.x verify unchanged because `String(1)` already produced `"1"` on the wire.
76
+
77
+ **What you have to change.** Every place your code held a fortress id as `number` is now `string`. The mechanical migration:
78
+ - `FortressUser['id']`, `TokenClaims['sub']`, `Subject['id']`, all role/group/permission/session/identifier `id` fields: `number → string`.
79
+ - HTTP path params: `Number(params.id) → params.id`. Zod schemas: `z.coerce.number() → z.string().min(1)`.
80
+ - Custom adapters returning `id: number`: stringify on read at the adapter boundary (`row.id.toString()`), parse on write if the column is numeric.
81
+ - Comparisons: `userId === 1 → userId === '1'`. Set/Map type params: `Set<number> → Set<string>`.
82
+ - Schema builder: new `id(description?)` helper exports a string-typed id schema. Path-param `:id` declarations should use it instead of `int()`.
83
+
84
+ **Files touched in this commit (for grep):** `src/core/types.ts`, `src/core/internal-adapter.ts`, `src/core/auth/jwt.ts` (round-trip removed), `src/core/auth/auth-service.ts`, `src/core/iam/iam-service.ts`, `src/core/iam/explain.ts`, `src/core/iam/permission-sync.ts`, `src/core/http/dispatch.ts`, `src/core/http/handle-request.ts`, `src/core/http/protect.ts`, `src/core/policy/apply.ts`, `src/core/policy/diff.ts`, `src/core/schema-builder.ts` (new `id()` helper), `src/drizzle/adapter.ts` (new `stringifyIds` boundary helper), every plugin (`admin`, `api-key`, `audit-log`, `email-verification`, `magic-link`, `oauth`, `rate-limit`, `social-login`, `tenancy`, `two-factor`, `webauthn`, `webhook`), and every framework binding (`hono`, `express`, `sveltekit`).
85
+
86
+ ### Fixed (security)
87
+ - **Phase 1 security hotfixes.** Hardened social login with verified OIDC ID tokens (JWKS signature plus issuer/audience/expiry/nonce, both checked unconditionally), timing-safe OAuth state validation, verified-email-only account linking, active-user guards, transactional provisioning/linking, unique social-account constraints, and opt-in AES-256-GCM provider-token storage. **`persistTokens` now defaults to `false`** (was `true` in the initial draft of this patch) so existing `socialLogin()` configs keep working without a key; opting in to server-side provider-token access requires `persistTokens: true` *and* a 32-byte `tokenEncryptionKey`, otherwise the plugin throws at construction. `getAuthorizationUrl()` now returns `{ url, state, codeVerifier, nonce }`; `handleCallback()` now requires returned/stored state and stored nonce; `ProviderProfile` includes `emailVerified`; `getProviderTokens()` decrypts stored provider tokens.
88
+ - **Webhook SSRF guard scoped to the built-in transport.** `defaultDeliver` resolves the host and pins the outbound `https.request` to that exact IP (closing the DNS-rebind TOCTOU) and rejects loopback / RFC1918 / link-local / multicast targets including IPv4-mapped IPv6 forms like `::ffff:169.254.169.254`. Custom `config.deliver` transports are no longer wrapped in a redundant `dns.lookup` — they are responsible for their own outbound safety, which un-breaks offline/CI consumers using injected transports. The guard is exported as `assertSafeWebhookUrl(url)` for custom transports that want to reuse it.
89
+ - **Two-factor PG migration parity.** The bundled `migrations/pg/0002_initial_schema.sql` adds `fortress_two_factor_secret.last_used_counter INTEGER` and the missing `UNIQUE (provider, provider_account_id)` on `fortress_social_account`, matching the SQLite migration and the in-process `migrations.ts` definitions. Without this, fresh PG databases installed from the bundled SQL files broke 2FA verification outright (not just replay-vulnerable) and silently lacked the social-account uniqueness constraint.
90
+ - **Admin bootstrap is no longer ambient.** Removed `adminUserIds`; `/iam/admin/bootstrap` is opt-in via `admin({ bootstrap: { enabled, secret } })`, requires the one-time secret, and only succeeds while no `fortress-admin` bindings exist.
91
+ - **Fortress OpenAPI Hono auto-mounts now delegate to `fortress.handleRequest`.** Auto-mounted IAM/admin routes get the same principal resolution, CSRF, validation, and RBAC enforcement as all other Fortress-managed routes.
92
+ - **Session, 2FA, webhook, lockout, data-isolation, and tenancy hardening.** Password changes revoke active refresh tokens; TOTP counters are single-use; webhooks are out-of-band with timeout and SSRF protections; lockout identifiers are normalized and window expiry self-recovers; scoped creates force resolved scope and scoped-field updates are rejected; tenant-less permission checks match only tenant-less bindings.
93
+
94
+ - **Login timing-oracle defense is now actually a defense.** The "user not found / no password" branch of `auth.login()` previously called `hasher.verify()` with a hard-coded, malformed Argon2 PHC string (`$argon2id$...$dummy`). `hash-wasm`'s parser threw before running the KDF, so the branch completed in ~0.3ms while a real password verify took ~50–200ms — a ~300× timing gap usable for user enumeration over the network. The dummy is now a lazily computed, well-formed Argon2id hash produced by the configured `PasswordHasher`, so the not-found branch performs a *real* verify at the same cost as a hit. Regression test in `src/core/auth/login-timing.test.ts`.
95
+
96
+ ### Changed (breaking)
97
+ - **`FortressConfig.jwt.secret` renamed to `FortressConfig.jwt.key`.** The field now accepts the new exported `JwtKeyMaterial` type (currently `string | string[]`, intentionally narrow so the runtime helpers don't lie about supported inputs). The alias exists so the public signature stays stable when fortress expands to asymmetric algorithms (RS256 / EdDSA) and JWKS-backed verification — the doc comment on `JwtKeyMaterial` in `core/auth/jwt.ts` shows the planned widening (`CryptoKey | KeyObject | JWK | Uint8Array | …` plus a `JwtVerifyKeyMaterial` variant for `JWTVerifyGetKey`). `signAccessToken` / `verifyAccessToken` parameter renamed `secret` → `key` to match. Codemod: `jwt: { secret: ... }` → `jwt: { key: ... }`.
98
+
99
+ ## [0.2.8] - 2026-06-10
100
+
101
+ ### Fixed
102
+ - fix(release): keep jsr.json version in sync with package.json
103
+
104
+ ## [0.2.7] - 2026-06-10
105
+
106
+ ### Fixed
107
+ - fix
108
+
109
+ ## [0.2.6] - 2026-06-10
110
+
111
+ ### Fixed
112
+ - fix
113
+
114
+ ## [0.2.5] - 2026-06-10
115
+
116
+ ### Changed
117
+ - **`ctx.body` is now non-optional when a body schema is declared.** `protect()` / `protectedRoute()` only invoke the handler after the declared body passes Standard Schema validation, so `ProtectedRouteContext['body']` narrows to `T` (not `T | undefined`) for endpoints with a `.body(...)` schema — use `ctx.body` directly without `!` or `ctx.input`. Endpoints with no declared body schema keep the loose `unknown`. Types-only change; runtime is identical.
118
+
119
+ ## [0.2.4] - 2026-06-09
120
+
121
+ ### Fixed
122
+ - Stabilized the SvelteKit auto-refresh test by using a deliberately expired access token instead of a 1-second token plus sleep. Slow CI runners could expire the freshly refreshed 1-second access token before locals were populated.
123
+
124
+ ## [0.2.3] - 2026-06-09
125
+
126
+ ### Added
127
+ - **Programmatic OpenAPI emission** — new standalone `toOpenAPI(endpoints, options)` helper plus `fortress.toOpenAPI(options)`. The standalone helper is env/DB-free for build scripts and client codegen; the instance method defaults to the endpoint definitions Fortress knows about: core auth/IAM routes, plugin routes, and top-level host `routes`. Supports `title`, `version`, `description`, `servers`, top-level `tags`, custom `schemas`, explicit endpoint override, and `operationId` strategy. Defaults `operationId` to endpoint `handler` names for host-app codegen use cases. This replaces hand-rolled builders like `reb-edit/apps/api/src/openapi.ts`. (Wishlist #2.)
128
+ - **OpenAPI builder hardening.** Schema objects are deep-cleaned before embedding in OpenAPI: `~standard` validators, functions, and `undefined` values are stripped recursively so Fortress schemas and external Standard Schema adapters serialize cleanly. The OpenAPI plugin now also sees top-level `createFortress({ routes })` host endpoints, not just plugin routes and `additionalEndpoints`.
129
+ - **`fortress.migrate()`** — single entrypoint that runs Fortress migrations and an optional app-supplied callback in the correct order. Eliminates the two-step `migrate(db, '...') + migrateUp(adapter)` footgun where forgetting the second call silently leaves auth tables unmigrated. Accepts `{ migrateApp?: () => Promise<void>, dialect?, targetVersion? }`; returns `{ fortress: MigrationApplyResult, appRan: boolean }`. (Wishlist #8.)
130
+ - **`fortress.syncPermissionsFromManifest()`** — walks `fortress.endpoints`, deduplicates `(resource, action)` pairs from `meta.permission`, and seeds them via `iam.createPermission`. Optional `defaultRoles: { admin: '*', member: ['school:read', ...] }` binds the discovered permissions to named roles (`'*'` = bind every permission discovered from the supplied endpoint manifest, not unrelated DB rows). Idempotent: re-running only grants, never revokes. `roles[role].bound` reports newly-added bindings for this run, not already-existing bindings. Replaces the seed script every consumer used to write. (Wishlist #7.)
131
+ - **`ctx.respond(status, body)`** on `ProtectedRouteContext` — typed JSON-response builder for non-2xx returns. When `protect()` is given a typed `EndpointDefinition`, `status` narrows to the response codes the endpoint declares and `body` narrows to the matching response schema. String-target endpoints fall back to `(number, unknown)`. Replaces hand-rolled `new Response(JSON.stringify(...), { status })` calls. (Wishlist #4.)
132
+ - **Top-level `routes` on `FortressConfig`.** Host applications can register their own `EndpointDefinition` map directly on `createFortress({ routes: ... })` instead of authoring a one-field `FortressPlugin`. Fortress synthesizes the plugin internally under the reserved name `__host`; declaring a user plugin named `__host` alongside `routes` is now a configuration error. (Wishlist #1.)
133
+ - **`ErrorEnvelope` schema + `.errorResponse()` builder shorthand.** Exported from the root entry: a canonical fortress schema matching the wire shape `FortressError.toJSON()` emits (`{ code, message, statusCode, details? }`). `EndpointBuilder.errorResponse(status, description)` is a one-liner that wires it into a status declaration so host APIs document the same error contract Fortress's own routes produce. (Wishlist #6.)
134
+ - **Drizzle Postgres SQLSTATE → `FortressError` mapping.** The Fortress Drizzle adapter's `create` / `update` / `delete` methods now translate Postgres constraint and concurrency states into the matching `FortressError` so `protect()` can serialize them as the right HTTP status. The same mapper is exported from `@bajustone/fortress/drizzle` as `findSqlstate` / `rethrowPgError` for host routes that use raw Drizzle directly. Mapped: `23505 → CONFLICT/409`, `23503 → UNPROCESSABLE_ENTITY/422`, `23502 → BAD_REQUEST/400`, `23514 → UNPROCESSABLE_ENTITY/422`, `40001` and `40P01 → CONFLICT/409`, `57014 → SERVICE_UNAVAILABLE/503`. No-op for non-`pg` dialects. (Wishlist #5.)
135
+ - `Errors.unprocessable()` and `Errors.serviceUnavailable()` factory functions for the new `UNPROCESSABLE_ENTITY` and `SERVICE_UNAVAILABLE` codes.
136
+
137
+ ### Changed
138
+ - **`FortressErrorCode` widened** with `'UNPROCESSABLE_ENTITY'` (422) and `'SERVICE_UNAVAILABLE'` (503). `Errors.conflict()` now accepts an optional `{ cause, details }` options bag so it can be threaded through error wrappers without losing the original.
139
+ - **`statusToErrorCode` fallback** now maps 422 → `UNPROCESSABLE_ENTITY` (was `VALIDATION_ERROR`) and 503 → `SERVICE_UNAVAILABLE`. Bodies that carry an explicit `code` field still round-trip unchanged via `Errors.fromHttpResponse`; this only affects bodies whose `code` cannot be parsed.
140
+ - **Typing (breaking, types-only):** `protect()` and the Hono / Express / SvelteKit `protectedRoute()` wrappers are now generic over the `EndpointDefinition` you point them at. Passing a typed endpoint (the value produced by `endpoint(...).build()`) flows its phantom `<TBody, TQuery, TParams, TResponses>` generics into `ctx.body` / `ctx.query` / `ctx.params` / `ctx.input` — no more `unknown` / `Record<string, unknown>` at the call boundary, and no more local casts in host apps. Passing a string `handler` name keeps the previous loose typing. The runtime is identical; this is a `.d.ts`-only change.
141
+ - Hono `protectedRoute()` renames its environment type parameter from `E` to `HEnv` so it no longer collides with the new endpoint generic. Callers who relied on positional type-argument inference are unaffected; callers that passed the env explicitly should rename the slot.
142
+
143
+ ## [0.2.0] - 2026-06-08
144
+
145
+ ### Added
146
+ - SQL-first initial-schema migration baseline with deep drift detection.
147
+ - CI matrix coverage and audit export pipeline.
148
+ - Phase 0 + Phase 1 + most of Phase 2 work from the library plan (see commit 8b94985 for scope).
149
+
150
+ ### Changed
151
+ - Tenancy plugin hardened: claim-based resolution and atomic schema isolation.
152
+ - Repo hygiene: ephemeral planning/review docs moved to `scratch/` and untracked.
153
+
154
+ ## [0.1.2] - 2026-06-06
155
+
156
+ ### Security
157
+ - Remediation pass for the 2026-06-05 independent review (tenancy plugin skeleton findings remain deferred/unmounted):
158
+ - SQLite adapter transactions are serialized and use `BEGIN IMMEDIATE`, restoring atomic CAS semantics for refresh rotation and OAuth code exchange.
159
+ - OAuth authorization codes are now atomically single-use under concurrency; public-client PKCE is enforced at exchange; per-client `grantTypes` are enforced for `authorization_code` and `refresh_token`.
160
+ - OAuth consent flows are bound to a user and deny cross-user get/approve/deny with 404.
161
+ - Pipeline CSRF protection is now enabled by default for unsafe, cookie-authenticated Fortress routes. Bearer/API-key requests are skipped; configure via `FortressConfig.csrf`.
162
+ - Data-isolation bypass windows now use `AsyncLocalStorage` and no longer leak across concurrent requests.
163
+ - JWT verification pins HS256 and configured issuer; reserved JWT claims (`sub`, `act`, `groups`, etc.) are stripped from custom claims.
164
+ - Disabled-account login returns the same generic invalid-credentials path and still verifies the password hash to reduce enumeration/timing signals.
165
+ - Impersonation token TTL is clamped (default max 3600s) and emits an auth observer event.
166
+ - Follow-up gap closure from the implementation review:
167
+ - Public-client PKCE is now also enforced at code-issuance time (`createAuthorizationCode`), so a binding-less code is never minted — not only rejected at exchange.
168
+ - `findOrCreatePermission`, `ensureResource`, and the role/permission/group binding idempotency helpers are race-safe: a lost create race re-reads the winner's row instead of surfacing a raw unique-constraint error.
169
+ - CSRF detection recognizes refresh-only cookie sessions (expired access cookie), closing a bypass where a refresh-only request skipped the check.
170
+ - The SvelteKit adapter's silent token refresh is restricted to safe HTTP methods, so a cross-site unsafe request can no longer trigger a refresh-token rotation.
171
+
172
+ ### Changed
173
+ - Auth cookie extraction now prefers an explicit `Authorization: Bearer` header over the access cookie (cookie-shadow hardening).
174
+ - Cookies default to `Secure` / `__Host-` names regardless of `NODE_ENV`; local HTTP development must opt out with `cookies: { secure: false }`.
175
+ - Schema migration required for OAuth pending flows (`user_id`) and permission uniqueness partial indexes.
176
+
177
+ ## [0.1.1] - 2026-05-07
178
+
179
+ ### Fixed
180
+ - fix(oauth): honor security: ['bearer'] on /oauth/* routes via bearerKind
181
+
182
+ ## [0.1.0] - 2026-05-07
183
+
184
+ ### Added
185
+ - oauth: full RFC + OIDC compliance pass
186
+
187
+ ## [0.0.42] - 2026-04-29
188
+
189
+ ### Added
190
+ - oauth2
191
+
192
+
193
+ ### Security
194
+ - Hardened the tenancy plugin: tenant schema names now use numeric ids, tenant context comes from verified JWT custom claims instead of `X-Tenant-Code`, PostgreSQL `search_path` is transaction-pinned with a bound parameter, invalid tenant claims are rejected, and missing tenant context fails closed.
195
+
196
+ ### Fixed
197
+ - **`security: ['bearer']` on `/oauth/*` routes is now honoured.** Previously the dispatcher had a path-based `startsWith('/oauth/')` short-circuit that skipped the plugin principal chain, the JWT bearer check, AND RBAC enforcement for *every* `/oauth/*` route, including the consent-flow endpoints (`/oauth/flows/:flowId{,/approve,/deny}`) added in 0.0.42 — leaving them silently unauthenticated despite their `security: ['bearer']` declaration. Host apps had to ship a workaround shim that re-implemented the routes on their own router; TDMP discovered this regression on the 0.0.42 → 0.1.0 upgrade after retiring the shim under the impression that the new `security` declarations were enforced. The fix introduces a per-route opt-in: `EndpointMeta.bearerKind` is a new field with two values — `'jwt'` (default) and `'oauth'`. When the field is absent or `'jwt'`, the dispatcher runs the normal auth pipeline (plugin principals → JWT verification → RBAC → body parse + validation) regardless of path. When set to `'oauth'`, the route is treated as a self-managed OAuth-protocol endpoint (handler parses the bearer itself, body is form-encoded) and the entire pipeline is skipped — same behaviour the path-based check used to provide. The seven OAuth-protocol routes (`/oauth/{authorize, token, introspect, revoke, userinfo, .well-known/openid-configuration, .well-known/jwks.json}`) now declare `bearerKind: 'oauth'` explicitly. Consent-flow routes inherit the JWT default and run through fortress's full auth pipeline, so `/oauth/flows/:flowId/approve` correctly receives `auth.userId` from the host app's session JWT and no host-app shim is needed. Three regression tests added under `core/http/handle-request.test.ts › bearerKind: 'jwt' default for /oauth/* routes`.
198
+
199
+ ### Added
200
+ - Route-security manifest: `fortress.manifest`, `buildRouteManifest()`, manifest drift-check helpers, `fortress manifest`, and `fortress manifest:check`. Hono, Express, and SvelteKit adapters now consume the manifest when deciding which routes to intercept.
201
+ - Host-owned route protection helpers: core `protect()` plus Hono/Express/SvelteKit `protectedRoute()` wrappers apply plugin middleware, CSRF, auth, RBAC, validation, and auth-cookie attachment from endpoint metadata.
202
+ - Migration tooling foundation: `fortress_schema_version` Drizzle table, bundled `migrations/{sqlite,pg}/0001_schema_version.sql`, migration runner/status/drift helpers, and `fortress migrate:*` catalog commands.
203
+ - Migration drift checker now reports missing Fortress tables via `FORTRESS_TABLES`, not only version-table state; new upgrade fixture proves bundled migrations apply end-to-end against a bare database. Workflow documented in `docs/migrations/upgrade-guide.md`.
204
+ - Production deployment guide (`docs/deployment.md`) covering JWT rotation, cookies behind reverse proxies, CSRF opt-outs, CORS recipes, HTTPS requirements, PostgreSQL vs SQLite tradeoffs, OAuth/OIDC RP setup, API-key/service-account ops, migration runbook, observability, and a release checklist.
205
+ - Typed adapter helpers (P1-6): `FortressEnv<TAppEnv>` is now generic so host Hono apps compose their own `Variables` / `Bindings` with Fortress's without `Context<AppEnv>` ↔ `Context<FortressEnv>` casts. `getSubject`/`getUserId`/`getClaims`/`getDb`/`getScopedDb` are generic in the env type; `getClaims<TCustomClaims>(c)` narrows `customClaims` to plugin-augmented shapes (e.g. tenancy's `tenantId`/`tenantCode`). Express exports `FortressExpressFields` for declaration-merging into `express-serve-static-core.Request`. SvelteKit's `FortressLocals` is already merge-friendly. Type-level tests under `src/hono/middleware/auth.types.test.ts`; full reference in `docs/adapter-typed-helpers.md`.
206
+ - CI / test utility package (P1-10): `@bajustone/fortress/testing` now ships `checkRouteManifestDrift`, `checkPublicRoutes`, `checkMigrationDrift`, `smokeTestAuth`, and the `runFortressChecks` aggregator so consumer apps can gate deploys on Fortress's own drift detectors. New `fortress check:routes`, `check:public-routes`, and `check:migrations` CLI commands; a drop-in GitHub Actions workflow at `docs/ci/github-actions.yml`. Full reference in `docs/ci.md`.
207
+ - Policy-as-code (P1-7): declarative `fortress.policy.json` (with optional `fortress.policy.<env>.json` override) covers resources, roles, groups, and service-account role bindings. New exports: `loadPolicy`, `diffPolicy`, `applyPolicyPlan`, `applyResourceOps`, `resolvePolicyPath`. CLI: `fortress policy:summary` (offline), `policy:diff`/`apply`/`check` (how-to printers). New `IamService.removePermissionFromRole(...)` underpins narrow-role diffs. Full reference in `docs/policy-as-code.md`; sample file at `examples/policy/fortress.policy.json`.
208
+ - Permission-debugging helper (P1-8): `explainPermission(db, iam, subject, resource, action)` returns the full attribution graph — every grant source (direct user / direct group / service account / role), group memberships, role bindings, and the final DENY-wins decision — powering admin "why does X have permission Y?" workflows. Recipes for admin/operator console workflows in `docs/admin-recipes.md`.
209
+ - Observability event + alert catalog (P2-13): expanded `docs/observability.md` with the full Auth/IAM event catalog (every `eventType` Fortress emits), recommended dashboards (auth, IAM, OAuth, database), starting-point alert thresholds, and audit-log integration recipe.
210
+ - Compatibility matrix (P2-11): `docs/compatibility.md` documenting the tested runtime/framework/database matrix and what's intentionally out of scope.
211
+ - Hardening guide (P2-14): `docs/hardening.md` — prescriptive production hardening checklist covering identity/credentials, authorization, transport/edge, data isolation, audit/logging, crypto, and deployment hygiene. Linked from `SECURITY.md`.
212
+ - Full initial-schema migration (P0-1): new bundled `0002_initial_schema` migration (SQLite + PostgreSQL) creates every Fortress-owned table, index, and constraint, so `migrateUp` now provisions a brand-new database end-to-end through the adapter's `rawQuery` — no Drizzle/`drizzle-kit` dependency at runtime. The migrations are the SQL-first source of truth: `createTestAdapter()` derives its schema from them via `getMigrationUpSql()`, so the test adapter and a production `migrateUp` can no longer drift. Per-release guide at `docs/migrations/0002-initial-schema.md`.
213
+ - Deep migration drift detection: `detectMigrationDrift` now reports `missingColumns` — tables that exist but are missing a column the bundled DDL defines (partial/stale schema). Expected columns are parsed straight from the migration SQL, so the check stays adapter-agnostic. Surfaced by `checkMigrationDrift` and `fortress migrate:check`.
214
+ - PostgreSQL migration upgrade fixture (testcontainers): provisions a bare real Postgres from the bundled migrations and asserts zero drift, catching dialect-specific issues (SERIAL, partial unique indexes, JSONB, FK drop ordering) the SQLite fixture can't.
215
+ - Audit-log export: `auditLog.exportEntries(format?, options?)` serializes entries to JSON or RFC 4180 CSV for compliance/retention, honouring the same `AuditLogQueryOptions` filters as `getAuditLog`. Docs in `docs/plugins/audit-log.md`.
216
+ - Public CI matrix: `.github/workflows/ci.yml` now runs the unit suite across Bun + Node 20 + Node 22, plus a PostgreSQL/testcontainers `integration` job (pg dialect, tenancy isolation, migration fixture, framework adapters) on PRs/main and a nightly cron.
217
+
218
+ ### Changed
219
+ - The migration version row is now stamped solely by the runner (`migrateUp`/`migrateDown`); the `0001_schema_version` forward SQL no longer self-inserts a version row, giving a single source of version truth.
220
+ - Examples scenario index (P2-12): `examples/README.md` maps the six plan-specified scenarios (cookie+CSRF, bearer-only API, API-key + service account, OAuth/OIDC provider, admin bootstrap + policy sync, tenancy) to the existing reference apps and recipe docs.
221
+ - Formal security-review packet and threat model docs covering OAuth/OIDC, refresh rotation, CSRF/cookies, IAM/RBAC, API-key/service-account flows, tenancy isolation, and drift controls.
222
+ - Tenancy: `deleteTenant`, `getMyTenants`, opt-in `/tenancy/*` routes, `onSchemaCreated`, and `dropSchemaOnDelete`.
223
+ - **OAuth 2 / OIDC compliance pass.** Brings `@bajustone/fortress/plugins/oauth` from "OAuth 2 + OIDC userinfo veneer" to a full RFC-conformant authorization server suitable for strict OIDC RPs (Moodle, openid-client, Keycloak federation, Spring Security). Closes the eleven gaps in the TDMP audit (`docs/oauth-compliance-plan.html`); every audit row now reads PASS. Headline changes:
224
+ - **OIDC userinfo (§4.1, OIDC Core §5.3)** — `/oauth/userinfo` now returns the OIDC-shaped JSON object (`sub` as string, `email`, `email_verified`, `name`, `preferred_username`, `updated_at`) instead of leaking the raw `FortressUser` row. Claims are scope-gated per OIDC §5.4 (`email` / `profile`); the legacy permissive baseline is kept for non-OIDC tokens. New `OAuthConfig.userinfoClaims` hook for per-deployment claim extension. Exported `toOidcUserinfo(user, scope)` for host-app composition.
225
+ - **id_token issuance (§4.2, OIDC Core §3.1.3.7)** — New `src/plugins/oauth/jwks.ts` + `id-token.ts` modules. The `/token` endpoint now issues a signed RS256 id_token alongside the access token whenever the request used `scope=openid`. New endpoint `GET /oauth/.well-known/jwks.json` exposes the verification JWKS (RFC 7517). `nonce` and `auth_time` are persisted on the auth code and echoed into the id_token (§3.1.2.1 / §2). New `oauth_signing_key` model holds the active RS256 keypair (kid = RFC 7638 thumbprint).
226
+ - **Refresh token grant + rotation (§4.3, RFC 6749 §6 + RFC 9700 §2.2.2)** — New `oauth_refresh_token` model with `familyId` rotation tracking. `exchangeCode` now returns a refresh token alongside the access token; `grant_type=refresh_token` is supported on `/token`. Reuse of an already-rotated token is treated as an attack and revokes the entire family (replay detection). Configurable via `OAuthConfig.refreshTokenExpirySeconds` (default 30 d, set to 0 to disable). Revoking a refresh token (RFC 7009) sweeps the whole family.
227
+ - **Constant-time secret compare (§4.4)** — New `src/core/auth/timing-safe.ts` (`timingSafeEqualHex`); replaces all `===` hash comparisons in OAuth (client secret check, code hash, refresh-token hash). Mitigates the hypothetical timing oracle even on uniform SHA-256 outputs.
228
+ - **Mandatory PKCE on `/oauth/authorize` (§4.5, RFC 9700 §2.1.1)** — Authorize requests without `code_challenge` now return `error=invalid_request`. Plain method is rejected. Escape hatch: `OAuthConfig.allowNonPkceConfidentialClients` for legacy server-side RPs (defaults `false`, discouraged).
229
+ - **RFC 9207 issuer identification (§4.6)** — Every authorize redirect (success and error) now carries `iss=<issuerUrl>`. Discovery declares `authorization_response_iss_parameter_supported: true`. Mitigates RFC 9700 §4.4 mix-up attacks.
230
+ - **Discovery completeness (§4.7)** — Adds `jwks_uri`, `id_token_signing_alg_values_supported`, `scopes_supported`, `claims_supported`, `response_modes_supported`, `authorization_response_iss_parameter_supported`, `'none'` in `token_endpoint_auth_methods_supported`, `'refresh_token'` in `grant_types_supported`. Strict autoconfig (openid-client) now succeeds with no manual config.
231
+ - **Public-client + RFC 8252 loopback (§4.8)** — New `tokenEndpointAuthMethod` per-client field (`'client_secret_basic'` | `'client_secret_post'` | `'none'`). Public clients (SPAs / native apps) authenticate via PKCE alone; presenting a client_secret is rejected per RFC 6749 §2.3.1; `client_credentials` is denied. Exported `matchRedirectUri(registered, inbound)` honours the loopback exception: registered `http://127.0.0.1/cb` matches any-port at runtime. `localhost` (DNS) is NOT widened (RFC 8252 §8.3 DNS-rebinding guidance).
232
+ - **Per-client scope allow-list (§4.9, RFC 6749 §3.3 + RFC 9700 §2.2.1)** — New `allowedScopes` field on `oauth_client`. Requested scope is intersected against the allow-list at authorize and `client_credentials` time; empty intersection returns `error=invalid_scope`. Refresh-token grant rejects scope widening, accepts narrowing. Legacy clients with no allow-list pass through unchanged.
233
+ - **RFC 6749 §5.2 / §4.1.2.1 error codes (§4.10)** — New `OAuthErrorCode` union (`invalid_request`, `invalid_client`, `invalid_grant`, `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`, `access_denied`, `unsupported_response_type`, `server_error`, `temporarily_unavailable`) + `Errors.oauth(code, description)` factory. The HTTP error mapper detects `oauthError` and emits the spec-required `{ error, error_description, error_uri? }` body shape on the token endpoint instead of the default fortress `{ code, message }` envelope.
234
+ - **HTTPS issuer assertion (§4.11, RFC 8414 §2)** — Production startup (`NODE_ENV=production`) refuses to register the OAuth plugin if `issuerUrl` is non-HTTPS. Dev / test (localhost over HTTP) is unaffected.
235
+
236
+ ### Changed
237
+ - **BREAKING tenancy:** removed `headerName` / `X-Tenant-Code` tenant selection; schemas are now named `tenant_<id>` instead of `tenant_<taxId>` (no in-place migration — rename existing schemas manually); `switchTenant` and `getMyTenants` now take `(input, routeCtx?)`.
238
+ - `OAuthMethods.handleUserInfoRequest` return type: `Promise<FortressUser | null>` → `Promise<Record<string, unknown>>` (OIDC claims, throws 401 on bad token).
239
+ - `OAuthMethods.exchangeCode` now also returns an optional `refreshToken` and `idToken`. Existing callers ignoring those fields are unaffected.
240
+ - `createClient` now accepts optional `tokenEndpointAuthMethod` and `allowedScopes`; returns `clientSecret: string | null` (null for public clients).
241
+
242
+ ### Docs
243
+ - New compliance plan: `docs/oauth-compliance-plan.html` (companion to TDMP's audit). Maps each finding to spec clause → observable behaviour → code shape → definition of done.
244
+
245
+ - **OAuth: SPA-friendly authorization flow (Pattern B).** The OAuth plugin can now drive a consent flow where the host app owns the login + consent UI and Fortress owns the state machine — no HTML ever leaves Fortress, the framework-agnostic stance stays intact.
246
+ - New `OAuthConfig` fields: `enableAuthorizeEndpoint`, `enableConsentApi`, `loginUrl`, `consentUrl`. Both endpoint groups default to off so existing setups are unaffected.
247
+ - New endpoints (opt-in): `GET /oauth/authorize` (front door, 302s to `loginUrl?flow=<id>` if no session, `consentUrl?flow=<id>` if authenticated), `GET /oauth/flows/:flowId` (consent metadata: client name, requested scopes, redirect URI — PKCE fields stripped), `POST /oauth/flows/:flowId/approve` (issues auth code, returns `{ redirectUrl }`), `POST /oauth/flows/:flowId/deny` (returns `access_denied` redirect URL).
248
+ - New plugin methods: `handleAuthorizeRequest`, `handleGetFlow`, `handleApproveFlow`, `handleDenyFlow`. All transport-agnostic; safe to call from `fortress.call.*` or directly.
249
+ - New method `getPendingFlow(flowId)` for non-destructive reads of pending flows. Existing `resumePendingFlow` (single-use consume) is unchanged.
250
+
251
+ ## [0.0.41] - 2026-04-22
252
+
253
+ ### Fixed
254
+ - fix input schemas
255
+
256
+ ## [0.0.40] - 2026-04-16
257
+
258
+ ### Fixed
259
+ - fix
260
+
261
+ ## [0.0.39] - 2026-04-16
262
+
263
+ ### Changed
264
+ - rate-limit: `login` and `register` are now **always on with defaults** when the plugin is registered — reverts the 0.0.37 opt-in change, which risked silent loss of auth DoS protection on upgrade. To turn either off explicitly, pass `login: { disabled: true }` / `register: { disabled: true }`. Other endpoint blocks (`refresh`, `oauthToken`, `apiKeyIssue`) remain opt-in.
265
+
266
+ ### Docs
267
+ - Clarify `paths` config vs the per-framework wrappers (`honoRateLimit` / `expressRateLimit` / `svelteKitRateLimit`). Both target the same store; the wrapper is for framework-mounted routes, `paths` is for serverless / declarative-only setups. Don't stack both on the same path — each match increments the counter, halving the effective limit.
268
+ - Spell out that `fortress.call.*` runs the full middleware pipeline — plugin middleware, rate limits, principal resolution, RBAC, validation, auth/IAM observers, OTel spans. Tests that need to bypass those should call the service layer directly (`fortress.auth.*`, `fortress.iam.*`).
269
+
270
+ ## [0.0.38] - 2026-04-16
271
+
272
+ ### Fixed
273
+ - fix(jsr)!: explicit types on `authEndpoints` / `iamEndpoints` / their component registries so JSR fast-check passes without `--allow-slow-types`. v0.0.36 and v0.0.37 silently failed `publish-jsr` (latest on the JSR registry was 0.0.35) — this release is the first since 0.0.35 to actually reach JSR. Removed `--allow-slow-types` from `publish:dry`, `jsr-check`, and `publish-jsr` so the regression can't recur.
274
+
275
+ ### Added
276
+ - `AuthEndpointsMap` and `IamEndpointsMap` exported interfaces listing each core endpoint's `EndpointDefinition<TBody, TQuery, TParams, TResponses>` generics. Used to constrain `authEndpoints` / `iamEndpoints` at declaration — `fortress.call.*` per-handler inference is fully preserved.
277
+ - Wire-format shape exports (`UserWire`, `AuthResponseWire`, `AuthTokenPairWire`, `ErrorResponseWire`, `SessionInfoWire`, `LoginIdentifierWire`, `CreateUserInputWire`, `OkResponseWire` in auth; `RoleWire`, `GroupWire`, `PermissionWire`, `PermissionInputWire`, `ServiceAccountWire` in iam). These mirror `src/core/types.ts` with `Date` fields widened to `string` (ISO 8601) — the shapes consumers actually see after JSON serialization.
278
+
279
+ ## [0.0.37] - 2026-04-16
280
+
281
+ ### Added
282
+ - feat(rate-limit)!: whole-app coverage via check() + framework wrappers
283
+
284
+
285
+ ### Fixed
286
+ - `extractJsonSchema` now recognizes Standard Schema implementations that are themselves JSON Schema objects (e.g. `@bajustone/fetcher`), not just fortress. Previously only schemas with `vendor: 'fortress'` or a `~standard.jsonSchema.input()` adapter (Zod, Valibot, ArkType) survived — fetcher schemas fell through to `{}` and bodies/queries/params disappeared from generated OpenAPI specs. Detection uses structural JSON Schema props (`type` matching a spec type, or `$ref`/`oneOf`/`anyOf`/`allOf`), so Zod-style wrappers whose `type` is an internal kind string (`'ZodObject'`) still route through the adapter path.
287
+
288
+ ### Added
289
+ - feat(rate-limit): extend coverage to `refresh` (`beforeTokenRefresh` hook), OAuth `/oauth/token`, and API-key issuance (`POST /api-key/keys`) via path-bound plugin middleware.
290
+ - feat(rate-limit): **named rules** (`config.rules`) referenced from a new programmatic `fortress.plugins['rate-limit'].check(ruleName, { ip, userId })` surface.
291
+ - feat(rate-limit): per-framework wrappers — `honoRateLimit`, `expressRateLimit`, `svelteKitRateLimit` — published as sub-path exports `./plugins/rate-limit/{hono,express,sveltekit}`. Rate-limit any user-owned route in one line.
292
+ - feat(rate-limit): `paths` config accepts arbitrary path-glob bindings to a rule + optional method filter, for any Fortress-handled route without a built-in block.
293
+
294
+ ### Changed (breaking)
295
+ - `RateLimitConfig.login` / `register` are now fully opt-in — omitting a block disables that hook (previously both were always enabled with defaults if the plugin was registered). Pass an empty object (`login: {}`) to enable defaults.
296
+ - Rate-limit store key format changed from `login:ip:<ip>` / `login:account:<email>` to `<rule>:ip:<ip>` / `<rule>:user:<value>` for consistency across all rule types. Clears automatically with in-memory store; external stores on v0.0.x may need a flush.
297
+
298
+ ## [0.0.36] - 2026-04-16
299
+
300
+ ### Added
301
+ - feat(endpoints): typed in-process client via fortress.call.*
302
+ - feat(observability): outer request span, token-verify histogram, DB spans
303
+
304
+ ## [0.0.36] - 2026-04-15
305
+
306
+ ### Added
307
+ - feat(endpoints): typed in-process client — `fortress.call.<handler>(input)` infers body/query/params from the endpoint's declared schemas and returns the 2xx response body typed. Non-2xx throws a structured `FortressError`.
308
+ - feat(schema-builder): `defineComponents({...})` returns a typed `ref` bound to a component map so `$ref`s carry their TS type through to endpoint responses. The old `ref(name)` still works; a new 2-arg overload `ref(name, schema)` preserves type for self-references inside a components literal.
309
+ - feat(endpoint): `EndpointDefinition` + `EndpointBuilder` are now generic in body/query/params/responses. Added `InferEndpointBody`, `InferEndpointQuery`, `InferEndpointParams`, `InferEndpointResponses`, `InferEndpointSuccessResponse`, `InferEndpointCallInput` helpers.
310
+ - feat(errors): `Errors.fromHttpResponse(status, body)` reconstructs a `FortressError` from a JSON error body — the inverse of `errorToResponse`.
311
+
312
+ ### Changed (breaking)
313
+ - `authEndpoints` / `iamEndpoints` changed shape from `EndpointDefinition[]` to keyed records (`Record<string, EndpointDefinition<...>>`). Preserves per-handler generic types for the new typed call surface. Consumers that iterated the arrays should now use `Object.values(authEndpoints)`.
314
+ - `FortressPlugin.routes` changed from `EndpointDefinition[]` to `Record<string, EndpointDefinition>` — keyed by handler name. All built-in plugins with routes (admin, api-key, oauth, openapi, webauthn) migrated. The `RouteDefinition` alias has been removed.
315
+ - `Fortress` interface grew a second generic parameter `TCall` and a new `call` field.
316
+
317
+ ## [0.0.35] - 2026-04-15
318
+
319
+ ### Added
320
+ - feat(observability): pluggable logger, auth/permission observers, optional OTel adapter
321
+
322
+ ## [0.0.34] - 2026-04-14
323
+
324
+ ### Added
325
+ - more tests
326
+
327
+ ### Fixed
328
+ - fix(validation): coerce URL-sourced query/params to their declared types
329
+
330
+ ## [0.0.33] - 2026-04-14
331
+
332
+ ### Added
333
+ - feat(admin): HTTP endpoints to mint api keys for any subject
334
+
335
+ ## [0.0.32] - 2026-04-14
336
+
337
+ ### Added
338
+ - docs: polish documentation
339
+ - feat(iam)!: promote SERVICE_ACCOUNT to first-class citizen
340
+ - feat(api-key,admin)!: opt-in HTTP routes for self-service + admin management
341
+ - feat(admin): HTTP endpoints to mint api keys for any subject (closes the SERVICE_ACCOUNT bootstrap gap — `POST /admin/users/:userId/api-keys` and `POST /admin/service-accounts/:id/api-keys`, plus GET/DELETE for service accounts)
342
+ - feat(webauthn)!: require ctx.userId for registration, drop body userId
343
+ - feat(plugins)!: pass PluginRouteContext to plugin route handlers
344
+
345
+ ## [0.0.31] - 2026-04-10
346
+
347
+ ### Added
348
+ - feat(openapi): emit per-resource discriminated unions for Permission schemas
349
+
350
+ ## [0.0.30] - 2026-04-09
351
+
352
+ ### Added
353
+ - feat!: validate consumer routes (hono/sveltekit/express) at runtime
354
+
355
+ ### Fixed
356
+ - fix jsr
357
+
358
+ ## [0.0.29] - 2026-04-09
359
+
360
+ ### Changed
361
+ - refactor(adapters)!: delete deprecated dispatch APIs, delegate fully to fortress.handleRequest
362
+ - refactor
363
+
364
+
365
+ ### Added
366
+ - **Pluggable logger via `FortressConfig.logger`.** Accepts any object
367
+ structurally compatible with `FortressLogger` — a `pino()` instance,
368
+ Fastify's `app.log`, or a hand-rolled `console` wrapper. Default is
369
+ `SILENT_LOGGER` so Fortress never writes to stderr unless the caller
370
+ opts in. Replaces the five hardcoded `console.warn`/`console.error`
371
+ call sites in `plugin-runner.ts`, `auth-service.ts`,
372
+ `http/error-response.ts`, and `express/middleware.ts`.
373
+ - **`AuthEvent` + `AuthService.addAuthObserver`.** Mirrors the existing
374
+ `addIamObserver` pattern. Emits `LOGIN_SUCCESS`, `LOGIN_FAILURE`,
375
+ `LOGOUT`, `REGISTER`, `TOKEN_REFRESH`, `TOKEN_REUSE_DETECTED`, and
376
+ `TOKEN_FINGERPRINT_MISMATCH` with optional actor, IP, user-agent,
377
+ method, and outcome metadata. Observers can subscribe at init time
378
+ from any plugin's `methods(ctx)` factory without re-implementing
379
+ every auth hook.
380
+ - **`PermissionCheckEvent` + `IamService.addPermissionCheckObserver`.**
381
+ Synchronous high-frequency listener for `checkPermission`, carrying
382
+ `cached` / `durationSeconds` / `allowed` / `subjectType` /
383
+ `subjectId` / `resource` / `action`. Separate from `IamEvent` so
384
+ audit-log (which subscribes to IAM mutations) isn't spammed with
385
+ per-check traffic. Listener signature is intentionally `void`, not
386
+ `Promise<void>`, to discourage awaiting expensive work on the hot path.
387
+ - **Unsubscribe functions on every observer adder.** `addAuthObserver`,
388
+ `addIamObserver`, and `addPermissionCheckObserver` all return a
389
+ `() => void` unsubscribe callback. Existing callers of
390
+ `addIamObserver` that ignored the `void` return type still compile
391
+ unchanged.
392
+ - **Observer error routing.** Observer failures are now logged at
393
+ `error` level via the configured `FortressLogger` instead of being
394
+ silently swallowed. Auth/IAM operations still never throw from an
395
+ observer bug.
396
+ - **`FortressConfig.observability` + OpenTelemetry adapter sub-path.**
397
+ New opt-in `@bajustone/fortress/otel` sub-path export with a single
398
+ `createOtelTelemetry({ name })` factory. The core package never
399
+ statically imports `@opentelemetry/api` — the adapter uses
400
+ `await import('@opentelemetry/api')` for true dynamic loading, so
401
+ runtimes that don't opt in (Cloudflare Workers, Deno without OTel)
402
+ never resolve the peer dep. `@opentelemetry/api` is declared as an
403
+ optional peer dependency with `peerDependenciesMeta.optional: true`.
404
+ - **Built-in metrics via the telemetry provider.** When a non-noop
405
+ `observability` provider is wired, Fortress emits:
406
+ - `fortress.auth.events.total` counter (attrs: `event`, `outcome`, `method`)
407
+ - `fortress.iam.events.total` counter (attrs: `event`)
408
+ - `fortress.iam.permission_check.duration` histogram in seconds
409
+ (attrs: `subject_type`, `result`, `cached`)
410
+ - `fortress.iam.permission_check.cache.hits` + `.cache.misses` counters
411
+ - `db.client.operation.duration` histogram in seconds — **the stable
412
+ OpenTelemetry semantic-convention metric name**, not a
413
+ Fortress-specific name — with `db.system.name`, `db.operation.name`,
414
+ and `db.collection.name` attributes. Every Fortress-internal DB
415
+ operation (and plugin queries going through the wrapped adapter)
416
+ flows into it.
417
+ - Plus a `fortress.iam.permission_check.deny` span emitted only on
418
+ denied checks (security-interesting). Allowed checks are metric
419
+ fodder — no span emitted, keeping the hot path cheap.
420
+ - **`fortress.logger` and `fortress.telemetry` exposed on the instance.**
421
+ Adapters and plugins can read them after construction.
422
+
423
+ ### Fixed
424
+ - **`resolvePrincipal` now fires on user-owned routes, not just
425
+ Fortress-managed ones.** Previously, only requests dispatched through
426
+ `fortress.handleRequest` (`/auth/*`, `/iam/*`, plugin routes, OAuth,
427
+ OpenAPI) walked the `resolvePrincipal` plugin chain — so
428
+ `Authorization: ApiKey ...` / `X-API-Key: ...` headers authenticated
429
+ Fortress routes but silently 401'd on any custom route protected by the
430
+ Hono / Express / SvelteKit auth middleware. API keys, and every future
431
+ credential plugin, now work uniformly on both surfaces.
432
+ - New `fortress.resolvePrincipal(request)` method on the Fortress
433
+ instance — plugin chain + non-throwing JWT fallback. Exposed publicly
434
+ alongside `fortress.extractAccessToken`, so third-party adapters can
435
+ delegate to it in ~3 lines.
436
+ - New `src/core/http/principal.ts` with `tryPluginPrincipal` (plugin
437
+ chain only, used by `handle-request`) and `resolveRequestPrincipal`
438
+ (chain + JWT, used by adapter user-route middleware).
439
+
440
+ ### Changed
441
+ - **Adapter request context is now subject-based, not USER-only.** All
442
+ three adapters now expose `fortressSubject: Subject` as the
443
+ authoritative principal field, with `fortressUserId` demoted to a
444
+ convenience alias populated only when `subject.type === 'USER'`. This
445
+ unblocks service-account principals resolved via api-key from using
446
+ *any* user-owned route, not just Fortress routes.
447
+ - **Hono**: `FortressEnv.Variables.fortressSubject: Subject`;
448
+ `fortressUserId` now optional. User-route RBAC middleware checks
449
+ `fortress.iam.checkPermission(subject, ...)` instead of hardcoding
450
+ `{ type: 'USER', id }`. New `getSubject(c)` helper.
451
+ - **Express**: `req.fortressSubject?: Subject`; `req.fortressUserId`
452
+ now populated only for USER subjects. New `getSubject(req)` helper.
453
+ - **SvelteKit**: `event.locals.fortress.subject?: Subject`;
454
+ `locals.fortress.userId` populated only for USER subjects. New
455
+ `getSubject(event)` helper. Auto-refresh on expired JWTs still
456
+ works, but plugin resolvers run first so api-key requests skip the
457
+ refresh path entirely.
458
+ - **Breaking**: `getUserId` / `getClaims` now throw 401 for
459
+ non-USER principals and for api-key-authenticated requests
460
+ (respectively). Code that accepts any principal should call
461
+ `getSubject` instead and branch on `subject.type`. Per the "no
462
+ compat burden" rule for v0.0.x, no shims are provided.
463
+ - **`PluginRequestContext` now carries `fortressSubject`** in addition to
464
+ `fortressUserId` / `fortressClaims`. All three adapters pass it through
465
+ to the `before-auth` / `after-auth` / `after-rbac` plugin middleware
466
+ slots; plugin middleware wanting subject-level identity (non-USER
467
+ principals) can read it instead of relying on the USER-only alias.
468
+
469
+ ### Added
470
+ - **`SERVICE_ACCOUNT` is now a first-class core IAM citizen.** Previously
471
+ `SERVICE_ACCOUNT` existed only as a `SubjectType` enum value and any role
472
+ binding to one was silently dropped at permission-check time. Service
473
+ accounts are now a fully-supported IAM entity with CRUD endpoints, role
474
+ bindings, direct permission bindings, and api-key authentication. Typical
475
+ use: CI/CD, M2M communication, mobile devices, or any machine principal
476
+ that should hold scoped permissions without being tied to a human user.
477
+ - **New table: `fortress_service_account`**. Columns: `id`, `name`
478
+ (unique machine identifier, immutable after creation), `displayName`,
479
+ `description`, `isActive`, `createdAt`, `updatedAt`. Service accounts
480
+ are globally scoped at the table level — tenant scoping happens via
481
+ `role_binding.tenantId`, the same mechanism users use.
482
+ - **New `IamService` methods**: `createServiceAccount`,
483
+ `getServiceAccount`, `listServiceAccounts`, `updateServiceAccount`
484
+ (rejects `name` changes), `deleteServiceAccount` (hard delete with
485
+ cascade cleanup of bindings + api keys), plus
486
+ `bindRoleToServiceAccount`, `unbindRoleFromServiceAccount`,
487
+ `bindPermissionToServiceAccount`, `unbindPermissionFromServiceAccount`.
488
+ - **New HTTP endpoints** under `/iam/service-accounts/*`: create, list,
489
+ get, patch, delete, `GET /iam/service-accounts/:id/permissions`,
490
+ `POST/DELETE /iam/roles/:id/bind/service-account`, and
491
+ `POST/DELETE /iam/permissions/bind/service-account`. All require the
492
+ new `fortress:createServiceAccount` / `fortress:viewServiceAccounts` /
493
+ `fortress:manageServiceAccount` permissions.
494
+ - **api-key plugin authenticates service accounts.** Keys can now belong
495
+ to either a USER or a SERVICE_ACCOUNT. The plugin implements the new
496
+ `resolvePrincipal` plugin capability (see below) so incoming requests
497
+ with `Authorization: ApiKey <key>` or `X-API-Key: <key>` headers
498
+ resolve to a subject principal end-to-end through RBAC. Deleting a
499
+ service account hard-deletes its api keys via the new multi-listener
500
+ IAM observer (no revocation step — the keys are gone).
501
+ - **The permission-resolution bug is fixed.** The legacy
502
+ `internal-adapter.getUserPermissions` hardcoded
503
+ `subject_type IN ('USER', 'GROUP')` in both the rawQuery and fallback
504
+ paths, silently dropping any `SERVICE_ACCOUNT` role binding. The new
505
+ `getSubjectPermissions(subject, tenantId?)` resolves permissions for
506
+ any subject type — users still walk their group memberships, service
507
+ accounts and other non-user subjects don't (they can't be group
508
+ members). The `direct_permission_binding` schema comment was also
509
+ updated from `'USER' | 'GROUP'` to
510
+ `'USER' | 'GROUP' | 'SERVICE_ACCOUNT'`.
511
+ - **Inactive service accounts authenticate to nothing.** Both
512
+ `resolveApiKey` and `getSubjectPermissions` short-circuit when
513
+ `service_account.isActive = false`, so deactivating a service account
514
+ immediately stops its keys from authenticating and drops any cached
515
+ permissions on the next check.
516
+ - **New plugin capability: `resolvePrincipal`.** Plugins can now attach
517
+ non-JWT credential mechanisms to the request pipeline:
518
+ ```ts
519
+ resolvePrincipal?: (
520
+ request: Request,
521
+ ctx: PluginContext,
522
+ ) => Promise<{ subject: Subject; claims?: TokenClaims } | null>;
523
+ ```
524
+ Resolvers are tried in registration order; the first non-null return
525
+ wins. If none resolve, the JWT fallback runs as before. The api-key
526
+ plugin implements this; future credential plugins (OAuth client
527
+ credentials, mTLS, signed JWT assertions) can implement it the same way
528
+ without core changes.
529
+ - **New `Subject` type.** `{ type: SubjectType; id: number }` —
530
+ discriminated principal shape threaded through
531
+ `IamService.checkPermission`, `enforceFortressPermission`,
532
+ `DispatchAuth`, and `PluginRouteContext`. Enables a single abstraction
533
+ across users, service accounts, and any future subject kinds.
534
+ - **New `TokenClaims.subjectType` field.** JWTs now carry the subject type
535
+ alongside `sub`. Verifier defaults missing `subjectType` to `'USER'` so
536
+ tokens minted before this change keep verifying until they expire.
537
+ - **`IamService.addIamObserver`** replaces `setIamObserver` (single-slot →
538
+ multi-listener). Enables plugins to coexist — the audit-log plugin keeps
539
+ its listener, and the api-key plugin attaches a cascade listener for
540
+ `SERVICE_ACCOUNT_DELETED`. Old `setIamObserver` is removed.
541
+ - **Audit event types `SERVICE_ACCOUNT_CREATED`, `SERVICE_ACCOUNT_UPDATED`,
542
+ `SERVICE_ACCOUNT_DELETED`** added to the audit-log plugin's
543
+ `AuditEventType` union.
544
+ - **`api-key` plugin now ships self-service HTTP routes — opt-in.** Pass
545
+ `apiKey({ routes: true })` to mount four endpoints under `/api-key/keys/*`:
546
+ `POST /api-key/keys` (create), `GET /api-key/keys` (list), `DELETE
547
+ /api-key/keys/:id` (revoke), `POST /api-key/keys/:id/rotate` (rotate). All
548
+ require a bearer token; the authenticated caller can only manage their own
549
+ keys — a body-supplied `userId` is ignored in favor of `ctx.userId` so
550
+ clients cannot forge keys for other users. The programmatic API on
551
+ `fortress.plugins['api-key']` is always available regardless of the flag.
552
+ - **`admin` plugin now exposes admin-side api-key management routes —
553
+ opt-in.** Pass `admin({ apiKeyRoutes: true })` alongside `apiKey()` to
554
+ mount `GET /admin/users/:userId/api-keys` and `DELETE
555
+ /admin/users/:userId/api-keys/:id`. Both are guarded by the
556
+ `apiKey:manage` permission, which bootstrap auto-discovers into the
557
+ `fortress-admin` role when the routes are mounted. Typical use:
558
+ responding to leaked keys or auditing a user's active surface.
559
+ - **Design convention: plugins that ship HTTP routes are moving to
560
+ opt-in.** New HTTP-ish surfaces (starting with `api-key` and the new
561
+ `admin` api-key routes) gate route mounting behind a boolean config
562
+ flag that defaults to `false`. The programmatic methods on
563
+ `fortress.plugins[name]` stay always-on, so consumers keep the library
564
+ behavior they had before and can explicitly opt in to the URL
565
+ namespace. Existing plugins (`webauthn`, `two-factor`, `oauth`,
566
+ `email-verification`, `magic-link`, `webhook`, `social-login`,
567
+ `openapi`) will migrate in a follow-up PR.
568
+ - **Plugin HTTP route handlers now receive a `PluginRouteContext`** as a
569
+ second argument: `(input, ctx) => ...`. `ctx` carries the verified
570
+ caller (`userId`, `claims`), request metadata (`meta` with `ipAddress`
571
+ / `userAgent`), and the raw `Request` — the data the dispatcher already
572
+ had but was dropping before calling plugin methods. Plugin handlers
573
+ that need to know who is calling them no longer have to trust a
574
+ client-supplied body field or re-verify the JWT themselves.
575
+ - The new `PluginRouteContext` type is exported from
576
+ `@bajustone/fortress` (re-exported from `src/core/plugin.ts`).
577
+ - Existing handlers that ignore the second argument still work — the
578
+ new argument is additive.
579
+ - `admin.bootstrap` now uses `ctx.userId` as the default target. Only
580
+ superadmins (configured via `adminUserIds`) may pass `body.userId`
581
+ to bootstrap another user. Programmatic callers
582
+ (`fortress.plugins.admin.bootstrap({ userId })`) are unaffected —
583
+ when `ctx` is absent the handler still trusts `body.userId`.
584
+ - The `POST /iam/admin/bootstrap` body schema no longer marks `userId`
585
+ as required.
586
+
587
+ ### Migration summary (SERVICE_ACCOUNT / Subject release)
588
+
589
+ If you're upgrading from the previous release, the minimum set of edits:
590
+
591
+ ```ts
592
+ // 1. Permission checks: wrap userId in a Subject
593
+ - fortress.iam.checkPermission(userId, 'post', 'read');
594
+ + fortress.iam.checkPermission({ type: 'USER', id: userId }, 'post', 'read');
595
+
596
+ // 2. getUserPermissions → getPermissionsForSubject
597
+ - fortress.iam.getUserPermissions(userId, tenantId);
598
+ + fortress.iam.getPermissionsForSubject({ type: 'USER', id: userId }, tenantId);
599
+
600
+ // 3. api-key plugin: userId → subject
601
+ - fortress.plugins['api-key'].createKey({ userId, name });
602
+ + fortress.plugins['api-key'].createKey({ subject: { type: 'USER', id: userId }, name });
603
+ // Same shape change for listKeys/revokeKey/rotateKey.
604
+
605
+ // 4. api-key plugin config
606
+ - apiKey({ maxKeysPerUser: 5 });
607
+ + apiKey({ maxKeysPerSubject: 5 });
608
+
609
+ // 5. resolveKey return shape
610
+ - const { userId } = await fortress.plugins['api-key'].resolveKey(raw);
611
+ + const { subject } = await fortress.plugins['api-key'].resolveKey(raw);
612
+
613
+ // 6. IAM observer registration
614
+ - fortress.iam.setIamObserver(listener);
615
+ + fortress.iam.addIamObserver(listener);
616
+ ```
617
+
618
+ And one schema migration for installs that manage DDL manually:
619
+
620
+ ```sql
621
+ -- api_key: polymorphic ownership (USER | SERVICE_ACCOUNT)
622
+ ALTER TABLE fortress_api_key ADD COLUMN subject_type varchar(20) NOT NULL DEFAULT 'USER';
623
+ ALTER TABLE fortress_api_key RENAME COLUMN user_id TO subject_id;
624
+ ALTER TABLE fortress_api_key DROP CONSTRAINT IF EXISTS fortress_api_key_user_id_fkey;
625
+ CREATE INDEX api_key_subject_idx ON fortress_api_key (subject_type, subject_id);
626
+
627
+ -- new service_accounts table (Postgres; adjust types for SQLite/MySQL)
628
+ CREATE TABLE fortress_service_account (
629
+ id serial PRIMARY KEY,
630
+ name varchar(100) NOT NULL UNIQUE,
631
+ display_name varchar(255),
632
+ description text,
633
+ is_active boolean NOT NULL DEFAULT true,
634
+ created_at timestamp NOT NULL DEFAULT NOW(),
635
+ updated_at timestamp NOT NULL DEFAULT NOW()
636
+ );
637
+ ```
638
+
639
+ Full per-field details below.
640
+
641
+ ### Changed (breaking)
642
+ - **`IamService.checkPermission` and `getUserPermissions` are now
643
+ subject-aware.** The first argument is a `Subject`, not a bare `userId`.
644
+ The old `getUserPermissions(userId, tenantId?)` is removed — use
645
+ `getPermissionsForSubject({ type: 'USER', id: userId }, tenantId?)` for
646
+ users or `{ type: 'SERVICE_ACCOUNT', id }` for service accounts.
647
+ ```ts
648
+ // Before
649
+ fortress.iam.checkPermission(userId, 'post', 'read');
650
+ fortress.iam.getUserPermissions(userId, tenantId);
651
+ // After
652
+ fortress.iam.checkPermission({ type: 'USER', id: userId }, 'post', 'read');
653
+ fortress.iam.getPermissionsForSubject({ type: 'USER', id: userId }, tenantId);
654
+ ```
655
+ - **`api-key` plugin method signatures now take a `subject` instead of a
656
+ `userId`.** The polymorphic `(subject_type, subject_id)` schema mirrors
657
+ `role_binding` / `direct_permission_binding`, and lets keys belong to
658
+ service accounts as well as users.
659
+ ```ts
660
+ // Before
661
+ await fortress.plugins['api-key'].createKey({ userId, name });
662
+ await fortress.plugins['api-key'].listKeys({ userId });
663
+ await fortress.plugins['api-key'].revokeKey({ userId, id });
664
+ await fortress.plugins['api-key'].rotateKey({ userId, id });
665
+ // After
666
+ await fortress.plugins['api-key'].createKey({ subject: { type: 'USER', id: userId }, name });
667
+ await fortress.plugins['api-key'].listKeys({ subject: { type: 'USER', id: userId } });
668
+ await fortress.plugins['api-key'].revokeKey({ subject: { type: 'USER', id: userId }, id });
669
+ await fortress.plugins['api-key'].rotateKey({ subject: { type: 'USER', id: userId }, id });
670
+ ```
671
+ `resolveKey(rawKey)` now returns `{ subject, scopes }` instead of
672
+ `{ userId, scopes }`.
673
+ - **`ApiKeyConfig.maxKeysPerUser` → `maxKeysPerSubject`.** The config
674
+ knob now counts keys per subject (either USER or SERVICE_ACCOUNT)
675
+ rather than per user specifically.
676
+ - **`api_key` schema migration.** `user_id` is replaced by
677
+ `(subject_type, subject_id)`. For installs that manage their own
678
+ schema, apply the following SQL:
679
+ ```sql
680
+ ALTER TABLE fortress_api_key ADD COLUMN subject_type varchar(20) NOT NULL DEFAULT 'USER';
681
+ ALTER TABLE fortress_api_key RENAME COLUMN user_id TO subject_id;
682
+ ALTER TABLE fortress_api_key DROP CONSTRAINT IF EXISTS fortress_api_key_user_id_fkey;
683
+ CREATE INDEX api_key_subject_idx ON fortress_api_key (subject_type, subject_id);
684
+ ```
685
+ - **`IamService.setIamObserver` → `addIamObserver`.** Semantics change
686
+ from single-slot overwrite to append-only. Multiple plugins (audit log,
687
+ api-key cascade, etc.) can now attach listeners without clobbering
688
+ each other.
689
+ - **`enforceFortressPermission` takes a `Subject`** instead of
690
+ `userId: number | undefined`. `PermissionEnforcement.checkPermission`
691
+ signature changes likewise. Custom adapters that call these directly
692
+ need to pass a `Subject`.
693
+ - **`DispatchAuth.subject`** is the canonical principal field.
694
+ `DispatchAuth.userId` remains as a convenience alias (`subject.id` when
695
+ `subject.type === 'USER'`, otherwise `undefined`) so most existing
696
+ handlers keep working.
697
+ - **`POST /iam/check` body shape accepts `{ subject, resource, action }`**
698
+ in addition to the legacy `{ userId, resource, action }`. The dispatcher
699
+ prefers `subject` when present; the userId form is kept for backwards
700
+ compatibility until the next major and will be removed.
701
+ - **`POST /webauthn/register/options` and `POST /webauthn/register/verify`
702
+ no longer accept `userId` in the request body.** The passkey is always
703
+ registered against the authenticated caller (`ctx.userId`). Clients
704
+ passing a different `userId` in the body were enabling a
705
+ privilege-escalation bug — any bearer-authenticated user could register
706
+ a credential on another user's account. Programmatic callers of
707
+ `fortress.plugins.webauthn.generateRegistrationOptions` /
708
+ `verifyRegistration` must now supply a `PluginRouteContext` as the
709
+ second argument instead of `{ userId }` in the first.
710
+ Before:
711
+ ```ts
712
+ await fortress.plugins.webauthn.generateRegistrationOptions({ userId });
713
+ ```
714
+ After:
715
+ ```ts
716
+ await fortress.plugins.webauthn.generateRegistrationOptions(
717
+ {},
718
+ { userId, request: new Request('http://localhost') },
719
+ );
720
+ ```
721
+
722
+ ### Added
723
+ - **`validateRequest` is now a public export** from `@bajustone/fortress`.
724
+ Framework-agnostic validation primitive that walks an `EndpointInput`,
725
+ aggregates body+query+params issues, and throws
726
+ `FortressError('VALIDATION_ERROR', 422)`. Use it from any runtime — Next.js
727
+ route handlers, SvelteKit `+server.ts`, Bun.serve, Deno, edge functions, or
728
+ custom middleware — to validate consumer-defined routes with the exact
729
+ shape fortress's own dispatch uses internally.
730
+ - **`vBody` / `vParam` / `vQuery` for SvelteKit**
731
+ (`@bajustone/fortress/sveltekit`). Take a `RequestEvent`, validate against
732
+ a Standard Schema, return the parsed value or throw
733
+ `FortressError('VALIDATION_ERROR', 422)`. Drop-in for `+server.ts` handlers.
734
+ - **`vBody` / `vParam` / `vQuery` for Express**
735
+ (`@bajustone/fortress/express`). Take a structurally typed Express
736
+ `Request`, validate against a Standard Schema, return the parsed value or
737
+ throw `FortressError('VALIDATION_ERROR', 422)`. The Express
738
+ `createErrorHandler` already maps the thrown error to a 422 JSON response.
739
+ - **SvelteKit adapter** at `@bajustone/fortress/sveltekit`. Single
740
+ `createSvelteKitHandle(fortress)` hook for `hooks.server.ts`. Intercepts
741
+ Fortress paths and delegates to `fortress.handleRequest`. Auto-refreshes
742
+ expired access tokens during SSR loads. Populates `event.locals.fortress`
743
+ for user routes. Form-action helpers (`fortressActions.login` /
744
+ `logout` / `register` / `refresh`). Optional catch-all `+server.ts`
745
+ escape hatch via `toSvelteKitHandler(fortress)`.
746
+ - **`fortress.handleRequest(request: Request): Promise<Response>`** —
747
+ framework-agnostic HTTP entry point on every Fortress instance. Composes
748
+ plugin middleware → token verification → fortress-managed RBAC →
749
+ validation → endpoint dispatch → cookie attachment. All adapters delegate
750
+ to it; future runtimes (Cloudflare Workers, Deno Deploy, etc.) only need a
751
+ ~10-line wrapper.
752
+ - **`FortressConfig.cookies`** — `__Host-` prefixed access/refresh cookie
753
+ names with `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/` defaults in
754
+ production. Auto-relaxes (drops `__Host-` and `Secure`) in
755
+ `NODE_ENV !== 'production'` so localhost over HTTP works.
756
+ - **`fortress.runPluginMiddleware`**, **`fortress.extractAccessToken`**,
757
+ **`fortress.serializeAuthCookies`**, **`fortress.cookies`** — auxiliary
758
+ HTTP helpers exposed on the Fortress instance for adapters that compose
759
+ custom flows on user-owned routes.
760
+ - **`mountFortress(app, fortress)`** in the Hono and Express adapters — new
761
+ modern entry point that delegates Fortress-managed paths to
762
+ `fortress.handleRequest` via a single middleware. Replaces the old split
763
+ surface (`createHonoMiddleware` + `mountPluginRoutes`).
764
+
765
+ ### Changed
766
+ - **BREAKING: Hono `vBody` / `vParam` / `vQuery` now validate at runtime.**
767
+ Previously they were type-only — the schema parameter was used purely for
768
+ TypeScript inference and the deleted `createValidationMiddleware` did the
769
+ actual validation upstream. With that middleware gone, the helpers now
770
+ call `schema['~standard'].validate()` themselves and throw
771
+ `FortressError('VALIDATION_ERROR', 422)` on failure (the same shape every
772
+ fortress-managed endpoint produces). **`vParam` and `vQuery` are now
773
+ `async`** — Standard Schema's `validate()` may return a promise, so all
774
+ three helpers had to align. Migration: add `await` to any sync
775
+ destructuring call sites (`const { id } = vParam(c, P)` →
776
+ `const { id } = await vParam(c, P)`).
777
+ - The Hono `createErrorHandler` now delegates to the framework-agnostic
778
+ `errorToResponse` from core so the FortressError → HTTP mapping
779
+ (`Retry-After`, sanitized 500s, etc.) stays in one place.
780
+ - The Hono `createAuthMiddleware` and Express `createAuthMiddleware` now
781
+ use `fortress.extractAccessToken` (cookie-first, `Authorization: Bearer`
782
+ fallback), so the same adapter serves both browsers and API clients.
783
+ - The Hono and Express RBAC middleware are simplified — they only handle
784
+ user-route `routeMap` lookups now. The default-deny logic for
785
+ Fortress-managed paths moved into core (`src/core/http/fortress-rbac.ts`)
786
+ and runs inside `fortress.handleRequest`.
787
+
788
+ ### Removed (breaking)
789
+ - **Deleted `mountPluginRoutes`** from the Hono adapter
790
+ (`src/hono/plugin-routes.ts`). Replaced by `mountFortress`, which
791
+ delegates to `fortress.handleRequest` and handles plugin routes
792
+ (OAuth, OpenAPI, etc.) automatically.
793
+ - **Deleted `createValidationMiddleware`** from the Hono and Express
794
+ adapters (`src/hono/validation-middleware.ts`,
795
+ `src/express/validation-middleware.ts`). Validation now runs
796
+ automatically inside `fortress.handleRequest` for every Fortress-managed
797
+ endpoint. For custom user routes, use the new runtime-validating
798
+ `vBody` / `vParam` / `vQuery` helpers (Hono / SvelteKit / Express) or
799
+ the framework-agnostic `validateRequest` export — see the Unreleased
800
+ section above.
801
+ - **Deleted `mountFortressRoutes` and `mountPluginRoutes`** from the
802
+ Express adapter (`src/express/routes.ts`). Same migration: use
803
+ `mountFortress(app, fortress)` from `@bajustone/fortress/express`.
804
+ - **Removed `RbacOptions.allowUnmappedFortressPaths`** from the Hono and
805
+ Express adapters. The fortress-path default-deny now lives in core, so
806
+ the adapter-side opt-out is gone. Core's default-deny is non-negotiable
807
+ (it's part of the security contract).
808
+ - **Migration**: replace
809
+ `mountPluginRoutes(app, fortress)` → `mountFortress(app, fortress)`,
810
+ delete any `createValidationMiddleware(...)` calls, and rely on the
811
+ automatic validation inside `fortress.handleRequest`.
812
+
813
+ ## [0.0.28] - 2026-04-09
814
+
815
+ ### Fixed
816
+ - fix jsr
817
+
818
+ ## [0.0.27] - 2026-04-09
819
+
820
+ ### Fixed
821
+ - fix jsr
822
+
823
+ ## [0.0.26] - 2026-04-09
824
+
825
+ ### Fixed
826
+ - fix jsr
827
+
828
+ ## [0.0.25] - 2026-04-09
829
+
830
+ ### Changed
831
+ - chore(jsr): eliminate all 62 slow-type errors by typing `fortressSchema` /
832
+ `fortressPgSchema` as `Record<string, AnySQLiteTable>` /
833
+ `Record<string, AnyPgTable>`. The drizzle adapter is unaffected (it already
834
+ accesses tables generically), but consumers who relied on column-level
835
+ inference from `fortressSchema.users.email` style access will now see the
836
+ loose `AnySQLiteTable`/`AnyPgTable` type. Workaround: declare your own
837
+ typed Drizzle tables and pass them via `createDrizzleAdapter(db, { tables })`.
838
+ - chore(jsr): drop `--allow-slow-types` from `publish:dry` and the publish
839
+ workflow so future regressions fail loudly. JSR will now ship proper
840
+ `.d.ts` files for Node consumers.
841
+ - docs: add `@module` JSDoc to every JSR entrypoint (22 files) and document
842
+ every re-export in `src/index.ts`.
843
+
844
+ ### Added
845
+ - chore(jsr): add `description`, `runtimeCompat` (node/deno/bun/workerd), and
846
+ `publish.exclude` to `jsr.json`. The published tarball no longer bundles
847
+ test files, vitest/tsup configs, `.github`, `examples`, `docs`, `scripts`,
848
+ or local tooling files.
849
+
850
+ ## [0.0.24] - 2026-04-07
851
+
852
+ ### Fixed
853
+ - fix: prevent Zod schemas from being misidentified as FortressSchema
854
+
855
+ ## [0.0.23] - 2026-04-07
856
+
857
+ ### Added
858
+ - feat(hono): add typed validation helpers and unmatched route warnings
859
+
860
+ ## [0.0.23] - 2026-04-08
861
+
862
+ ### Added
863
+ - feat: add typed validation helpers (`vBody`, `vParam`, `vQuery`) to Hono adapter — zero-cost type-safe request extraction using Standard Schema V1 inference, works with Zod, Valibot, ArkType, or fortress built-in schemas
864
+ - feat: export `InferOutput` utility type from `@bajustone/fortress/hono`
865
+
866
+ ## [0.0.22] - 2026-04-07
867
+
868
+ ### Fixed
869
+ - fix: use relative URL in openapi plugin Scalar UI for prefix compatibility
870
+
871
+ ## [0.0.21] - 2026-04-07
872
+
873
+ ### Added
874
+ - docs: sync documentation with source code
875
+
876
+
877
+ ### Fixed
878
+ - docs: sync README, SECURITY.md, docs/security.md, and architecture.md with source code
879
+ - WebAuthn plugin is fully implemented, not a stub — updated README, CLAUDE.md
880
+ - Fixed plugin count from 12 to 15 in architecture.md
881
+ - Fixed `breachedCacheTtlMs` default from 300000 to 86400000 in docs/security.md
882
+ - Fixed account lockout config names (`maxFailedAttempts`, `lockoutDurationSeconds`, `maxLockoutSeconds`) in docs/security.md
883
+ - Fixed rate limit config shape (`maxPerIp`, `maxPerAccount`, `windowSeconds`) in docs/security.md
884
+ - Fixed CSRF middleware import (`createCsrfMiddleware`) in docs/security.md
885
+ - Fixed supported version from 0.1.x to 0.0.x in SECURITY.md
886
+ - Synced jsr.json version to 0.0.20
887
+
888
+ ## [0.0.20] - 2026-04-07
889
+
890
+ ### Added
891
+ - feat: complete admin plugin with all IAM endpoints, type safety, and bug fixes
892
+
893
+ ## [0.0.20] - 2026-04-07
894
+
895
+ ### Added
896
+ - Admin plugin now mounts all 16 core IAM endpoints (roles CRUD, role/group bindings, permission bindings, getUserPermissions, checkPermission)
897
+ - `POST /auth/users` — admin-initiated user creation with `fortress:manageUsers` permission
898
+ - `POST /iam/sync` — push/pull resource sync endpoint
899
+ - `safeInt()` / `requireInt()` helpers for safe numeric input coercion in admin plugin
900
+ - `updateUser` now supports `password` field — hashed via configured `PasswordHasher` with password policy validation
901
+
902
+ ### Changed
903
+ - `PluginContext.auth` and `.iam` now typed as `AuthService` and `IamService` (was `Record<string, Function>`)
904
+ - Removed `as any` casts throughout admin plugin — all service calls are now type-checked
905
+ - All `Number(body.id)` calls replaced with `requireInt()` to prevent NaN propagation to database queries
906
+ - Endpoint deduplication in `fortress.ts` — plugin routes take priority over core definitions by `method+path`
907
+
908
+ ### Fixed
909
+ - `mountPluginRoutes` now returns HTML with `c.html()` instead of `c.json()` for plugin methods returning HTML strings (fixes broken Scalar UI)
910
+ - CHANGELOG formatting errors in v0.0.19 and v0.0.16 entries
911
+
912
+ ## [0.0.19] - 2026-04-07
913
+
914
+ ### Added
915
+ - fix: pass path params to plugin route handlers in Hono adapter
916
+
917
+ ## [0.0.18] - 2026-04-07
918
+
919
+ ### Added
920
+ - feat: Standard Schema V1 support with typed schemas and runtime validation
921
+ - feat: add admin CRUD endpoints for users, roles, groups, and permissions
922
+
923
+ ## [0.0.17] - 2026-04-07
924
+
925
+ ### Added
926
+ - feat: security-aware default deny and endpoint permission declarations
927
+
928
+ ## [0.0.16] - 2026-04-07
929
+
930
+ ### Added
931
+ - feat: add admin plugin, plugin middleware wiring, and default deny for fortress routes
932
+ - feat(openapi): add additionalEndpoints and convertRoutes for unified spec generation
933
+
934
+
935
+ ### Added
936
+ - **Admin CRUD endpoints** — 15 new endpoints in the admin plugin for managing users, roles, groups, and permissions
937
+ - Auth admin: `GET /auth/users`, `GET /auth/users/:id`, `PUT /auth/users/:id`, `DELETE /auth/users/:id`
938
+ - IAM admin: `GET /iam/roles/:id`, `PUT /iam/roles/:id`, `GET /iam/groups`, `GET /iam/groups/:id`, `PUT /iam/groups/:id`, `DELETE /iam/groups/:id`, `GET /iam/groups/:id/users`, `GET /iam/permissions`, `POST /iam/permissions`, `DELETE /iam/permissions/:id`, `POST /iam/roles/:id/permissions`
939
+ - **Auth service admin methods** — `listUsers`, `getUserById`, `updateUser`, `deleteUser` on `AuthService`
940
+ - **IAM service admin methods** — `getRole`, `updateRole`, `listGroups`, `getGroup`, `updateGroup`, `deleteGroup`, `getGroupUsers`, `listPermissions`, `createPermission`, `deletePermission`, `addPermissionToRole` on `IamService`
941
+ - `iam` property on `PluginContext` — plugins can now access the IAM service via `ctx.iam`
942
+ - `like` operator support in Drizzle adapter
943
+ - **Standard Schema V1 support** — `obj()`, `str()`, `int()`, etc. now implement Standard Schema, providing runtime validation + TypeScript type inference + JSON Schema for OpenAPI from a single definition
944
+ - `FortressSchema<T>`, `Infer<T>`, `StandardSchemaV1.InferOutput` types for type extraction
945
+ - Built-in JSON Schema validator for fortress schemas' `~standard.validate()`
946
+ - New schema helpers: `nullType()`, `record()`, `recordOf()`
947
+ - `isStandardSchema()`, `isFortressSchema()`, `extractJsonSchema()` utilities
948
+ - `endpoint().body()`, `.query()`, `.params()` accept both `FortressSchema` and external Standard Schema (Zod, Valibot, ArkType)
949
+ - `/auth/users` added to `FORTRESS_AUTH_PROTECTED` for default-deny
950
+
951
+ ### Changed
952
+ - Plugin route dispatch now merges path params into body, enabling plugin routes with `:id` params
953
+ - GET request handlers now receive query params (previously `undefined`) in both Hono and Express adapters
954
+ - Admin plugin superadmin middleware now covers `/auth/users/*` in addition to `/iam/*`
955
+
956
+ ### Previously Released
957
+
958
+ - **Admin plugin** (`@bajustone/fortress/plugins/admin`) — protects IAM routes with `fortress:*` permissions, provides bootstrap endpoint to assign first admin, and lists available resources/roles
959
+ - **Plugin middleware wiring** — `MiddlewareDefinition` from plugins is now executed in the request pipeline via `pluginMiddleware.beforeAuth`, `pluginMiddleware.afterAuth`, and `pluginMiddleware.afterRbac`
960
+ - **Endpoint permission declarations** — `EndpointMeta.permission` field and `.permission(resource, action)` builder method allow endpoints to declare IAM requirements
961
+ - `GET /iam/resources` endpoint — lists all available resources and their actions
962
+ - `GET /iam/roles` endpoint — lists all roles
963
+ - `POST /iam/admin/bootstrap` endpoint — auto-discovers all declared permissions from endpoint definitions and creates fortress-admin role
964
+ - `getResources()` and `getRoles()` methods on `IamService`
965
+ - `createPluginMiddleware()` for Hono adapter
966
+ - `createExpressPluginMiddleware()` for Express adapter
967
+ - **Security-aware default deny** — RBAC middleware respects endpoint security metadata
968
+ - **Default deny for fortress-owned routes** — RBAC middleware denies unmapped `/iam/*`, `/auth/impersonate`, and plugin-owned routes by default
969
+
970
+ ## [0.0.15] - 2026-04-07
971
+
972
+ ### Added
973
+ - add version lifecycle script
974
+ - `additionalEndpoints` option for OpenAPI plugin — consumers can merge app-specific routes into a single unified spec
975
+ - `convertRoutes` utility in Hono adapter — schema-agnostic converter from `createRoute`-style objects to `EndpointDefinition[]`
976
+
977
+ ## [0.0.14] - 2026-04-07
978
+
979
+ ### Added
980
+ - v0.0.14
981
+ - feat: webauthn plugin
982
+ - openapi
983
+ - support openapi via JSONSchema
984
+ - examples
985
+
986
+ ### Changed
987
+ - update doc/architecture.md
988
+ - improve tests
989
+
990
+ ### Fixed
991
+ - fix read me
992
+
993
+ ## [0.0.13] - 2026-04-07
994
+
995
+ ### Added
996
+ - Core auth: JWT (jose), Argon2id password hashing, refresh token rotation with family tracking
997
+ - Core IAM: resource+action permissions, conditions, deny rules, groups, roles
998
+ - LRU permission cache with TTL and invalidation
999
+ - Plugin system: 8 capabilities (models, hooks, methods, routes, middleware, wrapAdapter, enrichTokenClaims, scopeRules)
1000
+ - Plugins: email-verification, api-key, two-factor, social-login, data-isolation, tenancy, oauth, rate-limit, account-lockout, audit-log, webhook, magic-link, openapi, webauthn (stub)
1001
+ - Declarative endpoint definitions with OpenAPI metadata
1002
+ - JSON Schema builder DSL for fluent schema construction
1003
+ - Drizzle adapter: PostgreSQL, MySQL, SQLite
1004
+ - Hono middleware: auth, RBAC, CSRF, error handler, OpenAPI integration
1005
+ - Express middleware: auth, RBAC, error handler, route mounting
1006
+ - In-memory SQLite testing adapter
1007
+ - Password policy with NIST 800-63B defaults and HIBP breach checking
1008
+ - Session management: list, revoke, revoke all
1009
+ - Token fingerprinting on refresh
1010
+ - Admin impersonation with RFC 8693 `act` claim
1011
+ - Security documentation