@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,881 @@
1
+ import type { EndpointDefinition, EndpointResponse, HttpMethod, SecurityRequirement } from './endpoint';
2
+ import type { FortressSchema, Infer, JSONSchema, Simplify } from './json-schema';
3
+ import type { StandardSchemaV1 } from './standard-schema';
4
+ import { fromJSONSchema } from '@bajustone/fetcher/openapi';
5
+ import { date as fDate, datetime as fDatetime, email as fEmail, time as fTime, url as fUrl, uuid as fUuid } from '@bajustone/fetcher/schema';
6
+
7
+ /** Shorthand for the Standard Schema inferred output type. */
8
+ type InferSchema<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>;
9
+
10
+ /** Distribute a union into an intersection — `A | B` → `A & B`. Used by {@link intersect}. */
11
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
12
+
13
+ /** Options accepted by the {@link str} builder (string constraints + annotations). */
14
+ export interface StringOptions {
15
+ description?: string;
16
+ /** Minimum length (`minLength`), enforced at runtime. */
17
+ min?: number;
18
+ /** Maximum length (`maxLength`), enforced at runtime. */
19
+ max?: number;
20
+ /** Regular-expression source (`pattern`), enforced at runtime. */
21
+ pattern?: string;
22
+ /** OpenAPI `format` annotation (e.g. `email`, `uuid`). Annotation-only unless paired with `pattern`. */
23
+ format?: string;
24
+ }
25
+
26
+ /** Options accepted by the {@link num}/{@link int} builders (numeric bounds + annotations). */
27
+ export interface NumberOptions {
28
+ description?: string;
29
+ /** Inclusive minimum (`minimum`), enforced at runtime. */
30
+ min?: number;
31
+ /** Inclusive maximum (`maximum`), enforced at runtime. */
32
+ max?: number;
33
+ }
34
+
35
+ // ── Standard Schema wiring ─────────────────────────────────────────
36
+
37
+ /**
38
+ * Collect the component names referenced by `$ref` anywhere in a schema tree
39
+ * (the last path segment, e.g. `PermissionInput` for
40
+ * `#/components/schemas/PermissionInput`).
41
+ */
42
+ function collectRefNames(schema: JSONSchema | undefined, acc: Set<string>): Set<string> {
43
+ if (!schema || typeof schema !== 'object')
44
+ return acc;
45
+ if (typeof schema.$ref === 'string') {
46
+ const i = schema.$ref.lastIndexOf('/');
47
+ acc.add(i >= 0 ? schema.$ref.slice(i + 1) : schema.$ref);
48
+ }
49
+ if (schema.properties) {
50
+ for (const key of Object.keys(schema.properties))
51
+ collectRefNames(schema.properties[key], acc);
52
+ }
53
+ collectRefNames(schema.items, acc);
54
+ for (const key of ['oneOf', 'anyOf', 'allOf'] as const) {
55
+ const variants = schema[key];
56
+ if (variants) {
57
+ for (const variant of variants)
58
+ collectRefNames(variant, acc);
59
+ }
60
+ }
61
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object')
62
+ collectRefNames(schema.additionalProperties, acc);
63
+ return acc;
64
+ }
65
+
66
+ /**
67
+ * Component definitions bound to a particular `$ref` schema. A WeakMap keeps
68
+ * this runtime-only context out of emitted JSON Schema and, unlike a global
69
+ * name registry, prevents two independent component maps that both contain a
70
+ * `User` schema from overwriting each other before lazy validation runs.
71
+ */
72
+ const refDefinitionContexts = new WeakMap<object, Record<string, JSONSchema>>();
73
+
74
+ function refName(refPath: string): string {
75
+ const i = refPath.lastIndexOf('/');
76
+ return i >= 0 ? refPath.slice(i + 1) : refPath;
77
+ }
78
+
79
+ // OpenAPI 3.x component-map keys are restricted to this grammar. Rejecting
80
+ // unsupported names is safer than emitting an ambiguous JSON Pointer that
81
+ // silently resolves to a different final path segment.
82
+ const COMPONENT_NAME_RE = /^[\w.-]+$/;
83
+
84
+ function assertComponentName(name: string): void {
85
+ if (!COMPONENT_NAME_RE.test(name))
86
+ throw new Error(`Invalid OpenAPI component name '${name}'`);
87
+ }
88
+
89
+ /**
90
+ * Build the definitions map fed to `fromJSONSchema`, resolving each `$ref`
91
+ * against the context bound by {@link defineComponents} / typed {@link ref}.
92
+ * Context is inherited while traversing a referenced component, which supports
93
+ * transitive and recursive refs. Bare refs without any same-name definition
94
+ * in the composed schema remain permissive (`{}`); when composed with a bound
95
+ * ref of the same OpenAPI component name, both correctly resolve to that one
96
+ * global component definition.
97
+ */
98
+ function resolveRefDefs(schema: JSONSchema): Record<string, JSONSchema> | undefined {
99
+ if (collectRefNames(schema, new Set<string>()).size === 0)
100
+ return undefined;
101
+
102
+ const defs = Object.create(null) as Record<string, JSONSchema>;
103
+ const unresolved = new Set<string>();
104
+ const hasOwn = (value: object, key: string): boolean => Object.hasOwn(value, key);
105
+
106
+ const visit = (node: JSONSchema | undefined, inherited?: Record<string, JSONSchema>): void => {
107
+ if (!node || typeof node !== 'object')
108
+ return;
109
+ const context = refDefinitionContexts.get(node) ?? inherited;
110
+ if (typeof node.$ref === 'string') {
111
+ const name = refName(node.$ref);
112
+ const component = context && hasOwn(context, name) ? context[name] : undefined;
113
+ if (component) {
114
+ if (hasOwn(defs, name) && !unresolved.has(name) && defs[name] !== component) {
115
+ throw new Error(`Conflicting component definitions for $ref '${name}'`);
116
+ }
117
+ const firstResolution = !hasOwn(defs, name) || unresolved.delete(name);
118
+ defs[name] = component;
119
+ if (firstResolution)
120
+ visit(component, context);
121
+ }
122
+ else if (!hasOwn(defs, name)) {
123
+ defs[name] = {};
124
+ unresolved.add(name);
125
+ }
126
+ }
127
+ if (node.properties) {
128
+ for (const child of Object.values(node.properties))
129
+ visit(child, context);
130
+ }
131
+ visit(node.items, context);
132
+ for (const key of ['oneOf', 'anyOf', 'allOf'] as const) {
133
+ for (const child of node[key] ?? [])
134
+ visit(child, context);
135
+ }
136
+ if (node.additionalProperties && typeof node.additionalProperties === 'object')
137
+ visit(node.additionalProperties, context);
138
+ };
139
+
140
+ visit(schema);
141
+ return defs;
142
+ }
143
+
144
+ /**
145
+ * Build the `~standard` props for a fortress schema. Runtime validation is
146
+ * delegated to `@bajustone/fetcher`'s `fromJSONSchema`, which compiles the
147
+ * JSON Schema object into a Standard Schema V1 validator (lazily, on first
148
+ * use, then memoized). fortress schemas keep `vendor: 'fortress'` and remain
149
+ * plain JSON Schema objects — only the validation engine is fetcher's.
150
+ *
151
+ * `$ref` nodes are resolved against context bound by {@link defineComponents}
152
+ * / typed {@link ref}: the referenced component's JSON
153
+ * Schema (and any components it transitively references) is threaded into the
154
+ * compiled validator so ref'd request bodies are enforced. A `$ref` whose
155
+ * component is unavailable anywhere in the composed schema stays permissive
156
+ * (compiled against `{}`), and surrounding inline fields are always validated
157
+ * strictly.
158
+ */
159
+ function createStandardProps<T>(schema: JSONSchema): StandardSchemaV1<T, T>['~standard'] {
160
+ type ValidateFn = (value: unknown) => StandardSchemaV1.Result<T> | Promise<StandardSchemaV1.Result<T>>;
161
+ let validateFn: ValidateFn | undefined;
162
+ return {
163
+ version: 1,
164
+ vendor: 'fortress',
165
+ validate(value: unknown): StandardSchemaV1.Result<T> | Promise<StandardSchemaV1.Result<T>> {
166
+ if (!validateFn) {
167
+ const defs = resolveRefDefs(schema);
168
+ validateFn = fromJSONSchema<T>(schema, defs)['~standard'].validate as ValidateFn;
169
+ }
170
+ return validateFn(value);
171
+ },
172
+ } as StandardSchemaV1<T, T>['~standard'];
173
+ }
174
+
175
+ /**
176
+ * Standard Schema validation for builder `oneOf`: JSON Schema requires that
177
+ * exactly one branch succeeds, while some validator bridges treat it as an
178
+ * ordinary union. Validate each branch directly so this invariant is retained
179
+ * independently of the bridge, including for branches backed by `$ref`.
180
+ */
181
+ function createExactOneOfProps<T>(schemas: readonly FortressSchema<any>[]): StandardSchemaV1<T, T>['~standard'] {
182
+ type Result = StandardSchemaV1.Result<T>;
183
+ type ValidateFn = (value: unknown) => Result | Promise<Result>;
184
+ const validate: ValidateFn = (value) => {
185
+ const results = schemas.map(schema => schema['~standard'].validate(value));
186
+ if (results.some(result => result instanceof Promise)) {
187
+ return Promise.all(results).then(resolved => exactOneResult(resolved));
188
+ }
189
+ return exactOneResult(results as Result[]);
190
+ };
191
+ return {
192
+ version: 1,
193
+ vendor: 'fortress',
194
+ validate,
195
+ };
196
+ }
197
+
198
+ function exactOneResult<T>(results: readonly StandardSchemaV1.Result<T>[]): StandardSchemaV1.Result<T> {
199
+ const successes = results.filter((result): result is StandardSchemaV1.SuccessResult<T> => !result.issues);
200
+ if (successes.length === 1)
201
+ return { value: successes[0].value };
202
+ return {
203
+ issues: [{
204
+ message: successes.length === 0
205
+ ? 'Value must match exactly one oneOf variant'
206
+ : 'Value must match exactly one oneOf variant; multiple variants matched',
207
+ }],
208
+ };
209
+ }
210
+
211
+ function toFortressSchema<T>(
212
+ schema: JSONSchema,
213
+ refDefinitions?: Record<string, JSONSchema>,
214
+ standardProps?: StandardSchemaV1<T, T>['~standard'],
215
+ ): FortressSchema<T> {
216
+ if (refDefinitions)
217
+ refDefinitionContexts.set(schema, refDefinitions);
218
+ return Object.assign(schema, {
219
+ '~standard': standardProps ?? createStandardProps<T>(schema),
220
+ }) as FortressSchema<T>;
221
+ }
222
+
223
+ // ── Schema Builders ─────────────────────────────────────────────────
224
+
225
+ /**
226
+ * Build a string {@link FortressSchema}.
227
+ *
228
+ * Pass a string for just a description, or an options object to set
229
+ * `minLength`/`maxLength`/`pattern`/`format` — the length and pattern
230
+ * constraints are enforced at runtime by the validator.
231
+ */
232
+ export function str(opts?: string | StringOptions): FortressSchema<string> {
233
+ const o = typeof opts === 'string' ? { description: opts } : opts ?? {};
234
+ const s: JSONSchema = { type: 'string' };
235
+ if (o.description)
236
+ s.description = o.description;
237
+ if (o.min !== undefined)
238
+ s.minLength = o.min;
239
+ if (o.max !== undefined)
240
+ s.maxLength = o.max;
241
+ if (o.pattern !== undefined)
242
+ s.pattern = o.pattern;
243
+ if (o.format !== undefined)
244
+ s.format = o.format;
245
+ return toFortressSchema<string>(s);
246
+ }
247
+
248
+ /**
249
+ * Build a number {@link FortressSchema} (any numeric, integer or float).
250
+ *
251
+ * Pass a string for just a description, or an options object to set
252
+ * `minimum`/`maximum` — both are enforced at runtime by the validator.
253
+ */
254
+ export function num(opts?: string | NumberOptions): FortressSchema<number> {
255
+ const o = typeof opts === 'string' ? { description: opts } : opts ?? {};
256
+ const s: JSONSchema = { type: 'number' };
257
+ if (o.description)
258
+ s.description = o.description;
259
+ if (o.min !== undefined)
260
+ s.minimum = o.min;
261
+ if (o.max !== undefined)
262
+ s.maximum = o.max;
263
+ return toFortressSchema<number>(s);
264
+ }
265
+
266
+ /**
267
+ * Build an integer {@link FortressSchema}.
268
+ *
269
+ * Pass a string for just a description, or an options object to set
270
+ * `minimum`/`maximum` — both are enforced at runtime by the validator.
271
+ */
272
+ export function int(opts?: string | NumberOptions): FortressSchema<number> {
273
+ const o = typeof opts === 'string' ? { description: opts } : opts ?? {};
274
+ const s: JSONSchema = { type: 'integer' };
275
+ if (o.description)
276
+ s.description = o.description;
277
+ if (o.min !== undefined)
278
+ s.minimum = o.min;
279
+ if (o.max !== undefined)
280
+ s.maximum = o.max;
281
+ return toFortressSchema<number>(s);
282
+ }
283
+
284
+ /**
285
+ * Build a subject-id {@link FortressSchema}.
286
+ *
287
+ * IDs are opaque strings at the fortress API surface (RFC 7519 §4.1.2 for
288
+ * JWT `sub`). Numeric-keyed adapters stringify on read and parse on write
289
+ * at the adapter boundary; consumers never see the underlying representation.
290
+ */
291
+ export function id(description?: string): FortressSchema<string> {
292
+ const s: JSONSchema = { type: 'string', minLength: 1 };
293
+ if (description)
294
+ s.description = description;
295
+ return toFortressSchema<string>(s);
296
+ }
297
+
298
+ /** Build a boolean {@link FortressSchema}. */
299
+ export function bool(description?: string): FortressSchema<boolean> {
300
+ const s: JSONSchema = { type: 'boolean' };
301
+ if (description)
302
+ s.description = description;
303
+ return toFortressSchema<boolean>(s);
304
+ }
305
+
306
+ /** Build an array {@link FortressSchema} whose items match the supplied schema. */
307
+ export function arr<T>(items: FortressSchema<T>, description?: string): FortressSchema<T[]> {
308
+ const s: JSONSchema = { type: 'array', items };
309
+ if (description)
310
+ s.description = description;
311
+ return toFortressSchema<T[]>(s);
312
+ }
313
+
314
+ /**
315
+ * Build an object {@link FortressSchema}. Pass property names after `properties`
316
+ * to mark them as required — they become non-optional in the inferred type.
317
+ */
318
+ export function obj<
319
+ P extends Record<string, FortressSchema<any>>,
320
+ K extends (keyof P & string)[] = [],
321
+ >(
322
+ properties: P,
323
+ ...required: K
324
+ ): FortressSchema<
325
+ Simplify<
326
+ { [Key in K[number]]: Infer<P[Key]> }
327
+ & { [Key in Exclude<keyof P & string, K[number]>]?: Infer<P[Key]> }
328
+ >
329
+ > {
330
+ const s: JSONSchema = { type: 'object', properties: properties as Record<string, JSONSchema> };
331
+ if (required.length > 0)
332
+ s.required = required;
333
+ return toFortressSchema(s);
334
+ }
335
+
336
+ /** Wrap a schema so it also accepts `null`. */
337
+ export function nullable<T>(schema: FortressSchema<T>): FortressSchema<T | null> {
338
+ // Fetcher's JSON-Schema bridge resolves `$ref` before OpenAPI's legacy
339
+ // `nullable` sibling. Use a real union for refs so null is enforced
340
+ // correctly; keep the established compact shape for inline schemas.
341
+ if (typeof schema.$ref === 'string')
342
+ return toFortressSchema<T | null>({ oneOf: [schema, { type: 'null' }] });
343
+ const s: JSONSchema = { ...schema, nullable: true };
344
+ return toFortressSchema<T | null>(s, refDefinitionContexts.get(schema));
345
+ }
346
+
347
+ /** Build a discriminated-union schema (exactly one variant must match). */
348
+ export function oneOf<S extends FortressSchema<any>[]>(
349
+ ...schemas: S
350
+ ): FortressSchema<Infer<S[number]>> {
351
+ const schema: JSONSchema = { oneOf: schemas as JSONSchema[] };
352
+ return toFortressSchema<Infer<S[number]>>(
353
+ schema,
354
+ undefined,
355
+ createExactOneOfProps<Infer<S[number]>>(schemas),
356
+ );
357
+ }
358
+
359
+ /** Build a union schema (any one variant may match). */
360
+ export function anyOf<S extends FortressSchema<any>[]>(
361
+ ...schemas: S
362
+ ): FortressSchema<Infer<S[number]>> {
363
+ return toFortressSchema<Infer<S[number]>>({ anyOf: schemas as JSONSchema[] });
364
+ }
365
+
366
+ /**
367
+ * Build a `$ref` schema pointing at an OpenAPI component schema by name.
368
+ *
369
+ * Two forms:
370
+ * - `ref(name)` — untyped `$ref`, returns `FortressSchema<unknown>`. Use when
371
+ * the referenced component hasn't been declared yet (e.g. self-references
372
+ * inside a components literal).
373
+ * - `ref(name, schema)` — typed and runtime-bound `$ref`. The supplied schema
374
+ * provides both the inferred TypeScript type and the component definition
375
+ * used for runtime validation. Use when you already have the component
376
+ * schema and want downstream types and validation to flow through.
377
+ *
378
+ * For the common case (typed refs against a known components map), prefer
379
+ * {@link defineComponents} — it returns a bound `ref` that only needs a name.
380
+ */
381
+ export function ref<T>(name: string, schema: FortressSchema<T>): FortressSchema<T>;
382
+ export function ref(name: string): FortressSchema<unknown>;
383
+ export function ref<T>(name: string, schema?: FortressSchema<T>): FortressSchema<T> {
384
+ assertComponentName(name);
385
+ // Typed refs bind the real component schema. Bare `ref(name)` has no local
386
+ // definition context; it stays permissive unless the composed schema binds
387
+ // the same global OpenAPI component name elsewhere.
388
+ const definitions = schema ? { [name]: schema } : undefined;
389
+ return toFortressSchema<T>({ $ref: `#/components/schemas/${name}` }, definitions);
390
+ }
391
+
392
+ /**
393
+ * Declare a typed registry of OpenAPI component schemas and return a bound
394
+ * `ref` function that preserves each component's inferred TypeScript type.
395
+ *
396
+ * ```ts
397
+ * const User = obj({ id: int(), email: str() }, 'id', 'email');
398
+ * const { components, ref } = defineComponents({ User });
399
+ *
400
+ * const ep = endpoint('GET', '/me')
401
+ * .response(200, 'Current user', ref('User'))
402
+ * // ^ FortressSchema<{ id: string; email: string }>
403
+ * .handler('me')
404
+ * .build();
405
+ * ```
406
+ *
407
+ * The returned `components` is the same object you passed in (useful for
408
+ * OpenAPI spec emission). The returned `ref` is a generic function that
409
+ * looks up each key in `T` and returns a `$ref` schema carrying the
410
+ * inferred type of the referenced component.
411
+ */
412
+ export function defineComponents<T extends Record<string, FortressSchema<any>>>(
413
+ components: T,
414
+ ): {
415
+ components: T;
416
+ ref: <K extends keyof T & string>(name: K) => FortressSchema<Infer<T[K]>>;
417
+ } {
418
+ for (const name of Object.keys(components))
419
+ assertComponentName(name);
420
+ // Snapshot the name→schema bindings now. Validators compile lazily, so
421
+ // retaining the caller's mutable registry would otherwise make replacement
422
+ // before first validation behave differently from replacement afterward.
423
+ const definitions = Object.assign(
424
+ Object.create(null) as Record<string, JSONSchema>,
425
+ components,
426
+ );
427
+ return {
428
+ components,
429
+ ref: <K extends keyof T & string>(name: K): FortressSchema<Infer<T[K]>> => {
430
+ assertComponentName(name);
431
+ return toFortressSchema<Infer<T[K]>>(
432
+ { $ref: `#/components/schemas/${String(name)}` },
433
+ definitions,
434
+ );
435
+ },
436
+ };
437
+ }
438
+
439
+ /** Build an enum {@link FortressSchema} from a fixed set of string or number literal values. */
440
+ export function enums<T extends string | number>(...values: T[]): FortressSchema<T> {
441
+ return toFortressSchema<T>({ enum: values });
442
+ }
443
+
444
+ /** Build a string {@link FortressSchema} with an OpenAPI `format` annotation (e.g. `email`, `uri`, `uuid`). */
445
+ export function strFormat(format: string, description?: string): FortressSchema<string> {
446
+ const s: JSONSchema = { type: 'string', format };
447
+ if (description)
448
+ s.description = description;
449
+ return toFortressSchema<string>(s);
450
+ }
451
+
452
+ /** Build a {@link FortressSchema} that only accepts the literal `null`. */
453
+ export function nullType(): FortressSchema<null> {
454
+ return toFortressSchema<null>({ type: 'null' });
455
+ }
456
+
457
+ /** An object {@link FortressSchema} with unknown additional properties (e.g. plugin data, metadata). */
458
+ export function record(description?: string): FortressSchema<Record<string, unknown>> {
459
+ const s: JSONSchema = { type: 'object', additionalProperties: true };
460
+ if (description)
461
+ s.description = description;
462
+ return toFortressSchema<Record<string, unknown>>(s);
463
+ }
464
+
465
+ /** An object {@link FortressSchema} where every property value matches the supplied schema. */
466
+ export function recordOf<T>(valueSchema: FortressSchema<T>, description?: string): FortressSchema<Record<string, T>> {
467
+ const s: JSONSchema = { type: 'object', additionalProperties: valueSchema as JSONSchema };
468
+ if (description)
469
+ s.description = description;
470
+ return toFortressSchema<Record<string, T>>(s);
471
+ }
472
+
473
+ /**
474
+ * Build a literal {@link FortressSchema} accepting exactly one constant value
475
+ * (`const`). The value is enforced at runtime and the inferred type narrows to
476
+ * the literal (e.g. `literal('admin')` infers `'admin'`).
477
+ */
478
+ export function literal<const V extends string | number | boolean>(value: V, description?: string): FortressSchema<V> {
479
+ const s: JSONSchema = { const: value };
480
+ if (description)
481
+ s.description = description;
482
+ return toFortressSchema<V>(s);
483
+ }
484
+
485
+ /**
486
+ * Build an intersection {@link FortressSchema} (`allOf`) — a value must satisfy
487
+ * every supplied schema. The inferred type is the intersection of each schema's
488
+ * inferred type.
489
+ */
490
+ export function intersect<S extends FortressSchema<any>[]>(
491
+ ...schemas: S
492
+ ): FortressSchema<UnionToIntersection<Infer<S[number]>>> {
493
+ return toFortressSchema<UnionToIntersection<Infer<S[number]>>>({ allOf: schemas as JSONSchema[] });
494
+ }
495
+
496
+ /**
497
+ * Close an object {@link FortressSchema} — sets `additionalProperties: false`,
498
+ * so the validator rejects any key not declared in `properties`. Use to harden
499
+ * request bodies against over-posting (mass assignment).
500
+ */
501
+ export function strict<T>(schema: FortressSchema<T>): FortressSchema<T> {
502
+ return toFortressSchema<T>(
503
+ { ...schema, additionalProperties: false },
504
+ refDefinitionContexts.get(schema),
505
+ );
506
+ }
507
+
508
+ /**
509
+ * Build a discriminated-union {@link FortressSchema}. Emits `oneOf` plus an
510
+ * OpenAPI `discriminator`, which the validator uses to dispatch on
511
+ * `propertyName` (faster, with precise per-variant errors) instead of trying
512
+ * every branch.
513
+ */
514
+ export function discriminatedUnion<S extends FortressSchema<any>[]>(
515
+ propertyName: string,
516
+ ...variants: S
517
+ ): FortressSchema<Infer<S[number]>> {
518
+ return toFortressSchema<Infer<S[number]>>({
519
+ oneOf: variants as JSONSchema[],
520
+ discriminator: { propertyName },
521
+ });
522
+ }
523
+
524
+ // ── Enforced string formats ─────────────────────────────────────────
525
+ // Each lifts the `format` + ReDoS-safe `pattern` from the corresponding
526
+ // `@bajustone/fetcher/schema` format builder (the single source of truth), so
527
+ // the value is BOTH documented (`format`) and enforced at runtime (`pattern`),
528
+ // while the schema stays a plain fortress JSON Schema object.
529
+
530
+ /** Build a string format builder by lifting `{ format, pattern }` from a fetcher format factory. */
531
+ function makeFormatBuilder(factory: () => unknown): (description?: string) => FortressSchema<string> {
532
+ const f = factory() as { format?: string; pattern?: string };
533
+ return (description?: string): FortressSchema<string> => {
534
+ const s: JSONSchema = { type: 'string' };
535
+ if (f.format)
536
+ s.format = f.format;
537
+ if (f.pattern)
538
+ s.pattern = f.pattern;
539
+ if (description)
540
+ s.description = description;
541
+ return toFortressSchema<string>(s);
542
+ };
543
+ }
544
+
545
+ /** Email string schema (`format: 'email'`) — enforces the WHATWG HTML5 email grammar at runtime. */
546
+ export const email: (description?: string) => FortressSchema<string> = makeFormatBuilder(fEmail);
547
+
548
+ /** UUID string schema (`format: 'uuid'`) — enforces RFC 9562 versions 1–8 plus nil/max at runtime. */
549
+ export const uuid: (description?: string) => FortressSchema<string> = makeFormatBuilder(fUuid);
550
+
551
+ /** URL string schema (`format: 'uri'`) — enforces an explicit `scheme://` authority at runtime. */
552
+ export const url: (description?: string) => FortressSchema<string> = makeFormatBuilder(fUrl);
553
+
554
+ /** RFC 3339 date-time string schema (`format: 'date-time'`) — field ranges enforced at runtime. */
555
+ export const datetime: (description?: string) => FortressSchema<string> = makeFormatBuilder(fDatetime);
556
+
557
+ /** RFC 3339 full-date string schema (`format: 'date'`) — field ranges enforced at runtime. */
558
+ export const date: (description?: string) => FortressSchema<string> = makeFormatBuilder(fDate);
559
+
560
+ /** RFC 3339 time string schema (`format: 'time'`) — field ranges enforced at runtime. */
561
+ export const time: (description?: string) => FortressSchema<string> = makeFormatBuilder(fTime);
562
+
563
+ // ── Schema detection helpers ────────────────────────────────────────
564
+
565
+ /** Type guard — `true` if the value implements Standard Schema V1. */
566
+ export function isStandardSchema(value: unknown): value is StandardSchemaV1 {
567
+ return (
568
+ typeof value === 'object'
569
+ && value !== null
570
+ && '~standard' in value
571
+ && typeof (value as any)['~standard']?.validate === 'function'
572
+ );
573
+ }
574
+
575
+ /** Type guard — `true` if the value is a {@link FortressSchema} (has both JSON Schema fields and Standard Schema wiring). */
576
+ export function isFortressSchema(value: unknown): value is FortressSchema {
577
+ return isStandardSchema(value)
578
+ && (value as any)['~standard'].vendor === 'fortress'
579
+ && ('type' in (value as any) || '$ref' in (value as any) || 'oneOf' in (value as any) || 'anyOf' in (value as any));
580
+ }
581
+
582
+ /** Valid JSON Schema type values per the spec. */
583
+ const JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);
584
+
585
+ /**
586
+ * `true` if the value carries structural JSON Schema props directly on the
587
+ * object — a valid `type` value, or `$ref`/`oneOf`/`anyOf`/`allOf`. Libraries
588
+ * that implement Standard Schema over a JSON Schema object (fortress, fetcher)
589
+ * match this; wrapper schemas like Zod do not (their `type` holds internal
590
+ * kind strings like `'ZodObject'`, not JSON Schema types).
591
+ */
592
+ function hasJsonSchemaShape(value: unknown): boolean {
593
+ if (typeof value !== 'object' || value === null)
594
+ return false;
595
+ const v = value as any;
596
+ if ('$ref' in v || 'oneOf' in v || 'anyOf' in v || 'allOf' in v)
597
+ return true;
598
+ if (typeof v.type === 'string' && JSON_SCHEMA_TYPES.has(v.type))
599
+ return true;
600
+ if (Array.isArray(v.type) && v.type.every((t: unknown) => typeof t === 'string' && JSON_SCHEMA_TYPES.has(t)))
601
+ return true;
602
+ return false;
603
+ }
604
+
605
+ /**
606
+ * Extract a {@link JSONSchema} from any schema input.
607
+ *
608
+ * - {@link FortressSchema}: returned as-is (it already _is_ JSON Schema).
609
+ * - External Standard Schema with a `~standard.jsonSchema.input()` adapter
610
+ * (Zod, Valibot, ArkType, etc.): extracted via the adapter.
611
+ * - External Standard Schema that already IS a JSON Schema object (fetcher,
612
+ * etc.): returned as-is.
613
+ * - Anything else: empty object fallback.
614
+ */
615
+ export function extractJsonSchema(schema: FortressSchema<any> | StandardSchemaV1<any>): JSONSchema {
616
+ // Fortress schemas ARE JSON Schema — return directly
617
+ if (isFortressSchema(schema)) {
618
+ return schema;
619
+ }
620
+ if (isStandardSchema(schema)) {
621
+ const std = (schema as any)['~standard'];
622
+ // Wrapper Standard Schemas that expose a JSON Schema adapter (Zod, Valibot, ...)
623
+ if (std?.jsonSchema?.input) {
624
+ return std.jsonSchema.input({ target: 'draft-2020-12' }) as JSONSchema;
625
+ }
626
+ // Standard Schemas that already ARE JSON Schema objects (fetcher, ...)
627
+ if (hasJsonSchemaShape(schema)) {
628
+ return schema as JSONSchema;
629
+ }
630
+ }
631
+ // Fallback
632
+ return {};
633
+ }
634
+
635
+ // ── Schema input type for endpoint builder ──────────────────────────
636
+
637
+ /** Schema input accepted by `EndpointBuilder.body/query/params` — a fortress schema or any Standard Schema V1 implementation. */
638
+ export type SchemaInput = FortressSchema<any> | StandardSchemaV1<any>;
639
+
640
+ // ── Canonical error envelope ───────────────────────────────────────
641
+
642
+ /**
643
+ * Canonical fortress error response body — the exact wire shape emitted by
644
+ * {@link import('./errors').FortressError.toJSON}. Reference this from
645
+ * endpoint `.response(4xx|5xx, ...)` declarations so host APIs document the
646
+ * same error contract Fortress's own routes produce, and so clients can use
647
+ * a single error parser everywhere.
648
+ *
649
+ * Shape:
650
+ * ```ts
651
+ * {
652
+ * code: string, // FortressErrorCode
653
+ * message: string, // human-readable
654
+ * statusCode: number, // HTTP status
655
+ * details?: unknown, // optional structured payload (e.g. validation issues)
656
+ * }
657
+ * ```
658
+ *
659
+ * See {@link EndpointBuilder.errorResponse} for a shorthand that wires this
660
+ * schema into a status declaration in one call.
661
+ */
662
+ export interface ErrorEnvelopeBody {
663
+ code: string;
664
+ message: string;
665
+ statusCode: number;
666
+ details?: unknown;
667
+ }
668
+
669
+ export const ErrorEnvelope: FortressSchema<ErrorEnvelopeBody> = obj({
670
+ code: str('Machine-readable error code'),
671
+ message: str('Human-readable error message'),
672
+ statusCode: int('HTTP status code'),
673
+ details: toFortressSchema<unknown>({ description: 'Optional structured error details' }),
674
+ }, 'code', 'message', 'statusCode');
675
+
676
+ // ── Endpoint Builder ────────────────────────────────────────────────
677
+
678
+ /**
679
+ * Fluent builder for {@link EndpointDefinition} objects. Construct via the
680
+ * {@link endpoint} factory and chain `summary`, `body`, `response`, etc.
681
+ * before calling `build()`.
682
+ *
683
+ * The four type parameters accumulate as schemas are declared: each call to
684
+ * `.body()`, `.query()`, `.params()`, and `.response()` returns a new
685
+ * builder type with the inferred schema baked in. By the time `.build()` is
686
+ * called, the returned {@link EndpointDefinition} carries complete phantom
687
+ * type information about its request/response shape, which the
688
+ * `InferEndpoint*` helpers and the `fortress.call.*` proxy read at the call
689
+ * site.
690
+ */
691
+ export class EndpointBuilder<
692
+ // eslint-disable-next-line ts/no-empty-object-type
693
+ TBody = {},
694
+ // eslint-disable-next-line ts/no-empty-object-type
695
+ TQuery = {},
696
+ // eslint-disable-next-line ts/no-empty-object-type
697
+ TParams = {},
698
+ // eslint-disable-next-line ts/no-empty-object-type
699
+ TResponses extends Record<number, unknown> = {},
700
+ > {
701
+ private _method: HttpMethod;
702
+ private _path: string;
703
+ private _handler = '';
704
+ private _summary = '';
705
+ private _description?: string;
706
+ private _tags: string[] = [];
707
+ private _security: SecurityRequirement[] = [];
708
+ private _deprecated = false;
709
+ private _permission?: { resource: string; action: string };
710
+ private _body?: SchemaInput;
711
+ private _query?: SchemaInput;
712
+ private _params?: SchemaInput;
713
+ private _responses: Record<number, EndpointResponse> = {};
714
+
715
+ constructor(method: HttpMethod, path: string) {
716
+ this._method = method;
717
+ this._path = path;
718
+ }
719
+
720
+ summary(s: string): this {
721
+ this._summary = s;
722
+ return this;
723
+ }
724
+
725
+ description(s: string): this {
726
+ this._description = s;
727
+ return this;
728
+ }
729
+
730
+ tags(...t: string[]): this {
731
+ this._tags.push(...t);
732
+ return this;
733
+ }
734
+
735
+ security(...s: SecurityRequirement[]): this {
736
+ this._security.push(...s);
737
+ return this;
738
+ }
739
+
740
+ deprecated(): this {
741
+ this._deprecated = true;
742
+ return this;
743
+ }
744
+
745
+ permission(resource: string, action: string): this {
746
+ this._permission = { resource, action };
747
+ return this;
748
+ }
749
+
750
+ body<T extends StandardSchemaV1>(
751
+ schema: T,
752
+ ): EndpointBuilder<InferSchema<T>, TQuery, TParams, TResponses> {
753
+ this._body = schema;
754
+ return this as unknown as EndpointBuilder<InferSchema<T>, TQuery, TParams, TResponses>;
755
+ }
756
+
757
+ query<T extends StandardSchemaV1>(
758
+ schema: T,
759
+ ): EndpointBuilder<TBody, InferSchema<T>, TParams, TResponses> {
760
+ this._query = schema;
761
+ return this as unknown as EndpointBuilder<TBody, InferSchema<T>, TParams, TResponses>;
762
+ }
763
+
764
+ params<T extends StandardSchemaV1>(
765
+ schema: T,
766
+ ): EndpointBuilder<TBody, TQuery, InferSchema<T>, TResponses> {
767
+ this._params = schema;
768
+ return this as unknown as EndpointBuilder<TBody, TQuery, InferSchema<T>, TResponses>;
769
+ }
770
+
771
+ /**
772
+ * Declare a response for a given status code. Three call shapes:
773
+ *
774
+ * - `.response(200, 'Ok', schema)` — typed. The inferred output type of
775
+ * `schema` becomes the response body at that status.
776
+ * - `.response(401, 'Unauthorized', schema)` — also typed. Error bodies
777
+ * are inferred the same way as success bodies; the `fortress.call.*`
778
+ * proxy treats non-2xx as throws and only exposes 2xx responses.
779
+ * - `.response(204, 'No content')` — untyped. Use when there's no body.
780
+ */
781
+ response<S extends number, T extends StandardSchemaV1>(
782
+ status: S,
783
+ description: string,
784
+ schema: T,
785
+ ): EndpointBuilder<TBody, TQuery, TParams, TResponses & { [K in S]: InferSchema<T> }>;
786
+ response<S extends number>(
787
+ status: S,
788
+ description: string,
789
+ ): EndpointBuilder<TBody, TQuery, TParams, TResponses & { [K in S]: unknown }>;
790
+ response<S extends number>(
791
+ status: S,
792
+ description: string,
793
+ schema?: StandardSchemaV1 | JSONSchema,
794
+ ): EndpointBuilder<TBody, TQuery, TParams, any> {
795
+ const stored: EndpointResponse = { description };
796
+ if (schema !== undefined) {
797
+ stored.schema = isStandardSchema(schema) ? extractJsonSchema(schema as FortressSchema<any>) : schema as JSONSchema;
798
+ }
799
+ this._responses[status] = stored;
800
+ return this as unknown as EndpointBuilder<TBody, TQuery, TParams, any>;
801
+ }
802
+
803
+ /**
804
+ * Declare an error response that returns the canonical {@link ErrorEnvelope}
805
+ * body. Shorthand for `.response(status, description, ErrorEnvelope)`.
806
+ *
807
+ * Aligns host endpoint error responses with what Fortress's own routes
808
+ * emit, so a single client-side error parser works everywhere.
809
+ *
810
+ * ```ts
811
+ * endpoint('GET', '/schools/:id')
812
+ * .summary('Get a school')
813
+ * .params(obj({ id: str() }, 'id'))
814
+ * .response(200, 'OK', SchoolEnvelope)
815
+ * .errorResponse(404, 'School not found')
816
+ * .errorResponse(403, 'Forbidden')
817
+ * .build();
818
+ * ```
819
+ */
820
+ errorResponse<S extends number>(
821
+ status: S,
822
+ description: string,
823
+ ): EndpointBuilder<TBody, TQuery, TParams, TResponses & { [K in S]: InferSchema<typeof ErrorEnvelope> }> {
824
+ return this.response(status, description, ErrorEnvelope);
825
+ }
826
+
827
+ handler(name: string): this {
828
+ this._handler = name;
829
+ return this;
830
+ }
831
+
832
+ build(): EndpointDefinition<TBody, TQuery, TParams, TResponses> {
833
+ const def: EndpointDefinition<TBody, TQuery, TParams, TResponses> = {
834
+ method: this._method,
835
+ path: this._path,
836
+ handler: this._handler,
837
+ };
838
+
839
+ if (this._summary || this._tags.length > 0 || this._security.length > 0 || this._description || this._deprecated || this._permission) {
840
+ def.meta = {
841
+ summary: this._summary,
842
+ ...(this._description && { description: this._description }),
843
+ ...(this._tags.length > 0 && { tags: this._tags }),
844
+ ...(this._security.length > 0 && { security: this._security }),
845
+ ...(this._deprecated && { deprecated: true }),
846
+ ...(this._permission && { permission: this._permission }),
847
+ };
848
+ }
849
+
850
+ if (this._body || this._query || this._params) {
851
+ def.input = {};
852
+ if (this._body) {
853
+ def.input.body = extractJsonSchema(this._body);
854
+ if (isStandardSchema(this._body))
855
+ def.input.bodySchema = this._body;
856
+ }
857
+ if (this._query) {
858
+ def.input.query = extractJsonSchema(this._query);
859
+ if (isStandardSchema(this._query))
860
+ def.input.querySchema = this._query;
861
+ }
862
+ if (this._params) {
863
+ def.input.params = extractJsonSchema(this._params);
864
+ if (isStandardSchema(this._params))
865
+ def.input.paramsSchema = this._params;
866
+ }
867
+ }
868
+
869
+ if (Object.keys(this._responses).length > 0) {
870
+ def.responses = this._responses;
871
+ }
872
+
873
+ return def;
874
+ }
875
+ }
876
+
877
+ /** Start building a new {@link EndpointDefinition} for the given HTTP method and path. */
878
+ // eslint-disable-next-line ts/no-empty-object-type
879
+ export function endpoint(method: HttpMethod, path: string): EndpointBuilder<{}, {}, {}, {}> {
880
+ return new EndpointBuilder(method, path);
881
+ }