@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,1314 @@
1
+ // src/hono/convert-routes.ts
2
+ var TRAILING_SLASH_REGEX = /\/$/;
3
+ var RE_LEADING_SLASH = /^\//;
4
+ var RE_PARAM_BRACE = /\{(\w+)\}/g;
5
+ var RE_SLASH = /\//g;
6
+ var RE_NON_WORD = /\W/g;
7
+ function fromOpenAPIPath(path) {
8
+ return path.replace(RE_PARAM_BRACE, ":$1");
9
+ }
10
+ function toOperationId(method, path) {
11
+ const normalized = path.replace(RE_LEADING_SLASH, "").replace(RE_PARAM_BRACE, "$1").replace(RE_SLASH, "_").replace(RE_NON_WORD, "_");
12
+ return `${method.toLowerCase()}_${normalized}`;
13
+ }
14
+ var VALID_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH"]);
15
+ var SECURITY_REVERSE = {
16
+ bearer: "bearer",
17
+ bearerauth: "bearer",
18
+ basic: "basic",
19
+ basicauth: "basic",
20
+ apikey: "apiKey",
21
+ apikeyauth: "apiKey"
22
+ };
23
+ function mapSecurity(security) {
24
+ const result = [];
25
+ for (const entry of security) {
26
+ const keys = Object.keys(entry);
27
+ if (keys.length === 0) {
28
+ result.push("none");
29
+ continue;
30
+ }
31
+ for (const key of keys) {
32
+ const mapped = SECURITY_REVERSE[key.toLowerCase()];
33
+ if (mapped && !result.includes(mapped)) {
34
+ result.push(mapped);
35
+ }
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+ function convertRoutes(routes, options) {
41
+ const { schemaConverter, prefix } = options;
42
+ const result = [];
43
+ for (const route of routes) {
44
+ const method = route.method.toUpperCase();
45
+ if (!VALID_METHODS.has(method))
46
+ continue;
47
+ const rawPath = prefix ? `${prefix.replace(TRAILING_SLASH_REGEX, "")}${route.path}` : route.path;
48
+ const path = fromOpenAPIPath(rawPath);
49
+ const handler = toOperationId(method, rawPath);
50
+ const meta = {
51
+ summary: route.summary ?? ""
52
+ };
53
+ if (route.tags)
54
+ meta.tags = route.tags;
55
+ if (route.description)
56
+ meta.description = route.description;
57
+ if (route.deprecated)
58
+ meta.deprecated = true;
59
+ if (route.security) {
60
+ const mapped = mapSecurity(route.security);
61
+ if (mapped.length > 0)
62
+ meta.security = mapped;
63
+ }
64
+ const input = {};
65
+ let hasInput = false;
66
+ if (route.request?.body?.content?.["application/json"]?.schema) {
67
+ input.body = schemaConverter(route.request.body.content["application/json"].schema);
68
+ hasInput = true;
69
+ }
70
+ if (route.request?.params) {
71
+ input.params = schemaConverter(route.request.params);
72
+ hasInput = true;
73
+ }
74
+ if (route.request?.query) {
75
+ input.query = schemaConverter(route.request.query);
76
+ hasInput = true;
77
+ }
78
+ const responses = {};
79
+ let hasResponses = false;
80
+ for (const [status, resp] of Object.entries(route.responses)) {
81
+ const entry = { description: resp.description };
82
+ if (resp.content?.["application/json"]?.schema) {
83
+ entry.schema = schemaConverter(resp.content["application/json"].schema);
84
+ }
85
+ responses[Number(status)] = entry;
86
+ hasResponses = true;
87
+ }
88
+ const endpoint = {
89
+ method,
90
+ path,
91
+ handler,
92
+ meta
93
+ };
94
+ if (hasInput)
95
+ endpoint.input = input;
96
+ if (hasResponses)
97
+ endpoint.responses = responses;
98
+ result.push(endpoint);
99
+ }
100
+ return result;
101
+ }
102
+
103
+ // src/core/http/match.ts
104
+ function segmentSpecificity(segment) {
105
+ if (segment === "*")
106
+ return 0;
107
+ if (segment.startsWith(":"))
108
+ return 1;
109
+ return 2;
110
+ }
111
+ function canonicalizePath(pathname) {
112
+ const segments = pathname.split("/").filter(Boolean);
113
+ return segments.length === 0 ? "/" : `/${segments.join("/")}`;
114
+ }
115
+ function buildRouteTable(endpoints) {
116
+ const table = endpoints.map((ep) => {
117
+ const segments = canonicalizePath(ep.path).split("/").filter(Boolean);
118
+ const paramNames = [];
119
+ for (const seg of segments) {
120
+ if (seg.startsWith(":"))
121
+ paramNames.push(seg.slice(1));
122
+ }
123
+ return {
124
+ endpoint: ep,
125
+ method: ep.method.toUpperCase(),
126
+ segments,
127
+ paramNames
128
+ };
129
+ });
130
+ return table.map((route, index) => ({ route, index })).sort((a, b) => {
131
+ const length = Math.max(a.route.segments.length, b.route.segments.length);
132
+ for (let i = 0; i < length; i++) {
133
+ const difference = segmentSpecificity(b.route.segments[i] ?? "") - segmentSpecificity(a.route.segments[i] ?? "");
134
+ if (difference !== 0)
135
+ return difference;
136
+ }
137
+ return a.index - b.index;
138
+ }).map(({ route }) => route);
139
+ }
140
+ function matchRoute(table, method, pathname) {
141
+ const upperMethod = method.toUpperCase();
142
+ const pathSegments = canonicalizePath(pathname).split("/").filter(Boolean);
143
+ for (const route of table) {
144
+ if (route.method !== upperMethod)
145
+ continue;
146
+ if (route.segments.length !== pathSegments.length)
147
+ continue;
148
+ const params = {};
149
+ let matched = true;
150
+ for (let i = 0; i < route.segments.length; i++) {
151
+ const routeSeg = route.segments[i];
152
+ const pathSeg = pathSegments[i];
153
+ if (routeSeg.startsWith(":")) {
154
+ params[routeSeg.slice(1)] = decodeURIComponent(pathSeg);
155
+ } else if (routeSeg !== "*" && routeSeg !== pathSeg) {
156
+ matched = false;
157
+ break;
158
+ }
159
+ }
160
+ if (matched)
161
+ return { endpoint: route.endpoint, params };
162
+ }
163
+ return null;
164
+ }
165
+
166
+ // src/express/handle.ts
167
+ function mountFortress(app, fortress, options = {}) {
168
+ const prefix = options.prefix ?? "";
169
+ const routeTable = buildRouteTable(fortress.manifest.filter((route) => route.mounted));
170
+ const middleware = async (req, res, next) => {
171
+ let pathname = req.path;
172
+ if (prefix && pathname.startsWith(prefix)) {
173
+ pathname = pathname.slice(prefix.length) || "/";
174
+ } else if (prefix) {
175
+ next();
176
+ return;
177
+ }
178
+ if (!matchRoute(routeTable, req.method, pathname)) {
179
+ next();
180
+ return;
181
+ }
182
+ try {
183
+ const request = expressToWebRequest(req, pathname);
184
+ const response = await fortress.handleRequest(request);
185
+ await sendWebResponseToExpress(response, res);
186
+ } catch (err) {
187
+ next(err);
188
+ }
189
+ };
190
+ app.use(middleware);
191
+ }
192
+ function expressToWebRequest(req, pathname) {
193
+ const host = req.headers.host ?? "localhost";
194
+ const protocol = req.protocol ?? "http";
195
+ const search = (req.originalUrl ?? "").includes("?") ? `?${req.originalUrl.split("?")[1]}` : "";
196
+ const url = `${protocol}://${host}${pathname}${search}`;
197
+ const headers = new Headers();
198
+ for (const [k, v] of Object.entries(req.headers)) {
199
+ if (Array.isArray(v)) {
200
+ for (const item of v) headers.append(k, item);
201
+ } else if (typeof v === "string") {
202
+ headers.set(k, v);
203
+ }
204
+ }
205
+ const init = { method: req.method, headers };
206
+ if (req.method !== "GET" && req.method !== "HEAD") {
207
+ const body = req.body;
208
+ if (body !== void 0 && body !== null) {
209
+ if (typeof body === "string") {
210
+ init.body = body;
211
+ } else if (body instanceof Uint8Array) {
212
+ init.body = body;
213
+ } else if ((headers.get("content-type") ?? "").toLowerCase().includes("application/x-www-form-urlencoded")) {
214
+ const form = new URLSearchParams();
215
+ for (const [key, value] of Object.entries(body)) {
216
+ const values = Array.isArray(value) ? value : [value];
217
+ for (const item of values) {
218
+ if (item !== void 0 && item !== null)
219
+ form.append(key, String(item));
220
+ }
221
+ }
222
+ init.body = form.toString();
223
+ } else {
224
+ init.body = JSON.stringify(body);
225
+ if (!headers.has("content-type")) {
226
+ headers.set("content-type", "application/json");
227
+ }
228
+ }
229
+ }
230
+ }
231
+ return new Request(url, init);
232
+ }
233
+ async function sendWebResponseToExpress(response, res) {
234
+ res.status(response.status);
235
+ const setCookies = response.headers.getSetCookie();
236
+ for (const [k, v] of response.headers) {
237
+ if (k.toLowerCase() === "set-cookie")
238
+ continue;
239
+ res.setHeader(k, v);
240
+ }
241
+ if (setCookies.length > 0) {
242
+ res.setHeader("set-cookie", setCookies);
243
+ }
244
+ const text = await response.text();
245
+ const isJson = (response.headers.get("content-type") ?? "").includes("json");
246
+ if (isJson) {
247
+ try {
248
+ res.json(JSON.parse(text));
249
+ } catch {
250
+ res.json({ raw: text });
251
+ }
252
+ } else {
253
+ const resWithSend = res;
254
+ if (resWithSend.send) {
255
+ resWithSend.send(text);
256
+ } else {
257
+ res.json(text);
258
+ }
259
+ }
260
+ }
261
+
262
+ // src/core/errors.ts
263
+ var FortressError = class extends Error {
264
+ code;
265
+ statusCode;
266
+ retryAfter;
267
+ details;
268
+ /** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
269
+ oauthError;
270
+ /** Human-readable description for the OAuth `error_description` field. */
271
+ oauthDescription;
272
+ /** Optional URI for the OAuth `error_uri` field (§5.2). */
273
+ oauthErrorUri;
274
+ constructor(code, message, statusCode, options) {
275
+ super(message, { cause: options?.cause });
276
+ this.code = code;
277
+ this.statusCode = statusCode;
278
+ this.retryAfter = options?.retryAfter;
279
+ this.details = options?.details;
280
+ this.oauthError = options?.oauthError;
281
+ this.oauthDescription = options?.oauthDescription;
282
+ this.oauthErrorUri = options?.oauthErrorUri;
283
+ }
284
+ toJSON() {
285
+ return {
286
+ code: this.code,
287
+ message: this.message,
288
+ statusCode: this.statusCode,
289
+ ...this.details !== void 0 && { details: this.details }
290
+ };
291
+ }
292
+ };
293
+ var Errors = {
294
+ unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
295
+ tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
296
+ sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
297
+ sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
298
+ forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
299
+ badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
300
+ notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
301
+ conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
302
+ unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
303
+ rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
304
+ database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
305
+ serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
306
+ validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
307
+ /**
308
+ * RFC 6749 §5.2 / §4.1.2.1 OAuth error.
309
+ *
310
+ * The HTTP error mapper detects `oauthError` and emits the OAuth-spec
311
+ * JSON body `{ error, error_description, error_uri? }` instead of the
312
+ * default fortress `{ code, message }` shape, so strict OAuth clients
313
+ * (Moodle, openid-client, Spring Security, etc.) can switch behaviour
314
+ * on the machine-readable `error` field.
315
+ *
316
+ * Status is inferred from the OAuth code per the spec table:
317
+ * - `invalid_client` → 401
318
+ * - everything else → 400
319
+ */
320
+ oauth: (error, description, options) => {
321
+ const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
322
+ const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
323
+ return new FortressError(code, description ?? error, status, {
324
+ oauthError: error,
325
+ oauthDescription: description,
326
+ oauthErrorUri: options?.errorUri,
327
+ cause: options?.cause
328
+ });
329
+ },
330
+ /**
331
+ * Reconstruct a {@link FortressError} from a JSON error body emitted by
332
+ * `errorToResponse`. Used by the in-process `fortress.call.*` client to
333
+ * convert non-2xx responses into typed throws that mirror what a direct
334
+ * service-method call would produce.
335
+ *
336
+ * Maps by HTTP status when the body doesn't carry a recognizable
337
+ * `{ code, message }` payload, so network errors and opaque upstream
338
+ * failures still become structured `FortressError`s.
339
+ */
340
+ fromHttpResponse: (status, body) => {
341
+ const payload = body && typeof body === "object" ? body : {};
342
+ const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
343
+ const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
344
+ return new FortressError(code, message, status, { details: payload.details });
345
+ }
346
+ };
347
+ function isFortressErrorCode(code) {
348
+ return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
349
+ }
350
+ function statusToErrorCode(status) {
351
+ if (status === 401)
352
+ return "UNAUTHORIZED";
353
+ if (status === 403)
354
+ return "FORBIDDEN";
355
+ if (status === 404)
356
+ return "NOT_FOUND";
357
+ if (status === 409)
358
+ return "CONFLICT";
359
+ if (status === 422)
360
+ return "UNPROCESSABLE_ENTITY";
361
+ if (status === 429)
362
+ return "RATE_LIMITED";
363
+ if (status === 503)
364
+ return "SERVICE_UNAVAILABLE";
365
+ if (status >= 500)
366
+ return "DATABASE_ERROR";
367
+ return "BAD_REQUEST";
368
+ }
369
+
370
+ // src/core/plugin-runner.ts
371
+ function chainAdapterWrappers(plugins, baseAdapter, requestContext) {
372
+ let adapter = baseAdapter;
373
+ for (const plugin of plugins) {
374
+ if (plugin.wrapAdapter) {
375
+ adapter = plugin.wrapAdapter(adapter, requestContext);
376
+ }
377
+ }
378
+ return adapter;
379
+ }
380
+ async function collectScopeRules(plugins, userId, model, ctx) {
381
+ const allFilters = [];
382
+ const allDefaults = {};
383
+ for (const plugin of plugins) {
384
+ if (!plugin.scopeRules) {
385
+ continue;
386
+ }
387
+ const rule = await plugin.scopeRules(userId, model, ctx);
388
+ if (!rule) {
389
+ continue;
390
+ }
391
+ allFilters.push(...rule.filters);
392
+ Object.assign(allDefaults, rule.defaults);
393
+ }
394
+ if (allFilters.length === 0 && Object.keys(allDefaults).length === 0) {
395
+ return null;
396
+ }
397
+ return { filters: allFilters, defaults: allDefaults };
398
+ }
399
+ function wrapAdapterWithScopeRules(adapter, scopeRule) {
400
+ const { filters, defaults } = scopeRule;
401
+ return {
402
+ ...adapter,
403
+ create: (params) => adapter.create({
404
+ ...params,
405
+ data: { ...params.data, ...defaults }
406
+ }),
407
+ findOne: (params) => adapter.findOne({
408
+ ...params,
409
+ where: [...params.where, ...filters]
410
+ }),
411
+ findMany: (params) => adapter.findMany({
412
+ ...params,
413
+ where: [...params.where ?? [], ...filters]
414
+ }),
415
+ update: (params) => Promise.resolve().then(() => {
416
+ for (const key of Object.keys(defaults)) {
417
+ if (Object.hasOwn(params.data, key))
418
+ throw Errors.forbidden(`Cannot update scoped field '${key}'`);
419
+ }
420
+ return adapter.update({
421
+ ...params,
422
+ where: [...params.where, ...filters]
423
+ });
424
+ }),
425
+ delete: (params) => adapter.delete({
426
+ ...params,
427
+ where: [...params.where, ...filters]
428
+ }),
429
+ count: (params) => adapter.count({
430
+ ...params,
431
+ where: [...params.where ?? [], ...filters]
432
+ }),
433
+ transaction: (fn) => adapter.transaction((tx) => fn(wrapAdapterWithScopeRules(tx, scopeRule)))
434
+ };
435
+ }
436
+ function middlewarePathToRegex(pattern) {
437
+ const regexStr = canonicalizePath(pattern).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+").replace(/\\\*/g, ".*").replace(/\//g, "\\/");
438
+ return new RegExp(`^${regexStr}$`);
439
+ }
440
+ function collectPluginMiddleware(plugins, position) {
441
+ const result = [];
442
+ for (const plugin of plugins) {
443
+ if (!plugin.middleware)
444
+ continue;
445
+ for (const mw of plugin.middleware) {
446
+ if (mw.position === position) {
447
+ result.push({ plugin, middleware: mw });
448
+ }
449
+ }
450
+ }
451
+ return result;
452
+ }
453
+ async function executePluginMiddleware(plugins, position, requestPath, ctx, request) {
454
+ const canonicalRequestPath = canonicalizePath(requestPath);
455
+ const matching = collectPluginMiddleware(plugins, position).filter(({ middleware: mw }) => middlewarePathToRegex(mw.path).test(canonicalRequestPath));
456
+ if (matching.length === 0)
457
+ return;
458
+ let index = 0;
459
+ async function runNext() {
460
+ if (index >= matching.length)
461
+ return;
462
+ const current = matching[index++];
463
+ await current.middleware.handler(ctx, request, runNext);
464
+ }
465
+ await runNext();
466
+ }
467
+
468
+ // src/express/middleware.ts
469
+ function createAuthMiddleware(fortress) {
470
+ return async (req, _res, next) => {
471
+ try {
472
+ const headerJar = new Headers();
473
+ for (const [k, v] of Object.entries(req.headers)) {
474
+ if (Array.isArray(v)) {
475
+ for (const item of v) headerJar.append(k, item);
476
+ } else if (typeof v === "string") {
477
+ headerJar.set(k, v);
478
+ }
479
+ }
480
+ const requestPath = expressRequestPath(req);
481
+ const host = expressHeader(req, "host") ?? "localhost";
482
+ const protocol = req.protocol ?? expressHeader(req, "x-forwarded-proto") ?? "http";
483
+ const probe = new Request(`${protocol}://${host}${requestPath}`, {
484
+ method: req.method,
485
+ headers: headerJar
486
+ });
487
+ const resolved = await fortress.resolvePrincipal(probe);
488
+ if (!resolved) {
489
+ throw new FortressError("UNAUTHORIZED", "Missing or invalid credentials", 401);
490
+ }
491
+ const { subject, claims, scopes } = resolved;
492
+ req.fortressSubject = subject;
493
+ if (subject.type === "USER")
494
+ req.fortressUserId = subject.id;
495
+ if (claims)
496
+ req.fortressClaims = claims;
497
+ if (scopes !== void 0)
498
+ req.fortressScopes = scopes;
499
+ const plugins = fortress.config.plugins ?? [];
500
+ const requestContext = {
501
+ tenantId: claims?.customClaims?.tenantId,
502
+ ipAddress: req.headers["x-forwarded-for"] ?? req.headers["x-real-ip"],
503
+ userAgent: req.headers["user-agent"]
504
+ };
505
+ const wrappedAdapter = chainAdapterWrappers(plugins, fortress.config.database, requestContext);
506
+ req.fortressDb = wrappedAdapter;
507
+ const pluginCtx = { db: wrappedAdapter, config: fortress.config };
508
+ req.fortressGetScopedDb = async (model) => {
509
+ const scopeRule = await collectScopeRules(plugins, subject.id, model, pluginCtx);
510
+ if (!scopeRule)
511
+ return wrappedAdapter;
512
+ return wrapAdapterWithScopeRules(wrappedAdapter, scopeRule);
513
+ };
514
+ next();
515
+ } catch (err) {
516
+ next(err);
517
+ }
518
+ };
519
+ }
520
+ function createCsrfMiddleware(config) {
521
+ const headerName = (config?.headerName ?? "X-Fortress-CSRF").toLowerCase();
522
+ const skipPaths = config?.skipPaths ?? [];
523
+ const safeMethods = new Set((config?.safeMethods ?? ["GET", "HEAD", "OPTIONS"]).map((method) => method.toUpperCase()));
524
+ return (req, _res, next) => {
525
+ try {
526
+ if (safeMethods.has(req.method.toUpperCase()) || matchesCsrfSkipPath(req.path, skipPaths)) {
527
+ next();
528
+ return;
529
+ }
530
+ const fetchSite = expressHeader(req, "sec-fetch-site");
531
+ if (fetchSite === "cross-site")
532
+ throw new FortressError("FORBIDDEN", "CSRF: cross-site request rejected", 403);
533
+ if (!expressHeader(req, headerName))
534
+ throw new FortressError("FORBIDDEN", `CSRF: missing ${config?.headerName ?? "X-Fortress-CSRF"} header`, 403);
535
+ next();
536
+ } catch (error) {
537
+ next(error);
538
+ }
539
+ };
540
+ }
541
+ function createRbacMiddleware(fortress, options) {
542
+ const routeMap = normalizeRouteMap(options?.routeMap ?? {});
543
+ const skipPaths = options?.skipPaths ?? [];
544
+ const skipPatterns = skipPaths.map((p) => pathToRegex(p));
545
+ const unmappedRoutes = options?.unmappedRoutes ?? "allow";
546
+ return async (req, _res, next) => {
547
+ try {
548
+ const path = normalizeExpressPath(new URL(expressRequestPath(req), "http://localhost").pathname);
549
+ const method = req.method;
550
+ if (skipPatterns.some((pattern) => pattern.test(path))) {
551
+ next();
552
+ return;
553
+ }
554
+ const key = `${method} ${path}`;
555
+ let mapping = routeMap[key] ?? null;
556
+ if (!mapping) {
557
+ mapping = findRouteMapMatch(method, path, routeMap);
558
+ }
559
+ if (!mapping && options?.mapRequest) {
560
+ mapping = options.mapRequest(method, path);
561
+ }
562
+ if (!mapping) {
563
+ if (unmappedRoutes === "deny") {
564
+ throw new FortressError("FORBIDDEN", "No permission mapping for this route", 403);
565
+ }
566
+ next();
567
+ return;
568
+ }
569
+ if (!req.fortressSubject) {
570
+ throw new FortressError("UNAUTHORIZED", "Not authenticated", 401);
571
+ }
572
+ const allowed = await fortress.iam.checkPermission(req.fortressSubject, mapping.resource, mapping.action, {
573
+ credentialScopes: req.fortressScopes
574
+ });
575
+ if (!allowed) {
576
+ throw new FortressError("FORBIDDEN", "Insufficient permissions", 403);
577
+ }
578
+ next();
579
+ } catch (err) {
580
+ next(err);
581
+ }
582
+ };
583
+ }
584
+ function createErrorHandler(fortress) {
585
+ return (err, _req, res, _next) => {
586
+ if (err instanceof FortressError) {
587
+ if (err.code === "RATE_LIMITED" && err.retryAfter) {
588
+ res.setHeader("Retry-After", String(err.retryAfter));
589
+ }
590
+ res.status(err.statusCode).json(err.toJSON());
591
+ return;
592
+ }
593
+ fortress?.logger.error(
594
+ { err, message: err instanceof Error ? err.message : "Unknown error" },
595
+ "unhandled error in express middleware"
596
+ );
597
+ res.status(500).json({ code: "INTERNAL_ERROR", message: "Internal server error", statusCode: 500 });
598
+ };
599
+ }
600
+ function createExpressPluginMiddleware(fortress, position) {
601
+ const plugins = fortress.config.plugins ?? [];
602
+ return async (req, _res, next) => {
603
+ try {
604
+ const ctx = { db: fortress.config.database, config: fortress.config };
605
+ const headers = new Headers();
606
+ for (const [name, value] of Object.entries(req.headers)) {
607
+ if (Array.isArray(value)) {
608
+ for (const item of value) headers.append(name, item);
609
+ } else if (typeof value === "string") {
610
+ headers.set(name, value);
611
+ }
612
+ }
613
+ const requestPath = expressRequestPath(req);
614
+ const forwardedProtocol = req.headers["x-forwarded-proto"];
615
+ const protocol = req.protocol ?? (Array.isArray(forwardedProtocol) ? forwardedProtocol[0] : forwardedProtocol) ?? "http";
616
+ const hostHeader = req.headers.host;
617
+ const host = (Array.isArray(hostHeader) ? hostHeader[0] : hostHeader) ?? "localhost";
618
+ const method = req.method.toUpperCase();
619
+ let body;
620
+ if (method !== "GET" && method !== "HEAD" && req.body !== void 0) {
621
+ if (typeof req.body === "string" || req.body instanceof URLSearchParams)
622
+ body = req.body;
623
+ else if (req.body instanceof ArrayBuffer || ArrayBuffer.isView(req.body))
624
+ body = req.body;
625
+ else if (headers.get("content-type")?.toLowerCase().startsWith("application/x-www-form-urlencoded"))
626
+ body = new URLSearchParams(Object.entries(req.body).map(([key, value]) => [key, String(value)]));
627
+ else
628
+ body = JSON.stringify(req.body);
629
+ }
630
+ const requestContext = {
631
+ request: new Request(`${protocol}://${host}${requestPath}`, {
632
+ method,
633
+ headers,
634
+ body
635
+ }),
636
+ fortressSubject: req.fortressSubject,
637
+ fortressUserId: req.fortressUserId,
638
+ fortressClaims: req.fortressClaims,
639
+ fortressScopes: req.fortressScopes
640
+ };
641
+ await executePluginMiddleware(
642
+ plugins,
643
+ position,
644
+ new URL(requestPath, "http://localhost").pathname,
645
+ ctx,
646
+ requestContext
647
+ );
648
+ next();
649
+ } catch (err) {
650
+ next(err);
651
+ }
652
+ };
653
+ }
654
+ function createExpressMiddleware(fortress, options) {
655
+ return {
656
+ authMiddleware: createAuthMiddleware(fortress),
657
+ csrfMiddleware: createCsrfMiddleware(options?.csrf),
658
+ rbacMiddleware: createRbacMiddleware(fortress, options),
659
+ errorHandler: createErrorHandler(fortress),
660
+ pluginMiddleware: {
661
+ beforeAuth: createExpressPluginMiddleware(fortress, "before-auth"),
662
+ afterAuth: createExpressPluginMiddleware(fortress, "after-auth"),
663
+ afterRbac: createExpressPluginMiddleware(fortress, "after-rbac")
664
+ }
665
+ };
666
+ }
667
+ function getSubject(req) {
668
+ if (!req.fortressSubject) {
669
+ throw new FortressError("UNAUTHORIZED", "Not authenticated", 401);
670
+ }
671
+ return req.fortressSubject;
672
+ }
673
+ function getUserId(req) {
674
+ if (!req.fortressSubject || req.fortressSubject.type !== "USER") {
675
+ throw new FortressError("UNAUTHORIZED", "User not authenticated", 401);
676
+ }
677
+ return req.fortressSubject.id;
678
+ }
679
+ function getClaims(req) {
680
+ if (!req.fortressClaims) {
681
+ throw new FortressError("UNAUTHORIZED", "No JWT claims on this request", 401);
682
+ }
683
+ return req.fortressClaims;
684
+ }
685
+ function getDb(req) {
686
+ if (!req.fortressDb) {
687
+ throw new FortressError("UNAUTHORIZED", "User not authenticated", 401);
688
+ }
689
+ return req.fortressDb;
690
+ }
691
+ function getScopedDb(req, model) {
692
+ if (!req.fortressGetScopedDb) {
693
+ throw new FortressError("UNAUTHORIZED", "User not authenticated", 401);
694
+ }
695
+ return req.fortressGetScopedDb(model);
696
+ }
697
+ function findRouteMapMatch(method, path, routeMap) {
698
+ for (const [pattern, mapping] of Object.entries(routeMap)) {
699
+ const [patternMethod, patternPath] = pattern.split(" ", 2);
700
+ if (patternMethod !== method)
701
+ continue;
702
+ if (pathToRegex(patternPath).test(path))
703
+ return mapping;
704
+ }
705
+ return null;
706
+ }
707
+ function expressRequestPath(req) {
708
+ const path = req.originalUrl ?? req.url ?? req.path;
709
+ return path.startsWith("/") ? path : `/${path}`;
710
+ }
711
+ function expressHeader(req, name) {
712
+ const target = name.toLowerCase();
713
+ for (const [headerName, value] of Object.entries(req.headers)) {
714
+ if (headerName.toLowerCase() !== target)
715
+ continue;
716
+ return Array.isArray(value) ? value[0] : value;
717
+ }
718
+ return void 0;
719
+ }
720
+ function matchesCsrfSkipPath(path, skipPaths) {
721
+ return skipPaths.some((configured) => {
722
+ const withoutWildcard = configured.endsWith("/*") ? configured.slice(0, -2) : configured;
723
+ const base = withoutWildcard.length > 1 && withoutWildcard.endsWith("/") ? withoutWildcard.slice(0, -1) : withoutWildcard;
724
+ return path === base || path.startsWith(`${base}/`);
725
+ });
726
+ }
727
+ function normalizeRouteMap(routeMap) {
728
+ return Object.fromEntries(Object.entries(routeMap).map(([pattern, mapping]) => {
729
+ const separator = pattern.indexOf(" ");
730
+ if (separator < 0)
731
+ return [pattern, mapping];
732
+ const method = pattern.slice(0, separator);
733
+ const path = normalizeExpressPath(pattern.slice(separator + 1));
734
+ return [`${method} ${path}`, mapping];
735
+ }));
736
+ }
737
+ function normalizeExpressPath(path) {
738
+ const lowerCased = path.toLowerCase();
739
+ return lowerCased.length > 1 && lowerCased.endsWith("/") ? lowerCased.slice(0, -1) : lowerCased;
740
+ }
741
+ function pathToRegex(pattern) {
742
+ const regexStr = normalizeExpressPath(pattern).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+").replace(/\\\*/g, ".*").replace(/\//g, "\\/");
743
+ return new RegExp(`^${regexStr}$`, "i");
744
+ }
745
+
746
+ // src/core/validation.ts
747
+ import { fromJSONSchema } from "@bajustone/fetcher/openapi";
748
+ var DECIMAL_INTEGER_RE = /^-?\d+$/;
749
+ var DECIMAL_NUMBER_RE = /^-?\d+(?:\.\d+)?$/;
750
+ var rawSchemaValidators = /* @__PURE__ */ new WeakMap();
751
+ function standardSchemaFor(schema) {
752
+ if ("~standard" in schema)
753
+ return schema;
754
+ let compiled = rawSchemaValidators.get(schema);
755
+ if (!compiled) {
756
+ compiled = fromJSONSchema(schema);
757
+ rawSchemaValidators.set(schema, compiled);
758
+ }
759
+ return compiled;
760
+ }
761
+ function coerceBySchema(schema, data) {
762
+ if (!schema || !data || schema.type !== "object" || !schema.properties)
763
+ return data;
764
+ const out = { ...data };
765
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
766
+ const value = out[key];
767
+ if (typeof value !== "string")
768
+ continue;
769
+ const coerced = coerceScalar(propSchema, value);
770
+ if (coerced !== void 0)
771
+ out[key] = coerced;
772
+ }
773
+ return out;
774
+ }
775
+ function coerceScalar(schema, value) {
776
+ const type = schema.type ?? numericConstraintType(schema);
777
+ switch (type) {
778
+ case "integer": {
779
+ if (!DECIMAL_INTEGER_RE.test(value))
780
+ return void 0;
781
+ const n = Number(value);
782
+ if (Number.isFinite(n) && Number.isSafeInteger(n) && String(n) === value)
783
+ return n;
784
+ return void 0;
785
+ }
786
+ case "number": {
787
+ if (!DECIMAL_NUMBER_RE.test(value))
788
+ return void 0;
789
+ const n = Number(value);
790
+ if (Number.isFinite(n) && (!Number.isInteger(n) || Number.isSafeInteger(n)) && String(n) === value)
791
+ return n;
792
+ return void 0;
793
+ }
794
+ case "boolean": {
795
+ if (value === "true")
796
+ return true;
797
+ if (value === "false")
798
+ return false;
799
+ return void 0;
800
+ }
801
+ default:
802
+ return void 0;
803
+ }
804
+ }
805
+ function numericConstraintType(schema) {
806
+ if (typeof schema.const === "number")
807
+ return Number.isInteger(schema.const) ? "integer" : "number";
808
+ if (schema.enum && schema.enum.length > 0 && schema.enum.every((value) => typeof value === "number"))
809
+ return schema.enum.every((value) => Number.isInteger(value)) ? "integer" : "number";
810
+ return void 0;
811
+ }
812
+ async function validateRequest(input, data) {
813
+ if (!input)
814
+ return;
815
+ const allIssues = [];
816
+ if (input.bodySchema || input.body) {
817
+ const issues = await validateSchema(input.bodySchema ?? input.body, data.body, "body");
818
+ allIssues.push(...issues);
819
+ }
820
+ if (input.querySchema || input.query) {
821
+ const issues = await validateSchema(input.querySchema ?? input.query, data.query, "query");
822
+ allIssues.push(...issues);
823
+ }
824
+ if (input.paramsSchema || input.params) {
825
+ const issues = await validateSchema(input.paramsSchema ?? input.params, data.params, "params");
826
+ allIssues.push(...issues);
827
+ }
828
+ if (allIssues.length > 0) {
829
+ throw Errors.validationError(allIssues);
830
+ }
831
+ }
832
+ async function validateSchema(schema, data, location) {
833
+ const result = await standardSchemaFor(schema)["~standard"].validate(data);
834
+ if (result.issues) {
835
+ return result.issues.map((issue) => ({
836
+ path: issue.path,
837
+ message: issue.message,
838
+ location
839
+ }));
840
+ }
841
+ return [];
842
+ }
843
+ async function validateValue(schema, data, location) {
844
+ const result = await schema["~standard"].validate(data);
845
+ if (result.issues) {
846
+ throw Errors.validationError(
847
+ result.issues.map((issue) => ({
848
+ path: issue.path,
849
+ message: issue.message,
850
+ location
851
+ }))
852
+ );
853
+ }
854
+ return result.value;
855
+ }
856
+
857
+ // src/core/http/cookie-serialize.ts
858
+ function parseCookieHeader(header) {
859
+ if (!header)
860
+ return {};
861
+ const out = {};
862
+ for (const part of header.split(";")) {
863
+ const eq = part.indexOf("=");
864
+ if (eq === -1)
865
+ continue;
866
+ const name = part.slice(0, eq).trim();
867
+ if (!name)
868
+ continue;
869
+ const raw = part.slice(eq + 1).trim();
870
+ try {
871
+ out[name] = decodeURIComponent(raw);
872
+ } catch {
873
+ out[name] = raw;
874
+ }
875
+ }
876
+ return out;
877
+ }
878
+
879
+ // src/core/http/csrf.ts
880
+ function resolveCsrfConfig(config) {
881
+ return {
882
+ enabled: config?.enabled ?? true,
883
+ headerName: config?.headerName ?? "X-Fortress-CSRF",
884
+ skipPaths: config?.skipPaths ?? [],
885
+ rejectSameSite: config?.rejectSameSite ?? false
886
+ };
887
+ }
888
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
889
+ function matchesSkipPath(path, skipPaths) {
890
+ for (const skip of skipPaths) {
891
+ if (path === skip)
892
+ return true;
893
+ if (path.startsWith(`${skip}/`))
894
+ return true;
895
+ }
896
+ return false;
897
+ }
898
+ function isCookieAuthenticated(request, cookies) {
899
+ const jar = parseCookieHeader(request.headers.get("cookie"));
900
+ const hasCookie = (name) => typeof jar[name] === "string" && jar[name].length > 0;
901
+ return hasCookie(cookies.accessName) || hasCookie(cookies.refreshName);
902
+ }
903
+ function enforceCsrf(request, pathname, csrf, cookies) {
904
+ if (!csrf.enabled)
905
+ return;
906
+ if (SAFE_METHODS.has(request.method.toUpperCase()))
907
+ return;
908
+ if (matchesSkipPath(pathname, csrf.skipPaths))
909
+ return;
910
+ const fetchSite = request.headers.get("sec-fetch-site");
911
+ const cookieAuthenticated = isCookieAuthenticated(request, cookies);
912
+ if (!cookieAuthenticated) {
913
+ if (request.headers.has("authorization") || request.headers.has("x-api-key"))
914
+ return;
915
+ if (fetchSite === "cross-site")
916
+ throw Errors.forbidden("CSRF: cross-site request rejected");
917
+ const origin = request.headers.get("origin");
918
+ if (origin) {
919
+ try {
920
+ if (new URL(origin).origin !== new URL(request.url).origin)
921
+ throw Errors.forbidden("CSRF: cross-origin request rejected");
922
+ } catch (error) {
923
+ if (error instanceof Error && "code" in error)
924
+ throw error;
925
+ throw Errors.forbidden("CSRF: invalid Origin header");
926
+ }
927
+ }
928
+ return;
929
+ }
930
+ if (fetchSite === "cross-site")
931
+ throw Errors.forbidden("CSRF: cross-site request rejected");
932
+ if (csrf.rejectSameSite && fetchSite === "same-site")
933
+ throw Errors.forbidden("CSRF: same-site request rejected");
934
+ const tokenHeader = request.headers.get(csrf.headerName);
935
+ if (!tokenHeader)
936
+ throw Errors.forbidden(`CSRF: missing ${csrf.headerName} header`);
937
+ }
938
+
939
+ // src/core/http/error-response.ts
940
+ function errorToResponse(err, logger) {
941
+ if (err instanceof FortressError) {
942
+ const headers = { "Content-Type": "application/json" };
943
+ if (err.code === "RATE_LIMITED" && err.retryAfter !== void 0) {
944
+ headers["Retry-After"] = String(err.retryAfter);
945
+ }
946
+ if (err.oauthError) {
947
+ const body2 = { error: err.oauthError };
948
+ if (err.oauthDescription)
949
+ body2.error_description = err.oauthDescription;
950
+ if (err.oauthErrorUri)
951
+ body2.error_uri = err.oauthErrorUri;
952
+ return new Response(JSON.stringify(body2), {
953
+ status: err.statusCode,
954
+ headers
955
+ });
956
+ }
957
+ return new Response(JSON.stringify(err.toJSON()), {
958
+ status: err.statusCode,
959
+ headers
960
+ });
961
+ }
962
+ logger?.error(
963
+ { err, message: err instanceof Error ? err.message : "Unknown error" },
964
+ "unhandled error in fortress.handleRequest"
965
+ );
966
+ const body = {
967
+ code: "INTERNAL_ERROR",
968
+ message: "Internal server error",
969
+ statusCode: 500
970
+ };
971
+ return new Response(JSON.stringify(body), {
972
+ status: 500,
973
+ headers: { "Content-Type": "application/json" }
974
+ });
975
+ }
976
+ function withCookies(response, setCookies) {
977
+ if (setCookies.length === 0)
978
+ return response;
979
+ const headers = new Headers(response.headers);
980
+ for (const cookie of setCookies) {
981
+ headers.append("set-cookie", cookie);
982
+ }
983
+ return new Response(response.body, {
984
+ status: response.status,
985
+ statusText: response.statusText,
986
+ headers
987
+ });
988
+ }
989
+
990
+ // src/core/http/fortress-rbac.ts
991
+ async function enforceFortressPermission(endpoint, subject, enforcement, scopes) {
992
+ const security = endpoint.meta?.security;
993
+ if (endpoint.meta?.permission) {
994
+ if (!subject)
995
+ throw Errors.unauthorized("Not authenticated");
996
+ const allowed = await enforcement.checkPermission(
997
+ subject,
998
+ endpoint.meta.permission.resource,
999
+ endpoint.meta.permission.action,
1000
+ scopes
1001
+ );
1002
+ if (!allowed)
1003
+ throw Errors.forbidden("Insufficient permissions");
1004
+ return;
1005
+ }
1006
+ if (security?.includes("none") || security?.includes("basic"))
1007
+ return;
1008
+ if (security?.includes("bearer")) {
1009
+ if (!subject)
1010
+ throw Errors.unauthorized("Not authenticated");
1011
+ return;
1012
+ }
1013
+ throw Errors.forbidden("No permission mapping for this route");
1014
+ }
1015
+
1016
+ // src/core/http/plugin-middleware.ts
1017
+ async function runPluginMiddleware(plugins, config, phase, ctx) {
1018
+ if (plugins.length === 0)
1019
+ return;
1020
+ const pluginCtx = { db: config.database, config };
1021
+ const path = new URL(ctx.request.url).pathname;
1022
+ await executePluginMiddleware(plugins, phase, path, pluginCtx, ctx);
1023
+ }
1024
+
1025
+ // src/core/http/principal.ts
1026
+ async function tryPluginPrincipal(fortress, request) {
1027
+ const plugins = fortress.config.plugins ?? [];
1028
+ for (const plugin of plugins) {
1029
+ if (!plugin.resolvePrincipal)
1030
+ continue;
1031
+ const resolved = await plugin.resolvePrincipal(request, {
1032
+ db: fortress.config.database,
1033
+ config: fortress.config,
1034
+ auth: fortress.auth,
1035
+ iam: fortress.iam
1036
+ });
1037
+ if (resolved)
1038
+ return { subject: resolved.subject, claims: resolved.claims, scopes: resolved.scopes };
1039
+ }
1040
+ return null;
1041
+ }
1042
+
1043
+ // src/core/http/protect.ts
1044
+ function routeKey(route) {
1045
+ return `${route.method.toUpperCase()} ${route.path}`;
1046
+ }
1047
+ function findEndpoint(fortress, target, method) {
1048
+ if (typeof target !== "string") {
1049
+ return target;
1050
+ }
1051
+ const matches = fortress.endpoints.filter((endpoint) => endpoint.handler === target);
1052
+ if (matches.length === 0)
1053
+ throw Errors.notFound(`No endpoint found for handler '${target}'`);
1054
+ if (method) {
1055
+ const byMethod = matches.find((endpoint) => endpoint.method.toUpperCase() === method.toUpperCase());
1056
+ if (byMethod)
1057
+ return byMethod;
1058
+ }
1059
+ if (matches.length > 1) {
1060
+ throw Errors.badRequest(
1061
+ `Handler '${target}' maps to multiple endpoints; pass the EndpointDefinition or ProtectOptions.method.`
1062
+ );
1063
+ }
1064
+ return matches[0];
1065
+ }
1066
+ function findManifestEntry(fortress, endpoint) {
1067
+ const key = routeKey(endpoint);
1068
+ const entry = fortress.manifest.find((route) => `${route.method.toUpperCase()} ${route.path}` === key);
1069
+ if (!entry)
1070
+ throw Errors.notFound(`No route manifest entry found for ${key}`);
1071
+ return entry;
1072
+ }
1073
+ function rewriteRequestPath(request, pathname) {
1074
+ const current = new URL(request.url);
1075
+ if (current.pathname === pathname)
1076
+ return request;
1077
+ const rewritten = new URL(request.url);
1078
+ rewritten.pathname = pathname;
1079
+ return new Request(rewritten, request);
1080
+ }
1081
+ async function parseJsonBody(request) {
1082
+ if (request.method === "GET" || request.method === "HEAD")
1083
+ return void 0;
1084
+ if (!(request.headers.get("content-type") ?? "").includes("json"))
1085
+ return void 0;
1086
+ return request.clone().json().catch(() => void 0);
1087
+ }
1088
+ function objectOrEmpty(value) {
1089
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1090
+ }
1091
+ function successStatus(endpoint) {
1092
+ const status = Object.keys(endpoint.responses ?? {}).map(Number).find((code) => code >= 200 && code < 300);
1093
+ return status ?? 200;
1094
+ }
1095
+ function jsonResponse(body, status) {
1096
+ return new Response(JSON.stringify(body ?? { ok: true }), {
1097
+ status,
1098
+ headers: { "Content-Type": "application/json" }
1099
+ });
1100
+ }
1101
+ function maybeAttachAuthCookies(fortress, result, response) {
1102
+ if (result instanceof Response)
1103
+ return response;
1104
+ const obj = result;
1105
+ if (typeof obj?.accessToken !== "string")
1106
+ return response;
1107
+ return withCookies(response, fortress.serializeAuthCookies({
1108
+ accessToken: obj.accessToken,
1109
+ refreshToken: typeof obj.refreshToken === "string" ? obj.refreshToken : null
1110
+ }));
1111
+ }
1112
+ function protect(fortress, target, handler, options = {}) {
1113
+ const endpoint = findEndpoint(fortress, target, options.method);
1114
+ const manifest = findManifestEntry(fortress, endpoint);
1115
+ const endpointRouteTable = buildRouteTable([endpoint]);
1116
+ const csrfConfig = resolveCsrfConfig(fortress.config.csrf);
1117
+ const plugins = fortress.config.plugins ?? [];
1118
+ return async function protectedRequestHandler(request) {
1119
+ let response;
1120
+ try {
1121
+ const url = new URL(request.url);
1122
+ const pipelinePath = options.path ?? url.pathname;
1123
+ const pipelineRequest = rewriteRequestPath(request, pipelinePath);
1124
+ await runPluginMiddleware(plugins, fortress.config, "before-auth", { request: pipelineRequest });
1125
+ enforceCsrf(pipelineRequest, pipelinePath, csrfConfig, fortress.cookies);
1126
+ let subject;
1127
+ let userId;
1128
+ let claims;
1129
+ let scopes;
1130
+ const selfManagedBearer = endpoint.meta?.bearerKind === "oauth";
1131
+ if (!selfManagedBearer) {
1132
+ const pluginResolved = await tryPluginPrincipal(fortress, pipelineRequest);
1133
+ if (pluginResolved) {
1134
+ subject = pluginResolved.subject;
1135
+ claims = pluginResolved.claims;
1136
+ scopes = pluginResolved.scopes;
1137
+ }
1138
+ }
1139
+ const requiresBearer = !selfManagedBearer && ((endpoint.meta?.security?.includes("bearer") ?? false) || !!endpoint.meta?.permission);
1140
+ if (!subject && requiresBearer) {
1141
+ const token = fortress.extractAccessToken(pipelineRequest);
1142
+ if (!token)
1143
+ throw Errors.unauthorized("Missing access token");
1144
+ try {
1145
+ claims = await fortress.auth.verifyToken(token);
1146
+ subject = { type: claims.subjectType, id: claims.sub };
1147
+ } catch (err) {
1148
+ if (err instanceof FortressError)
1149
+ throw err;
1150
+ throw Errors.unauthorized("Invalid access token");
1151
+ }
1152
+ }
1153
+ if (subject?.type === "USER")
1154
+ userId = subject.id;
1155
+ await runPluginMiddleware(plugins, fortress.config, "after-auth", {
1156
+ request: pipelineRequest,
1157
+ fortressSubject: subject,
1158
+ fortressUserId: userId,
1159
+ fortressClaims: claims,
1160
+ fortressScopes: scopes
1161
+ });
1162
+ if (!selfManagedBearer) {
1163
+ await enforceFortressPermission(endpoint, subject, {
1164
+ checkPermission: (subj, resource, action, credentialScopes) => fortress.iam.checkPermission(subj, resource, action, { credentialScopes })
1165
+ }, scopes);
1166
+ }
1167
+ await runPluginMiddleware(plugins, fortress.config, "after-rbac", {
1168
+ request: pipelineRequest,
1169
+ fortressSubject: subject,
1170
+ fortressUserId: userId,
1171
+ fortressClaims: claims,
1172
+ fortressScopes: scopes
1173
+ });
1174
+ const body = await parseJsonBody(request);
1175
+ const rawQuery = Object.fromEntries(url.searchParams);
1176
+ const match = matchRoute(endpointRouteTable, endpoint.method, options.path ?? url.pathname);
1177
+ const rawParams = options.params ?? match?.params ?? {};
1178
+ const query = coerceBySchema(endpoint.input?.query, rawQuery) ?? rawQuery;
1179
+ const params = coerceBySchema(endpoint.input?.params, rawParams) ?? rawParams;
1180
+ await validateRequest(endpoint.input, { body, query, params });
1181
+ const input = { ...objectOrEmpty(body), ...query, ...params };
1182
+ const result = await handler({
1183
+ request,
1184
+ endpoint,
1185
+ manifest,
1186
+ subject,
1187
+ userId,
1188
+ claims,
1189
+ scopes,
1190
+ params,
1191
+ query,
1192
+ body,
1193
+ input,
1194
+ respond: (status, responseBody) => jsonResponse(responseBody, status)
1195
+ // Cast: the runtime values are untyped records; the generic context
1196
+ // type narrows them for the caller, but the impl signature uses the
1197
+ // loose `ProtectedRouteContext<EndpointDefinition>`.
1198
+ });
1199
+ response = result instanceof Response ? result : jsonResponse(result, successStatus(endpoint));
1200
+ if (options.attachAuthCookies ?? true)
1201
+ response = maybeAttachAuthCookies(fortress, result, response);
1202
+ return response;
1203
+ } catch (err) {
1204
+ response = errorToResponse(err, fortress.logger);
1205
+ return response;
1206
+ }
1207
+ };
1208
+ }
1209
+
1210
+ // src/express/protect.ts
1211
+ function protectedRoute(fortress, target, handler, options = {}) {
1212
+ return async (req, res, next) => {
1213
+ try {
1214
+ const request = expressToWebRequest2(req);
1215
+ const protectedHandler = protect(
1216
+ fortress,
1217
+ target,
1218
+ (ctx) => handler(req, res, ctx),
1219
+ { ...options, method: options.method ?? req.method }
1220
+ );
1221
+ const response = await protectedHandler(request);
1222
+ await sendWebResponseToExpress2(response, res);
1223
+ } catch (err) {
1224
+ next(err);
1225
+ }
1226
+ };
1227
+ }
1228
+ function expressToWebRequest2(req) {
1229
+ const host = req.headers.host ?? "localhost";
1230
+ const protocol = req.protocol ?? "http";
1231
+ const originalUrl = req.originalUrl ?? req.path;
1232
+ const search = originalUrl.includes("?") ? `?${originalUrl.split("?")[1]}` : "";
1233
+ const url = `${protocol}://${host}${req.path}${search}`;
1234
+ const headers = new Headers();
1235
+ for (const [k, v] of Object.entries(req.headers)) {
1236
+ if (Array.isArray(v)) {
1237
+ for (const item of v) headers.append(k, item);
1238
+ } else if (typeof v === "string") {
1239
+ headers.set(k, v);
1240
+ }
1241
+ }
1242
+ const init = { method: req.method, headers };
1243
+ if (req.method !== "GET" && req.method !== "HEAD") {
1244
+ const body = req.body;
1245
+ if (body !== void 0 && body !== null) {
1246
+ if (typeof body === "string") {
1247
+ init.body = body;
1248
+ } else if (body instanceof Uint8Array) {
1249
+ init.body = body;
1250
+ } else {
1251
+ init.body = JSON.stringify(body);
1252
+ if (!headers.has("content-type"))
1253
+ headers.set("content-type", "application/json");
1254
+ }
1255
+ }
1256
+ }
1257
+ return new Request(url, init);
1258
+ }
1259
+ async function sendWebResponseToExpress2(response, res) {
1260
+ res.status(response.status);
1261
+ for (const [k, v] of response.headers) {
1262
+ if (k.toLowerCase() !== "set-cookie")
1263
+ res.setHeader(k, v);
1264
+ }
1265
+ const setCookies = response.headers.getSetCookie();
1266
+ if (setCookies.length > 0) {
1267
+ res.setHeader("set-cookie", setCookies);
1268
+ }
1269
+ const text = await response.text();
1270
+ const isJson = (response.headers.get("content-type") ?? "").includes("json");
1271
+ if (isJson) {
1272
+ try {
1273
+ res.json(JSON.parse(text));
1274
+ } catch {
1275
+ res.json({ raw: text });
1276
+ }
1277
+ } else {
1278
+ const resWithSend = res;
1279
+ if (resWithSend.send)
1280
+ resWithSend.send(text);
1281
+ else
1282
+ res.json(text);
1283
+ }
1284
+ }
1285
+
1286
+ // src/express/validated.ts
1287
+ async function vBody(req, schema) {
1288
+ return validateValue(schema, req.body, "body");
1289
+ }
1290
+ async function vParam(req, schema) {
1291
+ return validateValue(schema, req.params, "params");
1292
+ }
1293
+ async function vQuery(req, schema) {
1294
+ return validateValue(schema, req.query, "query");
1295
+ }
1296
+ export {
1297
+ convertRoutes,
1298
+ createAuthMiddleware,
1299
+ createCsrfMiddleware,
1300
+ createErrorHandler,
1301
+ createExpressMiddleware,
1302
+ createExpressPluginMiddleware,
1303
+ createRbacMiddleware,
1304
+ getClaims,
1305
+ getDb,
1306
+ getScopedDb,
1307
+ getSubject,
1308
+ getUserId,
1309
+ mountFortress,
1310
+ protectedRoute,
1311
+ vBody,
1312
+ vParam,
1313
+ vQuery
1314
+ };