@bajustone/fortress 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. package/CHANGELOG.md +1011 -0
  2. package/LICENSE +21 -0
  3. package/README.md +760 -0
  4. package/SECURITY.md +197 -0
  5. package/bin/fortress.ts +667 -0
  6. package/dist/auth-service-BcyQ2PuZ.d.cts +307 -0
  7. package/dist/auth-service-BkzZkWfH.d.ts +307 -0
  8. package/dist/config-B2366s_G.d.ts +665 -0
  9. package/dist/config-GtlVt6ZD.d.cts +665 -0
  10. package/dist/convert-routes-BLCQT57f.d.ts +72 -0
  11. package/dist/convert-routes-BdMvP2_X.d.cts +72 -0
  12. package/dist/crypto.cjs +61 -0
  13. package/dist/crypto.d.cts +36 -0
  14. package/dist/crypto.d.ts +36 -0
  15. package/dist/crypto.js +35 -0
  16. package/dist/drift-BT1wtMDJ.d.cts +22 -0
  17. package/dist/drift-k9LiYpRP.d.ts +22 -0
  18. package/dist/drizzle/pg.cjs +481 -0
  19. package/dist/drizzle/pg.d.cts +15 -0
  20. package/dist/drizzle/pg.d.ts +15 -0
  21. package/dist/drizzle/pg.js +454 -0
  22. package/dist/drizzle.cjs +1442 -0
  23. package/dist/drizzle.d.cts +85 -0
  24. package/dist/drizzle.d.ts +85 -0
  25. package/dist/drizzle.js +1411 -0
  26. package/dist/express.cjs +1357 -0
  27. package/dist/express.d.cts +333 -0
  28. package/dist/express.d.ts +333 -0
  29. package/dist/express.js +1314 -0
  30. package/dist/fetcher.cjs +77 -0
  31. package/dist/fetcher.d.cts +7 -0
  32. package/dist/fetcher.d.ts +7 -0
  33. package/dist/fetcher.js +41 -0
  34. package/dist/fortress-BmSvFfqK.d.cts +1089 -0
  35. package/dist/fortress-uA-_cheR.d.ts +1089 -0
  36. package/dist/hono.cjs +1530 -0
  37. package/dist/hono.d.cts +514 -0
  38. package/dist/hono.d.ts +514 -0
  39. package/dist/hono.js +1483 -0
  40. package/dist/index-CxEkfCA0.d.cts +77 -0
  41. package/dist/index-CxEkfCA0.d.ts +77 -0
  42. package/dist/index-D054pcb-.d.cts +146 -0
  43. package/dist/index-jAMbbUAY.d.ts +146 -0
  44. package/dist/index.cjs +9046 -0
  45. package/dist/index.d.cts +717 -0
  46. package/dist/index.d.ts +717 -0
  47. package/dist/index.js +8935 -0
  48. package/dist/jwt.cjs +255 -0
  49. package/dist/jwt.d.cts +90 -0
  50. package/dist/jwt.d.ts +90 -0
  51. package/dist/jwt.js +227 -0
  52. package/dist/otel.cjs +108 -0
  53. package/dist/otel.d.cts +62 -0
  54. package/dist/otel.d.ts +62 -0
  55. package/dist/otel.js +73 -0
  56. package/dist/plugins/account-lockout.cjs +322 -0
  57. package/dist/plugins/account-lockout.d.cts +42 -0
  58. package/dist/plugins/account-lockout.d.ts +42 -0
  59. package/dist/plugins/account-lockout.js +295 -0
  60. package/dist/plugins/admin.cjs +2378 -0
  61. package/dist/plugins/admin.d.cts +72 -0
  62. package/dist/plugins/admin.d.ts +72 -0
  63. package/dist/plugins/admin.js +2351 -0
  64. package/dist/plugins/api-key.cjs +792 -0
  65. package/dist/plugins/api-key.d.cts +121 -0
  66. package/dist/plugins/api-key.d.ts +121 -0
  67. package/dist/plugins/api-key.js +765 -0
  68. package/dist/plugins/audit-log.cjs +493 -0
  69. package/dist/plugins/audit-log.d.cts +86 -0
  70. package/dist/plugins/audit-log.d.ts +86 -0
  71. package/dist/plugins/audit-log.js +468 -0
  72. package/dist/plugins/data-isolation.cjs +211 -0
  73. package/dist/plugins/data-isolation.d.cts +49 -0
  74. package/dist/plugins/data-isolation.d.ts +49 -0
  75. package/dist/plugins/data-isolation.js +186 -0
  76. package/dist/plugins/email-verification.cjs +273 -0
  77. package/dist/plugins/email-verification.d.cts +44 -0
  78. package/dist/plugins/email-verification.d.ts +44 -0
  79. package/dist/plugins/email-verification.js +246 -0
  80. package/dist/plugins/magic-link.cjs +231 -0
  81. package/dist/plugins/magic-link.d.cts +39 -0
  82. package/dist/plugins/magic-link.d.ts +39 -0
  83. package/dist/plugins/magic-link.js +204 -0
  84. package/dist/plugins/oauth.cjs +1696 -0
  85. package/dist/plugins/oauth.d.cts +406 -0
  86. package/dist/plugins/oauth.d.ts +406 -0
  87. package/dist/plugins/oauth.js +1669 -0
  88. package/dist/plugins/openapi.cjs +1185 -0
  89. package/dist/plugins/openapi.d.cts +6 -0
  90. package/dist/plugins/openapi.d.ts +6 -0
  91. package/dist/plugins/openapi.js +1158 -0
  92. package/dist/plugins/rate-limit/express.cjs +55 -0
  93. package/dist/plugins/rate-limit/express.d.cts +64 -0
  94. package/dist/plugins/rate-limit/express.d.ts +64 -0
  95. package/dist/plugins/rate-limit/express.js +30 -0
  96. package/dist/plugins/rate-limit/hono.cjs +51 -0
  97. package/dist/plugins/rate-limit/hono.d.cts +59 -0
  98. package/dist/plugins/rate-limit/hono.d.ts +59 -0
  99. package/dist/plugins/rate-limit/hono.js +26 -0
  100. package/dist/plugins/rate-limit/sveltekit.cjs +49 -0
  101. package/dist/plugins/rate-limit/sveltekit.d.cts +59 -0
  102. package/dist/plugins/rate-limit/sveltekit.d.ts +59 -0
  103. package/dist/plugins/rate-limit/sveltekit.js +24 -0
  104. package/dist/plugins/rate-limit.cjs +354 -0
  105. package/dist/plugins/rate-limit.d.cts +140 -0
  106. package/dist/plugins/rate-limit.d.ts +140 -0
  107. package/dist/plugins/rate-limit.js +325 -0
  108. package/dist/plugins/social-login.cjs +905 -0
  109. package/dist/plugins/social-login.d.cts +114 -0
  110. package/dist/plugins/social-login.d.ts +114 -0
  111. package/dist/plugins/social-login.js +880 -0
  112. package/dist/plugins/tenancy.cjs +780 -0
  113. package/dist/plugins/tenancy.d.cts +108 -0
  114. package/dist/plugins/tenancy.d.ts +108 -0
  115. package/dist/plugins/tenancy.js +753 -0
  116. package/dist/plugins/two-factor.cjs +555 -0
  117. package/dist/plugins/two-factor.d.cts +67 -0
  118. package/dist/plugins/two-factor.d.ts +67 -0
  119. package/dist/plugins/two-factor.js +526 -0
  120. package/dist/plugins/webauthn.cjs +786 -0
  121. package/dist/plugins/webauthn.d.cts +72 -0
  122. package/dist/plugins/webauthn.d.ts +72 -0
  123. package/dist/plugins/webauthn.js +766 -0
  124. package/dist/plugins/webhook.cjs +819 -0
  125. package/dist/plugins/webhook.d.cts +254 -0
  126. package/dist/plugins/webhook.d.ts +254 -0
  127. package/dist/plugins/webhook.js +789 -0
  128. package/dist/protect-D1v_0upK.d.cts +123 -0
  129. package/dist/protect-DsxV_-dJ.d.ts +123 -0
  130. package/dist/sveltekit.cjs +1258 -0
  131. package/dist/sveltekit.d.cts +420 -0
  132. package/dist/sveltekit.d.ts +420 -0
  133. package/dist/sveltekit.js +1207 -0
  134. package/dist/testing.cjs +4294 -0
  135. package/dist/testing.d.cts +197 -0
  136. package/dist/testing.d.ts +197 -0
  137. package/dist/testing.js +4264 -0
  138. package/dist/types-et0R8tsC.d.cts +217 -0
  139. package/dist/types-et0R8tsC.d.ts +217 -0
  140. package/docs/adapter-typed-helpers.md +192 -0
  141. package/docs/admin-recipes.md +227 -0
  142. package/docs/architecture.md +434 -0
  143. package/docs/ci/github-actions.yml +59 -0
  144. package/docs/ci.md +109 -0
  145. package/docs/compatibility.md +115 -0
  146. package/docs/deployment.md +305 -0
  147. package/docs/hardening.md +118 -0
  148. package/docs/host-owned-routes.md +154 -0
  149. package/docs/migrations/0001-schema-version.md +54 -0
  150. package/docs/migrations/0002-initial-schema.md +84 -0
  151. package/docs/migrations/upgrade-guide.md +160 -0
  152. package/docs/observability.md +394 -0
  153. package/docs/plugins/account-lockout.md +131 -0
  154. package/docs/plugins/admin.md +402 -0
  155. package/docs/plugins/api-key.md +327 -0
  156. package/docs/plugins/audit-log.md +261 -0
  157. package/docs/plugins/data-isolation.md +186 -0
  158. package/docs/plugins/email-verification.md +121 -0
  159. package/docs/plugins/magic-link.md +123 -0
  160. package/docs/plugins/oauth.md +544 -0
  161. package/docs/plugins/openapi.md +250 -0
  162. package/docs/plugins/rate-limit.md +223 -0
  163. package/docs/plugins/social-login.md +337 -0
  164. package/docs/plugins/tenancy.md +122 -0
  165. package/docs/plugins/two-factor.md +191 -0
  166. package/docs/plugins/webauthn.md +188 -0
  167. package/docs/plugins/webhook.md +242 -0
  168. package/docs/policy-as-code.md +156 -0
  169. package/docs/route-manifest.md +46 -0
  170. package/docs/security.md +261 -0
  171. package/docs/threat-model.md +161 -0
  172. package/examples/README.md +119 -0
  173. package/examples/express-app/index.ts +747 -0
  174. package/examples/hono-app/docker-compose.yml +14 -0
  175. package/examples/hono-app/index.ts +1009 -0
  176. package/examples/policy/fortress.policy.json +47 -0
  177. package/examples/sveltekit-app/README.md +125 -0
  178. package/examples/sveltekit-app/src/app.d.ts +16 -0
  179. package/examples/sveltekit-app/src/hooks.server.ts +23 -0
  180. package/examples/sveltekit-app/src/lib/server/fortress.ts +81 -0
  181. package/examples/sveltekit-app/src/routes/api/echo/[id]/+server.ts +32 -0
  182. package/examples/sveltekit-app/src/routes/api/fortress/[...path]/+server.ts +15 -0
  183. package/examples/sveltekit-app/src/routes/dashboard/+page.server.ts +20 -0
  184. package/examples/sveltekit-app/src/routes/login/+page.server.ts +48 -0
  185. package/examples/sveltekit-app/src/routes/oauth/consent/+page.server.ts +69 -0
  186. package/examples/sveltekit-app/src/routes/oauth/consent/+page.svelte +83 -0
  187. package/migrations/pg/0001_schema_version.down.sql +2 -0
  188. package/migrations/pg/0001_schema_version.sql +8 -0
  189. package/migrations/pg/0002_initial_schema.down.sql +36 -0
  190. package/migrations/pg/0002_initial_schema.sql +376 -0
  191. package/migrations/pg/0003_auth_continuation.down.sql +6 -0
  192. package/migrations/pg/0003_auth_continuation.sql +30 -0
  193. package/migrations/pg/0004_tenant_default_unique.down.sql +1 -0
  194. package/migrations/pg/0004_tenant_default_unique.sql +14 -0
  195. package/migrations/pg/0005_hot_indexes_timestamptz.down.sql +71 -0
  196. package/migrations/pg/0005_hot_indexes_timestamptz.sql +71 -0
  197. package/migrations/pg/0006_canonical_email.down.sql +3 -0
  198. package/migrations/pg/0006_canonical_email.sql +8 -0
  199. package/migrations/pg/0007_audit_chain_anchor.down.sql +1 -0
  200. package/migrations/pg/0007_audit_chain_anchor.sql +8 -0
  201. package/migrations/pg/0008_two_factor_hardening.down.sql +8 -0
  202. package/migrations/pg/0008_two_factor_hardening.sql +8 -0
  203. package/migrations/pg/0009_encrypt_totp_secrets.down.sql +2 -0
  204. package/migrations/pg/0009_encrypt_totp_secrets.sql +5 -0
  205. package/migrations/pg/0010_bigint_append_only_ids.down.sql +2 -0
  206. package/migrations/pg/0010_bigint_append_only_ids.sql +23 -0
  207. package/migrations/sqlite/0001_schema_version.down.sql +2 -0
  208. package/migrations/sqlite/0001_schema_version.sql +8 -0
  209. package/migrations/sqlite/0002_initial_schema.down.sql +36 -0
  210. package/migrations/sqlite/0002_initial_schema.sql +376 -0
  211. package/migrations/sqlite/0003_auth_continuation.down.sql +27 -0
  212. package/migrations/sqlite/0003_auth_continuation.sql +45 -0
  213. package/migrations/sqlite/0004_tenant_default_unique.down.sql +1 -0
  214. package/migrations/sqlite/0004_tenant_default_unique.sql +13 -0
  215. package/migrations/sqlite/0005_hot_indexes_timestamptz.down.sql +10 -0
  216. package/migrations/sqlite/0005_hot_indexes_timestamptz.sql +10 -0
  217. package/migrations/sqlite/0006_canonical_email.down.sql +3 -0
  218. package/migrations/sqlite/0006_canonical_email.sql +8 -0
  219. package/migrations/sqlite/0007_audit_chain_anchor.down.sql +1 -0
  220. package/migrations/sqlite/0007_audit_chain_anchor.sql +8 -0
  221. package/migrations/sqlite/0008_two_factor_hardening.down.sql +14 -0
  222. package/migrations/sqlite/0008_two_factor_hardening.sql +7 -0
  223. package/migrations/sqlite/0009_encrypt_totp_secrets.down.sql +2 -0
  224. package/migrations/sqlite/0009_encrypt_totp_secrets.sql +5 -0
  225. package/migrations/sqlite/0010_bigint_append_only_ids.down.sql +1 -0
  226. package/migrations/sqlite/0010_bigint_append_only_ids.sql +2 -0
  227. package/package.json +398 -0
  228. package/src/adapters/database/index.ts +63 -0
  229. package/src/adapters/database/types.ts +22 -0
  230. package/src/changelog-script.test.ts +21 -0
  231. package/src/cli.test.ts +79 -0
  232. package/src/core/auth/auth-admin.test.ts +247 -0
  233. package/src/core/auth/auth-endpoints.ts +558 -0
  234. package/src/core/auth/auth-observer.test.ts +191 -0
  235. package/src/core/auth/auth-service.ts +1464 -0
  236. package/src/core/auth/email.test.ts +17 -0
  237. package/src/core/auth/email.ts +11 -0
  238. package/src/core/auth/hooks.test.ts +251 -0
  239. package/src/core/auth/impersonation.test.ts +179 -0
  240. package/src/core/auth/jwt.test.ts +184 -0
  241. package/src/core/auth/jwt.ts +239 -0
  242. package/src/core/auth/login-timing.test.ts +77 -0
  243. package/src/core/auth/password-policy.test.ts +253 -0
  244. package/src/core/auth/password-policy.ts +220 -0
  245. package/src/core/auth/password.test.ts +42 -0
  246. package/src/core/auth/password.ts +62 -0
  247. package/src/core/auth/post-auth-gate.test.ts +258 -0
  248. package/src/core/auth/post-auth-gate.ts +228 -0
  249. package/src/core/auth/refresh-token.test.ts +86 -0
  250. package/src/core/auth/refresh-token.ts +105 -0
  251. package/src/core/auth/timing-safe.ts +23 -0
  252. package/src/core/config.ts +212 -0
  253. package/src/core/endpoint.ts +149 -0
  254. package/src/core/errors.ts +199 -0
  255. package/src/core/fetcher-interop.test.ts +106 -0
  256. package/src/core/fortress.test.ts +354 -0
  257. package/src/core/fortress.ts +550 -0
  258. package/src/core/http/call.test.ts +148 -0
  259. package/src/core/http/call.ts +149 -0
  260. package/src/core/http/cookie-serialize.test.ts +198 -0
  261. package/src/core/http/cookie-serialize.ts +107 -0
  262. package/src/core/http/csrf.test.ts +140 -0
  263. package/src/core/http/csrf.ts +173 -0
  264. package/src/core/http/dispatch.ts +652 -0
  265. package/src/core/http/error-response.test.ts +69 -0
  266. package/src/core/http/error-response.ts +93 -0
  267. package/src/core/http/fortress-rbac.test.ts +43 -0
  268. package/src/core/http/fortress-rbac.ts +127 -0
  269. package/src/core/http/handle-request.test.ts +441 -0
  270. package/src/core/http/handle-request.ts +354 -0
  271. package/src/core/http/match.test.ts +122 -0
  272. package/src/core/http/match.ts +126 -0
  273. package/src/core/http/outbound.test.ts +34 -0
  274. package/src/core/http/outbound.ts +105 -0
  275. package/src/core/http/plugin-middleware.ts +67 -0
  276. package/src/core/http/principal.test.ts +345 -0
  277. package/src/core/http/principal.ts +86 -0
  278. package/src/core/http/protect.test.ts +220 -0
  279. package/src/core/http/protect.ts +394 -0
  280. package/src/core/http/token-extraction.test.ts +59 -0
  281. package/src/core/http/token-extraction.ts +53 -0
  282. package/src/core/iam/explain.test.ts +177 -0
  283. package/src/core/iam/explain.ts +302 -0
  284. package/src/core/iam/iam-endpoints.ts +779 -0
  285. package/src/core/iam/iam-service.test.ts +829 -0
  286. package/src/core/iam/iam-service.ts +1137 -0
  287. package/src/core/iam/permission-cache.test.ts +115 -0
  288. package/src/core/iam/permission-cache.ts +71 -0
  289. package/src/core/iam/permission-check-observer.test.ts +90 -0
  290. package/src/core/iam/permission-evaluator.test.ts +291 -0
  291. package/src/core/iam/permission-evaluator.ts +198 -0
  292. package/src/core/iam/permission-sync.test.ts +166 -0
  293. package/src/core/iam/permission-sync.ts +192 -0
  294. package/src/core/iam/resource-sync.test.ts +70 -0
  295. package/src/core/iam/resource-sync.ts +182 -0
  296. package/src/core/iam/service-account-http.test.ts +529 -0
  297. package/src/core/iam/service-account.test.ts +282 -0
  298. package/src/core/internal-adapter.test.ts +389 -0
  299. package/src/core/internal-adapter.ts +435 -0
  300. package/src/core/json-schema.ts +50 -0
  301. package/src/core/manifest/__snapshots__/route-manifest.test.ts.snap +192 -0
  302. package/src/core/manifest/drift.ts +103 -0
  303. package/src/core/manifest/route-manifest.test.ts +142 -0
  304. package/src/core/manifest/route-manifest.ts +154 -0
  305. package/src/core/migrate.test.ts +72 -0
  306. package/src/core/migrations/engine.test.ts +308 -0
  307. package/src/core/migrations/engine.ts +548 -0
  308. package/src/core/migrations/migrations.ts +1771 -0
  309. package/src/core/migrations/pg-migration.integration-test.ts +353 -0
  310. package/src/core/migrations/upgrade-fixture.test.ts +378 -0
  311. package/src/core/observability/db-instrumentation.ts +205 -0
  312. package/src/core/observability/listener-list.test.ts +124 -0
  313. package/src/core/observability/listener-list.ts +73 -0
  314. package/src/core/observability/logger.test.ts +43 -0
  315. package/src/core/observability/logger.ts +49 -0
  316. package/src/core/observability/spans.test.ts +193 -0
  317. package/src/core/observability/types.ts +80 -0
  318. package/src/core/openapi-drift.test.ts +41 -0
  319. package/src/core/openapi.ts +47 -0
  320. package/src/core/plugin-methods-map.ts +75 -0
  321. package/src/core/plugin-runner.test.ts +233 -0
  322. package/src/core/plugin-runner.ts +276 -0
  323. package/src/core/plugin.ts +210 -0
  324. package/src/core/policy/apply.ts +272 -0
  325. package/src/core/policy/diff.ts +288 -0
  326. package/src/core/policy/loader.test.ts +63 -0
  327. package/src/core/policy/loader.ts +214 -0
  328. package/src/core/policy/policy.test.ts +232 -0
  329. package/src/core/policy/types.ts +105 -0
  330. package/src/core/schema-builder.test.ts +660 -0
  331. package/src/core/schema-builder.ts +881 -0
  332. package/src/core/standard-schema.ts +56 -0
  333. package/src/core/types.ts +258 -0
  334. package/src/core/validation.test.ts +195 -0
  335. package/src/core/validation.ts +192 -0
  336. package/src/drizzle/adapter.test.ts +425 -0
  337. package/src/drizzle/adapter.ts +474 -0
  338. package/src/drizzle/index.ts +31 -0
  339. package/src/drizzle/pg/adapter.integration-test.ts +456 -0
  340. package/src/drizzle/pg/index.ts +13 -0
  341. package/src/drizzle/pg/pg.integration-test.ts +1539 -0
  342. package/src/drizzle/pg/schema.ts +539 -0
  343. package/src/drizzle/pg-error-map.test.ts +218 -0
  344. package/src/drizzle/pg-error-map.ts +151 -0
  345. package/src/drizzle/schema.ts +544 -0
  346. package/src/drizzle/sqlite-serialization.test.ts +106 -0
  347. package/src/express/express-api-key-user-routes.test.ts +241 -0
  348. package/src/express/express.test.ts +442 -0
  349. package/src/express/handle.ts +191 -0
  350. package/src/express/index.ts +62 -0
  351. package/src/express/middleware.ts +551 -0
  352. package/src/express/protect.ts +154 -0
  353. package/src/express/validated.test.ts +86 -0
  354. package/src/express/validated.ts +99 -0
  355. package/src/fetcher/index.ts +81 -0
  356. package/src/hono/convert-routes.test.ts +220 -0
  357. package/src/hono/convert-routes.ts +196 -0
  358. package/src/hono/converters.test.ts +50 -0
  359. package/src/hono/converters.ts +43 -0
  360. package/src/hono/handle.ts +79 -0
  361. package/src/hono/helpers.ts +2 -0
  362. package/src/hono/hono-api-key-user-routes.test.ts +225 -0
  363. package/src/hono/hono-handlers.test.ts +861 -0
  364. package/src/hono/hono.test.ts +454 -0
  365. package/src/hono/index.ts +101 -0
  366. package/src/hono/middleware/auth.ts +236 -0
  367. package/src/hono/middleware/auth.types.test.ts +94 -0
  368. package/src/hono/middleware/csrf.test.ts +127 -0
  369. package/src/hono/middleware/csrf.ts +68 -0
  370. package/src/hono/middleware/error-handler.ts +12 -0
  371. package/src/hono/middleware/plugin-middleware.ts +35 -0
  372. package/src/hono/middleware/rbac.ts +131 -0
  373. package/src/hono/middleware/security-headers.test.ts +88 -0
  374. package/src/hono/middleware/security-headers.ts +68 -0
  375. package/src/hono/openapi.ts +237 -0
  376. package/src/hono/protect.ts +87 -0
  377. package/src/hono/validated.test.ts +123 -0
  378. package/src/hono/validated.ts +62 -0
  379. package/src/index.ts +309 -0
  380. package/src/integration.test.ts +718 -0
  381. package/src/internal/changelog.ts +56 -0
  382. package/src/otel/index.ts +158 -0
  383. package/src/otel/otel-types.test.ts +35 -0
  384. package/src/otel/otel.test.ts +235 -0
  385. package/src/plugins/account-lockout/account-lockout.test.ts +367 -0
  386. package/src/plugins/account-lockout/index.ts +267 -0
  387. package/src/plugins/admin/admin-api-key.test.ts +438 -0
  388. package/src/plugins/admin/index.ts +1612 -0
  389. package/src/plugins/api-key/api-key.test.ts +684 -0
  390. package/src/plugins/api-key/core.ts +336 -0
  391. package/src/plugins/api-key/index.ts +315 -0
  392. package/src/plugins/audit-log/audit-log.test.ts +749 -0
  393. package/src/plugins/audit-log/index.ts +680 -0
  394. package/src/plugins/data-isolation/data-isolation.test.ts +197 -0
  395. package/src/plugins/data-isolation/index.ts +157 -0
  396. package/src/plugins/email-verification/email-verification.test.ts +199 -0
  397. package/src/plugins/email-verification/index.ts +190 -0
  398. package/src/plugins/magic-link/index.ts +129 -0
  399. package/src/plugins/magic-link/magic-link.test.ts +156 -0
  400. package/src/plugins/oauth/id-token.ts +86 -0
  401. package/src/plugins/oauth/index.ts +2145 -0
  402. package/src/plugins/oauth/jwks.test.ts +32 -0
  403. package/src/plugins/oauth/jwks.ts +182 -0
  404. package/src/plugins/oauth/oauth.test.ts +2220 -0
  405. package/src/plugins/oauth/pkce.ts +58 -0
  406. package/src/plugins/openapi/index.ts +298 -0
  407. package/src/plugins/openapi/openapi.test.ts +425 -0
  408. package/src/plugins/openapi/spec-builder.ts +287 -0
  409. package/src/plugins/rate-limit/express.test.ts +89 -0
  410. package/src/plugins/rate-limit/express.ts +86 -0
  411. package/src/plugins/rate-limit/hono.test.ts +60 -0
  412. package/src/plugins/rate-limit/hono.ts +74 -0
  413. package/src/plugins/rate-limit/index.ts +383 -0
  414. package/src/plugins/rate-limit/memory-store.ts +61 -0
  415. package/src/plugins/rate-limit/rate-limit.test.ts +756 -0
  416. package/src/plugins/rate-limit/sveltekit.test.ts +58 -0
  417. package/src/plugins/rate-limit/sveltekit.ts +66 -0
  418. package/src/plugins/social-login/index.ts +715 -0
  419. package/src/plugins/social-login/providers/apple.ts +34 -0
  420. package/src/plugins/social-login/providers/discord.ts +26 -0
  421. package/src/plugins/social-login/providers/github.ts +54 -0
  422. package/src/plugins/social-login/providers/google.ts +23 -0
  423. package/src/plugins/social-login/providers/index.ts +22 -0
  424. package/src/plugins/social-login/providers/microsoft.ts +35 -0
  425. package/src/plugins/social-login/providers/oidc.ts +37 -0
  426. package/src/plugins/social-login/providers/providers.test.ts +211 -0
  427. package/src/plugins/social-login/social-login.test.ts +675 -0
  428. package/src/plugins/social-login/types.ts +80 -0
  429. package/src/plugins/tenancy/index.ts +544 -0
  430. package/src/plugins/tenancy/tenancy.test.ts +323 -0
  431. package/src/plugins/two-factor/index.ts +569 -0
  432. package/src/plugins/two-factor/two-factor.test.ts +235 -0
  433. package/src/plugins/webauthn/index.ts +554 -0
  434. package/src/plugins/webauthn/webauthn.test.ts +472 -0
  435. package/src/plugins/webhook/builtin-events.ts +29 -0
  436. package/src/plugins/webhook/delivery.test.ts +51 -0
  437. package/src/plugins/webhook/delivery.ts +154 -0
  438. package/src/plugins/webhook/hooks.ts +89 -0
  439. package/src/plugins/webhook/index.ts +445 -0
  440. package/src/plugins/webhook/queue/database.ts +67 -0
  441. package/src/plugins/webhook/queue/in-memory.test.ts +48 -0
  442. package/src/plugins/webhook/queue/in-memory.ts +71 -0
  443. package/src/plugins/webhook/queue/types.ts +51 -0
  444. package/src/plugins/webhook/registry.test.ts +48 -0
  445. package/src/plugins/webhook/registry.ts +64 -0
  446. package/src/plugins/webhook/signing.ts +45 -0
  447. package/src/plugins/webhook/ssrf.ts +198 -0
  448. package/src/plugins/webhook/types.ts +138 -0
  449. package/src/plugins/webhook/webhook.test.ts +324 -0
  450. package/src/sveltekit/actions.ts +233 -0
  451. package/src/sveltekit/catch-all.ts +43 -0
  452. package/src/sveltekit/cookies.ts +136 -0
  453. package/src/sveltekit/handle.ts +356 -0
  454. package/src/sveltekit/helpers.ts +69 -0
  455. package/src/sveltekit/index.ts +74 -0
  456. package/src/sveltekit/protect.ts +80 -0
  457. package/src/sveltekit/sveltekit-types.test.ts +31 -0
  458. package/src/sveltekit/sveltekit.test.ts +799 -0
  459. package/src/sveltekit/types.ts +134 -0
  460. package/src/sveltekit/validated.test.ts +117 -0
  461. package/src/sveltekit/validated.ts +78 -0
  462. package/src/testing/adapter-conformance.test.ts +408 -0
  463. package/src/testing/checks.test.ts +124 -0
  464. package/src/testing/checks.ts +304 -0
  465. package/src/testing/index.ts +107 -0
@@ -0,0 +1,236 @@
1
+ import type { Context, Env, MiddlewareHandler } from 'hono';
2
+ import type { DatabaseAdapter } from '../../adapters/database';
3
+ import type { Fortress } from '../../core/fortress';
4
+ import type { PluginContext } from '../../core/plugin';
5
+ import type { Subject, TokenClaims } from '../../core/types';
6
+ import { FortressError } from '../../core/errors';
7
+ import {
8
+ chainAdapterWrappers,
9
+ collectScopeRules,
10
+ wrapAdapterWithScopeRules,
11
+ } from '../../core/plugin-runner';
12
+
13
+ /**
14
+ * Hono `Variables` slot populated by the fortress auth middleware.
15
+ *
16
+ * `fortressSubject` is the authoritative principal — set for every
17
+ * authenticated request regardless of credential type (JWT, api-key, future
18
+ * OAuth client_credentials, mTLS, ...). `fortressUserId` is a convenience
19
+ * alias populated **only** when the subject is a `USER`; non-USER subjects
20
+ * (e.g. `SERVICE_ACCOUNT` via api-key) leave it `undefined`.
21
+ *
22
+ * Use {@link FortressEnv} to compose this with your own Hono env types
23
+ * without casts.
24
+ */
25
+ export interface FortressVariables {
26
+ fortressSubject: Subject;
27
+ fortressUserId?: string;
28
+ fortressClaims?: TokenClaims;
29
+ fortressScopes?: string[] | null;
30
+ fortressDb: DatabaseAdapter;
31
+ fortressGetScopedDb: (model: string) => Promise<DatabaseAdapter>;
32
+ }
33
+
34
+ type ExtractVariables<E> = E extends { Variables: infer V } ? V : Record<string, never>;
35
+ type ExtractBindings<E> = E extends { Bindings: infer B } ? B : Record<string, never>;
36
+
37
+ /**
38
+ * Generic Hono env that composes {@link FortressVariables} with the host
39
+ * app's own env types. Use it directly (`Hono<FortressEnv>`) for a
40
+ * Fortress-only app, or parameterize with your existing env
41
+ * (`Hono<FortressEnv<MyEnv>>`) to add Fortress's variables on top of
42
+ * yours — no `Context<AppEnv>` ↔ `Context<FortressEnv>` casts.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * interface MyEnv {
47
+ * Variables: { requestId: string };
48
+ * Bindings: { DB: D1Database };
49
+ * }
50
+ *
51
+ * const app = new Hono<FortressEnv<MyEnv>>();
52
+ * app.use(createAuthMiddleware(fortress));
53
+ * app.get('/me', (c) => {
54
+ * const requestId = c.get('requestId'); // host-defined, typed
55
+ * const subject = getSubject(c); // fortress-defined, typed
56
+ * return c.json({ requestId, subject });
57
+ * });
58
+ * ```
59
+ */
60
+ export interface FortressEnv<TAppEnv extends Env = Env> {
61
+ Variables: FortressVariables & ExtractVariables<TAppEnv>;
62
+ Bindings: ExtractBindings<TAppEnv>;
63
+ }
64
+
65
+ /** Minimum-viable context shape the typed helpers need — works with any `Context<E>` whose `Variables` include {@link FortressVariables}. */
66
+ export type FortressContext<E extends Env = FortressEnv> = Context<E>;
67
+
68
+ /**
69
+ * Hono middleware that resolves the request principal and populates the
70
+ * Hono context with `fortressSubject`, `fortressUserId` (USER alias),
71
+ * `fortressClaims`, `fortressDb`, and `fortressGetScopedDb`.
72
+ *
73
+ * Principal resolution goes through `fortress.resolvePrincipal`, which
74
+ * tries plugin `resolvePrincipal` hooks (api-key, future OAuth
75
+ * client_credentials, mTLS) first and then falls back to the JWT bearer
76
+ * token (cookie-first, `Authorization: Bearer` second). This means the
77
+ * same Hono app can authenticate browsers (cookies), SPA clients (Bearer),
78
+ * and service accounts (`Authorization: ApiKey ...` or `X-API-Key`)
79
+ * without extra wiring on any route.
80
+ *
81
+ * `fortressDb` has plugin `wrapAdapter` applied (e.g. tenancy schema
82
+ * switching). `fortressGetScopedDb(model)` additionally applies
83
+ * `scopeRules` for the requested model.
84
+ */
85
+ export function createAuthMiddleware(
86
+ fortress: Fortress,
87
+ ): MiddlewareHandler<FortressEnv> {
88
+ // Internally typed as the base FortressEnv. The middleware is variance-safe
89
+ // when mounted on a `Hono<FortressEnv<MyApp>>()` because the augmented env's
90
+ // Variables are a superset of FortressVariables — Hono accepts the wider
91
+ // shape during use().
92
+ return async (c, next) => {
93
+ const resolved = await fortress.resolvePrincipal(c.req.raw);
94
+ if (!resolved) {
95
+ throw new FortressError(
96
+ 'UNAUTHORIZED',
97
+ 'Missing or invalid credentials',
98
+ 401,
99
+ );
100
+ }
101
+
102
+ const { subject, claims, scopes } = resolved;
103
+ c.set('fortressSubject', subject);
104
+ if (subject.type === 'USER')
105
+ c.set('fortressUserId', subject.id);
106
+ if (claims)
107
+ c.set('fortressClaims', claims);
108
+ if (scopes !== undefined)
109
+ c.set('fortressScopes', scopes);
110
+
111
+ // Build request context for plugin adapter wrappers. The tenant is taken
112
+ // from the verified JWT claim (set by tenancy's enrichTokenClaims), never
113
+ // a client header — so a caller can only reach a tenant they belong to.
114
+ const plugins = fortress.config.plugins ?? [];
115
+ const requestContext: Record<string, unknown> = {
116
+ tenantId: claims?.customClaims?.tenantId,
117
+ ipAddress:
118
+ c.req.header('X-Forwarded-For') ?? c.req.header('X-Real-IP'),
119
+ userAgent: c.req.header('User-Agent'),
120
+ };
121
+
122
+ // Chain adapter wrappers (e.g., tenancy schema switching)
123
+ const wrappedAdapter = chainAdapterWrappers(
124
+ plugins,
125
+ fortress.config.database,
126
+ requestContext,
127
+ );
128
+ c.set('fortressDb', wrappedAdapter);
129
+
130
+ // Lazy scope rule applicator — routes pick the model they need. Scope
131
+ // rules key off the subject id; for non-USER subjects the plugins that
132
+ // care (data-isolation) typically noop, but the call still works.
133
+ const pluginCtx: PluginContext = {
134
+ db: wrappedAdapter,
135
+ config: fortress.config,
136
+ };
137
+ c.set(
138
+ 'fortressGetScopedDb',
139
+ async (model: string): Promise<DatabaseAdapter> => {
140
+ const scopeRule = await collectScopeRules(
141
+ plugins,
142
+ subject.id,
143
+ model,
144
+ pluginCtx,
145
+ );
146
+ if (!scopeRule)
147
+ return wrappedAdapter;
148
+ return wrapAdapterWithScopeRules(wrappedAdapter, scopeRule);
149
+ },
150
+ );
151
+
152
+ await next();
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Get the authenticated principal from the Hono context. Works for every
158
+ * subject kind (`USER`, `SERVICE_ACCOUNT`, ...). Generic in the env type
159
+ * so host apps can pass their own `Context<FortressEnv<MyEnv>>` without
160
+ * casts.
161
+ */
162
+ export function getSubject<E extends Env = FortressEnv>(c: Context<E>): Subject {
163
+ const subject = c.get('fortressSubject' as never) as Subject | undefined;
164
+ if (!subject) {
165
+ throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
166
+ }
167
+ return subject;
168
+ }
169
+
170
+ /**
171
+ * Get the authenticated user ID from Hono context. Throws 401 if the
172
+ * request was authenticated by a non-USER subject (e.g. a service account
173
+ * via api-key) — use {@link getSubject} for handlers that need to accept
174
+ * any principal.
175
+ */
176
+ export function getUserId<E extends Env = FortressEnv>(c: Context<E>): string {
177
+ const subject = c.get('fortressSubject' as never) as Subject | undefined;
178
+ if (!subject || subject.type !== 'USER') {
179
+ throw new FortressError('UNAUTHORIZED', 'User not authenticated', 401);
180
+ }
181
+ return subject.id;
182
+ }
183
+
184
+ /**
185
+ * Get the authenticated user's JWT claims from Hono context. Claims are
186
+ * only populated when the request was authenticated via a JWT — api-key
187
+ * principals have no JWT claims. Throws 401 if unavailable.
188
+ *
189
+ * Pass a `TCustomClaims` type parameter to narrow `customClaims` to your
190
+ * deployment's plugin-augmented claim shape (e.g. tenancy adds
191
+ * `tenantId` / `tenantCode`).
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * interface MyClaims { tenantId: string; tenantCode: string }
196
+ * const claims = getClaims<MyClaims>(c);
197
+ * claims.customClaims?.tenantCode; // string | undefined
198
+ * ```
199
+ */
200
+ export function getClaims<
201
+ TCustomClaims extends object = Record<string, unknown>,
202
+ E extends Env = FortressEnv,
203
+ >(c: Context<E>): TokenClaims & { customClaims?: TCustomClaims } {
204
+ const claims = c.get('fortressClaims' as never) as TokenClaims | undefined;
205
+ if (!claims) {
206
+ throw new FortressError('UNAUTHORIZED', 'No JWT claims on this request', 401);
207
+ }
208
+ return claims as TokenClaims & { customClaims?: TCustomClaims };
209
+ }
210
+
211
+ /**
212
+ * Get the request-scoped database adapter with plugin wrapAdapter applied.
213
+ */
214
+ export function getDb<E extends Env = FortressEnv>(c: Context<E>): DatabaseAdapter {
215
+ const db = c.get('fortressDb' as never) as DatabaseAdapter | undefined;
216
+ if (!db) {
217
+ throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
218
+ }
219
+ return db;
220
+ }
221
+
222
+ /**
223
+ * Get a model-scoped database adapter with both wrapAdapter and scopeRules applied.
224
+ */
225
+ export function getScopedDb<E extends Env = FortressEnv>(
226
+ c: Context<E>,
227
+ model: string,
228
+ ): Promise<DatabaseAdapter> {
229
+ const fn = c.get('fortressGetScopedDb' as never) as
230
+ | ((model: string) => Promise<DatabaseAdapter>)
231
+ | undefined;
232
+ if (!fn) {
233
+ throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
234
+ }
235
+ return fn(model);
236
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Type-level tests for the Hono adapter typed helpers (P1-6).
3
+ *
4
+ * Uses `expectTypeOf` for compile-time assertions. The body of every `it`
5
+ * is intentionally type-only and contains no runtime assertions; vitest
6
+ * runs them once just to confirm the file imports cleanly.
7
+ */
8
+
9
+ import type { Subject, TokenClaims } from '../../core/types';
10
+ import type { FortressEnv } from './auth';
11
+ import { Hono } from 'hono';
12
+ import { describe, expectTypeOf, it } from 'vitest';
13
+ import { getClaims, getSubject, getUserId } from './auth';
14
+
15
+ interface MyEnv {
16
+ Variables: { requestId: string };
17
+ Bindings: { DB: 'pretend-binding' };
18
+ }
19
+
20
+ interface MyCustomClaims {
21
+ tenantId: string;
22
+ tenantCode: string;
23
+ }
24
+
25
+ describe('fortressEnv<TAppEnv> composition', () => {
26
+ it('exposes both host and fortress variables on a composed env', () => {
27
+ // Build a context typed as FortressEnv<MyEnv> and verify the types.
28
+ type Composed = FortressEnv<MyEnv>;
29
+ type Variables = Composed['Variables'];
30
+
31
+ expectTypeOf<Variables['fortressSubject']>().toEqualTypeOf<Subject>();
32
+ expectTypeOf<Variables['requestId']>().toEqualTypeOf<string>();
33
+ expectTypeOf<Composed['Bindings']['DB']>().toEqualTypeOf<'pretend-binding'>();
34
+ });
35
+
36
+ it('app = new Hono<FortressEnv<MyEnv>>() retains host variables', () => {
37
+ const app = new Hono<FortressEnv<MyEnv>>();
38
+ app.get('/x', (c) => {
39
+ const requestId = c.get('requestId');
40
+ expectTypeOf(requestId).toEqualTypeOf<string>();
41
+ return c.json({ requestId });
42
+ });
43
+ });
44
+ });
45
+
46
+ describe('typed helpers accept any FortressEnv-compatible context', () => {
47
+ it('getSubject works on the default FortressEnv', () => {
48
+ const app = new Hono<FortressEnv>();
49
+ app.get('/x', (c) => {
50
+ const subject = getSubject(c);
51
+ expectTypeOf(subject).toEqualTypeOf<Subject>();
52
+ return c.json({ subject });
53
+ });
54
+ });
55
+
56
+ it('getSubject / getUserId work on FortressEnv<MyEnv> without casts', () => {
57
+ const app = new Hono<FortressEnv<MyEnv>>();
58
+ app.get('/y', (c) => {
59
+ const subject = getSubject(c);
60
+ const userId = getUserId(c);
61
+ expectTypeOf(subject).toEqualTypeOf<Subject>();
62
+ expectTypeOf(userId).toEqualTypeOf<string>();
63
+ return c.json({ subject, userId });
64
+ });
65
+ });
66
+
67
+ it('getClaims narrows customClaims via the type parameter', () => {
68
+ const app = new Hono<FortressEnv>();
69
+ app.get('/z', (c) => {
70
+ const claims = getClaims<MyCustomClaims>(c);
71
+ expectTypeOf(claims).toEqualTypeOf<TokenClaims & { customClaims?: MyCustomClaims }>();
72
+ if (claims.customClaims) {
73
+ expectTypeOf(claims.customClaims.tenantId).toEqualTypeOf<string>();
74
+ expectTypeOf(claims.customClaims.tenantCode).toEqualTypeOf<string>();
75
+ }
76
+ return c.json({ ok: true });
77
+ });
78
+ });
79
+ });
80
+
81
+ describe('fortressEnv default keeps existing callers working', () => {
82
+ it('hono<FortressEnv> still resolves to a usable env', () => {
83
+ type Variables = FortressEnv['Variables'];
84
+ expectTypeOf<Variables['fortressSubject']>().toEqualTypeOf<Subject>();
85
+ // The unconstrained generic falls back to an empty bindings/extra-vars
86
+ // shape; Hono accepts it as a valid Env and the typed helpers work.
87
+ const app = new Hono<FortressEnv>();
88
+ app.get('/x', (c) => {
89
+ const subject = getSubject(c);
90
+ expectTypeOf(subject).toEqualTypeOf<Subject>();
91
+ return c.json({ subject });
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,127 @@
1
+ import { Hono } from 'hono';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createCsrfMiddleware } from './csrf';
4
+
5
+ describe('csrf middleware', () => {
6
+ it('allows GET requests without CSRF header', async () => {
7
+ const app = new Hono();
8
+ app.use('/*', createCsrfMiddleware());
9
+ app.get('/api/data', c => c.json({ ok: true }));
10
+
11
+ const res = await app.request('/api/data', { method: 'GET' });
12
+
13
+ expect(res.status).toBe(200);
14
+ const body = await res.json();
15
+ expect(body).toEqual({ ok: true });
16
+ });
17
+
18
+ it('allows POST requests with valid CSRF header', async () => {
19
+ const app = new Hono();
20
+ app.use('/*', createCsrfMiddleware());
21
+ app.post('/api/data', c => c.json({ ok: true }));
22
+
23
+ const res = await app.request('/api/data', {
24
+ method: 'POST',
25
+ headers: { 'X-Fortress-CSRF': '1' },
26
+ });
27
+
28
+ expect(res.status).toBe(200);
29
+ const body = await res.json();
30
+ expect(body).toEqual({ ok: true });
31
+ });
32
+
33
+ it('blocks POST requests without CSRF header (403)', async () => {
34
+ const app = new Hono();
35
+ app.use('/*', createCsrfMiddleware());
36
+ app.post('/api/data', c => c.json({ ok: true }));
37
+
38
+ const res = await app.request('/api/data', {
39
+ method: 'POST',
40
+ });
41
+
42
+ expect(res.status).toBe(403);
43
+ const body = await res.json();
44
+ expect(body.error).toBe('CSRF_MISSING');
45
+ });
46
+
47
+ it('allows requests to skip paths', async () => {
48
+ const app = new Hono();
49
+ app.use('/*', createCsrfMiddleware({ skipPaths: ['/webhook'] }));
50
+ app.post('/webhook', c => c.json({ ok: true }));
51
+
52
+ const res = await app.request('/webhook', {
53
+ method: 'POST',
54
+ });
55
+
56
+ expect(res.status).toBe(200);
57
+ const body = await res.json();
58
+ expect(body).toEqual({ ok: true });
59
+ });
60
+
61
+ it('matches skip paths at segment boundaries', async () => {
62
+ const app = new Hono();
63
+ app.use('/*', createCsrfMiddleware({ skipPaths: ['/webhook'] }));
64
+ app.post('/webhook/delivery', c => c.json({ ok: true }));
65
+ app.post('/webhook-evil', c => c.json({ ok: true }));
66
+
67
+ expect((await app.request('/webhook/delivery', { method: 'POST' })).status).toBe(200);
68
+ expect((await app.request('/webhook-evil', { method: 'POST' })).status).toBe(403);
69
+ });
70
+
71
+ it('blocks cross-site requests (Sec-Fetch-Site: cross-site)', async () => {
72
+ const app = new Hono();
73
+ app.use('/*', createCsrfMiddleware());
74
+ app.post('/api/data', c => c.json({ ok: true }));
75
+
76
+ const res = await app.request('/api/data', {
77
+ method: 'POST',
78
+ headers: {
79
+ 'X-Fortress-CSRF': '1',
80
+ 'Sec-Fetch-Site': 'cross-site',
81
+ },
82
+ });
83
+
84
+ expect(res.status).toBe(403);
85
+ const body = await res.json();
86
+ expect(body.error).toBe('CSRF_REJECTED');
87
+ });
88
+
89
+ it('custom header name works', async () => {
90
+ const app = new Hono();
91
+ app.use('/*', createCsrfMiddleware({ headerName: 'X-Custom-CSRF' }));
92
+ app.post('/api/data', c => c.json({ ok: true }));
93
+
94
+ // Should fail with default header name
95
+ const res1 = await app.request('/api/data', {
96
+ method: 'POST',
97
+ headers: { 'X-Fortress-CSRF': '1' },
98
+ });
99
+ expect(res1.status).toBe(403);
100
+
101
+ // Should succeed with custom header name
102
+ const res2 = await app.request('/api/data', {
103
+ method: 'POST',
104
+ headers: { 'X-Custom-CSRF': '1' },
105
+ });
106
+ expect(res2.status).toBe(200);
107
+ });
108
+
109
+ it('custom safe methods work', async () => {
110
+ const app = new Hono();
111
+ app.use('/*', createCsrfMiddleware({ safeMethods: ['GET', 'HEAD', 'OPTIONS', 'POST'] }));
112
+ app.post('/api/data', c => c.json({ ok: true }));
113
+ app.put('/api/data', c => c.json({ ok: true }));
114
+
115
+ // POST is now safe, should pass without CSRF header
116
+ const res1 = await app.request('/api/data', {
117
+ method: 'POST',
118
+ });
119
+ expect(res1.status).toBe(200);
120
+
121
+ // PUT is not safe, should fail without CSRF header
122
+ const res2 = await app.request('/api/data', {
123
+ method: 'PUT',
124
+ });
125
+ expect(res2.status).toBe(403);
126
+ });
127
+ });
@@ -0,0 +1,68 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+
3
+ /** Options accepted by {@link createCsrfMiddleware}. */
4
+ export interface CsrfConfig {
5
+ /** Header name to check. Default: 'X-Fortress-CSRF'. */
6
+ headerName?: string;
7
+ /** Paths to skip CSRF checking. */
8
+ skipPaths?: string[];
9
+ /** HTTP methods that don't need CSRF checking. Default: ['GET', 'HEAD', 'OPTIONS']. */
10
+ safeMethods?: string[];
11
+ }
12
+
13
+ /**
14
+ * CSRF middleware using the custom-header strategy.
15
+ *
16
+ * Browsers enforce CORS preflight on custom headers, so cross-origin requests
17
+ * cannot set them without explicit server permission. This middleware requires
18
+ * a specific header to be present on non-safe HTTP methods.
19
+ *
20
+ * Additionally checks `Sec-Fetch-Site` to reject cross-site requests.
21
+ */
22
+ export function createCsrfMiddleware(config?: CsrfConfig): MiddlewareHandler {
23
+ const headerName = config?.headerName ?? 'X-Fortress-CSRF';
24
+ const skipPaths = config?.skipPaths ?? [];
25
+ const safeMethods = config?.safeMethods ?? ['GET', 'HEAD', 'OPTIONS'];
26
+
27
+ return async (c, next): Promise<Response | void> => {
28
+ const method = c.req.method.toUpperCase();
29
+
30
+ // Safe methods don't need CSRF protection
31
+ if (safeMethods.includes(method)) {
32
+ await next();
33
+ return;
34
+ }
35
+
36
+ // Check skip paths at segment boundaries: `/foo` covers `/foo/bar` but
37
+ // never `/foobar`. A trailing `/*` is accepted as subtree notation.
38
+ const path = new URL(c.req.url).pathname;
39
+ if (matchesSkipPath(path, skipPaths)) {
40
+ await next();
41
+ return;
42
+ }
43
+
44
+ // Check Sec-Fetch-Site header - reject cross-site requests
45
+ const fetchSite = c.req.header('Sec-Fetch-Site');
46
+ if (fetchSite === 'cross-site') {
47
+ return c.json({ error: 'CSRF_REJECTED', message: 'Cross-site requests are not allowed' }, 403);
48
+ }
49
+
50
+ // Require the custom CSRF header
51
+ const csrfHeader = c.req.header(headerName);
52
+ if (!csrfHeader) {
53
+ return c.json({ error: 'CSRF_MISSING', message: 'Missing CSRF header' }, 403);
54
+ }
55
+
56
+ await next();
57
+ };
58
+ }
59
+
60
+ function matchesSkipPath(path: string, skipPaths: string[]): boolean {
61
+ return skipPaths.some((configured) => {
62
+ const withoutWildcard = configured.endsWith('/*') ? configured.slice(0, -2) : configured;
63
+ const base = withoutWildcard.length > 1 && withoutWildcard.endsWith('/')
64
+ ? withoutWildcard.slice(0, -1)
65
+ : withoutWildcard;
66
+ return path === base || path.startsWith(`${base}/`);
67
+ });
68
+ }
@@ -0,0 +1,12 @@
1
+ import type { ErrorHandler } from 'hono';
2
+ import { errorToResponse } from '../../core/http/error-response';
3
+
4
+ /**
5
+ * Hono error handler that maps FortressError to HTTP responses.
6
+ * Delegates to the framework-agnostic `errorToResponse` in core so the
7
+ * mapping (status codes, `Retry-After`, sanitized 500s) stays consistent
8
+ * across Hono / Express / SvelteKit / `fortress.handleRequest`.
9
+ */
10
+ export function createErrorHandler(): ErrorHandler {
11
+ return err => errorToResponse(err);
12
+ }
@@ -0,0 +1,35 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { Fortress } from '../../core/fortress';
3
+ import type { PluginRequestContext } from '../../core/http/plugin-middleware';
4
+ import type { MiddlewareDefinition } from '../../core/plugin';
5
+ import type { FortressEnv } from './auth';
6
+ import { executePluginMiddleware } from '../../core/plugin-runner';
7
+
8
+ /**
9
+ * Hono middleware that executes plugin-defined middleware for a given position.
10
+ *
11
+ * Normalizes Hono's context to the shared {@link PluginRequestContext}, so
12
+ * plugin middleware receives the same web-standard Request and resolved auth
13
+ * fields under core, Hono, and Express.
14
+ */
15
+ export function createPluginMiddleware(
16
+ fortress: Fortress,
17
+ position: MiddlewareDefinition['position'],
18
+ ): MiddlewareHandler<FortressEnv> {
19
+ const plugins = fortress.config.plugins ?? [];
20
+
21
+ return async (c, next) => {
22
+ const path = c.req.path;
23
+ const ctx = { db: fortress.config.database, config: fortress.config };
24
+ const requestContext: PluginRequestContext = {
25
+ request: c.req.raw,
26
+ fortressSubject: c.get('fortressSubject'),
27
+ fortressUserId: c.get('fortressUserId'),
28
+ fortressClaims: c.get('fortressClaims'),
29
+ fortressScopes: c.get('fortressScopes'),
30
+ };
31
+
32
+ await executePluginMiddleware(plugins, position, path, ctx, requestContext);
33
+ await next();
34
+ };
35
+ }