@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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/actions/action_bridge.d.ts +65 -0
- package/dist/actions/action_bridge.d.ts.map +1 -0
- package/dist/actions/action_bridge.js +76 -0
- package/dist/actions/action_codegen.d.ts +97 -0
- package/dist/actions/action_codegen.d.ts.map +1 -0
- package/dist/actions/action_codegen.js +280 -0
- package/dist/actions/action_registry.d.ts +35 -0
- package/dist/actions/action_registry.d.ts.map +1 -0
- package/dist/actions/action_registry.js +83 -0
- package/dist/actions/action_spec.d.ts +169 -0
- package/dist/actions/action_spec.d.ts.map +1 -0
- package/dist/actions/action_spec.js +76 -0
- package/dist/auth/account_queries.d.ts +96 -0
- package/dist/auth/account_queries.d.ts.map +1 -0
- package/dist/auth/account_queries.js +172 -0
- package/dist/auth/account_routes.d.ts +86 -0
- package/dist/auth/account_routes.d.ts.map +1 -0
- package/dist/auth/account_routes.js +406 -0
- package/dist/auth/account_schema.d.ts +192 -0
- package/dist/auth/account_schema.d.ts.map +1 -0
- package/dist/auth/account_schema.js +105 -0
- package/dist/auth/admin_routes.d.ts +29 -0
- package/dist/auth/admin_routes.d.ts.map +1 -0
- package/dist/auth/admin_routes.js +193 -0
- package/dist/auth/api_token.d.ts +33 -0
- package/dist/auth/api_token.d.ts.map +1 -0
- package/dist/auth/api_token.js +36 -0
- package/dist/auth/api_token_queries.d.ts +80 -0
- package/dist/auth/api_token_queries.d.ts.map +1 -0
- package/dist/auth/api_token_queries.js +116 -0
- package/dist/auth/app_settings_queries.d.ts +33 -0
- package/dist/auth/app_settings_queries.d.ts.map +1 -0
- package/dist/auth/app_settings_queries.js +51 -0
- package/dist/auth/app_settings_routes.d.ts +27 -0
- package/dist/auth/app_settings_routes.d.ts.map +1 -0
- package/dist/auth/app_settings_routes.js +66 -0
- package/dist/auth/app_settings_schema.d.ts +35 -0
- package/dist/auth/app_settings_schema.d.ts.map +1 -0
- package/dist/auth/app_settings_schema.js +22 -0
- package/dist/auth/audit_log_queries.d.ts +90 -0
- package/dist/auth/audit_log_queries.d.ts.map +1 -0
- package/dist/auth/audit_log_queries.js +205 -0
- package/dist/auth/audit_log_routes.d.ts +33 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -0
- package/dist/auth/audit_log_routes.js +106 -0
- package/dist/auth/audit_log_schema.d.ts +259 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -0
- package/dist/auth/audit_log_schema.js +123 -0
- package/dist/auth/bearer_auth.d.ts +32 -0
- package/dist/auth/bearer_auth.d.ts.map +1 -0
- package/dist/auth/bearer_auth.js +90 -0
- package/dist/auth/bootstrap_account.d.ts +82 -0
- package/dist/auth/bootstrap_account.d.ts.map +1 -0
- package/dist/auth/bootstrap_account.js +97 -0
- package/dist/auth/bootstrap_routes.d.ts +74 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -0
- package/dist/auth/bootstrap_routes.js +154 -0
- package/dist/auth/daemon_token.d.ts +49 -0
- package/dist/auth/daemon_token.d.ts.map +1 -0
- package/dist/auth/daemon_token.js +49 -0
- package/dist/auth/daemon_token_middleware.d.ts +93 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -0
- package/dist/auth/daemon_token_middleware.js +167 -0
- package/dist/auth/ddl.d.ts +27 -0
- package/dist/auth/ddl.d.ts.map +1 -0
- package/dist/auth/ddl.js +111 -0
- package/dist/auth/deps.d.ts +52 -0
- package/dist/auth/deps.d.ts.map +1 -0
- package/dist/auth/deps.js +10 -0
- package/dist/auth/invite_queries.d.ts +68 -0
- package/dist/auth/invite_queries.d.ts.map +1 -0
- package/dist/auth/invite_queries.js +105 -0
- package/dist/auth/invite_routes.d.ts +18 -0
- package/dist/auth/invite_routes.d.ts.map +1 -0
- package/dist/auth/invite_routes.js +129 -0
- package/dist/auth/invite_schema.d.ts +51 -0
- package/dist/auth/invite_schema.d.ts.map +1 -0
- package/dist/auth/invite_schema.js +25 -0
- package/dist/auth/keyring.d.ts +87 -0
- package/dist/auth/keyring.d.ts.map +1 -0
- package/dist/auth/keyring.js +142 -0
- package/dist/auth/middleware.d.ts +40 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +64 -0
- package/dist/auth/migrations.d.ts +42 -0
- package/dist/auth/migrations.d.ts.map +1 -0
- package/dist/auth/migrations.js +79 -0
- package/dist/auth/password.d.ts +39 -0
- package/dist/auth/password.d.ts.map +1 -0
- package/dist/auth/password.js +25 -0
- package/dist/auth/password_argon2.d.ts +43 -0
- package/dist/auth/password_argon2.d.ts.map +1 -0
- package/dist/auth/password_argon2.js +76 -0
- package/dist/auth/permit_queries.d.ts +72 -0
- package/dist/auth/permit_queries.d.ts.map +1 -0
- package/dist/auth/permit_queries.js +116 -0
- package/dist/auth/request_context.d.ts +114 -0
- package/dist/auth/request_context.d.ts.map +1 -0
- package/dist/auth/request_context.js +176 -0
- package/dist/auth/require_keeper.d.ts +20 -0
- package/dist/auth/require_keeper.d.ts.map +1 -0
- package/dist/auth/require_keeper.js +35 -0
- package/dist/auth/role_schema.d.ts +69 -0
- package/dist/auth/role_schema.d.ts.map +1 -0
- package/dist/auth/role_schema.js +70 -0
- package/dist/auth/route_guards.d.ts +21 -0
- package/dist/auth/route_guards.d.ts.map +1 -0
- package/dist/auth/route_guards.js +32 -0
- package/dist/auth/session_cookie.d.ts +158 -0
- package/dist/auth/session_cookie.d.ts.map +1 -0
- package/dist/auth/session_cookie.js +135 -0
- package/dist/auth/session_lifecycle.d.ts +35 -0
- package/dist/auth/session_lifecycle.d.ts.map +1 -0
- package/dist/auth/session_lifecycle.js +27 -0
- package/dist/auth/session_middleware.d.ts +33 -0
- package/dist/auth/session_middleware.d.ts.map +1 -0
- package/dist/auth/session_middleware.js +62 -0
- package/dist/auth/session_queries.d.ts +135 -0
- package/dist/auth/session_queries.d.ts.map +1 -0
- package/dist/auth/session_queries.js +186 -0
- package/dist/auth/signup_routes.d.ts +32 -0
- package/dist/auth/signup_routes.d.ts.map +1 -0
- package/dist/auth/signup_routes.js +150 -0
- package/dist/cli/args.d.ts +48 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +76 -0
- package/dist/cli/config.d.ts +48 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +77 -0
- package/dist/cli/daemon.d.ts +82 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +149 -0
- package/dist/cli/help.d.ts +85 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +138 -0
- package/dist/cli/logger.d.ts +46 -0
- package/dist/cli/logger.d.ts.map +1 -0
- package/dist/cli/logger.js +48 -0
- package/dist/cli/util.d.ts +36 -0
- package/dist/cli/util.d.ts.map +1 -0
- package/dist/cli/util.js +50 -0
- package/dist/crypto.d.ts +13 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +19 -0
- package/dist/db/assert_row.d.ts +18 -0
- package/dist/db/assert_row.d.ts.map +1 -0
- package/dist/db/assert_row.js +24 -0
- package/dist/db/create_db.d.ts +38 -0
- package/dist/db/create_db.d.ts.map +1 -0
- package/dist/db/create_db.js +57 -0
- package/dist/db/db.d.ts +97 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +76 -0
- package/dist/db/db_pg.d.ts +21 -0
- package/dist/db/db_pg.d.ts.map +1 -0
- package/dist/db/db_pg.js +45 -0
- package/dist/db/db_pglite.d.ts +21 -0
- package/dist/db/db_pglite.d.ts.map +1 -0
- package/dist/db/db_pglite.js +28 -0
- package/dist/db/migrate.d.ts +67 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +118 -0
- package/dist/db/pg_error.d.ts +16 -0
- package/dist/db/pg_error.d.ts.map +1 -0
- package/dist/db/pg_error.js +15 -0
- package/dist/db/query_deps.d.ts +14 -0
- package/dist/db/query_deps.d.ts.map +1 -0
- package/dist/db/query_deps.js +9 -0
- package/dist/db/sql_identifier.d.ts +27 -0
- package/dist/db/sql_identifier.d.ts.map +1 -0
- package/dist/db/sql_identifier.js +31 -0
- package/dist/db/status.d.ts +62 -0
- package/dist/db/status.d.ts.map +1 -0
- package/dist/db/status.js +116 -0
- package/dist/dev/setup.d.ts +159 -0
- package/dist/dev/setup.d.ts.map +1 -0
- package/dist/dev/setup.js +265 -0
- package/dist/env/dotenv.d.ts +25 -0
- package/dist/env/dotenv.d.ts.map +1 -0
- package/dist/env/dotenv.js +52 -0
- package/dist/env/load.d.ts +52 -0
- package/dist/env/load.d.ts.map +1 -0
- package/dist/env/load.js +79 -0
- package/dist/env/mask.d.ts +19 -0
- package/dist/env/mask.d.ts.map +1 -0
- package/dist/env/mask.js +26 -0
- package/dist/env/resolve.d.ts +126 -0
- package/dist/env/resolve.d.ts.map +1 -0
- package/dist/env/resolve.js +200 -0
- package/dist/hono_context.d.ts +48 -0
- package/dist/hono_context.d.ts.map +1 -0
- package/dist/hono_context.js +22 -0
- package/dist/http/common_routes.d.ts +52 -0
- package/dist/http/common_routes.d.ts.map +1 -0
- package/dist/http/common_routes.js +65 -0
- package/dist/http/db_routes.d.ts +57 -0
- package/dist/http/db_routes.d.ts.map +1 -0
- package/dist/http/db_routes.js +176 -0
- package/dist/http/error_schemas.d.ts +169 -0
- package/dist/http/error_schemas.d.ts.map +1 -0
- package/dist/http/error_schemas.js +178 -0
- package/dist/http/middleware_spec.d.ts +19 -0
- package/dist/http/middleware_spec.d.ts.map +1 -0
- package/dist/http/middleware_spec.js +9 -0
- package/dist/http/origin.d.ts +57 -0
- package/dist/http/origin.d.ts.map +1 -0
- package/dist/http/origin.js +207 -0
- package/dist/http/proxy.d.ts +112 -0
- package/dist/http/proxy.d.ts.map +1 -0
- package/dist/http/proxy.js +240 -0
- package/dist/http/route_spec.d.ts +197 -0
- package/dist/http/route_spec.d.ts.map +1 -0
- package/dist/http/route_spec.js +243 -0
- package/dist/http/schema_helpers.d.ts +64 -0
- package/dist/http/schema_helpers.d.ts.map +1 -0
- package/dist/http/schema_helpers.js +90 -0
- package/dist/http/surface.d.ts +132 -0
- package/dist/http/surface.d.ts.map +1 -0
- package/dist/http/surface.js +156 -0
- package/dist/http/surface_query.d.ts +77 -0
- package/dist/http/surface_query.d.ts.map +1 -0
- package/dist/http/surface_query.js +86 -0
- package/dist/rate_limiter.d.ts +94 -0
- package/dist/rate_limiter.d.ts.map +1 -0
- package/dist/rate_limiter.js +156 -0
- package/dist/realtime/sse.d.ts +80 -0
- package/dist/realtime/sse.d.ts.map +1 -0
- package/dist/realtime/sse.js +109 -0
- package/dist/realtime/sse_auth_guard.d.ts +93 -0
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -0
- package/dist/realtime/sse_auth_guard.js +111 -0
- package/dist/realtime/subscriber_registry.d.ts +85 -0
- package/dist/realtime/subscriber_registry.d.ts.map +1 -0
- package/dist/realtime/subscriber_registry.js +108 -0
- package/dist/runtime/deno.d.ts +21 -0
- package/dist/runtime/deno.d.ts.map +1 -0
- package/dist/runtime/deno.js +83 -0
- package/dist/runtime/deps.d.ts +113 -0
- package/dist/runtime/deps.d.ts.map +1 -0
- package/dist/runtime/deps.js +10 -0
- package/dist/runtime/fs.d.ts +15 -0
- package/dist/runtime/fs.d.ts.map +1 -0
- package/dist/runtime/fs.js +17 -0
- package/dist/runtime/mock.d.ts +81 -0
- package/dist/runtime/mock.d.ts.map +1 -0
- package/dist/runtime/mock.js +195 -0
- package/dist/runtime/node.d.ts +17 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +117 -0
- package/dist/schema_meta.d.ts +16 -0
- package/dist/schema_meta.d.ts.map +1 -0
- package/dist/schema_meta.js +9 -0
- package/dist/sensitivity.d.ts +15 -0
- package/dist/sensitivity.d.ts.map +1 -0
- package/dist/sensitivity.js +9 -0
- package/dist/server/app_backend.d.ts +74 -0
- package/dist/server/app_backend.d.ts.map +1 -0
- package/dist/server/app_backend.js +39 -0
- package/dist/server/app_server.d.ts +201 -0
- package/dist/server/app_server.d.ts.map +1 -0
- package/dist/server/app_server.js +266 -0
- package/dist/server/env.d.ts +68 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +95 -0
- package/dist/server/startup.d.ts +22 -0
- package/dist/server/startup.d.ts.map +1 -0
- package/dist/server/startup.js +48 -0
- package/dist/server/static.d.ts +39 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +38 -0
- package/dist/server/validate_nginx.d.ts +34 -0
- package/dist/server/validate_nginx.d.ts.map +1 -0
- package/dist/server/validate_nginx.js +118 -0
- package/dist/testing/CLAUDE.md +3 -0
- package/dist/testing/admin_integration.d.ts +45 -0
- package/dist/testing/admin_integration.d.ts.map +1 -0
- package/dist/testing/admin_integration.js +840 -0
- package/dist/testing/adversarial_404.d.ts +15 -0
- package/dist/testing/adversarial_404.d.ts.map +1 -0
- package/dist/testing/adversarial_404.js +118 -0
- package/dist/testing/adversarial_headers.d.ts +36 -0
- package/dist/testing/adversarial_headers.d.ts.map +1 -0
- package/dist/testing/adversarial_headers.js +128 -0
- package/dist/testing/adversarial_input.d.ts +56 -0
- package/dist/testing/adversarial_input.d.ts.map +1 -0
- package/dist/testing/adversarial_input.js +494 -0
- package/dist/testing/app_server.d.ts +169 -0
- package/dist/testing/app_server.d.ts.map +1 -0
- package/dist/testing/app_server.js +240 -0
- package/dist/testing/assert_dev_env.d.ts +10 -0
- package/dist/testing/assert_dev_env.d.ts.map +1 -0
- package/dist/testing/assert_dev_env.js +13 -0
- package/dist/testing/assertions.d.ts +61 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +96 -0
- package/dist/testing/attack_surface.d.ts +63 -0
- package/dist/testing/attack_surface.d.ts.map +1 -0
- package/dist/testing/attack_surface.js +224 -0
- package/dist/testing/audit_completeness.d.ts +29 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -0
- package/dist/testing/audit_completeness.js +410 -0
- package/dist/testing/auth_apps.d.ts +55 -0
- package/dist/testing/auth_apps.d.ts.map +1 -0
- package/dist/testing/auth_apps.js +122 -0
- package/dist/testing/data_exposure.d.ts +62 -0
- package/dist/testing/data_exposure.d.ts.map +1 -0
- package/dist/testing/data_exposure.js +297 -0
- package/dist/testing/db.d.ts +111 -0
- package/dist/testing/db.d.ts.map +1 -0
- package/dist/testing/db.js +258 -0
- package/dist/testing/entities.d.ts +21 -0
- package/dist/testing/entities.d.ts.map +1 -0
- package/dist/testing/entities.js +42 -0
- package/dist/testing/error_coverage.d.ts +78 -0
- package/dist/testing/error_coverage.d.ts.map +1 -0
- package/dist/testing/error_coverage.js +135 -0
- package/dist/testing/integration.d.ts +37 -0
- package/dist/testing/integration.d.ts.map +1 -0
- package/dist/testing/integration.js +1139 -0
- package/dist/testing/integration_helpers.d.ts +107 -0
- package/dist/testing/integration_helpers.d.ts.map +1 -0
- package/dist/testing/integration_helpers.js +246 -0
- package/dist/testing/middleware.d.ts +125 -0
- package/dist/testing/middleware.d.ts.map +1 -0
- package/dist/testing/middleware.js +210 -0
- package/dist/testing/rate_limiting.d.ts +43 -0
- package/dist/testing/rate_limiting.d.ts.map +1 -0
- package/dist/testing/rate_limiting.js +216 -0
- package/dist/testing/round_trip.d.ts +37 -0
- package/dist/testing/round_trip.d.ts.map +1 -0
- package/dist/testing/round_trip.js +128 -0
- package/dist/testing/schema_generators.d.ts +33 -0
- package/dist/testing/schema_generators.d.ts.map +1 -0
- package/dist/testing/schema_generators.js +137 -0
- package/dist/testing/standard.d.ts +49 -0
- package/dist/testing/standard.d.ts.map +1 -0
- package/dist/testing/standard.js +16 -0
- package/dist/testing/stubs.d.ts +96 -0
- package/dist/testing/stubs.d.ts.map +1 -0
- package/dist/testing/stubs.js +192 -0
- package/dist/testing/surface_invariants.d.ts +189 -0
- package/dist/testing/surface_invariants.d.ts.map +1 -0
- package/dist/testing/surface_invariants.js +450 -0
- package/dist/ui/AccountSessions.svelte +75 -0
- package/dist/ui/AccountSessions.svelte.d.ts +19 -0
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -0
- package/dist/ui/AdminAccounts.svelte +107 -0
- package/dist/ui/AdminAccounts.svelte.d.ts +19 -0
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -0
- package/dist/ui/AdminAuditLog.svelte +144 -0
- package/dist/ui/AdminAuditLog.svelte.d.ts +4 -0
- package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -0
- package/dist/ui/AdminInvites.svelte +142 -0
- package/dist/ui/AdminInvites.svelte.d.ts +4 -0
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -0
- package/dist/ui/AdminOverview.svelte +337 -0
- package/dist/ui/AdminOverview.svelte.d.ts +4 -0
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -0
- package/dist/ui/AdminPermitHistory.svelte +61 -0
- package/dist/ui/AdminPermitHistory.svelte.d.ts +19 -0
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -0
- package/dist/ui/AdminSessions.svelte +85 -0
- package/dist/ui/AdminSessions.svelte.d.ts +19 -0
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -0
- package/dist/ui/AdminSettings.svelte +32 -0
- package/dist/ui/AdminSettings.svelte.d.ts +19 -0
- package/dist/ui/AdminSettings.svelte.d.ts.map +1 -0
- package/dist/ui/AdminSurface.svelte +42 -0
- package/dist/ui/AdminSurface.svelte.d.ts +4 -0
- package/dist/ui/AdminSurface.svelte.d.ts.map +1 -0
- package/dist/ui/AppShell.svelte +93 -0
- package/dist/ui/AppShell.svelte.d.ts +20 -0
- package/dist/ui/AppShell.svelte.d.ts.map +1 -0
- package/dist/ui/BootstrapForm.svelte +105 -0
- package/dist/ui/BootstrapForm.svelte.d.ts +4 -0
- package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -0
- package/dist/ui/ColumnLayout.svelte +46 -0
- package/dist/ui/ColumnLayout.svelte.d.ts +11 -0
- package/dist/ui/ColumnLayout.svelte.d.ts.map +1 -0
- package/dist/ui/ConfirmButton.svelte +125 -0
- package/dist/ui/ConfirmButton.svelte.d.ts +54 -0
- package/dist/ui/ConfirmButton.svelte.d.ts.map +1 -0
- package/dist/ui/Datatable.svelte +185 -0
- package/dist/ui/Datatable.svelte.d.ts +35 -0
- package/dist/ui/Datatable.svelte.d.ts.map +1 -0
- package/dist/ui/LoginForm.svelte +82 -0
- package/dist/ui/LoginForm.svelte.d.ts +8 -0
- package/dist/ui/LoginForm.svelte.d.ts.map +1 -0
- package/dist/ui/LogoutButton.svelte +36 -0
- package/dist/ui/LogoutButton.svelte.d.ts +10 -0
- package/dist/ui/LogoutButton.svelte.d.ts.map +1 -0
- package/dist/ui/MenuLink.svelte +35 -0
- package/dist/ui/MenuLink.svelte.d.ts +12 -0
- package/dist/ui/MenuLink.svelte.d.ts.map +1 -0
- package/dist/ui/OpenSignupToggle.svelte +36 -0
- package/dist/ui/OpenSignupToggle.svelte.d.ts +19 -0
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -0
- package/dist/ui/PopoverButton.svelte +136 -0
- package/dist/ui/PopoverButton.svelte.d.ts +63 -0
- package/dist/ui/PopoverButton.svelte.d.ts.map +1 -0
- package/dist/ui/SignupForm.svelte +117 -0
- package/dist/ui/SignupForm.svelte.d.ts +7 -0
- package/dist/ui/SignupForm.svelte.d.ts.map +1 -0
- package/dist/ui/SurfaceExplorer.svelte +287 -0
- package/dist/ui/SurfaceExplorer.svelte.d.ts +8 -0
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.d.ts +15 -0
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.js +45 -0
- package/dist/ui/admin_accounts_state.svelte.d.ts +19 -0
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -0
- package/dist/ui/admin_accounts_state.svelte.js +65 -0
- package/dist/ui/admin_invites_state.svelte.d.ts +19 -0
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -0
- package/dist/ui/admin_invites_state.svelte.js +71 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts +18 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -0
- package/dist/ui/admin_sessions_state.svelte.js +62 -0
- package/dist/ui/app_settings_state.svelte.d.ts +14 -0
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -0
- package/dist/ui/app_settings_state.svelte.js +44 -0
- package/dist/ui/audit_log_state.svelte.d.ts +40 -0
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -0
- package/dist/ui/audit_log_state.svelte.js +153 -0
- package/dist/ui/auth_state.svelte.d.ts +85 -0
- package/dist/ui/auth_state.svelte.d.ts.map +1 -0
- package/dist/ui/auth_state.svelte.js +238 -0
- package/dist/ui/datatable.d.ts +25 -0
- package/dist/ui/datatable.d.ts.map +1 -0
- package/dist/ui/datatable.js +9 -0
- package/dist/ui/enter_advance.d.ts +13 -0
- package/dist/ui/enter_advance.d.ts.map +1 -0
- package/dist/ui/enter_advance.js +30 -0
- package/dist/ui/loadable.svelte.d.ts +55 -0
- package/dist/ui/loadable.svelte.d.ts.map +1 -0
- package/dist/ui/loadable.svelte.js +75 -0
- package/dist/ui/popover.svelte.d.ts +137 -0
- package/dist/ui/popover.svelte.d.ts.map +1 -0
- package/dist/ui/popover.svelte.js +288 -0
- package/dist/ui/position_helpers.d.ts +27 -0
- package/dist/ui/position_helpers.d.ts.map +1 -0
- package/dist/ui/position_helpers.js +81 -0
- package/dist/ui/sidebar_state.svelte.d.ts +30 -0
- package/dist/ui/sidebar_state.svelte.d.ts.map +1 -0
- package/dist/ui/sidebar_state.svelte.js +39 -0
- package/dist/ui/table_state.svelte.d.ts +63 -0
- package/dist/ui/table_state.svelte.d.ts.map +1 -0
- package/dist/ui/table_state.svelte.js +117 -0
- package/dist/ui/ui_fetch.d.ts +29 -0
- package/dist/ui/ui_fetch.d.ts.map +1 -0
- package/dist/ui/ui_fetch.js +37 -0
- package/dist/ui/ui_format.d.ts +63 -0
- package/dist/ui/ui_format.d.ts.map +1 -0
- package/dist/ui/ui_format.js +196 -0
- 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
|
+
};
|