@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,1158 @@
1
+ // src/core/schema-builder.ts
2
+ import { fromJSONSchema } from "@bajustone/fetcher/openapi";
3
+ import { date as fDate, datetime as fDatetime, email as fEmail, time as fTime, url as fUrl, uuid as fUuid } from "@bajustone/fetcher/schema";
4
+ function collectRefNames(schema, acc) {
5
+ if (!schema || typeof schema !== "object")
6
+ return acc;
7
+ if (typeof schema.$ref === "string") {
8
+ const i = schema.$ref.lastIndexOf("/");
9
+ acc.add(i >= 0 ? schema.$ref.slice(i + 1) : schema.$ref);
10
+ }
11
+ if (schema.properties) {
12
+ for (const key of Object.keys(schema.properties))
13
+ collectRefNames(schema.properties[key], acc);
14
+ }
15
+ collectRefNames(schema.items, acc);
16
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
17
+ const variants = schema[key];
18
+ if (variants) {
19
+ for (const variant of variants)
20
+ collectRefNames(variant, acc);
21
+ }
22
+ }
23
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object")
24
+ collectRefNames(schema.additionalProperties, acc);
25
+ return acc;
26
+ }
27
+ var refDefinitionContexts = /* @__PURE__ */ new WeakMap();
28
+ function refName(refPath) {
29
+ const i = refPath.lastIndexOf("/");
30
+ return i >= 0 ? refPath.slice(i + 1) : refPath;
31
+ }
32
+ var COMPONENT_NAME_RE = /^[\w.-]+$/;
33
+ function assertComponentName(name) {
34
+ if (!COMPONENT_NAME_RE.test(name))
35
+ throw new Error(`Invalid OpenAPI component name '${name}'`);
36
+ }
37
+ function resolveRefDefs(schema) {
38
+ if (collectRefNames(schema, /* @__PURE__ */ new Set()).size === 0)
39
+ return void 0;
40
+ const defs = /* @__PURE__ */ Object.create(null);
41
+ const unresolved = /* @__PURE__ */ new Set();
42
+ const hasOwn = (value, key) => Object.hasOwn(value, key);
43
+ const visit = (node, inherited) => {
44
+ if (!node || typeof node !== "object")
45
+ return;
46
+ const context = refDefinitionContexts.get(node) ?? inherited;
47
+ if (typeof node.$ref === "string") {
48
+ const name = refName(node.$ref);
49
+ const component = context && hasOwn(context, name) ? context[name] : void 0;
50
+ if (component) {
51
+ if (hasOwn(defs, name) && !unresolved.has(name) && defs[name] !== component) {
52
+ throw new Error(`Conflicting component definitions for $ref '${name}'`);
53
+ }
54
+ const firstResolution = !hasOwn(defs, name) || unresolved.delete(name);
55
+ defs[name] = component;
56
+ if (firstResolution)
57
+ visit(component, context);
58
+ } else if (!hasOwn(defs, name)) {
59
+ defs[name] = {};
60
+ unresolved.add(name);
61
+ }
62
+ }
63
+ if (node.properties) {
64
+ for (const child of Object.values(node.properties))
65
+ visit(child, context);
66
+ }
67
+ visit(node.items, context);
68
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
69
+ for (const child of node[key] ?? [])
70
+ visit(child, context);
71
+ }
72
+ if (node.additionalProperties && typeof node.additionalProperties === "object")
73
+ visit(node.additionalProperties, context);
74
+ };
75
+ visit(schema);
76
+ return defs;
77
+ }
78
+ function createStandardProps(schema) {
79
+ let validateFn;
80
+ return {
81
+ version: 1,
82
+ vendor: "fortress",
83
+ validate(value) {
84
+ if (!validateFn) {
85
+ const defs = resolveRefDefs(schema);
86
+ validateFn = fromJSONSchema(schema, defs)["~standard"].validate;
87
+ }
88
+ return validateFn(value);
89
+ }
90
+ };
91
+ }
92
+ function createExactOneOfProps(schemas) {
93
+ const validate = (value) => {
94
+ const results = schemas.map((schema) => schema["~standard"].validate(value));
95
+ if (results.some((result) => result instanceof Promise)) {
96
+ return Promise.all(results).then((resolved) => exactOneResult(resolved));
97
+ }
98
+ return exactOneResult(results);
99
+ };
100
+ return {
101
+ version: 1,
102
+ vendor: "fortress",
103
+ validate
104
+ };
105
+ }
106
+ function exactOneResult(results) {
107
+ const successes = results.filter((result) => !result.issues);
108
+ if (successes.length === 1)
109
+ return { value: successes[0].value };
110
+ return {
111
+ issues: [{
112
+ message: successes.length === 0 ? "Value must match exactly one oneOf variant" : "Value must match exactly one oneOf variant; multiple variants matched"
113
+ }]
114
+ };
115
+ }
116
+ function toFortressSchema(schema, refDefinitions, standardProps) {
117
+ if (refDefinitions)
118
+ refDefinitionContexts.set(schema, refDefinitions);
119
+ return Object.assign(schema, {
120
+ "~standard": standardProps ?? createStandardProps(schema)
121
+ });
122
+ }
123
+ function str(opts) {
124
+ const o = typeof opts === "string" ? { description: opts } : opts ?? {};
125
+ const s = { type: "string" };
126
+ if (o.description)
127
+ s.description = o.description;
128
+ if (o.min !== void 0)
129
+ s.minLength = o.min;
130
+ if (o.max !== void 0)
131
+ s.maxLength = o.max;
132
+ if (o.pattern !== void 0)
133
+ s.pattern = o.pattern;
134
+ if (o.format !== void 0)
135
+ s.format = o.format;
136
+ return toFortressSchema(s);
137
+ }
138
+ function int(opts) {
139
+ const o = typeof opts === "string" ? { description: opts } : opts ?? {};
140
+ const s = { type: "integer" };
141
+ if (o.description)
142
+ s.description = o.description;
143
+ if (o.min !== void 0)
144
+ s.minimum = o.min;
145
+ if (o.max !== void 0)
146
+ s.maximum = o.max;
147
+ return toFortressSchema(s);
148
+ }
149
+ function id(description) {
150
+ const s = { type: "string", minLength: 1 };
151
+ if (description)
152
+ s.description = description;
153
+ return toFortressSchema(s);
154
+ }
155
+ function bool(description) {
156
+ const s = { type: "boolean" };
157
+ if (description)
158
+ s.description = description;
159
+ return toFortressSchema(s);
160
+ }
161
+ function arr(items, description) {
162
+ const s = { type: "array", items };
163
+ if (description)
164
+ s.description = description;
165
+ return toFortressSchema(s);
166
+ }
167
+ function obj(properties, ...required) {
168
+ const s = { type: "object", properties };
169
+ if (required.length > 0)
170
+ s.required = required;
171
+ return toFortressSchema(s);
172
+ }
173
+ function nullable(schema) {
174
+ if (typeof schema.$ref === "string")
175
+ return toFortressSchema({ oneOf: [schema, { type: "null" }] });
176
+ const s = { ...schema, nullable: true };
177
+ return toFortressSchema(s, refDefinitionContexts.get(schema));
178
+ }
179
+ function oneOf(...schemas) {
180
+ const schema = { oneOf: schemas };
181
+ return toFortressSchema(
182
+ schema,
183
+ void 0,
184
+ createExactOneOfProps(schemas)
185
+ );
186
+ }
187
+ function defineComponents(components) {
188
+ for (const name of Object.keys(components))
189
+ assertComponentName(name);
190
+ const definitions = Object.assign(
191
+ /* @__PURE__ */ Object.create(null),
192
+ components
193
+ );
194
+ return {
195
+ components,
196
+ ref: (name) => {
197
+ assertComponentName(name);
198
+ return toFortressSchema(
199
+ { $ref: `#/components/schemas/${String(name)}` },
200
+ definitions
201
+ );
202
+ }
203
+ };
204
+ }
205
+ function enums(...values) {
206
+ return toFortressSchema({ enum: values });
207
+ }
208
+ function strFormat(format, description) {
209
+ const s = { type: "string", format };
210
+ if (description)
211
+ s.description = description;
212
+ return toFortressSchema(s);
213
+ }
214
+ function nullType() {
215
+ return toFortressSchema({ type: "null" });
216
+ }
217
+ function record(description) {
218
+ const s = { type: "object", additionalProperties: true };
219
+ if (description)
220
+ s.description = description;
221
+ return toFortressSchema(s);
222
+ }
223
+ function recordOf(valueSchema, description) {
224
+ const s = { type: "object", additionalProperties: valueSchema };
225
+ if (description)
226
+ s.description = description;
227
+ return toFortressSchema(s);
228
+ }
229
+ function makeFormatBuilder(factory) {
230
+ const f = factory();
231
+ return (description) => {
232
+ const s = { type: "string" };
233
+ if (f.format)
234
+ s.format = f.format;
235
+ if (f.pattern)
236
+ s.pattern = f.pattern;
237
+ if (description)
238
+ s.description = description;
239
+ return toFortressSchema(s);
240
+ };
241
+ }
242
+ var email = makeFormatBuilder(fEmail);
243
+ var uuid = makeFormatBuilder(fUuid);
244
+ var url = makeFormatBuilder(fUrl);
245
+ var datetime = makeFormatBuilder(fDatetime);
246
+ var date = makeFormatBuilder(fDate);
247
+ var time = makeFormatBuilder(fTime);
248
+ function isStandardSchema(value) {
249
+ return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
250
+ }
251
+ function isFortressSchema(value) {
252
+ return isStandardSchema(value) && value["~standard"].vendor === "fortress" && ("type" in value || "$ref" in value || "oneOf" in value || "anyOf" in value);
253
+ }
254
+ var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
255
+ function hasJsonSchemaShape(value) {
256
+ if (typeof value !== "object" || value === null)
257
+ return false;
258
+ const v = value;
259
+ if ("$ref" in v || "oneOf" in v || "anyOf" in v || "allOf" in v)
260
+ return true;
261
+ if (typeof v.type === "string" && JSON_SCHEMA_TYPES.has(v.type))
262
+ return true;
263
+ if (Array.isArray(v.type) && v.type.every((t) => typeof t === "string" && JSON_SCHEMA_TYPES.has(t)))
264
+ return true;
265
+ return false;
266
+ }
267
+ function extractJsonSchema(schema) {
268
+ if (isFortressSchema(schema)) {
269
+ return schema;
270
+ }
271
+ if (isStandardSchema(schema)) {
272
+ const std = schema["~standard"];
273
+ if (std?.jsonSchema?.input) {
274
+ return std.jsonSchema.input({ target: "draft-2020-12" });
275
+ }
276
+ if (hasJsonSchemaShape(schema)) {
277
+ return schema;
278
+ }
279
+ }
280
+ return {};
281
+ }
282
+ var ErrorEnvelope = obj({
283
+ code: str("Machine-readable error code"),
284
+ message: str("Human-readable error message"),
285
+ statusCode: int("HTTP status code"),
286
+ details: toFortressSchema({ description: "Optional structured error details" })
287
+ }, "code", "message", "statusCode");
288
+ var EndpointBuilder = class {
289
+ _method;
290
+ _path;
291
+ _handler = "";
292
+ _summary = "";
293
+ _description;
294
+ _tags = [];
295
+ _security = [];
296
+ _deprecated = false;
297
+ _permission;
298
+ _body;
299
+ _query;
300
+ _params;
301
+ _responses = {};
302
+ constructor(method, path) {
303
+ this._method = method;
304
+ this._path = path;
305
+ }
306
+ summary(s) {
307
+ this._summary = s;
308
+ return this;
309
+ }
310
+ description(s) {
311
+ this._description = s;
312
+ return this;
313
+ }
314
+ tags(...t) {
315
+ this._tags.push(...t);
316
+ return this;
317
+ }
318
+ security(...s) {
319
+ this._security.push(...s);
320
+ return this;
321
+ }
322
+ deprecated() {
323
+ this._deprecated = true;
324
+ return this;
325
+ }
326
+ permission(resource, action) {
327
+ this._permission = { resource, action };
328
+ return this;
329
+ }
330
+ body(schema) {
331
+ this._body = schema;
332
+ return this;
333
+ }
334
+ query(schema) {
335
+ this._query = schema;
336
+ return this;
337
+ }
338
+ params(schema) {
339
+ this._params = schema;
340
+ return this;
341
+ }
342
+ response(status, description, schema) {
343
+ const stored = { description };
344
+ if (schema !== void 0) {
345
+ stored.schema = isStandardSchema(schema) ? extractJsonSchema(schema) : schema;
346
+ }
347
+ this._responses[status] = stored;
348
+ return this;
349
+ }
350
+ /**
351
+ * Declare an error response that returns the canonical {@link ErrorEnvelope}
352
+ * body. Shorthand for `.response(status, description, ErrorEnvelope)`.
353
+ *
354
+ * Aligns host endpoint error responses with what Fortress's own routes
355
+ * emit, so a single client-side error parser works everywhere.
356
+ *
357
+ * ```ts
358
+ * endpoint('GET', '/schools/:id')
359
+ * .summary('Get a school')
360
+ * .params(obj({ id: str() }, 'id'))
361
+ * .response(200, 'OK', SchoolEnvelope)
362
+ * .errorResponse(404, 'School not found')
363
+ * .errorResponse(403, 'Forbidden')
364
+ * .build();
365
+ * ```
366
+ */
367
+ errorResponse(status, description) {
368
+ return this.response(status, description, ErrorEnvelope);
369
+ }
370
+ handler(name) {
371
+ this._handler = name;
372
+ return this;
373
+ }
374
+ build() {
375
+ const def = {
376
+ method: this._method,
377
+ path: this._path,
378
+ handler: this._handler
379
+ };
380
+ if (this._summary || this._tags.length > 0 || this._security.length > 0 || this._description || this._deprecated || this._permission) {
381
+ def.meta = {
382
+ summary: this._summary,
383
+ ...this._description && { description: this._description },
384
+ ...this._tags.length > 0 && { tags: this._tags },
385
+ ...this._security.length > 0 && { security: this._security },
386
+ ...this._deprecated && { deprecated: true },
387
+ ...this._permission && { permission: this._permission }
388
+ };
389
+ }
390
+ if (this._body || this._query || this._params) {
391
+ def.input = {};
392
+ if (this._body) {
393
+ def.input.body = extractJsonSchema(this._body);
394
+ if (isStandardSchema(this._body))
395
+ def.input.bodySchema = this._body;
396
+ }
397
+ if (this._query) {
398
+ def.input.query = extractJsonSchema(this._query);
399
+ if (isStandardSchema(this._query))
400
+ def.input.querySchema = this._query;
401
+ }
402
+ if (this._params) {
403
+ def.input.params = extractJsonSchema(this._params);
404
+ if (isStandardSchema(this._params))
405
+ def.input.paramsSchema = this._params;
406
+ }
407
+ }
408
+ if (Object.keys(this._responses).length > 0) {
409
+ def.responses = this._responses;
410
+ }
411
+ return def;
412
+ }
413
+ };
414
+ function endpoint(method, path) {
415
+ return new EndpointBuilder(method, path);
416
+ }
417
+
418
+ // src/core/auth/auth-endpoints.ts
419
+ var User = obj(
420
+ {
421
+ id: id("User ID"),
422
+ email: strFormat("email", "User email"),
423
+ name: str("Display name"),
424
+ isActive: bool("Account active status"),
425
+ emailVerified: bool("Email verification status"),
426
+ createdAt: strFormat("date-time", "Creation timestamp"),
427
+ updatedAt: strFormat("date-time", "Last update timestamp")
428
+ },
429
+ "id",
430
+ "email",
431
+ "name",
432
+ "isActive",
433
+ "createdAt",
434
+ "updatedAt"
435
+ );
436
+ var AuthChallenge = obj(
437
+ {
438
+ reason: enums("two-factor", "webauthn", "email-verification", "magic-link"),
439
+ continuationToken: str("Single-use post-auth continuation token")
440
+ },
441
+ "reason",
442
+ "continuationToken"
443
+ );
444
+ var AuthResult = oneOf(
445
+ obj(
446
+ {
447
+ status: enums("success"),
448
+ user: User,
449
+ method: enums("password", "refresh", "two-factor", "webauthn", "magic-link", "impersonation"),
450
+ accessToken: str("JWT access token"),
451
+ refreshToken: str("Refresh token for rotation"),
452
+ pluginData: record()
453
+ },
454
+ "status",
455
+ "user",
456
+ "method",
457
+ "accessToken",
458
+ "refreshToken"
459
+ ),
460
+ obj(
461
+ {
462
+ status: enums("pending"),
463
+ user: User,
464
+ pending: AuthChallenge,
465
+ pluginData: record()
466
+ },
467
+ "status",
468
+ "user",
469
+ "pending"
470
+ ),
471
+ obj(
472
+ {
473
+ status: enums("impersonation"),
474
+ user: User,
475
+ accessToken: str("JWT access token"),
476
+ refreshToken: nullType(),
477
+ pluginData: record()
478
+ },
479
+ "status",
480
+ "user",
481
+ "accessToken",
482
+ "refreshToken"
483
+ )
484
+ );
485
+ var AuthTokenPair = obj(
486
+ {
487
+ accessToken: str("JWT access token"),
488
+ refreshToken: str("New refresh token")
489
+ },
490
+ "accessToken",
491
+ "refreshToken"
492
+ );
493
+ var SessionInfo = obj(
494
+ {
495
+ id: id("Token ID"),
496
+ ipAddress: nullable(str("Client IP address")),
497
+ userAgent: nullable(str("Client user agent")),
498
+ deviceName: nullable(str("Device name")),
499
+ createdAt: strFormat("date-time", "Session creation time"),
500
+ lastActiveAt: nullable(strFormat("date-time", "Last activity time"))
501
+ },
502
+ "id",
503
+ "createdAt"
504
+ );
505
+ var CreateUserInput = obj(
506
+ {
507
+ email: email("User email"),
508
+ // enforced format (ReDoS-safe pattern), not annotation-only
509
+ name: str("Display name"),
510
+ password: str("User password"),
511
+ isActive: bool("Set active status (default true)")
512
+ },
513
+ "email",
514
+ "name"
515
+ );
516
+ var ErrorResponse = obj(
517
+ {
518
+ code: str("Error code (e.g. UNAUTHORIZED)"),
519
+ message: str("Human-readable message"),
520
+ statusCode: int("HTTP status code")
521
+ },
522
+ "code",
523
+ "message",
524
+ "statusCode"
525
+ );
526
+ var LoginIdentifier = obj(
527
+ {
528
+ id: id("Identifier ID"),
529
+ userId: id("User ID"),
530
+ type: enums("email", "phone", "username"),
531
+ value: str("Identifier value")
532
+ },
533
+ "id",
534
+ "userId",
535
+ "type",
536
+ "value"
537
+ );
538
+ var authComponents = defineComponents({
539
+ User,
540
+ AuthChallenge,
541
+ AuthResult,
542
+ AuthTokenPair,
543
+ SessionInfo,
544
+ CreateUserInput,
545
+ ErrorResponse,
546
+ LoginIdentifier
547
+ });
548
+ var authComponentSchemas = authComponents.components;
549
+ var authRef = authComponents.ref;
550
+ var authEndpoints = {
551
+ login: endpoint("POST", "/auth/login").summary("Login with credentials").tags("Auth").security("none").body(obj(
552
+ {
553
+ identifier: str("Email, username, or phone"),
554
+ password: str("User password"),
555
+ trustedDeviceToken: str("Opaque trusted-device token previously returned after two-factor verification")
556
+ },
557
+ "identifier",
558
+ "password"
559
+ )).response(200, "Login successful", authRef("AuthResult")).response(401, "Invalid credentials", authRef("ErrorResponse")).handler("login").build(),
560
+ verifyTwoFactor: endpoint("POST", "/auth/2fa/verify").summary("Complete a pending two-factor challenge").tags("Auth", "Two-Factor").security("none").body(obj(
561
+ {
562
+ continuationToken: str("Single-use auth continuation token"),
563
+ code: str("TOTP or backup code"),
564
+ rememberDevice: bool("Issue an opaque trusted-device token after successful verification")
565
+ },
566
+ "continuationToken",
567
+ "code"
568
+ )).response(200, "Authentication result", authRef("AuthResult")).response(400, "Two-factor plugin unavailable", authRef("ErrorResponse")).response(401, "Invalid continuation or verification code", authRef("ErrorResponse")).handler("verifyTwoFactor").build(),
569
+ verifyMagicLink: endpoint("POST", "/auth/magic-link/verify").summary("Verify a magic-link token").tags("Auth", "Magic Link").security("none").body(obj({ token: str("Raw magic-link token") }, "token")).response(200, "Authentication result", authRef("AuthResult")).response(400, "Magic-link plugin unavailable", authRef("ErrorResponse")).response(404, "Invalid or expired magic link", authRef("ErrorResponse")).handler("verifyMagicLink").build(),
570
+ createUser: endpoint("POST", "/auth/register").summary("Create a new user").tags("Auth").security("none").body(CreateUserInput).response(201, "User created", authRef("User")).response(409, "Email already exists", authRef("ErrorResponse")).handler("createUser").build(),
571
+ refresh: endpoint("POST", "/auth/refresh").summary("Refresh access token").tags("Auth").security("none").body(obj({ refreshToken: str("Current refresh token") }, "refreshToken")).response(200, "Tokens refreshed", authRef("AuthTokenPair")).response(401, "Invalid or expired refresh token", authRef("ErrorResponse")).handler("refresh").build(),
572
+ logout: endpoint("POST", "/auth/logout").summary("Logout and revoke refresh token").tags("Auth").security("none").body(obj({ refreshToken: str("Refresh token to revoke") }, "refreshToken")).response(200, "Logged out", obj({ ok: bool() }, "ok")).handler("logout").build(),
573
+ me: endpoint("GET", "/auth/me").summary("Get current user profile").tags("Auth").security("bearer").response(200, "Current user", authRef("User")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("me").build(),
574
+ listSessions: endpoint("GET", "/auth/sessions").summary("List active sessions").tags("Auth").security("bearer").response(200, "Active sessions", arr(authRef("SessionInfo"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("listSessions").build(),
575
+ revokeSession: endpoint("DELETE", "/auth/sessions/:id").summary("Revoke a specific session").tags("Auth").security("bearer").params(obj({ id: id("Session/token ID") }, "id")).response(200, "Session revoked", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("revokeSession").build(),
576
+ revokeAllOtherSessions: endpoint("DELETE", "/auth/sessions").summary("Revoke all other sessions").description("Revokes all active sessions except the current one. Requires the current token ID.").tags("Auth").security("bearer").body(obj({ currentTokenId: id("Current token ID to keep") }, "currentTokenId")).response(200, "Other sessions revoked", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("revokeAllOtherSessions").build(),
577
+ addLoginIdentifier: endpoint("POST", "/auth/identifiers").summary("Add a login identifier").tags("Auth").security("bearer").body(obj(
578
+ {
579
+ type: enums("email", "phone", "username"),
580
+ value: str("Identifier value")
581
+ },
582
+ "type",
583
+ "value"
584
+ )).response(200, "Identifier added", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("addLoginIdentifier").build(),
585
+ removeLoginIdentifier: endpoint("DELETE", "/auth/identifiers").summary("Remove a login identifier").tags("Auth").security("bearer").body(obj(
586
+ {
587
+ type: enums("email", "phone", "username"),
588
+ value: str("Identifier value")
589
+ },
590
+ "type",
591
+ "value"
592
+ )).response(200, "Identifier removed", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("removeLoginIdentifier").build(),
593
+ getLoginIdentifiers: endpoint("GET", "/auth/identifiers").summary("List login identifiers").tags("Auth").security("bearer").response(200, "Login identifiers", arr(authRef("LoginIdentifier"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getLoginIdentifiers").build(),
594
+ impersonate: endpoint("POST", "/auth/impersonate").summary("Impersonate a user").description("Issue a short-lived, non-renewable token to act as another user. Requires fortress:impersonate permission.").tags("Auth").security("bearer").permission("fortress", "impersonate").body(obj(
595
+ {
596
+ targetUserId: id("User to impersonate"),
597
+ reason: str("Reason for impersonation"),
598
+ expirySeconds: int("Token expiry in seconds (default 3600)")
599
+ },
600
+ "targetUserId"
601
+ )).response(200, "Impersonation token issued", authRef("AuthResult")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Target user not found", authRef("ErrorResponse")).handler("impersonate").build()
602
+ };
603
+
604
+ // src/core/iam/iam-endpoints.ts
605
+ var Role = obj(
606
+ {
607
+ id: id("Role ID"),
608
+ name: str("Role name"),
609
+ description: nullable(str("Role description")),
610
+ isSystem: bool("Whether this is a system role")
611
+ },
612
+ "id",
613
+ "name"
614
+ );
615
+ var Group = obj(
616
+ {
617
+ id: id("Group ID"),
618
+ name: str("Group name"),
619
+ description: nullable(str("Group description"))
620
+ },
621
+ "id",
622
+ "name"
623
+ );
624
+ var Permission = obj(
625
+ {
626
+ id: id("Permission ID"),
627
+ resource: str("Resource name"),
628
+ action: str("Action name"),
629
+ effect: enums("ALLOW", "DENY"),
630
+ conditions: arr(obj({
631
+ field: str("Condition field"),
632
+ operator: enums("eq", "neq", "in", "startsWith"),
633
+ value: str("Condition value")
634
+ }), "Permission conditions"),
635
+ description: nullable(str("Permission description"))
636
+ },
637
+ "id",
638
+ "resource",
639
+ "action",
640
+ "effect"
641
+ );
642
+ var PermissionInput = obj(
643
+ {
644
+ resource: str("Resource name"),
645
+ action: str("Action name"),
646
+ effect: enums("ALLOW", "DENY"),
647
+ conditions: arr(obj({
648
+ field: str("Condition field"),
649
+ operator: enums("eq", "neq", "in", "startsWith"),
650
+ value: str("Condition value")
651
+ }))
652
+ },
653
+ "resource",
654
+ "action"
655
+ );
656
+ var ServiceAccount = obj(
657
+ {
658
+ id: id("Service account ID"),
659
+ name: str("Machine identifier \u2014 immutable after creation"),
660
+ displayName: nullable(str("Human-readable label")),
661
+ description: nullable(str("Free-form description")),
662
+ isActive: bool("Whether the account can authenticate and resolve permissions"),
663
+ createdAt: str("ISO 8601 creation timestamp"),
664
+ updatedAt: str("ISO 8601 update timestamp")
665
+ },
666
+ "id",
667
+ "name",
668
+ "isActive"
669
+ );
670
+ var iamComponents = defineComponents({
671
+ Role,
672
+ Group,
673
+ Permission,
674
+ PermissionInput,
675
+ ServiceAccount
676
+ });
677
+ var iamComponentSchemas = iamComponents.components;
678
+ var iamRef = iamComponents.ref;
679
+ var iamEndpoints = {
680
+ // ── Resources ──
681
+ getResources: endpoint("GET", "/iam/resources").summary("List all available resources").tags("IAM", "Resources").security("bearer").permission("fortress", "viewResources").response(200, "Available resources with their actions", obj({
682
+ resources: recordOf(obj({
683
+ actions: arr(str("Action name"), "Available actions"),
684
+ description: str("Resource description")
685
+ }), "Map of resource name to definition")
686
+ }, "resources")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getResources").build(),
687
+ // ── Roles ──
688
+ getRoles: endpoint("GET", "/iam/roles").summary("List all roles").tags("IAM", "Roles").security("bearer").permission("fortress", "viewRoles").response(200, "All roles", arr(iamRef("Role"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getRoles").build(),
689
+ createRole: endpoint("POST", "/iam/roles").summary("Create a role").tags("IAM", "Roles").security("bearer").permission("fortress", "createRole").body(obj(
690
+ {
691
+ name: str("Role name"),
692
+ permissions: arr(iamRef("PermissionInput"), "Permissions to assign"),
693
+ description: str("Role description")
694
+ },
695
+ "name",
696
+ "permissions"
697
+ )).response(201, "Role created", iamRef("Role")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createRole").build(),
698
+ deleteRole: endpoint("DELETE", "/iam/roles/:id").summary("Delete a role").tags("IAM", "Roles").security("bearer").permission("fortress", "deleteRole").params(obj({ id: id("Role ID") }, "id")).response(200, "Role deleted", obj({ ok: bool() }, "ok")).response(400, "Cannot delete system role", authRef("ErrorResponse")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("deleteRole").build(),
699
+ // ── Role Bindings ──
700
+ bindRoleToUser: endpoint("POST", "/iam/roles/:id/bind/user").summary("Bind role to a user").tags("IAM", "Roles").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
701
+ { userId: id("User ID"), tenantId: str("Tenant ID (optional)") },
702
+ "userId"
703
+ )).response(200, "Role bound to user", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToUser").build(),
704
+ bindRoleToGroup: endpoint("POST", "/iam/roles/:id/bind/group").summary("Bind role to a group").tags("IAM", "Roles").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
705
+ { groupId: id("Group ID"), tenantId: str("Tenant ID (optional)") },
706
+ "groupId"
707
+ )).response(200, "Role bound to group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToGroup").build(),
708
+ unbindRole: endpoint("DELETE", "/iam/roles/:id/bind").summary("Unbind a role").tags("IAM", "Roles").security("bearer").permission("fortress", "unbindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
709
+ {
710
+ subjectType: enums("USER", "GROUP", "SERVICE_ACCOUNT"),
711
+ subjectId: id("Subject ID"),
712
+ tenantId: str("Tenant ID (optional)")
713
+ },
714
+ "subjectType",
715
+ "subjectId"
716
+ )).response(200, "Role unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindRole").build(),
717
+ // ── Groups ──
718
+ createGroup: endpoint("POST", "/iam/groups").summary("Create a group").tags("IAM", "Groups").security("bearer").permission("fortress", "createGroup").body(obj(
719
+ { name: str("Group name"), description: str("Group description") },
720
+ "name"
721
+ )).response(201, "Group created", iamRef("Group")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createGroup").build(),
722
+ addUserToGroup: endpoint("POST", "/iam/groups/:id/users").summary("Add user to group").tags("IAM", "Groups").security("bearer").permission("fortress", "manageGroup").params(obj({ id: id("Group ID") }, "id")).body(obj({ userId: id("User ID") }, "userId")).response(200, "User added to group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("addUserToGroup").build(),
723
+ removeUserFromGroup: endpoint("DELETE", "/iam/groups/:id/users/:userId").summary("Remove user from group").tags("IAM", "Groups").security("bearer").permission("fortress", "manageGroup").params(obj({ id: id("Group ID"), userId: id("User ID") }, "id", "userId")).response(200, "User removed from group", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("removeUserFromGroup").build(),
724
+ // ── Permissions ──
725
+ getUserPermissions: endpoint("GET", "/iam/users/:id/permissions").summary("Get user permissions").tags("IAM", "Permissions").security("bearer").permission("fortress", "viewPermissions").params(obj({ id: id("User ID") }, "id")).query(obj({ tenantId: str("Tenant ID (optional)") })).response(200, "User permissions", arr(iamRef("Permission"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getUserPermissions").build(),
726
+ checkPermission: endpoint("POST", "/iam/check").summary("Check if user has permission").tags("IAM", "Permissions").security("bearer").permission("fortress", "viewPermissions").body(obj(
727
+ {
728
+ userId: id("User ID"),
729
+ resource: str("Resource name"),
730
+ action: str("Action name"),
731
+ context: record("Permission context")
732
+ },
733
+ "userId",
734
+ "resource",
735
+ "action"
736
+ )).response(200, "Permission check result", obj({ allowed: bool("Whether permission is granted") }, "allowed")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("checkPermission").build(),
737
+ bindPermissionToUser: endpoint("POST", "/iam/permissions/bind/user").summary("Bind permission directly to a user").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
738
+ {
739
+ userId: id("User ID"),
740
+ permission: iamRef("PermissionInput"),
741
+ tenantId: str("Tenant ID (optional)")
742
+ },
743
+ "userId",
744
+ "permission"
745
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToUser").build(),
746
+ bindPermissionToGroup: endpoint("POST", "/iam/permissions/bind/group").summary("Bind permission directly to a group").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
747
+ {
748
+ groupId: id("Group ID"),
749
+ permission: iamRef("PermissionInput"),
750
+ tenantId: str("Tenant ID (optional)")
751
+ },
752
+ "groupId",
753
+ "permission"
754
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToGroup").build(),
755
+ unbindPermissionFromUser: endpoint("DELETE", "/iam/permissions/bind/user").summary("Unbind permission from a user").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
756
+ {
757
+ userId: id("User ID"),
758
+ permissionId: id("Permission ID"),
759
+ tenantId: str("Tenant ID (optional)")
760
+ },
761
+ "userId",
762
+ "permissionId"
763
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromUser").build(),
764
+ unbindPermissionFromGroup: endpoint("DELETE", "/iam/permissions/bind/group").summary("Unbind permission from a group").tags("IAM", "Permissions").security("bearer").permission("fortress", "managePermissions").body(obj(
765
+ {
766
+ groupId: id("Group ID"),
767
+ permissionId: id("Permission ID"),
768
+ tenantId: str("Tenant ID (optional)")
769
+ },
770
+ "groupId",
771
+ "permissionId"
772
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromGroup").build(),
773
+ // ── Service Accounts ──
774
+ createServiceAccount: endpoint("POST", "/iam/service-accounts").summary("Create a service account").description("Create a non-human IAM principal. Service accounts hold roles and direct permissions just like users, but have no sessions, passwords, or group memberships. The `name` is immutable after creation.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "createServiceAccount").body(obj(
775
+ {
776
+ name: str("Machine identifier (immutable)"),
777
+ displayName: str("Human-readable label"),
778
+ description: str("Free-form description")
779
+ },
780
+ "name"
781
+ )).response(201, "Service account created", iamRef("ServiceAccount")).response(400, "Bad request", authRef("ErrorResponse")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("createServiceAccount").build(),
782
+ listServiceAccounts: endpoint("GET", "/iam/service-accounts").summary("List service accounts").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "viewServiceAccounts").query(obj({
783
+ limit: int("Page size (default 50)"),
784
+ offset: int("Offset from the start of the list")
785
+ })).response(200, "Service accounts", obj({
786
+ serviceAccounts: arr(iamRef("ServiceAccount")),
787
+ total: int("Total service accounts")
788
+ }, "serviceAccounts", "total")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("listServiceAccounts").build(),
789
+ getServiceAccount: endpoint("GET", "/iam/service-accounts/:id").summary("Get a service account by ID").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "viewServiceAccounts").params(obj({ id: id("Service account ID") }, "id")).response(200, "Service account", iamRef("ServiceAccount")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("getServiceAccount").build(),
790
+ updateServiceAccount: endpoint("PATCH", "/iam/service-accounts/:id").summary("Update a service account").description("Update displayName, description, or isActive. The `name` field is immutable \u2014 to rename, delete and recreate.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "manageServiceAccount").params(obj({ id: id("Service account ID") }, "id")).body(obj({
791
+ displayName: nullable(str("New human-readable label")),
792
+ description: nullable(str("New description")),
793
+ isActive: bool("Whether the account is active")
794
+ })).response(200, "Service account updated", iamRef("ServiceAccount")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("updateServiceAccount").build(),
795
+ deleteServiceAccount: endpoint("DELETE", "/iam/service-accounts/:id").summary("Delete a service account").description("Hard-deletes the service account and cascades to role bindings, direct permission bindings, and (via plugin observer) api keys owned by the account.").tags("IAM", "Service Accounts").security("bearer").permission("fortress", "manageServiceAccount").params(obj({ id: id("Service account ID") }, "id")).response(200, "Service account deleted", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).response(404, "Not found", authRef("ErrorResponse")).handler("deleteServiceAccount").build(),
796
+ getServiceAccountPermissions: endpoint("GET", "/iam/service-accounts/:id/permissions").summary("Get effective permissions for a service account").tags("IAM", "Service Accounts", "Permissions").security("bearer").permission("fortress", "viewPermissions").params(obj({ id: id("Service account ID") }, "id")).query(obj({ tenantId: str("Tenant ID (optional)") })).response(200, "Service account permissions", arr(iamRef("Permission"))).response(401, "Not authenticated", authRef("ErrorResponse")).handler("getServiceAccountPermissions").build(),
797
+ bindRoleToServiceAccount: endpoint("POST", "/iam/roles/:id/bind/service-account").summary("Bind role to a service account").tags("IAM", "Roles", "Service Accounts").security("bearer").permission("fortress", "bindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
798
+ { serviceAccountId: id("Service account ID"), tenantId: str("Tenant ID (optional)") },
799
+ "serviceAccountId"
800
+ )).response(200, "Role bound to service account", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindRoleToServiceAccount").build(),
801
+ unbindRoleFromServiceAccount: endpoint("DELETE", "/iam/roles/:id/bind/service-account").summary("Unbind role from a service account").tags("IAM", "Roles", "Service Accounts").security("bearer").permission("fortress", "unbindRole").params(obj({ id: id("Role ID") }, "id")).body(obj(
802
+ { serviceAccountId: id("Service account ID"), tenantId: str("Tenant ID (optional)") },
803
+ "serviceAccountId"
804
+ )).response(200, "Role unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindRoleFromServiceAccount").build(),
805
+ bindPermissionToServiceAccount: endpoint("POST", "/iam/permissions/bind/service-account").summary("Bind permission directly to a service account").tags("IAM", "Permissions", "Service Accounts").security("bearer").permission("fortress", "managePermissions").body(obj(
806
+ {
807
+ serviceAccountId: id("Service account ID"),
808
+ permission: iamRef("PermissionInput"),
809
+ tenantId: str("Tenant ID (optional)")
810
+ },
811
+ "serviceAccountId",
812
+ "permission"
813
+ )).response(200, "Permission bound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("bindPermissionToServiceAccount").build(),
814
+ unbindPermissionFromServiceAccount: endpoint("DELETE", "/iam/permissions/bind/service-account").summary("Unbind permission from a service account").tags("IAM", "Permissions", "Service Accounts").security("bearer").permission("fortress", "managePermissions").body(obj(
815
+ {
816
+ serviceAccountId: id("Service account ID"),
817
+ permissionId: id("Permission ID"),
818
+ tenantId: str("Tenant ID (optional)")
819
+ },
820
+ "serviceAccountId",
821
+ "permissionId"
822
+ )).response(200, "Permission unbound", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", authRef("ErrorResponse")).handler("unbindPermissionFromServiceAccount").build()
823
+ };
824
+
825
+ // src/plugins/openapi/spec-builder.ts
826
+ var SECURITY_SCHEMES = {
827
+ bearer: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
828
+ basic: { type: "http", scheme: "basic" },
829
+ apiKey: { type: "apiKey", name: "X-API-Key", in: "header" }
830
+ };
831
+ var SECURITY_MAP = {
832
+ bearer: { bearerAuth: [] },
833
+ basic: { basicAuth: [] },
834
+ apiKey: { apiKeyAuth: [] },
835
+ none: {}
836
+ };
837
+ var RE_LEADING_SLASH = /^\//;
838
+ var RE_PARAM_COLON = /\/:(\w+)/g;
839
+ var RE_SLASH = /\//g;
840
+ var RE_NON_WORD = /\W/g;
841
+ function toOperationId(method, path) {
842
+ const normalized = path.replace(RE_LEADING_SLASH, "").replace(RE_PARAM_COLON, "/$1").replace(RE_SLASH, "_").replace(RE_NON_WORD, "_");
843
+ return `${method.toLowerCase()}_${normalized}`;
844
+ }
845
+ function extractPathParams(path, paramSchemas) {
846
+ const params = [];
847
+ const matches = path.matchAll(/:(\w+)/g);
848
+ for (const match of matches) {
849
+ const name = match[1];
850
+ const schema = paramSchemas?.properties?.[name] ?? { type: "string" };
851
+ params.push({
852
+ name,
853
+ in: "path",
854
+ required: true,
855
+ description: schema.description,
856
+ schema: { type: schema.type ?? "string" }
857
+ });
858
+ }
859
+ return params;
860
+ }
861
+ function extractQueryParams(querySchema) {
862
+ if (!querySchema?.properties)
863
+ return [];
864
+ const params = [];
865
+ const required = new Set(querySchema.required ?? []);
866
+ for (const [name, schema] of Object.entries(querySchema.properties)) {
867
+ params.push({
868
+ name,
869
+ in: "query",
870
+ required: required.has(name),
871
+ description: schema.description,
872
+ schema
873
+ });
874
+ }
875
+ return params;
876
+ }
877
+ function toOpenAPIPath(path) {
878
+ return path.replace(/:(\w+)/g, "{$1}");
879
+ }
880
+ function cleanSchema(value) {
881
+ if (Array.isArray(value))
882
+ return value.map(cleanSchema);
883
+ if (typeof value === "object" && value !== null) {
884
+ return Object.fromEntries(
885
+ Object.entries(value).filter(([key]) => !key.startsWith("~")).filter(([, entry]) => typeof entry !== "function" && entry !== void 0).map(([key, entry]) => [key, cleanSchema(entry)])
886
+ );
887
+ }
888
+ return value;
889
+ }
890
+ function resolveOperationId(endpoint2, options) {
891
+ if (typeof options.operationId === "function")
892
+ return options.operationId(endpoint2);
893
+ if (options.operationId === "handler")
894
+ return endpoint2.handler;
895
+ return toOperationId(endpoint2.method, endpoint2.path);
896
+ }
897
+ function buildOpenAPISpec(endpoints, componentSchemas, options) {
898
+ const paths = {};
899
+ const usedSecuritySchemes = /* @__PURE__ */ new Set();
900
+ for (const ep of endpoints) {
901
+ const openAPIPath = toOpenAPIPath(ep.path);
902
+ const method = ep.method.toLowerCase();
903
+ if (!paths[openAPIPath]) {
904
+ paths[openAPIPath] = {};
905
+ }
906
+ const operation = {
907
+ operationId: resolveOperationId(ep, options),
908
+ responses: {}
909
+ };
910
+ if (ep.meta) {
911
+ if (ep.meta.summary)
912
+ operation.summary = ep.meta.summary;
913
+ if (ep.meta.description)
914
+ operation.description = ep.meta.description;
915
+ if (ep.meta.tags)
916
+ operation.tags = ep.meta.tags;
917
+ if (ep.meta.deprecated)
918
+ operation.deprecated = true;
919
+ if (ep.meta.security) {
920
+ const secArr = [];
921
+ for (const s of ep.meta.security) {
922
+ if (s === "none") {
923
+ secArr.push({});
924
+ } else {
925
+ secArr.push(SECURITY_MAP[s]);
926
+ usedSecuritySchemes.add(s);
927
+ }
928
+ }
929
+ operation.security = secArr;
930
+ }
931
+ }
932
+ const params = [
933
+ ...extractPathParams(ep.path, cleanSchema(ep.input?.params)),
934
+ ...extractQueryParams(cleanSchema(ep.input?.query))
935
+ ];
936
+ if (params.length > 0) {
937
+ operation.parameters = params;
938
+ }
939
+ if (ep.input?.body) {
940
+ operation.requestBody = {
941
+ required: true,
942
+ content: { "application/json": { schema: cleanSchema(ep.input.body) } }
943
+ };
944
+ }
945
+ if (ep.responses) {
946
+ for (const [status, resp] of Object.entries(ep.responses)) {
947
+ const entry = {
948
+ description: resp.description
949
+ };
950
+ if (resp.schema) {
951
+ entry.content = { "application/json": { schema: cleanSchema(resp.schema) } };
952
+ }
953
+ operation.responses[status] = entry;
954
+ }
955
+ } else {
956
+ operation.responses["200"] = { description: "Success" };
957
+ }
958
+ paths[openAPIPath][method] = operation;
959
+ }
960
+ const securitySchemes = {};
961
+ const schemeNameMap = { bearer: "bearerAuth", basic: "basicAuth", apiKey: "apiKeyAuth" };
962
+ for (const scheme of usedSecuritySchemes) {
963
+ const name = schemeNameMap[scheme];
964
+ if (name && SECURITY_SCHEMES[scheme]) {
965
+ securitySchemes[name] = SECURITY_SCHEMES[scheme];
966
+ }
967
+ }
968
+ const spec = {
969
+ openapi: "3.1.0",
970
+ info: {
971
+ title: options.title,
972
+ version: options.version,
973
+ ...options.description && { description: options.description }
974
+ },
975
+ paths,
976
+ components: {
977
+ schemas: cleanSchema(componentSchemas),
978
+ securitySchemes
979
+ }
980
+ };
981
+ if (options.servers && options.servers.length > 0) {
982
+ spec.servers = options.servers;
983
+ }
984
+ if (options.tags && options.tags.length > 0) {
985
+ spec.tags = options.tags;
986
+ }
987
+ return spec;
988
+ }
989
+
990
+ // src/plugins/openapi/index.ts
991
+ function computeRelativeUrl(fromPath, toPath) {
992
+ const fromParts = fromPath.split("/").slice(0, -1);
993
+ const toParts = toPath.split("/");
994
+ let common = 0;
995
+ while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
996
+ common++;
997
+ }
998
+ const ups = fromParts.length - common;
999
+ const rest = toParts.slice(common).join("/");
1000
+ if (ups === 0)
1001
+ return `./${rest}`;
1002
+ return "../".repeat(ups) + rest;
1003
+ }
1004
+ function buildScalarHTML(specPath, title) {
1005
+ return `<!DOCTYPE html>
1006
+ <html>
1007
+ <head>
1008
+ <title>${title} \u2014 API Reference</title>
1009
+ <meta charset="utf-8" />
1010
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1011
+ </head>
1012
+ <body>
1013
+ <script id="api-reference" data-url="${specPath}"></script>
1014
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
1015
+ </body>
1016
+ </html>`;
1017
+ }
1018
+ function buildResourceBranch(resourceName, actions, sharedProps, requiredFields) {
1019
+ const actionSchema = actions.length > 0 ? { type: "string", enum: actions, description: "Action name" } : { type: "string", description: "Action name" };
1020
+ return {
1021
+ type: "object",
1022
+ properties: {
1023
+ resource: { type: "string", const: resourceName, description: "Resource name" },
1024
+ action: actionSchema,
1025
+ ...sharedProps
1026
+ },
1027
+ required: requiredFields
1028
+ };
1029
+ }
1030
+ function enrichIamSchemas(componentSchemas, allEndpoints, resourceFile) {
1031
+ const entries = Object.entries(resourceFile.resources);
1032
+ if (entries.length === 0)
1033
+ return;
1034
+ const permissionShared = {
1035
+ id: { type: "integer", description: "Permission ID" },
1036
+ effect: { type: "string", enum: ["ALLOW", "DENY"] },
1037
+ conditions: componentSchemas.Permission?.properties?.conditions ?? { type: "array" },
1038
+ description: { type: "string", description: "Permission description", nullable: true }
1039
+ };
1040
+ const permissionInputShared = {
1041
+ effect: { type: "string", enum: ["ALLOW", "DENY"] },
1042
+ conditions: componentSchemas.PermissionInput?.properties?.conditions ?? { type: "array" }
1043
+ };
1044
+ const permissionBranches = [];
1045
+ const permissionInputBranches = [];
1046
+ for (const [name, def] of entries) {
1047
+ permissionBranches.push(
1048
+ buildResourceBranch(name, def.actions, permissionShared, ["id", "resource", "action", "effect"])
1049
+ );
1050
+ permissionInputBranches.push(
1051
+ buildResourceBranch(name, def.actions, permissionInputShared, ["resource", "action"])
1052
+ );
1053
+ }
1054
+ componentSchemas.Permission = { oneOf: permissionBranches };
1055
+ componentSchemas.PermissionInput = { oneOf: permissionInputBranches };
1056
+ const allResourceNames = entries.map(([name]) => name).sort();
1057
+ const allActions = [...new Set(entries.flatMap(([, def]) => def.actions))].sort();
1058
+ for (const ep of allEndpoints) {
1059
+ if (ep.path === "/iam/check" && ep.input?.body?.properties) {
1060
+ const bodyClone = JSON.parse(JSON.stringify(ep.input.body));
1061
+ if (bodyClone.properties.resource) {
1062
+ bodyClone.properties.resource = { ...bodyClone.properties.resource, enum: allResourceNames };
1063
+ }
1064
+ if (bodyClone.properties.action && allActions.length > 0) {
1065
+ bodyClone.properties.action = { ...bodyClone.properties.action, enum: allActions };
1066
+ }
1067
+ ep.input = { ...ep.input, body: bodyClone };
1068
+ break;
1069
+ }
1070
+ }
1071
+ }
1072
+ function openapi(config = {}) {
1073
+ const specPath = config.specPath ?? "/openapi.json";
1074
+ const uiPath = config.uiPath ?? "/openapi";
1075
+ const title = config.title ?? "Fortress Auth API";
1076
+ const version = config.version ?? "1.0.0";
1077
+ let cachedSpec = null;
1078
+ const routes = {
1079
+ getSpec: {
1080
+ method: "GET",
1081
+ path: specPath,
1082
+ handler: "getSpec",
1083
+ meta: { summary: "OpenAPI specification", tags: ["OpenAPI"], security: ["none"] },
1084
+ responses: { 200: { description: "OpenAPI 3.1 JSON spec" } }
1085
+ }
1086
+ };
1087
+ if (!config.disableUI) {
1088
+ routes.getUI = {
1089
+ method: "GET",
1090
+ path: uiPath,
1091
+ handler: "getUI",
1092
+ meta: { summary: "API reference (Scalar)", tags: ["OpenAPI"], security: ["none"] },
1093
+ responses: { 200: { description: "Scalar API reference HTML" } }
1094
+ };
1095
+ }
1096
+ return {
1097
+ name: "openapi",
1098
+ routes,
1099
+ methods: (ctx) => {
1100
+ async function generateSpec() {
1101
+ if (cachedSpec)
1102
+ return cachedSpec;
1103
+ const allEndpoints = [];
1104
+ const plugins = ctx.config.plugins ?? [];
1105
+ if (config.includeCoreAuth !== false) {
1106
+ allEndpoints.push(...Object.values(authEndpoints));
1107
+ }
1108
+ if (config.includeCoreIam !== false) {
1109
+ allEndpoints.push(...Object.values(iamEndpoints));
1110
+ }
1111
+ if (ctx.config.routes) {
1112
+ allEndpoints.push(...Object.values(ctx.config.routes));
1113
+ }
1114
+ for (const plugin of plugins) {
1115
+ if (plugin.name === "openapi" || !plugin.routes)
1116
+ continue;
1117
+ allEndpoints.push(...Object.values(plugin.routes));
1118
+ }
1119
+ if (config.additionalEndpoints) {
1120
+ allEndpoints.push(...config.additionalEndpoints);
1121
+ }
1122
+ const componentSchemas = {
1123
+ ...config.includeCoreAuth !== false ? authComponentSchemas : {},
1124
+ ...config.includeCoreIam !== false ? iamComponentSchemas : {},
1125
+ ...config.additionalSchemas ?? {}
1126
+ };
1127
+ if (config.includeCoreIam !== false && ctx.iam) {
1128
+ try {
1129
+ const resourceFile = await ctx.iam.getResources();
1130
+ enrichIamSchemas(componentSchemas, allEndpoints, resourceFile);
1131
+ } catch {
1132
+ }
1133
+ }
1134
+ cachedSpec = buildOpenAPISpec(allEndpoints, componentSchemas, {
1135
+ title,
1136
+ version,
1137
+ description: config.description,
1138
+ servers: config.servers,
1139
+ tags: config.tags,
1140
+ operationId: config.operationId
1141
+ });
1142
+ return cachedSpec;
1143
+ }
1144
+ return {
1145
+ generateSpec,
1146
+ async getSpec() {
1147
+ return await generateSpec();
1148
+ },
1149
+ getUI() {
1150
+ return buildScalarHTML(computeRelativeUrl(uiPath, specPath), title);
1151
+ }
1152
+ };
1153
+ }
1154
+ };
1155
+ }
1156
+ export {
1157
+ openapi
1158
+ };