@fuzdev/fuz_app 0.1.0

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 (457) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/dist/actions/action_bridge.d.ts +65 -0
  4. package/dist/actions/action_bridge.d.ts.map +1 -0
  5. package/dist/actions/action_bridge.js +76 -0
  6. package/dist/actions/action_codegen.d.ts +97 -0
  7. package/dist/actions/action_codegen.d.ts.map +1 -0
  8. package/dist/actions/action_codegen.js +280 -0
  9. package/dist/actions/action_registry.d.ts +35 -0
  10. package/dist/actions/action_registry.d.ts.map +1 -0
  11. package/dist/actions/action_registry.js +83 -0
  12. package/dist/actions/action_spec.d.ts +169 -0
  13. package/dist/actions/action_spec.d.ts.map +1 -0
  14. package/dist/actions/action_spec.js +76 -0
  15. package/dist/auth/account_queries.d.ts +96 -0
  16. package/dist/auth/account_queries.d.ts.map +1 -0
  17. package/dist/auth/account_queries.js +172 -0
  18. package/dist/auth/account_routes.d.ts +86 -0
  19. package/dist/auth/account_routes.d.ts.map +1 -0
  20. package/dist/auth/account_routes.js +406 -0
  21. package/dist/auth/account_schema.d.ts +192 -0
  22. package/dist/auth/account_schema.d.ts.map +1 -0
  23. package/dist/auth/account_schema.js +105 -0
  24. package/dist/auth/admin_routes.d.ts +29 -0
  25. package/dist/auth/admin_routes.d.ts.map +1 -0
  26. package/dist/auth/admin_routes.js +193 -0
  27. package/dist/auth/api_token.d.ts +33 -0
  28. package/dist/auth/api_token.d.ts.map +1 -0
  29. package/dist/auth/api_token.js +36 -0
  30. package/dist/auth/api_token_queries.d.ts +80 -0
  31. package/dist/auth/api_token_queries.d.ts.map +1 -0
  32. package/dist/auth/api_token_queries.js +116 -0
  33. package/dist/auth/app_settings_queries.d.ts +33 -0
  34. package/dist/auth/app_settings_queries.d.ts.map +1 -0
  35. package/dist/auth/app_settings_queries.js +51 -0
  36. package/dist/auth/app_settings_routes.d.ts +27 -0
  37. package/dist/auth/app_settings_routes.d.ts.map +1 -0
  38. package/dist/auth/app_settings_routes.js +66 -0
  39. package/dist/auth/app_settings_schema.d.ts +35 -0
  40. package/dist/auth/app_settings_schema.d.ts.map +1 -0
  41. package/dist/auth/app_settings_schema.js +22 -0
  42. package/dist/auth/audit_log_queries.d.ts +90 -0
  43. package/dist/auth/audit_log_queries.d.ts.map +1 -0
  44. package/dist/auth/audit_log_queries.js +205 -0
  45. package/dist/auth/audit_log_routes.d.ts +33 -0
  46. package/dist/auth/audit_log_routes.d.ts.map +1 -0
  47. package/dist/auth/audit_log_routes.js +106 -0
  48. package/dist/auth/audit_log_schema.d.ts +259 -0
  49. package/dist/auth/audit_log_schema.d.ts.map +1 -0
  50. package/dist/auth/audit_log_schema.js +123 -0
  51. package/dist/auth/bearer_auth.d.ts +32 -0
  52. package/dist/auth/bearer_auth.d.ts.map +1 -0
  53. package/dist/auth/bearer_auth.js +90 -0
  54. package/dist/auth/bootstrap_account.d.ts +82 -0
  55. package/dist/auth/bootstrap_account.d.ts.map +1 -0
  56. package/dist/auth/bootstrap_account.js +97 -0
  57. package/dist/auth/bootstrap_routes.d.ts +74 -0
  58. package/dist/auth/bootstrap_routes.d.ts.map +1 -0
  59. package/dist/auth/bootstrap_routes.js +154 -0
  60. package/dist/auth/daemon_token.d.ts +49 -0
  61. package/dist/auth/daemon_token.d.ts.map +1 -0
  62. package/dist/auth/daemon_token.js +49 -0
  63. package/dist/auth/daemon_token_middleware.d.ts +93 -0
  64. package/dist/auth/daemon_token_middleware.d.ts.map +1 -0
  65. package/dist/auth/daemon_token_middleware.js +167 -0
  66. package/dist/auth/ddl.d.ts +27 -0
  67. package/dist/auth/ddl.d.ts.map +1 -0
  68. package/dist/auth/ddl.js +111 -0
  69. package/dist/auth/deps.d.ts +52 -0
  70. package/dist/auth/deps.d.ts.map +1 -0
  71. package/dist/auth/deps.js +10 -0
  72. package/dist/auth/invite_queries.d.ts +68 -0
  73. package/dist/auth/invite_queries.d.ts.map +1 -0
  74. package/dist/auth/invite_queries.js +105 -0
  75. package/dist/auth/invite_routes.d.ts +18 -0
  76. package/dist/auth/invite_routes.d.ts.map +1 -0
  77. package/dist/auth/invite_routes.js +129 -0
  78. package/dist/auth/invite_schema.d.ts +51 -0
  79. package/dist/auth/invite_schema.d.ts.map +1 -0
  80. package/dist/auth/invite_schema.js +25 -0
  81. package/dist/auth/keyring.d.ts +87 -0
  82. package/dist/auth/keyring.d.ts.map +1 -0
  83. package/dist/auth/keyring.js +142 -0
  84. package/dist/auth/middleware.d.ts +40 -0
  85. package/dist/auth/middleware.d.ts.map +1 -0
  86. package/dist/auth/middleware.js +64 -0
  87. package/dist/auth/migrations.d.ts +42 -0
  88. package/dist/auth/migrations.d.ts.map +1 -0
  89. package/dist/auth/migrations.js +79 -0
  90. package/dist/auth/password.d.ts +39 -0
  91. package/dist/auth/password.d.ts.map +1 -0
  92. package/dist/auth/password.js +25 -0
  93. package/dist/auth/password_argon2.d.ts +43 -0
  94. package/dist/auth/password_argon2.d.ts.map +1 -0
  95. package/dist/auth/password_argon2.js +76 -0
  96. package/dist/auth/permit_queries.d.ts +72 -0
  97. package/dist/auth/permit_queries.d.ts.map +1 -0
  98. package/dist/auth/permit_queries.js +116 -0
  99. package/dist/auth/request_context.d.ts +114 -0
  100. package/dist/auth/request_context.d.ts.map +1 -0
  101. package/dist/auth/request_context.js +176 -0
  102. package/dist/auth/require_keeper.d.ts +20 -0
  103. package/dist/auth/require_keeper.d.ts.map +1 -0
  104. package/dist/auth/require_keeper.js +35 -0
  105. package/dist/auth/role_schema.d.ts +69 -0
  106. package/dist/auth/role_schema.d.ts.map +1 -0
  107. package/dist/auth/role_schema.js +70 -0
  108. package/dist/auth/route_guards.d.ts +21 -0
  109. package/dist/auth/route_guards.d.ts.map +1 -0
  110. package/dist/auth/route_guards.js +32 -0
  111. package/dist/auth/session_cookie.d.ts +158 -0
  112. package/dist/auth/session_cookie.d.ts.map +1 -0
  113. package/dist/auth/session_cookie.js +135 -0
  114. package/dist/auth/session_lifecycle.d.ts +35 -0
  115. package/dist/auth/session_lifecycle.d.ts.map +1 -0
  116. package/dist/auth/session_lifecycle.js +27 -0
  117. package/dist/auth/session_middleware.d.ts +33 -0
  118. package/dist/auth/session_middleware.d.ts.map +1 -0
  119. package/dist/auth/session_middleware.js +62 -0
  120. package/dist/auth/session_queries.d.ts +135 -0
  121. package/dist/auth/session_queries.d.ts.map +1 -0
  122. package/dist/auth/session_queries.js +186 -0
  123. package/dist/auth/signup_routes.d.ts +32 -0
  124. package/dist/auth/signup_routes.d.ts.map +1 -0
  125. package/dist/auth/signup_routes.js +150 -0
  126. package/dist/cli/args.d.ts +48 -0
  127. package/dist/cli/args.d.ts.map +1 -0
  128. package/dist/cli/args.js +76 -0
  129. package/dist/cli/config.d.ts +48 -0
  130. package/dist/cli/config.d.ts.map +1 -0
  131. package/dist/cli/config.js +77 -0
  132. package/dist/cli/daemon.d.ts +82 -0
  133. package/dist/cli/daemon.d.ts.map +1 -0
  134. package/dist/cli/daemon.js +149 -0
  135. package/dist/cli/help.d.ts +85 -0
  136. package/dist/cli/help.d.ts.map +1 -0
  137. package/dist/cli/help.js +138 -0
  138. package/dist/cli/logger.d.ts +46 -0
  139. package/dist/cli/logger.d.ts.map +1 -0
  140. package/dist/cli/logger.js +48 -0
  141. package/dist/cli/util.d.ts +36 -0
  142. package/dist/cli/util.d.ts.map +1 -0
  143. package/dist/cli/util.js +50 -0
  144. package/dist/crypto.d.ts +13 -0
  145. package/dist/crypto.d.ts.map +1 -0
  146. package/dist/crypto.js +19 -0
  147. package/dist/db/assert_row.d.ts +18 -0
  148. package/dist/db/assert_row.d.ts.map +1 -0
  149. package/dist/db/assert_row.js +24 -0
  150. package/dist/db/create_db.d.ts +38 -0
  151. package/dist/db/create_db.d.ts.map +1 -0
  152. package/dist/db/create_db.js +57 -0
  153. package/dist/db/db.d.ts +97 -0
  154. package/dist/db/db.d.ts.map +1 -0
  155. package/dist/db/db.js +76 -0
  156. package/dist/db/db_pg.d.ts +21 -0
  157. package/dist/db/db_pg.d.ts.map +1 -0
  158. package/dist/db/db_pg.js +45 -0
  159. package/dist/db/db_pglite.d.ts +21 -0
  160. package/dist/db/db_pglite.d.ts.map +1 -0
  161. package/dist/db/db_pglite.js +28 -0
  162. package/dist/db/migrate.d.ts +67 -0
  163. package/dist/db/migrate.d.ts.map +1 -0
  164. package/dist/db/migrate.js +118 -0
  165. package/dist/db/pg_error.d.ts +16 -0
  166. package/dist/db/pg_error.d.ts.map +1 -0
  167. package/dist/db/pg_error.js +15 -0
  168. package/dist/db/query_deps.d.ts +14 -0
  169. package/dist/db/query_deps.d.ts.map +1 -0
  170. package/dist/db/query_deps.js +9 -0
  171. package/dist/db/sql_identifier.d.ts +27 -0
  172. package/dist/db/sql_identifier.d.ts.map +1 -0
  173. package/dist/db/sql_identifier.js +31 -0
  174. package/dist/db/status.d.ts +62 -0
  175. package/dist/db/status.d.ts.map +1 -0
  176. package/dist/db/status.js +116 -0
  177. package/dist/dev/setup.d.ts +159 -0
  178. package/dist/dev/setup.d.ts.map +1 -0
  179. package/dist/dev/setup.js +265 -0
  180. package/dist/env/dotenv.d.ts +25 -0
  181. package/dist/env/dotenv.d.ts.map +1 -0
  182. package/dist/env/dotenv.js +52 -0
  183. package/dist/env/load.d.ts +52 -0
  184. package/dist/env/load.d.ts.map +1 -0
  185. package/dist/env/load.js +79 -0
  186. package/dist/env/mask.d.ts +19 -0
  187. package/dist/env/mask.d.ts.map +1 -0
  188. package/dist/env/mask.js +26 -0
  189. package/dist/env/resolve.d.ts +126 -0
  190. package/dist/env/resolve.d.ts.map +1 -0
  191. package/dist/env/resolve.js +200 -0
  192. package/dist/hono_context.d.ts +48 -0
  193. package/dist/hono_context.d.ts.map +1 -0
  194. package/dist/hono_context.js +22 -0
  195. package/dist/http/common_routes.d.ts +52 -0
  196. package/dist/http/common_routes.d.ts.map +1 -0
  197. package/dist/http/common_routes.js +65 -0
  198. package/dist/http/db_routes.d.ts +57 -0
  199. package/dist/http/db_routes.d.ts.map +1 -0
  200. package/dist/http/db_routes.js +176 -0
  201. package/dist/http/error_schemas.d.ts +169 -0
  202. package/dist/http/error_schemas.d.ts.map +1 -0
  203. package/dist/http/error_schemas.js +178 -0
  204. package/dist/http/middleware_spec.d.ts +19 -0
  205. package/dist/http/middleware_spec.d.ts.map +1 -0
  206. package/dist/http/middleware_spec.js +9 -0
  207. package/dist/http/origin.d.ts +57 -0
  208. package/dist/http/origin.d.ts.map +1 -0
  209. package/dist/http/origin.js +207 -0
  210. package/dist/http/proxy.d.ts +112 -0
  211. package/dist/http/proxy.d.ts.map +1 -0
  212. package/dist/http/proxy.js +240 -0
  213. package/dist/http/route_spec.d.ts +197 -0
  214. package/dist/http/route_spec.d.ts.map +1 -0
  215. package/dist/http/route_spec.js +243 -0
  216. package/dist/http/schema_helpers.d.ts +64 -0
  217. package/dist/http/schema_helpers.d.ts.map +1 -0
  218. package/dist/http/schema_helpers.js +90 -0
  219. package/dist/http/surface.d.ts +132 -0
  220. package/dist/http/surface.d.ts.map +1 -0
  221. package/dist/http/surface.js +156 -0
  222. package/dist/http/surface_query.d.ts +77 -0
  223. package/dist/http/surface_query.d.ts.map +1 -0
  224. package/dist/http/surface_query.js +86 -0
  225. package/dist/rate_limiter.d.ts +94 -0
  226. package/dist/rate_limiter.d.ts.map +1 -0
  227. package/dist/rate_limiter.js +156 -0
  228. package/dist/realtime/sse.d.ts +80 -0
  229. package/dist/realtime/sse.d.ts.map +1 -0
  230. package/dist/realtime/sse.js +109 -0
  231. package/dist/realtime/sse_auth_guard.d.ts +93 -0
  232. package/dist/realtime/sse_auth_guard.d.ts.map +1 -0
  233. package/dist/realtime/sse_auth_guard.js +111 -0
  234. package/dist/realtime/subscriber_registry.d.ts +85 -0
  235. package/dist/realtime/subscriber_registry.d.ts.map +1 -0
  236. package/dist/realtime/subscriber_registry.js +108 -0
  237. package/dist/runtime/deno.d.ts +21 -0
  238. package/dist/runtime/deno.d.ts.map +1 -0
  239. package/dist/runtime/deno.js +83 -0
  240. package/dist/runtime/deps.d.ts +113 -0
  241. package/dist/runtime/deps.d.ts.map +1 -0
  242. package/dist/runtime/deps.js +10 -0
  243. package/dist/runtime/fs.d.ts +15 -0
  244. package/dist/runtime/fs.d.ts.map +1 -0
  245. package/dist/runtime/fs.js +17 -0
  246. package/dist/runtime/mock.d.ts +81 -0
  247. package/dist/runtime/mock.d.ts.map +1 -0
  248. package/dist/runtime/mock.js +195 -0
  249. package/dist/runtime/node.d.ts +17 -0
  250. package/dist/runtime/node.d.ts.map +1 -0
  251. package/dist/runtime/node.js +117 -0
  252. package/dist/schema_meta.d.ts +16 -0
  253. package/dist/schema_meta.d.ts.map +1 -0
  254. package/dist/schema_meta.js +9 -0
  255. package/dist/sensitivity.d.ts +15 -0
  256. package/dist/sensitivity.d.ts.map +1 -0
  257. package/dist/sensitivity.js +9 -0
  258. package/dist/server/app_backend.d.ts +74 -0
  259. package/dist/server/app_backend.d.ts.map +1 -0
  260. package/dist/server/app_backend.js +39 -0
  261. package/dist/server/app_server.d.ts +201 -0
  262. package/dist/server/app_server.d.ts.map +1 -0
  263. package/dist/server/app_server.js +266 -0
  264. package/dist/server/env.d.ts +68 -0
  265. package/dist/server/env.d.ts.map +1 -0
  266. package/dist/server/env.js +95 -0
  267. package/dist/server/startup.d.ts +22 -0
  268. package/dist/server/startup.d.ts.map +1 -0
  269. package/dist/server/startup.js +48 -0
  270. package/dist/server/static.d.ts +39 -0
  271. package/dist/server/static.d.ts.map +1 -0
  272. package/dist/server/static.js +38 -0
  273. package/dist/server/validate_nginx.d.ts +34 -0
  274. package/dist/server/validate_nginx.d.ts.map +1 -0
  275. package/dist/server/validate_nginx.js +118 -0
  276. package/dist/testing/CLAUDE.md +3 -0
  277. package/dist/testing/admin_integration.d.ts +45 -0
  278. package/dist/testing/admin_integration.d.ts.map +1 -0
  279. package/dist/testing/admin_integration.js +840 -0
  280. package/dist/testing/adversarial_404.d.ts +15 -0
  281. package/dist/testing/adversarial_404.d.ts.map +1 -0
  282. package/dist/testing/adversarial_404.js +118 -0
  283. package/dist/testing/adversarial_headers.d.ts +36 -0
  284. package/dist/testing/adversarial_headers.d.ts.map +1 -0
  285. package/dist/testing/adversarial_headers.js +128 -0
  286. package/dist/testing/adversarial_input.d.ts +56 -0
  287. package/dist/testing/adversarial_input.d.ts.map +1 -0
  288. package/dist/testing/adversarial_input.js +494 -0
  289. package/dist/testing/app_server.d.ts +169 -0
  290. package/dist/testing/app_server.d.ts.map +1 -0
  291. package/dist/testing/app_server.js +240 -0
  292. package/dist/testing/assert_dev_env.d.ts +10 -0
  293. package/dist/testing/assert_dev_env.d.ts.map +1 -0
  294. package/dist/testing/assert_dev_env.js +13 -0
  295. package/dist/testing/assertions.d.ts +61 -0
  296. package/dist/testing/assertions.d.ts.map +1 -0
  297. package/dist/testing/assertions.js +96 -0
  298. package/dist/testing/attack_surface.d.ts +63 -0
  299. package/dist/testing/attack_surface.d.ts.map +1 -0
  300. package/dist/testing/attack_surface.js +224 -0
  301. package/dist/testing/audit_completeness.d.ts +29 -0
  302. package/dist/testing/audit_completeness.d.ts.map +1 -0
  303. package/dist/testing/audit_completeness.js +410 -0
  304. package/dist/testing/auth_apps.d.ts +55 -0
  305. package/dist/testing/auth_apps.d.ts.map +1 -0
  306. package/dist/testing/auth_apps.js +122 -0
  307. package/dist/testing/data_exposure.d.ts +62 -0
  308. package/dist/testing/data_exposure.d.ts.map +1 -0
  309. package/dist/testing/data_exposure.js +297 -0
  310. package/dist/testing/db.d.ts +111 -0
  311. package/dist/testing/db.d.ts.map +1 -0
  312. package/dist/testing/db.js +258 -0
  313. package/dist/testing/entities.d.ts +21 -0
  314. package/dist/testing/entities.d.ts.map +1 -0
  315. package/dist/testing/entities.js +42 -0
  316. package/dist/testing/error_coverage.d.ts +78 -0
  317. package/dist/testing/error_coverage.d.ts.map +1 -0
  318. package/dist/testing/error_coverage.js +135 -0
  319. package/dist/testing/integration.d.ts +37 -0
  320. package/dist/testing/integration.d.ts.map +1 -0
  321. package/dist/testing/integration.js +1139 -0
  322. package/dist/testing/integration_helpers.d.ts +107 -0
  323. package/dist/testing/integration_helpers.d.ts.map +1 -0
  324. package/dist/testing/integration_helpers.js +246 -0
  325. package/dist/testing/middleware.d.ts +125 -0
  326. package/dist/testing/middleware.d.ts.map +1 -0
  327. package/dist/testing/middleware.js +210 -0
  328. package/dist/testing/rate_limiting.d.ts +43 -0
  329. package/dist/testing/rate_limiting.d.ts.map +1 -0
  330. package/dist/testing/rate_limiting.js +216 -0
  331. package/dist/testing/round_trip.d.ts +37 -0
  332. package/dist/testing/round_trip.d.ts.map +1 -0
  333. package/dist/testing/round_trip.js +128 -0
  334. package/dist/testing/schema_generators.d.ts +33 -0
  335. package/dist/testing/schema_generators.d.ts.map +1 -0
  336. package/dist/testing/schema_generators.js +137 -0
  337. package/dist/testing/standard.d.ts +49 -0
  338. package/dist/testing/standard.d.ts.map +1 -0
  339. package/dist/testing/standard.js +16 -0
  340. package/dist/testing/stubs.d.ts +96 -0
  341. package/dist/testing/stubs.d.ts.map +1 -0
  342. package/dist/testing/stubs.js +192 -0
  343. package/dist/testing/surface_invariants.d.ts +189 -0
  344. package/dist/testing/surface_invariants.d.ts.map +1 -0
  345. package/dist/testing/surface_invariants.js +450 -0
  346. package/dist/ui/AccountSessions.svelte +75 -0
  347. package/dist/ui/AccountSessions.svelte.d.ts +19 -0
  348. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -0
  349. package/dist/ui/AdminAccounts.svelte +107 -0
  350. package/dist/ui/AdminAccounts.svelte.d.ts +19 -0
  351. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -0
  352. package/dist/ui/AdminAuditLog.svelte +144 -0
  353. package/dist/ui/AdminAuditLog.svelte.d.ts +4 -0
  354. package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -0
  355. package/dist/ui/AdminInvites.svelte +142 -0
  356. package/dist/ui/AdminInvites.svelte.d.ts +4 -0
  357. package/dist/ui/AdminInvites.svelte.d.ts.map +1 -0
  358. package/dist/ui/AdminOverview.svelte +337 -0
  359. package/dist/ui/AdminOverview.svelte.d.ts +4 -0
  360. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -0
  361. package/dist/ui/AdminPermitHistory.svelte +61 -0
  362. package/dist/ui/AdminPermitHistory.svelte.d.ts +19 -0
  363. package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -0
  364. package/dist/ui/AdminSessions.svelte +85 -0
  365. package/dist/ui/AdminSessions.svelte.d.ts +19 -0
  366. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -0
  367. package/dist/ui/AdminSettings.svelte +32 -0
  368. package/dist/ui/AdminSettings.svelte.d.ts +19 -0
  369. package/dist/ui/AdminSettings.svelte.d.ts.map +1 -0
  370. package/dist/ui/AdminSurface.svelte +42 -0
  371. package/dist/ui/AdminSurface.svelte.d.ts +4 -0
  372. package/dist/ui/AdminSurface.svelte.d.ts.map +1 -0
  373. package/dist/ui/AppShell.svelte +93 -0
  374. package/dist/ui/AppShell.svelte.d.ts +20 -0
  375. package/dist/ui/AppShell.svelte.d.ts.map +1 -0
  376. package/dist/ui/BootstrapForm.svelte +105 -0
  377. package/dist/ui/BootstrapForm.svelte.d.ts +4 -0
  378. package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -0
  379. package/dist/ui/ColumnLayout.svelte +46 -0
  380. package/dist/ui/ColumnLayout.svelte.d.ts +11 -0
  381. package/dist/ui/ColumnLayout.svelte.d.ts.map +1 -0
  382. package/dist/ui/ConfirmButton.svelte +125 -0
  383. package/dist/ui/ConfirmButton.svelte.d.ts +54 -0
  384. package/dist/ui/ConfirmButton.svelte.d.ts.map +1 -0
  385. package/dist/ui/Datatable.svelte +185 -0
  386. package/dist/ui/Datatable.svelte.d.ts +35 -0
  387. package/dist/ui/Datatable.svelte.d.ts.map +1 -0
  388. package/dist/ui/LoginForm.svelte +82 -0
  389. package/dist/ui/LoginForm.svelte.d.ts +8 -0
  390. package/dist/ui/LoginForm.svelte.d.ts.map +1 -0
  391. package/dist/ui/LogoutButton.svelte +36 -0
  392. package/dist/ui/LogoutButton.svelte.d.ts +10 -0
  393. package/dist/ui/LogoutButton.svelte.d.ts.map +1 -0
  394. package/dist/ui/MenuLink.svelte +35 -0
  395. package/dist/ui/MenuLink.svelte.d.ts +12 -0
  396. package/dist/ui/MenuLink.svelte.d.ts.map +1 -0
  397. package/dist/ui/OpenSignupToggle.svelte +36 -0
  398. package/dist/ui/OpenSignupToggle.svelte.d.ts +19 -0
  399. package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -0
  400. package/dist/ui/PopoverButton.svelte +136 -0
  401. package/dist/ui/PopoverButton.svelte.d.ts +63 -0
  402. package/dist/ui/PopoverButton.svelte.d.ts.map +1 -0
  403. package/dist/ui/SignupForm.svelte +117 -0
  404. package/dist/ui/SignupForm.svelte.d.ts +7 -0
  405. package/dist/ui/SignupForm.svelte.d.ts.map +1 -0
  406. package/dist/ui/SurfaceExplorer.svelte +287 -0
  407. package/dist/ui/SurfaceExplorer.svelte.d.ts +8 -0
  408. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -0
  409. package/dist/ui/account_sessions_state.svelte.d.ts +15 -0
  410. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -0
  411. package/dist/ui/account_sessions_state.svelte.js +45 -0
  412. package/dist/ui/admin_accounts_state.svelte.d.ts +19 -0
  413. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -0
  414. package/dist/ui/admin_accounts_state.svelte.js +65 -0
  415. package/dist/ui/admin_invites_state.svelte.d.ts +19 -0
  416. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -0
  417. package/dist/ui/admin_invites_state.svelte.js +71 -0
  418. package/dist/ui/admin_sessions_state.svelte.d.ts +18 -0
  419. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -0
  420. package/dist/ui/admin_sessions_state.svelte.js +62 -0
  421. package/dist/ui/app_settings_state.svelte.d.ts +14 -0
  422. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -0
  423. package/dist/ui/app_settings_state.svelte.js +44 -0
  424. package/dist/ui/audit_log_state.svelte.d.ts +40 -0
  425. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -0
  426. package/dist/ui/audit_log_state.svelte.js +153 -0
  427. package/dist/ui/auth_state.svelte.d.ts +85 -0
  428. package/dist/ui/auth_state.svelte.d.ts.map +1 -0
  429. package/dist/ui/auth_state.svelte.js +238 -0
  430. package/dist/ui/datatable.d.ts +25 -0
  431. package/dist/ui/datatable.d.ts.map +1 -0
  432. package/dist/ui/datatable.js +9 -0
  433. package/dist/ui/enter_advance.d.ts +13 -0
  434. package/dist/ui/enter_advance.d.ts.map +1 -0
  435. package/dist/ui/enter_advance.js +30 -0
  436. package/dist/ui/loadable.svelte.d.ts +55 -0
  437. package/dist/ui/loadable.svelte.d.ts.map +1 -0
  438. package/dist/ui/loadable.svelte.js +75 -0
  439. package/dist/ui/popover.svelte.d.ts +137 -0
  440. package/dist/ui/popover.svelte.d.ts.map +1 -0
  441. package/dist/ui/popover.svelte.js +288 -0
  442. package/dist/ui/position_helpers.d.ts +27 -0
  443. package/dist/ui/position_helpers.d.ts.map +1 -0
  444. package/dist/ui/position_helpers.js +81 -0
  445. package/dist/ui/sidebar_state.svelte.d.ts +30 -0
  446. package/dist/ui/sidebar_state.svelte.d.ts.map +1 -0
  447. package/dist/ui/sidebar_state.svelte.js +39 -0
  448. package/dist/ui/table_state.svelte.d.ts +63 -0
  449. package/dist/ui/table_state.svelte.d.ts.map +1 -0
  450. package/dist/ui/table_state.svelte.js +117 -0
  451. package/dist/ui/ui_fetch.d.ts +29 -0
  452. package/dist/ui/ui_fetch.d.ts.map +1 -0
  453. package/dist/ui/ui_fetch.js +37 -0
  454. package/dist/ui/ui_format.d.ts +63 -0
  455. package/dist/ui/ui_format.d.ts.map +1 -0
  456. package/dist/ui/ui_format.js +196 -0
  457. package/package.json +121 -0
@@ -0,0 +1,1139 @@
1
+ import './assert_dev_env.js';
2
+ /**
3
+ * Standard integration test suite for fuz_app auth routes.
4
+ *
5
+ * `describe_standard_integration_tests` creates a composable test suite that
6
+ * exercises the full middleware stack (origin, session, bearer_auth, request_context)
7
+ * against a real PGlite database. Consumers call it with their route factory and
8
+ * session config — all auth route tests come for free.
9
+ *
10
+ * Tests use `stub_password_deps` (deterministic hashing, no Argon2 overhead).
11
+ * Login handlers call `verify_password(submitted, stored_hash)` which works because
12
+ * both hash and verify use the same stub logic.
13
+ *
14
+ * Rate limiters are disabled by default — tests make many login attempts and would
15
+ * trigger limits otherwise.
16
+ *
17
+ * @module
18
+ */
19
+ import { describe, test, assert, afterAll } from 'vitest';
20
+ import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
21
+ import { create_test_app } from './app_server.js';
22
+ import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
23
+ import { find_auth_route, assert_response_matches_spec, create_expired_test_cookie, assert_no_error_info_leakage, } from './integration_helpers.js';
24
+ import { RateLimiter } from '../rate_limiter.js';
25
+ import { run_migrations } from '../db/migrate.js';
26
+ import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
27
+ /**
28
+ * Build `CreateTestAppOptions` from standard options plus a database.
29
+ */
30
+ const build_test_app_options = (options, db) => ({
31
+ session_options: options.session_options,
32
+ create_route_specs: options.create_route_specs,
33
+ db,
34
+ app_options: options.app_options,
35
+ });
36
+ /**
37
+ * Standard integration test suite for fuz_app auth routes.
38
+ *
39
+ * Exercises login/logout, cookie attributes, session security, session
40
+ * revocation, password change (incl. API token revocation), origin
41
+ * verification, bearer auth (incl. browser context rejection on mutations),
42
+ * token revocation, cross-account isolation, expired credential rejection,
43
+ * signup invite edge cases, and response body validation.
44
+ *
45
+ * Each test group asserts that required routes exist, failing with a descriptive
46
+ * message if the consumer's route specs are misconfigured.
47
+ *
48
+ * @param options - session config and route factory
49
+ */
50
+ export const describe_standard_integration_tests = (options) => {
51
+ const init_schema = async (db) => {
52
+ await run_migrations(db, [AUTH_MIGRATION_NS]);
53
+ };
54
+ const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
55
+ const describe_db = create_describe_db(factories, AUTH_INTEGRATION_TRUNCATE_TABLES);
56
+ describe_db('standard_integration', (get_db) => {
57
+ const { cookie_name } = options.session_options;
58
+ // Error coverage tracking across test groups
59
+ const error_collector = new ErrorCoverageCollector();
60
+ let captured_route_specs = null;
61
+ afterAll(() => {
62
+ if (captured_route_specs) {
63
+ // Scope coverage to auth-related routes that this suite exercises.
64
+ // Consumer-specific routes (tx runs, state, etc.) are not exercised
65
+ // by the standard suite and would dilute the coverage percentage.
66
+ const auth_suffixes = [
67
+ '/login',
68
+ '/logout',
69
+ '/verify',
70
+ '/sessions',
71
+ '/sessions/revoke-all',
72
+ '/tokens',
73
+ '/tokens/create',
74
+ '/password',
75
+ '/signup',
76
+ '/bootstrap',
77
+ ];
78
+ const auth_routes = captured_route_specs.filter((s) => (auth_suffixes.some((suffix) => s.path.endsWith(suffix)) ||
79
+ s.path.includes('/sessions/:') ||
80
+ s.path.includes('/tokens/:')) &&
81
+ !(s.auth.type === 'role' && s.auth.role === 'admin'));
82
+ assert_error_coverage(error_collector, auth_routes.length > 0 ? auth_routes : captured_route_specs, {
83
+ min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE,
84
+ });
85
+ }
86
+ });
87
+ // --- 1. Login/logout lifecycle ---
88
+ describe('login/logout lifecycle', () => {
89
+ test('login with correct credentials returns 200 with Set-Cookie', async () => {
90
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
91
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
92
+ assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
93
+ const res = await test_app.app.request(login_route.path, {
94
+ method: 'POST',
95
+ headers: {
96
+ host: 'localhost',
97
+ origin: 'http://localhost:5173',
98
+ 'content-type': 'application/json',
99
+ },
100
+ body: JSON.stringify({
101
+ username: test_app.backend.account.username,
102
+ password: 'test-password-123',
103
+ }),
104
+ });
105
+ assert.strictEqual(res.status, 200);
106
+ const body = await res.json();
107
+ assert.strictEqual(body.ok, true);
108
+ const set_cookie = res.headers.get('set-cookie');
109
+ assert.ok(set_cookie, 'Expected Set-Cookie header');
110
+ assert.ok(set_cookie.includes(`${cookie_name}=`), `Expected ${cookie_name} cookie`);
111
+ });
112
+ test('login with wrong password returns 401', async () => {
113
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
114
+ captured_route_specs ??= test_app.route_specs;
115
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
116
+ assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
117
+ const res = await test_app.app.request(login_route.path, {
118
+ method: 'POST',
119
+ headers: {
120
+ host: 'localhost',
121
+ origin: 'http://localhost:5173',
122
+ 'content-type': 'application/json',
123
+ },
124
+ body: JSON.stringify({
125
+ username: test_app.backend.account.username,
126
+ password: 'wrong-password',
127
+ }),
128
+ });
129
+ assert.strictEqual(res.status, 401);
130
+ error_collector.record(test_app.route_specs, 'POST', login_route.path, 401);
131
+ const body = await res.json();
132
+ assert.strictEqual(body.error, 'invalid_credentials');
133
+ });
134
+ test('login with nonexistent user returns 401', async () => {
135
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
136
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
137
+ assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
138
+ const res = await test_app.app.request(login_route.path, {
139
+ method: 'POST',
140
+ headers: {
141
+ host: 'localhost',
142
+ origin: 'http://localhost:5173',
143
+ 'content-type': 'application/json',
144
+ },
145
+ body: JSON.stringify({
146
+ username: 'nonexistent_user',
147
+ password: 'test-password-123',
148
+ }),
149
+ });
150
+ assert.strictEqual(res.status, 401);
151
+ error_collector.record(test_app.route_specs, 'POST', login_route.path, 401);
152
+ const body = await res.json();
153
+ assert.strictEqual(body.error, 'invalid_credentials');
154
+ });
155
+ test('login trims whitespace from username', async () => {
156
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
157
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
158
+ assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
159
+ const res = await test_app.app.request(login_route.path, {
160
+ method: 'POST',
161
+ headers: {
162
+ host: 'localhost',
163
+ origin: 'http://localhost:5173',
164
+ 'content-type': 'application/json',
165
+ },
166
+ body: JSON.stringify({
167
+ username: ` ${test_app.backend.account.username} `,
168
+ password: 'test-password-123',
169
+ }),
170
+ });
171
+ assert.strictEqual(res.status, 200);
172
+ const body = await res.json();
173
+ assert.strictEqual(body.ok, true);
174
+ });
175
+ test('full cycle: login → verify → logout → verify fails', async () => {
176
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
177
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
178
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
179
+ const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
180
+ assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
181
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
182
+ assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
183
+ // Login
184
+ const login_res = await test_app.app.request(login_route.path, {
185
+ method: 'POST',
186
+ headers: {
187
+ host: 'localhost',
188
+ origin: 'http://localhost:5173',
189
+ 'content-type': 'application/json',
190
+ },
191
+ body: JSON.stringify({
192
+ username: test_app.backend.account.username,
193
+ password: 'test-password-123',
194
+ }),
195
+ });
196
+ assert.strictEqual(login_res.status, 200);
197
+ // Extract cookie from Set-Cookie
198
+ const set_cookie = login_res.headers.get('set-cookie');
199
+ assert.ok(set_cookie);
200
+ const cookie_match = new RegExp(`${cookie_name}=([^;]+)`).exec(set_cookie);
201
+ assert.ok(cookie_match?.[1]);
202
+ const login_cookie = cookie_match[1];
203
+ const create_headers = () => ({
204
+ host: 'localhost',
205
+ origin: 'http://localhost:5173',
206
+ cookie: `${cookie_name}=${login_cookie}`,
207
+ });
208
+ // Verify works
209
+ const verify_res = await test_app.app.request(verify_route.path, {
210
+ headers: create_headers(),
211
+ });
212
+ assert.strictEqual(verify_res.status, 200);
213
+ // Logout
214
+ const logout_res = await test_app.app.request(logout_route.path, {
215
+ method: 'POST',
216
+ headers: create_headers(),
217
+ });
218
+ assert.strictEqual(logout_res.status, 200);
219
+ const logout_body = await logout_res.json();
220
+ assert.strictEqual(logout_body.ok, true);
221
+ assert.strictEqual(logout_body.username, test_app.backend.account.username, 'Logout response should include the username');
222
+ // Verify fails after logout (session revoked)
223
+ const verify_after = await test_app.app.request(verify_route.path, {
224
+ headers: create_headers(),
225
+ });
226
+ assert.strictEqual(verify_after.status, 401);
227
+ });
228
+ });
229
+ // --- 1b. Login response body identity (account enumeration prevention) ---
230
+ describe('login response body identity', () => {
231
+ test('nonexistent user and wrong password responses are structurally identical', async () => {
232
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
233
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
234
+ assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
235
+ const make_login = (username, password) => test_app.app.request(login_route.path, {
236
+ method: 'POST',
237
+ headers: {
238
+ host: 'localhost',
239
+ origin: 'http://localhost:5173',
240
+ 'content-type': 'application/json',
241
+ },
242
+ body: JSON.stringify({ username, password }),
243
+ });
244
+ // wrong password for existing user
245
+ const wrong_pw_res = await make_login(test_app.backend.account.username, 'wrong-password-999');
246
+ assert.strictEqual(wrong_pw_res.status, 401);
247
+ const wrong_pw_body = await wrong_pw_res.json();
248
+ // nonexistent user
249
+ const no_user_res = await make_login('nonexistent_user_xyz', 'any-password');
250
+ assert.strictEqual(no_user_res.status, 401);
251
+ const no_user_body = await no_user_res.json();
252
+ // same keys, same error code, no extra fields
253
+ const wrong_pw_keys = Object.keys(wrong_pw_body).sort();
254
+ const no_user_keys = Object.keys(no_user_body).sort();
255
+ assert.deepStrictEqual(wrong_pw_keys, no_user_keys, 'Response keys must be identical to prevent account enumeration');
256
+ assert.strictEqual(wrong_pw_body.error, no_user_body.error, 'Error codes must be identical');
257
+ });
258
+ });
259
+ // --- 2. Cookie attributes ---
260
+ describe('cookie attributes', () => {
261
+ test('session cookie has secure attributes', async () => {
262
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
263
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
264
+ assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
265
+ const res = await test_app.app.request(login_route.path, {
266
+ method: 'POST',
267
+ headers: {
268
+ host: 'localhost',
269
+ origin: 'http://localhost:5173',
270
+ 'content-type': 'application/json',
271
+ },
272
+ body: JSON.stringify({
273
+ username: test_app.backend.account.username,
274
+ password: 'test-password-123',
275
+ }),
276
+ });
277
+ assert.strictEqual(res.status, 200);
278
+ const set_cookie = res.headers.get('set-cookie');
279
+ assert.ok(set_cookie);
280
+ const lower = set_cookie.toLowerCase();
281
+ assert.ok(lower.includes('httponly'), 'Expected HttpOnly');
282
+ assert.ok(lower.includes('samesite=strict'), 'Expected SameSite=Strict');
283
+ assert.ok(lower.includes('secure'), 'Expected Secure');
284
+ assert.ok(lower.includes('path=/'), 'Expected Path=/');
285
+ });
286
+ });
287
+ // --- 3. Session security ---
288
+ describe('session security', () => {
289
+ test('no cookie on protected route returns 401', async () => {
290
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
291
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
292
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
293
+ const res = await test_app.app.request(verify_route.path, {
294
+ headers: { host: 'localhost' },
295
+ });
296
+ assert.strictEqual(res.status, 401);
297
+ error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
298
+ });
299
+ test('corrupted cookie returns 401', async () => {
300
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
301
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
302
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
303
+ const res = await test_app.app.request(verify_route.path, {
304
+ headers: {
305
+ host: 'localhost',
306
+ cookie: `${cookie_name}=random_garbage_value`,
307
+ },
308
+ });
309
+ assert.strictEqual(res.status, 401);
310
+ error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
311
+ });
312
+ test('expired cookie returns 401', async () => {
313
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
314
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
315
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
316
+ const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
317
+ const res = await test_app.app.request(verify_route.path, {
318
+ headers: {
319
+ host: 'localhost',
320
+ cookie: `${cookie_name}=${expired_cookie}`,
321
+ },
322
+ });
323
+ assert.strictEqual(res.status, 401);
324
+ error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
325
+ });
326
+ });
327
+ // --- 4. Session revocation ---
328
+ describe('session revocation', () => {
329
+ test('revoke single session by ID invalidates that session', async () => {
330
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
331
+ const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
332
+ const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
333
+ s.path.endsWith('/sessions/:id/revoke') &&
334
+ s.auth.type === 'authenticated');
335
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
336
+ assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
337
+ assert.ok(revoke_route, 'Expected POST /sessions/:id/revoke route — ensure create_route_specs includes account routes');
338
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
339
+ const headers = test_app.create_session_headers();
340
+ // List own sessions to get the session ID
341
+ const list_res = await test_app.app.request(sessions_route.path, { headers });
342
+ assert.strictEqual(list_res.status, 200);
343
+ const list_body = await list_res.json();
344
+ assert.ok(list_body.sessions.length >= 1);
345
+ const session_id = list_body.sessions[0].id;
346
+ // Revoke that session by ID
347
+ const revoke_path = revoke_route.path.replace(':id', session_id);
348
+ const revoke_res = await test_app.app.request(revoke_path, {
349
+ method: 'POST',
350
+ headers,
351
+ });
352
+ assert.strictEqual(revoke_res.status, 200);
353
+ const revoke_body = await revoke_res.json();
354
+ assert.strictEqual(revoke_body.ok, true);
355
+ assert.strictEqual(revoke_body.revoked, true);
356
+ // Session should no longer work
357
+ const after = await test_app.app.request(verify_route.path, { headers });
358
+ assert.strictEqual(after.status, 401);
359
+ });
360
+ test('revoke-all invalidates existing session', async () => {
361
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
362
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
363
+ const revoke_route = find_auth_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
364
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
365
+ assert.ok(revoke_route, 'Expected POST /sessions/revoke-all route — ensure create_route_specs includes account routes');
366
+ const headers = test_app.create_session_headers();
367
+ // Verify works
368
+ const before = await test_app.app.request(verify_route.path, { headers });
369
+ assert.strictEqual(before.status, 200);
370
+ // Revoke all sessions
371
+ const revoke_res = await test_app.app.request(revoke_route.path, {
372
+ method: 'POST',
373
+ headers,
374
+ });
375
+ assert.strictEqual(revoke_res.status, 200);
376
+ // Verify fails after revocation
377
+ const after = await test_app.app.request(verify_route.path, { headers });
378
+ assert.strictEqual(after.status, 401);
379
+ });
380
+ });
381
+ // --- 4b. Password change ---
382
+ describe('password change', () => {
383
+ test('password change invalidates all sessions and allows login with new password', async () => {
384
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
385
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
386
+ const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
387
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
388
+ assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
389
+ assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
390
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
391
+ const headers = test_app.create_session_headers({
392
+ 'content-type': 'application/json',
393
+ });
394
+ // Change password
395
+ const change_res = await test_app.app.request(password_route.path, {
396
+ method: 'POST',
397
+ headers,
398
+ body: JSON.stringify({
399
+ current_password: 'test-password-123',
400
+ new_password: 'new-password-456',
401
+ }),
402
+ });
403
+ assert.strictEqual(change_res.status, 200);
404
+ const change_body = await change_res.json();
405
+ assert.strictEqual(change_body.ok, true);
406
+ assert.ok(typeof change_body.sessions_revoked === 'number', 'Expected sessions_revoked count');
407
+ assert.ok(change_body.sessions_revoked >= 1, 'Expected at least 1 session revoked');
408
+ // Old session should be invalid
409
+ const verify_after = await test_app.app.request(verify_route.path, {
410
+ headers: test_app.create_session_headers(),
411
+ });
412
+ assert.strictEqual(verify_after.status, 401);
413
+ // Login with new password works
414
+ const login_res = await test_app.app.request(login_route.path, {
415
+ method: 'POST',
416
+ headers: {
417
+ host: 'localhost',
418
+ origin: 'http://localhost:5173',
419
+ 'content-type': 'application/json',
420
+ },
421
+ body: JSON.stringify({
422
+ username: test_app.backend.account.username,
423
+ password: 'new-password-456',
424
+ }),
425
+ });
426
+ assert.strictEqual(login_res.status, 200);
427
+ const login_body = await login_res.json();
428
+ assert.strictEqual(login_body.ok, true);
429
+ });
430
+ test('password change with wrong current password returns 401', async () => {
431
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
432
+ const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
433
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
434
+ assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
435
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
436
+ const res = await test_app.app.request(password_route.path, {
437
+ method: 'POST',
438
+ headers: test_app.create_session_headers({
439
+ 'content-type': 'application/json',
440
+ }),
441
+ body: JSON.stringify({
442
+ current_password: 'wrong-password-999',
443
+ new_password: 'new-password-456',
444
+ }),
445
+ });
446
+ assert.strictEqual(res.status, 401);
447
+ error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
448
+ // Session should still be valid (password didn't change)
449
+ const verify_res = await test_app.app.request(verify_route.path, {
450
+ headers: test_app.create_session_headers(),
451
+ });
452
+ assert.strictEqual(verify_res.status, 200);
453
+ });
454
+ });
455
+ // --- 5. Origin verification ---
456
+ describe('origin verification', () => {
457
+ test('evil origin is rejected with 403', async () => {
458
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
459
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
460
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
461
+ const res = await test_app.app.request(verify_route.path, {
462
+ headers: {
463
+ host: 'localhost',
464
+ origin: 'http://evil.com',
465
+ cookie: `${cookie_name}=${test_app.backend.session_cookie}`,
466
+ },
467
+ });
468
+ assert.strictEqual(res.status, 403);
469
+ const body = await res.json();
470
+ assert.strictEqual(body.error, 'forbidden_origin');
471
+ });
472
+ test('valid origin is accepted', async () => {
473
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
474
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
475
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
476
+ const res = await test_app.app.request(verify_route.path, {
477
+ headers: test_app.create_session_headers(),
478
+ });
479
+ assert.strictEqual(res.status, 200);
480
+ });
481
+ test('no origin header is allowed (direct access)', async () => {
482
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
483
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
484
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
485
+ const res = await test_app.app.request(verify_route.path, {
486
+ headers: {
487
+ host: 'localhost',
488
+ cookie: `${cookie_name}=${test_app.backend.session_cookie}`,
489
+ },
490
+ });
491
+ assert.notStrictEqual(res.status, 403);
492
+ });
493
+ });
494
+ // --- 6. Bearer auth ---
495
+ describe('bearer auth', () => {
496
+ test('valid bearer token authenticates', async () => {
497
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
498
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
499
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
500
+ const res = await test_app.app.request(verify_route.path, {
501
+ headers: test_app.create_bearer_headers(),
502
+ });
503
+ assert.strictEqual(res.status, 200);
504
+ });
505
+ test('invalid bearer token returns 401', async () => {
506
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
507
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
508
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
509
+ const res = await test_app.app.request(verify_route.path, {
510
+ headers: {
511
+ host: 'localhost',
512
+ authorization: 'Bearer secret_fuz_token_invalid',
513
+ },
514
+ });
515
+ assert.strictEqual(res.status, 401);
516
+ error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
517
+ });
518
+ test('bearer token with Origin header is rejected', async () => {
519
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
520
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
521
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
522
+ const bearer_headers = test_app.create_bearer_headers();
523
+ // Without Origin — works
524
+ const ok_res = await test_app.app.request(verify_route.path, {
525
+ headers: bearer_headers,
526
+ });
527
+ assert.strictEqual(ok_res.status, 200);
528
+ // With Origin — rejected (browser context)
529
+ const res = await test_app.app.request(verify_route.path, {
530
+ headers: {
531
+ ...bearer_headers,
532
+ origin: 'http://localhost:5173',
533
+ },
534
+ });
535
+ assert.strictEqual(res.status, 403);
536
+ error_collector.record(test_app.route_specs, 'GET', verify_route.path, 403);
537
+ const body = await res.json();
538
+ assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
539
+ });
540
+ });
541
+ // --- 7. Token revocation ---
542
+ describe('token revocation', () => {
543
+ test('revoked API token returns 401', async () => {
544
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
545
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
546
+ const create_token_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
547
+ const revoke_token_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/tokens/:id/revoke'));
548
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
549
+ assert.ok(create_token_route, 'Expected POST /tokens/create route — ensure create_route_specs includes account routes');
550
+ assert.ok(revoke_token_route, 'Expected POST /tokens/:id/revoke route — ensure create_route_specs includes account routes');
551
+ // Create a new token via the API
552
+ const create_res = await test_app.app.request(create_token_route.path, {
553
+ method: 'POST',
554
+ headers: {
555
+ ...test_app.create_session_headers(),
556
+ 'content-type': 'application/json',
557
+ },
558
+ body: JSON.stringify({ name: 'test-revoke' }),
559
+ });
560
+ assert.strictEqual(create_res.status, 200);
561
+ const { token, id } = (await create_res.json());
562
+ // Verify token works
563
+ const use_res = await test_app.app.request(verify_route.path, {
564
+ headers: { host: 'localhost', authorization: `Bearer ${token}` },
565
+ });
566
+ assert.strictEqual(use_res.status, 200);
567
+ // Revoke via HTTP
568
+ const revoke_path = revoke_token_route.path.replace(':id', id);
569
+ const revoke_res = await test_app.app.request(revoke_path, {
570
+ method: 'POST',
571
+ headers: test_app.create_session_headers(),
572
+ });
573
+ assert.strictEqual(revoke_res.status, 200);
574
+ // Token should no longer work
575
+ const after_res = await test_app.app.request(verify_route.path, {
576
+ headers: { host: 'localhost', authorization: `Bearer ${token}` },
577
+ });
578
+ assert.strictEqual(after_res.status, 401);
579
+ error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
580
+ });
581
+ });
582
+ // --- 8. Cross-account isolation ---
583
+ describe('cross-account isolation', () => {
584
+ test('non-admin cannot access admin routes', async () => {
585
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
586
+ // admin routes are optional in the base suite — admin-specific coverage
587
+ // lives in describe_standard_admin_integration_tests
588
+ const admin_route = test_app.route_specs.find((s) => s.auth.type === 'role' && s.auth.role === 'admin');
589
+ if (!admin_route)
590
+ return;
591
+ const res = await test_app.app.request(admin_route.path, {
592
+ method: admin_route.method,
593
+ headers: test_app.create_session_headers(),
594
+ });
595
+ assert.strictEqual(res.status, 403);
596
+ const body = await res.json();
597
+ assert.strictEqual(body.error, 'insufficient_permissions');
598
+ });
599
+ test("user A cannot revoke user B's sessions", async () => {
600
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
601
+ const revoke_all_route = find_auth_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
602
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
603
+ assert.ok(revoke_all_route, 'Expected POST /sessions/revoke-all route — ensure create_route_specs includes account routes');
604
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
605
+ // Create a second account
606
+ const user_b = await test_app.create_account({ username: 'user_b' });
607
+ // User A revokes all their own sessions
608
+ const revoke_res = await test_app.app.request(revoke_all_route.path, {
609
+ method: 'POST',
610
+ headers: test_app.create_session_headers(),
611
+ });
612
+ assert.strictEqual(revoke_res.status, 200);
613
+ // User B's session should still work
614
+ const verify_b = await test_app.app.request(verify_route.path, {
615
+ headers: {
616
+ host: 'localhost',
617
+ cookie: `${cookie_name}=${user_b.session_cookie}`,
618
+ },
619
+ });
620
+ assert.strictEqual(verify_b.status, 200);
621
+ });
622
+ test("user A cannot revoke user B's session by ID", async () => {
623
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
624
+ const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
625
+ const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
626
+ s.path.endsWith('/sessions/:id/revoke') &&
627
+ s.auth.type === 'authenticated');
628
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
629
+ assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
630
+ assert.ok(revoke_route, 'Expected POST /sessions/:id/revoke route — ensure create_route_specs includes account routes');
631
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
632
+ const user_b = await test_app.create_account({ username: 'user_b' });
633
+ const user_b_headers = {
634
+ host: 'localhost',
635
+ cookie: `${cookie_name}=${user_b.session_cookie}`,
636
+ };
637
+ // Get user B's session ID by listing as user B
638
+ const list_res = await test_app.app.request(sessions_route.path, {
639
+ headers: user_b_headers,
640
+ });
641
+ assert.strictEqual(list_res.status, 200);
642
+ const list_body = await list_res.json();
643
+ assert.ok(list_body.sessions.length >= 1);
644
+ const session_id_b = list_body.sessions[0].id;
645
+ // User A tries to revoke user B's session by ID
646
+ const revoke_path = revoke_route.path.replace(':id', session_id_b);
647
+ const revoke_res = await test_app.app.request(revoke_path, {
648
+ method: 'POST',
649
+ headers: test_app.create_session_headers(),
650
+ });
651
+ assert.strictEqual(revoke_res.status, 200);
652
+ const revoke_body = await revoke_res.json();
653
+ assert.strictEqual(revoke_body.revoked, false, 'Should not revoke another account session');
654
+ // User B's session should still work
655
+ const verify_b = await test_app.app.request(verify_route.path, {
656
+ headers: user_b_headers,
657
+ });
658
+ assert.strictEqual(verify_b.status, 200);
659
+ });
660
+ test("user A cannot revoke user B's token by ID", async () => {
661
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
662
+ const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
663
+ const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
664
+ s.path.endsWith('/tokens/:id/revoke') &&
665
+ s.auth.type === 'authenticated');
666
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
667
+ assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
668
+ assert.ok(revoke_route, 'Expected POST /tokens/:id/revoke route — ensure create_route_specs includes account routes');
669
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
670
+ const user_b = await test_app.create_account({ username: 'user_b' });
671
+ const user_b_headers = {
672
+ host: 'localhost',
673
+ cookie: `${cookie_name}=${user_b.session_cookie}`,
674
+ };
675
+ // Get user B's token ID by listing as user B
676
+ const list_res = await test_app.app.request(tokens_route.path, {
677
+ headers: user_b_headers,
678
+ });
679
+ assert.strictEqual(list_res.status, 200);
680
+ const list_body = await list_res.json();
681
+ assert.ok(list_body.tokens.length >= 1);
682
+ const token_id_b = list_body.tokens[0].id;
683
+ // User A tries to revoke user B's token by ID
684
+ const revoke_path = revoke_route.path.replace(':id', token_id_b);
685
+ const revoke_res = await test_app.app.request(revoke_path, {
686
+ method: 'POST',
687
+ headers: test_app.create_session_headers(),
688
+ });
689
+ assert.strictEqual(revoke_res.status, 200);
690
+ const revoke_body = await revoke_res.json();
691
+ assert.strictEqual(revoke_body.revoked, false, 'Should not revoke another account token');
692
+ // User B's bearer token should still work
693
+ const verify_b = await test_app.app.request(verify_route.path, {
694
+ headers: { host: 'localhost', authorization: `Bearer ${user_b.api_token}` },
695
+ });
696
+ assert.strictEqual(verify_b.status, 200);
697
+ });
698
+ test("user A's session list does not include user B's sessions", async () => {
699
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
700
+ const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
701
+ assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
702
+ const user_b = await test_app.create_account({ username: 'user_b' });
703
+ // User A lists sessions
704
+ const res = await test_app.app.request(sessions_route.path, {
705
+ headers: test_app.create_session_headers(),
706
+ });
707
+ assert.strictEqual(res.status, 200);
708
+ const body = await res.json();
709
+ // Sessions should only belong to user A's account
710
+ for (const session of body.sessions) {
711
+ assert.strictEqual(session.account_id, test_app.backend.account.id, `Session ${session.id} should belong to user A, not user B (${user_b.account.id})`);
712
+ }
713
+ });
714
+ test("user A's token list does not include user B's tokens", async () => {
715
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
716
+ const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
717
+ assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
718
+ const user_b = await test_app.create_account({ username: 'user_b' });
719
+ // User A lists tokens
720
+ const res = await test_app.app.request(tokens_route.path, {
721
+ headers: test_app.create_session_headers(),
722
+ });
723
+ assert.strictEqual(res.status, 200);
724
+ const body = await res.json();
725
+ // Tokens should only belong to user A's account
726
+ for (const token of body.tokens) {
727
+ assert.strictEqual(token.account_id, test_app.backend.account.id, `Token ${token.id} should belong to user A, not user B (${user_b.account.id})`);
728
+ }
729
+ });
730
+ });
731
+ // --- 9. Response body validation ---
732
+ describe('response body validation', () => {
733
+ test('401 response matches declared error schema', async () => {
734
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
735
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
736
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
737
+ const res = await test_app.app.request(verify_route.path, {
738
+ headers: { host: 'localhost' },
739
+ });
740
+ assert.strictEqual(res.status, 401);
741
+ // Should not throw — body matches the declared error schema
742
+ await assert_response_matches_spec(test_app.route_specs, 'GET', verify_route.path, res);
743
+ });
744
+ test('GET /verify 200 response matches output schema', async () => {
745
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
746
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
747
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
748
+ const res = await test_app.app.request(verify_route.path, {
749
+ headers: test_app.create_session_headers(),
750
+ });
751
+ assert.strictEqual(res.status, 200);
752
+ await assert_response_matches_spec(test_app.route_specs, 'GET', verify_route.path, res);
753
+ });
754
+ test('GET /sessions 200 response matches output schema', async () => {
755
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
756
+ const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
757
+ assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
758
+ const res = await test_app.app.request(sessions_route.path, {
759
+ headers: test_app.create_session_headers(),
760
+ });
761
+ assert.strictEqual(res.status, 200);
762
+ await assert_response_matches_spec(test_app.route_specs, 'GET', sessions_route.path, res);
763
+ });
764
+ test('GET /tokens 200 response matches output schema', async () => {
765
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
766
+ const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
767
+ assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
768
+ const res = await test_app.app.request(tokens_route.path, {
769
+ headers: test_app.create_session_headers(),
770
+ });
771
+ assert.strictEqual(res.status, 200);
772
+ await assert_response_matches_spec(test_app.route_specs, 'GET', tokens_route.path, res);
773
+ });
774
+ test('POST /tokens/create 200 response matches output schema', async () => {
775
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
776
+ const create_token_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
777
+ assert.ok(create_token_route, 'Expected POST /tokens/create route — ensure create_route_specs includes account routes');
778
+ const res = await test_app.app.request(create_token_route.path, {
779
+ method: 'POST',
780
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
781
+ body: JSON.stringify({ name: 'schema-test' }),
782
+ });
783
+ assert.strictEqual(res.status, 200);
784
+ await assert_response_matches_spec(test_app.route_specs, 'POST', create_token_route.path, res);
785
+ });
786
+ });
787
+ // --- 10b. Rate limiting smoke test (full middleware stack) ---
788
+ describe('rate limiting smoke test', () => {
789
+ test('rate limiter fires in full middleware stack', async () => {
790
+ const test_app = await create_test_app({
791
+ ...build_test_app_options(options, get_db()),
792
+ app_options: {
793
+ ...options.app_options,
794
+ // tight limiter: 2 attempts / 1 minute
795
+ ip_rate_limiter: new RateLimiter({
796
+ max_attempts: 2,
797
+ window_ms: 60_000,
798
+ cleanup_interval_ms: 0,
799
+ }),
800
+ },
801
+ });
802
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
803
+ if (!login_route)
804
+ return; // skip if login route not wired
805
+ const make_bad_login = (ip_header) => {
806
+ const headers = {
807
+ host: 'localhost',
808
+ origin: 'http://localhost:5173',
809
+ 'content-type': 'application/json',
810
+ };
811
+ if (ip_header) {
812
+ headers['x-forwarded-for'] = ip_header;
813
+ }
814
+ return test_app.app.request(login_route.path, {
815
+ method: 'POST',
816
+ headers,
817
+ body: JSON.stringify({ username: 'nobody', password: 'wrong' }),
818
+ });
819
+ };
820
+ // exhaust the limiter (2 attempts)
821
+ await make_bad_login();
822
+ await make_bad_login();
823
+ // third attempt should be rate-limited
824
+ const limited_res = await make_bad_login();
825
+ assert.strictEqual(limited_res.status, 429, 'Expected 429 after exceeding rate limit');
826
+ error_collector.record(test_app.route_specs, 'POST', login_route.path, 429);
827
+ const limited_body = await limited_res.json();
828
+ assert.strictEqual(limited_body.error, 'rate_limit_exceeded');
829
+ // Retry-After header present
830
+ const retry_after = limited_res.headers.get('Retry-After');
831
+ assert.ok(retry_after, 'Expected Retry-After header on 429 response');
832
+ });
833
+ });
834
+ // --- 10c2. Error coverage: unauthenticated access to auth-required routes ---
835
+ describe('error coverage breadth', () => {
836
+ test('exercises 401 on multiple auth-required routes for error coverage', async () => {
837
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
838
+ // Hit several auth-required routes without credentials to broaden
839
+ // error coverage beyond just /verify and /login
840
+ const route_suffixes = ['/sessions', '/tokens', '/sessions/revoke-all', '/tokens/create'];
841
+ for (const suffix of route_suffixes) {
842
+ const route = find_auth_route(test_app.route_specs, suffix, suffix === '/tokens/create' || suffix === '/sessions/revoke-all' ? 'POST' : 'GET');
843
+ if (!route)
844
+ continue;
845
+ // eslint-disable-next-line no-await-in-loop
846
+ const res = await test_app.app.request(route.path, {
847
+ method: route.method,
848
+ headers: { host: 'localhost' },
849
+ });
850
+ if (res.status === 401) {
851
+ error_collector.record(test_app.route_specs, route.method, route.path, 401);
852
+ }
853
+ }
854
+ // Also exercise POST /logout without auth
855
+ const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
856
+ if (logout_route) {
857
+ const res = await test_app.app.request(logout_route.path, {
858
+ method: 'POST',
859
+ headers: { host: 'localhost' },
860
+ });
861
+ if (res.status === 401) {
862
+ error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
863
+ }
864
+ }
865
+ });
866
+ });
867
+ // --- 10c. Error response information leakage ---
868
+ describe('error response information leakage', () => {
869
+ test('401 responses contain no leaky fields', async () => {
870
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
871
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
872
+ if (!verify_route)
873
+ return;
874
+ const res = await test_app.app.request(verify_route.path, {
875
+ headers: { host: 'localhost' },
876
+ });
877
+ assert.strictEqual(res.status, 401);
878
+ const body = await res.json();
879
+ assert_no_error_info_leakage(body, `GET ${verify_route.path} 401`);
880
+ });
881
+ });
882
+ // --- 11. Expired credential rejection ---
883
+ describe('expired credential rejection', () => {
884
+ test('expired session cookie returns 401', async () => {
885
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
886
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
887
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
888
+ const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
889
+ const res = await test_app.app.request(verify_route.path, {
890
+ headers: {
891
+ host: 'localhost',
892
+ cookie: `${cookie_name}=${expired_cookie}`,
893
+ },
894
+ });
895
+ assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected');
896
+ error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
897
+ });
898
+ test('expired session cookie returns 401 on mutation route', async () => {
899
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
900
+ const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
901
+ assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
902
+ const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
903
+ const res = await test_app.app.request(logout_route.path, {
904
+ method: 'POST',
905
+ headers: {
906
+ host: 'localhost',
907
+ cookie: `${cookie_name}=${expired_cookie}`,
908
+ },
909
+ });
910
+ assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected on POST');
911
+ error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
912
+ });
913
+ });
914
+ // --- 12. Bearer token browser context on mutation routes ---
915
+ describe('bearer token browser context rejection on mutations', () => {
916
+ test('bearer token with Origin header rejected on POST logout', async () => {
917
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
918
+ const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
919
+ assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
920
+ const bearer_headers = test_app.create_bearer_headers({
921
+ 'content-type': 'application/json',
922
+ });
923
+ const res = await test_app.app.request(logout_route.path, {
924
+ method: 'POST',
925
+ headers: { ...bearer_headers, origin: 'http://localhost:5173' },
926
+ });
927
+ assert.strictEqual(res.status, 403, 'Bearer with Origin should be rejected on mutation');
928
+ const body = await res.json();
929
+ assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
930
+ error_collector.record(test_app.route_specs, 'POST', logout_route.path, 403);
931
+ });
932
+ test('bearer token with Referer header rejected on POST password', async () => {
933
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
934
+ const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
935
+ assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
936
+ const bearer_headers = test_app.create_bearer_headers({
937
+ 'content-type': 'application/json',
938
+ });
939
+ const res = await test_app.app.request(password_route.path, {
940
+ method: 'POST',
941
+ headers: { ...bearer_headers, referer: 'http://localhost:5173/admin' },
942
+ });
943
+ assert.strictEqual(res.status, 403, 'Bearer with Referer should be rejected on mutation');
944
+ const body = await res.json();
945
+ assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
946
+ error_collector.record(test_app.route_specs, 'POST', password_route.path, 403);
947
+ });
948
+ });
949
+ // --- 13. Password change revokes API tokens ---
950
+ describe('password change revokes API tokens', () => {
951
+ test('API tokens are invalidated after password change', async () => {
952
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
953
+ const token_create_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
954
+ const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
955
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
956
+ assert.ok(token_create_route, 'Expected POST /tokens/create route');
957
+ assert.ok(password_route, 'Expected POST /password route');
958
+ assert.ok(verify_route, 'Expected GET /verify route');
959
+ // Create an API token
960
+ const create_res = await test_app.app.request(token_create_route.path, {
961
+ method: 'POST',
962
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
963
+ body: JSON.stringify({ name: 'test-token' }),
964
+ });
965
+ assert.strictEqual(create_res.status, 200);
966
+ const { token: raw_token } = await create_res.json();
967
+ assert.ok(raw_token, 'Expected raw token in create response');
968
+ // Verify bearer token works
969
+ const verify_before = await test_app.app.request(verify_route.path, {
970
+ headers: { host: 'localhost', authorization: `Bearer ${raw_token}` },
971
+ });
972
+ assert.strictEqual(verify_before.status, 200, 'Bearer token should work before password change');
973
+ // Change password
974
+ const change_res = await test_app.app.request(password_route.path, {
975
+ method: 'POST',
976
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
977
+ body: JSON.stringify({
978
+ current_password: 'test-password-123',
979
+ new_password: 'new-password-456',
980
+ }),
981
+ });
982
+ assert.strictEqual(change_res.status, 200);
983
+ const change_body = await change_res.json();
984
+ assert.ok(typeof change_body.tokens_revoked === 'number', 'Expected tokens_revoked count');
985
+ assert.ok(change_body.tokens_revoked >= 1, 'Expected at least 1 token revoked');
986
+ // Bearer token should now be invalid
987
+ const verify_after = await test_app.app.request(verify_route.path, {
988
+ headers: { host: 'localhost', authorization: `Bearer ${raw_token}` },
989
+ });
990
+ assert.strictEqual(verify_after.status, 401, 'Bearer token should be rejected after password change');
991
+ });
992
+ });
993
+ // --- 14. Signup invite edge cases ---
994
+ describe('signup invite edge cases', () => {
995
+ test('signup with non-matching email cannot claim another email invite', async () => {
996
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
997
+ const signup_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && s.auth.type === 'none');
998
+ if (!signup_route)
999
+ return; // signup is optional
1000
+ const invite_route = test_app.route_specs.find((s) => s.method === 'POST' &&
1001
+ s.path.endsWith('/invites') &&
1002
+ s.auth.type === 'role' &&
1003
+ s.auth.role === 'admin');
1004
+ if (!invite_route)
1005
+ return; // invite routes are optional
1006
+ // Create an admin to manage invites
1007
+ const admin = await test_app.create_account({
1008
+ username: 'invite_edge_admin',
1009
+ roles: ['admin'],
1010
+ });
1011
+ const admin_headers = {
1012
+ host: 'localhost',
1013
+ origin: 'http://localhost:5173',
1014
+ cookie: `${cookie_name}=${admin.session_cookie}`,
1015
+ 'content-type': 'application/json',
1016
+ };
1017
+ // Create invite for alice@example.com
1018
+ const invite_res = await test_app.app.request(invite_route.path, {
1019
+ method: 'POST',
1020
+ headers: admin_headers,
1021
+ body: JSON.stringify({ email: 'alice@example.com' }),
1022
+ });
1023
+ assert.strictEqual(invite_res.status, 200);
1024
+ // Try to sign up with a different email — should fail (no matching invite)
1025
+ const signup_res = await test_app.app.request(signup_route.path, {
1026
+ method: 'POST',
1027
+ headers: {
1028
+ host: 'localhost',
1029
+ origin: 'http://localhost:5173',
1030
+ 'content-type': 'application/json',
1031
+ },
1032
+ body: JSON.stringify({
1033
+ username: 'eve_attacker',
1034
+ password: 'test-password-123456',
1035
+ email: 'eve@attacker.com',
1036
+ }),
1037
+ });
1038
+ assert.strictEqual(signup_res.status, 403, 'Signup with non-matching email should be rejected');
1039
+ const body = await signup_res.json();
1040
+ assert.strictEqual(body.error, 'no_matching_invite');
1041
+ });
1042
+ });
1043
+ // --- 15. Signup response body identity ---
1044
+ describe('signup response body identity', () => {
1045
+ test('no-invite and conflict failure responses are structurally identical', async () => {
1046
+ const test_app = await create_test_app(build_test_app_options(options, get_db()));
1047
+ // Find signup route (POST ending in /signup, public)
1048
+ const signup_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && s.auth.type === 'none');
1049
+ if (!signup_route)
1050
+ return; // signup is optional
1051
+ // Find admin invite creation route (POST ending in /invites, admin-gated)
1052
+ const invite_route = test_app.route_specs.find((s) => s.method === 'POST' &&
1053
+ s.path.endsWith('/invites') &&
1054
+ s.auth.type === 'role' &&
1055
+ s.auth.role === 'admin');
1056
+ if (!invite_route)
1057
+ return; // invite routes are optional
1058
+ // Find admin accounts route to get admin's account ID
1059
+ const accounts_route = test_app.route_specs.find((s) => s.method === 'GET' &&
1060
+ s.path.endsWith('/accounts') &&
1061
+ s.auth.type === 'role' &&
1062
+ s.auth.role === 'admin');
1063
+ if (!accounts_route)
1064
+ return;
1065
+ // We need admin access — create an admin account
1066
+ const admin = await test_app.create_account({
1067
+ username: 'signup_test_admin',
1068
+ roles: ['admin'],
1069
+ });
1070
+ const admin_headers = {
1071
+ host: 'localhost',
1072
+ origin: 'http://localhost:5173',
1073
+ cookie: `${cookie_name}=${admin.session_cookie}`,
1074
+ 'content-type': 'application/json',
1075
+ };
1076
+ // Create an invite for a specific test email
1077
+ const test_email = 'signup-test@example.com';
1078
+ const invite_res = await test_app.app.request(invite_route.path, {
1079
+ method: 'POST',
1080
+ headers: admin_headers,
1081
+ body: JSON.stringify({ email: test_email }),
1082
+ });
1083
+ assert.strictEqual(invite_res.status, 200, 'Expected invite creation to succeed');
1084
+ // Attempt 1: signup with a non-matching email (no invite match) → 403
1085
+ const no_match_res = await test_app.app.request(signup_route.path, {
1086
+ method: 'POST',
1087
+ headers: {
1088
+ host: 'localhost',
1089
+ origin: 'http://localhost:5173',
1090
+ 'content-type': 'application/json',
1091
+ },
1092
+ body: JSON.stringify({
1093
+ username: 'nomatch_user',
1094
+ password: 'test-password-123456',
1095
+ email: 'wrong-email@example.com',
1096
+ }),
1097
+ });
1098
+ assert.strictEqual(no_match_res.status, 403, 'Expected 403 for non-matching invite');
1099
+ const no_match_body = await no_match_res.json();
1100
+ // For conflict test: create a second account with a known username,
1101
+ // then create an invite for a different email, then try signup with
1102
+ // the invited email but the colliding username
1103
+ const existing_user = await test_app.create_account({ username: 'existing_user' });
1104
+ // Create invite for a different email
1105
+ const conflict_email = 'conflict-test@example.com';
1106
+ const invite2_res = await test_app.app.request(invite_route.path, {
1107
+ method: 'POST',
1108
+ headers: admin_headers,
1109
+ body: JSON.stringify({ email: conflict_email }),
1110
+ });
1111
+ assert.strictEqual(invite2_res.status, 200, 'Expected second invite creation to succeed');
1112
+ // Attempt 2: signup with the invited email but a colliding username → 409
1113
+ const conflict_res = await test_app.app.request(signup_route.path, {
1114
+ method: 'POST',
1115
+ headers: {
1116
+ host: 'localhost',
1117
+ origin: 'http://localhost:5173',
1118
+ 'content-type': 'application/json',
1119
+ },
1120
+ body: JSON.stringify({
1121
+ username: existing_user.account.username,
1122
+ password: 'test-password-123456',
1123
+ email: conflict_email,
1124
+ }),
1125
+ });
1126
+ assert.strictEqual(conflict_res.status, 409, 'Expected 409 for username conflict');
1127
+ const conflict_body = await conflict_res.json();
1128
+ // Assert both failure responses have identical Object.keys()
1129
+ const no_match_keys = Object.keys(no_match_body).sort();
1130
+ const conflict_keys = Object.keys(conflict_body).sort();
1131
+ assert.deepStrictEqual(no_match_keys, conflict_keys, 'Response keys must be identical — no extra fields should reveal ' +
1132
+ 'whether the failure was "no invite" vs "conflict"');
1133
+ // Assert both use documented generic error codes with no field-level detail
1134
+ assert.strictEqual(no_match_body.error, 'no_matching_invite', 'Expected generic no_matching_invite error code');
1135
+ assert.strictEqual(conflict_body.error, 'signup_conflict', 'Expected generic signup_conflict error code');
1136
+ });
1137
+ });
1138
+ });
1139
+ };