@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
package/dist/jwt.cjs ADDED
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/core/auth/jwt.ts
21
+ var jwt_exports = {};
22
+ __export(jwt_exports, {
23
+ RESERVED_JWT_CLAIMS: () => RESERVED_JWT_CLAIMS,
24
+ signAccessToken: () => signAccessToken,
25
+ stripReservedClaims: () => stripReservedClaims,
26
+ verifyAccessToken: () => verifyAccessToken
27
+ });
28
+ module.exports = __toCommonJS(jwt_exports);
29
+ var import_jose = require("jose");
30
+
31
+ // src/core/errors.ts
32
+ var FortressError = class extends Error {
33
+ code;
34
+ statusCode;
35
+ retryAfter;
36
+ details;
37
+ /** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
38
+ oauthError;
39
+ /** Human-readable description for the OAuth `error_description` field. */
40
+ oauthDescription;
41
+ /** Optional URI for the OAuth `error_uri` field (§5.2). */
42
+ oauthErrorUri;
43
+ constructor(code, message, statusCode, options) {
44
+ super(message, { cause: options?.cause });
45
+ this.code = code;
46
+ this.statusCode = statusCode;
47
+ this.retryAfter = options?.retryAfter;
48
+ this.details = options?.details;
49
+ this.oauthError = options?.oauthError;
50
+ this.oauthDescription = options?.oauthDescription;
51
+ this.oauthErrorUri = options?.oauthErrorUri;
52
+ }
53
+ toJSON() {
54
+ return {
55
+ code: this.code,
56
+ message: this.message,
57
+ statusCode: this.statusCode,
58
+ ...this.details !== void 0 && { details: this.details }
59
+ };
60
+ }
61
+ };
62
+ var Errors = {
63
+ unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
64
+ tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
65
+ sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
66
+ sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
67
+ forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
68
+ badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
69
+ notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
70
+ conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
71
+ unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
72
+ rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
73
+ database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
74
+ serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
75
+ validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
76
+ /**
77
+ * RFC 6749 §5.2 / §4.1.2.1 OAuth error.
78
+ *
79
+ * The HTTP error mapper detects `oauthError` and emits the OAuth-spec
80
+ * JSON body `{ error, error_description, error_uri? }` instead of the
81
+ * default fortress `{ code, message }` shape, so strict OAuth clients
82
+ * (Moodle, openid-client, Spring Security, etc.) can switch behaviour
83
+ * on the machine-readable `error` field.
84
+ *
85
+ * Status is inferred from the OAuth code per the spec table:
86
+ * - `invalid_client` → 401
87
+ * - everything else → 400
88
+ */
89
+ oauth: (error, description, options) => {
90
+ const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
91
+ const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
92
+ return new FortressError(code, description ?? error, status, {
93
+ oauthError: error,
94
+ oauthDescription: description,
95
+ oauthErrorUri: options?.errorUri,
96
+ cause: options?.cause
97
+ });
98
+ },
99
+ /**
100
+ * Reconstruct a {@link FortressError} from a JSON error body emitted by
101
+ * `errorToResponse`. Used by the in-process `fortress.call.*` client to
102
+ * convert non-2xx responses into typed throws that mirror what a direct
103
+ * service-method call would produce.
104
+ *
105
+ * Maps by HTTP status when the body doesn't carry a recognizable
106
+ * `{ code, message }` payload, so network errors and opaque upstream
107
+ * failures still become structured `FortressError`s.
108
+ */
109
+ fromHttpResponse: (status, body) => {
110
+ const payload = body && typeof body === "object" ? body : {};
111
+ const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
112
+ const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
113
+ return new FortressError(code, message, status, { details: payload.details });
114
+ }
115
+ };
116
+ function isFortressErrorCode(code) {
117
+ return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
118
+ }
119
+ function statusToErrorCode(status) {
120
+ if (status === 401)
121
+ return "UNAUTHORIZED";
122
+ if (status === 403)
123
+ return "FORBIDDEN";
124
+ if (status === 404)
125
+ return "NOT_FOUND";
126
+ if (status === 409)
127
+ return "CONFLICT";
128
+ if (status === 422)
129
+ return "UNPROCESSABLE_ENTITY";
130
+ if (status === 429)
131
+ return "RATE_LIMITED";
132
+ if (status === 503)
133
+ return "SERVICE_UNAVAILABLE";
134
+ if (status >= 500)
135
+ return "DATABASE_ERROR";
136
+ return "BAD_REQUEST";
137
+ }
138
+
139
+ // src/core/auth/jwt.ts
140
+ var RESERVED_JWT_CLAIMS = /* @__PURE__ */ new Set([
141
+ "sub",
142
+ "iss",
143
+ "iat",
144
+ "exp",
145
+ "nbf",
146
+ "aud",
147
+ "jti",
148
+ "act",
149
+ "groups",
150
+ "subjectType",
151
+ "name"
152
+ ]);
153
+ var REQUIRED_ACCESS_TOKEN_CLAIMS = ["sub", "subjectType", "name", "groups", "iss", "iat", "exp"];
154
+ var SUBJECT_TYPES = ["USER", "GROUP", "SERVICE_ACCOUNT"];
155
+ function stripReservedClaims(claims, context) {
156
+ if (!claims)
157
+ return {};
158
+ const out = {};
159
+ let stripped;
160
+ for (const [key, value] of Object.entries(claims)) {
161
+ if (RESERVED_JWT_CLAIMS.has(key)) {
162
+ (stripped ??= []).push(key);
163
+ continue;
164
+ }
165
+ out[key] = value;
166
+ }
167
+ if (stripped && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
168
+ console.warn(`[fortress/jwt] dropping reserved claim(s) from ${context}: ${stripped.join(", ")}`);
169
+ }
170
+ return out;
171
+ }
172
+ function encodeSecret(secret) {
173
+ return new TextEncoder().encode(secret);
174
+ }
175
+ function normalizeKeys(key) {
176
+ const keys = Array.isArray(key) ? key : [key];
177
+ return keys.map(encodeSecret);
178
+ }
179
+ async function signAccessToken(claims, key, expiresInSeconds, options) {
180
+ const [signingKey] = normalizeKeys(key);
181
+ const safeCustom = stripReservedClaims(claims.customClaims, "signAccessToken.customClaims");
182
+ let jwt = new import_jose.SignJWT({
183
+ name: claims.name,
184
+ subjectType: claims.subjectType,
185
+ groups: claims.groups,
186
+ ...claims.act ? { act: claims.act } : {},
187
+ ...safeCustom
188
+ }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${expiresInSeconds}s`).setIssuer(claims.iss);
189
+ if (options?.audience !== void 0)
190
+ jwt = jwt.setAudience(options.audience);
191
+ return jwt.setSubject(claims.sub).sign(signingKey);
192
+ }
193
+ async function verifyAccessToken(token, key, options) {
194
+ const keys = normalizeKeys(key);
195
+ const requiredClaims = [.../* @__PURE__ */ new Set([...REQUIRED_ACCESS_TOKEN_CLAIMS, ...options?.requiredClaims ?? []])];
196
+ const verifyOptions = { algorithms: ["HS256"], requiredClaims };
197
+ if (options?.issuer)
198
+ verifyOptions.issuer = options.issuer;
199
+ if (options?.audience)
200
+ verifyOptions.audience = options.audience;
201
+ if (options?.clockTolerance !== void 0)
202
+ verifyOptions.clockTolerance = options.clockTolerance;
203
+ for (const candidate of keys) {
204
+ try {
205
+ const { payload } = await (0, import_jose.jwtVerify)(token, candidate, verifyOptions);
206
+ const subjectType = payload.subjectType;
207
+ const groups = payload.groups;
208
+ const name = payload.name;
209
+ const issuer = payload.iss;
210
+ const audience = payload.aud;
211
+ const issuedAt = payload.iat;
212
+ const expiresAt = payload.exp;
213
+ if (typeof payload.sub !== "string" || payload.sub.length === 0)
214
+ throw new Error("Invalid sub claim");
215
+ if (typeof subjectType !== "string" || !SUBJECT_TYPES.includes(subjectType))
216
+ throw new Error("Invalid subjectType claim");
217
+ if (typeof name !== "string")
218
+ throw new Error("Invalid name claim");
219
+ if (!Array.isArray(groups) || !groups.every((group) => typeof group === "string"))
220
+ throw new Error("Invalid groups claim");
221
+ if (typeof issuer !== "string" || issuer.length === 0)
222
+ throw new Error("Invalid iss claim");
223
+ if (audience !== void 0 && (!Array.isArray(audience) || !audience.every((value) => typeof value === "string")) && typeof audience !== "string")
224
+ throw new Error("Invalid aud claim");
225
+ if (typeof issuedAt !== "number")
226
+ throw new Error("Invalid iat claim");
227
+ if (typeof expiresAt !== "number")
228
+ throw new Error("Invalid exp claim");
229
+ return {
230
+ sub: payload.sub,
231
+ subjectType,
232
+ name,
233
+ groups,
234
+ iss: issuer,
235
+ ...audience !== void 0 ? { aud: audience } : {},
236
+ iat: issuedAt,
237
+ exp: expiresAt,
238
+ act: payload.act,
239
+ customClaims: Object.fromEntries(
240
+ Object.entries(payload).filter(([k]) => !RESERVED_JWT_CLAIMS.has(k))
241
+ )
242
+ };
243
+ } catch {
244
+ continue;
245
+ }
246
+ }
247
+ throw Errors.unauthorized("Invalid or expired token");
248
+ }
249
+ // Annotate the CommonJS export names for ESM import in node:
250
+ 0 && (module.exports = {
251
+ RESERVED_JWT_CLAIMS,
252
+ signAccessToken,
253
+ stripReservedClaims,
254
+ verifyAccessToken
255
+ });
package/dist/jwt.d.cts ADDED
@@ -0,0 +1,90 @@
1
+ import { T as TokenClaims } from './types-et0R8tsC.cjs';
2
+
3
+ /**
4
+ * JWT signing and verification helpers built on `jose` (Web Crypto API).
5
+ *
6
+ * Provides {@link signAccessToken} and {@link verifyAccessToken} for short-lived
7
+ * access tokens. Refresh tokens are handled separately in
8
+ * `core/auth/refresh-token.ts`. Supports key rotation by accepting an array
9
+ * of keys — the first is used to sign, all are accepted on verify.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ /**
15
+ * Key material accepted by {@link signAccessToken} / {@link verifyAccessToken}.
16
+ *
17
+ * Today this is just an HS256 shared secret (string) or a rotation array of
18
+ * shared secrets. The alias exists so the public signature is stable when we
19
+ * expand to asymmetric algorithms (RS256 / EdDSA) and JWKS-backed verification.
20
+ *
21
+ * Expected expansion:
22
+ *
23
+ * ```ts
24
+ * import type { CryptoKey, JWK, JWTVerifyGetKey, KeyObject } from 'jose';
25
+ *
26
+ * export type JwtStaticKey =
27
+ * | string // HS* shared secret (utf-8)
28
+ * | Uint8Array // HS* shared secret (raw bytes)
29
+ * | CryptoKey // Web Crypto key (any alg)
30
+ * | KeyObject // Node KeyObject (any alg)
31
+ * | JWK; // JSON Web Key
32
+ *
33
+ * export type JwtKeyMaterial = JwtStaticKey | readonly JwtStaticKey[];
34
+ *
35
+ * // Verification additionally accepts a resolver (e.g. createRemoteJWKSet):
36
+ * export type JwtVerifyKeyMaterial = JwtKeyMaterial | JWTVerifyGetKey;
37
+ * ```
38
+ *
39
+ * Until then, the runtime helpers ({@link normalizeKeys}) only know how to
40
+ * encode strings — widening the type without widening the helpers would be a
41
+ * lie, so do both together.
42
+ */
43
+ type JwtKeyMaterial = string | string[];
44
+ /**
45
+ * JWT claim names fortress controls and that must never be overridable by
46
+ * caller-supplied `customClaims` or by plugin `enrichTokenClaims` output.
47
+ *
48
+ * Letting a caller set `act` would forge an impersonation marker; letting a
49
+ * caller set `sub`/`iss`/`exp` would let them mint a token for any subject
50
+ * or extend its lifetime. The denylist is enforced in {@link signAccessToken}
51
+ * — extra claims are dropped (with a `warn` log in dev) before the safe
52
+ * claims are spread.
53
+ */
54
+ declare const RESERVED_JWT_CLAIMS: ReadonlySet<string>;
55
+ interface SignAccessTokenOptions {
56
+ /** Audience to include in the JWT `aud` claim. Omitted when unset. */
57
+ audience?: string | string[];
58
+ }
59
+ interface VerifyAccessTokenOptions {
60
+ /** Expected issuer. When set, jose also requires the `iss` claim to be present. */
61
+ issuer?: string | string[];
62
+ /** Expected audience. When set, jose also requires the `aud` claim to be present. */
63
+ audience?: string | string[];
64
+ /** Clock skew tolerance for `nbf`, `exp`, and max-token-age checks. */
65
+ clockTolerance?: string | number;
66
+ /** Additional claim names that must be present alongside Fortress's required access-token claims. */
67
+ requiredClaims?: string[];
68
+ }
69
+ /**
70
+ * Strip any reserved keys from a custom-claims object. In development we
71
+ * log a warning so library users notice the collision; in production we
72
+ * silently drop (the safe default — a misbehaving plugin shouldn't be able
73
+ * to break token issuance).
74
+ */
75
+ declare function stripReservedClaims(claims: Record<string, unknown> | undefined, context: string): Record<string, unknown>;
76
+ /**
77
+ * Sign a JWT access token.
78
+ */
79
+ declare function signAccessToken(claims: Omit<TokenClaims, 'iat' | 'exp' | 'aud'>, key: JwtKeyMaterial, expiresInSeconds: number, options?: SignAccessTokenOptions): Promise<string>;
80
+ /**
81
+ * Verify a JWT access token. Tries each key in order for rotation support.
82
+ *
83
+ * Pins `alg: HS256` and (when supplied) the expected `issuer`. jose already
84
+ * rejects `alg: none` by default; the explicit allowlist is defense in
85
+ * depth against future jose behavioural drift and against confused-deputy
86
+ * attacks if anyone later adds asymmetric key support.
87
+ */
88
+ declare function verifyAccessToken(token: string, key: JwtKeyMaterial, options?: VerifyAccessTokenOptions): Promise<TokenClaims>;
89
+
90
+ export { type JwtKeyMaterial, RESERVED_JWT_CLAIMS, type SignAccessTokenOptions, type VerifyAccessTokenOptions, signAccessToken, stripReservedClaims, verifyAccessToken };
package/dist/jwt.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { T as TokenClaims } from './types-et0R8tsC.js';
2
+
3
+ /**
4
+ * JWT signing and verification helpers built on `jose` (Web Crypto API).
5
+ *
6
+ * Provides {@link signAccessToken} and {@link verifyAccessToken} for short-lived
7
+ * access tokens. Refresh tokens are handled separately in
8
+ * `core/auth/refresh-token.ts`. Supports key rotation by accepting an array
9
+ * of keys — the first is used to sign, all are accepted on verify.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ /**
15
+ * Key material accepted by {@link signAccessToken} / {@link verifyAccessToken}.
16
+ *
17
+ * Today this is just an HS256 shared secret (string) or a rotation array of
18
+ * shared secrets. The alias exists so the public signature is stable when we
19
+ * expand to asymmetric algorithms (RS256 / EdDSA) and JWKS-backed verification.
20
+ *
21
+ * Expected expansion:
22
+ *
23
+ * ```ts
24
+ * import type { CryptoKey, JWK, JWTVerifyGetKey, KeyObject } from 'jose';
25
+ *
26
+ * export type JwtStaticKey =
27
+ * | string // HS* shared secret (utf-8)
28
+ * | Uint8Array // HS* shared secret (raw bytes)
29
+ * | CryptoKey // Web Crypto key (any alg)
30
+ * | KeyObject // Node KeyObject (any alg)
31
+ * | JWK; // JSON Web Key
32
+ *
33
+ * export type JwtKeyMaterial = JwtStaticKey | readonly JwtStaticKey[];
34
+ *
35
+ * // Verification additionally accepts a resolver (e.g. createRemoteJWKSet):
36
+ * export type JwtVerifyKeyMaterial = JwtKeyMaterial | JWTVerifyGetKey;
37
+ * ```
38
+ *
39
+ * Until then, the runtime helpers ({@link normalizeKeys}) only know how to
40
+ * encode strings — widening the type without widening the helpers would be a
41
+ * lie, so do both together.
42
+ */
43
+ type JwtKeyMaterial = string | string[];
44
+ /**
45
+ * JWT claim names fortress controls and that must never be overridable by
46
+ * caller-supplied `customClaims` or by plugin `enrichTokenClaims` output.
47
+ *
48
+ * Letting a caller set `act` would forge an impersonation marker; letting a
49
+ * caller set `sub`/`iss`/`exp` would let them mint a token for any subject
50
+ * or extend its lifetime. The denylist is enforced in {@link signAccessToken}
51
+ * — extra claims are dropped (with a `warn` log in dev) before the safe
52
+ * claims are spread.
53
+ */
54
+ declare const RESERVED_JWT_CLAIMS: ReadonlySet<string>;
55
+ interface SignAccessTokenOptions {
56
+ /** Audience to include in the JWT `aud` claim. Omitted when unset. */
57
+ audience?: string | string[];
58
+ }
59
+ interface VerifyAccessTokenOptions {
60
+ /** Expected issuer. When set, jose also requires the `iss` claim to be present. */
61
+ issuer?: string | string[];
62
+ /** Expected audience. When set, jose also requires the `aud` claim to be present. */
63
+ audience?: string | string[];
64
+ /** Clock skew tolerance for `nbf`, `exp`, and max-token-age checks. */
65
+ clockTolerance?: string | number;
66
+ /** Additional claim names that must be present alongside Fortress's required access-token claims. */
67
+ requiredClaims?: string[];
68
+ }
69
+ /**
70
+ * Strip any reserved keys from a custom-claims object. In development we
71
+ * log a warning so library users notice the collision; in production we
72
+ * silently drop (the safe default — a misbehaving plugin shouldn't be able
73
+ * to break token issuance).
74
+ */
75
+ declare function stripReservedClaims(claims: Record<string, unknown> | undefined, context: string): Record<string, unknown>;
76
+ /**
77
+ * Sign a JWT access token.
78
+ */
79
+ declare function signAccessToken(claims: Omit<TokenClaims, 'iat' | 'exp' | 'aud'>, key: JwtKeyMaterial, expiresInSeconds: number, options?: SignAccessTokenOptions): Promise<string>;
80
+ /**
81
+ * Verify a JWT access token. Tries each key in order for rotation support.
82
+ *
83
+ * Pins `alg: HS256` and (when supplied) the expected `issuer`. jose already
84
+ * rejects `alg: none` by default; the explicit allowlist is defense in
85
+ * depth against future jose behavioural drift and against confused-deputy
86
+ * attacks if anyone later adds asymmetric key support.
87
+ */
88
+ declare function verifyAccessToken(token: string, key: JwtKeyMaterial, options?: VerifyAccessTokenOptions): Promise<TokenClaims>;
89
+
90
+ export { type JwtKeyMaterial, RESERVED_JWT_CLAIMS, type SignAccessTokenOptions, type VerifyAccessTokenOptions, signAccessToken, stripReservedClaims, verifyAccessToken };
package/dist/jwt.js ADDED
@@ -0,0 +1,227 @@
1
+ // src/core/auth/jwt.ts
2
+ import { jwtVerify, SignJWT } from "jose";
3
+
4
+ // src/core/errors.ts
5
+ var FortressError = class extends Error {
6
+ code;
7
+ statusCode;
8
+ retryAfter;
9
+ details;
10
+ /** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
11
+ oauthError;
12
+ /** Human-readable description for the OAuth `error_description` field. */
13
+ oauthDescription;
14
+ /** Optional URI for the OAuth `error_uri` field (§5.2). */
15
+ oauthErrorUri;
16
+ constructor(code, message, statusCode, options) {
17
+ super(message, { cause: options?.cause });
18
+ this.code = code;
19
+ this.statusCode = statusCode;
20
+ this.retryAfter = options?.retryAfter;
21
+ this.details = options?.details;
22
+ this.oauthError = options?.oauthError;
23
+ this.oauthDescription = options?.oauthDescription;
24
+ this.oauthErrorUri = options?.oauthErrorUri;
25
+ }
26
+ toJSON() {
27
+ return {
28
+ code: this.code,
29
+ message: this.message,
30
+ statusCode: this.statusCode,
31
+ ...this.details !== void 0 && { details: this.details }
32
+ };
33
+ }
34
+ };
35
+ var Errors = {
36
+ unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
37
+ tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
38
+ sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
39
+ sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
40
+ forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
41
+ badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
42
+ notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
43
+ conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
44
+ unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
45
+ rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
46
+ database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
47
+ serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
48
+ validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
49
+ /**
50
+ * RFC 6749 §5.2 / §4.1.2.1 OAuth error.
51
+ *
52
+ * The HTTP error mapper detects `oauthError` and emits the OAuth-spec
53
+ * JSON body `{ error, error_description, error_uri? }` instead of the
54
+ * default fortress `{ code, message }` shape, so strict OAuth clients
55
+ * (Moodle, openid-client, Spring Security, etc.) can switch behaviour
56
+ * on the machine-readable `error` field.
57
+ *
58
+ * Status is inferred from the OAuth code per the spec table:
59
+ * - `invalid_client` → 401
60
+ * - everything else → 400
61
+ */
62
+ oauth: (error, description, options) => {
63
+ const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
64
+ const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
65
+ return new FortressError(code, description ?? error, status, {
66
+ oauthError: error,
67
+ oauthDescription: description,
68
+ oauthErrorUri: options?.errorUri,
69
+ cause: options?.cause
70
+ });
71
+ },
72
+ /**
73
+ * Reconstruct a {@link FortressError} from a JSON error body emitted by
74
+ * `errorToResponse`. Used by the in-process `fortress.call.*` client to
75
+ * convert non-2xx responses into typed throws that mirror what a direct
76
+ * service-method call would produce.
77
+ *
78
+ * Maps by HTTP status when the body doesn't carry a recognizable
79
+ * `{ code, message }` payload, so network errors and opaque upstream
80
+ * failures still become structured `FortressError`s.
81
+ */
82
+ fromHttpResponse: (status, body) => {
83
+ const payload = body && typeof body === "object" ? body : {};
84
+ const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
85
+ const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
86
+ return new FortressError(code, message, status, { details: payload.details });
87
+ }
88
+ };
89
+ function isFortressErrorCode(code) {
90
+ return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
91
+ }
92
+ function statusToErrorCode(status) {
93
+ if (status === 401)
94
+ return "UNAUTHORIZED";
95
+ if (status === 403)
96
+ return "FORBIDDEN";
97
+ if (status === 404)
98
+ return "NOT_FOUND";
99
+ if (status === 409)
100
+ return "CONFLICT";
101
+ if (status === 422)
102
+ return "UNPROCESSABLE_ENTITY";
103
+ if (status === 429)
104
+ return "RATE_LIMITED";
105
+ if (status === 503)
106
+ return "SERVICE_UNAVAILABLE";
107
+ if (status >= 500)
108
+ return "DATABASE_ERROR";
109
+ return "BAD_REQUEST";
110
+ }
111
+
112
+ // src/core/auth/jwt.ts
113
+ var RESERVED_JWT_CLAIMS = /* @__PURE__ */ new Set([
114
+ "sub",
115
+ "iss",
116
+ "iat",
117
+ "exp",
118
+ "nbf",
119
+ "aud",
120
+ "jti",
121
+ "act",
122
+ "groups",
123
+ "subjectType",
124
+ "name"
125
+ ]);
126
+ var REQUIRED_ACCESS_TOKEN_CLAIMS = ["sub", "subjectType", "name", "groups", "iss", "iat", "exp"];
127
+ var SUBJECT_TYPES = ["USER", "GROUP", "SERVICE_ACCOUNT"];
128
+ function stripReservedClaims(claims, context) {
129
+ if (!claims)
130
+ return {};
131
+ const out = {};
132
+ let stripped;
133
+ for (const [key, value] of Object.entries(claims)) {
134
+ if (RESERVED_JWT_CLAIMS.has(key)) {
135
+ (stripped ??= []).push(key);
136
+ continue;
137
+ }
138
+ out[key] = value;
139
+ }
140
+ if (stripped && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
141
+ console.warn(`[fortress/jwt] dropping reserved claim(s) from ${context}: ${stripped.join(", ")}`);
142
+ }
143
+ return out;
144
+ }
145
+ function encodeSecret(secret) {
146
+ return new TextEncoder().encode(secret);
147
+ }
148
+ function normalizeKeys(key) {
149
+ const keys = Array.isArray(key) ? key : [key];
150
+ return keys.map(encodeSecret);
151
+ }
152
+ async function signAccessToken(claims, key, expiresInSeconds, options) {
153
+ const [signingKey] = normalizeKeys(key);
154
+ const safeCustom = stripReservedClaims(claims.customClaims, "signAccessToken.customClaims");
155
+ let jwt = new SignJWT({
156
+ name: claims.name,
157
+ subjectType: claims.subjectType,
158
+ groups: claims.groups,
159
+ ...claims.act ? { act: claims.act } : {},
160
+ ...safeCustom
161
+ }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${expiresInSeconds}s`).setIssuer(claims.iss);
162
+ if (options?.audience !== void 0)
163
+ jwt = jwt.setAudience(options.audience);
164
+ return jwt.setSubject(claims.sub).sign(signingKey);
165
+ }
166
+ async function verifyAccessToken(token, key, options) {
167
+ const keys = normalizeKeys(key);
168
+ const requiredClaims = [.../* @__PURE__ */ new Set([...REQUIRED_ACCESS_TOKEN_CLAIMS, ...options?.requiredClaims ?? []])];
169
+ const verifyOptions = { algorithms: ["HS256"], requiredClaims };
170
+ if (options?.issuer)
171
+ verifyOptions.issuer = options.issuer;
172
+ if (options?.audience)
173
+ verifyOptions.audience = options.audience;
174
+ if (options?.clockTolerance !== void 0)
175
+ verifyOptions.clockTolerance = options.clockTolerance;
176
+ for (const candidate of keys) {
177
+ try {
178
+ const { payload } = await jwtVerify(token, candidate, verifyOptions);
179
+ const subjectType = payload.subjectType;
180
+ const groups = payload.groups;
181
+ const name = payload.name;
182
+ const issuer = payload.iss;
183
+ const audience = payload.aud;
184
+ const issuedAt = payload.iat;
185
+ const expiresAt = payload.exp;
186
+ if (typeof payload.sub !== "string" || payload.sub.length === 0)
187
+ throw new Error("Invalid sub claim");
188
+ if (typeof subjectType !== "string" || !SUBJECT_TYPES.includes(subjectType))
189
+ throw new Error("Invalid subjectType claim");
190
+ if (typeof name !== "string")
191
+ throw new Error("Invalid name claim");
192
+ if (!Array.isArray(groups) || !groups.every((group) => typeof group === "string"))
193
+ throw new Error("Invalid groups claim");
194
+ if (typeof issuer !== "string" || issuer.length === 0)
195
+ throw new Error("Invalid iss claim");
196
+ if (audience !== void 0 && (!Array.isArray(audience) || !audience.every((value) => typeof value === "string")) && typeof audience !== "string")
197
+ throw new Error("Invalid aud claim");
198
+ if (typeof issuedAt !== "number")
199
+ throw new Error("Invalid iat claim");
200
+ if (typeof expiresAt !== "number")
201
+ throw new Error("Invalid exp claim");
202
+ return {
203
+ sub: payload.sub,
204
+ subjectType,
205
+ name,
206
+ groups,
207
+ iss: issuer,
208
+ ...audience !== void 0 ? { aud: audience } : {},
209
+ iat: issuedAt,
210
+ exp: expiresAt,
211
+ act: payload.act,
212
+ customClaims: Object.fromEntries(
213
+ Object.entries(payload).filter(([k]) => !RESERVED_JWT_CLAIMS.has(k))
214
+ )
215
+ };
216
+ } catch {
217
+ continue;
218
+ }
219
+ }
220
+ throw Errors.unauthorized("Invalid or expired token");
221
+ }
222
+ export {
223
+ RESERVED_JWT_CLAIMS,
224
+ signAccessToken,
225
+ stripReservedClaims,
226
+ verifyAccessToken
227
+ };