@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,149 @@
1
+ import type { JSONSchema, Simplify } from './json-schema';
2
+ import type { StandardSchemaV1 } from './standard-schema';
3
+
4
+ /** HTTP method an {@link EndpointDefinition} can declare. */
5
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
6
+
7
+ /** Authentication scheme required to call an endpoint. */
8
+ export type SecurityRequirement = 'bearer' | 'basic' | 'apiKey' | 'none';
9
+
10
+ /** A required IAM permission, expressed as a `(resource, action)` pair. */
11
+ export interface EndpointPermission {
12
+ resource: string;
13
+ action: string;
14
+ }
15
+
16
+ /** OpenAPI / IAM metadata attached to an {@link EndpointDefinition}. */
17
+ export interface EndpointMeta {
18
+ summary: string;
19
+ description?: string;
20
+ tags?: string[];
21
+ security?: SecurityRequirement[];
22
+ deprecated?: boolean;
23
+ /** IAM permission required to access this endpoint. Enforced by RBAC middleware. */
24
+ permission?: EndpointPermission;
25
+ /**
26
+ * What kind of bearer token this route accepts.
27
+ *
28
+ * - `'jwt'` (default) — the route expects a Fortress session JWT in
29
+ * `Authorization: Bearer <jwt>` (or the cookie). The dispatcher runs
30
+ * the plugin principal chain, JWT verification, and RBAC enforcement
31
+ * automatically before invoking the handler.
32
+ * - `'oauth'` — the route's bearer is an OAuth 2.0 access token (or no
33
+ * token at all, e.g. `/oauth/authorize`). The dispatcher skips its
34
+ * auth pipeline entirely and the handler self-parses the bearer
35
+ * (typical for `/oauth/userinfo`, `/oauth/token`, etc.). Body parsing
36
+ * and validation are also skipped, since OAuth bodies are
37
+ * `application/x-www-form-urlencoded` per RFC 6749.
38
+ *
39
+ * Routes that don't set this field default to `'jwt'`. Without this
40
+ * marker, the OAuth plugin's consent-flow endpoints
41
+ * (`/oauth/flows/:flowId{,/approve,/deny}`) — which are SPA-driven and
42
+ * require a Fortress JWT — used to be silently unauthenticated due to
43
+ * a path-based `startsWith('/oauth/')` short-circuit in the dispatcher,
44
+ * forcing every host app to ship a workaround shim. Setting
45
+ * `bearerKind: 'oauth'` only on the actual protocol routes lets the
46
+ * dispatcher honour `security: ['bearer']` everywhere else.
47
+ */
48
+ bearerKind?: 'jwt' | 'oauth';
49
+ }
50
+
51
+ /** Request input declarations for an endpoint — JSON Schemas for OpenAPI plus Standard Schemas for runtime validation. */
52
+ export interface EndpointInput {
53
+ /** JSON Schema for OpenAPI spec generation. */
54
+ body?: JSONSchema;
55
+ query?: JSONSchema;
56
+ params?: JSONSchema;
57
+ /** Standard Schema references for runtime validation (set by endpoint builder). */
58
+ bodySchema?: StandardSchemaV1;
59
+ querySchema?: StandardSchemaV1;
60
+ paramsSchema?: StandardSchemaV1;
61
+ }
62
+
63
+ /** A single OpenAPI response definition (description plus optional JSON Schema). */
64
+ export interface EndpointResponse {
65
+ description: string;
66
+ schema?: JSONSchema;
67
+ }
68
+
69
+ /**
70
+ * Declarative description of one HTTP endpoint.
71
+ *
72
+ * The four generic parameters are **phantom types** — they're populated by
73
+ * the fluent {@link EndpointBuilder} as schemas are declared via `.body()`,
74
+ * `.query()`, `.params()`, and `.response()`, and then extracted at the call
75
+ * site by the `InferEndpoint*` helpers (and by the typed `fortress.call.*`
76
+ * proxy). None of them exist at runtime.
77
+ *
78
+ * The defaults are `{}` (empty object), not `unknown`, so that the
79
+ * intersection-based `InferEndpointCallInput` collapses cleanly for
80
+ * endpoints that only declare one of the three input slots.
81
+ */
82
+ export interface EndpointDefinition<
83
+ // eslint-disable-next-line ts/no-empty-object-type -- default must be {} so the input intersection collapses
84
+ TBody = {},
85
+ // eslint-disable-next-line ts/no-empty-object-type
86
+ TQuery = {},
87
+ // eslint-disable-next-line ts/no-empty-object-type
88
+ TParams = {},
89
+ // eslint-disable-next-line ts/no-empty-object-type
90
+ TResponses extends Record<number, unknown> = {},
91
+ > {
92
+ method: HttpMethod;
93
+ path: string;
94
+ handler: string;
95
+ meta?: EndpointMeta;
96
+ input?: EndpointInput;
97
+ responses?: Record<number, EndpointResponse>;
98
+ /** Phantom — never present at runtime. Consumed by `InferEndpoint*` helpers. */
99
+ __types?: {
100
+ body: TBody;
101
+ query: TQuery;
102
+ params: TParams;
103
+ responses: TResponses;
104
+ };
105
+ }
106
+
107
+ // ── Endpoint type inference helpers ────────────────────────────────
108
+
109
+ /** Extract the request-body type from an {@link EndpointDefinition}. */
110
+ export type InferEndpointBody<E> = E extends EndpointDefinition<infer B, any, any, any> ? B : never;
111
+ /** Extract the query-string type from an {@link EndpointDefinition}. */
112
+ export type InferEndpointQuery<E> = E extends EndpointDefinition<any, infer Q, any, any> ? Q : never;
113
+ /** Extract the path-params type from an {@link EndpointDefinition}. */
114
+ export type InferEndpointParams<E> = E extends EndpointDefinition<any, any, infer P, any> ? P : never;
115
+ /** Extract the full `Record<status, body>` response map from an {@link EndpointDefinition}. */
116
+ export type InferEndpointResponses<E> = E extends EndpointDefinition<any, any, any, infer R> ? R : never;
117
+
118
+ /**
119
+ * Extract the success-response body from an {@link EndpointDefinition}.
120
+ *
121
+ * Tries `200`, then `201`, then `204`, then falls back to `unknown`.
122
+ * This matches the behavior of the `fortress.call.*` proxy, which always
123
+ * resolves to the first successful status declared.
124
+ */
125
+ export type InferEndpointSuccessResponse<E>
126
+ = InferEndpointResponses<E> extends infer R
127
+ ? R extends { 200: infer T } ? T
128
+ : R extends { 201: infer T } ? T
129
+ : R extends { 204: infer T } ? T
130
+ : unknown
131
+ : never;
132
+
133
+ /**
134
+ * The flat input shape accepted by the typed `fortress.call.<handler>(input)`
135
+ * proxy — the intersection of body, query, and params. For endpoints that
136
+ * only declare one of the three, the other slots are `{}` and disappear from
137
+ * the intersection.
138
+ */
139
+ export type InferEndpointCallInput<E> = Simplify<
140
+ InferEndpointBody<E> & InferEndpointQuery<E> & InferEndpointParams<E>
141
+ >;
142
+
143
+ /**
144
+ * Component schemas are reusable JSON Schema definitions
145
+ * that endpoints reference via $ref (e.g., '#/components/schemas/User').
146
+ */
147
+ export interface ComponentSchemas {
148
+ [name: string]: JSONSchema;
149
+ }
@@ -0,0 +1,199 @@
1
+ /** Discriminated string union of every error code fortress can throw. Maps 1:1 to an HTTP status. */
2
+ export type FortressErrorCode
3
+ = | 'UNAUTHORIZED'
4
+ | 'TOKEN_REUSE'
5
+ | 'SESSION_IDLE_TIMEOUT'
6
+ | 'SESSION_ABSOLUTE_TIMEOUT'
7
+ | 'FORBIDDEN'
8
+ | 'BAD_REQUEST'
9
+ | 'NOT_FOUND'
10
+ | 'CONFLICT'
11
+ | 'UNPROCESSABLE_ENTITY'
12
+ | 'RATE_LIMITED'
13
+ | 'DATABASE_ERROR'
14
+ | 'VALIDATION_ERROR'
15
+ | 'SERVICE_UNAVAILABLE';
16
+
17
+ /**
18
+ * Machine-readable error codes defined by RFC 6749 §5.2 (token endpoint) and
19
+ * §4.1.2.1 (authorization endpoint). The OAuth plugin uses these as the
20
+ * `error` field in token-endpoint failure responses; the HTTP error mapper
21
+ * detects {@link FortressError.oauthError} and emits the
22
+ * `{ error, error_description }` shape required by the spec.
23
+ */
24
+ export type OAuthErrorCode
25
+ = | 'invalid_request'
26
+ | 'invalid_client'
27
+ | 'invalid_grant'
28
+ | 'unauthorized_client'
29
+ | 'unsupported_grant_type'
30
+ | 'invalid_scope'
31
+ | 'access_denied'
32
+ | 'unsupported_response_type'
33
+ | 'server_error'
34
+ | 'temporarily_unavailable';
35
+
36
+ /** The single error class fortress throws. Carries an error code, HTTP status, and structured details. */
37
+ export class FortressError extends Error {
38
+ readonly code: FortressErrorCode;
39
+ readonly statusCode: number;
40
+ readonly retryAfter?: number;
41
+ readonly details?: unknown;
42
+ /** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
43
+ readonly oauthError?: OAuthErrorCode;
44
+ /** Human-readable description for the OAuth `error_description` field. */
45
+ readonly oauthDescription?: string;
46
+ /** Optional URI for the OAuth `error_uri` field (§5.2). */
47
+ readonly oauthErrorUri?: string;
48
+
49
+ constructor(
50
+ code: FortressErrorCode,
51
+ message: string,
52
+ statusCode: number,
53
+ options?: {
54
+ cause?: unknown;
55
+ retryAfter?: number;
56
+ details?: unknown;
57
+ oauthError?: OAuthErrorCode;
58
+ oauthDescription?: string;
59
+ oauthErrorUri?: string;
60
+ },
61
+ ) {
62
+ super(message, { cause: options?.cause });
63
+ this.code = code;
64
+ this.statusCode = statusCode;
65
+ this.retryAfter = options?.retryAfter;
66
+ this.details = options?.details;
67
+ this.oauthError = options?.oauthError;
68
+ this.oauthDescription = options?.oauthDescription;
69
+ this.oauthErrorUri = options?.oauthErrorUri;
70
+ }
71
+
72
+ toJSON(): { code: FortressErrorCode; message: string; statusCode: number; details?: unknown } {
73
+ return {
74
+ code: this.code,
75
+ message: this.message,
76
+ statusCode: this.statusCode,
77
+ ...(this.details !== undefined && { details: this.details }),
78
+ };
79
+ }
80
+ }
81
+
82
+ /** Typed factory of {@link FortressError} constructors — one helper per error code. */
83
+ export const Errors = {
84
+ unauthorized: (message = 'Unauthorized'): FortressError =>
85
+ new FortressError('UNAUTHORIZED', message, 401),
86
+ tokenReuse: (): FortressError =>
87
+ new FortressError('TOKEN_REUSE', 'Token reuse detected', 401),
88
+ sessionIdleTimeout: (): FortressError =>
89
+ new FortressError('SESSION_IDLE_TIMEOUT', 'Session idle timeout exceeded', 401),
90
+ sessionAbsoluteTimeout: (): FortressError =>
91
+ new FortressError('SESSION_ABSOLUTE_TIMEOUT', 'Session absolute timeout exceeded', 401),
92
+ forbidden: (message = 'Forbidden'): FortressError =>
93
+ new FortressError('FORBIDDEN', message, 403),
94
+ badRequest: (message = 'Bad request'): FortressError =>
95
+ new FortressError('BAD_REQUEST', message, 400),
96
+ notFound: (message = 'Not found'): FortressError =>
97
+ new FortressError('NOT_FOUND', message, 404),
98
+ conflict: (message = 'Conflict', options?: { cause?: unknown; details?: unknown }): FortressError =>
99
+ new FortressError('CONFLICT', message, 409, options),
100
+ unprocessable: (message = 'Unprocessable entity', options?: { cause?: unknown; details?: unknown }): FortressError =>
101
+ new FortressError('UNPROCESSABLE_ENTITY', message, 422, options),
102
+ rateLimited: (retryAfter: number): FortressError =>
103
+ new FortressError('RATE_LIMITED', 'Too many requests', 429, { retryAfter }),
104
+ database: (message = 'Database error', cause?: unknown): FortressError =>
105
+ new FortressError('DATABASE_ERROR', message, 500, { cause }),
106
+ serviceUnavailable: (message = 'Service unavailable', options?: { cause?: unknown; retryAfter?: number }): FortressError =>
107
+ new FortressError('SERVICE_UNAVAILABLE', message, 503, options),
108
+ validationError: (issues: Array<{ path?: unknown; message: string }>): FortressError =>
109
+ new FortressError('VALIDATION_ERROR', 'Validation failed', 422, { details: issues }),
110
+
111
+ /**
112
+ * RFC 6749 §5.2 / §4.1.2.1 OAuth error.
113
+ *
114
+ * The HTTP error mapper detects `oauthError` and emits the OAuth-spec
115
+ * JSON body `{ error, error_description, error_uri? }` instead of the
116
+ * default fortress `{ code, message }` shape, so strict OAuth clients
117
+ * (Moodle, openid-client, Spring Security, etc.) can switch behaviour
118
+ * on the machine-readable `error` field.
119
+ *
120
+ * Status is inferred from the OAuth code per the spec table:
121
+ * - `invalid_client` → 401
122
+ * - everything else → 400
123
+ */
124
+ oauth: (
125
+ error: OAuthErrorCode,
126
+ description?: string,
127
+ options?: { status?: number; errorUri?: string; cause?: unknown },
128
+ ): FortressError => {
129
+ const status = options?.status ?? (error === 'invalid_client' ? 401 : 400);
130
+ const code: FortressErrorCode = status === 401 ? 'UNAUTHORIZED' : 'BAD_REQUEST';
131
+ return new FortressError(code, description ?? error, status, {
132
+ oauthError: error,
133
+ oauthDescription: description,
134
+ oauthErrorUri: options?.errorUri,
135
+ cause: options?.cause,
136
+ });
137
+ },
138
+
139
+ /**
140
+ * Reconstruct a {@link FortressError} from a JSON error body emitted by
141
+ * `errorToResponse`. Used by the in-process `fortress.call.*` client to
142
+ * convert non-2xx responses into typed throws that mirror what a direct
143
+ * service-method call would produce.
144
+ *
145
+ * Maps by HTTP status when the body doesn't carry a recognizable
146
+ * `{ code, message }` payload, so network errors and opaque upstream
147
+ * failures still become structured `FortressError`s.
148
+ */
149
+ fromHttpResponse: (status: number, body: unknown): FortressError => {
150
+ const payload = (body && typeof body === 'object' ? body : {}) as {
151
+ code?: string;
152
+ message?: string;
153
+ details?: unknown;
154
+ };
155
+ const message = typeof payload.message === 'string' ? payload.message : `HTTP ${status}`;
156
+ const code = typeof payload.code === 'string' && isFortressErrorCode(payload.code)
157
+ ? payload.code
158
+ : statusToErrorCode(status);
159
+ return new FortressError(code, message, status, { details: payload.details });
160
+ },
161
+ } as const;
162
+
163
+ function isFortressErrorCode(code: string): code is FortressErrorCode {
164
+ return (
165
+ code === 'UNAUTHORIZED'
166
+ || code === 'TOKEN_REUSE'
167
+ || code === 'SESSION_IDLE_TIMEOUT'
168
+ || code === 'SESSION_ABSOLUTE_TIMEOUT'
169
+ || code === 'FORBIDDEN'
170
+ || code === 'BAD_REQUEST'
171
+ || code === 'NOT_FOUND'
172
+ || code === 'CONFLICT'
173
+ || code === 'UNPROCESSABLE_ENTITY'
174
+ || code === 'RATE_LIMITED'
175
+ || code === 'DATABASE_ERROR'
176
+ || code === 'VALIDATION_ERROR'
177
+ || code === 'SERVICE_UNAVAILABLE'
178
+ );
179
+ }
180
+
181
+ function statusToErrorCode(status: number): FortressErrorCode {
182
+ if (status === 401)
183
+ return 'UNAUTHORIZED';
184
+ if (status === 403)
185
+ return 'FORBIDDEN';
186
+ if (status === 404)
187
+ return 'NOT_FOUND';
188
+ if (status === 409)
189
+ return 'CONFLICT';
190
+ if (status === 422)
191
+ return 'UNPROCESSABLE_ENTITY';
192
+ if (status === 429)
193
+ return 'RATE_LIMITED';
194
+ if (status === 503)
195
+ return 'SERVICE_UNAVAILABLE';
196
+ if (status >= 500)
197
+ return 'DATABASE_ERROR';
198
+ return 'BAD_REQUEST';
199
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Interop: authoring fortress endpoint schemas with `@bajustone/fetcher`'s own
3
+ * builder (re-exported at `@bajustone/fortress/fetcher`). Fetcher schemas are
4
+ * JSON Schema objects that implement Standard Schema V1, so they drop straight
5
+ * into `endpoint().body()/.query()/.params()/.response()`:
6
+ * - runtime validation uses the fetcher schema's own compiled validator
7
+ * - OpenAPI emission strips fetcher's internal `~`-prefixed keys, leaving
8
+ * clean JSON Schema
9
+ *
10
+ * This locks that contract in so neither `extractJsonSchema` (detection) nor
11
+ * `cleanSchema` (OpenAPI serialization) can regress it.
12
+ */
13
+ import type { StandardSchemaV1 } from './standard-schema';
14
+ import { discriminatedUnion, email as femail, integer, literal, nullable, object, optional, string } from '@bajustone/fetcher/schema';
15
+ import { describe, expect, it } from 'vitest';
16
+ import { toOpenAPI } from './openapi';
17
+ import { endpoint, extractJsonSchema, isStandardSchema } from './schema-builder';
18
+ import { validateValue } from './validation';
19
+
20
+ /** Recursively collect any object key beginning with `~` (fetcher internals). */
21
+ function tildeKeys(value: unknown, acc: string[] = []): string[] {
22
+ if (Array.isArray(value)) {
23
+ for (const v of value)
24
+ tildeKeys(v, acc);
25
+ }
26
+ else if (value && typeof value === 'object') {
27
+ for (const [k, v] of Object.entries(value)) {
28
+ if (k.startsWith('~'))
29
+ acc.push(k);
30
+ tildeKeys(v, acc);
31
+ }
32
+ }
33
+ return acc;
34
+ }
35
+
36
+ const Body = object({
37
+ email: femail(),
38
+ name: string({ minLength: 1 }),
39
+ age: optional(integer()),
40
+ nick: nullable(string()),
41
+ kind: discriminatedUnion('t', {
42
+ a: object({ t: literal('a'), x: integer() }),
43
+ b: object({ t: literal('b'), y: string() }),
44
+ }),
45
+ });
46
+
47
+ describe('fetcher builder interop — endpoint authoring', () => {
48
+ it('a fetcher schema is a valid Standard Schema and JSON Schema', () => {
49
+ expect(isStandardSchema(Body)).toBe(true);
50
+ expect((Body as any).type).toBe('object');
51
+ // extractJsonSchema accepts the fetcher-shaped schema (no Zod-style adapter)
52
+ expect((extractJsonSchema(Body as any) as any).type).toBe('object');
53
+ });
54
+
55
+ it('validates request data via the fetcher schema validator', async () => {
56
+ const okValue = await validateValue(
57
+ Body as unknown as StandardSchemaV1,
58
+ { email: 'user@example.com', name: 'x', nick: null, kind: { t: 'a', x: 1 } },
59
+ 'body',
60
+ );
61
+ expect(okValue).toMatchObject({ email: 'user@example.com', name: 'x' });
62
+
63
+ await expect(
64
+ validateValue(
65
+ Body as unknown as StandardSchemaV1,
66
+ { email: 'not-an-email', name: '', nick: 1, kind: { t: 'a', x: 'no' } },
67
+ 'body',
68
+ ),
69
+ ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', statusCode: 422 });
70
+ });
71
+
72
+ it('stores both JSON Schema (OpenAPI) and the validator on the endpoint', () => {
73
+ const ep = endpoint('POST', '/users')
74
+ .summary('Create user')
75
+ .body(Body as any)
76
+ .response(200, 'Created', object({ id: string() }) as any)
77
+ .handler('createUser')
78
+ .build();
79
+
80
+ expect(ep.input?.body?.type).toBe('object');
81
+ expect(typeof (ep.input?.bodySchema as any)?.['~standard'].validate).toBe('function');
82
+ });
83
+
84
+ it('emits clean OpenAPI — no fetcher ~ keys, correct shape', () => {
85
+ const ep = endpoint('POST', '/users')
86
+ .summary('Create user')
87
+ .body(Body as any)
88
+ .handler('createUser')
89
+ .build();
90
+
91
+ const spec = toOpenAPI([ep], { title: 't', version: '1' });
92
+ expect(tildeKeys(spec)).toEqual([]);
93
+
94
+ const schema = (spec as any).paths['/users'].post.requestBody.content['application/json'].schema;
95
+ // optional `age` is excluded from required; required keys present
96
+ expect(schema.required).toEqual(['email', 'name', 'nick', 'kind']);
97
+ // enforced email format surfaces as format + pattern
98
+ expect(schema.properties.email.format).toBe('email');
99
+ expect(typeof schema.properties.email.pattern).toBe('string');
100
+ // nullable → anyOf with a null branch (valid OpenAPI 3.1)
101
+ expect(schema.properties.nick.anyOf).toEqual([{ type: 'string' }, { type: 'null' }]);
102
+ // discriminatedUnion → oneOf + discriminator
103
+ expect(schema.properties.kind.oneOf).toHaveLength(2);
104
+ expect(schema.properties.kind.discriminator.propertyName).toBe('t');
105
+ });
106
+ });