@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,840 @@
1
+ import './assert_dev_env.js';
2
+ /**
3
+ * Standard admin integration test suite for fuz_app admin routes.
4
+ *
5
+ * `describe_standard_admin_integration_tests` creates a composable test suite
6
+ * that exercises admin account listing, permit grant/revoke, session/token
7
+ * management, and audit log routes against a real PGlite database.
8
+ *
9
+ * Consumers call it with their route factory, session config, and role schema —
10
+ * all admin route tests come for free.
11
+ *
12
+ * @module
13
+ */
14
+ import { describe, test, assert, afterAll } from 'vitest';
15
+ import { ROLE_KEEPER, ROLE_ADMIN } from '../auth/role_schema.js';
16
+ import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
17
+ import { create_test_app } from './app_server.js';
18
+ import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
19
+ import { find_auth_route, assert_response_matches_spec } from './integration_helpers.js';
20
+ import { run_migrations } from '../db/migrate.js';
21
+ import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
22
+ /**
23
+ * Find an admin route by suffix, method, and role requirement.
24
+ *
25
+ * Disambiguates admin routes (e.g., `GET /admin/sessions`) from account-scoped
26
+ * routes (e.g., `GET /account/sessions`) by checking `auth.type === 'role'`.
27
+ */
28
+ const find_admin_route = (specs, suffix, method) => specs.find((s) => s.method === method &&
29
+ s.path.endsWith(suffix) &&
30
+ s.auth.type === 'role' &&
31
+ s.auth.role === 'admin');
32
+ /**
33
+ * Pick a web-grantable role for testing, preferring a non-admin app-defined role.
34
+ */
35
+ const pick_grantable_role = (role_options) => {
36
+ for (const [name, opts] of role_options) {
37
+ if (opts.web_grantable && name !== ROLE_ADMIN)
38
+ return name;
39
+ }
40
+ return ROLE_ADMIN; // fallback
41
+ };
42
+ /**
43
+ * Build `CreateTestAppOptions` from admin test options plus a database and roles.
44
+ */
45
+ const build_admin_test_app_options = (options, db, roles) => ({
46
+ session_options: options.session_options,
47
+ create_route_specs: options.create_route_specs,
48
+ db,
49
+ roles: roles ?? [ROLE_KEEPER, ROLE_ADMIN],
50
+ app_options: options.app_options,
51
+ });
52
+ /**
53
+ * Standard admin integration test suite for fuz_app admin routes.
54
+ *
55
+ * Exercises account listing, permit grant/revoke, session management, token
56
+ * management, audit log routes, admin-to-admin isolation, and response
57
+ * schema validation.
58
+ *
59
+ * Each test group asserts that required routes exist, failing with a descriptive
60
+ * message if the consumer's route specs are misconfigured.
61
+ *
62
+ * @param options - session config, route factory, and role schema
63
+ */
64
+ export const describe_standard_admin_integration_tests = (options) => {
65
+ const init_schema = async (db) => {
66
+ await run_migrations(db, [AUTH_MIGRATION_NS]);
67
+ };
68
+ const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
69
+ const describe_db = create_describe_db(factories, AUTH_INTEGRATION_TRUNCATE_TABLES);
70
+ describe_db('standard_admin_integration', (get_db) => {
71
+ const { cookie_name } = options.session_options;
72
+ const { role_options } = options.roles;
73
+ const grantable_role = pick_grantable_role(role_options);
74
+ // Error coverage tracking across test groups
75
+ const error_collector = new ErrorCoverageCollector();
76
+ let captured_route_specs = null;
77
+ afterAll(() => {
78
+ if (captured_route_specs) {
79
+ // Scope coverage to admin auth-related routes.
80
+ const admin_suffixes = [
81
+ '/accounts',
82
+ '/permits/grant',
83
+ '/sessions',
84
+ '/sessions/revoke-all',
85
+ '/tokens/revoke-all',
86
+ '/audit-log',
87
+ '/audit-log/permit-history',
88
+ '/invites',
89
+ ];
90
+ const admin_routes = captured_route_specs.filter((s) => (admin_suffixes.some((suffix) => s.path.endsWith(suffix)) ||
91
+ s.path.includes('/permits/:') ||
92
+ s.path.includes('/invites/:')) &&
93
+ s.auth.type === 'role' &&
94
+ s.auth.role === 'admin');
95
+ assert_error_coverage(error_collector, admin_routes.length > 0 ? admin_routes : captured_route_specs, { min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE });
96
+ }
97
+ });
98
+ /** Make request headers for a given session cookie. */
99
+ const create_headers = (session_cookie, extra) => ({
100
+ host: 'localhost',
101
+ origin: 'http://localhost:5173',
102
+ cookie: `${cookie_name}=${session_cookie}`,
103
+ ...extra,
104
+ });
105
+ // --- 1. Admin account listing ---
106
+ describe('admin account listing', () => {
107
+ test('admin can list all accounts', async () => {
108
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
109
+ const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
110
+ assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
111
+ const user_two = await test_app.create_account({ username: 'user_two' });
112
+ const res = await test_app.app.request(accounts_route.path, {
113
+ headers: test_app.create_session_headers(),
114
+ });
115
+ assert.strictEqual(res.status, 200);
116
+ const body = await res.json();
117
+ assert.ok(Array.isArray(body.accounts), 'Expected accounts array');
118
+ assert.ok(body.accounts.length >= 2, 'Expected at least 2 accounts');
119
+ assert.ok(Array.isArray(body.grantable_roles), 'Expected grantable_roles array');
120
+ // Verify user_two appears in the listing
121
+ const found = body.accounts.find((e) => e.account.id === user_two.account.id);
122
+ assert.ok(found, 'Expected user_two in accounts listing');
123
+ });
124
+ test('non-admin cannot list accounts', async () => {
125
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db(), [ROLE_KEEPER]));
126
+ captured_route_specs ??= test_app.route_specs;
127
+ const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
128
+ assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
129
+ const res = await test_app.app.request(accounts_route.path, {
130
+ headers: test_app.create_session_headers(),
131
+ });
132
+ assert.strictEqual(res.status, 403);
133
+ error_collector.record(test_app.route_specs, 'GET', accounts_route.path, 403);
134
+ const body = await res.json();
135
+ assert.strictEqual(body.error, 'insufficient_permissions');
136
+ });
137
+ });
138
+ // --- 2. Permit grant lifecycle ---
139
+ describe('permit grant lifecycle', () => {
140
+ test('admin can grant a web-grantable role', async () => {
141
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
142
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
143
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
144
+ const user_two = await test_app.create_account({ username: 'user_two' });
145
+ const path = grant_route.path.replace(':account_id', user_two.account.id);
146
+ const res = await test_app.app.request(path, {
147
+ method: 'POST',
148
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
149
+ body: JSON.stringify({ role: grantable_role }),
150
+ });
151
+ assert.strictEqual(res.status, 200);
152
+ const body = await res.json();
153
+ assert.strictEqual(body.ok, true);
154
+ assert.ok(body.permit);
155
+ assert.strictEqual(body.permit.role, grantable_role);
156
+ assert.ok(body.permit.id, 'Expected permit id');
157
+ });
158
+ test('admin cannot grant a non-web-grantable role', async () => {
159
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
160
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
161
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
162
+ const user_two = await test_app.create_account({ username: 'user_two' });
163
+ const path = grant_route.path.replace(':account_id', user_two.account.id);
164
+ const res = await test_app.app.request(path, {
165
+ method: 'POST',
166
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
167
+ body: JSON.stringify({ role: ROLE_KEEPER }),
168
+ });
169
+ assert.strictEqual(res.status, 403);
170
+ error_collector.record(test_app.route_specs, 'POST', grant_route.path, 403);
171
+ const body = await res.json();
172
+ assert.strictEqual(body.error, 'role_not_web_grantable');
173
+ });
174
+ test('granting same role twice is idempotent (returns same permit)', async () => {
175
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
176
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
177
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
178
+ const user_two = await test_app.create_account({ username: 'user_two' });
179
+ const path = grant_route.path.replace(':account_id', user_two.account.id);
180
+ const headers = test_app.create_session_headers({ 'content-type': 'application/json' });
181
+ const body = JSON.stringify({ role: grantable_role });
182
+ // First grant
183
+ const res1 = await test_app.app.request(path, {
184
+ method: 'POST',
185
+ headers,
186
+ body,
187
+ });
188
+ assert.strictEqual(res1.status, 200);
189
+ const body1 = await res1.json();
190
+ assert.strictEqual(body1.ok, true);
191
+ const permit_id_1 = body1.permit.id;
192
+ // Second grant — same role, same account
193
+ const res2 = await test_app.app.request(path, {
194
+ method: 'POST',
195
+ headers,
196
+ body,
197
+ });
198
+ assert.strictEqual(res2.status, 200);
199
+ const body2 = await res2.json();
200
+ assert.strictEqual(body2.ok, true);
201
+ assert.strictEqual(body2.permit.id, permit_id_1, 'Expected same permit ID on idempotent grant');
202
+ assert.strictEqual(body2.permit.role, grantable_role);
203
+ });
204
+ test('grant with unknown role returns 400', async () => {
205
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
206
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
207
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
208
+ const user_two = await test_app.create_account({ username: 'user_two' });
209
+ const path = grant_route.path.replace(':account_id', user_two.account.id);
210
+ const res = await test_app.app.request(path, {
211
+ method: 'POST',
212
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
213
+ body: JSON.stringify({ role: 'nonexistent_role' }),
214
+ });
215
+ assert.strictEqual(res.status, 400);
216
+ error_collector.record(test_app.route_specs, 'POST', grant_route.path, 400);
217
+ });
218
+ test('grant to nonexistent account returns 404', async () => {
219
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
220
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
221
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
222
+ const fake_id = '00000000-0000-0000-0000-000000000000';
223
+ const path = grant_route.path.replace(':account_id', fake_id);
224
+ const res = await test_app.app.request(path, {
225
+ method: 'POST',
226
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
227
+ body: JSON.stringify({ role: grantable_role }),
228
+ });
229
+ assert.strictEqual(res.status, 404);
230
+ error_collector.record(test_app.route_specs, 'POST', grant_route.path, 404);
231
+ const body = await res.json();
232
+ assert.strictEqual(body.error, 'account_not_found');
233
+ });
234
+ test('admin can revoke a permit', async () => {
235
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
236
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
237
+ const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
238
+ const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
239
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
240
+ assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
241
+ assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
242
+ const user_two = await test_app.create_account({ username: 'user_two' });
243
+ const admin_headers = test_app.create_session_headers({ 'content-type': 'application/json' });
244
+ // Grant
245
+ const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
246
+ await test_app.app.request(grant_path, {
247
+ method: 'POST',
248
+ headers: admin_headers,
249
+ body: JSON.stringify({ role: grantable_role }),
250
+ });
251
+ // Find the permit ID via account listing
252
+ const list_res = await test_app.app.request(accounts_route.path, {
253
+ headers: test_app.create_session_headers(),
254
+ });
255
+ const list_body = await list_res.json();
256
+ const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
257
+ const permit = entry.permits.find((p) => p.role === grantable_role);
258
+ assert.ok(permit, 'Expected granted permit in listing');
259
+ // Revoke
260
+ const revoke_path = revoke_route.path
261
+ .replace(':account_id', user_two.account.id)
262
+ .replace(':permit_id', permit.id);
263
+ const revoke_res = await test_app.app.request(revoke_path, {
264
+ method: 'POST',
265
+ headers: test_app.create_session_headers(),
266
+ });
267
+ assert.strictEqual(revoke_res.status, 200);
268
+ const revoke_body = await revoke_res.json();
269
+ assert.strictEqual(revoke_body.ok, true);
270
+ assert.strictEqual(revoke_body.revoked, true);
271
+ });
272
+ test('revoking an already-revoked permit returns 404', async () => {
273
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
274
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
275
+ const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
276
+ const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
277
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
278
+ assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
279
+ assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
280
+ const user_two = await test_app.create_account({ username: 'user_two' });
281
+ const admin_headers = test_app.create_session_headers({ 'content-type': 'application/json' });
282
+ // Grant
283
+ const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
284
+ await test_app.app.request(grant_path, {
285
+ method: 'POST',
286
+ headers: admin_headers,
287
+ body: JSON.stringify({ role: grantable_role }),
288
+ });
289
+ // Find permit ID
290
+ const list_res = await test_app.app.request(accounts_route.path, {
291
+ headers: test_app.create_session_headers(),
292
+ });
293
+ const list_body = await list_res.json();
294
+ const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
295
+ const permit = entry.permits.find((p) => p.role === grantable_role);
296
+ assert.ok(permit);
297
+ const revoke_path = revoke_route.path
298
+ .replace(':account_id', user_two.account.id)
299
+ .replace(':permit_id', permit.id);
300
+ // First revoke — succeeds
301
+ const first = await test_app.app.request(revoke_path, {
302
+ method: 'POST',
303
+ headers: test_app.create_session_headers(),
304
+ });
305
+ assert.strictEqual(first.status, 200);
306
+ // Second revoke — already revoked, returns 404
307
+ const second = await test_app.app.request(revoke_path, {
308
+ method: 'POST',
309
+ headers: test_app.create_session_headers(),
310
+ });
311
+ assert.strictEqual(second.status, 404);
312
+ error_collector.record(test_app.route_specs, 'POST', revoke_route.path, 404);
313
+ const body = await second.json();
314
+ assert.strictEqual(body.error, 'permit_not_found');
315
+ });
316
+ });
317
+ // --- 3. Admin session management ---
318
+ describe('admin session management', () => {
319
+ test('admin can list all active sessions', async () => {
320
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
321
+ const sessions_route = find_admin_route(test_app.route_specs, '/sessions', 'GET');
322
+ assert.ok(sessions_route, 'Expected admin GET /sessions route — ensure create_route_specs includes admin routes');
323
+ await test_app.create_account({ username: 'user_two' });
324
+ const res = await test_app.app.request(sessions_route.path, {
325
+ headers: test_app.create_session_headers(),
326
+ });
327
+ assert.strictEqual(res.status, 200);
328
+ const body = await res.json();
329
+ assert.ok(Array.isArray(body.sessions), 'Expected sessions array');
330
+ assert.ok(body.sessions.length >= 2, 'Expected sessions from multiple accounts');
331
+ });
332
+ test('admin can revoke all sessions for another account', async () => {
333
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
334
+ const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
335
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
336
+ assert.ok(revoke_sessions_route, 'Expected admin POST /sessions/revoke-all route — ensure create_route_specs includes admin routes');
337
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
338
+ const user_two = await test_app.create_account({ username: 'user_two' });
339
+ // Verify user_two's session works
340
+ const before = await test_app.app.request(verify_route.path, {
341
+ headers: create_headers(user_two.session_cookie),
342
+ });
343
+ assert.strictEqual(before.status, 200);
344
+ // Admin revokes all sessions for user_two
345
+ const path = revoke_sessions_route.path.replace(':account_id', user_two.account.id);
346
+ const res = await test_app.app.request(path, {
347
+ method: 'POST',
348
+ headers: test_app.create_session_headers(),
349
+ });
350
+ assert.strictEqual(res.status, 200);
351
+ const body = await res.json();
352
+ assert.strictEqual(body.ok, true);
353
+ assert.ok(body.count >= 1, 'Expected at least 1 revoked session');
354
+ // Verify user_two's session no longer works
355
+ const after = await test_app.app.request(verify_route.path, {
356
+ headers: create_headers(user_two.session_cookie),
357
+ });
358
+ assert.strictEqual(after.status, 401);
359
+ });
360
+ test('admin revoking own sessions invalidates own session', async () => {
361
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
362
+ const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
363
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
364
+ assert.ok(revoke_sessions_route, 'Expected admin POST /sessions/revoke-all route — ensure create_route_specs includes admin routes');
365
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
366
+ // Admin revokes own sessions
367
+ const path = revoke_sessions_route.path.replace(':account_id', test_app.backend.account.id);
368
+ const res = await test_app.app.request(path, {
369
+ method: 'POST',
370
+ headers: test_app.create_session_headers(),
371
+ });
372
+ assert.strictEqual(res.status, 200);
373
+ const body = await res.json();
374
+ assert.strictEqual(body.ok, true);
375
+ assert.ok(body.count >= 1, 'Expected at least 1 revoked session');
376
+ // Admin's own session should no longer work
377
+ const after = await test_app.app.request(verify_route.path, {
378
+ headers: test_app.create_session_headers(),
379
+ });
380
+ assert.strictEqual(after.status, 401);
381
+ });
382
+ });
383
+ // --- 4. Admin token management ---
384
+ describe('admin token management', () => {
385
+ test('admin can revoke all tokens for another account', async () => {
386
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
387
+ const revoke_tokens_route = find_admin_route(test_app.route_specs, '/tokens/revoke-all', 'POST');
388
+ const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
389
+ assert.ok(revoke_tokens_route, 'Expected admin POST /tokens/revoke-all route — ensure create_route_specs includes admin routes');
390
+ assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
391
+ const user_two = await test_app.create_account({ username: 'user_two' });
392
+ // Verify user_two's bearer token works
393
+ const before = await test_app.app.request(verify_route.path, {
394
+ headers: { host: 'localhost', authorization: `Bearer ${user_two.api_token}` },
395
+ });
396
+ assert.strictEqual(before.status, 200);
397
+ // Admin revokes all tokens for user_two
398
+ const path = revoke_tokens_route.path.replace(':account_id', user_two.account.id);
399
+ const res = await test_app.app.request(path, {
400
+ method: 'POST',
401
+ headers: test_app.create_session_headers(),
402
+ });
403
+ assert.strictEqual(res.status, 200);
404
+ const body = await res.json();
405
+ assert.strictEqual(body.ok, true);
406
+ assert.ok(body.count >= 1, 'Expected at least 1 revoked token');
407
+ // Verify user_two's bearer token no longer works
408
+ const after = await test_app.app.request(verify_route.path, {
409
+ headers: { host: 'localhost', authorization: `Bearer ${user_two.api_token}` },
410
+ });
411
+ assert.strictEqual(after.status, 401);
412
+ });
413
+ });
414
+ // --- 5. Audit log routes ---
415
+ describe('audit log routes', () => {
416
+ test('admin can list audit log events', async () => {
417
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
418
+ const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
419
+ assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
420
+ const res = await test_app.app.request(audit_route.path, {
421
+ headers: test_app.create_session_headers(),
422
+ });
423
+ assert.strictEqual(res.status, 200);
424
+ const body = await res.json();
425
+ assert.ok(Array.isArray(body.events), 'Expected events array');
426
+ });
427
+ test('audit log supports event_type filter', async () => {
428
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
429
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
430
+ const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
431
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
432
+ assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
433
+ // Create a grant to produce an audit event
434
+ const user_two = await test_app.create_account({ username: 'user_two' });
435
+ const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
436
+ await test_app.app.request(grant_path, {
437
+ method: 'POST',
438
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
439
+ body: JSON.stringify({ role: grantable_role }),
440
+ });
441
+ // Filter by event_type
442
+ const res = await test_app.app.request(`${audit_route.path}?event_type=permit_grant`, {
443
+ headers: test_app.create_session_headers(),
444
+ });
445
+ assert.strictEqual(res.status, 200);
446
+ const body = await res.json();
447
+ assert.ok(Array.isArray(body.events));
448
+ assert.ok(body.events.length >= 1, 'Expected at least 1 permit_grant event');
449
+ for (const event of body.events) {
450
+ assert.strictEqual(event.event_type, 'permit_grant');
451
+ }
452
+ });
453
+ test('admin can view permit history', async () => {
454
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
455
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
456
+ const history_route = find_admin_route(test_app.route_specs, '/audit-log/permit-history', 'GET');
457
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
458
+ assert.ok(history_route, 'Expected admin GET /audit-log/permit-history route — ensure create_route_specs includes admin routes');
459
+ // Create a grant to produce audit data
460
+ const user_two = await test_app.create_account({ username: 'user_two' });
461
+ const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
462
+ await test_app.app.request(grant_path, {
463
+ method: 'POST',
464
+ headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
465
+ body: JSON.stringify({ role: grantable_role }),
466
+ });
467
+ const res = await test_app.app.request(history_route.path, {
468
+ headers: test_app.create_session_headers(),
469
+ });
470
+ assert.strictEqual(res.status, 200);
471
+ const body = await res.json();
472
+ assert.ok(Array.isArray(body.events), 'Expected events array');
473
+ assert.ok(body.events.length >= 1, 'Expected at least 1 permit history event');
474
+ });
475
+ });
476
+ // --- 6. Admin audit trail ---
477
+ describe('admin audit trail', () => {
478
+ test('permit revoke creates audit event', async () => {
479
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
480
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
481
+ const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
482
+ const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
483
+ const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
484
+ assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
485
+ assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
486
+ assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
487
+ assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
488
+ const user_two = await test_app.create_account({ username: 'user_two' });
489
+ const admin_headers = test_app.create_session_headers({ 'content-type': 'application/json' });
490
+ // Grant a role
491
+ const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
492
+ await test_app.app.request(grant_path, {
493
+ method: 'POST',
494
+ headers: admin_headers,
495
+ body: JSON.stringify({ role: grantable_role }),
496
+ });
497
+ // Find the permit ID
498
+ const list_res = await test_app.app.request(accounts_route.path, {
499
+ headers: test_app.create_session_headers(),
500
+ });
501
+ const list_body = await list_res.json();
502
+ const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
503
+ const permit = entry.permits.find((p) => p.role === grantable_role);
504
+ // Revoke the permit
505
+ const revoke_path = revoke_route.path
506
+ .replace(':account_id', user_two.account.id)
507
+ .replace(':permit_id', permit.id);
508
+ await test_app.app.request(revoke_path, {
509
+ method: 'POST',
510
+ headers: test_app.create_session_headers(),
511
+ });
512
+ // Check audit log for permit_revoke event
513
+ const audit_res = await test_app.app.request(`${audit_route.path}?event_type=permit_revoke`, { headers: test_app.create_session_headers() });
514
+ assert.strictEqual(audit_res.status, 200);
515
+ const audit_body = await audit_res.json();
516
+ assert.ok(audit_body.events.length >= 1, 'Expected permit_revoke audit event');
517
+ assert.strictEqual(audit_body.events[0].event_type, 'permit_revoke');
518
+ });
519
+ test('admin session revoke-all creates audit event', async () => {
520
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
521
+ const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
522
+ const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
523
+ assert.ok(revoke_sessions_route, 'Expected admin POST /sessions/revoke-all route — ensure create_route_specs includes admin routes');
524
+ assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
525
+ const user_two = await test_app.create_account({ username: 'user_two' });
526
+ // Revoke all sessions for user_two
527
+ const path = revoke_sessions_route.path.replace(':account_id', user_two.account.id);
528
+ await test_app.app.request(path, {
529
+ method: 'POST',
530
+ headers: test_app.create_session_headers(),
531
+ });
532
+ // Check audit log
533
+ const audit_res = await test_app.app.request(`${audit_route.path}?event_type=session_revoke_all`, { headers: test_app.create_session_headers() });
534
+ assert.strictEqual(audit_res.status, 200);
535
+ const audit_body = await audit_res.json();
536
+ assert.ok(audit_body.events.length >= 1, 'Expected session_revoke_all audit event');
537
+ assert.strictEqual(audit_body.events[0].event_type, 'session_revoke_all');
538
+ });
539
+ test('admin token revoke-all creates audit event', async () => {
540
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
541
+ const revoke_tokens_route = find_admin_route(test_app.route_specs, '/tokens/revoke-all', 'POST');
542
+ const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
543
+ assert.ok(revoke_tokens_route, 'Expected admin POST /tokens/revoke-all route — ensure create_route_specs includes admin routes');
544
+ assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
545
+ const user_two = await test_app.create_account({ username: 'user_two' });
546
+ // Revoke all tokens for user_two
547
+ const path = revoke_tokens_route.path.replace(':account_id', user_two.account.id);
548
+ await test_app.app.request(path, {
549
+ method: 'POST',
550
+ headers: test_app.create_session_headers(),
551
+ });
552
+ // Check audit log
553
+ const audit_res = await test_app.app.request(`${audit_route.path}?event_type=token_revoke_all`, { headers: test_app.create_session_headers() });
554
+ assert.strictEqual(audit_res.status, 200);
555
+ const audit_body = await audit_res.json();
556
+ assert.ok(audit_body.events.length >= 1, 'Expected token_revoke_all audit event');
557
+ assert.strictEqual(audit_body.events[0].event_type, 'token_revoke_all');
558
+ });
559
+ });
560
+ // --- 7. Audit log completeness ---
561
+ describe('audit log completeness', () => {
562
+ test('auth mutations each produce exactly one audit event', async () => {
563
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
564
+ const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
565
+ const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
566
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
567
+ const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
568
+ const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
569
+ const create_token_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
570
+ const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
571
+ const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
572
+ assert.ok(audit_route, 'Expected admin GET /audit-log route');
573
+ // skip if required routes are missing (consumer may not wire all routes)
574
+ if (!login_route ||
575
+ !logout_route ||
576
+ !grant_route ||
577
+ !revoke_route ||
578
+ !accounts_route ||
579
+ !create_token_route ||
580
+ !password_route)
581
+ return;
582
+ const user_two = await test_app.create_account({ username: 'audit_user' });
583
+ const admin_headers = test_app.create_session_headers({
584
+ 'content-type': 'application/json',
585
+ });
586
+ // 1. login (user_two logs in)
587
+ const login_res = await test_app.app.request(login_route.path, {
588
+ method: 'POST',
589
+ headers: {
590
+ host: 'localhost',
591
+ origin: 'http://localhost:5173',
592
+ 'content-type': 'application/json',
593
+ },
594
+ body: JSON.stringify({ username: 'audit_user', password: 'test-password-123' }),
595
+ });
596
+ assert.strictEqual(login_res.status, 200);
597
+ // extract user_two session cookie for logout
598
+ const set_cookie = login_res.headers.get('set-cookie');
599
+ const cookie_match = new RegExp(`${cookie_name}=([^;]+)`).exec(set_cookie ?? '');
600
+ const user_two_cookie = cookie_match?.[1];
601
+ // 2. logout (user_two logs out)
602
+ if (user_two_cookie) {
603
+ await test_app.app.request(logout_route.path, {
604
+ method: 'POST',
605
+ headers: {
606
+ host: 'localhost',
607
+ origin: 'http://localhost:5173',
608
+ cookie: `${cookie_name}=${user_two_cookie}`,
609
+ },
610
+ });
611
+ }
612
+ // 3. grant permit (admin grants grantable_role to user_two)
613
+ const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
614
+ await test_app.app.request(grant_path, {
615
+ method: 'POST',
616
+ headers: admin_headers,
617
+ body: JSON.stringify({ role: grantable_role }),
618
+ });
619
+ // find permit ID
620
+ const list_res = await test_app.app.request(accounts_route.path, {
621
+ headers: test_app.create_session_headers(),
622
+ });
623
+ const list_body = await list_res.json();
624
+ const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
625
+ const permit = entry?.permits?.find((p) => p.role === grantable_role);
626
+ // 4. revoke permit
627
+ if (permit) {
628
+ const rev_path = revoke_route.path
629
+ .replace(':account_id', user_two.account.id)
630
+ .replace(':permit_id', permit.id);
631
+ await test_app.app.request(rev_path, {
632
+ method: 'POST',
633
+ headers: test_app.create_session_headers(),
634
+ });
635
+ }
636
+ // 5. create token
637
+ await test_app.app.request(create_token_route.path, {
638
+ method: 'POST',
639
+ headers: admin_headers,
640
+ body: JSON.stringify({ name: 'audit-test-token' }),
641
+ });
642
+ // 6. password change
643
+ await test_app.app.request(password_route.path, {
644
+ method: 'POST',
645
+ headers: test_app.create_session_headers({
646
+ 'content-type': 'application/json',
647
+ }),
648
+ body: JSON.stringify({
649
+ current_password: 'test-password-123',
650
+ new_password: 'new-audit-password-789',
651
+ }),
652
+ });
653
+ // query audit log and verify events
654
+ // re-login as admin since password change revoked sessions
655
+ const relogin_res = await test_app.app.request(login_route.path, {
656
+ method: 'POST',
657
+ headers: {
658
+ host: 'localhost',
659
+ origin: 'http://localhost:5173',
660
+ 'content-type': 'application/json',
661
+ },
662
+ body: JSON.stringify({
663
+ username: test_app.backend.account.username,
664
+ password: 'new-audit-password-789',
665
+ }),
666
+ });
667
+ assert.strictEqual(relogin_res.status, 200);
668
+ const relogin_cookie_header = relogin_res.headers.get('set-cookie');
669
+ const relogin_match = new RegExp(`${cookie_name}=([^;]+)`).exec(relogin_cookie_header ?? '');
670
+ assert.ok(relogin_match?.[1], 'Expected session cookie from re-login');
671
+ const relogin_headers = {
672
+ host: 'localhost',
673
+ origin: 'http://localhost:5173',
674
+ cookie: `${cookie_name}=${relogin_match[1]}`,
675
+ };
676
+ const audit_res = await test_app.app.request(audit_route.path, {
677
+ headers: relogin_headers,
678
+ });
679
+ assert.strictEqual(audit_res.status, 200);
680
+ const audit_body = await audit_res.json();
681
+ const events = audit_body.events;
682
+ // check that each operation produced at least one event
683
+ const expected_types = [
684
+ 'login',
685
+ 'logout',
686
+ 'permit_grant',
687
+ 'permit_revoke',
688
+ 'token_create',
689
+ 'password_change',
690
+ ];
691
+ for (const event_type of expected_types) {
692
+ const found = events.filter((e) => e.event_type === event_type);
693
+ assert.ok(found.length >= 1, `Expected at least 1 '${event_type}' audit event, found ${found.length}. ` +
694
+ `This may indicate audit_log_fire_and_forget was removed from a handler.`);
695
+ }
696
+ });
697
+ });
698
+ // --- 8. Admin-to-admin isolation ---
699
+ describe('admin-to-admin isolation', () => {
700
+ test('admin A cannot revoke admin B permits via mismatched account_id', async () => {
701
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
702
+ captured_route_specs ??= test_app.route_specs;
703
+ // Bootstrap user is admin A. Create admin B.
704
+ const admin_b = await test_app.create_account({
705
+ username: 'admin_b_iso',
706
+ roles: ['admin'],
707
+ });
708
+ // Find the permit grant route to give admin B a grantable role
709
+ const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
710
+ assert.ok(grant_route, 'Expected POST /permits/grant admin route');
711
+ // Admin A grants a role to admin B
712
+ const grant_res = await test_app.app.request(grant_route.path.replace(':account_id', admin_b.account.id), {
713
+ method: 'POST',
714
+ headers: create_headers(test_app.backend.session_cookie, {
715
+ 'content-type': 'application/json',
716
+ }),
717
+ body: JSON.stringify({ role: grantable_role }),
718
+ });
719
+ assert.strictEqual(grant_res.status, 200);
720
+ const grant_body = await grant_res.json();
721
+ assert.ok(grant_body.permit, 'Expected permit in grant response');
722
+ const permit_id = grant_body.permit.id;
723
+ // Admin B revokes their own permit via admin route — should succeed
724
+ const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
725
+ s.path.includes('/permits/:permit_id/revoke') &&
726
+ s.auth.type === 'role' &&
727
+ s.auth.role === 'admin');
728
+ assert.ok(revoke_route, 'Expected POST /permits/:permit_id/revoke admin route');
729
+ const revoke_res = await test_app.app.request(revoke_route.path
730
+ .replace(':account_id', admin_b.account.id)
731
+ .replace(':permit_id', permit_id), {
732
+ method: 'POST',
733
+ headers: create_headers(admin_b.session_cookie),
734
+ });
735
+ assert.strictEqual(revoke_res.status, 200);
736
+ const revoke_body = await revoke_res.json();
737
+ assert.strictEqual(revoke_body.revoked, true);
738
+ });
739
+ test('admin revoke-all sessions for another admin works', async () => {
740
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
741
+ const admin_b = await test_app.create_account({
742
+ username: 'admin_b_sess',
743
+ roles: ['admin'],
744
+ });
745
+ const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
746
+ assert.ok(revoke_sessions_route, 'Expected POST /sessions/revoke-all admin route');
747
+ // Admin A revokes all of admin B's sessions
748
+ const res = await test_app.app.request(revoke_sessions_route.path.replace(':account_id', admin_b.account.id), {
749
+ method: 'POST',
750
+ headers: create_headers(test_app.backend.session_cookie),
751
+ });
752
+ assert.strictEqual(res.status, 200);
753
+ const body = await res.json();
754
+ assert.ok(typeof body.count === 'number', 'Expected count field in response');
755
+ assert.ok(body.count >= 1, 'Expected at least 1 session revoked');
756
+ });
757
+ test('admin revoke-all tokens for another admin works', async () => {
758
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
759
+ const admin_b = await test_app.create_account({
760
+ username: 'admin_b_tok',
761
+ roles: ['admin'],
762
+ });
763
+ // Admin B creates an API token
764
+ const token_create_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/tokens/create'));
765
+ if (token_create_route) {
766
+ await test_app.app.request(token_create_route.path, {
767
+ method: 'POST',
768
+ headers: create_headers(admin_b.session_cookie, {
769
+ 'content-type': 'application/json',
770
+ }),
771
+ body: JSON.stringify({ name: 'admin-b-token' }),
772
+ });
773
+ }
774
+ const revoke_tokens_route = find_admin_route(test_app.route_specs, '/tokens/revoke-all', 'POST');
775
+ assert.ok(revoke_tokens_route, 'Expected POST /tokens/revoke-all admin route');
776
+ // Admin A revokes all of admin B's tokens
777
+ const res = await test_app.app.request(revoke_tokens_route.path.replace(':account_id', admin_b.account.id), {
778
+ method: 'POST',
779
+ headers: create_headers(test_app.backend.session_cookie),
780
+ });
781
+ assert.strictEqual(res.status, 200);
782
+ const body = await res.json();
783
+ assert.ok(typeof body.count === 'number', 'Expected count field in response');
784
+ });
785
+ test('non-admin cannot access admin routes for another account', async () => {
786
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
787
+ const regular_user = await test_app.create_account({ username: 'regular_user_iso' });
788
+ const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
789
+ assert.ok(accounts_route, 'Expected GET /accounts admin route');
790
+ // Regular user tries to list accounts — should get 403
791
+ const res = await test_app.app.request(accounts_route.path, {
792
+ headers: create_headers(regular_user.session_cookie),
793
+ });
794
+ assert.strictEqual(res.status, 403);
795
+ error_collector.record(test_app.route_specs, 'GET', accounts_route.path, 403);
796
+ });
797
+ });
798
+ // --- 8a. Error coverage: unauthenticated access to admin routes ---
799
+ describe('error coverage breadth', () => {
800
+ test('exercises 401/403 on admin routes for error coverage', async () => {
801
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
802
+ captured_route_specs ??= test_app.route_specs;
803
+ const prefix = options.admin_prefix ?? '/api/admin';
804
+ const admin_routes = test_app.route_specs.filter((s) => s.path.startsWith(prefix) && s.auth.type === 'role' && s.auth.role === 'admin');
805
+ // Hit admin routes without auth to exercise 401 error schemas
806
+ for (const route of admin_routes.slice(0, 5)) {
807
+ // eslint-disable-next-line no-await-in-loop
808
+ const res = await test_app.app.request(route.path, {
809
+ method: route.method,
810
+ headers: { host: 'localhost' },
811
+ });
812
+ if (res.status === 401 || res.status === 403) {
813
+ error_collector.record(test_app.route_specs, route.method, route.path, res.status);
814
+ }
815
+ }
816
+ });
817
+ });
818
+ // --- 8. Admin response schema validation ---
819
+ describe('admin response schema validation', () => {
820
+ test('admin route 200 responses match declared output schemas', async () => {
821
+ const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
822
+ const prefix = options.admin_prefix ?? '/api/admin';
823
+ const admin_get_routes = test_app.route_specs.filter((s) => s.method === 'GET' &&
824
+ s.path.startsWith(prefix) &&
825
+ s.auth.type === 'role' &&
826
+ s.auth.role === 'admin');
827
+ assert.ok(admin_get_routes.length > 0, 'Expected at least one admin GET route — ensure create_route_specs includes admin routes');
828
+ for (const route of admin_get_routes) {
829
+ // eslint-disable-next-line no-await-in-loop
830
+ const res = await test_app.app.request(route.path, {
831
+ headers: test_app.create_session_headers(),
832
+ });
833
+ assert.strictEqual(res.status, 200, `${route.method} ${route.path} should return 200`);
834
+ // eslint-disable-next-line no-await-in-loop
835
+ await assert_response_matches_spec(test_app.route_specs, route.method, route.path, res);
836
+ }
837
+ });
838
+ });
839
+ });
840
+ };