@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,131 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { Fortress } from '../../core/fortress';
3
+ import type { FortressEnv } from './auth';
4
+ import { FortressError } from '../../core/errors';
5
+
6
+ /** A `(resource, action)` IAM mapping for an HTTP route. */
7
+ export interface RouteMapping {
8
+ resource: string;
9
+ action: string;
10
+ }
11
+
12
+ /** Options accepted by {@link createRbacMiddleware} (and the Hono adapter factory). */
13
+ export interface RbacOptions {
14
+ /** Declarative route-to-resource mapping: 'METHOD /path' → { resource, action } */
15
+ routeMap?: Record<string, RouteMapping>;
16
+ /** Dynamic mapping function (used if routeMap doesn't match) */
17
+ mapRequest?: (method: string, path: string) => RouteMapping | null;
18
+ /** Paths that skip permission checks entirely (supports * wildcards) */
19
+ skipPaths?: string[];
20
+ /** Behavior for non-skipped routes absent from routeMap/mapRequest. */
21
+ unmappedRoutes?: 'allow' | 'deny';
22
+ /** @deprecated Use `unmappedRoutes: 'deny'`. */
23
+ defaultDeny?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Hono middleware that checks IAM permissions for **user-owned routes**
28
+ * via a declarative route map. Fortress-managed routes (`/auth/*`,
29
+ * `/iam/*`, plugin paths) are protected automatically inside
30
+ * `fortress.handleRequest` — this middleware only handles routes the
31
+ * caller registered themselves.
32
+ *
33
+ * Lookup order:
34
+ * 1. {@link RbacOptions.skipPaths} — if matched, allow.
35
+ * 2. {@link RbacOptions.routeMap} — exact `'METHOD /path'` then
36
+ * parameterized match.
37
+ * 3. {@link RbacOptions.mapRequest} — dynamic resolver fallback.
38
+ * 4. No mapping found → allow, unless {@link RbacOptions.defaultDeny} is set,
39
+ * in which case the route is refused with a 403 (fail closed).
40
+ *
41
+ * Throws {@link FortressError} on missing/insufficient permissions.
42
+ */
43
+ export function createRbacMiddleware(
44
+ fortress: Fortress,
45
+ options?: RbacOptions,
46
+ ): MiddlewareHandler<FortressEnv> {
47
+ const routeMap = options?.routeMap ?? {};
48
+ const skipPaths = options?.skipPaths ?? [];
49
+ const skipPatterns = skipPaths.map(p => pathToRegex(p));
50
+ const unmappedRoutes = options?.unmappedRoutes
51
+ ?? (options?.defaultDeny ? 'deny' : 'allow');
52
+
53
+ return async (c, next) => {
54
+ const path = c.req.path;
55
+ const method = c.req.method;
56
+
57
+ if (skipPatterns.some(pattern => pattern.test(path))) {
58
+ await next();
59
+ return;
60
+ }
61
+
62
+ const key = `${method} ${path}`;
63
+ let mapping: RouteMapping | null = routeMap[key] ?? null;
64
+
65
+ if (!mapping) {
66
+ mapping = findRouteMapMatch(method, path, routeMap);
67
+ }
68
+
69
+ if (!mapping && options?.mapRequest) {
70
+ mapping = options.mapRequest(method, path);
71
+ }
72
+
73
+ if (!mapping) {
74
+ if (unmappedRoutes === 'deny') {
75
+ // Fail closed: an unmapped route is refused rather than treated as
76
+ // public. Genuinely public routes belong in skipPaths.
77
+ throw new FortressError('FORBIDDEN', 'No permission mapping for this route', 403);
78
+ }
79
+ // No declarative mapping — caller treats this as a public route from
80
+ // the adapter's perspective. (Fortress-managed paths have already
81
+ // been caught and protected inside fortress.handleRequest.)
82
+ await next();
83
+ return;
84
+ }
85
+
86
+ const subject = c.get('fortressSubject');
87
+ if (!subject) {
88
+ throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
89
+ }
90
+
91
+ const allowed = await fortress.iam.checkPermission(subject, mapping.resource, mapping.action, {
92
+ credentialScopes: c.get('fortressScopes'),
93
+ });
94
+ if (!allowed) {
95
+ throw new FortressError('FORBIDDEN', 'Insufficient permissions', 403);
96
+ }
97
+
98
+ await next();
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Match a request against parameterized route map entries.
104
+ * 'GET /api/users/:id' matches 'GET /api/users/123'
105
+ */
106
+ function findRouteMapMatch(method: string, path: string, routeMap: Record<string, RouteMapping>): RouteMapping | null {
107
+ for (const [pattern, mapping] of Object.entries(routeMap)) {
108
+ const [patternMethod, patternPath] = pattern.split(' ', 2);
109
+ if (patternMethod !== method)
110
+ continue;
111
+
112
+ const regex = pathToRegex(patternPath);
113
+ if (regex.test(path))
114
+ return mapping;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Convert a route pattern to a regex.
121
+ * :param → [^/]+
122
+ * → .*
123
+ */
124
+ function pathToRegex(pattern: string): RegExp {
125
+ const regexStr = pattern
126
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
127
+ .replace(/:[^/]+/g, '[^/]+')
128
+ .replace(/\\\*/g, '.*')
129
+ .replace(/\//g, '\\/');
130
+ return new RegExp(`^${regexStr}$`);
131
+ }
@@ -0,0 +1,88 @@
1
+ import { Hono } from 'hono';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createSecurityHeadersMiddleware } from './security-headers';
4
+
5
+ describe('security headers middleware', () => {
6
+ it('sets all default security headers', async () => {
7
+ const app = new Hono();
8
+ app.use('*', createSecurityHeadersMiddleware());
9
+ app.get('/test', c => c.text('ok'));
10
+
11
+ const res = await app.request('/test');
12
+ expect(res.status).toBe(200);
13
+ expect(res.headers.get('Strict-Transport-Security')).toBe('max-age=63072000; includeSubDomains');
14
+ expect(res.headers.get('X-Frame-Options')).toBe('DENY');
15
+ expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff');
16
+ expect(res.headers.get('Content-Security-Policy')).toBe('default-src \'self\'');
17
+ expect(res.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
18
+ expect(res.headers.get('X-Permitted-Cross-Domain-Policies')).toBe('none');
19
+ });
20
+
21
+ it('sets security headers on error responses', async () => {
22
+ const app = new Hono();
23
+ app.use('*', createSecurityHeadersMiddleware());
24
+ app.get('/error', () => {
25
+ throw new Error('boom');
26
+ });
27
+
28
+ const res = await app.request('/error');
29
+ expect(res.status).toBe(500);
30
+ expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff');
31
+ expect(res.headers.get('X-Frame-Options')).toBe('DENY');
32
+ });
33
+
34
+ it('allows custom CSP', async () => {
35
+ const app = new Hono();
36
+ app.use('*', createSecurityHeadersMiddleware({
37
+ contentSecurityPolicy: 'default-src \'self\'; script-src \'self\' cdn.example.com',
38
+ }));
39
+ app.get('/test', c => c.text('ok'));
40
+
41
+ const res = await app.request('/test');
42
+ expect(res.headers.get('Content-Security-Policy')).toBe('default-src \'self\'; script-src \'self\' cdn.example.com');
43
+ });
44
+
45
+ it('allows disabling individual headers', async () => {
46
+ const app = new Hono();
47
+ app.use('*', createSecurityHeadersMiddleware({
48
+ frameOptions: false,
49
+ contentSecurityPolicy: false,
50
+ referrerPolicy: false,
51
+ }));
52
+ app.get('/test', c => c.text('ok'));
53
+
54
+ const res = await app.request('/test');
55
+ expect(res.headers.get('X-Frame-Options')).toBeNull();
56
+ expect(res.headers.get('Content-Security-Policy')).toBeNull();
57
+ expect(res.headers.get('Referrer-Policy')).toBeNull();
58
+ // Others still present
59
+ expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff');
60
+ });
61
+
62
+ it('supports HSTS preload', async () => {
63
+ const app = new Hono();
64
+ app.use('*', createSecurityHeadersMiddleware({ hstsPreload: true }));
65
+ app.get('/test', c => c.text('ok'));
66
+
67
+ const res = await app.request('/test');
68
+ expect(res.headers.get('Strict-Transport-Security')).toBe('max-age=63072000; includeSubDomains; preload');
69
+ });
70
+
71
+ it('disables HSTS when maxAge is 0', async () => {
72
+ const app = new Hono();
73
+ app.use('*', createSecurityHeadersMiddleware({ hstsMaxAge: 0 }));
74
+ app.get('/test', c => c.text('ok'));
75
+
76
+ const res = await app.request('/test');
77
+ expect(res.headers.get('Strict-Transport-Security')).toBeNull();
78
+ });
79
+
80
+ it('allows SAMEORIGIN for X-Frame-Options', async () => {
81
+ const app = new Hono();
82
+ app.use('*', createSecurityHeadersMiddleware({ frameOptions: 'SAMEORIGIN' }));
83
+ app.get('/test', c => c.text('ok'));
84
+
85
+ const res = await app.request('/test');
86
+ expect(res.headers.get('X-Frame-Options')).toBe('SAMEORIGIN');
87
+ });
88
+ });
@@ -0,0 +1,68 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+
3
+ /** Options accepted by {@link createSecurityHeadersMiddleware}. */
4
+ export interface SecurityHeadersConfig {
5
+ /** Strict-Transport-Security max-age in seconds. Default: 63072000 (2 years). Set to 0 to disable. */
6
+ hstsMaxAge?: number;
7
+ /** Include subdomains in HSTS. Default: true. */
8
+ hstsIncludeSubdomains?: boolean;
9
+ /** HSTS preload flag. Default: false. */
10
+ hstsPreload?: boolean;
11
+ /** X-Frame-Options value. Default: 'DENY'. Set to false to disable. */
12
+ frameOptions?: 'DENY' | 'SAMEORIGIN' | false;
13
+ /** X-Content-Type-Options nosniff. Default: true. */
14
+ noSniff?: boolean;
15
+ /** Content-Security-Policy value. Default: "default-src 'self'". Set to false to disable. */
16
+ contentSecurityPolicy?: string | false;
17
+ /** Referrer-Policy value. Default: 'strict-origin-when-cross-origin'. Set to false to disable. */
18
+ referrerPolicy?: string | false;
19
+ /** X-Permitted-Cross-Domain-Policies value. Default: 'none'. Set to false to disable. */
20
+ permittedCrossDomainPolicies?: string | false;
21
+ }
22
+
23
+ /**
24
+ * Hono middleware that sets security-related HTTP headers.
25
+ * All headers are enabled by default with secure values.
26
+ */
27
+ export function createSecurityHeadersMiddleware(config: SecurityHeadersConfig = {}): MiddlewareHandler {
28
+ const hstsMaxAge = config.hstsMaxAge ?? 63072000;
29
+ const hstsIncludeSubdomains = config.hstsIncludeSubdomains ?? true;
30
+ const hstsPreload = config.hstsPreload ?? false;
31
+ const frameOptions = config.frameOptions ?? 'DENY';
32
+ const noSniff = config.noSniff ?? true;
33
+ const csp = config.contentSecurityPolicy ?? 'default-src \'self\'';
34
+ const referrerPolicy = config.referrerPolicy ?? 'strict-origin-when-cross-origin';
35
+ const crossDomain = config.permittedCrossDomainPolicies ?? 'none';
36
+
37
+ // Pre-build HSTS header value
38
+ let hstsValue: string | null = null;
39
+ if (hstsMaxAge > 0) {
40
+ hstsValue = `max-age=${hstsMaxAge}`;
41
+ if (hstsIncludeSubdomains)
42
+ hstsValue += '; includeSubDomains';
43
+ if (hstsPreload)
44
+ hstsValue += '; preload';
45
+ }
46
+
47
+ return async (c, next) => {
48
+ try {
49
+ await next();
50
+ }
51
+ finally {
52
+ // Apply to success and error responses alike. Security headers should
53
+ // not disappear on the responses most likely to contain diagnostics.
54
+ if (hstsValue)
55
+ c.header('Strict-Transport-Security', hstsValue);
56
+ if (frameOptions)
57
+ c.header('X-Frame-Options', frameOptions);
58
+ if (noSniff)
59
+ c.header('X-Content-Type-Options', 'nosniff');
60
+ if (csp)
61
+ c.header('Content-Security-Policy', csp);
62
+ if (referrerPolicy)
63
+ c.header('Referrer-Policy', referrerPolicy);
64
+ if (crossDomain)
65
+ c.header('X-Permitted-Cross-Domain-Policies', crossDomain);
66
+ }
67
+ };
68
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Hono OpenAPI adapter for Fortress.
3
+ *
4
+ * Schema-library agnostic — accepts a converter function from the user.
5
+ * Users pass their own JSON Schema → schema-library converter (Zod, Valibot, etc.).
6
+ *
7
+ * Users pass their schema library's JSON Schema converter:
8
+ * - Zod v4: `z.fromJSONSchema`
9
+ * - TypeBox: schemas are JSON Schema natively
10
+ * - ArkType: `@ark/jsonschema`
11
+ */
12
+
13
+ import type { EndpointDefinition } from '../core/endpoint';
14
+ import type { Fortress } from '../core/fortress';
15
+ import type { JSONSchema } from '../core/json-schema';
16
+
17
+ /**
18
+ * A function that converts a JSON Schema to whatever schema type your
19
+ * framework adapter expects (e.g. Zod schema, Valibot schema). Used by
20
+ * {@link buildRouteDefinition} so fortress's OpenAPI generator stays
21
+ * schema-library agnostic.
22
+ */
23
+ export type SchemaConverter<T = unknown> = (jsonSchema: JSONSchema) => T;
24
+
25
+ interface MountOptions<T = unknown> {
26
+ /** Convert JSON Schema → your schema library (e.g., jsonSchemaToZod from '@bajustone/fortress/zod') */
27
+ schemaConverter: SchemaConverter<T>;
28
+ /** Paths to skip (won't be mounted) */
29
+ skipPaths?: string[];
30
+ /** Function to create a route definition from an endpoint (framework-specific) */
31
+ createRoute: (def: Record<string, unknown>) => unknown;
32
+ /** Function to register security schemes on the app */
33
+ registerSecuritySchemes?: (app: unknown) => void;
34
+ }
35
+
36
+ interface GenericOpenAPIApp {
37
+ openapi: (route: unknown, handler: (...args: any[]) => any) => void;
38
+ }
39
+
40
+ function toOpenAPIPath(path: string): string {
41
+ return path.replace(/:(\w+)/g, '{$1}');
42
+ }
43
+
44
+ /**
45
+ * Build a framework-agnostic route definition object from an EndpointDefinition.
46
+ * The schemaConverter transforms JSON Schema into whatever the target framework expects.
47
+ */
48
+ export function buildRouteDefinition<T>(
49
+ ep: EndpointDefinition,
50
+ schemaConverter: SchemaConverter<T>,
51
+ ): Record<string, unknown> {
52
+ const openAPIPath = toOpenAPIPath(ep.path);
53
+ const method = ep.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch';
54
+
55
+ const routeDef: Record<string, unknown> = {
56
+ method,
57
+ path: openAPIPath,
58
+ operationId: ep.handler,
59
+ responses: {} as Record<number, { description: string; content?: Record<string, unknown> }>,
60
+ };
61
+
62
+ if (ep.meta?.summary)
63
+ routeDef.summary = ep.meta.summary;
64
+ if (ep.meta?.description)
65
+ routeDef.description = ep.meta.description;
66
+ if (ep.meta?.tags)
67
+ routeDef.tags = ep.meta.tags;
68
+ if (ep.meta?.deprecated)
69
+ routeDef.deprecated = true;
70
+
71
+ // Security
72
+ if (ep.meta?.security) {
73
+ const sec: Array<Record<string, string[]>> = [];
74
+ for (const s of ep.meta.security) {
75
+ if (s === 'none')
76
+ sec.push({});
77
+ else if (s === 'bearer')
78
+ sec.push({ bearerAuth: [] });
79
+ else if (s === 'basic')
80
+ sec.push({ basicAuth: [] });
81
+ else if (s === 'apiKey')
82
+ sec.push({ apiKeyAuth: [] });
83
+ }
84
+ routeDef.security = sec;
85
+ }
86
+
87
+ // Request
88
+ const request: Record<string, unknown> = {};
89
+
90
+ if (ep.input?.body) {
91
+ request.body = {
92
+ content: {
93
+ 'application/json': { schema: schemaConverter(ep.input.body) },
94
+ },
95
+ };
96
+ }
97
+
98
+ if (ep.input?.params?.properties) {
99
+ const shape: Record<string, unknown> = {};
100
+ const required = new Set(ep.input.params.required ?? []);
101
+
102
+ for (const [key, schema] of Object.entries(ep.input.params.properties)) {
103
+ const converted = schemaConverter({ ...schema, type: schema.type ?? 'string' });
104
+ shape[key] = required.has(key) ? converted : converted;
105
+ }
106
+ // Wrap params in an object schema
107
+ request.params = schemaConverter({
108
+ type: 'object',
109
+ properties: ep.input.params.properties,
110
+ required: ep.input.params.required,
111
+ });
112
+ }
113
+
114
+ if (ep.input?.query?.properties) {
115
+ request.query = schemaConverter(ep.input.query);
116
+ }
117
+
118
+ if (Object.keys(request).length > 0) {
119
+ routeDef.request = request;
120
+ }
121
+
122
+ // Responses
123
+ const responses: Record<number, { description: string; content?: Record<string, unknown> }> = {};
124
+ if (ep.responses) {
125
+ for (const [status, resp] of Object.entries(ep.responses)) {
126
+ const entry: { description: string; content?: Record<string, unknown> } = {
127
+ description: resp.description,
128
+ };
129
+ if (resp.schema) {
130
+ entry.content = {
131
+ 'application/json': { schema: schemaConverter(resp.schema) },
132
+ };
133
+ }
134
+ responses[Number(status)] = entry;
135
+ }
136
+ }
137
+ else {
138
+ responses[200] = { description: 'Success' };
139
+ }
140
+ routeDef.responses = responses;
141
+
142
+ return routeDef;
143
+ }
144
+
145
+ /**
146
+ * Auto-mount all Fortress endpoints on an OpenAPI-compatible Hono app.
147
+ *
148
+ * Schema-library agnostic: you provide the converter.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * import { OpenAPIHono, createRoute } from '@hono/zod-openapi';
153
+ * import { mountFortressOpenAPI } from '@bajustone/fortress/hono';
154
+ * import { jsonSchemaToZod } from '@bajustone/fortress/zod';
155
+ *
156
+ * const app = new OpenAPIHono();
157
+ * mountFortressOpenAPI(app, fortress, {
158
+ * schemaConverter: jsonSchemaToZod,
159
+ * createRoute,
160
+ * });
161
+ * ```
162
+ */
163
+ export function mountFortressOpenAPI<T>(
164
+ app: GenericOpenAPIApp & { openAPIRegistry?: { registerComponent: (type: string, name: string, schema: Record<string, unknown>) => void } },
165
+ fortress: Fortress,
166
+ options: MountOptions<T>,
167
+ ): void {
168
+ const skipPaths = new Set(options.skipPaths ?? []);
169
+
170
+ // Register security schemes if the app supports it
171
+ if (app.openAPIRegistry) {
172
+ app.openAPIRegistry.registerComponent('securitySchemes', 'bearerAuth', {
173
+ type: 'http',
174
+ scheme: 'bearer',
175
+ bearerFormat: 'JWT',
176
+ });
177
+ app.openAPIRegistry.registerComponent('securitySchemes', 'basicAuth', {
178
+ type: 'http',
179
+ scheme: 'basic',
180
+ });
181
+ app.openAPIRegistry.registerComponent('securitySchemes', 'apiKeyAuth', {
182
+ type: 'apiKey',
183
+ name: 'X-API-Key',
184
+ in: 'header',
185
+ });
186
+ }
187
+
188
+ // Custom security scheme registration
189
+ options.registerSecuritySchemes?.(app);
190
+
191
+ for (const ep of fortress.endpoints) {
192
+ if (skipPaths.has(ep.path))
193
+ continue;
194
+
195
+ const routeDef = buildRouteDefinition(ep, options.schemaConverter);
196
+ const route = options.createRoute(routeDef);
197
+ const handler = createAutoHandler(fortress, ep);
198
+
199
+ app.openapi(route, handler);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Get all Fortress endpoint definitions as framework route definitions.
205
+ * Returns a map of `{ [handlerName]: routeDef }`.
206
+ */
207
+ export function getFortressRoutes<T>(
208
+ fortress: Fortress,
209
+ schemaConverter: SchemaConverter<T>,
210
+ createRoute: (def: Record<string, unknown>) => unknown,
211
+ ): Record<string, unknown> {
212
+ const result: Record<string, unknown> = {};
213
+
214
+ for (const ep of fortress.endpoints) {
215
+ const routeDef = buildRouteDefinition(ep, schemaConverter);
216
+ result[ep.handler] = createRoute(routeDef);
217
+ }
218
+
219
+ return result;
220
+ }
221
+
222
+ // ── Auto-handler wiring ──────���──────────────────────────────────────
223
+
224
+ function createAutoHandler(fortress: Fortress, _ep: EndpointDefinition): (c: any) => Promise<Response> {
225
+ return async (c: any) => {
226
+ const raw = c.req?.raw as Request | undefined;
227
+ if (raw)
228
+ return fortress.handleRequest(raw);
229
+
230
+ const request = new Request(c.req.url, {
231
+ method: c.req.method,
232
+ headers: c.req.raw?.headers ?? undefined,
233
+ body: ['GET', 'HEAD'].includes(c.req.method) ? undefined : await c.req.text(),
234
+ });
235
+ return fortress.handleRequest(request);
236
+ };
237
+ }
@@ -0,0 +1,87 @@
1
+ import type { Context, Env, MiddlewareHandler } from 'hono';
2
+ import type { EndpointDefinition } from '../core/endpoint';
3
+ import type { Fortress } from '../core/fortress';
4
+ import type {
5
+ ProtectedRouteContext,
6
+ ProtectedRouteHandler,
7
+ ProtectedRouteTarget,
8
+ ProtectOptions,
9
+ } from '../core/http/protect';
10
+ import { protect } from '../core/http/protect';
11
+
12
+ /**
13
+ * Hono-flavoured host callback. `E` flows in from the endpoint passed to
14
+ * `protectedRoute()`, so `ctx.body` / `ctx.query` / `ctx.params` / `ctx.input`
15
+ * are typed from the endpoint's phantom generics.
16
+ *
17
+ * `HEnv` is Hono's environment generic (renamed from `E` so it no longer
18
+ * collides with the endpoint generic). String targets degrade to the
19
+ * loose default just like the core helper.
20
+ */
21
+ export type HonoProtectedRouteHandler<
22
+
23
+ E extends EndpointDefinition<any, any, any, any> = EndpointDefinition,
24
+ HEnv extends Env = Env,
25
+ TResult = unknown,
26
+ > = (
27
+ c: Context<HEnv>,
28
+ ctx: ProtectedRouteContext<E>,
29
+ ) => TResult | Response | Promise<TResult | Response>;
30
+
31
+ /**
32
+ * Wrap a host-owned Hono route in Fortress's route protection pipeline.
33
+ *
34
+ * Two overloads, mirroring `protect()`:
35
+ *
36
+ * - **Typed target** — passing an `EndpointDefinition` flows its phantom
37
+ * generics through `ctx.body` / `ctx.query` / `ctx.params` / `ctx.input`.
38
+ * - **String target** — passing a unique `handler` name keeps the loose
39
+ * `Record<string, unknown>` / `unknown` typing.
40
+ */
41
+ export function protectedRoute<
42
+
43
+ E extends EndpointDefinition<any, any, any, any>,
44
+ HEnv extends Env = Env,
45
+ TResult = unknown,
46
+ >(
47
+ fortress: Fortress,
48
+ target: E,
49
+ handler: HonoProtectedRouteHandler<E, HEnv, TResult>,
50
+ options?: ProtectOptions,
51
+ ): MiddlewareHandler<HEnv>;
52
+ export function protectedRoute<HEnv extends Env = Env, TResult = unknown>(
53
+ fortress: Fortress,
54
+ target: string,
55
+ handler: HonoProtectedRouteHandler<EndpointDefinition, HEnv, TResult>,
56
+ options?: ProtectOptions,
57
+ ): MiddlewareHandler<HEnv>;
58
+ export function protectedRoute(
59
+ fortress: Fortress,
60
+ target: ProtectedRouteTarget,
61
+
62
+ handler: HonoProtectedRouteHandler<any, any, unknown>,
63
+ options: ProtectOptions = {},
64
+ ): MiddlewareHandler {
65
+ return async (c) => {
66
+ // Cast: core `protect` overloads require a concrete branch; impl is loose.
67
+ const protectedHandler = (protect as (
68
+ f: Fortress,
69
+ t: ProtectedRouteTarget,
70
+ h: ProtectedRouteHandler,
71
+ o?: ProtectOptions,
72
+ ) => (request: Request) => Promise<Response>)(
73
+ fortress,
74
+ target,
75
+ ctx => handler(c, ctx),
76
+ { ...options, method: options.method ?? c.req.method },
77
+ );
78
+ return protectedHandler(c.req.raw);
79
+ };
80
+ }
81
+
82
+ export type {
83
+ ProtectedRouteContext,
84
+ ProtectedRouteHandler,
85
+ ProtectedRouteTarget,
86
+ ProtectOptions,
87
+ };