@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,550 @@
1
+ import type { OpenAPISpec } from '../plugins/openapi/spec-builder';
2
+ import type { AuthEvent, AuthService } from './auth/auth-service';
3
+ import type { FortressConfig, ResolvedCookieConfig } from './config';
4
+ import type { EndpointDefinition } from './endpoint';
5
+ import type { AuthCookiePayload } from './http/cookie-serialize';
6
+ import type { PluginRequestContext } from './http/plugin-middleware';
7
+ import type { ResolvedPrincipal } from './http/principal';
8
+ import type { IamEvent, IamService, PermissionCheckEvent } from './iam/iam-service';
9
+ import type { PermissionSyncOptions, PermissionSyncResult } from './iam/permission-sync';
10
+ import type { RouteManifestEntry } from './manifest/route-manifest';
11
+ import type { MigrationApplyResult } from './migrations/engine';
12
+ import type { FortressLogger } from './observability/logger';
13
+ import type { TelemetryProvider } from './observability/types';
14
+ import type { ToOpenAPIOptions } from './openapi';
15
+ import type { FortressPlugin, MiddlewareDefinition } from './plugin';
16
+ import type { CallableForEndpoints, InferPluginCallMap, InferPlugins } from './plugin-methods-map';
17
+ import { authEndpoints } from './auth/auth-endpoints';
18
+ import { createAuthService } from './auth/auth-service';
19
+ import { resolveCookieConfig } from './config';
20
+ import { Errors } from './errors';
21
+ import { buildCall } from './http/call';
22
+ import { serializeAuthCookies as serializeAuthCookiesFn } from './http/cookie-serialize';
23
+ import { buildHandleRequest } from './http/handle-request';
24
+ import { canonicalizePath } from './http/match';
25
+ import { runPluginMiddleware as runPluginMiddlewareFn } from './http/plugin-middleware';
26
+ import { resolveRequestPrincipal as resolveRequestPrincipalFn } from './http/principal';
27
+ import { extractAccessToken as extractAccessTokenFn } from './http/token-extraction';
28
+ import { iamEndpoints } from './iam/iam-endpoints';
29
+ import { createIamService } from './iam/iam-service';
30
+ import { runPermissionSync } from './iam/permission-sync';
31
+ import { buildRouteManifest } from './manifest/route-manifest';
32
+ import { migrateUp } from './migrations/engine';
33
+ import { instrumentAdapter } from './observability/db-instrumentation';
34
+ import { SILENT_LOGGER } from './observability/logger';
35
+ import { NO_OP_TELEMETRY } from './observability/types';
36
+ import { toOpenAPI as endpointsToOpenAPI } from './openapi';
37
+ import { processPlugins } from './plugin-runner';
38
+
39
+ /**
40
+ * Typed in-process call surface. Composes auth + IAM endpoint callables
41
+ * with any plugin-contributed routes (derived from the plugins tuple).
42
+ *
43
+ * Each handler becomes a `(input, options?) => Promise<successBody>` where
44
+ * the input shape is the inferred intersection of the endpoint's body,
45
+ * query, and params schemas, and the output is the inferred 2xx response
46
+ * type. Non-2xx responses throw a `FortressError`.
47
+ */
48
+ export type TypedCall<T extends readonly FortressPlugin[]>
49
+ = & CallableForEndpoints<typeof authEndpoints>
50
+ & CallableForEndpoints<typeof iamEndpoints>
51
+ & InferPluginCallMap<T>;
52
+
53
+ /**
54
+ * Configured fortress instance returned by {@link createFortress}. Holds the
55
+ * core auth and IAM services, the resolved config, every endpoint definition
56
+ * (auth + IAM + plugin routes), and the typed plugin method surface.
57
+ *
58
+ * Also exposes the framework-agnostic HTTP entry points
59
+ * ({@link Fortress.handleRequest}, {@link Fortress.runPluginMiddleware},
60
+ * {@link Fortress.extractAccessToken}, {@link Fortress.serializeAuthCookies})
61
+ * that adapters delegate to. See `src/core/http/`.
62
+ */
63
+
64
+ export interface Fortress<
65
+ // eslint-disable-next-line ts/no-unsafe-function-type -- fallback type for untyped plugin access
66
+ TPlugins = Record<string, Record<string, Function>>,
67
+ TCall = Record<string, (input?: any, options?: any) => Promise<any>>,
68
+ > {
69
+ auth: AuthService;
70
+ iam: IamService;
71
+ plugins: TPlugins;
72
+ /**
73
+ * Typed in-process client. Each key is a handler name (from the core
74
+ * auth/IAM endpoint records or a plugin's `routes` record); each value is
75
+ * an async function whose input/output types are inferred from the
76
+ * endpoint's declared body/query/params/response schemas.
77
+ *
78
+ * Under the hood, each callable serializes its input to a `Request` and
79
+ * delegates to `fortress.handleRequest`, so middleware, token
80
+ * verification, RBAC, and validation all run — the same path a network
81
+ * client would eventually hit. See `src/core/http/call.ts`.
82
+ */
83
+ call: TCall;
84
+ config: Readonly<FortressConfig>;
85
+ /** All endpoint definitions (auth + IAM + plugins) with JSON Schema metadata. */
86
+ endpoints: EndpointDefinition[];
87
+ /** Canonical generated route-security manifest derived from endpoint metadata. */
88
+ manifest: RouteManifestEntry[];
89
+ /** Resolved auth-cookie names and attributes (NODE_ENV-aware). */
90
+ cookies: ResolvedCookieConfig;
91
+ /** Resolved logger (defaults to a silent no-op when `config.logger` is unset). */
92
+ logger: FortressLogger;
93
+ /** Resolved telemetry provider (defaults to a no-op when `config.observability` is unset). */
94
+ telemetry: TelemetryProvider;
95
+ /**
96
+ * Handle a Fortress-managed request and return a web-standard `Response`.
97
+ * Composes plugin middleware, token verification, default-deny RBAC,
98
+ * validation, and dispatch.
99
+ *
100
+ * Adapters call this for paths owned by Fortress (e.g. `/auth/*`,
101
+ * `/iam/*`, plugin-registered routes). See `src/core/http/handle-request.ts`.
102
+ */
103
+ handleRequest: (request: Request) => Promise<Response>;
104
+ /**
105
+ * Run plugin middleware at a given lifecycle phase, passing a duck-typed
106
+ * request context. Adapters call this on user-owned routes so plugins
107
+ * like rate-limit and audit-log still apply outside Fortress paths.
108
+ */
109
+ runPluginMiddleware: (
110
+ phase: MiddlewareDefinition['position'],
111
+ ctx: PluginRequestContext,
112
+ ) => Promise<void>;
113
+ /**
114
+ * Extract the access token from a request, preferring the configured
115
+ * cookie and falling back to `Authorization: Bearer`.
116
+ */
117
+ extractAccessToken: (request: Request) => string | null;
118
+ /**
119
+ * Resolve the request principal by trying plugin `resolvePrincipal`
120
+ * hooks (e.g. `api-key`) first, then falling back to the configured JWT
121
+ * bearer token. Non-throwing — returns `null` when no credential is
122
+ * present or the JWT fails to verify.
123
+ *
124
+ * Adapter user-route middleware calls this so api-keys, cookies, and
125
+ * bearer tokens authenticate uniformly on user-owned routes — not just
126
+ * Fortress-owned routes dispatched through `handleRequest`.
127
+ */
128
+ resolvePrincipal: (request: Request) => Promise<ResolvedPrincipal | null>;
129
+ /**
130
+ * Serialize an auth result (or pair of tokens) into one or two
131
+ * `Set-Cookie` header values using the resolved cookie config and the
132
+ * configured token expiries.
133
+ */
134
+ serializeAuthCookies: (payload: AuthCookiePayload) => string[];
135
+ /**
136
+ * Run Fortress's pending schema migrations against the configured database
137
+ * and, optionally, an application-supplied migration step afterwards.
138
+ *
139
+ * Fortress migrations always run first because app schemas commonly
140
+ * reference fortress tables (foreign keys to `user.id`, etc.). The
141
+ * `migrateApp` callback (when provided) is awaited after Fortress
142
+ * migrations complete, so a host can do something like:
143
+ *
144
+ * ```ts
145
+ * await fortress.migrate({
146
+ * migrateApp: () => migrate(db, { migrationsFolder: './drizzle' }),
147
+ * });
148
+ * ```
149
+ *
150
+ * Eliminates the two-step "forget to call `migrateUp` and silently
151
+ * have no auth tables" footgun.
152
+ */
153
+ migrate: (opts?: MigrateOptions) => Promise<MigrateResult>;
154
+ /**
155
+ * Seed permissions discovered on registered endpoints (`fortress.endpoints`)
156
+ * into the IAM database and, optionally, bind them onto a set of default
157
+ * roles. Idempotent — re-running adds only what's missing and never
158
+ * revokes anything that was already there.
159
+ *
160
+ * Typical bootstrap sequence:
161
+ *
162
+ * ```ts
163
+ * await fortress.migrate({ migrateApp: () => migrate(db, '...') });
164
+ * await fortress.syncPermissionsFromManifest({
165
+ * defaultRoles: { admin: '*', member: ['school:read', 'school:list'] },
166
+ * });
167
+ * ```
168
+ *
169
+ * Replaces the seed script every consumer used to write that walked
170
+ * `fortress.endpoints`, deduped `(resource, action)` pairs, and called
171
+ * `iam.createPermission(...)` for each.
172
+ */
173
+ syncPermissionsFromManifest: (opts?: PermissionSyncOptions) => Promise<PermissionSyncResult>;
174
+ /**
175
+ * Emit a complete OpenAPI 3.1 spec from the endpoint definitions Fortress
176
+ * knows about: core auth/IAM routes, plugin routes, and any top-level
177
+ * host `routes` registered on {@link FortressConfig}.
178
+ *
179
+ * This is the programmatic companion to the OpenAPI plugin. Use it when
180
+ * you already own the docs route or when a build script needs to write
181
+ * `openapi.json` for client codegen:
182
+ *
183
+ * ```ts
184
+ * const spec = fortress.toOpenAPI({
185
+ * title: 'My API',
186
+ * version: '0.0.0',
187
+ * servers: [{ url: 'http://localhost:3001', description: 'Local' }],
188
+ * tags: [{ name: 'Schools' }],
189
+ * });
190
+ * ```
191
+ *
192
+ * Defaults to endpoint `handler` names for `operationId`, matching most
193
+ * host route-contract files. Pass `operationId: 'methodPath'` to keep the
194
+ * historical OpenAPI-plugin ID style.
195
+ */
196
+ toOpenAPI: (opts?: FortressToOpenAPIOptions) => OpenAPISpec;
197
+ }
198
+
199
+ /** Options accepted by {@link Fortress.toOpenAPI}. */
200
+ export interface FortressToOpenAPIOptions extends ToOpenAPIOptions {
201
+ /** Override the endpoints to emit. Defaults to `fortress.endpoints`. */
202
+ endpoints?: EndpointDefinition[];
203
+ }
204
+
205
+ /** Options accepted by {@link Fortress.migrate}. */
206
+ export interface MigrateOptions {
207
+ /**
208
+ * Optional host-app migration step. Runs after Fortress migrations
209
+ * complete; any thrown error propagates after Fortress migrations have
210
+ * already been applied. Library-agnostic: pass any `() => Promise<void>`
211
+ * — e.g. drizzle's `migrate(db, { migrationsFolder })`, Knex's
212
+ * `db.migrate.latest()`, a hand-rolled SQL runner.
213
+ */
214
+ migrateApp?: () => Promise<void>;
215
+ /**
216
+ * Override the migration dialect. Defaults to the adapter's `dialect`
217
+ * property (matches the behavior of {@link migrateUp}).
218
+ */
219
+ dialect?: 'sqlite' | 'pg';
220
+ /** Stop applying Fortress migrations after this version. Defaults to the latest. */
221
+ targetVersion?: number;
222
+ }
223
+
224
+ /** Result returned by {@link Fortress.migrate}. */
225
+ export interface MigrateResult {
226
+ /** Result of the Fortress-managed migration step. */
227
+ fortress: MigrationApplyResult;
228
+ /** `true` if `migrateApp` was supplied and ran to completion. */
229
+ appRan: boolean;
230
+ }
231
+
232
+ /**
233
+ * Type-safe helper to retrieve a plugin's methods from a Fortress instance.
234
+ *
235
+ * Since plugin methods are dynamically typed at runtime, this helper lets
236
+ * consumers provide a known interface for type-safe access without casting.
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * interface TwoFactorMethods {
241
+ * setup: (userId: string) => Promise<{ secret: string; qrCode: string }>;
242
+ * verify: (userId: string, code: string) => Promise<boolean>;
243
+ * }
244
+ *
245
+ * const twoFactor = getPluginMethods<TwoFactorMethods>(fortress, 'two-factor');
246
+ * const result = await twoFactor.setup(userId); // fully typed
247
+ * ```
248
+ */
249
+ export function getPluginMethods<T>(fortress: Fortress, pluginName: string): T {
250
+ const methods = fortress.plugins[pluginName];
251
+ if (!methods) {
252
+ throw Errors.notFound(`Plugin '${pluginName}' is not registered`);
253
+ }
254
+ return methods as T;
255
+ }
256
+
257
+ const MIN_SECRET_BYTES = 32;
258
+
259
+ /**
260
+ * Build a configured {@link Fortress} instance from a {@link FortressConfig}.
261
+ *
262
+ * Validates the JWT secret strength, deduplicates and processes the plugin
263
+ * list, wires up auth/IAM services, and returns the assembled instance with
264
+ * type-safe plugin method access for any plugins listed in the config.
265
+ */
266
+ export function createFortress<const T extends readonly FortressPlugin[]>(
267
+ config: FortressConfig & { plugins?: T },
268
+ ): Fortress<InferPlugins<T>, TypedCall<T>> {
269
+ // Validate JWT key strength (HS256 shared-secret bytes).
270
+ const keys = Array.isArray(config.jwt.key) ? config.jwt.key : [config.jwt.key];
271
+ for (const key of keys) {
272
+ if (new TextEncoder().encode(key).length < MIN_SECRET_BYTES) {
273
+ throw Errors.badRequest(
274
+ `JWT key must be at least ${MIN_SECRET_BYTES} bytes for HS256 security. Got ${new TextEncoder().encode(key).length} bytes.`,
275
+ );
276
+ }
277
+ }
278
+
279
+ const session = config.jwt.session;
280
+ if (session) {
281
+ for (const [name, value] of Object.entries(session)) {
282
+ if (value != null && (!Number.isInteger(value) || value <= 0)) {
283
+ throw Errors.badRequest(`jwt.session.${name} must be a positive integer`);
284
+ }
285
+ }
286
+ }
287
+
288
+ // Synthesize a virtual plugin from any top-level `routes` field so host
289
+ // apps don't have to hand-roll a one-field plugin just to register their
290
+ // own endpoints. Prepended to the plugin list so its routes appear before
291
+ // explicit plugins in the manifest, matching the registration order a
292
+ // user would write themselves.
293
+ const HOST_ROUTES_PLUGIN_NAME = '__host';
294
+ const userPlugins = config.plugins ?? [];
295
+ if (config.routes) {
296
+ const collision = userPlugins.find(p => p.name === HOST_ROUTES_PLUGIN_NAME);
297
+ if (collision) {
298
+ throw Errors.badRequest(
299
+ `Plugin name '${HOST_ROUTES_PLUGIN_NAME}' is reserved for top-level \`routes\`; rename your plugin.`,
300
+ );
301
+ }
302
+ }
303
+ const hostRoutesPlugin: FortressPlugin | null = config.routes
304
+ ? { name: HOST_ROUTES_PLUGIN_NAME, routes: config.routes }
305
+ : null;
306
+ const plugins = hostRoutesPlugin ? [hostRoutesPlugin, ...userPlugins] : userPlugins;
307
+
308
+ // Resolve observability defaults. SILENT_LOGGER and NO_OP_TELEMETRY are
309
+ // zero-allocation singletons — if the caller doesn't opt in, Fortress
310
+ // never writes to stderr and every metric/span call is a no-op.
311
+ const logger = config.logger ?? SILENT_LOGGER;
312
+ const telemetry = config.observability ?? NO_OP_TELEMETRY;
313
+
314
+ // Wrap the adapter with DB instrumentation. When `telemetry` is the
315
+ // no-op default, the wrapper records into a no-op histogram — essentially
316
+ // free. When a real OTel adapter is wired, every Fortress-internal query
317
+ // (and every plugin query that also uses this adapter) emits the stable
318
+ // `db.client.operation.duration` metric with standard attributes.
319
+ const db = instrumentAdapter(config.database, telemetry);
320
+
321
+ // Validate plugin name uniqueness
322
+ const pluginNames = new Set<string>();
323
+ for (const plugin of plugins) {
324
+ if (pluginNames.has(plugin.name)) {
325
+ throw Errors.badRequest(`Duplicate plugin name: '${plugin.name}'`);
326
+ }
327
+ pluginNames.add(plugin.name);
328
+ }
329
+
330
+ // Token-verify histogram is built before the auth service so it can be
331
+ // passed in via deps. Kept here (not inside auth-service) so the metric
332
+ // catalog lives in one place.
333
+ const tokenVerifyDuration = telemetry.meter.createHistogram('fortress.auth.token_verify.duration', {
334
+ unit: 's',
335
+ description: 'JWT access token verification latency',
336
+ boundaries: [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
337
+ });
338
+
339
+ const auth = createAuthService(db, config, plugins, { logger, telemetry, tokenVerifyDuration });
340
+ const iam = createIamService(db, config, { logger, telemetry });
341
+ const pluginMethods = processPlugins(plugins, db, config, auth, iam, logger);
342
+
343
+ // --- Wire built-in telemetry observers ------------------------------
344
+ //
345
+ // Translate AuthEvent / IamEvent / PermissionCheckEvent into metric
346
+ // updates. Attribute keys follow the Prometheus `.total` convention
347
+ // for counters and seconds for durations. User IDs are NEVER placed
348
+ // on metric attributes (cardinality bomb) — they go on spans/logs.
349
+ const authEventCounter = telemetry.meter.createCounter('fortress.auth.events.total', {
350
+ description: 'Auth lifecycle events (login, register, refresh, logout, token reuse)',
351
+ });
352
+ const iamEventCounter = telemetry.meter.createCounter('fortress.iam.events.total', {
353
+ description: 'IAM mutation events (role/permission/binding/group changes)',
354
+ });
355
+ const permissionCheckDuration = telemetry.meter.createHistogram('fortress.iam.permission_check.duration', {
356
+ unit: 's',
357
+ description: 'Permission check latency',
358
+ boundaries: [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
359
+ });
360
+ const cacheHitCounter = telemetry.meter.createCounter('fortress.iam.permission_check.cache.hits', {
361
+ description: 'Permission check resolved from cache',
362
+ });
363
+ const cacheMissCounter = telemetry.meter.createCounter('fortress.iam.permission_check.cache.misses', {
364
+ description: 'Permission check that hit the database',
365
+ });
366
+
367
+ auth.addAuthObserver((event: AuthEvent) => {
368
+ authEventCounter.add(1, {
369
+ event: event.eventType,
370
+ outcome: event.outcome ?? 'n/a',
371
+ method: event.method ?? 'n/a',
372
+ });
373
+ });
374
+
375
+ iam.addIamObserver((event: IamEvent) => {
376
+ iamEventCounter.add(1, { event: event.eventType });
377
+ });
378
+
379
+ iam.addPermissionCheckObserver((event: PermissionCheckEvent) => {
380
+ permissionCheckDuration.record(event.durationSeconds, {
381
+ subject_type: event.subjectType,
382
+ result: event.allowed ? 'allow' : 'deny',
383
+ cached: event.cached ? 'true' : 'false',
384
+ });
385
+ if (event.cached) {
386
+ cacheHitCounter.add(1);
387
+ }
388
+ else {
389
+ cacheMissCounter.add(1);
390
+ }
391
+ });
392
+
393
+ // Wire IAM events → audit log if the plugin is registered
394
+ if (pluginMethods['audit-log']?.logCustomEvent) {
395
+ const logCustomEvent = pluginMethods['audit-log'].logCustomEvent as (event: IamEvent) => Promise<void>;
396
+ iam.addIamObserver(event => logCustomEvent(event));
397
+ }
398
+
399
+ // Assemble all endpoint definitions. A plugin may intentionally override a
400
+ // core route, but two plugins claiming the same method+path is ambiguous and
401
+ // therefore rejected instead of depending on registration order.
402
+ const endpointMap = new Map<string, EndpointDefinition>();
403
+ const endpointOwners = new Map<string, string>();
404
+ const coreEndpoints: EndpointDefinition[] = [
405
+ ...Object.values(authEndpoints) as EndpointDefinition[],
406
+ ...Object.values(iamEndpoints) as EndpointDefinition[],
407
+ ];
408
+ for (const ep of coreEndpoints) {
409
+ const routeKey = `${ep.method.toUpperCase()} ${canonicalizePath(ep.path)}`;
410
+ endpointMap.set(routeKey, ep);
411
+ endpointOwners.set(routeKey, 'core');
412
+ }
413
+ for (const plugin of plugins) {
414
+ for (const ep of Object.values(plugin.routes ?? {}) as EndpointDefinition[]) {
415
+ const routeKey = `${ep.method.toUpperCase()} ${canonicalizePath(ep.path)}`;
416
+ const owner = endpointOwners.get(routeKey);
417
+ if (owner && owner !== 'core') {
418
+ throw Errors.badRequest(
419
+ `Duplicate endpoint ${routeKey} declared by plugins "${owner}" and "${plugin.name}"`,
420
+ );
421
+ }
422
+ endpointMap.set(routeKey, ep);
423
+ endpointOwners.set(routeKey, plugin.name);
424
+ }
425
+ }
426
+ const endpoints = Array.from(endpointMap.values());
427
+
428
+ // L-tier: fail-fast on `security: ['none']` + `permission` collisions.
429
+ // The two are mutually exclusive: an unauthenticated route has no subject
430
+ // to evaluate the permission against, so default-deny RBAC would always
431
+ // reject it. Catch the misconfiguration at startup, not at first request.
432
+ const oauthSelfAuthAllowlist = new Set([
433
+ 'GET /oauth/authorize',
434
+ 'POST /oauth/token',
435
+ 'POST /oauth/introspect',
436
+ 'POST /oauth/revoke',
437
+ 'GET /oauth/userinfo',
438
+ 'GET /oauth/.well-known/openid-configuration',
439
+ 'GET /oauth/.well-known/jwks.json',
440
+ ]);
441
+ for (const ep of endpoints) {
442
+ const security = ep.meta?.security ?? [];
443
+ if (security.includes('none') && ep.meta?.permission) {
444
+ throw Errors.badRequest(
445
+ `Endpoint ${ep.method} ${ep.path} declares security:['none'] AND a permission `
446
+ + `(${ep.meta.permission.resource}:${ep.meta.permission.action}). These are mutually `
447
+ + `exclusive — default-deny RBAC would reject every request.`,
448
+ );
449
+ }
450
+
451
+ // P3.6: `bearerKind: 'oauth'` means "this handler self-authenticates"
452
+ // and therefore skips the normal plugin/JWT/RBAC/JSON-validation
453
+ // pipeline. It is intentionally reserved for the OAuth protocol routes
454
+ // that parse form bodies or OAuth access tokens themselves. Any other
455
+ // route trying to use it is almost certainly a latent auth bypass.
456
+ if (ep.meta?.bearerKind === 'oauth' && !oauthSelfAuthAllowlist.has(`${ep.method} ${ep.path}`)) {
457
+ throw Errors.badRequest(
458
+ `Endpoint ${ep.method} ${ep.path} sets bearerKind:'oauth' but is not an approved self-auth OAuth protocol route.`,
459
+ );
460
+ }
461
+ }
462
+
463
+ // Resolve cookie config once at startup so all HTTP entry points share names.
464
+ const cookies = resolveCookieConfig(config.cookies);
465
+
466
+ // Build the framework-agnostic HTTP auxiliary closures upfront. `handleRequest`
467
+ // and `call` both get bound after the instance is constructed because
468
+ // they need the assembled `Fortress` object (route table is built from
469
+ // `endpoints`; `call` delegates to `handleRequest`).
470
+ let routeManifest: RouteManifestEntry[] | undefined;
471
+
472
+ const instance: Fortress<InferPlugins<T>, TypedCall<T>> = {
473
+ auth,
474
+ iam,
475
+ plugins: pluginMethods as InferPlugins<T>,
476
+ call: {} as TypedCall<T>,
477
+ config,
478
+ endpoints,
479
+ get manifest(): RouteManifestEntry[] {
480
+ routeManifest ??= buildRouteManifest(this as Fortress);
481
+ return routeManifest;
482
+ },
483
+ cookies,
484
+ logger,
485
+ telemetry,
486
+ handleRequest: undefined as unknown as (request: Request) => Promise<Response>,
487
+ runPluginMiddleware: (phase, ctx): Promise<void> => runPluginMiddlewareFn(plugins, config, phase, ctx),
488
+ extractAccessToken: (request: Request): string | null => extractAccessTokenFn(request, cookies),
489
+ resolvePrincipal: (request: Request): Promise<ResolvedPrincipal | null> =>
490
+ resolveRequestPrincipalFn(instance as Fortress, request),
491
+ serializeAuthCookies: (payload: AuthCookiePayload): string[] =>
492
+ serializeAuthCookiesFn(payload, cookies, {
493
+ access: config.jwt.accessTokenExpirySeconds ?? 900,
494
+ refresh: config.jwt.refreshTokenExpirySeconds ?? 604_800,
495
+ }),
496
+ async migrate(opts?: MigrateOptions): Promise<MigrateResult> {
497
+ // Fortress migrations first — app schemas commonly FK to fortress
498
+ // tables, so this order is the safe default. Unwrap the instrumented
499
+ // adapter so `db.dialect` reflects the underlying engine.
500
+ const fortressResult = await migrateUp(db, opts?.dialect, opts?.targetVersion);
501
+ let appRan = false;
502
+ if (opts?.migrateApp) {
503
+ await opts.migrateApp();
504
+ appRan = true;
505
+ }
506
+ return { fortress: fortressResult, appRan };
507
+ },
508
+ syncPermissionsFromManifest(opts?: PermissionSyncOptions): Promise<PermissionSyncResult> {
509
+ return runPermissionSync(iam, opts?.endpoints ?? instance.endpoints, opts);
510
+ },
511
+ toOpenAPI(opts?: FortressToOpenAPIOptions): OpenAPISpec {
512
+ return endpointsToOpenAPI(opts?.endpoints ?? instance.endpoints, opts);
513
+ },
514
+ };
515
+
516
+ instance.handleRequest = buildHandleRequest(instance as Fortress);
517
+
518
+ // Assemble the typed call map. Core auth/IAM handlers come first; plugin
519
+ // routes are layered on top so they can override a core handler that
520
+ // shares a key (matches the `endpointMap` dedup order).
521
+ const callEndpoints: Record<string, EndpointDefinition> = {
522
+ ...(authEndpoints as unknown as Record<string, EndpointDefinition>),
523
+ ...(iamEndpoints as unknown as Record<string, EndpointDefinition>),
524
+ };
525
+ const callOwners = new Map<string, string>();
526
+ for (const key of Object.keys(callEndpoints))
527
+ callOwners.set(key, 'core');
528
+ for (const plugin of plugins) {
529
+ // Top-level `routes` are metadata for manifest/OpenAPI/protected host
530
+ // routes; they do not come with plugin methods, so exposing them through
531
+ // `fortress.call.*` would create a runtime `NOT_FOUND` footgun. If a
532
+ // consumer wants typed in-process callables for custom routes, use a real
533
+ // plugin that supplies both `routes` and matching `methods`.
534
+ if (plugin.name === HOST_ROUTES_PLUGIN_NAME)
535
+ continue;
536
+ for (const [key, endpoint] of Object.entries(plugin.routes ?? {}) as [string, EndpointDefinition][]) {
537
+ const owner = callOwners.get(key);
538
+ if (owner && owner !== 'core') {
539
+ throw Errors.badRequest(
540
+ `Duplicate fortress.call key "${key}" declared by plugins "${owner}" and "${plugin.name}"`,
541
+ );
542
+ }
543
+ callEndpoints[key] = endpoint;
544
+ callOwners.set(key, plugin.name);
545
+ }
546
+ }
547
+ instance.call = buildCall(instance as Fortress, callEndpoints) as TypedCall<T>;
548
+
549
+ return instance;
550
+ }