@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,1009 @@
1
+ /**
2
+ * Example Hono app using Fortress
3
+ *
4
+ * Demonstrates EVERY feature Fortress offers:
5
+ *
6
+ * Core: registration, login, logout, token refresh, sessions, impersonation,
7
+ * RBAC (roles, groups, permissions), password policy
8
+ *
9
+ * Plugins: rate-limit, account-lockout, email-verification, two-factor,
10
+ * magic-link, api-key, social-login, tenancy, data-isolation,
11
+ * audit-log, webhook, oauth
12
+ *
13
+ * Middleware: auth, RBAC, CSRF, security headers
14
+ *
15
+ * Helpers: getUserId, getClaims, getDb, getScopedDb
16
+ *
17
+ * Run: bun run dev
18
+ * Or: bun run examples/hono-app/index.ts
19
+ */
20
+ import { Hono } from 'hono';
21
+ import { createFortress, email, endpoint, int, obj, str, strict } from '../../src';
22
+ import {
23
+ convertRoutes,
24
+ createCsrfMiddleware,
25
+ createHonoMiddleware,
26
+ createSecurityHeadersMiddleware,
27
+ getClaims,
28
+ getDb,
29
+ getScopedDb,
30
+ getSubject,
31
+ getUserId,
32
+ mountFortress,
33
+ protectedRoute,
34
+ vBody,
35
+ vParam,
36
+ vQuery,
37
+ } from '../../src/hono';
38
+ import { accountLockout } from '../../src/plugins/account-lockout';
39
+ import { admin } from '../../src/plugins/admin';
40
+ import { apiKey } from '../../src/plugins/api-key';
41
+ import { auditLog } from '../../src/plugins/audit-log';
42
+ import { dataIsolation } from '../../src/plugins/data-isolation';
43
+ import { emailVerification } from '../../src/plugins/email-verification';
44
+ import { magicLink } from '../../src/plugins/magic-link';
45
+ import { oauth } from '../../src/plugins/oauth';
46
+ import { openapi } from '../../src/plugins/openapi';
47
+ import { rateLimit } from '../../src/plugins/rate-limit';
48
+ import { honoRateLimit } from '../../src/plugins/rate-limit/hono';
49
+ import { socialLogin } from '../../src/plugins/social-login';
50
+ import { tenancy } from '../../src/plugins/tenancy';
51
+ import { twoFactor } from '../../src/plugins/two-factor';
52
+ import { builtinEvents, webhook } from '../../src/plugins/webhook';
53
+ import { createTestAdapter } from '../../src/testing';
54
+
55
+ // ═══════════════════════════════════════════════════════════════════════════
56
+ // 1. Create Fortress instance with ALL plugins
57
+ // ═══════════════════════════════════════════════════════════════════════════
58
+
59
+ const db = createTestAdapter();
60
+
61
+ const hostOwnedStatsEndpoint = endpoint('GET', '/api/host-owned-stats')
62
+ .summary('Host-owned route protected by Fortress metadata')
63
+ .description('Demonstrates protectedRoute(): this Hono handler is app-owned, but Fortress supplies auth, RBAC, plugin middleware, and validation from endpoint metadata.')
64
+ .tags('App')
65
+ .security('bearer')
66
+ .permission('fortress', 'viewResources')
67
+ .response(200, 'Host route stats', obj({ users: int(), subjectType: str() }, 'users', 'subjectType'))
68
+ .handler('hostOwnedStats')
69
+ .build();
70
+
71
+ // ── Observability (optional but demonstrated) ─────────────────────────────
72
+ // A minimal console-backed logger satisfies the FortressLogger interface.
73
+ // Drop in `pino()` or `fastify.log` for production.
74
+ const logger = {
75
+ level: 'info',
76
+ fatal: (...args: unknown[]): void => console.error('[fortress:fatal]', ...args),
77
+ error: (...args: unknown[]): void => console.error('[fortress:error]', ...args),
78
+ warn: (...args: unknown[]): void => console.warn('[fortress:warn]', ...args),
79
+ // eslint-disable-next-line no-console -- example-only; real apps pass pino()
80
+ info: (...args: unknown[]): void => console.info('[fortress:info]', ...args),
81
+ // eslint-disable-next-line no-console -- example-only; real apps pass pino()
82
+ debug: (...args: unknown[]): void => console.debug('[fortress:debug]', ...args),
83
+ // eslint-disable-next-line no-console -- example-only; real apps pass pino()
84
+ trace: (...args: unknown[]): void => console.debug('[fortress:trace]', ...args),
85
+ silent: (): void => {},
86
+ };
87
+
88
+ const fortress = createFortress({
89
+ jwt: {
90
+ key: 'dev-secret-minimum-32-bytes-long!',
91
+ // key: ['new-secret-32-bytes-minimum!!!!!!', 'old-secret-32-bytes-minimum!!!!!!'], // ← secret rotation
92
+ accessTokenExpirySeconds: 900,
93
+ refreshTokenExpirySeconds: 604800,
94
+ },
95
+ database: db,
96
+ passwordPolicy: { minLength: 10 },
97
+ logger,
98
+ // To wire OpenTelemetry metrics/spans, install @opentelemetry/api and:
99
+ //
100
+ // import { createOtelTelemetry } from '../../src/otel';
101
+ // observability: await createOtelTelemetry({ name: 'fortress-example' }),
102
+ //
103
+ // The adapter is the ONLY file in the repo that imports @opentelemetry/api
104
+ // (dynamically), so this example stays workerd/Deno-clean when not used.
105
+
106
+ // Plugin order matters — hooks run in array order
107
+ plugins: [
108
+ // ── Admin (IAM route protection + bootstrap) ──
109
+ admin({
110
+ apiKeyRoutes: true,
111
+ bootstrap: {
112
+ enabled: Boolean(process.env.FORTRESS_ADMIN_BOOTSTRAP_SECRET),
113
+ secret: process.env.FORTRESS_ADMIN_BOOTSTRAP_SECRET,
114
+ },
115
+ }),
116
+
117
+ // ── Gate plugins (reject early) ──
118
+ rateLimit({
119
+ login: { maxPerIp: 100, maxPerAccount: 10, windowSeconds: 60 },
120
+ register: { maxPerIp: 50, windowSeconds: 60 },
121
+ refresh: { maxPerIp: 120, windowSeconds: 60 },
122
+ oauthToken: { maxPerIp: 60, windowSeconds: 60 },
123
+ apiKeyIssue: { maxPerIp: 10, maxPerUser: 10, windowSeconds: 3600 },
124
+ // Named rules consumed by honoRateLimit() on user-owned routes.
125
+ rules: {
126
+ api: { maxPerIp: 200, maxPerUser: 1000, windowSeconds: 60 },
127
+ },
128
+ }),
129
+ accountLockout({
130
+ maxFailedAttempts: 3,
131
+ lockoutDurationSeconds: 60,
132
+ escalation: true,
133
+ }),
134
+ emailVerification({
135
+ requireVerification: false, // set true in production
136
+ onSendVerification: async (email, token, userId) => {
137
+ console.warn(`[email-verification] userId=${userId} email=${email} token=${token}`);
138
+ },
139
+ }),
140
+
141
+ // ── Auth enhancement plugins ──
142
+ twoFactor({
143
+ secretEncryptionKey: process.env.FORTRESS_TOTP_ENCRYPTION_KEY!,
144
+ totp: { issuer: 'Fortress Example' },
145
+ }),
146
+ magicLink({
147
+ onSendMagicLink: async (email, token) => {
148
+ console.warn(`[magic-link] email=${email} token=${token}`);
149
+ },
150
+ }),
151
+ apiKey({ prefix: 'fortress', maxKeysPerSubject: 5, routes: true }),
152
+ // ^ `routes: true` mounts the self-service HTTP endpoints:
153
+ // POST /api-key/keys — create a key for the caller
154
+ // GET /api-key/keys — list the caller's keys
155
+ // DELETE /api-key/keys/:id — revoke one of the caller's keys
156
+ // POST /api-key/keys/:id/rotate — rotate one of the caller's keys
157
+ // The admin plugin above adds admin-side routes when `apiKeyRoutes: true`:
158
+ // GET /admin/users/:userId/api-keys — list any user's keys
159
+ // DELETE /admin/users/:userId/api-keys/:id — revoke any user's key
160
+ // Both require the `apiKey:manage` permission (auto-registered into
161
+ // `fortress-admin` via `/iam/admin/bootstrap`).
162
+ socialLogin({
163
+ tokenEncryptionKey: process.env.FORTRESS_SOCIAL_TOKEN_KEY ?? 'dev-social-token-key-32-bytes!!!',
164
+ providers: [
165
+ // Placeholder credentials — shows the config API but won't complete real OAuth flows
166
+ { name: 'google', clientId: 'GOOGLE_CLIENT_ID', clientSecret: 'GOOGLE_SECRET' },
167
+ { name: 'github', clientId: 'GITHUB_CLIENT_ID', clientSecret: 'GITHUB_SECRET' },
168
+ ],
169
+ }),
170
+
171
+ // ── Multi-tenancy & data isolation ──
172
+ tenancy(),
173
+ dataIsolation({
174
+ scopes: [{
175
+ name: 'org',
176
+ field: 'orgId',
177
+ models: ['document'],
178
+ resolveValue: async (userId, ctx) => {
179
+ const assignment = await ctx.db.findOne<{ scopeValue: string }>({
180
+ model: 'user_scope_assignment',
181
+ where: [
182
+ { field: 'userId', operator: '=', value: userId },
183
+ { field: 'scopeName', operator: '=', value: 'org' },
184
+ ],
185
+ });
186
+ // Missing assignments fail closed (FORBIDDEN) by default.
187
+ return assignment?.scopeValue ?? null;
188
+ },
189
+ }],
190
+ }),
191
+
192
+ // ── Observability plugins (log last) ──
193
+ auditLog({ hashChain: true }),
194
+ webhook({
195
+ // Declare a custom event alongside the built-in auth events.
196
+ events: [...builtinEvents(), { name: 'order.shipped', schema: obj({ orderId: str() }, 'orderId') }],
197
+ delivery: {
198
+ // Demo transport: log instead of making a real outbound request. Omit
199
+ // `fetch` in production to use the SSRF-safe (IP-pinning) default.
200
+ fetch: async (req) => {
201
+ console.warn(`[webhook] → ${req.url}`, await req.text());
202
+ return new Response(null, { status: 200 });
203
+ },
204
+ },
205
+ }),
206
+
207
+ // ── OAuth server ──
208
+ oauth({
209
+ issuerUrl: 'http://localhost:3000',
210
+ scopePermissionMap: {
211
+ 'read:users': { resource: 'user', action: 'list' },
212
+ },
213
+ }),
214
+
215
+ // ── Host-owned routes metadata ──
216
+ // protectedRoute() below uses this endpoint metadata for auth/RBAC and
217
+ // OpenAPI can include it alongside Fortress-owned endpoints.
218
+ { name: 'host-routes', routes: { hostOwnedStats: hostOwnedStatsEndpoint } },
219
+
220
+ // ── OpenAPI (API docs) ──
221
+ // convertRoutes turns createRoute-style objects into EndpointDefinitions
222
+ // using your own schema converter (Zod, Valibot, TypeBox, etc.)
223
+ openapi({
224
+ title: 'Fortress Example API',
225
+ version: '0.0.15',
226
+ additionalEndpoints: convertRoutes(
227
+ [
228
+ // These would normally be imported from your route modules, e.g.:
229
+ // import { loginRoute, listUsersRoute } from './modules/auth/routes';
230
+ {
231
+ method: 'get',
232
+ path: '/health',
233
+ tags: ['App'],
234
+ summary: 'Health check',
235
+ responses: {
236
+ 200: {
237
+ description: 'OK',
238
+ content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string' } } } } },
239
+ },
240
+ },
241
+ },
242
+ ],
243
+ {
244
+ // User brings their own converter, e.g. z.toJSONSchema for Zod v4
245
+ // Here we use identity since schemas are already JSON Schema
246
+ schemaConverter: s => s as Record<string, unknown>,
247
+ },
248
+ ),
249
+ }),
250
+ ],
251
+ });
252
+
253
+ // ── Demonstrate the observer surfaces ─────────────────────────────────────
254
+ // Auth observer: fires on login/failure/logout/register/refresh/token-reuse.
255
+ fortress.auth.addAuthObserver((event) => {
256
+ logger.info({ event }, `auth.${event.eventType}`);
257
+ });
258
+
259
+ // IAM observer: fires on role/permission/binding mutations.
260
+ fortress.iam.addIamObserver((event) => {
261
+ logger.info({ event }, `iam.${event.eventType}`);
262
+ });
263
+
264
+ // Permission check observer: SYNCHRONOUS, hot path. Keep it bounded.
265
+ fortress.iam.addPermissionCheckObserver((event) => {
266
+ if (!event.allowed) {
267
+ logger.warn(
268
+ { resource: event.resource, action: event.action, cached: event.cached },
269
+ 'permission denied',
270
+ );
271
+ }
272
+ });
273
+
274
+ // Route-security manifest: the same generated inventory adapters use to
275
+ // decide which paths are Fortress-managed. Useful for CI drift checks or
276
+ // startup visibility.
277
+ logger.info(
278
+ {
279
+ routes: fortress.manifest.length,
280
+ rbacRoutes: fortress.manifest.filter(route => route.classification === 'rbac').length,
281
+ },
282
+ 'fortress route manifest generated',
283
+ );
284
+
285
+ // ═══════════════════════════════════════════════════════════════════════════
286
+ // 2. Create Hono app + middleware
287
+ // ═══════════════════════════════════════════════════════════════════════════
288
+
289
+ const app = new Hono();
290
+
291
+ const { authMiddleware, rbacMiddleware, errorHandler, pluginMiddleware } = createHonoMiddleware(fortress, {
292
+ routeMap: {
293
+ // User management
294
+ 'GET /api/users': { resource: 'user', action: 'list' },
295
+ 'POST /api/users': { resource: 'user', action: 'create' },
296
+ // Groups
297
+ 'POST /api/groups': { resource: 'group', action: 'create' },
298
+ 'POST /api/groups/:id/members': { resource: 'group', action: 'manage' },
299
+ 'DELETE /api/groups/:id/members/:userId': { resource: 'group', action: 'manage' },
300
+ // Audit log
301
+ 'GET /api/audit-log': { resource: 'audit-log', action: 'read' },
302
+ 'POST /api/audit-log/verify-chain': { resource: 'audit-log', action: 'read' },
303
+ // Webhooks
304
+ 'POST /api/webhooks': { resource: 'webhook', action: 'manage' },
305
+ 'GET /api/webhooks': { resource: 'webhook', action: 'manage' },
306
+ 'DELETE /api/webhooks/:id': { resource: 'webhook', action: 'manage' },
307
+ // Admin
308
+ 'POST /admin/impersonate': { resource: 'user', action: 'impersonate' },
309
+ 'GET /admin/lockout/:identifier': { resource: 'lockout', action: 'read' },
310
+ 'POST /admin/lockout/:identifier/reset': { resource: 'lockout', action: 'manage' },
311
+ },
312
+ skipPaths: ['/health', '/auth/*', '/magic-link/*', '/email/*', '/social/*'],
313
+ });
314
+
315
+ // Global middleware
316
+ app.use('*', createSecurityHeadersMiddleware());
317
+ app.use('*', createCsrfMiddleware({
318
+ skipPaths: ['/auth', '/oauth', '/magic-link', '/email', '/social', '/health'],
319
+ }));
320
+ app.onError(errorHandler);
321
+
322
+ // IAM routes: auth + admin plugin middleware (default deny via admin plugin)
323
+ app.use('/iam/*', authMiddleware);
324
+ app.use('/iam/*', pluginMiddleware.afterAuth);
325
+
326
+ // ═══════════════════════════════════════════════════════════════════════════
327
+ // 3. Public routes
328
+ // ═══════════════════════════════════════════════════════════════════════════
329
+
330
+ // curl http://localhost:3000/health
331
+ app.get('/health', c => c.json({ status: 'ok' }));
332
+
333
+ // ── Validated Request Helpers (vBody / vParam / vQuery) ──
334
+ // vBody/vParam/vQuery extract request data AND validate it at runtime against
335
+ // the supplied Standard Schema. On failure they throw FortressError with code
336
+ // VALIDATION_ERROR (HTTP 422) — the same shape every fortress-managed endpoint
337
+ // produces — so the registered Hono error handler formats it identically.
338
+
339
+ // `email()` enforces the email format at runtime (ReDoS-safe pattern); `strict()`
340
+ // rejects unknown body keys (no over-posting). Both come from the fetcher-backed
341
+ // schema builder re-exported at `@bajustone/fortress/fetcher`.
342
+ const RegisterBody = strict(obj({ email: email(), password: str({ min: 8 }), name: str() }, 'email', 'password', 'name'));
343
+ const LoginBody = obj({ identifier: str(), password: str() }, 'identifier', 'password');
344
+
345
+ // curl -X POST http://localhost:3000/auth/register -H 'Content-Type: application/json' \
346
+ // -d '{"email":"new@example.com","password":"MyPassword123!","name":"New User"}'
347
+ app.post('/auth/register', async (c) => {
348
+ const { email, password, name } = await vBody(c, RegisterBody);
349
+ const user = await fortress.auth.createUser({ email, password, name });
350
+ return c.json({ data: user }, 201);
351
+ });
352
+
353
+ // curl -X POST http://localhost:3000/auth/login -H 'Content-Type: application/json' \
354
+ // -d '{"identifier":"admin@example.com","password":"Password123!"}'
355
+ app.post('/auth/login', async (c) => {
356
+ const { identifier, password } = await vBody(c, LoginBody);
357
+ const result = await fortress.auth.login(identifier, password, {
358
+ ipAddress: c.req.header('x-forwarded-for'),
359
+ userAgent: c.req.header('user-agent'),
360
+ });
361
+ return c.json({ data: result });
362
+ });
363
+
364
+ // ── vParam / vQuery demo ──
365
+ // curl 'http://localhost:3000/echo/42?greeting=hello' → 200 { id: '42', greeting: 'hello' }
366
+ // curl 'http://localhost:3000/echo/42' → 422 VALIDATION_ERROR (missing greeting)
367
+ const EchoParam = obj({ id: str('Echo ID') }, 'id');
368
+ const EchoQuery = obj({ greeting: str('Greeting') }, 'greeting');
369
+ app.get('/echo/:id', async (c) => {
370
+ const { id } = await vParam(c, EchoParam);
371
+ const { greeting } = await vQuery(c, EchoQuery);
372
+ return c.json({ id, greeting });
373
+ });
374
+
375
+ // curl -X POST http://localhost:3000/auth/refresh -H 'Content-Type: application/json' \
376
+ // -d '{"refreshToken":"<token>"}'
377
+ app.post('/auth/refresh', async (c) => {
378
+ const { refreshToken } = await c.req.json();
379
+ const result = await fortress.auth.refresh(refreshToken);
380
+ return c.json({ data: result });
381
+ });
382
+
383
+ // curl -X POST http://localhost:3000/auth/logout -H 'Content-Type: application/json' \
384
+ // -d '{"refreshToken":"<token>"}'
385
+ app.post('/auth/logout', async (c) => {
386
+ const { refreshToken } = await c.req.json();
387
+ await fortress.auth.logout(refreshToken);
388
+ return c.json({ data: { loggedOut: true } });
389
+ });
390
+
391
+ // ── Magic Link (passwordless login) ──
392
+
393
+ // curl -X POST http://localhost:3000/magic-link/send -H 'Content-Type: application/json' \
394
+ // -d '{"email":"admin@example.com"}'
395
+ // (token is logged to console — copy it for the verify step)
396
+ app.post('/magic-link/send', async (c) => {
397
+ const { email } = await c.req.json();
398
+ const result = await fortress.plugins['magic-link'].sendMagicLink(email);
399
+ return c.json({ data: result });
400
+ });
401
+
402
+ // curl -X POST http://localhost:3000/magic-link/verify -H 'Content-Type: application/json' \
403
+ // -d '{"token":"<token-from-console>"}'
404
+ app.post('/magic-link/verify', async (c) => {
405
+ const { token } = await c.req.json();
406
+ const result = await fortress.plugins['magic-link'].verify(token);
407
+ return c.json({ data: result });
408
+ });
409
+
410
+ // ── Email Verification ──
411
+
412
+ // curl -X POST http://localhost:3000/email/verify -H 'Content-Type: application/json' \
413
+ // -d '{"token":"<token-from-console>"}'
414
+ app.post('/email/verify', async (c) => {
415
+ const { token } = await c.req.json();
416
+ const result = await fortress.plugins['email-verification'].verify(token);
417
+ return c.json({ data: result });
418
+ });
419
+
420
+ // ── Social Login ──
421
+
422
+ // curl http://localhost:3000/social/providers
423
+ app.get('/social/providers', (c) => {
424
+ const providers = fortress.plugins['social-login'].getProviders();
425
+ return c.json({ data: providers });
426
+ });
427
+
428
+ // curl 'http://localhost:3000/social/authorize/google?redirect_uri=http://localhost:3000/callback'
429
+ // (returns authorization URL — in a real app you'd redirect the browser)
430
+ app.get('/social/authorize/:provider', async (c) => {
431
+ const provider = c.req.param('provider');
432
+ const redirectUri = c.req.query('redirect_uri') ?? 'http://localhost:3000/callback';
433
+ const result = await fortress.plugins['social-login'].getAuthorizationUrl(provider, redirectUri);
434
+ return c.json({ data: result });
435
+ });
436
+
437
+ // ═══════════════════════════════════════════════════════════════════════════
438
+ // 4. Protected auth routes (require JWT)
439
+ // ═══════════════════════════════════════════════════════════════════════════
440
+
441
+ app.use('/auth/me', authMiddleware);
442
+ app.use('/auth/sessions/*', authMiddleware);
443
+ app.use('/auth/sessions', authMiddleware);
444
+ app.use('/auth/2fa/enable', authMiddleware);
445
+ app.use('/auth/2fa/confirm-setup', authMiddleware);
446
+ app.use('/auth/2fa/disable', authMiddleware);
447
+ app.use('/auth/email/*', authMiddleware);
448
+ app.use('/auth/api-keys/*', authMiddleware);
449
+ app.use('/auth/api-keys', authMiddleware);
450
+ app.use('/auth/social/*', authMiddleware);
451
+
452
+ // curl http://localhost:3000/auth/me -H 'Authorization: Bearer <token>'
453
+ app.get('/auth/me', async (c) => {
454
+ const userId = getUserId(c);
455
+ const claims = getClaims(c);
456
+ const user = await fortress.auth.me(userId);
457
+ return c.json({ data: { user, claims } });
458
+ });
459
+
460
+ // ── Sessions ──
461
+
462
+ // curl http://localhost:3000/auth/sessions -H 'Authorization: Bearer <token>'
463
+ app.get('/auth/sessions', async (c) => {
464
+ const userId = getUserId(c);
465
+ const sessions = await fortress.auth.listSessions(userId);
466
+ return c.json({ data: sessions });
467
+ });
468
+
469
+ // curl -X DELETE http://localhost:3000/auth/sessions/1 -H 'Authorization: Bearer <token>'
470
+ app.delete('/auth/sessions/:id', async (c) => {
471
+ const userId = getUserId(c);
472
+ const tokenId = c.req.param('id');
473
+ await fortress.auth.revokeSession(userId, tokenId);
474
+ return c.json({ data: { revoked: true } });
475
+ });
476
+
477
+ // curl -X DELETE http://localhost:3000/auth/sessions -H 'Authorization: Bearer <token>' \
478
+ // -H 'Content-Type: application/json' -d '{"currentTokenId":1}'
479
+ app.delete('/auth/sessions', async (c) => {
480
+ const userId = getUserId(c);
481
+ const { currentTokenId } = await c.req.json();
482
+ await fortress.auth.revokeAllOtherSessions(userId, currentTokenId);
483
+ return c.json({ data: { revokedOthers: true } });
484
+ });
485
+
486
+ // ── Two-Factor Auth ──
487
+
488
+ // curl -X POST http://localhost:3000/auth/2fa/enable -H 'Authorization: Bearer <token>'
489
+ app.post('/auth/2fa/enable', async (c) => {
490
+ const userId = getUserId(c);
491
+ const result = await fortress.plugins['two-factor'].enable(userId);
492
+ return c.json({ data: { otpauthUrl: result.otpauthUrl, backupCodes: result.backupCodes } });
493
+ });
494
+
495
+ // curl -X POST http://localhost:3000/auth/2fa/confirm-setup -H 'Authorization: Bearer <token>' \
496
+ // -H 'Content-Type: application/json' -d '{"code":"123456"}'
497
+ app.post('/auth/2fa/confirm-setup', async (c) => {
498
+ const userId = getUserId(c);
499
+ const { code } = await c.req.json();
500
+ const result = await fortress.plugins['two-factor'].confirmSetup(userId, code, {
501
+ userAgent: c.req.header('user-agent'),
502
+ });
503
+ return c.json({ data: result });
504
+ });
505
+
506
+ // Pending-login completion is public: the single-use continuation is the credential.
507
+ // curl -X POST http://localhost:3000/auth/2fa/verify \
508
+ // -H 'Content-Type: application/json' \
509
+ // -d '{"continuationToken":"<from-pending-login>","code":"123456"}'
510
+ app.post('/auth/2fa/verify', async (c) => {
511
+ const { continuationToken, code } = await c.req.json();
512
+ const result = await fortress.plugins['two-factor'].verify(continuationToken, code, {
513
+ userAgent: c.req.header('user-agent'),
514
+ });
515
+ return c.json({ data: result });
516
+ });
517
+
518
+ // curl -X POST http://localhost:3000/auth/2fa/disable -H 'Authorization: Bearer <token>'
519
+ app.post('/auth/2fa/disable', async (c) => {
520
+ const userId = getUserId(c);
521
+ await fortress.plugins['two-factor'].disable(userId);
522
+ return c.json({ data: { disabled: true } });
523
+ });
524
+
525
+ // ── Email Verification (resend) ──
526
+
527
+ // curl -X POST http://localhost:3000/auth/email/send-verification -H 'Authorization: Bearer <token>'
528
+ // (token is logged to console)
529
+ app.post('/auth/email/send-verification', async (c) => {
530
+ const userId = getUserId(c);
531
+ const result = await fortress.plugins['email-verification'].sendVerification(userId);
532
+ return c.json({ data: result });
533
+ });
534
+
535
+ // ── API Keys ──
536
+
537
+ // curl -X POST http://localhost:3000/auth/api-keys -H 'Authorization: Bearer <token>' \
538
+ // -H 'Content-Type: application/json' -d '{"name":"My CI Key","scopes":["read:users"]}'
539
+ app.post('/auth/api-keys', async (c) => {
540
+ const userId = getUserId(c);
541
+ const { name, scopes } = await c.req.json();
542
+ const result = await fortress.plugins['api-key'].createKey({ subject: { type: 'USER', id: userId }, name, scopes });
543
+ return c.json({ data: result }, 201);
544
+ });
545
+
546
+ // curl http://localhost:3000/auth/api-keys -H 'Authorization: Bearer <token>'
547
+ app.get('/auth/api-keys', async (c) => {
548
+ const userId = getUserId(c);
549
+ const keys = await fortress.plugins['api-key'].listKeys({ subject: { type: 'USER', id: userId } });
550
+ return c.json({ data: keys });
551
+ });
552
+
553
+ // curl -X DELETE http://localhost:3000/auth/api-keys/1 -H 'Authorization: Bearer <token>'
554
+ app.delete('/auth/api-keys/:id', async (c) => {
555
+ const userId = getUserId(c);
556
+ const keyId = c.req.param('id');
557
+ await fortress.plugins['api-key'].revokeKey({ subject: { type: 'USER', id: userId }, id: keyId });
558
+ return c.json({ data: { revoked: true } });
559
+ });
560
+
561
+ // curl -X POST http://localhost:3000/auth/api-keys/1/rotate -H 'Authorization: Bearer <token>'
562
+ app.post('/auth/api-keys/:id/rotate', async (c) => {
563
+ const userId = getUserId(c);
564
+ const keyId = c.req.param('id');
565
+ const result = await fortress.plugins['api-key'].rotateKey({ subject: { type: 'USER', id: userId }, id: keyId });
566
+ return c.json({ data: result });
567
+ });
568
+
569
+ // ── Service Accounts ──
570
+ //
571
+ // Service accounts are first-class IAM principals — no sessions or passwords,
572
+ // authenticated by long-lived api keys. Typical use: CI/CD, devices, M2M.
573
+ // These endpoints are admin-facing (the admin user creates service accounts
574
+ // and mints keys for them). Once a key is issued, the service account sends
575
+ // it in `Authorization: ApiKey <key>` or `X-API-Key: <key>` — see
576
+ // `/deploy/run` below for the consuming side.
577
+
578
+ // curl -X POST http://localhost:3000/admin/service-accounts \
579
+ // -H 'Authorization: Bearer <admin-token>' \
580
+ // -H 'Content-Type: application/json' \
581
+ // -d '{"name":"ci-deploy","displayName":"CI Deploy"}'
582
+ app.post('/admin/service-accounts', async (c) => {
583
+ const { name, displayName, description } = await c.req.json();
584
+ const sa = await fortress.iam.createServiceAccount({ name, displayName, description });
585
+ return c.json({ data: sa }, 201);
586
+ });
587
+
588
+ // Grant a service account a role.
589
+ // curl -X POST http://localhost:3000/admin/service-accounts/1/roles \
590
+ // -H 'Authorization: Bearer <admin-token>' -H 'Content-Type: application/json' \
591
+ // -d '{"roleId":42}'
592
+ app.post('/admin/service-accounts/:id/roles', async (c) => {
593
+ const saId = c.req.param('id');
594
+ const { roleId, tenantId } = await c.req.json();
595
+ await fortress.iam.bindRoleToServiceAccount(saId, String(roleId), tenantId);
596
+ return c.json({ data: { bound: true } });
597
+ });
598
+
599
+ // Mint an api key for a service account. The raw key is returned exactly once;
600
+ // store it in your CI secrets store immediately.
601
+ // curl -X POST http://localhost:3000/admin/service-accounts/1/keys \
602
+ // -H 'Authorization: Bearer <admin-token>' -H 'Content-Type: application/json' \
603
+ // -d '{"name":"ci-deploy-github-actions"}'
604
+ app.post('/admin/service-accounts/:id/keys', async (c) => {
605
+ const saId = c.req.param('id');
606
+ const { name, scopes, expiresAt } = await c.req.json();
607
+ const result = await fortress.plugins['api-key'].createKey({
608
+ subject: { type: 'SERVICE_ACCOUNT', id: saId },
609
+ name,
610
+ scopes,
611
+ expiresAt,
612
+ });
613
+ return c.json({ data: result }, 201);
614
+ });
615
+
616
+ // Deactivate (kill-switch) or delete a service account.
617
+ // curl -X PATCH http://localhost:3000/admin/service-accounts/1 \
618
+ // -H 'Authorization: Bearer <admin-token>' -H 'Content-Type: application/json' \
619
+ // -d '{"isActive":false}'
620
+ app.patch('/admin/service-accounts/:id', async (c) => {
621
+ const saId = c.req.param('id');
622
+ const patch = await c.req.json();
623
+ const updated = await fortress.iam.updateServiceAccount(saId, patch);
624
+ return c.json({ data: updated });
625
+ });
626
+
627
+ // Hard delete (cascades to role bindings, direct permission bindings, and keys).
628
+ // curl -X DELETE http://localhost:3000/admin/service-accounts/1 \
629
+ // -H 'Authorization: Bearer <admin-token>'
630
+ app.delete('/admin/service-accounts/:id', async (c) => {
631
+ const saId = c.req.param('id');
632
+ await fortress.iam.deleteServiceAccount(saId);
633
+ return c.json({ data: { deleted: true } });
634
+ });
635
+
636
+ // The consuming side — an endpoint a service account would call. This is
637
+ // mounted under the standard `authMiddleware`, so the api-key plugin's
638
+ // `resolvePrincipal` hook handles `Authorization: ApiKey <key>` /
639
+ // `X-API-Key` headers transparently. A JWT-authenticated browser session
640
+ // would work on exactly the same route.
641
+ //
642
+ // curl -X POST http://localhost:3000/deploy/run -H 'Authorization: ApiKey <key>'
643
+ app.post('/deploy/run', authMiddleware, async (c) => {
644
+ const subject = getSubject(c);
645
+ const allowed = await fortress.iam.checkPermission(subject, 'deploy', 'run');
646
+ if (!allowed)
647
+ return c.json({ error: 'Forbidden' }, 403);
648
+
649
+ // ... trigger the deploy ...
650
+ return c.json({ data: { deployId: 'dpl_abc123', startedBy: subject } });
651
+ });
652
+
653
+ // ── Social Login (linked accounts) ──
654
+
655
+ // curl http://localhost:3000/auth/social/accounts -H 'Authorization: Bearer <token>'
656
+ app.get('/auth/social/accounts', async (c) => {
657
+ const userId = getUserId(c);
658
+ const accounts = await fortress.plugins['social-login'].getLinkedAccounts(userId);
659
+ return c.json({ data: accounts });
660
+ });
661
+
662
+ // curl -X DELETE http://localhost:3000/auth/social/accounts/google -H 'Authorization: Bearer <token>'
663
+ app.delete('/auth/social/accounts/:provider', async (c) => {
664
+ const userId = getUserId(c);
665
+ const provider = c.req.param('provider');
666
+ await fortress.plugins['social-login'].unlinkAccount(userId, provider);
667
+ return c.json({ data: { unlinked: true } });
668
+ });
669
+
670
+ // ═══════════════════════════════════════════════════════════════════════════
671
+ // 5. Tenant routes (require JWT)
672
+ // ═══════════════════════════════════════════════════════════════════════════
673
+
674
+ app.use('/api/tenants/*', authMiddleware);
675
+ app.use('/api/tenants', authMiddleware);
676
+
677
+ // curl -X POST http://localhost:3000/api/tenants -H 'Authorization: Bearer <token>' \
678
+ // -H 'Content-Type: application/json' -d '{"name":"Acme Corp","taxId":"acme"}'
679
+ app.post('/api/tenants', async (c) => {
680
+ const { name, taxId, description } = await c.req.json();
681
+ const tenant = await fortress.plugins.tenancy.createTenant({ name, taxId, description });
682
+ return c.json({ data: tenant }, 201);
683
+ });
684
+
685
+ // curl http://localhost:3000/api/tenants -H 'Authorization: Bearer <token>'
686
+ app.get('/api/tenants', async (c) => {
687
+ const userId = getUserId(c);
688
+ const tenants = await fortress.plugins.tenancy.getUserTenants(userId);
689
+ return c.json({ data: tenants });
690
+ });
691
+
692
+ // curl -X POST http://localhost:3000/api/tenants/switch -H 'Authorization: Bearer <token>' \
693
+ // -H 'Content-Type: application/json' -d '{"taxId":"acme"}'
694
+ app.post('/api/tenants/switch', async (c) => {
695
+ const userId = getUserId(c);
696
+ const { taxId } = await c.req.json();
697
+ // userId is derived server-side from the verified token, never the body.
698
+ await fortress.plugins.tenancy.switchTenant({ taxId, userId });
699
+ return c.json({ data: { switched: true } });
700
+ });
701
+
702
+ // curl -X POST http://localhost:3000/api/tenants/1/members -H 'Authorization: Bearer <token>' \
703
+ // -H 'Content-Type: application/json' -d '{"userId":2}'
704
+ app.post('/api/tenants/:id/members', async (c) => {
705
+ const tenantId = c.req.param('id');
706
+ const { userId } = await c.req.json();
707
+ await fortress.plugins.tenancy.addUserToTenant(userId, tenantId);
708
+ return c.json({ data: { added: true } });
709
+ });
710
+
711
+ // ═══════════════════════════════════════════════════════════════════════════
712
+ // 6. RBAC-protected routes (require JWT + permissions)
713
+ // ═══════════════════════════════════════════════════════════════════════════
714
+
715
+ // Rate-limit all /api/* routes using the named 'api' rule defined above.
716
+ // Sits before authMiddleware so abusive unauthenticated hits are rejected
717
+ // early without the cost of principal resolution.
718
+ app.use('/api/*', honoRateLimit(fortress, 'api'));
719
+
720
+ app.use('/api/users', authMiddleware);
721
+ app.use('/api/groups/*', authMiddleware);
722
+ app.use('/api/groups', authMiddleware);
723
+ app.use('/api/users', rbacMiddleware);
724
+ app.use('/api/groups/*', rbacMiddleware);
725
+ app.use('/api/groups', rbacMiddleware);
726
+
727
+ // Host-owned route protected by endpoint metadata instead of an adapter routeMap.
728
+ // Runs the same core protections as fortress-managed routes, then calls this
729
+ // Hono handler. Register before mountFortress so the app-owned handler wins.
730
+ // curl http://localhost:3000/api/host-owned-stats -H 'Authorization: Bearer <token>'
731
+ app.get('/api/host-owned-stats', protectedRoute(fortress, hostOwnedStatsEndpoint, async (_c, ctx) => {
732
+ const users = await fortress.config.database.count({ model: 'user' });
733
+ return { users, subjectType: ctx.subject?.type ?? 'UNKNOWN' };
734
+ }));
735
+
736
+ // curl http://localhost:3000/api/users -H 'Authorization: Bearer <token>'
737
+ app.get('/api/users', async (c) => {
738
+ // getDb() returns the request-scoped adapter (tenant-aware when tenancy plugin is active)
739
+ const reqDb = getDb(c);
740
+ const users = await reqDb.findMany({ model: 'user' });
741
+ return c.json({ data: users });
742
+ });
743
+
744
+ // curl -X POST http://localhost:3000/api/users -H 'Authorization: Bearer <token>' \
745
+ // -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' \
746
+ // -d '{"email":"new@example.com","name":"New User"}'
747
+ app.post('/api/users', async (c) => {
748
+ const body = await c.req.json();
749
+ const user = await fortress.auth.createUser({ ...body, password: 'TempPassword123!' });
750
+ return c.json({ data: user }, 201);
751
+ });
752
+
753
+ // ── Groups ──
754
+
755
+ // curl -X POST http://localhost:3000/api/groups -H 'Authorization: Bearer <token>' \
756
+ // -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' \
757
+ // -d '{"name":"engineering","description":"Engineering team"}'
758
+ app.post('/api/groups', async (c) => {
759
+ const { name, description } = await c.req.json();
760
+ const group = await fortress.iam.createGroup(name, description);
761
+ return c.json({ data: group }, 201);
762
+ });
763
+
764
+ // curl -X POST http://localhost:3000/api/groups/1/members -H 'Authorization: Bearer <token>' \
765
+ // -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' -d '{"userId":2}'
766
+ app.post('/api/groups/:id/members', async (c) => {
767
+ const groupId = c.req.param('id');
768
+ const { userId } = await c.req.json();
769
+ await fortress.iam.addUserToGroup(groupId, userId);
770
+ return c.json({ data: { added: true } });
771
+ });
772
+
773
+ // curl -X DELETE http://localhost:3000/api/groups/1/members/2 -H 'Authorization: Bearer <token>'
774
+ app.delete('/api/groups/:id/members/:userId', async (c) => {
775
+ const groupId = c.req.param('id');
776
+ const userId = c.req.param('userId');
777
+ await fortress.iam.removeUserFromGroup(groupId, userId);
778
+ return c.json({ data: { removed: true } });
779
+ });
780
+
781
+ // ═══════════════════════════════════════════════════════════════════════════
782
+ // 7. Admin routes (require JWT + elevated permissions)
783
+ // ═══════════════════════════════════════════════════════════════════════════
784
+
785
+ app.use('/api/audit-log/*', authMiddleware);
786
+ app.use('/api/audit-log', authMiddleware);
787
+ app.use('/api/webhooks/*', authMiddleware);
788
+ app.use('/api/webhooks', authMiddleware);
789
+ app.use('/admin/*', authMiddleware);
790
+ app.use('/api/audit-log/*', rbacMiddleware);
791
+ app.use('/api/audit-log', rbacMiddleware);
792
+ app.use('/api/webhooks/*', rbacMiddleware);
793
+ app.use('/api/webhooks', rbacMiddleware);
794
+ app.use('/admin/*', rbacMiddleware);
795
+
796
+ // ── Audit Log ──
797
+
798
+ // curl 'http://localhost:3000/api/audit-log?limit=20' -H 'Authorization: Bearer <token>'
799
+ app.get('/api/audit-log', async (c) => {
800
+ const options: Record<string, unknown> = {};
801
+ const userId = c.req.query('userId');
802
+ const eventType = c.req.query('eventType');
803
+ const limit = c.req.query('limit');
804
+ if (userId)
805
+ options.userId = Number(userId);
806
+ if (eventType)
807
+ options.eventType = eventType;
808
+ if (limit)
809
+ options.limit = Number(limit);
810
+ const entries = await fortress.plugins['audit-log'].getAuditLog(options);
811
+ return c.json({ data: entries });
812
+ });
813
+
814
+ // curl -X POST http://localhost:3000/api/audit-log/verify-chain -H 'Authorization: Bearer <token>' \
815
+ // -H 'X-Fortress-CSRF: 1'
816
+ app.post('/api/audit-log/verify-chain', async (c) => {
817
+ const result = await fortress.plugins['audit-log'].verifyChain();
818
+ return c.json({ data: result });
819
+ });
820
+
821
+ // ── Impersonation ──
822
+
823
+ // curl -X POST http://localhost:3000/admin/impersonate -H 'Authorization: Bearer <token>' \
824
+ // -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' -d '{"targetUserId":2}'
825
+ app.post('/admin/impersonate', async (c) => {
826
+ const adminId = getUserId(c);
827
+ const { targetUserId, reason } = await c.req.json();
828
+ const result = await fortress.auth.impersonate(adminId, targetUserId, { reason });
829
+ return c.json({ data: result });
830
+ });
831
+
832
+ // ── Account Lockout ──
833
+
834
+ // curl http://localhost:3000/admin/lockout/user@example.com -H 'Authorization: Bearer <token>'
835
+ app.get('/admin/lockout/:identifier', async (c) => {
836
+ const identifier = c.req.param('identifier');
837
+ const status = await fortress.plugins['account-lockout'].getLockoutStatus(identifier);
838
+ return c.json({ data: status });
839
+ });
840
+
841
+ // curl -X POST http://localhost:3000/admin/lockout/user@example.com/reset \
842
+ // -H 'Authorization: Bearer <token>' -H 'X-Fortress-CSRF: 1'
843
+ app.post('/admin/lockout/:identifier/reset', async (c) => {
844
+ const identifier = c.req.param('identifier');
845
+ await fortress.plugins['account-lockout'].resetLockout(identifier);
846
+ return c.json({ data: { reset: true } });
847
+ });
848
+
849
+ // ── Webhooks ──
850
+
851
+ // curl -X POST http://localhost:3000/api/webhooks -H 'Authorization: Bearer <token>' \
852
+ // -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' \
853
+ // -d '{"url":"https://example.com/hook","events":["auth.login.success","order.shipped"]}'
854
+ // A signing secret is generated and returned ONCE on the response — store it now.
855
+ app.post('/api/webhooks', async (c) => {
856
+ const { url, events, secret } = await c.req.json();
857
+ const endpoint = await fortress.plugins.webhook.registerEndpoint(url, events, secret ? { secret } : undefined);
858
+ return c.json({ data: endpoint }, 201);
859
+ });
860
+
861
+ // curl http://localhost:3000/api/webhooks -H 'Authorization: Bearer <token>'
862
+ app.get('/api/webhooks', async (c) => {
863
+ const endpoints = await fortress.plugins.webhook.listEndpoints();
864
+ return c.json({ data: endpoints });
865
+ });
866
+
867
+ // curl -X DELETE http://localhost:3000/api/webhooks/1 -H 'Authorization: Bearer <token>'
868
+ app.delete('/api/webhooks/:id', async (c) => {
869
+ const id = c.req.param('id');
870
+ await fortress.plugins.webhook.removeEndpoint(id);
871
+ return c.json({ data: { removed: true } });
872
+ });
873
+
874
+ // Emit a custom event — delivered to every endpoint subscribed to 'order.shipped'.
875
+ // curl -X POST http://localhost:3000/api/events/order-shipped -H 'Authorization: Bearer <token>' \
876
+ // -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' -d '{"orderId":"o_123"}'
877
+ app.post('/api/events/order-shipped', async (c) => {
878
+ const { orderId } = await c.req.json();
879
+ await fortress.plugins.webhook.emit('order.shipped', { orderId }, { idempotencyKey: orderId });
880
+ return c.json({ data: { emitted: true } });
881
+ });
882
+
883
+ // ── Data Isolation (demonstrates getScopedDb) ──
884
+
885
+ app.use('/api/documents', authMiddleware);
886
+
887
+ // curl http://localhost:3000/api/documents -H 'Authorization: Bearer <token>'
888
+ // getScopedDb() applies row-level isolation — queries are automatically filtered
889
+ // by the user's org scope (configured in the dataIsolation plugin above).
890
+ app.get('/api/documents', async (c) => {
891
+ const scopedDb = await getScopedDb(c, 'document');
892
+ const docs = await scopedDb.findMany({ model: 'document' });
893
+ return c.json({ data: docs });
894
+ });
895
+
896
+ // ═══════════════════════════════════════════════════════════════════════════
897
+ // 8. Mount Fortress routes (auth, IAM, plugins, OAuth, OpenAPI)
898
+ // ═══════════════════════════════════════════════════════════════════════════
899
+ //
900
+ // `mountFortress` registers a single Hono middleware that detects any
901
+ // Fortress-managed path (`/auth/*`, `/iam/*`, `/oauth/*`, `/openapi*`,
902
+ // plugin routes) and delegates to `fortress.handleRequest`. The custom
903
+ // `/auth/*` handlers above are registered FIRST, so Hono's first-match
904
+ // routing means they win — `mountFortress` only handles paths the user
905
+ // did not handle manually.
906
+ //
907
+ // Auth issuing endpoints (login/refresh/impersonate) inside core
908
+ // dispatch automatically attach `Set-Cookie` headers using
909
+ // `FortressConfig.cookies` defaults.
910
+ mountFortress(app, fortress);
911
+
912
+ // ═══════════════════════════════════════════════════════════════════════════
913
+ // 9. Seed data on startup
914
+ // ═══════════════════════════════════════════════════════════════════════════
915
+
916
+ async function seed(): Promise<void> {
917
+ // Create users
918
+ const admin = await fortress.auth.createUser({
919
+ email: 'admin@example.com',
920
+ password: 'Password123!',
921
+ name: 'Admin User',
922
+ });
923
+ const user = await fortress.auth.createUser({
924
+ email: 'user@example.com',
925
+ password: 'Password123!',
926
+ name: 'Regular User',
927
+ });
928
+
929
+ // Mark emails as verified
930
+ await db.update({ model: 'user', where: [{ field: 'id', operator: '=', value: admin.id }], data: { emailVerified: true } });
931
+ await db.update({ model: 'user', where: [{ field: 'id', operator: '=', value: user.id }], data: { emailVerified: true } });
932
+
933
+ // Bootstrap admin user — creates fortress-admin role with all IAM permissions
934
+ await fortress.plugins.admin.bootstrap({ userId: admin.id });
935
+
936
+ // Create app-specific roles with permissions
937
+ const adminRole = await fortress.iam.createRole('admin', [
938
+ { resource: 'user', action: 'list' },
939
+ { resource: 'user', action: 'create' },
940
+ { resource: 'user', action: 'read' },
941
+ { resource: 'user', action: 'impersonate' },
942
+ { resource: 'group', action: 'create' },
943
+ { resource: 'group', action: 'manage' },
944
+ { resource: 'audit-log', action: 'read' },
945
+ { resource: 'webhook', action: 'manage' },
946
+ { resource: 'lockout', action: 'read' },
947
+ { resource: 'lockout', action: 'manage' },
948
+ ], 'Full admin access');
949
+
950
+ const viewerRole = await fortress.iam.createRole('viewer', [
951
+ { resource: 'user', action: 'list' },
952
+ { resource: 'user', action: 'read' },
953
+ ], 'Read-only access');
954
+
955
+ // Bind roles
956
+ await fortress.iam.bindRole('USER', admin.id, adminRole.id);
957
+ await fortress.iam.bindRole('USER', user.id, viewerRole.id);
958
+
959
+ // Create a group and add admin
960
+ const engineering = await fortress.iam.createGroup('engineering', 'Engineering team');
961
+ await fortress.iam.addUserToGroup(engineering.id, admin.id);
962
+
963
+ // Create a tenant and add admin
964
+ const tenant = await fortress.plugins.tenancy.createTenant({ name: 'Acme Corp', taxId: 'acme' });
965
+ await fortress.plugins.tenancy.addUserToTenant(admin.id, tenant.id);
966
+
967
+ // Register a webhook endpoint
968
+ await fortress.plugins.webhook.registerEndpoint(
969
+ 'https://example.com/webhooks',
970
+ ['LOGIN_SUCCESS', 'REGISTER'],
971
+ 'webhook-signing-secret',
972
+ );
973
+
974
+ // ── Typed in-process client ─────────────────────────────────────────
975
+ // `fortress.call.*` is the typed client surface. Each handler is
976
+ // inferred from its EndpointDefinition: input shape matches the body/
977
+ // query/params schemas, output matches the 2xx response schema.
978
+ //
979
+ // const { accessToken, user } = await fortress.call.login({
980
+ // identifier: 'admin@example.com',
981
+ // password: 'Password123!',
982
+ // });
983
+ //
984
+ // Under the hood, each call serializes to a Request, runs through
985
+ // fortress.handleRequest (full pipeline: middleware → token verify →
986
+ // RBAC → validation → dispatch), and parses the JSON body. Non-2xx
987
+ // responses throw a `FortressError` with the original `code`/`statusCode`.
988
+ // Same type surface a future over-the-wire client SDK will expose.
989
+
990
+ console.warn('');
991
+ console.warn('╔══════════════════════════════════════════════╗');
992
+ console.warn('║ Fortress Example App ║');
993
+ console.warn('╠══════════════════════════════════════════════╣');
994
+ console.warn('║ Admin: admin@example.com / Password123! ║');
995
+ console.warn('║ User: user@example.com / Password123! ║');
996
+ console.warn('║ Tenant: Acme Corp (taxId: acme) ║');
997
+ console.warn('║ Group: engineering ║');
998
+ console.warn('╠══════════════════════════════════════════════╣');
999
+ console.warn('║ http://localhost:3000 ║');
1000
+ console.warn('╚══════════════════════════════════════════════╝');
1001
+ console.warn('');
1002
+ }
1003
+
1004
+ seed().catch(console.error);
1005
+
1006
+ export default {
1007
+ port: 3000,
1008
+ fetch: app.fetch,
1009
+ };