@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,124 @@
1
+ import type { FortressLogger } from './logger';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { createListenerList } from './listener-list';
4
+ import { SILENT_LOGGER } from './logger';
5
+
6
+ function mockLogger(): FortressLogger & { errorSpy: ReturnType<typeof vi.fn> } {
7
+ const errorSpy = vi.fn();
8
+ return {
9
+ ...SILENT_LOGGER,
10
+ error: errorSpy,
11
+ errorSpy,
12
+ };
13
+ }
14
+
15
+ describe('createListenerList (sync)', () => {
16
+ it('invokes listeners in registration order and returns an unsubscribe fn', () => {
17
+ const logger = mockLogger();
18
+ const list = createListenerList<number>({
19
+ kind: 'sync',
20
+ eventLabel: 'test',
21
+ logger: () => logger,
22
+ });
23
+
24
+ const calls: string[] = [];
25
+ const off1 = list.add((n) => {
26
+ calls.push(`a:${n}`);
27
+ });
28
+ const off2 = list.add((n) => {
29
+ calls.push(`b:${n}`);
30
+ });
31
+ expect(list.size()).toBe(2);
32
+
33
+ list.emit(1);
34
+ expect(calls).toEqual(['a:1', 'b:1']);
35
+
36
+ off1();
37
+ list.emit(2);
38
+ expect(calls).toEqual(['a:1', 'b:1', 'b:2']);
39
+
40
+ off2();
41
+ expect(list.size()).toBe(0);
42
+ list.emit(3);
43
+ expect(calls).toEqual(['a:1', 'b:1', 'b:2']);
44
+ });
45
+
46
+ it('catches thrown errors and routes them to the logger', () => {
47
+ const logger = mockLogger();
48
+ const list = createListenerList<{ id: string }>({
49
+ kind: 'sync',
50
+ eventLabel: 'boom',
51
+ logger: () => logger,
52
+ });
53
+
54
+ list.add(() => {
55
+ throw new Error('listener bug');
56
+ });
57
+ list.add(() => {
58
+ // healthy listener
59
+ });
60
+
61
+ // Emit should NOT throw — error is routed to logger.error.
62
+ expect(() => list.emit({ id: '1' })).not.toThrow();
63
+ expect(logger.errorSpy).toHaveBeenCalledTimes(1);
64
+ expect(logger.errorSpy.mock.calls[0][0]).toMatchObject({
65
+ event: 'boom',
66
+ });
67
+ });
68
+
69
+ it('remaining listeners still fire after one throws', () => {
70
+ const logger = mockLogger();
71
+ const list = createListenerList<number>({
72
+ kind: 'sync',
73
+ eventLabel: 'test',
74
+ logger: () => logger,
75
+ });
76
+ const after = vi.fn();
77
+ list.add(() => {
78
+ throw new Error('first listener');
79
+ });
80
+ list.add(after);
81
+ list.emit(42);
82
+ expect(after).toHaveBeenCalledWith(42);
83
+ });
84
+ });
85
+
86
+ describe('createListenerList (async)', () => {
87
+ it('fires listeners without awaiting their resolution', async () => {
88
+ const logger = mockLogger();
89
+ const list = createListenerList<string>({
90
+ kind: 'async',
91
+ eventLabel: 'async',
92
+ logger: () => logger,
93
+ });
94
+
95
+ let resolved = false;
96
+ list.add(async (v) => {
97
+ await new Promise(r => setTimeout(r, 10));
98
+ resolved = true;
99
+ expect(v).toBe('hello');
100
+ });
101
+
102
+ list.emit('hello');
103
+ // Listener hasn't finished yet — emit is non-blocking.
104
+ expect(resolved).toBe(false);
105
+ await new Promise(r => setTimeout(r, 30));
106
+ expect(resolved).toBe(true);
107
+ });
108
+
109
+ it('routes rejected promises to logger.error', async () => {
110
+ const logger = mockLogger();
111
+ const list = createListenerList<number>({
112
+ kind: 'async',
113
+ eventLabel: 'async',
114
+ logger: () => logger,
115
+ });
116
+ list.add(async () => {
117
+ throw new Error('async boom');
118
+ });
119
+
120
+ list.emit(1);
121
+ await new Promise(r => setTimeout(r, 10));
122
+ expect(logger.errorSpy).toHaveBeenCalledTimes(1);
123
+ });
124
+ });
@@ -0,0 +1,73 @@
1
+ import type { FortressLogger } from './logger';
2
+
3
+ export type Unsubscribe = () => void;
4
+
5
+ export interface ListenerList<E> {
6
+ emit: (event: E) => void;
7
+ add: (listener: (event: E) => void | Promise<void>) => Unsubscribe;
8
+ size: () => number;
9
+ }
10
+
11
+ /**
12
+ * Create a typed listener list with error routing and a sync/async contract.
13
+ *
14
+ * - `sync` kind: listeners return `void`. Used for hot-path observers
15
+ * (permission checks) where the type signature discourages awaiting
16
+ * expensive work inline.
17
+ * - `async` kind: listeners may return `void | Promise<void>`. Promises
18
+ * returned by listeners are attached a `.catch` that routes failures to
19
+ * the logger at `error` level — observer bugs never break the caller.
20
+ *
21
+ * **Error-routing gotcha (async kind):** the safety net only engages
22
+ * when the listener *returns* the promise. A listener that fires work
23
+ * via `void asyncWork()` inside a sync body returns `undefined`, so the
24
+ * listener list cannot observe a later rejection — it escapes to the
25
+ * runtime's unhandled-rejection handler. If you want the safety net,
26
+ * `return asyncWork()` or make the whole body `async`. `void asyncWork()`
27
+ * is an explicit opt-out.
28
+ *
29
+ * The `logger` parameter is a thunk so the list can resolve the current
30
+ * logger lazily — allowing callers to construct the list before the final
31
+ * logger is available in the closure.
32
+ */
33
+ export function createListenerList<E>(opts: {
34
+ kind: 'sync' | 'async';
35
+ eventLabel: string;
36
+ logger: () => FortressLogger;
37
+ }): ListenerList<E> {
38
+ const listeners: Array<(event: E) => void | Promise<void>> = [];
39
+
40
+ return {
41
+ emit(event: E): void {
42
+ for (const listener of listeners) {
43
+ try {
44
+ const result = listener(event);
45
+ if (opts.kind === 'async' && result instanceof Promise) {
46
+ result.catch((err: unknown) => {
47
+ opts.logger().error(
48
+ { err, event: opts.eventLabel },
49
+ 'fortress observer threw',
50
+ );
51
+ });
52
+ }
53
+ }
54
+ catch (err) {
55
+ opts.logger().error(
56
+ { err, event: opts.eventLabel },
57
+ 'fortress observer threw',
58
+ );
59
+ }
60
+ }
61
+ },
62
+ add(listener): Unsubscribe {
63
+ listeners.push(listener);
64
+ return () => {
65
+ const i = listeners.indexOf(listener);
66
+ if (i >= 0) {
67
+ listeners.splice(i, 1);
68
+ }
69
+ };
70
+ },
71
+ size: () => listeners.length,
72
+ };
73
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SILENT_LOGGER } from './logger';
3
+
4
+ describe('sILENT_LOGGER', () => {
5
+ it('is a no-op — every level callable with zero side effects', () => {
6
+ expect(SILENT_LOGGER.level).toBe('silent');
7
+ expect(() => SILENT_LOGGER.fatal('msg')).not.toThrow();
8
+ expect(() => SILENT_LOGGER.error('msg')).not.toThrow();
9
+ expect(() => SILENT_LOGGER.warn('msg')).not.toThrow();
10
+ expect(() => SILENT_LOGGER.info('msg')).not.toThrow();
11
+ expect(() => SILENT_LOGGER.debug('msg')).not.toThrow();
12
+ expect(() => SILENT_LOGGER.trace('msg')).not.toThrow();
13
+ expect(() => SILENT_LOGGER.silent('msg')).not.toThrow();
14
+ });
15
+
16
+ it('accepts the three pino-compatible argument shapes at every level', () => {
17
+ expect(() => SILENT_LOGGER.info('just a string')).not.toThrow();
18
+ expect(() => SILENT_LOGGER.info({ key: 'value' }, 'with object')).not.toThrow();
19
+ expect(() => SILENT_LOGGER.info('format %s', 'arg')).not.toThrow();
20
+ });
21
+
22
+ it('isLevelEnabled always returns false for the silent default', () => {
23
+ expect(SILENT_LOGGER.isLevelEnabled?.('debug')).toBe(false);
24
+ expect(SILENT_LOGGER.isLevelEnabled?.('error')).toBe(false);
25
+ });
26
+
27
+ it('is structurally compatible with a hand-rolled console logger', () => {
28
+ const calls: string[] = [];
29
+ const consoleLogger = {
30
+ level: 'info',
31
+ fatal: (...args: unknown[]): void => { calls.push(`fatal:${JSON.stringify(args)}`); },
32
+ error: (...args: unknown[]): void => { calls.push(`error:${JSON.stringify(args)}`); },
33
+ warn: (...args: unknown[]): void => { calls.push(`warn:${JSON.stringify(args)}`); },
34
+ info: (...args: unknown[]): void => { calls.push(`info:${JSON.stringify(args)}`); },
35
+ debug: (...args: unknown[]): void => { calls.push(`debug:${JSON.stringify(args)}`); },
36
+ trace: (...args: unknown[]): void => { calls.push(`trace:${JSON.stringify(args)}`); },
37
+ silent: (): void => {},
38
+ };
39
+ consoleLogger.warn({ plugin: 'test' }, 'warning');
40
+ expect(calls).toHaveLength(1);
41
+ expect(calls[0]).toContain('warn:');
42
+ });
43
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Structural logger contract. Shaped to be assignment-compatible with pino's
3
+ * `BaseLogger` and Fastify's `FastifyBaseLogger`, so consumers can drop a
4
+ * `pino()` instance or `fastify.log` in directly with zero adapter code.
5
+ *
6
+ * The default is {@link SILENT_LOGGER} — libraries that log to stderr by
7
+ * default are universally hated. Opt-in logging only.
8
+ */
9
+
10
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
11
+
12
+ export interface LogFn {
13
+ (msg: string): void;
14
+ (obj: Record<string, unknown>, msg?: string): void;
15
+ (msg: string, ...args: unknown[]): void;
16
+ }
17
+
18
+ export interface FortressLogger {
19
+ level: LogLevel | 'silent' | string;
20
+ fatal: LogFn;
21
+ error: LogFn;
22
+ warn: LogFn;
23
+ info: LogFn;
24
+ debug: LogFn;
25
+ trace: LogFn;
26
+ silent: LogFn;
27
+ /** Optional on pino/Fastify; Fortress calls it defensively. */
28
+ child?: (bindings: Record<string, unknown>) => FortressLogger;
29
+ /** Optional guard for lazy evaluation of expensive log metadata. */
30
+ isLevelEnabled?: (level: LogLevel) => boolean;
31
+ }
32
+
33
+ const NOOP: LogFn = () => {};
34
+
35
+ /**
36
+ * No-op logger used when {@link FortressConfig.logger} is unset. Every method
37
+ * is a shared arrow function — zero allocation per call.
38
+ */
39
+ export const SILENT_LOGGER: FortressLogger = {
40
+ level: 'silent',
41
+ fatal: NOOP,
42
+ error: NOOP,
43
+ warn: NOOP,
44
+ info: NOOP,
45
+ debug: NOOP,
46
+ trace: NOOP,
47
+ silent: NOOP,
48
+ isLevelEnabled: () => false,
49
+ };
@@ -0,0 +1,193 @@
1
+ import type { Fortress } from '../fortress';
2
+ import type { Attributes, AttributeValue, Span, TelemetryProvider } from './types';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+ import { createTestAdapter } from '../../testing';
5
+ import { createFortress } from '../fortress';
6
+ import { NO_OP_TELEMETRY } from './types';
7
+
8
+ interface SpanRecord {
9
+ name: string;
10
+ attributes: Record<string, AttributeValue>;
11
+ ended: boolean;
12
+ status?: { code: 'ok' | 'error'; message?: string };
13
+ exception?: unknown;
14
+ }
15
+
16
+ /**
17
+ * Build a {@link TelemetryProvider} that records every span that gets
18
+ * started — used here instead of `@opentelemetry/sdk-trace-base` so we
19
+ * can assert on spans without adding another dev dep. The meter falls
20
+ * back to no-op because these tests only care about spans.
21
+ */
22
+ function createSpyTelemetry(): { provider: TelemetryProvider; spans: SpanRecord[] } {
23
+ const spans: SpanRecord[] = [];
24
+ const startSpan = (name: string, attributes?: Attributes): Span => {
25
+ const record: SpanRecord = {
26
+ name,
27
+ attributes: { ...(attributes ?? {}) },
28
+ ended: false,
29
+ };
30
+ spans.push(record);
31
+ return {
32
+ setAttribute(key, value): void {
33
+ record.attributes[key] = value;
34
+ },
35
+ recordException(error): void {
36
+ record.exception = error;
37
+ },
38
+ setStatus(status): void {
39
+ record.status = status;
40
+ },
41
+ end(): void {
42
+ record.ended = true;
43
+ },
44
+ };
45
+ };
46
+ return {
47
+ provider: {
48
+ tracer: {
49
+ startSpan,
50
+ async startActiveSpan(name, attributes, callback) {
51
+ return callback(startSpan(name, attributes));
52
+ },
53
+ },
54
+ meter: NO_OP_TELEMETRY.meter,
55
+ },
56
+ spans,
57
+ };
58
+ }
59
+
60
+ const SECRET = 'span-test-secret-must-be-32-chars!';
61
+
62
+ let fortress: Fortress;
63
+ let spans: SpanRecord[];
64
+
65
+ describe('handleRequest outer span', () => {
66
+ beforeEach(async () => {
67
+ const telemetry = createSpyTelemetry();
68
+ spans = telemetry.spans;
69
+ fortress = createFortress({
70
+ jwt: { key: SECRET },
71
+ database: createTestAdapter(),
72
+ observability: telemetry.provider,
73
+ });
74
+ await fortress.auth.createUser({
75
+ email: 'spans@example.com',
76
+ name: 'Span User',
77
+ password: 'password-123456',
78
+ });
79
+ });
80
+
81
+ it('falls back to startSpan for legacy custom telemetry providers', async () => {
82
+ const telemetry = createSpyTelemetry();
83
+ delete telemetry.provider.tracer.startActiveSpan;
84
+ const legacyFortress = createFortress({
85
+ jwt: { key: SECRET },
86
+ database: createTestAdapter(),
87
+ observability: telemetry.provider,
88
+ });
89
+
90
+ const response = await legacyFortress.handleRequest(new Request('http://localhost/nope'));
91
+ expect(response.status).toBe(404);
92
+ expect(telemetry.spans.find(span => span.name === 'fortress.handleRequest')?.ended).toBe(true);
93
+ });
94
+
95
+ it('starts and ends a fortress.handleRequest span for every dispatched request', async () => {
96
+ const res = await fortress.handleRequest(
97
+ new Request('http://local/auth/login', {
98
+ method: 'POST',
99
+ headers: { 'content-type': 'application/json' },
100
+ body: JSON.stringify({ identifier: 'spans@example.com', password: 'password-123456' }),
101
+ }),
102
+ );
103
+ expect(res.status).toBe(200);
104
+
105
+ const requestSpans = spans.filter(s => s.name === 'fortress.handleRequest');
106
+ expect(requestSpans.length).toBe(1);
107
+ const span = requestSpans[0]!;
108
+ expect(span.ended).toBe(true);
109
+ expect(span.attributes['http.method']).toBe('POST');
110
+ expect(span.attributes['http.route']).toBe('/auth/login');
111
+ expect(span.attributes['fortress.handler']).toBe('login');
112
+ expect(span.attributes['http.status_code']).toBe(200);
113
+ expect(span.status?.code).toBe('ok');
114
+ });
115
+
116
+ it('sets error status and records exception when dispatch throws', async () => {
117
+ const res = await fortress.handleRequest(
118
+ new Request('http://local/auth/login', {
119
+ method: 'POST',
120
+ headers: { 'content-type': 'application/json' },
121
+ body: JSON.stringify({ identifier: 'spans@example.com', password: 'wrong' }),
122
+ }),
123
+ );
124
+ expect(res.status).toBe(401);
125
+
126
+ const requestSpan = spans.find(s => s.name === 'fortress.handleRequest');
127
+ expect(requestSpan).toBeDefined();
128
+ expect(requestSpan!.ended).toBe(true);
129
+ expect(requestSpan!.exception).toBeDefined();
130
+ expect(requestSpan!.status?.code).toBe('error');
131
+ expect(requestSpan!.attributes['http.status_code']).toBe(401);
132
+ });
133
+
134
+ it('sets error status on 4xx without a thrown exception', async () => {
135
+ // A request to an unknown route raises notFound — FortressError
136
+ // caught by the outer try.
137
+ const res = await fortress.handleRequest(
138
+ new Request('http://local/iam/roles/9999', { method: 'GET' }),
139
+ );
140
+ // iam/roles is owned by fortress — 401 because no bearer token,
141
+ // so we land in the exception branch. That's still "error" status.
142
+ // Use this assertion set to check the finally correctly marks 4xx.
143
+ expect([401, 403, 404]).toContain(res.status);
144
+
145
+ const span = spans.find(s => s.name === 'fortress.handleRequest');
146
+ expect(span?.ended).toBe(true);
147
+ expect(span?.status?.code).toBe('error');
148
+ });
149
+ });
150
+
151
+ describe('db-instrumentation spans', () => {
152
+ beforeEach(() => {
153
+ const telemetry = createSpyTelemetry();
154
+ spans = telemetry.spans;
155
+ fortress = createFortress({
156
+ jwt: { key: SECRET },
157
+ database: createTestAdapter(),
158
+ observability: telemetry.provider,
159
+ });
160
+ });
161
+
162
+ it('emits a span per DB operation with semantic-convention attributes', async () => {
163
+ await fortress.auth.createUser({
164
+ email: 'db-span@example.com',
165
+ name: 'DB Span User',
166
+ password: 'password-123456',
167
+ });
168
+
169
+ // Any span with a db.operation.name attribute is a DB span.
170
+ const dbSpans = spans.filter(s => typeof s.attributes['db.operation.name'] === 'string');
171
+ expect(dbSpans.length).toBeGreaterThan(0);
172
+
173
+ for (const span of dbSpans) {
174
+ expect(span.ended).toBe(true);
175
+ expect(span.attributes['db.system.name']).toBeDefined();
176
+ }
177
+
178
+ // Creating a user hits at least findOne(user), create(user), create(login_identifier).
179
+ const findOneUser = dbSpans.find(s =>
180
+ s.attributes['db.operation.name'] === 'findOne'
181
+ && s.attributes['db.collection.name'] === 'user',
182
+ );
183
+ expect(findOneUser).toBeDefined();
184
+ expect(findOneUser?.name).toBe('findOne user');
185
+
186
+ const createUser = dbSpans.find(s =>
187
+ s.attributes['db.operation.name'] === 'create'
188
+ && s.attributes['db.collection.name'] === 'user',
189
+ );
190
+ expect(createUser).toBeDefined();
191
+ expect(createUser?.name).toBe('create user');
192
+ });
193
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Fortress-internal telemetry interfaces.
3
+ *
4
+ * Core code depends on these shapes, never on `@opentelemetry/api`. The OTel
5
+ * adapter in `src/otel/index.ts` is the only file in the repo that imports
6
+ * `@opentelemetry/api`; it returns a {@link TelemetryProvider} that satisfies
7
+ * these interfaces. This keeps `@opentelemetry/api` off the core critical
8
+ * path — runtimes like Cloudflare Workers and Deno never resolve it unless
9
+ * the user explicitly imports `@bajustone/fortress/otel`.
10
+ */
11
+
12
+ export type AttributeValue = string | number | boolean;
13
+ export type Attributes = Record<string, AttributeValue>;
14
+
15
+ export interface Span {
16
+ setAttribute: (key: string, value: AttributeValue) => void;
17
+ recordException: (error: unknown) => void;
18
+ setStatus: (status: { code: 'ok' | 'error'; message?: string }) => void;
19
+ end: () => void;
20
+ }
21
+
22
+ export interface Tracer {
23
+ startSpan: (name: string, attributes?: Attributes) => Span;
24
+ /** Run a callback with this span as the active parent for nested spans. */
25
+ startActiveSpan?: <T>(
26
+ name: string,
27
+ attributes: Attributes | undefined,
28
+ callback: (span: Span) => T | Promise<T>,
29
+ ) => Promise<T>;
30
+ }
31
+
32
+ export interface Counter {
33
+ add: (value: number, attributes?: Attributes) => void;
34
+ }
35
+
36
+ export interface Histogram {
37
+ record: (value: number, attributes?: Attributes) => void;
38
+ }
39
+
40
+ export interface Meter {
41
+ createCounter: (
42
+ name: string,
43
+ options?: { description?: string; unit?: string },
44
+ ) => Counter;
45
+ createHistogram: (
46
+ name: string,
47
+ options?: { description?: string; unit?: string; boundaries?: number[] },
48
+ ) => Histogram;
49
+ }
50
+
51
+ export interface TelemetryProvider {
52
+ tracer: Tracer;
53
+ meter: Meter;
54
+ }
55
+
56
+ const NO_OP_SPAN: Span = {
57
+ setAttribute: () => {},
58
+ recordException: () => {},
59
+ setStatus: () => {},
60
+ end: () => {},
61
+ };
62
+
63
+ const NO_OP_COUNTER: Counter = { add: () => {} };
64
+ const NO_OP_HISTOGRAM: Histogram = { record: () => {} };
65
+
66
+ /**
67
+ * Default telemetry provider used when {@link FortressConfig.observability}
68
+ * is not set. Zero allocation per call — the instrument factories return
69
+ * shared no-op singletons and {@link Tracer.startSpan} returns a shared span.
70
+ */
71
+ export const NO_OP_TELEMETRY: TelemetryProvider = {
72
+ tracer: {
73
+ startSpan: () => NO_OP_SPAN,
74
+ startActiveSpan: async (_name, _attributes, callback) => callback(NO_OP_SPAN),
75
+ },
76
+ meter: {
77
+ createCounter: () => NO_OP_COUNTER,
78
+ createHistogram: () => NO_OP_HISTOGRAM,
79
+ },
80
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * CI gate over fortress's emitted OpenAPI spec, using `@bajustone/fetcher`'s
3
+ * `lintSpec`. It surfaces keyword usages the runtime validator does not enforce
4
+ * (type-vs-runtime divergence).
5
+ *
6
+ * Two keywords are intentionally allowed: `format` (always annotation-only in
7
+ * JSON Schema — fortress pairs it with an enforcing `pattern` via `email()` /
8
+ * `uuid()` / … and keeps it for OpenAPI docs) and `additionalProperties`
9
+ * (records, and response-only schemas that are never request-validated). Any
10
+ * OTHER keyword fetcher's runtime can't enforce (`multipleOf`,
11
+ * `exclusiveMinimum`, `minItems`, `uniqueItems`, …) is a real gap — adding one
12
+ * to a fortress schema fails this gate so it's caught before it ships.
13
+ */
14
+ import { lintSpec } from '@bajustone/fetcher/spec-tools';
15
+ import { describe, expect, it } from 'vitest';
16
+ import { createTestAdapter } from '../testing';
17
+ import { createFortress } from './fortress';
18
+ import { toOpenAPI } from './openapi';
19
+
20
+ interface SpecDriftIssue { pointer: string; keyword: string; severity: string; message: string }
21
+
22
+ const KNOWN_UNENFORCED = new Set(['format', 'additionalProperties']);
23
+
24
+ function coreSpec(): unknown {
25
+ const fortress = createFortress({ jwt: { key: 'x'.repeat(32) }, database: createTestAdapter() });
26
+ return toOpenAPI([...fortress.endpoints], { title: 'Fortress', version: '0.0.0' });
27
+ }
28
+
29
+ describe('openAPI spec drift (lintSpec gate)', () => {
30
+ it('emits no error-severity drift', () => {
31
+ const issues = lintSpec(coreSpec()) as SpecDriftIssue[];
32
+ expect(issues.filter(i => i.severity === 'error')).toEqual([]);
33
+ });
34
+
35
+ it('emits no unexpected unenforced keywords', () => {
36
+ const issues = lintSpec(coreSpec()) as SpecDriftIssue[];
37
+ const unexpected = issues.filter(i => !KNOWN_UNENFORCED.has(i.keyword));
38
+ // Rendered as readable strings so a failure points straight at the offending schema.
39
+ expect(unexpected.map(i => `${i.keyword} @ ${i.pointer}`)).toEqual([]);
40
+ });
41
+ });
@@ -0,0 +1,47 @@
1
+ import type { OpenAPISpec, SpecBuilderOptions } from '../plugins/openapi/spec-builder';
2
+ import type { ComponentSchemas, EndpointDefinition } from './endpoint';
3
+ import { buildOpenAPISpec } from '../plugins/openapi/spec-builder';
4
+
5
+ /** Options accepted by {@link toOpenAPI}. */
6
+ export interface ToOpenAPIOptions extends Partial<SpecBuilderOptions> {
7
+ /** Additional reusable schemas to place under `components.schemas`. */
8
+ schemas?: ComponentSchemas;
9
+ }
10
+
11
+ /**
12
+ * Emit an OpenAPI 3.1 spec from a set of Fortress endpoint definitions.
13
+ *
14
+ * This is the env/DB-free helper for build scripts and codegen pipelines.
15
+ * If you already have a configured Fortress instance, prefer
16
+ * `fortress.toOpenAPI()` so the endpoint list defaults to everything that
17
+ * instance knows about.
18
+ *
19
+ * ```ts
20
+ * import { toOpenAPI } from '@bajustone/fortress';
21
+ * import { appEndpointList } from './routes/v1/endpoints';
22
+ *
23
+ * export const OPENAPI_SPEC = toOpenAPI(appEndpointList, {
24
+ * title: 'My API',
25
+ * version: '0.0.0',
26
+ * servers: [{ url: 'http://localhost:3001' }],
27
+ * tags: [{ name: 'Schools' }],
28
+ * });
29
+ * ```
30
+ *
31
+ * Defaults to endpoint `handler` values for `operationId`, matching host
32
+ * route-contract names. Pass `operationId: 'methodPath'` for Fortress's
33
+ * historical generated IDs.
34
+ */
35
+ export function toOpenAPI(
36
+ endpoints: readonly EndpointDefinition[],
37
+ opts: ToOpenAPIOptions = {},
38
+ ): OpenAPISpec {
39
+ return buildOpenAPISpec([...endpoints], opts.schemas ?? {}, {
40
+ title: opts.title ?? 'Fortress API',
41
+ version: opts.version ?? '1.0.0',
42
+ description: opts.description,
43
+ servers: opts.servers,
44
+ tags: opts.tags,
45
+ operationId: opts.operationId ?? 'handler',
46
+ });
47
+ }