diff --git a/AGENTS.md b/AGENTS.md index 8191cd5..84f6f33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,8 @@ Start with [`CONTRIBUTING.md`](CONTRIBUTING.md) for repository boundaries, devel - Official TypeScript, Python, and Rust SDKs cover Runtime Project Auth only. Server API has a generated OpenAPI document but no official SDK and must never be imported into hosted-web code. - Keep Auth and Control listeners independently configurable. Keep Runtime, Server API, and Control internal authority boundaries independently configurable while all three surfaces remain bound to one PostgreSQL server/database authority. The Runtime Hosted Authentication UI and Control Management Console are the only browser surfaces. - PostgreSQL is the durable authority. Do not add browser credential persistence, file-store fallbacks, unreviewed secret delivery, or dependencies from CLI/SDKs into the server implementation. -- Edit Rust DTOs in `owlauth-types`; regenerate derived OpenAPI/hosted-web contracts rather than hand-editing generated files. Until the first deployed schema is declared, rebuild clean module baselines instead of preserving pre-release migration history; after deployment, freeze those baselines and add ordered migrations only. +- Edit Rust DTOs in `owlauth-types`; regenerate derived OpenAPI/hosted-web contracts rather than hand-editing generated files. +- The PostgreSQL schema has been deployed. Existing migration files are immutable compatibility artifacts: never edit, reorder, squash, or replace them. Every schema change must use a new ordered migration and must be tested both as a clean install and as an upgrade from the deployed migration set. ## Development quick start @@ -76,7 +77,3 @@ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for changelog scopes, SemVer rules, sha - [`scripts/`](scripts/) — repository, package, release, installer, SDK, and container checks - [`dev/`](dev/README.md) — local PostgreSQL development service - [`.github/workflows/`](.github/workflows/) — CI, documentation, image, and component release automation - -## AnyCap - -This project uses [AnyCap](https://anycap.ai) for multimodal capabilities, web search and crawling, file sharing, and static-page hosting. For these tasks, use the installed AnyCap skills and verify availability with `anycap status`. AnyCap evolves rapidly, so check or update its skills when commands or capabilities appear stale; submit `anycap feedback` for service or capability issues. diff --git a/crates/owlauth-cli/README.md b/crates/owlauth-cli/README.md index 38d6fc1..e69983e 100644 --- a/crates/owlauth-cli/README.md +++ b/crates/owlauth-cli/README.md @@ -42,7 +42,7 @@ Every authenticated command repeats discovery validation before reading the refe The self-hosted client supports typed commands for: -- Project list/get/create/disable, token/session policy get/set, and Project-user authoritative search/filter/sort/page, exact-email lookup, get/identity/session inspection, disable, and exact session revoke; +- Project list/get/create/disable/enable/permanent-delete, token/session policy get/set, and Project-user authoritative search/filter/sort/page, exact-email lookup, get/identity/session inspection, disable, and exact session revoke; - Application list/get/create/disable and cursor-bounded immutable user-event history; - provider Project-egress get/set, Custom OIDC preflight, and list/create/disable/assign/unassign for the closed `oidc`, `google`, and `github` kinds; - signing-key list/rotate/revoke, with provisioning, publication, activation, and retirement handled by the automatic lifecycle; @@ -58,6 +58,14 @@ owlauth --profile local project list owlauth --profile local project create \ --display-name 'Example' \ --idempotency-key project_create_20260803 +owlauth --profile local project enable \ + 11111111-1111-4111-8111-111111111111 \ + --expected-security-revision 2 \ + --yes +owlauth --profile local project delete \ + 11111111-1111-4111-8111-111111111111 \ + --expected-security-revision 3 \ + --yes owlauth --profile local application list \ 11111111-1111-4111-8111-111111111111 owlauth --profile local project user list \ @@ -81,7 +89,7 @@ owlauth --profile local server-key acknowledge \ --yes ``` -All Control path identifiers must be canonical lowercase hyphenated UUIDs. Create commands require an explicit 8–128 character `--idempotency-key`; retain and reuse that key when reconciling an ambiguous transport outcome instead of submitting the same normalized create under a new key. `server-key create` leaves replacement creation blocked until the emitted credential is durably stored and the exact returned key revision is passed to `server-key acknowledge`, or the key is revoked. Acknowledgement is an explicit assertion about external secret-manager storage; successful stdout delivery alone does not make that assertion. Revision-fenced trust, visibility, activation, disable, retirement, revoke, assignment, unassignment, endpoint-test, and policy changes require explicit `--yes` where exposed. The CLI rejects the operation before authentication when confirmation is absent; when present, it prints a redacted preview containing the selected profile, pinned endpoint/instance, exact target, operation, and bounded effect before authenticating. Full-replacement booleans such as `--browser-session-reuse` require an explicit `true` or `false` value. +All Control path identifiers must be canonical lowercase hyphenated UUIDs. Create commands require an explicit 8–128 character `--idempotency-key`; retain and reuse that key when reconciling an ambiguous transport outcome instead of submitting the same normalized create under a new key. `server-key create` leaves replacement creation blocked until the emitted credential is durably stored and the exact returned key revision is passed to `server-key acknowledge`, or the key is revoked. Acknowledgement is an explicit assertion about external secret-manager storage; successful stdout delivery alone does not make that assertion. Revision-fenced trust, visibility, activation, disable, retirement, revoke, assignment, unassignment, endpoint-test, and policy changes require confirmation. Project disable, enable, and permanent delete require `--yes`. The CLI rejects an unconfirmed operation before authentication and prints a redacted preview containing the selected profile, pinned endpoint/instance, exact target, operation, and bounded effect before the mutation. Full-replacement booleans such as `--browser-session-reuse` require an explicit `true` or `false` value. Provider client secrets and webhook signing secrets, including candidate rotation generations, are accepted only through named environment-variable references: diff --git a/crates/owlauth-cli/src/control.rs b/crates/owlauth-cli/src/control.rs index 0f1c434..b6f1cb4 100644 --- a/crates/owlauth-cli/src/control.rs +++ b/crates/owlauth-cli/src/control.rs @@ -142,7 +142,7 @@ enum ProjectCommand { #[arg(long)] idempotency_key: String, }, - /// Monotonically disable a Project. + /// Disable a Project until it is explicitly enabled again. Disable { project_id: String, #[arg(long)] @@ -150,6 +150,22 @@ enum ProjectCommand { #[arg(long)] yes: bool, }, + /// Enable a disabled Project. + Enable { + project_id: String, + #[arg(long)] + expected_security_revision: i64, + #[arg(long)] + yes: bool, + }, + /// Permanently delete a Project and all of its live authority data. + Delete { + project_id: String, + #[arg(long)] + expected_security_revision: i64, + #[arg(long)] + yes: bool, + }, /// Read or replace Project token/session policy. Policy(ProjectPolicyArgs), /// Inspect and administer Project users and their sessions. @@ -707,6 +723,67 @@ enum WebhookDeliveryCommand { }, } +fn run_project_transition( + profile: Option<&str>, + project_id: &str, + expected_security_revision: i64, + confirmed: bool, + action: &str, + effect: &str, +) -> Result<(), RemoteError> { + resource(project_id)?; + let target = format!("projects/{project_id}/{action}"); + let stored = require_confirmation( + confirmed, + profile, + &format!("project.{action}"), + &target, + &serde_json::json!({ + "effect": effect, + "expected_security_revision": expected_security_revision, + }), + )?; + let value: Project = authenticated_server_snapshot(stored)?.send( + Method::POST, + &target, + &ExpectedSecurityRevision { + expected_security_revision, + }, + None, + )?; + print_json(&value) +} + +fn run_project_delete( + profile: Option<&str>, + project_id: &str, + expected_security_revision: i64, + confirmed: bool, +) -> Result<(), RemoteError> { + resource(project_id)?; + let target = format!("projects/{project_id}"); + let stored = require_confirmation( + confirmed, + profile, + "project.delete", + &target, + &serde_json::json!({ + "effect": "permanently delete the Project after provider cleanup", + "expected_security_revision": expected_security_revision, + "recoverable": false, + }), + )?; + let value: Project = authenticated_server_snapshot(stored)?.send( + Method::DELETE, + &target, + &ExpectedSecurityRevision { + expected_security_revision, + }, + None, + )?; + print_json(&value) +} + pub(crate) fn run_project(profile: Option<&str>, args: ProjectArgs) -> Result<(), RemoteError> { match args.command { ProjectCommand::List { belongs_to } => { @@ -744,29 +821,31 @@ pub(crate) fn run_project(profile: Option<&str>, args: ProjectArgs) -> Result<() project_id, expected_security_revision, yes, - } => { - resource(&project_id)?; - let target = format!("projects/{project_id}/disable"); - let stored = require_confirmation( - yes, - profile, - "project.disable", - &target, - &serde_json::json!({ - "effect": "monotonically disable the Project", - "expected_security_revision": expected_security_revision, - }), - )?; - let value: Project = authenticated_server_snapshot(stored)?.send( - Method::POST, - &target, - &ExpectedSecurityRevision { - expected_security_revision, - }, - None, - )?; - print_json(&value) - } + } => run_project_transition( + profile, + &project_id, + expected_security_revision, + yes, + "disable", + "disable the Project until it is explicitly enabled", + ), + ProjectCommand::Enable { + project_id, + expected_security_revision, + yes, + } => run_project_transition( + profile, + &project_id, + expected_security_revision, + yes, + "enable", + "enable the Project", + ), + ProjectCommand::Delete { + project_id, + expected_security_revision, + yes, + } => run_project_delete(profile, &project_id, expected_security_revision, yes), ProjectCommand::Policy(args) => run_project_policy(profile, args), ProjectCommand::User(args) => run_project_user(profile, args), } diff --git a/crates/owlauth-cli/src/main.rs b/crates/owlauth-cli/src/main.rs index 2b308bb..6582931 100644 --- a/crates/owlauth-cli/src/main.rs +++ b/crates/owlauth-cli/src/main.rs @@ -215,6 +215,28 @@ mod tests { ); } + #[test] + fn project_lifecycle_commands_require_security_revisions() { + const PROJECT: &str = "11111111-1111-4111-8111-111111111111"; + for transition in ["disable", "enable", "delete"] { + assert!( + Cli::try_parse_from([ + "owlauth", + "project", + transition, + PROJECT, + "--expected-security-revision", + "2", + "--yes", + ]) + .is_ok() + ); + assert!( + Cli::try_parse_from(["owlauth", "project", transition, PROJECT, "--yes",]).is_err() + ); + } + } + #[test] fn project_user_directory_commands_parse_closed_arguments() { const PROJECT: &str = "11111111-1111-4111-8111-111111111111"; diff --git a/crates/owlauth-server/migrations/20260810000000_project_lifecycle.sql b/crates/owlauth-server/migrations/20260810000000_project_lifecycle.sql new file mode 100644 index 0000000..d836c25 --- /dev/null +++ b/crates/owlauth-server/migrations/20260810000000_project_lifecycle.sql @@ -0,0 +1,576 @@ +-- Project re-enablement and provider-safe permanent deletion. +-- +-- The 20260806 baselines have shipped and remain immutable. This ordered migration extends the +-- deployed schema without rewriting prior migration bytes. + +ALTER TABLE public.projects + ADD COLUMN deletion_requested_at timestamp with time zone; + +ALTER TABLE public.projects + DROP CONSTRAINT projects_status_check; +ALTER TABLE public.projects + ADD CONSTRAINT projects_status_check + CHECK (status = ANY (ARRAY['active'::text, 'disabled'::text, 'deleting'::text])); +ALTER TABLE public.projects + ADD CONSTRAINT projects_deletion_shape_check + CHECK ((status = 'deleting'::text) = (deletion_requested_at IS NOT NULL)); + +CREATE FUNCTION public.owlauth_enforce_project_lifecycle() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF OLD.status = 'deleting' THEN + RAISE EXCEPTION 'deleting Project is terminal' + USING ERRCODE = '23514'; + END IF; + IF NEW.status IS DISTINCT FROM OLD.status THEN + IF NOT ( + (OLD.status = 'active' AND NEW.status IN ('disabled', 'deleting')) + OR (OLD.status = 'disabled' AND NEW.status IN ('active', 'deleting')) + ) OR NEW.security_revision <> OLD.security_revision + 1 THEN + RAISE EXCEPTION 'invalid Project lifecycle transition' + USING ERRCODE = '23514'; + END IF; + ELSIF NEW.deletion_requested_at IS DISTINCT FROM OLD.deletion_requested_at THEN + RAISE EXCEPTION 'Project deletion timestamp is lifecycle-owned' + USING ERRCODE = '23514'; + END IF; + RETURN NEW; +END +$$; + +CREATE TRIGGER projects_enforce_lifecycle + BEFORE UPDATE ON public.projects + FOR EACH ROW EXECUTE FUNCTION public.owlauth_enforce_project_lifecycle(); + +-- The owner-executed finalizer sets this transaction-local value only after locking and proving one +-- exact deleting Project has no outstanding protected material or provider cleanup. Immutable-row +-- triggers use the exact Project context while the reviewed cascade is in progress. +CREATE FUNCTION public.owlauth_project_deletion_allowed(target_project_id uuid) RETURNS boolean + LANGUAGE sql STABLE + SET search_path = pg_catalog + AS $$ +SELECT current_user = pg_catalog.pg_get_userbyid(( + SELECT relation.relowner + FROM pg_catalog.pg_class AS relation + WHERE relation.oid = 'public.projects'::pg_catalog.regclass + )) + AND pg_catalog.current_setting('owlauth.project_deletion_id', true) = target_project_id::text +$$; + +CREATE OR REPLACE FUNCTION public.owlauth_reject_identity_mutation_create_result_delete() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF public.owlauth_project_deletion_allowed(OLD.project_id) THEN + RETURN OLD; + END IF; + IF EXISTS (SELECT 1 FROM projects WHERE id = OLD.project_id) THEN + RAISE EXCEPTION 'identity-mutation create-result authority tombstone cannot be deleted' + USING ERRCODE = '23514'; + END IF; + RETURN OLD; +END +$$; + +CREATE OR REPLACE FUNCTION public.owlauth_reject_identity_mutation_intent_delete_with_result() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF public.owlauth_project_deletion_allowed(OLD.project_id) THEN + RETURN OLD; + END IF; + IF EXISTS (SELECT 1 FROM projects WHERE id = OLD.project_id) + AND EXISTS ( + SELECT 1 FROM identity_mutation_create_results + WHERE project_id = OLD.project_id AND intent_id = OLD.id + ) + THEN + RAISE EXCEPTION 'identity-mutation intent with durable create authority cannot be deleted' + USING ERRCODE = '23514'; + END IF; + RETURN OLD; +END +$$; + +CREATE OR REPLACE FUNCTION public.owlauth_reject_merged_binding_delete() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF public.owlauth_project_deletion_allowed(OLD.project_id) THEN + RETURN OLD; + END IF; + IF OLD.status = 'merged' + AND EXISTS (SELECT 1 FROM projects WHERE id = OLD.project_id) + THEN + RAISE EXCEPTION 'merged Application binding attribution cannot be deleted' + USING ERRCODE = '23514'; + END IF; + RETURN OLD; +END +$$; + +CREATE OR REPLACE FUNCTION public.owlauth_reject_project_user_merge_tombstone_mutation() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF TG_OP = 'DELETE' AND public.owlauth_project_deletion_allowed(OLD.project_id) THEN + RETURN OLD; + END IF; + RAISE EXCEPTION 'Project-user merge tombstones are immutable' + USING ERRCODE = '23514'; +END +$$; + +CREATE OR REPLACE FUNCTION public.reject_application_sync_immutable_mutation() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF TG_OP = 'DELETE' AND public.owlauth_project_deletion_allowed(OLD.project_id) THEN + RETURN OLD; + END IF; + IF TG_OP = 'DELETE' + AND OLD.retain_until <= transaction_timestamp() + AND NOT EXISTS ( + SELECT 1 FROM webhook_deliveries delivery + WHERE delivery.event_id = OLD.id + AND delivery.state NOT IN ('delivered', 'terminal', 'cancelled') + ) + THEN + RETURN OLD; + END IF; + RAISE EXCEPTION '% is append-only', TG_TABLE_NAME; +END; +$$; + +CREATE OR REPLACE FUNCTION public.reject_audit_event_mutation() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF TG_OP = 'DELETE' AND public.owlauth_project_deletion_allowed(OLD.project_id) THEN + RETURN OLD; + END IF; + RAISE EXCEPTION '% is append-only', TG_TABLE_NAME + USING ERRCODE = '23514'; +END +$$; + +CREATE OR REPLACE FUNCTION public.reject_webhook_attempt_immutable_mutation() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF TG_OP = 'DELETE' + AND EXISTS ( + SELECT 1 + FROM webhook_deliveries delivery + WHERE delivery.id = OLD.delivery_id + AND public.owlauth_project_deletion_allowed(delivery.project_id) + ) + THEN + RETURN OLD; + END IF; + IF TG_OP = 'DELETE' + AND EXISTS ( + SELECT 1 + FROM webhook_deliveries delivery + JOIN application_user_events event ON event.id = delivery.event_id + WHERE delivery.id = OLD.delivery_id + AND delivery.state IN ('delivered', 'terminal', 'cancelled') + AND event.retain_until <= transaction_timestamp() + ) + THEN + RETURN OLD; + END IF; + RAISE EXCEPTION '% is append-only', TG_TABLE_NAME; +END; +$$; + +-- Existing root constraints that intentionally retained Project state become cascading only for the +-- reviewed whole-Project finalizer. All ordinary child deletes remain governed by their existing +-- constraints and immutable-history triggers. +ALTER TABLE public.applications DROP CONSTRAINT applications_project_id_fkey; +ALTER TABLE public.applications + ADD CONSTRAINT applications_project_id_fkey + FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE public.applications VALIDATE CONSTRAINT applications_project_id_fkey; + +ALTER TABLE public.audit_events DROP CONSTRAINT audit_events_project_id_fkey; +ALTER TABLE public.audit_events + ADD CONSTRAINT audit_events_project_id_fkey + FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE public.audit_events VALIDATE CONSTRAINT audit_events_project_id_fkey; + +ALTER TABLE public.control_idempotency_records DROP CONSTRAINT control_idempotency_records_project_id_fkey; +ALTER TABLE public.control_idempotency_records + ADD CONSTRAINT control_idempotency_records_project_id_fkey + FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE public.control_idempotency_records VALIDATE CONSTRAINT control_idempotency_records_project_id_fkey; + +ALTER TABLE public.project_server_keys DROP CONSTRAINT project_server_keys_project_id_fkey; +ALTER TABLE public.project_server_keys + ADD CONSTRAINT project_server_keys_project_id_fkey + FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE public.project_server_keys VALIDATE CONSTRAINT project_server_keys_project_id_fkey; + +ALTER TABLE public.protected_materials DROP CONSTRAINT protected_materials_project_id_fkey; +ALTER TABLE public.protected_materials + ADD CONSTRAINT protected_materials_project_id_fkey + FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE public.protected_materials VALIDATE CONSTRAINT protected_materials_project_id_fkey; + +-- Complete the natural ownership graph at the few deployed NO ACTION edges that otherwise retain +-- Project-owned rows. This reuses existing parents instead of adding a second Project FK to every +-- child table. +ALTER TABLE public.application_sessions + DROP CONSTRAINT application_sessions_binding_identity_fk; +ALTER TABLE public.application_sessions + ADD CONSTRAINT application_sessions_binding_identity_fk + FOREIGN KEY (project_id, binding_id, application_id) + REFERENCES public.application_user_bindings(project_id, id, application_id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.application_sessions + VALIDATE CONSTRAINT application_sessions_binding_identity_fk; + +ALTER TABLE public.application_user_events + DROP CONSTRAINT application_user_event_binding_fk; +ALTER TABLE public.application_user_events + ADD CONSTRAINT application_user_event_binding_fk + FOREIGN KEY (project_id, binding_id, application_id) + REFERENCES public.application_user_bindings(project_id, id, application_id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.application_user_events + VALIDATE CONSTRAINT application_user_event_binding_fk; + +ALTER TABLE public.email_challenges + DROP CONSTRAINT email_challenges_project_id_application_id_fkey; +ALTER TABLE public.email_challenges + ADD CONSTRAINT email_challenges_project_id_application_id_fkey + FOREIGN KEY (project_id, application_id) + REFERENCES public.applications(project_id, id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.email_challenges + VALIDATE CONSTRAINT email_challenges_project_id_application_id_fkey; + +ALTER TABLE public.key_state_events + DROP CONSTRAINT key_state_events_project_id_ring_id_signing_key_id_fkey; +ALTER TABLE public.key_state_events + ADD CONSTRAINT key_state_events_project_id_ring_id_signing_key_id_fkey + FOREIGN KEY (project_id, ring_id, signing_key_id) + REFERENCES public.project_signing_keys(project_id, ring_id, id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.key_state_events + VALIDATE CONSTRAINT key_state_events_project_id_ring_id_signing_key_id_fkey; + +ALTER TABLE public.project_browser_logout_interactions + DROP CONSTRAINT project_browser_logout_intera_project_id_application_sessi_fkey; +ALTER TABLE public.project_browser_logout_interactions + ADD CONSTRAINT project_browser_logout_intera_project_id_application_sessi_fkey + FOREIGN KEY (project_id, application_session_id, application_id, user_id) + REFERENCES public.application_sessions(project_id, id, application_id, user_id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.project_browser_logout_interactions + VALIDATE CONSTRAINT project_browser_logout_intera_project_id_application_sessi_fkey; + +ALTER TABLE public.project_user_merge_tombstones + DROP CONSTRAINT project_user_merge_tombstones_intent_fk; +ALTER TABLE public.project_user_merge_tombstones + ADD CONSTRAINT project_user_merge_tombstones_intent_fk + FOREIGN KEY (project_id, identity_mutation_intent_id) + REFERENCES public.identity_mutation_intents(project_id, id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.project_user_merge_tombstones + VALIDATE CONSTRAINT project_user_merge_tombstones_intent_fk; + +ALTER TABLE public.provider_callback_owners + DROP CONSTRAINT provider_callback_owners_project_id_provider_configuration_fkey; +ALTER TABLE public.provider_callback_owners + ADD CONSTRAINT provider_callback_owners_project_id_provider_configuration_fkey + FOREIGN KEY (project_id, provider_configuration_id) + REFERENCES public.provider_configurations(project_id, id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.provider_callback_owners + VALIDATE CONSTRAINT provider_callback_owners_project_id_provider_configuration_fkey; + +ALTER TABLE public.webhook_application_dispatch_state + DROP CONSTRAINT webhook_dispatch_application_fk; +ALTER TABLE public.webhook_application_dispatch_state + ADD CONSTRAINT webhook_dispatch_application_fk + FOREIGN KEY (project_id, application_id) + REFERENCES public.applications(project_id, id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.webhook_application_dispatch_state + VALIDATE CONSTRAINT webhook_dispatch_application_fk; + +ALTER TABLE public.webhook_endpoints + DROP CONSTRAINT webhook_endpoint_application_fk; +ALTER TABLE public.webhook_endpoints + ADD CONSTRAINT webhook_endpoint_application_fk + FOREIGN KEY (project_id, application_id) + REFERENCES public.applications(project_id, id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.webhook_endpoints + VALIDATE CONSTRAINT webhook_endpoint_application_fk; + +ALTER TABLE public.webhook_deliveries + DROP CONSTRAINT webhook_delivery_event_fk; +ALTER TABLE public.webhook_deliveries + ADD CONSTRAINT webhook_delivery_event_fk + FOREIGN KEY (project_id, application_id, event_id) + REFERENCES public.application_user_events(project_id, application_id, id) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.webhook_deliveries + VALIDATE CONSTRAINT webhook_delivery_event_fk; + +-- These two blocker tables already cascade through their natural parents. A direct root edge is +-- retained only as an insertion fence after the finalizer locks the Project and proves no cleanup +-- blocker remains. +ALTER TABLE public.key_provisioning_operations + ADD CONSTRAINT key_provisioning_operations_project_root_fkey + FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE public.key_provisioning_operations + VALIDATE CONSTRAINT key_provisioning_operations_project_root_fkey; + +ALTER TABLE public.managed_provider_credentials + ADD CONSTRAINT managed_provider_credentials_project_root_fkey + FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE public.managed_provider_credentials + VALIDATE CONSTRAINT managed_provider_credentials_project_root_fkey; + +-- Secret generations and cleanup operations carry no direct Project edge. Cascade their natural +-- endpoint ownership so the Project root reaches that child-only graph. +ALTER TABLE public.webhook_secret_cleanup_operations + DROP CONSTRAINT webhook_secret_cleanup_generation_fk; +ALTER TABLE public.webhook_secret_cleanup_operations + ADD CONSTRAINT webhook_secret_cleanup_generation_fk + FOREIGN KEY (endpoint_id, generation) + REFERENCES public.webhook_secret_generations(endpoint_id, generation) + ON DELETE CASCADE NOT VALID; +ALTER TABLE public.webhook_secret_cleanup_operations + VALIDATE CONSTRAINT webhook_secret_cleanup_generation_fk; + +ALTER TABLE public.webhook_secret_generations + DROP CONSTRAINT webhook_secret_generations_endpoint_id_fkey; +ALTER TABLE public.webhook_secret_generations + ADD CONSTRAINT webhook_secret_generations_endpoint_id_fkey + FOREIGN KEY (endpoint_id) REFERENCES public.webhook_endpoints(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE public.webhook_secret_generations + VALIDATE CONSTRAINT webhook_secret_generations_endpoint_id_fkey; + +-- Cross-links inside the cascade closure remain NO ACTION authorities for ordinary operations. +-- Make them deferrable but initially immediate so only the reviewed finalizer can postpone their +-- ordering checks while the complete graph is removed. +ALTER TABLE public.application_sessions + ALTER CONSTRAINT application_sessions_credential_user_fk DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT application_sessions_project_id_browser_session_id_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.application_user_events + ALTER CONSTRAINT application_user_event_application_fk DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT application_user_event_historical_user_fk DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.email_challenges + ALTER CONSTRAINT email_challenges_project_id_smtp_configuration_id_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.handoff_tickets + ALTER CONSTRAINT handoff_tickets_project_id_application_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT handoff_tickets_project_id_browser_session_id_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT handoff_tickets_project_id_provider_configuration_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT handoff_tickets_project_id_user_id_fkey DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.identity_mutation_create_results + ALTER CONSTRAINT identity_mutation_create_results_idempotency_key_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.identity_mutation_intents + ALTER CONSTRAINT identity_mutation_intents_project_id_destination_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_intents_project_id_identity_owner_user_i_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_intents_project_id_loser_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_intents_project_id_primary_email_identit_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_intents_project_id_primary_provider_iden_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_intents_project_id_winner_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.identity_mutation_proof_slots + ALTER CONSTRAINT identity_mutation_proof_slots_project_id_application_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_proof_slots_project_id_application_id_pr_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_proof_slots_project_id_email_assignment__fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_proof_slots_project_id_existing_email_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_proof_slots_project_id_existing_provider_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_proof_slots_project_id_proof_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_mutation_proof_slots_project_id_provider_configur_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.identity_proof_receipts + ALTER CONSTRAINT identity_proof_receipts_project_id_email_identity_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_proof_receipts_project_id_proof_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT identity_proof_receipts_project_id_provider_identity_id_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.linked_identities + ALTER CONSTRAINT linked_identities_project_id_created_via_provider_configur_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.login_email_method_snapshots + ALTER CONSTRAINT login_email_method_snapshots_project_id_application_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT login_email_method_snapshots_project_id_smtp_configuration_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.login_transaction_methods + ALTER CONSTRAINT login_transaction_methods_project_id_provider_configuratio_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.login_transactions + ALTER CONSTRAINT login_transactions_project_id_provider_configuration_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT login_transactions_project_id_user_id_fkey DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.mail_outbox + ALTER CONSTRAINT mail_outbox_project_id_smtp_configuration_id_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.managed_provider_connections + ALTER CONSTRAINT managed_provider_connections_project_id_provider_configura_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.managed_provider_reauthorization_interactions + ALTER CONSTRAINT managed_provider_reauthorizat_project_id_application_id_pr_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT managed_provider_reauthorizat_project_id_provider_configur_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT managed_provider_reauthorization_project_id_application_id_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.managed_reauthorization_create_results + ALTER CONSTRAINT managed_reauthorization_create_results_idempotency_key_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.project_browser_logout_interactions + ALTER CONSTRAINT project_browser_logout_intera_project_id_browser_session_i_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.project_user_merge_tombstones + ALTER CONSTRAINT project_user_merge_tombstones_primary_email_fk + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT project_user_merge_tombstones_primary_provider_fk + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT project_user_merge_tombstones_project_id_loser_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT project_user_merge_tombstones_project_id_winner_user_id_fkey + DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.project_users + ALTER CONSTRAINT project_users_primary_email_identity_fk DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT project_users_primary_profile_identity_fk DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE public.webhook_deliveries + ALTER CONSTRAINT webhook_deliveries_endpoint_id_fkey DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT webhook_deliveries_replay_of_delivery_id_fkey + DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT webhook_delivery_dispatch_state_fk DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT webhook_delivery_replay_parent_fk DEFERRABLE INITIALLY IMMEDIATE, + ALTER CONSTRAINT webhook_delivery_scope_fk DEFERRABLE INITIALLY IMMEDIATE; + +-- Reject accidental direct Project deletion unless the reviewed finalizer has selected this exact +-- row and installed its transaction-local cascade context. +CREATE FUNCTION public.owlauth_require_project_finalizer() RETURNS trigger + LANGUAGE plpgsql + SET search_path = pg_catalog + AS $$ +BEGIN + IF NOT public.owlauth_project_deletion_allowed(OLD.id) THEN + RAISE EXCEPTION 'Project deletion requires the reviewed finalizer' + USING ERRCODE = '42501'; + END IF; + RETURN OLD; +END +$$; + +CREATE TRIGGER projects_require_finalizer + BEFORE DELETE ON public.projects + FOR EACH ROW EXECUTE FUNCTION public.owlauth_require_project_finalizer(); + +-- This owner-owned, fixed-search-path function selects and locks one eligible Project, repeats +-- every cleanup proof under that lock, enters the +-- exact trigger context, cascades the complete graph, and appends detached deployment audit in the +-- same transaction. A NULL result means no Project is currently safe to finalize. +CREATE FUNCTION public.owlauth_finalize_project_deletion( + audit_id uuid, + correlation_id uuid +) RETURNS uuid + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = pg_catalog, public + AS $$ +DECLARE + target_project_id uuid; +BEGIN + IF pg_catalog.current_setting('transaction_isolation') <> 'read committed' THEN + RAISE EXCEPTION 'Project finalization requires READ COMMITTED isolation' + USING ERRCODE = '25001'; + END IF; + + FOR target_project_id IN + SELECT candidate.id + FROM public.projects AS candidate + WHERE candidate.status = 'deleting' + ORDER BY candidate.deletion_requested_at, candidate.id + FOR UPDATE OF candidate SKIP LOCKED + LOOP + -- Each statement in a READ COMMITTED function receives a fresh snapshot. Repeat every + -- provider-cleanup proof only after the Project row is locked so a writer that committed + -- while finalization waited cannot be hidden behind the candidate-selection snapshot. + IF EXISTS ( + SELECT 1 + FROM public.protected_materials AS material + WHERE material.project_id = target_project_id + AND material.state <> 'erased' + ) OR EXISTS ( + SELECT 1 + FROM public.managed_provider_credentials AS credential + WHERE credential.project_id = target_project_id + AND credential.ciphertext IS NOT NULL + ) OR EXISTS ( + SELECT 1 + FROM public.key_provisioning_operations AS operation + WHERE operation.project_id = target_project_id + AND operation.state IN ( + 'prepared', 'submitted', 'stored', 'cleanup_pending', + 'cleanup_leased', 'cleanup_blocked' + ) + ) + THEN + CONTINUE; + END IF; + + PERFORM pg_catalog.set_config( + 'owlauth.project_deletion_id', target_project_id::text, true + ); + SET CONSTRAINTS ALL DEFERRED; + -- Attempt rows are append-only and do not carry project_id. Delete them while their delivery + -- parent is still visible so the immutable-history trigger can derive and verify the exact + -- finalizer context; the remaining webhook graph then follows reviewed cascading edges. + DELETE FROM public.webhook_delivery_attempts AS attempt + USING public.webhook_deliveries AS delivery + WHERE attempt.delivery_id = delivery.id + AND delivery.project_id = target_project_id; + DELETE FROM public.projects + WHERE id = target_project_id + AND status = 'deleting'; + IF NOT FOUND THEN + RAISE EXCEPTION 'Project finalization lost its locked target' + USING ERRCODE = '40001'; + END IF; + + INSERT INTO public.audit_events( + id, project_id, actor_kind, action, target_kind, target_id, + outcome, correlation_id, safe_context + ) VALUES ( + audit_id, NULL, 'deployment_operator', 'project.deleted', 'project', + target_project_id, 'succeeded', correlation_id, '{}'::pg_catalog.jsonb + ); + RETURN target_project_id; + END LOOP; + RETURN NULL; +END +$$; + +REVOKE ALL ON FUNCTION public.owlauth_finalize_project_deletion(uuid, uuid) FROM PUBLIC; diff --git a/crates/owlauth-server/src/adapters/postgres/capability_journeys.rs b/crates/owlauth-server/src/adapters/postgres/capability_journeys.rs index b7a3860..322c5b5 100644 --- a/crates/owlauth-server/src/adapters/postgres/capability_journeys.rs +++ b/crates/owlauth-server/src/adapters/postgres/capability_journeys.rs @@ -234,8 +234,10 @@ impl SigningKeyProvisioner for StatefulRemoteSigningProvider { RetryClassification::Reconcile, ) })?; + let next_security_revision = project.security_revision + 1; let mut project = project.into_active_model(); project.status = Set("disabled".to_owned()); + project.security_revision = Set(next_security_revision); project.update(database).await.map_err(|_| { ProviderError::new( ProviderErrorClass::Unavailable, @@ -477,7 +479,7 @@ fn unavailable_or_fail(error: impl std::fmt::Display) -> bool { clippy::too_many_lines, reason = "integration fixture enumerates the complete split-plane key configuration" )] -fn server_config(migration_url: &str, runtime_url: &str, control_url: &str) -> ServerConfig { +fn server_config(database_url: &str) -> ServerConfig { let values = BTreeMap::from([ ("OWLAUTH_MODE".to_owned(), "all".to_owned()), ( @@ -492,15 +494,11 @@ fn server_config(migration_url: &str, runtime_url: &str, control_url: &str) -> S "OWLAUTH_SOFTWARE_CUSTODY_KEY".to_owned(), "Hh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4".to_owned(), ), - ( - "OWLAUTH_MIGRATION_OWNER_ROLE".to_owned(), - "owlauth_owner".to_owned(), - ), ( "OWLAUTH_DATABASE_LOCK_TIMEOUT_MS".to_owned(), "250".to_owned(), ), - ("OWLAUTH_POSTGRES_URL".to_owned(), runtime_url.to_owned()), + ("OWLAUTH_POSTGRES_URL".to_owned(), database_url.to_owned()), ("OWLAUTH_RUNTIME_KEY_VERSION".to_owned(), "1".to_owned()), ( "OWLAUTH_RUNTIME_DIGEST_KEY".to_owned(), @@ -570,22 +568,6 @@ fn server_config(migration_url: &str, runtime_url: &str, control_url: &str) -> S "OWLAUTH_SERVER_KEY_DIGEST_KEY".to_owned(), "WlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlo".to_owned(), ), - ( - "OWLAUTH_RUNTIME_POSTGRES_URL".to_owned(), - runtime_url.to_owned(), - ), - ( - "OWLAUTH_SERVER_POSTGRES_URL".to_owned(), - runtime_url.to_owned(), - ), - ( - "OWLAUTH_CONTROL_POSTGRES_URL".to_owned(), - control_url.to_owned(), - ), - ( - "OWLAUTH_MIGRATION_POSTGRES_URL".to_owned(), - migration_url.to_owned(), - ), ( "OWLAUTH_RUNTIME_DATABASE_MAX_CONNECTIONS".to_owned(), "2".to_owned(), @@ -2250,16 +2232,250 @@ async fn verify_pools_unit_of_work_and_egress(config: &ServerConfig) -> Database pools } +#[allow( + clippy::too_many_lines, + reason = "the upgrade proof deliberately installs every immutable baseline, applies the ordered migration, introspects graph closure, and exercises the finalizer together" +)] +async fn verify_deployed_baseline_upgrade(host: &str, port: u16) { + const FOUNDATION: &str = include_str!("../../../migrations/20260806000000_foundation.sql"); + const AUTHORITIES: &str = include_str!("../../../migrations/20260806001000_authorities.sql"); + const INVARIANTS: &str = include_str!("../../../migrations/20260806002000_invariants.sql"); + const PROJECT_LIFECYCLE: &str = + include_str!("../../../migrations/20260810000000_project_lifecycle.sql"); + + let admin_url = format!("postgres://owlauth:owlauth_test@{host}:{port}/owlauth_test"); + let mut admin = PgConnection::connect(&admin_url) + .await + .expect("upgrade-proof administrator should connect"); + sqlx::query("CREATE DATABASE owlauth_deployed_upgrade") + .execute(&mut admin) + .await + .expect("isolated deployed-schema upgrade database should be created"); + admin + .close() + .await + .expect("upgrade-proof administrator should close"); + + let upgrade_url = + format!("postgres://owlauth:owlauth_test@{host}:{port}/owlauth_deployed_upgrade"); + let mut connection = PgConnection::connect(&upgrade_url) + .await + .expect("deployed-schema upgrade connection should open"); + sqlx::raw_sql(FOUNDATION) + .execute(&mut connection) + .await + .expect("the immutable deployed foundation should install"); + sqlx::raw_sql(AUTHORITIES) + .execute(&mut connection) + .await + .expect("the immutable deployed singleton authorities should install"); + sqlx::raw_sql(INVARIANTS) + .execute(&mut connection) + .await + .expect("the immutable deployed invariants should install"); + sqlx::query( + "INSERT INTO projects(id,public_id,status,metadata_revision,security_revision,display_name) + VALUES('20000000-0000-0000-0000-000000000001','upgrade_fixture','active',1,1, + 'Deployed upgrade fixture')", + ) + .execute(&mut connection) + .await + .expect("pre-upgrade Project fixture should satisfy the deployed schema"); + + sqlx::raw_sql(PROJECT_LIFECYCLE) + .execute(&mut connection) + .await + .expect("the ordered Project lifecycle migration should upgrade deployed schema in place"); + let deletion_requested_at: Option = sqlx::query_scalar( + "SELECT deletion_requested_at FROM projects + WHERE id='20000000-0000-0000-0000-000000000001'", + ) + .fetch_one(&mut connection) + .await + .expect("upgraded Project should remain readable"); + assert_eq!(deletion_requested_at, None); + sqlx::query( + "UPDATE projects SET status='disabled',security_revision=2 + WHERE id='20000000-0000-0000-0000-000000000001'", + ) + .execute(&mut connection) + .await + .expect("upgraded deployed Project should support disable"); + assert!( + sqlx::query( + "UPDATE projects SET status='active' + WHERE id='20000000-0000-0000-0000-000000000001'", + ) + .execute(&mut connection) + .await + .is_err(), + "the upgrade must enforce the security-revision lifecycle fence" + ); + sqlx::query( + "UPDATE projects SET status='active',security_revision=3 + WHERE id='20000000-0000-0000-0000-000000000001'", + ) + .execute(&mut connection) + .await + .expect("upgraded deployed Project should support revision-fenced enable"); + let unvalidated_project_roots: i64 = sqlx::query_scalar( + "SELECT count(*) FROM pg_constraint + WHERE conname LIKE '%\\_project\\_root\\_fkey' ESCAPE '\\' AND NOT convalidated", + ) + .fetch_one(&mut connection) + .await + .expect("Project deletion root constraints should be inspectable"); + assert_eq!(unvalidated_project_roots, 0); + let project_owned_tables_outside_cascade_closure: Vec = sqlx::query_scalar( + "WITH RECURSIVE project_roots(relation_id) AS ( + SELECT DISTINCT relation.oid + FROM pg_attribute AS attribute + JOIN pg_class AS relation ON relation.oid = attribute.attrelid + JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace + WHERE namespace.nspname = 'public' + AND relation.relkind IN ('r', 'p') + AND attribute.attname = 'project_id' + AND attribute.attnotnull + AND NOT attribute.attisdropped + UNION + SELECT 'public.projects'::regclass::oid + ), project_owned(relation_id) AS ( + SELECT relation_id FROM project_roots + UNION + SELECT foreign_key.conrelid + FROM pg_constraint AS foreign_key + JOIN project_owned AS parent + ON parent.relation_id = foreign_key.confrelid + JOIN pg_class AS child ON child.oid = foreign_key.conrelid + JOIN pg_namespace AS namespace ON namespace.oid = child.relnamespace + WHERE foreign_key.contype = 'f' + AND namespace.nspname = 'public' + AND NOT EXISTS ( + SELECT 1 + FROM unnest(foreign_key.conkey) AS key_column(attnum) + JOIN pg_attribute AS key_attribute + ON key_attribute.attrelid = foreign_key.conrelid + AND key_attribute.attnum = key_column.attnum + WHERE NOT key_attribute.attnotnull + ) + ), cascade_closure(relation_id) AS ( + SELECT 'public.projects'::regclass::oid + UNION + SELECT foreign_key.conrelid + FROM pg_constraint AS foreign_key + JOIN cascade_closure AS parent + ON parent.relation_id = foreign_key.confrelid + JOIN pg_class AS child ON child.oid = foreign_key.conrelid + JOIN pg_namespace AS namespace ON namespace.oid = child.relnamespace + WHERE foreign_key.contype = 'f' + AND foreign_key.confdeltype = 'c' + AND namespace.nspname = 'public' + AND NOT EXISTS ( + SELECT 1 + FROM unnest(foreign_key.conkey) AS key_column(attnum) + JOIN pg_attribute AS key_attribute + ON key_attribute.attrelid = foreign_key.conrelid + AND key_attribute.attnum = key_column.attnum + WHERE NOT key_attribute.attnotnull + ) + ) + SELECT format('%I.%I', namespace.nspname, relation.relname) + FROM project_owned + JOIN pg_class AS relation ON relation.oid = project_owned.relation_id + JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace + WHERE project_owned.relation_id <> 'public.projects'::regclass + AND relation.relname <> 'webhook_delivery_attempts' + AND NOT EXISTS ( + SELECT 1 FROM cascade_closure + WHERE cascade_closure.relation_id = project_owned.relation_id + ) + ORDER BY namespace.nspname, relation.relname", + ) + .fetch_all(&mut connection) + .await + .expect("the recursive Project-owned deletion graph should be introspectable"); + assert!( + project_owned_tables_outside_cascade_closure.is_empty(), + "Project-owned tables fall outside the cascade or explicit-delete closure: \ + {project_owned_tables_outside_cascade_closure:?}" + ); + let immediate_internal_deletion_blockers: Vec = sqlx::query_scalar( + "WITH RECURSIVE cascade_closure(relation_id) AS ( + SELECT 'public.projects'::regclass::oid + UNION + SELECT foreign_key.conrelid + FROM pg_constraint AS foreign_key + JOIN cascade_closure AS parent + ON parent.relation_id = foreign_key.confrelid + JOIN pg_class AS child ON child.oid = foreign_key.conrelid + JOIN pg_namespace AS namespace ON namespace.oid = child.relnamespace + WHERE foreign_key.contype = 'f' + AND foreign_key.confdeltype = 'c' + AND namespace.nspname = 'public' + ) + SELECT format('%I.%I', child.relname, foreign_key.conname) + FROM pg_constraint AS foreign_key + JOIN cascade_closure AS child_closure + ON child_closure.relation_id = foreign_key.conrelid + JOIN cascade_closure AS parent_closure + ON parent_closure.relation_id = foreign_key.confrelid + JOIN pg_class AS child ON child.oid = foreign_key.conrelid + WHERE foreign_key.contype = 'f' + AND foreign_key.confdeltype IN ('a', 'r') + AND NOT foreign_key.condeferrable + AND child.relname <> 'webhook_delivery_attempts' + ORDER BY child.relname, foreign_key.conname", + ) + .fetch_all(&mut connection) + .await + .expect("Project cascade ordering constraints should be introspectable"); + assert!( + immediate_internal_deletion_blockers.is_empty(), + "immediate foreign keys can block Project cascade ordering: \ + {immediate_internal_deletion_blockers:?}" + ); + + sqlx::query( + "UPDATE projects + SET status='deleting',security_revision=4, + deletion_requested_at=transaction_timestamp() + WHERE id='20000000-0000-0000-0000-000000000001'", + ) + .execute(&mut connection) + .await + .expect("upgraded deployed Project should enter terminal deletion"); + let finalized: Option = sqlx::query_scalar( + "SELECT public.owlauth_finalize_project_deletion( + '20000000-0000-0000-0000-000000000002', + '20000000-0000-0000-0000-000000000003')", + ) + .fetch_one(&mut connection) + .await + .expect("the owner-executed finalizer should delete an eligible upgraded Project"); + assert_eq!( + finalized, + Some(Uuid::parse_str("20000000-0000-0000-0000-000000000001").unwrap()) + ); + let remaining: i64 = sqlx::query_scalar( + "SELECT count(*) FROM projects + WHERE id='20000000-0000-0000-0000-000000000001'", + ) + .fetch_one(&mut connection) + .await + .expect("upgraded Project absence should be queryable"); + assert_eq!(remaining, 0); + connection + .close() + .await + .expect("deployed-schema upgrade connection should close"); +} + #[allow( clippy::too_many_lines, reason = "the migration journey keeps lock, ownership, and history checks together" )] -async fn migrate_and_verify_main_database( - admin_url: &str, - runtime_url: &str, - control_url: &str, -) -> ServerConfig { - let mut config = server_config(admin_url, runtime_url, control_url); +async fn migrate_and_verify_main_database(admin_url: &str) -> ServerConfig { + let mut config = server_config(admin_url); config.postgres.migration_mode = MigrationMode::Verify; assert_eq!( @@ -2303,13 +2519,15 @@ async fn migrate_and_verify_main_database( let mut ownership_connection = PgConnection::connect(admin_url) .await .expect("ownership query connection should open"); - let schema_owner: String = sqlx::query_scalar( - "SELECT pg_get_userbyid(nspowner) FROM pg_namespace WHERE nspname = 'public'", + let finalizer_owner: String = sqlx::query_scalar( + "SELECT pg_get_userbyid(proowner) + FROM pg_proc + WHERE oid = 'public.owlauth_finalize_project_deletion(uuid,uuid)'::regprocedure", ) .fetch_one(&mut ownership_connection) .await - .expect("schema owner should be queryable"); - assert_eq!(schema_owner, "owlauth_owner"); + .expect("Project finalizer owner should be queryable"); + assert_eq!(finalizer_owner, "owlauth"); let table_owners: Vec = sqlx::query_scalar( "SELECT DISTINCT tableowner FROM pg_tables \ WHERE schemaname = 'public' AND tablename IN (\ @@ -2320,23 +2538,7 @@ async fn migrate_and_verify_main_database( .fetch_all(&mut ownership_connection) .await .expect("migration-created table owners should be queryable"); - assert_eq!(table_owners, vec!["owlauth_owner".to_owned()]); - sqlx::query( - "REVOKE INSERT, UPDATE, DELETE ON _sqlx_migrations \ - FROM owlauth_runtime, owlauth_control", - ) - .execute(&mut ownership_connection) - .await - .expect("serving roles should retain read-only migration history access"); - let history_privileges: (bool, bool) = sqlx::query_as( - "SELECT \ - has_table_privilege('owlauth_runtime', '_sqlx_migrations', 'SELECT'), \ - has_table_privilege('owlauth_runtime', '_sqlx_migrations', 'INSERT,UPDATE,DELETE')", - ) - .fetch_one(&mut ownership_connection) - .await - .expect("serving history grants should be queryable"); - assert_eq!(history_privileges, (true, false)); + assert_eq!(table_owners, vec![finalizer_owner]); let provider_constraints: Vec<(String, String)> = sqlx::query_as( "SELECT conname, pg_get_constraintdef(oid) \ FROM pg_constraint \ @@ -3434,6 +3636,406 @@ async fn verify_server_key_and_listener_journeys( assert!(second_page.iter().all(|key| !first_ids.contains(&key.id))); } +#[allow( + clippy::too_many_lines, + reason = "the end-to-end Project lifecycle fixture keeps its deletion fence, provider cleanup, physical deletion, and detached audit assertions together" +)] +async fn verify_project_lifecycle_and_permanent_deletion( + provisioning: &ProvisioningService, + control: &DatabaseConnection, + database_url: &str, +) { + let project = provisioning + .create_project( + CreateProject { + display_name: "Project lifecycle deletion".to_owned(), + belongs_to: None, + idempotency_key: "project-lifecycle-deletion-12345678".to_owned(), + }, + Uuid::new_v4(), + ) + .await + .expect("lifecycle Project should be created"); + for _ in 0..10 { + provisioning + .reconcile_signing_key_lifecycle(100) + .await + .expect("initial signing lifecycle should reconcile"); + let keys = provisioning + .list_signing_keys(project.id) + .await + .expect("lifecycle Project signing keys should list"); + if keys.iter().any(|key| key.state == "active") { + break; + } + } + let signing_material_id = project_signing_key::Entity::find() + .filter(project_signing_key::Column::ProjectId.eq(project.id)) + .one(control) + .await + .expect("lifecycle signing material owner query should work") + .expect("lifecycle Project should own a signing key") + .signer_material_id; + + let managed_connection_id = Uuid::new_v4(); + let managed_operation_id = Uuid::new_v4(); + let managed_attempt_id = Uuid::new_v4(); + let managed_lease_owner = Uuid::new_v4(); + let webhook_endpoint_id = Uuid::new_v4(); + let webhook_delivery_id = Uuid::new_v4(); + let webhook_event_id = Uuid::new_v4(); + let webhook_cleanup_id = Uuid::new_v4(); + let webhook_application_id = Uuid::new_v4(); + let webhook_user_id = Uuid::new_v4(); + let webhook_binding_id = Uuid::new_v4(); + let mut owner_fixture = PgConnection::connect(database_url) + .await + .expect("managed-credential deletion fixture connection should open"); + sqlx::query("SET session_replication_role=replica") + .execute(&mut owner_fixture) + .await + .expect("managed-credential fixture should disable unrelated graph triggers"); + sqlx::query( + "INSERT INTO managed_provider_credentials( + project_id,connection_id,connection_generation,credential_generation, + key_version,ciphertext,created_at) + VALUES($1,$2,1,1,1,decode(repeat('01',40),'hex'),transaction_timestamp())", + ) + .bind(project.id) + .bind(managed_connection_id) + .execute(&mut owner_fixture) + .await + .expect("live managed credential fixture should insert"); + sqlx::query( + "INSERT INTO managed_provider_renewal_operations( + id,project_id,connection_id,expected_connection_generation, + expected_credential_generation,successor_connection_generation, + successor_credential_generation,attempt_id,state,adapter_idempotent_replay, + lease_owner,lease_expires_at,safe_outcome,prepared_at,submitted_at,updated_at) + VALUES($1,$2,$3,1,1,2,2,$4,'submitted',true,$5, + transaction_timestamp()+interval '1 minute','submitted', + transaction_timestamp(),transaction_timestamp(),transaction_timestamp())", + ) + .bind(managed_operation_id) + .bind(project.id) + .bind(managed_connection_id) + .bind(managed_attempt_id) + .bind(managed_lease_owner) + .execute(&mut owner_fixture) + .await + .expect("in-flight managed renewal fixture should insert"); + sqlx::query( + "INSERT INTO applications( + id,project_id,public_id,status,revision,display_name,application_type, + metadata_revision,security_revision) + VALUES($1,$2,'app_lifecycle_cleanup','active',1,'Lifecycle cleanup','web',1,1)", + ) + .bind(webhook_application_id) + .bind(project.id) + .execute(&mut owner_fixture) + .await + .expect("webhook Application deletion fixture should insert"); + sqlx::query( + "INSERT INTO project_users( + id,project_id,public_id,status,user_revision,security_revision, + base_profile_digest,primary_source_kind) + VALUES($1,$2,'usr_lifecycle_cleanup','active',1,1, + decode(repeat('05',32),'hex'),'provider')", + ) + .bind(webhook_user_id) + .bind(project.id) + .execute(&mut owner_fixture) + .await + .expect("webhook user deletion fixture should insert"); + sqlx::query( + "INSERT INTO application_user_bindings( + id,project_id,application_id,user_id,status,binding_revision) + VALUES($1,$2,$3,$4,'active',1)", + ) + .bind(webhook_binding_id) + .bind(project.id) + .bind(webhook_application_id) + .bind(webhook_user_id) + .execute(&mut owner_fixture) + .await + .expect("webhook binding deletion fixture should insert"); + sqlx::query( + "INSERT INTO application_user_events( + id,event_id,project_id,application_id,binding_id,user_id,event_type, + user_revision,projection_revision,projection_schema,safe_body, + canonical_body_digest,occurred_at,replay_until,retain_until,created_at) + VALUES($1,'evt_lifecycle_cleanup',$2,$3,$4,$5,'user.projection.updated', + 1,1,'owlauth.user.v1', + '{\"data\":{\"projection\":{\"verified_email\":null}}}'::jsonb, + decode(repeat('04',32),'hex'),transaction_timestamp(), + transaction_timestamp()+interval '1 hour', + transaction_timestamp()+interval '2 hours',transaction_timestamp())", + ) + .bind(webhook_event_id) + .bind(project.id) + .bind(webhook_application_id) + .bind(webhook_binding_id) + .bind(webhook_user_id) + .execute(&mut owner_fixture) + .await + .expect("webhook event deletion fixture should insert"); + sqlx::query( + "INSERT INTO webhook_endpoints( + id,project_id,application_id,public_id,idempotency_key, + secret_request_fingerprint,url,subscribed_event_types,status,revision, + created_at,updated_at,disabled_at) + VALUES($1,$2,$3,'whk_lifecycle_cleanup','webhook-lifecycle-cleanup', + decode(repeat('02',32),'hex'),'https://webhook.example/lifecycle', + ARRAY['user.projection.updated'],'disabled',1, + transaction_timestamp(),transaction_timestamp(),transaction_timestamp())", + ) + .bind(webhook_endpoint_id) + .bind(project.id) + .bind(webhook_application_id) + .execute(&mut owner_fixture) + .await + .expect("webhook endpoint deletion fixture should insert"); + sqlx::query( + "INSERT INTO webhook_secret_generations( + endpoint_id,generation,idempotency_key,request_fingerprint,state, + created_at,retired_at,material_id) + VALUES($1,1,'webhook-generation-cleanup',decode(repeat('03',32),'hex'), + 'retired',transaction_timestamp(),transaction_timestamp(),$2)", + ) + .bind(webhook_endpoint_id) + .bind(signing_material_id) + .execute(&mut owner_fixture) + .await + .expect("webhook secret-generation deletion fixture should insert"); + sqlx::query( + "INSERT INTO webhook_secret_cleanup_operations( + id,endpoint_id,generation,state,erased_at,material_id) + VALUES($1,$2,1,'erased',transaction_timestamp(),$3)", + ) + .bind(webhook_cleanup_id) + .bind(webhook_endpoint_id) + .bind(signing_material_id) + .execute(&mut owner_fixture) + .await + .expect("webhook secret-cleanup deletion fixture should insert"); + sqlx::query( + "INSERT INTO webhook_deliveries( + id,project_id,application_id,endpoint_id,event_id,state,attempt_count, + next_attempt_at,created_at,updated_at,terminal_at) + VALUES($1,$2,$3,$4,$5,'terminal',1,transaction_timestamp(), + transaction_timestamp(),transaction_timestamp(),transaction_timestamp())", + ) + .bind(webhook_delivery_id) + .bind(project.id) + .bind(webhook_application_id) + .bind(webhook_endpoint_id) + .bind(webhook_event_id) + .execute(&mut owner_fixture) + .await + .expect("webhook delivery deletion fixture should insert"); + sqlx::query( + "INSERT INTO webhook_delivery_attempts( + delivery_id,attempt_number,lease_generation,attempted_at,attempt_timestamp, + outcome_class,http_status,duration_millis,correlation_id) + VALUES($1,1,1,transaction_timestamp(),1,'http_4xx',400,1,$2)", + ) + .bind(webhook_delivery_id) + .bind(Uuid::new_v4()) + .execute(&mut owner_fixture) + .await + .expect("webhook delivery-attempt deletion fixture should insert"); + sqlx::query("SET session_replication_role=origin") + .execute(&mut owner_fixture) + .await + .expect("managed-credential fixture should restore graph triggers"); + owner_fixture + .close() + .await + .expect("managed-credential fixture connection should close"); + + let disabled = provisioning + .disable_project(project.id, project.security_revision, Uuid::new_v4()) + .await + .expect("active Project should disable"); + assert_eq!(disabled.status, "disabled"); + let enabled = provisioning + .enable_project(project.id, disabled.security_revision, Uuid::new_v4()) + .await + .expect("disabled Project should re-enable"); + assert_eq!(enabled.status, "active"); + assert_eq!(enabled.security_revision, project.security_revision + 2); + + let deleting = provisioning + .delete_project(project.id, enabled.security_revision, Uuid::new_v4()) + .await + .expect("active Project deletion should be accepted"); + assert_eq!(deleting.status, "deleting"); + assert_eq!(deleting.security_revision, enabled.security_revision + 1); + let replayed = provisioning + .delete_project(project.id, project.security_revision, Uuid::new_v4()) + .await + .expect("deleting Project request should replay current terminal state"); + assert_eq!(replayed, deleting); + + let mut control_sql = PgConnection::connect(database_url) + .await + .expect("Project-deletion control-role probe should connect"); + let managed_ciphertext: Option> = sqlx::query_scalar( + "SELECT ciphertext FROM managed_provider_credentials + WHERE project_id=$1 AND connection_id=$2 AND credential_generation=1", + ) + .bind(project.id) + .bind(managed_connection_id) + .fetch_one(&mut control_sql) + .await + .expect("managed credential should remain queryable until physical deletion"); + assert!( + managed_ciphertext.is_none(), + "Project deletion must crypto-erase locally renewable provider credentials" + ); + let renewal_state: (String, Option, Option) = sqlx::query_as( + "SELECT state,lease_owner,terminal_at FROM managed_provider_renewal_operations + WHERE id=$1", + ) + .bind(managed_operation_id) + .fetch_one(&mut control_sql) + .await + .expect("terminalized managed renewal should remain queryable"); + assert_eq!(renewal_state.0, "abandoned"); + assert!(renewal_state.1.is_none()); + assert!(renewal_state.2.is_some()); + + sqlx::query("BEGIN ISOLATION LEVEL REPEATABLE READ") + .execute(&mut control_sql) + .await + .expect("non-READ-COMMITTED finalizer probe should begin"); + let isolation_error = sqlx::query("SELECT public.owlauth_finalize_project_deletion($1,$2)") + .bind(Uuid::new_v4()) + .bind(Uuid::new_v4()) + .execute(&mut control_sql) + .await + .expect_err("the finalizer must reject a stale-snapshot isolation level"); + assert_eq!( + isolation_error + .as_database_error() + .and_then(sqlx::error::DatabaseError::code) + .as_deref(), + Some("25001") + ); + sqlx::query("ROLLBACK") + .execute(&mut control_sql) + .await + .expect("non-READ-COMMITTED finalizer probe should roll back"); + + assert!( + sqlx::query("DELETE FROM projects WHERE id=$1") + .bind(project.id) + .execute(&mut control_sql) + .await + .is_err(), + "direct Project DELETE must not bypass the finalizer" + ); + let deletion_audit_id: Uuid = sqlx::query_scalar( + "SELECT id FROM audit_events + WHERE project_id=$1 AND action='project.deletion_requested'", + ) + .bind(project.id) + .fetch_one(&mut control_sql) + .await + .expect("deletion-request audit should exist"); + assert!( + sqlx::query("DELETE FROM audit_events WHERE id=$1") + .bind(deletion_audit_id) + .execute(&mut control_sql) + .await + .is_err(), + "ordinary deletion must not bypass immutable Project audit" + ); + control_sql + .close() + .await + .expect("Project-deletion control-role probe should close"); + + assert_eq!( + provisioning + .update_project( + project.id, + UpdateProject { + display_name: "Must stay fenced".to_owned(), + belongs_to: None, + expected_metadata_revision: project.metadata_revision, + }, + Uuid::new_v4(), + ) + .await, + Err(ApplicationError::Disabled), + "deleting must fence ordinary Control writes immediately" + ); + assert_eq!( + provisioning + .finalize_project_deletions(100) + .await + .expect("premature deletion finalization should remain safe"), + 0, + "live signing material must block physical deletion" + ); + for _ in 0..10 { + provisioning + .reconcile_signing_key_lifecycle(100) + .await + .expect("deleting Project signing cleanup should reconcile"); + if provisioning.get_project(project.id).await == Err(ApplicationError::NotFound) { + break; + } + } + assert_eq!( + provisioning.get_project(project.id).await, + Err(ApplicationError::NotFound), + "Project row should be physically removed after provider cleanup" + ); + assert!( + protected_material::Entity::find_by_id(signing_material_id) + .one(control) + .await + .expect("deleted material lookup should work") + .is_none(), + "Project protected material should be physically removed" + ); + let completion = audit_event::Entity::find() + .filter(audit_event::Column::ProjectId.is_null()) + .filter(audit_event::Column::Action.eq("project.deleted")) + .filter(audit_event::Column::TargetId.eq(project.id)) + .one(control) + .await + .expect("detached deletion completion audit should be queryable"); + assert!(completion.is_some()); + let mut residue_sql = PgConnection::connect(database_url) + .await + .expect("deleted Project residue probe should connect"); + let webhook_residue: i64 = sqlx::query_scalar( + "SELECT + (SELECT COUNT(*) FROM webhook_delivery_attempts WHERE delivery_id=$1) + + (SELECT COUNT(*) FROM webhook_deliveries WHERE id=$1) + + (SELECT COUNT(*) FROM webhook_secret_cleanup_operations WHERE id=$2) + + (SELECT COUNT(*) FROM webhook_secret_generations WHERE endpoint_id=$3) + + (SELECT COUNT(*) FROM webhook_endpoints WHERE id=$3)", + ) + .bind(webhook_delivery_id) + .bind(webhook_cleanup_id) + .bind(webhook_endpoint_id) + .fetch_one(&mut residue_sql) + .await + .expect("deleted Project webhook graph should remain queryable as absent"); + residue_sql + .close() + .await + .expect("deleted Project residue probe should close"); + assert_eq!( + webhook_residue, 0, + "Project deletion must remove child-only webhook history and secret cleanup rows" + ); +} + #[allow( clippy::too_many_lines, reason = "one container preserves the ordered shared fixtures across named PostgreSQL capability journeys" @@ -3469,29 +4071,10 @@ async fn postgres_capability_journeys_are_real() { .await .expect("mapped port should be available"); let url = format!("postgres://owlauth:owlauth_test@{host}:{port}/owlauth_test"); - let mut owner_setup = PgConnection::connect(&url) - .await - .expect("owner setup connection should open"); - for statement in [ - "CREATE ROLE owlauth_owner NOLOGIN", - "CREATE ROLE owlauth_runtime LOGIN PASSWORD 'runtime_test'", - "CREATE ROLE owlauth_control LOGIN PASSWORD 'control_test'", - "GRANT owlauth_owner TO owlauth", - "ALTER SCHEMA public OWNER TO owlauth_owner", - "GRANT USAGE ON SCHEMA public TO owlauth_runtime, owlauth_control", - "ALTER DEFAULT PRIVILEGES FOR ROLE owlauth_owner IN SCHEMA public \ - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO owlauth_runtime, owlauth_control", - "ALTER DEFAULT PRIVILEGES FOR ROLE owlauth_owner IN SCHEMA public \ - GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO owlauth_runtime, owlauth_control", - ] { - sqlx::query(statement) - .execute(&mut owner_setup) - .await - .expect("owner and serving-role setup should succeed"); - } - let runtime_url = format!("postgres://owlauth_runtime:runtime_test@{host}:{port}/owlauth_test"); - let control_url = format!("postgres://owlauth_control:control_test@{host}:{port}/owlauth_test"); - let config = migrate_and_verify_main_database(&url, &runtime_url, &control_url).await; + let host_name = host.to_string(); + verify_deployed_baseline_upgrade(&host_name, port).await; + + let config = migrate_and_verify_main_database(&url).await; let pools = verify_pools_unit_of_work_and_egress(&config).await; let runtime = pools.runtime.as_ref().expect("Runtime pool should exist"); @@ -3957,8 +4540,10 @@ async fn postgres_capability_journeys_are_real() { .await .expect("disabled-cleanup Project query should work") .expect("disabled-cleanup Project should exist"); + let next_security_revision = disabled_cleanup_project_model.security_revision + 1; let mut disabled_project = disabled_cleanup_project_model.into_active_model(); disabled_project.status = Set("disabled".to_owned()); + disabled_project.security_revision = Set(next_security_revision); disabled_project .update(control) .await @@ -5135,10 +5720,12 @@ async fn postgres_capability_journeys_are_real() { &config, &pools, readiness.clone(), - control_url.clone(), + url.clone(), )) .await; + verify_project_lifecycle_and_permanent_deletion(&provisioning, control, &url).await; + control .execute_raw(Statement::from_string( DbBackend::Postgres, @@ -5202,7 +5789,7 @@ async fn postgres_capability_journeys_are_real() { if initially_deferred.contains(&key_id) ))); - verify_capacity_and_replay_limits(&control_url).await; + verify_capacity_and_replay_limits(&url).await; pools.close().await; } diff --git a/crates/owlauth-server/src/adapters/postgres/email_tests.rs b/crates/owlauth-server/src/adapters/postgres/email_tests.rs index 6b3b039..cff34f4 100644 --- a/crates/owlauth-server/src/adapters/postgres/email_tests.rs +++ b/crates/owlauth-server/src/adapters/postgres/email_tests.rs @@ -752,7 +752,7 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr }; for (mutation, expected) in [ ( - "UPDATE projects SET status='disabled' WHERE id=$1", + "UPDATE projects SET status='disabled', security_revision=security_revision + 1 WHERE id=$1", crate::application::ApplicationError::Disabled, ), ( @@ -806,6 +806,15 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr persisted, 0, "failed create must not snapshot stale authority" ); + sqlx::query( + "UPDATE projects + SET status='active', security_revision=security_revision + 1 + WHERE id=$1 AND status='disabled'", + ) + .bind(project_id) + .execute(&pool) + .await + .expect("restore disabled project before resetting revisions"); for restore in [ "UPDATE projects SET status='active',metadata_revision=1,security_revision=1 WHERE id=$1", "UPDATE applications SET status='active',security_revision=1 WHERE project_id=$1", @@ -868,11 +877,13 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr // Hold the Project mutation lock while selection reaches its owner fence. After the disable // commits, the blocked selection must reject without burning the login revision. let mut disable_project = pool.begin().await.expect("begin Project-disable race"); - sqlx::query("UPDATE projects SET status='disabled' WHERE id=$1") - .bind(project_id) - .execute(&mut *disable_project) - .await - .expect("hold Project disable lock"); + sqlx::query( + "UPDATE projects SET status='disabled',security_revision=security_revision + 1 WHERE id=$1", + ) + .bind(project_id) + .execute(&mut *disable_project) + .await + .expect("hold Project disable lock"); let racing_email = email.clone(); let racing_selection = selection.clone(); let racing_select = @@ -894,15 +905,22 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr .await .expect("racing selection rejection state"); assert_eq!(unchanged, ("awaiting_method_selection".to_owned(), 2)); - sqlx::query("UPDATE projects SET status='active' WHERE id=$1") + sqlx::query( + "UPDATE projects SET status='active',security_revision=security_revision + 1 WHERE id=$1", + ) + .bind(project_id) + .execute(&pool) + .await + .expect("restore Project after selection race"); + sqlx::query("UPDATE projects SET security_revision=1 WHERE id=$1") .bind(project_id) .execute(&pool) .await - .expect("restore Project after selection race"); + .expect("restore Project revision after selection race"); for (mutation, expected) in [ ( - "UPDATE projects SET status='disabled' WHERE id=$1", + "UPDATE projects SET status='disabled',security_revision=security_revision + 1 WHERE id=$1", crate::application::ApplicationError::RevisionConflict, ), ( @@ -951,6 +969,15 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr .await .expect("selection rejection state"); assert_eq!(unchanged, ("awaiting_method_selection".to_owned(), 2)); + sqlx::query( + "UPDATE projects + SET status='active', security_revision=security_revision + 1 + WHERE id=$1 AND status='disabled'", + ) + .bind(project_id) + .execute(&pool) + .await + .expect("restore disabled Project before resetting selection revisions"); for restore in [ "UPDATE projects SET status='active',metadata_revision=1,security_revision=1 WHERE id=$1", "UPDATE applications SET status='active',security_revision=1 WHERE project_id=$1", @@ -1048,7 +1075,7 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr .expect("restore Application after generation race"); for mutation in [ - "UPDATE projects SET status='disabled' WHERE id=$1", + "UPDATE projects SET status='disabled',security_revision=security_revision + 1 WHERE id=$1", "UPDATE projects SET metadata_revision=2 WHERE id=$1", "UPDATE projects SET security_revision=2 WHERE id=$1", "UPDATE applications SET status='disabled' WHERE project_id=$1", @@ -1074,6 +1101,15 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr .await .expect("rejected generation leaves no durable email work"); assert_eq!(unchanged, ("email_address_entry".to_owned(), 3, 0, 0)); + sqlx::query( + "UPDATE projects + SET status='active', security_revision=security_revision + 1 + WHERE id=$1 AND status='disabled'", + ) + .bind(project_id) + .execute(&pool) + .await + .expect("restore disabled Project before resetting generation revisions"); sqlx::query( "UPDATE projects SET status='active',metadata_revision=1,security_revision=1 WHERE id=$1", ) @@ -1294,8 +1330,8 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr for (disable, restore) in [ ( - "UPDATE projects SET status='disabled' WHERE id=$1", - "UPDATE projects SET status='active' WHERE id=$1", + "UPDATE projects SET status='disabled',security_revision=security_revision + 1 WHERE id=$1", + "UPDATE projects SET status='active',security_revision=security_revision + 1 WHERE id=$1", ), ( "UPDATE applications SET status='disabled' WHERE project_id=$1", @@ -1330,6 +1366,11 @@ async fn email_generation_sibling_proofs_and_completion_are_one_winner_in_postgr .execute(&pool) .await .expect("restore owner after claim fence"); + sqlx::query("UPDATE projects SET security_revision=1 WHERE id=$1 AND status='active'") + .bind(project_id) + .execute(&pool) + .await + .expect("restore Project revision after claim fence"); } // Mail claim eligibility is PostgreSQL-clock based. Make the fixture durably due before the diff --git a/crates/owlauth-server/src/adapters/postgres/entity.rs b/crates/owlauth-server/src/adapters/postgres/entity.rs index 608295f..b66b04c 100644 --- a/crates/owlauth-server/src/adapters/postgres/entity.rs +++ b/crates/owlauth-server/src/adapters/postgres/entity.rs @@ -12,6 +12,7 @@ pub(crate) mod project { pub status: String, pub metadata_revision: i64, pub security_revision: i64, + pub deletion_requested_at: Option, pub created_at: TimeDateTimeWithTimeZone, pub updated_at: TimeDateTimeWithTimeZone, } diff --git a/crates/owlauth-server/src/adapters/postgres/provisioning.rs b/crates/owlauth-server/src/adapters/postgres/provisioning.rs index 65d93cb..24682d0 100644 --- a/crates/owlauth-server/src/adapters/postgres/provisioning.rs +++ b/crates/owlauth-server/src/adapters/postgres/provisioning.rs @@ -22,7 +22,7 @@ use crate::{ postgres::{ custody::{ MaterialOwnerKind, MaterialPurpose, ProtectedMaterialRepository, - finalize_pending_material, + finalize_pending_material, lock_material_inventory, }, entity::{ application, application_origin, application_provider_assignment, diff --git a/crates/owlauth-server/src/adapters/postgres/provisioning/project_application.rs b/crates/owlauth-server/src/adapters/postgres/provisioning/project_application.rs index 741cb39..0024650 100644 --- a/crates/owlauth-server/src/adapters/postgres/provisioning/project_application.rs +++ b/crates/owlauth-server/src/adapters/postgres/provisioning/project_application.rs @@ -1,22 +1,28 @@ use super::{ ActiveModelTrait, ApplicationConfiguration, ApplicationError, ApplicationProvisioningPort, ApplicationRecord, ApplicationStatus, BrowserOrigin, CONFIGURATION_VALUE_LIMIT, ColumnTrait, - CreateApplication, CreateProject, EntityTrait, IntoActiveModel, LIST_LIMIT, - MAX_WEBHOOK_ENDPOINTS_PER_APPLICATION, MaterialKind, MaterialOwnerKind, MaterialPurpose, - PostgresProvisioningAdapter, ProjectPolicyRecord, ProjectProvisioningPort, ProjectRecord, - ProjectStatus, QueryFilter, QueryOrder, QuerySelect, RedirectUri, - ReplaceApplicationConfiguration, SIGNING_ALGORITHM, SIGNING_PURPOSE, Set, SigningKeyState, - TransactionTrait, UpdateApplication, UpdateProject, UpdateProjectPolicy, Uuid, Value, - active_application, active_project, application, application_origin, - application_provider_assignment, application_publishable_key, application_redirect, - async_trait, bounded_items, bounded_list, bump_provider_revision, complete_idempotency, - ensure_application_capacity, ensure_capacity, ensure_no_pending_secret_replacement, - ensure_project, find_application, generated_id, insert_audit, json, key_provisioning_operation, - lock_idempotency_key, lock_project_capacity, locked_active_provider, parse_application_type, + ConnectionTrait, CreateApplication, CreateProject, DbBackend, EntityTrait, FromQueryResult, + IntoActiveModel, LIST_LIMIT, MAX_WEBHOOK_ENDPOINTS_PER_APPLICATION, MaterialKind, + MaterialOwnerKind, MaterialPurpose, PostgresProvisioningAdapter, ProjectPolicyRecord, + ProjectProvisioningPort, ProjectRecord, ProjectStatus, QueryFilter, QueryOrder, QuerySelect, + RedirectUri, ReplaceApplicationConfiguration, SIGNING_ALGORITHM, SIGNING_PURPOSE, Set, + SigningKeyState, Statement, TransactionTrait, UpdateApplication, UpdateProject, + UpdateProjectPolicy, Uuid, Value, active_application, active_project, application, + application_origin, application_provider_assignment, application_publishable_key, + application_redirect, async_trait, bounded_items, bounded_list, bump_provider_revision, + complete_idempotency, ensure_application_capacity, ensure_capacity, + ensure_no_pending_secret_replacement, ensure_project, find_application, generated_id, + insert_audit, json, key_provisioning_operation, lock_idempotency_key, lock_material_inventory, + lock_project_capacity, locked_active_provider, locked_project, parse_application_type, persistence, project, project_key_ring, project_policy, project_policy_record, project_record, project_signing_key, reject_duplicates, replay, webhook_endpoint, }; +#[derive(FromQueryResult)] +struct FinalizedProject { + id: Option, +} + impl PostgresProvisioningAdapter { #[allow( clippy::too_many_lines, @@ -361,6 +367,231 @@ impl PostgresProvisioningAdapter { Ok(project_record(updated)) } + async fn enable_project( + &self, + project_id: Uuid, + expected_security_revision: i64, + correlation_id: Uuid, + ) -> Result { + let transaction = self.database.begin().await.map_err(persistence)?; + let model = locked_project(&transaction, project_id).await?; + if model.security_revision != expected_security_revision { + return Err(ApplicationError::RevisionConflict); + } + if model.status != ProjectStatus::Disabled.as_str() { + return Err(ApplicationError::InvalidTransition); + } + let mut status = ProjectStatus::Disabled; + status + .enable() + .map_err(|_| ApplicationError::InvalidTransition)?; + let mut active = model.into_active_model(); + active.status = Set(status.as_str().to_owned()); + active.security_revision = Set(expected_security_revision + 1); + active.updated_at = Set(self.clock.now()); + let updated = active.update(&transaction).await.map_err(persistence)?; + insert_audit( + &transaction, + Some(project_id), + "project.enabled", + "project", + Some(project_id), + correlation_id, + ) + .await?; + transaction.commit().await.map_err(persistence)?; + Ok(project_record(updated)) + } + + #[allow( + clippy::too_many_lines, + reason = "the deletion fence, crypto-erasure, signing cleanup handoff, and audit remain one visible transaction" + )] + async fn delete_project( + &self, + project_id: Uuid, + expected_security_revision: i64, + correlation_id: Uuid, + ) -> Result { + let transaction = self.database.begin().await.map_err(persistence)?; + let model = locked_project(&transaction, project_id).await?; + if model.status == ProjectStatus::Deleting.as_str() { + transaction.commit().await.map_err(persistence)?; + return Ok(project_record(model)); + } + if model.security_revision != expected_security_revision { + return Err(ApplicationError::RevisionConflict); + } + let mut status = match model.status.as_str() { + "active" => ProjectStatus::Active, + "disabled" => ProjectStatus::Disabled, + _ => return Err(ApplicationError::Integrity), + }; + status + .delete() + .map_err(|_| ApplicationError::InvalidTransition)?; + let now = self.clock.now(); + let mut active = model.into_active_model(); + active.status = Set(status.as_str().to_owned()); + active.security_revision = Set(expected_security_revision + 1); + active.deletion_requested_at = Set(Some(now)); + active.updated_at = Set(now); + let updated = active.update(&transaction).await.map_err(persistence)?; + + // Managed credential custody is independent from protected_materials. Lock its owner, + // ciphertext, and renewal operation rows in their canonical order before crypto-erasing + // every generation and terminalizing in-flight renewal work. A late worker completion is + // then fenced both by the terminal Project and by its abandoned operation/cleared lease. + transaction + .query_all_raw(Statement::from_sql_and_values( + DbBackend::Postgres, + "SELECT id FROM managed_provider_connections + WHERE project_id = $1 + ORDER BY id + FOR UPDATE", + vec![project_id.into()], + )) + .await + .map_err(persistence)?; + transaction + .query_all_raw(Statement::from_sql_and_values( + DbBackend::Postgres, + "SELECT connection_id, credential_generation + FROM managed_provider_credentials + WHERE project_id = $1 + ORDER BY connection_id, credential_generation + FOR UPDATE", + vec![project_id.into()], + )) + .await + .map_err(persistence)?; + transaction + .execute_raw(Statement::from_sql_and_values( + DbBackend::Postgres, + "UPDATE managed_provider_credentials + SET ciphertext = NULL, + destroyed_at = $2 + WHERE project_id = $1 + AND ciphertext IS NOT NULL", + vec![project_id.into(), now.into()], + )) + .await + .map_err(persistence)?; + transaction + .execute_raw(Statement::from_sql_and_values( + DbBackend::Postgres, + "UPDATE managed_provider_renewal_operations + SET state = 'abandoned', + safe_outcome = 'project_deleting', + lease_owner = NULL, + lease_expires_at = NULL, + terminal_at = $2, + updated_at = $2 + WHERE project_id = $1 + AND state IN ('prepared', 'submitted')", + vec![project_id.into(), now.into()], + )) + .await + .map_err(persistence)?; + transaction + .execute_raw(Statement::from_sql_and_values( + DbBackend::Postgres, + "UPDATE managed_provider_connections + SET lease_owner = NULL, + lease_kind = NULL, + lease_expires_at = NULL, + next_synchronize_at = NULL, + next_renewal_at = NULL, + last_safe_outcome = 'project_deleting', + updated_at = $2 + WHERE project_id = $1", + vec![project_id.into(), now.into()], + )) + .await + .map_err(persistence)?; + + lock_material_inventory(&transaction).await?; + transaction + .execute_raw(Statement::from_sql_and_values( + DbBackend::Postgres, + "UPDATE protected_materials + SET opaque_value = NULL, + state = 'erased', + erased_at = $2, + updated_at = $2 + WHERE project_id = $1 + AND material_kind = 'configuration_secret' + AND state <> 'erased'", + vec![project_id.into(), now.into()], + )) + .await + .map_err(persistence)?; + transaction + .execute_raw(Statement::from_sql_and_values( + DbBackend::Postgres, + "UPDATE key_provisioning_operations AS operation + SET state = 'cleanup_pending', + provider_lease_token = NULL, + provider_lease_expires_at = NULL, + next_attempt_at = NULL, + maintenance_claimed_at = NULL + FROM protected_materials AS material + WHERE operation.project_id = $1 + AND material.id = operation.material_id + AND material.state <> 'erased' + AND operation.state IN ( + 'prepared', 'submitted', 'stored', 'completed', + 'cleanup_pending', 'cleanup_leased', 'failed' + )", + vec![project_id.into()], + )) + .await + .map_err(persistence)?; + insert_audit( + &transaction, + Some(project_id), + "project.deletion_requested", + "project", + Some(project_id), + correlation_id, + ) + .await?; + transaction.commit().await.map_err(persistence)?; + Ok(project_record(updated)) + } + + async fn finalize_project_deletions(&self, limit: usize) -> Result { + if !(1..=usize::try_from(LIST_LIMIT).expect("list limit fits usize")).contains(&limit) { + return Err(ApplicationError::InvalidInput); + } + let mut finalized = 0; + for _ in 0..limit { + let transaction = self.database.begin().await.map_err(persistence)?; + transaction + .execute_raw(Statement::from_string( + DbBackend::Postgres, + "SET TRANSACTION ISOLATION LEVEL READ COMMITTED".to_owned(), + )) + .await + .map_err(persistence)?; + let result = FinalizedProject::find_by_statement(Statement::from_sql_and_values( + DbBackend::Postgres, + "SELECT public.owlauth_finalize_project_deletion($1, $2) AS id", + vec![Uuid::new_v4().into(), Uuid::new_v4().into()], + )) + .one(&transaction) + .await + .map_err(persistence)? + .ok_or(ApplicationError::Integrity)?; + transaction.commit().await.map_err(persistence)?; + if result.id.is_none() { + break; + } + finalized += 1; + } + Ok(finalized) + } + async fn create_application( &self, project_id: Uuid, @@ -840,6 +1071,40 @@ impl ProjectProvisioningPort for PostgresProvisioningAdapter { ) .await } + + async fn enable_project( + &self, + project_id: Uuid, + expected_security_revision: i64, + correlation_id: Uuid, + ) -> Result { + PostgresProvisioningAdapter::enable_project( + self, + project_id, + expected_security_revision, + correlation_id, + ) + .await + } + + async fn delete_project( + &self, + project_id: Uuid, + expected_security_revision: i64, + correlation_id: Uuid, + ) -> Result { + PostgresProvisioningAdapter::delete_project( + self, + project_id, + expected_security_revision, + correlation_id, + ) + .await + } + + async fn finalize_project_deletions(&self, limit: usize) -> Result { + PostgresProvisioningAdapter::finalize_project_deletions(self, limit).await + } } #[async_trait] diff --git a/crates/owlauth-server/src/adapters/postgres/provisioning/shared.rs b/crates/owlauth-server/src/adapters/postgres/provisioning/shared.rs index ed06b43..3ff8368 100644 --- a/crates/owlauth-server/src/adapters/postgres/provisioning/shared.rs +++ b/crates/owlauth-server/src/adapters/postgres/provisioning/shared.rs @@ -301,11 +301,14 @@ pub(in crate::adapters::postgres) async fn ensure_project( where C: ConnectionTrait, { - project::Entity::find_by_id(project_id) + let project = project::Entity::find_by_id(project_id) .one(connection) .await .map_err(persistence)? .ok_or(ApplicationError::NotFound)?; + if project.status == ProjectStatus::Deleting.as_str() { + return Err(ApplicationError::Disabled); + } Ok(()) } diff --git a/crates/owlauth-server/src/adapters/postgres/provisioning/signing.rs b/crates/owlauth-server/src/adapters/postgres/provisioning/signing.rs index 5375d71..eb84a82 100644 --- a/crates/owlauth-server/src/adapters/postgres/provisioning/signing.rs +++ b/crates/owlauth-server/src/adapters/postgres/provisioning/signing.rs @@ -3,18 +3,19 @@ use super::{ DbBackend, Duration, EntityTrait, IntoActiveModel, LIST_LIMIT, MAX_ACCESS_TOKEN_LIFETIME_SECONDS, MaterialKind, MaterialOwnerKind, MaterialPurpose, OffsetDateTime, OpaqueHandle, PostgresProvisioningAdapter, PreparedSigningKey, - PreparedSigningMaterial, ProviderErrorClass, ProvisionedProtectedSigningMaterial, QueryFilter, - QueryOrder, QuerySelect, RetryClassification, SIGNING_ALGORITHM, SIGNING_PURPOSE, Set, - SigningKeyMaintenanceItem, SigningKeyProvisioningPort, SigningKeyRecord, SigningKeyState, - SigningProviderAction, SigningProviderCall, SigningProviderLease, Statement, TransactionTrait, - Uuid, Value, abandon_signing_key_operation, active_project, async_trait, - authenticate_committed_signing_provider_replay, bounded_list, database_now, ensure_capacity, - ensure_project, ensure_publishable_signing_key_capacity, ensure_signing_provider_lease, - finalize_pending_material, find_signing_key, generated_id, insert_audit, - insert_key_state_event, json, key_provisioning_operation, locked_project, parse_signing_state, - persistence, prepared_signing_key, project, project_key_ring, project_signing_key, - protected_material, provider_error_class_name, retry_classification_name, signing_key_record, - signing_public_key_from_jwk, validate_protected_signing_jwk, validate_signing_operation, + PreparedSigningMaterial, ProjectStatus, ProviderErrorClass, + ProvisionedProtectedSigningMaterial, QueryFilter, QueryOrder, QuerySelect, RetryClassification, + SIGNING_ALGORITHM, SIGNING_PURPOSE, Set, SigningKeyMaintenanceItem, SigningKeyProvisioningPort, + SigningKeyRecord, SigningKeyState, SigningProviderAction, SigningProviderCall, + SigningProviderLease, Statement, TransactionTrait, Uuid, Value, abandon_signing_key_operation, + active_project, async_trait, authenticate_committed_signing_provider_replay, bounded_list, + database_now, ensure_capacity, ensure_project, ensure_publishable_signing_key_capacity, + ensure_signing_provider_lease, finalize_pending_material, find_signing_key, generated_id, + insert_audit, insert_key_state_event, json, key_provisioning_operation, locked_project, + parse_signing_state, persistence, prepared_signing_key, project, project_key_ring, + project_signing_key, protected_material, provider_error_class_name, retry_classification_name, + signing_key_record, signing_public_key_from_jwk, validate_protected_signing_jwk, + validate_signing_operation, }; async fn claim_maintenance_ids( @@ -480,18 +481,14 @@ impl PostgresProvisioningAdapter { completed_at: OffsetDateTime, ) -> Result<(), ApplicationError> { let transaction = self.database.begin().await.map_err(persistence)?; - let operation = key_provisioning_operation::Entity::find_by_id(prepared.operation_id) - .filter(key_provisioning_operation::Column::ProjectId.eq(project_id)) + // Project is the root lock for deletion and every signing transition. Acquire it before + // ring, key, and operation locks so cleanup cannot deadlock with terminal Project fencing. + let project = project::Entity::find_by_id(project_id) + .lock_shared() .one(&transaction) .await .map_err(persistence)? .ok_or(ApplicationError::NotFound)?; - validate_signing_operation(prepared, &operation)?; - if operation.state != "cleanup_leased" - || operation.provider_lease_token != Some(lease.token) - { - return Err(ApplicationError::OperationInProgress); - } let ring = project_key_ring::Entity::find_by_id(prepared.ring_id) .filter(project_key_ring::Column::ProjectId.eq(project_id)) .lock_exclusive() @@ -530,6 +527,27 @@ impl PostgresProvisioningAdapter { return Ok(()); } let material_id = operation.material_id; + if project.status == ProjectStatus::Deleting.as_str() { + let custody = self.custody(); + custody + .materials + .erase_by_id_in_transaction(&transaction, material_id, completed_at) + .await?; + let mut operation_active = operation.into_active_model(); + operation_active.state = Set("abandoned".to_owned()); + operation_active.provider_lease_token = Set(None); + operation_active.provider_lease_expires_at = Set(None); + operation_active.next_attempt_at = Set(None); + operation_active.abandoned_at = Set(Some(completed_at)); + operation_active.destroyed_at = Set(Some(completed_at)); + operation_active.last_attempt_at = Set(Some(completed_at)); + operation_active + .update(&transaction) + .await + .map_err(persistence)?; + transaction.commit().await.map_err(persistence)?; + return Ok(()); + } if key.state != SigningKeyState::Provisioning.as_str() && key.state != SigningKeyState::Abandoned.as_str() { diff --git a/crates/owlauth-server/src/application/provisioning.rs b/crates/owlauth-server/src/application/provisioning.rs index 152af80..95dd847 100644 --- a/crates/owlauth-server/src/application/provisioning.rs +++ b/crates/owlauth-server/src/application/provisioning.rs @@ -526,6 +526,19 @@ pub(crate) trait ProjectProvisioningPort: Send + Sync { expected_security_revision: i64, correlation_id: Uuid, ) -> Result; + async fn enable_project( + &self, + project_id: Uuid, + expected_security_revision: i64, + correlation_id: Uuid, + ) -> Result; + async fn delete_project( + &self, + project_id: Uuid, + expected_security_revision: i64, + correlation_id: Uuid, + ) -> Result; + async fn finalize_project_deletions(&self, limit: usize) -> Result; } #[async_trait] diff --git a/crates/owlauth-server/src/application/provisioning/project_application.rs b/crates/owlauth-server/src/application/provisioning/project_application.rs index bb0d32c..82d1063 100644 --- a/crates/owlauth-server/src/application/provisioning/project_application.rs +++ b/crates/owlauth-server/src/application/provisioning/project_application.rs @@ -70,6 +70,32 @@ impl ProvisioningService { .disable_project(project_id, expected_security_revision, correlation_id) .await } + pub(crate) async fn enable_project( + &self, + project_id: Uuid, + expected_security_revision: i64, + correlation_id: Uuid, + ) -> Result { + self.projects + .enable_project(project_id, expected_security_revision, correlation_id) + .await + } + pub(crate) async fn delete_project( + &self, + project_id: Uuid, + expected_security_revision: i64, + correlation_id: Uuid, + ) -> Result { + self.projects + .delete_project(project_id, expected_security_revision, correlation_id) + .await + } + pub(crate) async fn finalize_project_deletions( + &self, + limit: usize, + ) -> Result { + self.projects.finalize_project_deletions(limit).await + } pub(crate) async fn create_application( &self, project_id: Uuid, diff --git a/crates/owlauth-server/src/application/provisioning/signing.rs b/crates/owlauth-server/src/application/provisioning/signing.rs index 58e17c3..99c9528 100644 --- a/crates/owlauth-server/src/application/provisioning/signing.rs +++ b/crates/owlauth-server/src/application/provisioning/signing.rs @@ -138,6 +138,14 @@ impl ProvisioningService { } } } + match self.finalize_project_deletions(limit).await { + Ok(finalized) => progressed += finalized, + Err(error) => { + if first_hard_error.is_none() { + first_hard_error = Some(error); + } + } + } if let Some(error) = first_hard_error { Err(error) } else { diff --git a/crates/owlauth-server/src/domain/project.rs b/crates/owlauth-server/src/domain/project.rs index 982d147..f285556 100644 --- a/crates/owlauth-server/src/domain/project.rs +++ b/crates/owlauth-server/src/domain/project.rs @@ -65,6 +65,7 @@ impl fmt::Display for PublicId { pub(crate) enum ProjectStatus { Active, Disabled, + Deleting, } impl ProjectStatus { @@ -72,6 +73,7 @@ impl ProjectStatus { match self { Self::Active => "active", Self::Disabled => "disabled", + Self::Deleting => "deleting", } } @@ -82,6 +84,22 @@ impl ProjectStatus { *self = Self::Disabled; Ok(()) } + + pub(crate) fn enable(&mut self) -> Result<(), DomainError> { + if *self != Self::Disabled { + return Err(DomainError::InvalidTransition); + } + *self = Self::Active; + Ok(()) + } + + pub(crate) fn delete(&mut self) -> Result<(), DomainError> { + if !matches!(*self, Self::Active | Self::Disabled) { + return Err(DomainError::InvalidTransition); + } + *self = Self::Deleting; + Ok(()) + } } fn validate_bounded_text(value: &str, max: usize) -> Result<(), DomainError> { @@ -116,10 +134,15 @@ mod tests { } #[test] - fn project_disable_is_monotonic() { + fn project_status_supports_reversible_disable_and_terminal_delete() { let mut status = ProjectStatus::Active; status.disable().unwrap(); assert_eq!(status, ProjectStatus::Disabled); - assert_eq!(status.disable(), Err(DomainError::InvalidTransition)); + status.enable().unwrap(); + assert_eq!(status, ProjectStatus::Active); + status.delete().unwrap(); + assert_eq!(status, ProjectStatus::Deleting); + assert_eq!(status.enable(), Err(DomainError::InvalidTransition)); + assert_eq!(status.delete(), Err(DomainError::InvalidTransition)); } } diff --git a/crates/owlauth-server/src/http.rs b/crates/owlauth-server/src/http.rs index 452ad3a..f25f7b8 100644 --- a/crates/owlauth-server/src/http.rs +++ b/crates/owlauth-server/src/http.rs @@ -591,13 +591,14 @@ fn control_api_router() -> Router { .route("/projects", get(list_projects).post(create_project)) .route( "/projects/{project_id}", - get(get_project).patch(update_project), + get(get_project).patch(update_project).delete(delete_project), ) .route( "/projects/{project_id}/policy", get(get_project_policy).put(update_project_policy), ) .route("/projects/{project_id}/disable", post(disable_project)) + .route("/projects/{project_id}/enable", post(enable_project)) .route( "/projects/{project_id}/applications", get(list_applications).post(create_application), @@ -4426,6 +4427,61 @@ async fn disable_project( } } +async fn enable_project( + State(state): State, + Extension(request_id): Extension, + Path(project_id): Path, + ControlJson(body): ControlJson, +) -> Response { + let project_id = match resource_uuid(&project_id, &request_id) { + Ok(id) => id, + Err(response) => return response, + }; + match provisioning(&state) { + Ok(service) => match service + .enable_project( + project_id, + body.expected_security_revision, + request_uuid(&request_id), + ) + .await + { + Ok(project) => control_json(control_project(project), &request_id), + Err(error) => application_problem(error, &request_id), + }, + Err(error) => application_problem(error, &request_id), + } +} + +async fn delete_project( + State(state): State, + Extension(request_id): Extension, + Path(project_id): Path, + ControlJson(body): ControlJson, +) -> Response { + let project_id = match resource_uuid(&project_id, &request_id) { + Ok(id) => id, + Err(response) => return response, + }; + match provisioning(&state) { + Ok(service) => match service + .delete_project( + project_id, + body.expected_security_revision, + request_uuid(&request_id), + ) + .await + { + Ok(project) => match control_project(project) { + Ok(project) => (StatusCode::ACCEPTED, Json(project)).into_response(), + Err(error) => application_problem(error, &request_id), + }, + Err(error) => application_problem(error, &request_id), + }, + Err(error) => application_problem(error, &request_id), + } +} + async fn get_email_method_policy( State(state): State, Extension(request_id): Extension, @@ -8201,6 +8257,7 @@ fn control_project( let status = match project.status.as_str() { "active" => control_types::ProjectStatus::Active, "disabled" => control_types::ProjectStatus::Disabled, + "deleting" => control_types::ProjectStatus::Deleting, _ => return Err(ApplicationError::Integrity), }; Ok(control_types::Project { diff --git a/crates/owlauth-server/src/http/mcp.rs b/crates/owlauth-server/src/http/mcp.rs index 7e1705c..2c3236a 100644 --- a/crates/owlauth-server/src/http/mcp.rs +++ b/crates/owlauth-server/src/http/mcp.rs @@ -1328,7 +1328,7 @@ mod tests { let mut tools = server.tool_router.list_all(); tools.extend(server.control_tools.tools()); tools.sort_by(|left, right| left.name.cmp(&right.name)); - assert_eq!(tools.len(), 85); + assert_eq!(tools.len(), 87); for tools in tools.chunks(TOOL_PAGE_SIZE) { let encoded = serde_json::to_vec(tools).unwrap(); assert!( diff --git a/crates/owlauth-server/src/http/mcp/control.rs b/crates/owlauth-server/src/http/mcp/control.rs index 42ee3e2..1a67da6 100644 --- a/crates/owlauth-server/src/http/mcp/control.rs +++ b/crates/owlauth-server/src/http/mcp/control.rs @@ -287,8 +287,9 @@ impl ControlOperation { let tool_name = format!("owlauth_{operation_id}"); let title = operation_title(operation_id); let read_only = is_read_only_operation(&method, operation_id); - let idempotent = - read_only || matches!(method, Method::PUT | Method::PATCH) || accepts_idempotency_key; + let idempotent = read_only + || matches!(method, Method::PUT | Method::PATCH | Method::DELETE) + || accepts_idempotency_key; let destructive = !read_only && is_destructive_operation(operation_id); let open_world = is_open_world_operation(operation_id); let tool = Tool::new( @@ -711,9 +712,11 @@ mod tests { #[test] fn generated_catalog_covers_every_non_hand_designed_control_operation() { let catalog = ControlToolCatalog::from_control_openapi().unwrap(); - assert_eq!(catalog.tools.len() + HAND_DESIGNED_OPERATION_IDS.len(), 85); + assert_eq!(catalog.tools.len() + HAND_DESIGNED_OPERATION_IDS.len(), 87); assert!(catalog.contains("owlauth_create_project")); assert!(catalog.contains("owlauth_update_project")); + assert!(catalog.contains("owlauth_enable_project")); + assert!(catalog.contains("owlauth_delete_project")); assert!(catalog.contains("owlauth_create_provider")); assert!(catalog.contains("owlauth_create_project_server_key")); assert!(catalog.contains("owlauth_confirm_identity_mutation_intent")); @@ -764,6 +767,35 @@ mod tests { assert_eq!(disable.read_only_hint, Some(false)); assert_eq!(disable.destructive_hint, Some(true)); + let enable_project = annotations("owlauth_enable_project"); + assert_eq!(enable_project.read_only_hint, Some(false)); + assert_eq!(enable_project.destructive_hint, Some(true)); + assert_eq!(enable_project.idempotent_hint, Some(false)); + + let delete_project = annotations("owlauth_delete_project"); + assert_eq!(delete_project.read_only_hint, Some(false)); + assert_eq!(delete_project.destructive_hint, Some(true)); + assert_eq!(delete_project.idempotent_hint, Some(true)); + assert_eq!(delete_project.open_world_hint, Some(false)); + + let delete_schema = Value::Object( + catalog + .get_tool("owlauth_delete_project") + .unwrap() + .input_schema + .as_ref() + .clone(), + ); + assert_eq!(delete_schema["additionalProperties"], false); + assert_eq!( + delete_schema["properties"]["body"]["additionalProperties"], + false + ); + assert_eq!( + delete_schema["properties"]["body"]["required"], + json!(["expected_security_revision"]) + ); + let oidc = annotations("owlauth_preflight_oidc_provider"); assert_eq!(oidc.read_only_hint, Some(true)); assert_eq!(oidc.destructive_hint, Some(false)); diff --git a/crates/owlauth-server/web/dist/control/.vite/manifest.json b/crates/owlauth-server/web/dist/control/.vite/manifest.json index 58f82ec..b5f0620 100644 --- a/crates/owlauth-server/web/dist/control/.vite/manifest.json +++ b/crates/owlauth-server/web/dist/control/.vite/manifest.json @@ -1,11 +1,11 @@ { "src/control/main.tsx": { - "file": "assets/control-C2nM8UOp.js", + "file": "assets/control-IsQYFkbw.js", "name": "main", "src": "src/control/main.tsx", "isEntry": true, "css": [ - "assets/control-BhtOPU71.css" + "assets/control-B1J_rydp.css" ] } } \ No newline at end of file diff --git a/crates/owlauth-server/web/dist/control/assets/control-BhtOPU71.css b/crates/owlauth-server/web/dist/control/assets/control-B1J_rydp.css similarity index 80% rename from crates/owlauth-server/web/dist/control/assets/control-BhtOPU71.css rename to crates/owlauth-server/web/dist/control/assets/control-B1J_rydp.css index 83fd494..60cba74 100644 --- a/crates/owlauth-server/web/dist/control/assets/control-BhtOPU71.css +++ b/crates/owlauth-server/web/dist/control/assets/control-B1J_rydp.css @@ -1 +1 @@ -:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;font-synthesis:none;text-rendering:optimizelegibility;--owl-canvas:#f7faf9;--owl-sidebar:#f1f5f3;--owl-surface:#fff;--owl-surface-raised:#f7faf9;--owl-surface-hover:#edf4f1;--owl-border:#d6e1dd;--owl-border-strong:#83978f;--owl-text:#13201c;--owl-text-muted:#4f625b;--owl-text-subtle:#586a63;--owl-accent:#0f766e;--owl-accent-hover:#0b5f59;--owl-accent-strong:#0f766e;--owl-accent-soft:#dff4ef;--owl-danger:#b42318;--owl-danger-hover:#8f1c13;--owl-danger-border:#e6a6a1;--owl-danger-surface:#fff1f0;--owl-warning:#7a5200;--owl-warning-border:#dfc477;--owl-warning-surface:#fff8e1;--owl-info:#175cd3;--owl-info-border:#9fc3f5;--owl-info-surface:#eff6ff;--owl-success:#067647;--owl-success-border:#8ccab0;--owl-success-surface:#ecfdf3;--owl-backdrop:#13201c7a;--owl-focus:#0f766e;--owl-radius-control:.5rem;--owl-radius-surface:.75rem;--owl-shadow-raised:0 1rem 3rem #13201c29;--owl-shadow-subtle:0 .125rem .5rem #13201c14;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}html{background:var(--owl-canvas);min-height:100%}body{min-height:100vh;color:var(--owl-text);background:var(--owl-canvas);margin:0;font-size:.9375rem;line-height:1.5}button,input,select,textarea{font:inherit}button,a,input,select,textarea,[tabindex]:not([tabindex="-1"]){outline:none}:focus-visible{outline:2px solid var(--owl-focus);outline-offset:2px}button,a{-webkit-tap-highlight-color:transparent}a{color:var(--owl-accent);text-underline-offset:.18em}a:hover{color:var(--owl-accent-hover)}h1,h2,h3,h4,p{margin-block-start:0}h1,h2,h3,h4{color:var(--owl-text);line-height:1.3}code,.monospace{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,monospace}::selection{color:#fff;background:var(--owl-accent-strong)}.visually-hidden{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;padding:0;position:absolute;overflow:hidden}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (forced-colors:active){:root{--owl-canvas:Canvas;--owl-sidebar:Canvas;--owl-surface:Canvas;--owl-surface-raised:Canvas;--owl-surface-hover:Highlight;--owl-border:CanvasText;--owl-border-strong:CanvasText;--owl-text:CanvasText;--owl-text-muted:CanvasText;--owl-text-subtle:CanvasText;--owl-accent:LinkText;--owl-accent-strong:Highlight;--owl-accent-soft:Canvas;--owl-danger:CanvasText;--owl-danger-surface:Canvas;--owl-warning:CanvasText;--owl-warning-surface:Canvas;--owl-info:CanvasText;--owl-info-surface:Canvas;--owl-success:CanvasText;--owl-focus:Highlight;--owl-shadow-raised:none}:focus-visible{outline-color:highlight}}._pageHeader_11ua9_1{justify-content:space-between;align-items:flex-start;gap:1.5rem;margin-bottom:1.5rem;display:flex}._pageTitleRow_11ua9_9{flex-wrap:wrap;align-items:center;gap:.65rem;min-width:0;display:flex}._pageHeader_11ua9_1 h1{overflow-wrap:anywhere;letter-spacing:-.025em;min-width:0;max-width:100%;margin:0;font-size:1.75rem;font-weight:680;line-height:2.25rem}._pageHeader_11ua9_1 p{max-width:50rem;color:var(--owl-text-muted);margin:.35rem 0 0}._pageActions_11ua9_34{flex-wrap:wrap;flex:none;gap:.75rem;display:flex}._breadcrumbs_11ua9_41{flex:auto;min-width:0}._breadcrumbs_11ua9_41 ol{min-width:0;color:var(--owl-text-muted);flex-wrap:wrap;align-items:center;gap:.45rem;margin:0;padding:0;font-size:.8125rem;list-style:none;display:flex}._breadcrumbs_11ua9_41 li,._breadcrumbs_11ua9_41 a{overflow-wrap:anywhere;min-width:0}._breadcrumbs_11ua9_41 li:not(:last-child):after{color:var(--owl-text-subtle);content:"/";margin-left:.45rem}._breadcrumbs_11ua9_41 a{color:inherit}._tableRegion_11ua9_75{contain:inline-size paint;border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);scrollbar-color:var(--owl-border-strong) transparent;scrollbar-width:thin;width:100%;min-width:0;max-width:100%;overflow-x:auto}._tableRegion_11ua9_75:focus-visible{outline:3px solid var(--owl-focus);outline-offset:2px}._tableScrollHint_11ua9_92{color:var(--owl-text-muted);background:var(--owl-surface-raised);border-bottom:1px solid var(--owl-border);white-space:nowrap;margin:0;padding:.5rem 1rem;font-size:.75rem;display:none}._table_11ua9_75{border-collapse:collapse;text-align:left;width:100%}._table_11ua9_75 th,._table_11ua9_75 td{border-bottom:1px solid var(--owl-border);vertical-align:top;padding:.75rem 1rem}._table_11ua9_75 th{z-index:1;color:var(--owl-text-muted);background:var(--owl-surface-raised);font-size:.8125rem;font-weight:600;position:sticky;top:0}._table_11ua9_75 td{color:var(--owl-text);background:var(--owl-surface)}._table_11ua9_75 tbody tr:last-child td{border-bottom:0}._table_11ua9_75 tbody tr:hover td{background:var(--owl-surface-hover)}._loading_11ua9_139{min-height:5rem;color:var(--owl-text-muted);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);align-items:center;gap:.75rem;padding:1rem 1.25rem;display:flex}._loadingIndicator_11ua9_151{border:2px solid var(--owl-border-strong);border-top-color:var(--owl-accent-strong);border-radius:50%;flex:none;width:1rem;height:1rem;animation:.7s linear infinite _loading-spin_11ua9_1}._empty_11ua9_161{background:var(--owl-surface);border:1px dashed var(--owl-border-strong);border-radius:var(--owl-radius-surface);justify-items:start;gap:.6rem;padding:2rem;display:grid}._empty_11ua9_161 h1,._empty_11ua9_161 h2,._empty_11ua9_161 h3,._empty_11ua9_161 p{margin:0}._empty_11ua9_161 p{max-width:40rem;color:var(--owl-text-muted)}._empty_11ua9_161>a{min-height:2.5rem;color:var(--owl-text);background:var(--owl-surface-raised);border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-control);align-items:center;padding:.5rem .875rem;font-weight:600;text-decoration:none;display:inline-flex}._empty_11ua9_161>a:hover{background:var(--owl-surface-hover)}._descriptionList_11ua9_200{border-top:1px solid var(--owl-border);grid-template-columns:minmax(8rem,12rem) minmax(0,1fr);margin:0;display:grid}._descriptionList_11ua9_200 dt,._descriptionList_11ua9_200 dd{border-bottom:1px solid var(--owl-border);min-width:0;margin:0;padding:.7rem 0}._descriptionList_11ua9_200 dt{color:var(--owl-text-muted);padding-right:1rem;font-size:.8125rem;font-weight:600}._descriptionList_11ua9_200 dd{overflow-wrap:anywhere}._tabs_11ua9_226{overscroll-behavior-inline:contain;border-bottom:1px solid var(--owl-border);scrollbar-color:var(--owl-border-strong) transparent;scrollbar-width:thin;gap:.25rem;margin-bottom:1.5rem;display:flex;overflow-x:auto}._tab_11ua9_75{min-height:2.75rem;color:var(--owl-text-muted);white-space:nowrap;border-bottom:2px solid #0000;align-items:center;padding:.5rem .75rem;font-weight:600;text-decoration:none;display:inline-flex}._tab_11ua9_75[aria-current=page]{color:var(--owl-text);border-bottom-color:var(--owl-accent)}._section_11ua9_254{margin-block:1.5rem}._sectionHeader_11ua9_258{justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:1rem;display:flex}._sectionHeader_11ua9_258 h2,._sectionHeader_11ua9_258 h3,._sectionHeader_11ua9_258 p{margin:0}._sectionHeader_11ua9_258 p{color:var(--owl-text-muted)}@keyframes _loading-spin_11ua9_1{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){._loadingIndicator_11ua9_151{border-top-color:var(--owl-border-strong);background:var(--owl-accent-strong);animation:none}}@media (width<=48rem){._tableScrollHint_11ua9_92{display:block}._pageHeader_11ua9_1,._sectionHeader_11ua9_258{flex-direction:column;align-items:stretch}._pageActions_11ua9_34>*{flex:auto}._descriptionList_11ua9_200{grid-template-columns:1fr}._descriptionList_11ua9_200 dt{border-bottom:0;padding-bottom:0}._descriptionList_11ua9_200 dd{padding-top:.25rem}}@media (width<=40rem){._tab_11ua9_75{padding-inline:.5rem;font-size:.875rem}._breadcrumbs_11ua9_41 ol{flex-wrap:nowrap;overflow:hidden}._breadcrumbs_11ua9_41 li:not(:last-child){display:none}._breadcrumbs_11ua9_41 li:last-child{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}}._button_2cp7j_1{border-radius:var(--owl-radius-control);cursor:pointer;border:1px solid #0000;justify-content:center;align-items:center;gap:.5rem;min-height:2.5rem;padding:.5rem .875rem;font-weight:600;line-height:1.25rem;text-decoration:none;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}._button_2cp7j_1:disabled,._button_2cp7j_1[aria-disabled=true]{cursor:not-allowed;opacity:.58}._primary_2cp7j_26{color:#fff;background:var(--owl-accent-strong);border-color:var(--owl-accent-strong)}._primary_2cp7j_26:not(:disabled):hover{background:var(--owl-accent-hover);border-color:var(--owl-accent-hover)}._secondary_2cp7j_37{color:var(--owl-text);background:var(--owl-surface-raised);border-color:var(--owl-border-strong)}._secondary_2cp7j_37:not(:disabled):hover,._quiet_2cp7j_44:not(:disabled):hover{background:var(--owl-surface-hover)}._quiet_2cp7j_44{color:var(--owl-text-muted);background:0 0;border-color:#0000}._danger_2cp7j_54{color:var(--owl-danger);background:var(--owl-danger-surface);border-color:var(--owl-danger-border)}._danger_2cp7j_54:not(:disabled):hover{color:#fff;background:var(--owl-danger-hover);border-color:var(--owl-danger-hover)}._fullWidth_2cp7j_66{width:100%}._iconOnly_2cp7j_70{width:2.75rem;min-width:2.75rem;padding-inline:0}._field_2cp7j_76{gap:.4rem;display:grid}._label_2cp7j_81{color:var(--owl-text);font-size:.875rem;font-weight:600}._description_2cp7j_87{color:var(--owl-text-muted);margin:0;font-size:.8125rem;line-height:1.3rem}._input_2cp7j_94,._select_2cp7j_95,._textarea_2cp7j_96{width:100%;min-height:2.5rem;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-control);padding:.55rem .7rem}._textarea_2cp7j_96{resize:vertical;min-height:6rem}._input_2cp7j_94::placeholder,._textarea_2cp7j_96::placeholder{color:var(--owl-text-subtle)}._input_2cp7j_94[aria-invalid=true],._select_2cp7j_95[aria-invalid=true],._textarea_2cp7j_96[aria-invalid=true]{border-color:var(--owl-danger)}._input_2cp7j_94:disabled,._select_2cp7j_95:disabled,._textarea_2cp7j_96:disabled{cursor:not-allowed;opacity:.68}._fieldError_2cp7j_129{color:var(--owl-danger);margin:0;font-size:.8125rem}._checkbox_2cp7j_135{min-height:2.5rem;color:var(--owl-text);align-items:flex-start;gap:.65rem;display:flex}._checkbox_2cp7j_135 input{width:1.125rem;height:1.125rem;accent-color:var(--owl-accent-strong);margin-top:.2rem}._alert_2cp7j_150{border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-control);color:var(--owl-text);background:var(--owl-surface-raised);grid-template-columns:auto 1fr;gap:.65rem;padding:.75rem .875rem;display:grid}._alert_2cp7j_150 p,._alert_2cp7j_150 ul{margin:0}._info_2cp7j_166{color:var(--owl-info);background:var(--owl-info-surface);border-color:var(--owl-info-border)}._success_2cp7j_172{color:var(--owl-success);background:var(--owl-success-surface);border-color:var(--owl-success-border)}._warning_2cp7j_178{color:var(--owl-warning);background:var(--owl-warning-surface);border-color:var(--owl-warning-border)}._alertDanger_2cp7j_184{color:var(--owl-danger);background:var(--owl-danger-surface);border-color:var(--owl-danger-border)}._alertIcon_2cp7j_190{margin-top:.08rem;display:inline-flex}._badge_2cp7j_195{border:1px solid var(--owl-border);min-height:1.625rem;color:var(--owl-text-muted);background:var(--owl-surface-raised);border-radius:999px;align-items:center;gap:.35rem;padding:.15rem .55rem;font-size:.75rem;font-weight:600;line-height:1.1rem;display:inline-flex;position:relative}._badge_2cp7j_195:before{content:"";background:currentColor;border-radius:50%;width:.42rem;height:.42rem}._badgeActive_2cp7j_219{color:var(--owl-success);background:var(--owl-accent-soft)}._badgePending_2cp7j_224{color:var(--owl-info);background:var(--owl-info-surface)}._badgeAttention_2cp7j_229{color:var(--owl-warning);background:var(--owl-warning-surface)}._badgeDanger_2cp7j_234{color:var(--owl-danger);background:var(--owl-danger-surface)}._notificationRegion_2cp7j_239{gap:.75rem;margin-bottom:1rem;display:grid}._toastRegion_2cp7j_245{z-index:120;pointer-events:none;gap:.75rem;width:min(24rem,100vw - 2rem);display:grid;position:fixed;bottom:1.25rem;right:1.25rem}._toast_2cp7j_245{border-radius:var(--owl-radius-surface);min-height:3.25rem;box-shadow:var(--owl-shadow-raised);pointer-events:auto;border:1px solid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.65rem;padding:.65rem .65rem .65rem .85rem;display:grid}._backdrop_2cp7j_269{z-index:100;background:var(--owl-backdrop);place-items:center;padding:1rem;display:grid;position:fixed;inset:0}._dialog_2cp7j_279{width:min(100%,32rem);max-height:calc(100vh - 2rem);color:var(--owl-text);background:var(--owl-surface-raised);border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-surface);box-shadow:var(--owl-shadow-raised);overflow:auto}._dialogHeader_2cp7j_290{justify-content:space-between;align-items:flex-start;gap:1rem;padding:1.25rem 1.25rem 0;display:flex}._dialogHeader_2cp7j_290 h2{margin:0;font-size:1.125rem}._dialogBody_2cp7j_303{padding:1.25rem}._dialogActions_2cp7j_307{flex-wrap:wrap;justify-content:flex-end;gap:.75rem;padding:0 1.25rem 1.25rem;display:flex}._sheetBackdrop_2cp7j_315{z-index:90;background:var(--owl-backdrop);position:fixed;inset:0}._sheet_2cp7j_315{inset-block:0;background:var(--owl-sidebar);border-right:1px solid var(--owl-border-strong);width:min(20rem,100vw - 2rem);box-shadow:var(--owl-shadow-raised);position:absolute;left:0;overflow-y:auto}._sheetRight_2cp7j_333{background:var(--owl-surface);border-right:0;border-left:1px solid var(--owl-border-strong);width:min(38rem,100vw - 2rem);left:auto;right:0}._srOnly_2cp7j_342{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;padding:0;position:absolute;overflow:hidden}@media (width<=30rem){._toastRegion_2cp7j_245{width:calc(100vw - 2rem);bottom:1rem;right:1rem}._backdrop_2cp7j_269{align-items:stretch;padding:0}._dialog_2cp7j_279{border-radius:0;width:100%;max-height:100vh}._dialogActions_2cp7j_307 ._button_2cp7j_1{flex:auto}}@media (forced-colors:active){._primary_2cp7j_26,._secondary_2cp7j_37,._quiet_2cp7j_44,._danger_2cp7j_54,._badge_2cp7j_195,._alert_2cp7j_150{border-color:buttontext}}._lockedCanvas_1a3cj_1{background:radial-gradient(circle at 50% 10%, #dff4efb8, transparent 30rem), var(--owl-canvas);place-items:center;min-height:100vh;padding:1rem;display:grid}._lockedPanel_1a3cj_10{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);width:min(100%,28rem);box-shadow:var(--owl-shadow-raised);padding:2rem}._wordmark_1a3cj_19{color:var(--owl-text);letter-spacing:-.01em;align-items:center;gap:.65rem;font-size:1rem;font-weight:700;display:flex}._mark_1a3cj_29{flex:none;width:1.8rem;height:1.8rem}._lockedPanel_1a3cj_10 h1{margin:1.5rem 0 .5rem;font-size:1.5rem}._lockedPanel_1a3cj_10>p{color:var(--owl-text-muted)}._lockedForm_1a3cj_44{gap:1rem;margin-top:1.5rem;display:grid}._workspace_1a3cj_50{background:var(--owl-canvas);grid-template:"sidebar topbar"3.75rem"sidebar main"minmax(0,1fr)/16rem minmax(0,1fr);min-height:100vh;display:grid}._sidebar_1a3cj_59{background:var(--owl-sidebar);border-right:1px solid var(--owl-border);flex-direction:column;grid-area:sidebar;height:100vh;padding:1.25rem 1rem 1rem;display:flex;position:sticky;top:0}._navigation_1a3cj_71{align-content:start;gap:.9rem;min-height:0;margin:1.5rem 0 1rem;display:grid;overflow-y:auto}._workspaceNavigation_1a3cj_80,._navigationGroup_1a3cj_81,._settingsNavigation_1a3cj_82{gap:.2rem;display:grid}._workspaceNavigation_1a3cj_80{gap:.35rem}._workspaceNavigation_1a3cj_80 a svg{flex:none}._navigationGroup_1a3cj_81,._settingsNavigation_1a3cj_82{border-top:1px solid var(--owl-border);padding-top:.8rem}._navigationLabel_1a3cj_101{color:var(--owl-text-subtle);padding:0 .75rem .25rem;font-size:.75rem;font-weight:650}._currentProject_1a3cj_108{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.5rem;min-width:0;padding:.2rem .5rem .35rem .75rem;display:grid}._currentProject_1a3cj_108 strong{color:var(--owl-text);text-overflow:ellipsis;white-space:nowrap;font-size:.9375rem;font-weight:650;display:block;overflow:hidden}._currentProject_1a3cj_108 button{min-height:2rem;padding:.25rem .45rem;font-size:.75rem}._navigation_1a3cj_71>:not(._currentProject_1a3cj_108) a{min-height:2.5rem;color:var(--owl-text-muted);border-radius:var(--owl-radius-control);border:1px solid #0000;align-items:center;padding:.5rem .75rem;font-weight:560;text-decoration:none;display:flex}._navigation_1a3cj_71 a:hover{color:var(--owl-text);background:var(--owl-surface-hover)}._navigation_1a3cj_71>:not(._currentProject_1a3cj_108) a[aria-current=page]{color:var(--owl-accent-hover);background:var(--owl-accent-soft);border-color:color-mix(in srgb, var(--owl-accent) 24%, var(--owl-border));box-shadow:inset 3px 0 var(--owl-accent);font-weight:650}._sidebarFooter_1a3cj_158{border-top:1px solid var(--owl-border);margin-top:auto;padding-top:1rem}._topbar_1a3cj_164{z-index:20;background:var(--owl-surface);border-bottom:1px solid var(--owl-border);box-shadow:var(--owl-shadow-subtle);grid-area:topbar;align-items:center;gap:1rem;padding:0 2rem;display:flex;position:sticky;top:0}._menuButton_1a3cj_178{display:none}._main_1a3cj_182{grid-area:main;width:100%;min-width:0;max-width:104rem;margin-inline:auto;padding:2rem}._skipLink_1a3cj_191{z-index:200;color:var(--owl-text);background:var(--owl-accent-strong);border-radius:var(--owl-radius-control);padding:.6rem .8rem;position:fixed;top:.5rem;left:.5rem;transform:translateY(-200%)}._skipLink_1a3cj_191:focus{transform:translateY(0)}._mobileOnly_1a3cj_207{display:none}@media (width<64rem){._workspace_1a3cj_50{grid-template:"topbar"3.75rem"main"minmax(0,1fr)/minmax(0,1fr)}._sidebar_1a3cj_59{display:none}._topbar_1a3cj_164{padding-inline:1.5rem}._menuButton_1a3cj_178,._mobileOnly_1a3cj_207{display:inline-flex}._main_1a3cj_182{padding:1.5rem}}@media (width<=40rem){._topbar_1a3cj_164,._main_1a3cj_182{padding-inline:1rem}._lockedPanel_1a3cj_10{padding:1.5rem}}._listField_16ujt_1{border:0;gap:.75rem;min-width:0;margin:0;padding:0;display:grid}._listField_16ujt_1 legend{color:var(--owl-text);padding:0;font-size:.875rem;font-weight:600}._fieldDescription_16ujt_17{color:var(--owl-text-muted);margin:-.4rem 0 0;font-size:.8125rem}._listRows_16ujt_23{gap:.65rem;display:grid}._listRow_16ujt_23{grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:.5rem;display:grid}._rowError_16ujt_35{color:var(--owl-danger);grid-column:1/-1;margin:-.25rem 0 0;font-size:.8125rem}._copyValue_16ujt_42,._copyValueBlock_16ujt_43{align-items:center;gap:.25rem;min-width:0;display:inline-flex}._copyValueBlock_16ujt_43{background:var(--owl-surface-raised);border:1px solid var(--owl-border);border-radius:var(--owl-radius-control);justify-content:space-between;width:100%;padding:.4rem .4rem .4rem .65rem}._copyValue_16ujt_42 code,._copyValueBlock_16ujt_43 code{overflow-wrap:anywhere;min-width:0;color:var(--owl-text-muted);-webkit-user-select:all;user-select:all}._form_10g3i_1{gap:.85rem;max-width:36rem;display:grid}._identityMutation_10g3i_7>._form_10g3i_1{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);margin-block:1rem;padding:1rem}._form_10g3i_1>label,._inlineForm_10g3i_16>label,._confirmation_10g3i_17>label{color:var(--owl-text);font-size:.875rem;font-weight:600}._form_10g3i_1 button{justify-self:start}._workspace_10g3i_27{gap:2rem;display:grid}._sectionHeader_10g3i_32,._actions_10g3i_33,._inlineForm_10g3i_16{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;display:flex}._sectionHeader_10g3i_32 h2,._sectionHeader_10g3i_32 h3,._sectionHeader_10g3i_32 p{margin:0}._sectionHeader_10g3i_32>div{gap:.25rem;min-width:0;display:grid}._filterToolbar_10g3i_54{border-bottom:1px solid var(--owl-border);grid-template-columns:repeat(12,minmax(0,1fr));align-items:end;gap:.75rem;width:100%;min-width:0;padding-bottom:1rem;display:grid}._filterFields_10g3i_65{display:contents}._filterField_10g3i_65,._searchField_10g3i_70{min-width:0;color:var(--owl-text);grid-column:span 2;gap:.35rem;font-size:.8125rem;font-weight:600;display:grid}._searchField_10g3i_70{grid-column:span 4}._filterError_10g3i_84{color:var(--owl-danger);font-size:.8125rem;font-weight:500}._filterActions_10g3i_90{grid-column:span 2;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem;min-width:0;display:grid}._filterActions_10g3i_90>button{width:100%}._panel_10g3i_102,._cards_10g3i_103>li{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);min-width:0;padding:1rem}._list_10g3i_111,._cards_10g3i_103{gap:.5rem;margin:0;padding:0;list-style:none;display:grid}._list_10g3i_111 button,._list_10g3i_111 a{width:100%;min-height:3rem;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-control);cursor:pointer;font:inherit;text-align:left;justify-content:space-between;align-items:center;gap:1rem;padding:.6rem .75rem;font-weight:600;text-decoration:none;display:flex}._list_10g3i_111 a:hover,._list_10g3i_111 button:not(:disabled):hover{background:var(--owl-surface-hover)}._list_10g3i_111 button:disabled{cursor:not-allowed;opacity:.58}._list_10g3i_111 button[aria-pressed=true]{background:var(--owl-accent-soft);box-shadow:inset 3px 0 var(--owl-accent)}._userName_10g3i_155{overflow-wrap:anywhere;min-width:0}._cards_10g3i_103>li{gap:.75rem;display:grid}._cards_10g3i_103>li>*{min-width:0}._emptyNote_10g3i_169{color:var(--owl-text-muted);background:var(--owl-surface-raised);border:1px dashed var(--owl-border);border-radius:var(--owl-radius-control);margin:0;padding:.75rem}._panel_10g3i_102>._sectionHeader_10g3i_32{flex-direction:column;align-items:stretch}._panel_10g3i_102 h3,._cards_10g3i_103 strong,._cards_10g3i_103 span{overflow-wrap:anywhere;min-width:0}._actions_10g3i_33,._inlineForm_10g3i_16{justify-content:flex-start}._inlineForm_10g3i_16{max-width:36rem}._inlineForm_10g3i_16>label{flex-basis:100%}._inlineForm_10g3i_16>input{flex:16rem;width:auto}._userInventory_10g3i_208{gap:1rem;width:100%;min-width:0;display:grid}._userGrid_10g3i_215,._sessionColumns_10g3i_216{grid-template-columns:minmax(12rem,1fr) minmax(0,3fr);gap:1rem;display:grid}._sessionColumns_10g3i_216{grid-template-columns:repeat(auto-fit,minmax(16rem,1fr))}._identityMutation_10g3i_7{border-top:1px solid var(--owl-border);gap:1rem;margin-top:1.5rem;padding-top:1.5rem;display:grid}._authority_10g3i_234,._confirmation_10g3i_17{background:var(--owl-surface-raised);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);gap:.65rem;padding:1rem;display:grid}._authority_10g3i_234 legend{font-weight:700}._planReview_10g3i_248{background:var(--owl-accent-soft);border:2px solid var(--owl-accent-strong);border-radius:var(--owl-radius-surface);gap:.75rem;padding:1rem;display:grid}._planGroup_10g3i_257{border-left:.2rem solid var(--owl-border-strong);gap:.25rem;padding-left:.75rem;display:grid}._planReview_10g3i_248 p,._planGroup_10g3i_257 p{margin:0}._machineValue_10g3i_269{overflow-wrap:anywhere;color:var(--owl-text-muted);font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:.8125rem;display:block}._disclosureIcon_10g3i_277,._disclosureIconExpanded_10g3i_278{transition:transform .15s}._disclosureIconExpanded_10g3i_278{transform:rotate(180deg)}._expandedDetailCell_10g3i_286{background:var(--owl-surface-raised)!important;padding:0 1rem 1rem!important}._expandedDetailCell_10g3i_286>*{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-control);padding:1rem}code{overflow-wrap:anywhere}@media (width<=72rem){._filterToolbar_10g3i_54{grid-template-columns:repeat(4,minmax(0,1fr))}._searchField_10g3i_70,._filterActions_10g3i_90{grid-column:1/-1}._filterField_10g3i_65{grid-column:span 1}}@media (width<=48rem){._userGrid_10g3i_215{grid-template-columns:1fr}._filterToolbar_10g3i_54{grid-template-columns:repeat(2,minmax(0,1fr))}._searchField_10g3i_70,._filterActions_10g3i_90{grid-column:1/-1}._sectionHeader_10g3i_32{flex-direction:column;align-items:stretch}}@media (width<=30rem){._filterToolbar_10g3i_54{grid-template-columns:minmax(0,1fr)}._filterField_10g3i_65,._searchField_10g3i_70,._filterActions_10g3i_90{grid-column:1}._filterActions_10g3i_90{grid-template-columns:minmax(0,1fr)}._actions_10g3i_33>*{flex:100%}}._page_1uh72_1{margin-top:.5rem}._form_1uh72_5{gap:1rem;max-width:36rem;display:grid}._formActions_1uh72_11{flex-wrap:wrap;justify-content:flex-end;gap:.75rem;margin-top:.5rem;display:flex}._stack_1uh72_19{gap:1rem;display:grid}._projectIdentity_1uh72_24{flex-wrap:wrap;align-items:center;gap:.45rem;min-width:0;display:inline-flex}._projectIdentityLabel_1uh72_32{color:var(--owl-text-subtle);font-size:.75rem;font-weight:650}._projectIdentity_1uh72_24 code{overflow-wrap:anywhere;font-size:.8125rem}._dashboardGrid_1uh72_43{grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:1rem;margin:0;padding:0;list-style:none;display:grid}._dashboardGrid_1uh72_43>li{min-width:0}._dashboardCard_1uh72_56{min-width:0;height:100%;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);gap:.65rem;padding:1.1rem;text-decoration:none;transition:background-color .15s,border-color .15s,box-shadow .15s;display:grid}._dashboardCard_1uh72_56:hover{background:var(--owl-surface-hover);border-color:var(--owl-border-strong);box-shadow:var(--owl-shadow-subtle)}._dashboardCard_1uh72_56:focus-visible{outline:3px solid var(--owl-focus);outline-offset:2px}._dashboardCardHeader_1uh72_84{justify-content:space-between;align-items:center;gap:.75rem;min-width:0;display:flex}._dashboardCardHeader_1uh72_84 h3{overflow-wrap:anywhere;margin:0;font-size:.9375rem}._dashboardCardHeader_1uh72_84 svg{color:var(--owl-text-subtle);flex:none}._dashboardValue_1uh72_103{font-variant-numeric:tabular-nums;font-size:2rem;line-height:1}._dashboardDetail_1uh72_109{overflow-wrap:anywhere;min-width:0;color:var(--owl-text-muted);font-size:.8125rem}._projectDirectory_1uh72_116{gap:.75rem;margin:0;padding:0;list-style:none;display:grid}._projectDirectoryItem_1uh72_124{min-width:0;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:1rem;padding:1rem 1.1rem;text-decoration:none;transition:background-color .15s,border-color .15s,box-shadow .15s;display:grid}._projectDirectoryItem_1uh72_124:hover{background:var(--owl-surface-hover);border-color:var(--owl-border-strong);box-shadow:var(--owl-shadow-subtle)}._projectDirectoryBody_1uh72_148{gap:.2rem;min-width:0;display:grid}._projectDirectoryBody_1uh72_148 strong{overflow-wrap:anywhere;font-size:1rem;font-weight:650}._projectDirectoryBody_1uh72_148 code{overflow-wrap:anywhere;color:var(--owl-text-muted);font-size:.8125rem}._projectDirectoryMeta_1uh72_166{color:var(--owl-text-subtle);align-items:center;gap:.75rem;display:inline-flex}._resourceLink_1uh72_173{color:var(--owl-text);font-weight:600}._muted_1uh72_178{color:var(--owl-text-muted)}._machineValue_1uh72_182{overflow-wrap:anywhere;max-width:38rem;color:var(--owl-text-muted);font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:.8125rem;display:block}._credential_1uh72_191{max-width:100%;color:var(--owl-text);background:var(--owl-canvas);border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-control);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all;margin-top:.75rem;padding:.75rem;font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,monospace;display:block;overflow-x:auto}._detailSurface_1uh72_206{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);padding:1.25rem}._disclosureIcon_1uh72_213,._disclosureIconExpanded_1uh72_214{transition:transform .15s}._disclosureIconExpanded_1uh72_214{transform:rotate(180deg)}._expandedDetailCell_1uh72_222{background:var(--owl-surface-raised)!important;padding:0 1rem 1rem!important}._inlineDetail_1uh72_227{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-control);gap:1rem;padding:1rem;display:grid}._inlineDetail_1uh72_227>p{margin:0}._inlineDangerZone_1uh72_240{background:color-mix(in srgb, var(--owl-danger-surface) 55%, var(--owl-surface));border:1px solid var(--owl-danger-border);border-radius:var(--owl-radius-control);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:1rem;margin-top:.5rem;padding:1rem;display:flex}._inlineDangerZone_1uh72_240 h3,._inlineDangerZone_1uh72_240 p{margin:0}._dangerZone_1uh72_258{background:color-mix(in srgb, var(--owl-danger-surface) 55%, var(--owl-surface));border:1px solid var(--owl-danger-border);border-radius:var(--owl-radius-surface);margin-top:3rem;padding:1.25rem}._dangerZone_1uh72_258 h2,._dangerZone_1uh72_258 p{margin-top:0}._actions_1uh72_271{flex-wrap:wrap;gap:.75rem;display:flex}._providerChoices_1uh72_277{gap:.75rem;margin-top:1rem;display:grid}._providerChoice_1uh72_277{width:100%;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);cursor:pointer;text-align:left;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:.85rem;padding:1rem;display:grid}._providerChoice_1uh72_277:hover{background:var(--owl-surface-hover);border-color:var(--owl-border-strong)}._providerChoice_1uh72_277 svg{width:1.5rem;height:1.5rem;color:var(--owl-accent)}._providerChoice_1uh72_277 span{gap:.2rem;display:grid}._providerChoice_1uh72_277 small{color:var(--owl-text-muted);font-size:.8125rem}@media (width<=48rem){._projectDirectoryItem_1uh72_124{grid-template-columns:1fr}._projectDirectoryMeta_1uh72_166{justify-content:space-between}} +:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;font-synthesis:none;text-rendering:optimizelegibility;--owl-canvas:#f7faf9;--owl-sidebar:#f1f5f3;--owl-surface:#fff;--owl-surface-raised:#f7faf9;--owl-surface-hover:#edf4f1;--owl-border:#d6e1dd;--owl-border-strong:#83978f;--owl-text:#13201c;--owl-text-muted:#4f625b;--owl-text-subtle:#586a63;--owl-accent:#0f766e;--owl-accent-hover:#0b5f59;--owl-accent-strong:#0f766e;--owl-accent-soft:#dff4ef;--owl-danger:#b42318;--owl-danger-hover:#8f1c13;--owl-danger-border:#e6a6a1;--owl-danger-surface:#fff1f0;--owl-warning:#7a5200;--owl-warning-border:#dfc477;--owl-warning-surface:#fff8e1;--owl-info:#175cd3;--owl-info-border:#9fc3f5;--owl-info-surface:#eff6ff;--owl-success:#067647;--owl-success-border:#8ccab0;--owl-success-surface:#ecfdf3;--owl-backdrop:#13201c7a;--owl-focus:#0f766e;--owl-radius-control:.5rem;--owl-radius-surface:.75rem;--owl-shadow-raised:0 1rem 3rem #13201c29;--owl-shadow-subtle:0 .125rem .5rem #13201c14;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}html{background:var(--owl-canvas);min-height:100%}body{min-height:100vh;color:var(--owl-text);background:var(--owl-canvas);margin:0;font-size:.9375rem;line-height:1.5}button,input,select,textarea{font:inherit}button,a,input,select,textarea,[tabindex]:not([tabindex="-1"]){outline:none}:focus-visible{outline:2px solid var(--owl-focus);outline-offset:2px}button,a{-webkit-tap-highlight-color:transparent}a{color:var(--owl-accent);text-underline-offset:.18em}a:hover{color:var(--owl-accent-hover)}h1,h2,h3,h4,p{margin-block-start:0}h1,h2,h3,h4{color:var(--owl-text);line-height:1.3}code,.monospace{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,monospace}::selection{color:#fff;background:var(--owl-accent-strong)}.visually-hidden{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;padding:0;position:absolute;overflow:hidden}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (forced-colors:active){:root{--owl-canvas:Canvas;--owl-sidebar:Canvas;--owl-surface:Canvas;--owl-surface-raised:Canvas;--owl-surface-hover:Highlight;--owl-border:CanvasText;--owl-border-strong:CanvasText;--owl-text:CanvasText;--owl-text-muted:CanvasText;--owl-text-subtle:CanvasText;--owl-accent:LinkText;--owl-accent-strong:Highlight;--owl-accent-soft:Canvas;--owl-danger:CanvasText;--owl-danger-surface:Canvas;--owl-warning:CanvasText;--owl-warning-surface:Canvas;--owl-info:CanvasText;--owl-info-surface:Canvas;--owl-success:CanvasText;--owl-focus:Highlight;--owl-shadow-raised:none}:focus-visible{outline-color:highlight}}._pageHeader_11ua9_1{justify-content:space-between;align-items:flex-start;gap:1.5rem;margin-bottom:1.5rem;display:flex}._pageTitleRow_11ua9_9{flex-wrap:wrap;align-items:center;gap:.65rem;min-width:0;display:flex}._pageHeader_11ua9_1 h1{overflow-wrap:anywhere;letter-spacing:-.025em;min-width:0;max-width:100%;margin:0;font-size:1.75rem;font-weight:680;line-height:2.25rem}._pageHeader_11ua9_1 p{max-width:50rem;color:var(--owl-text-muted);margin:.35rem 0 0}._pageActions_11ua9_34{flex-wrap:wrap;flex:none;gap:.75rem;display:flex}._breadcrumbs_11ua9_41{flex:auto;min-width:0}._breadcrumbs_11ua9_41 ol{min-width:0;color:var(--owl-text-muted);flex-wrap:wrap;align-items:center;gap:.45rem;margin:0;padding:0;font-size:.8125rem;list-style:none;display:flex}._breadcrumbs_11ua9_41 li,._breadcrumbs_11ua9_41 a{overflow-wrap:anywhere;min-width:0}._breadcrumbs_11ua9_41 li:not(:last-child):after{color:var(--owl-text-subtle);content:"/";margin-left:.45rem}._breadcrumbs_11ua9_41 a{color:inherit}._tableRegion_11ua9_75{contain:inline-size paint;border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);scrollbar-color:var(--owl-border-strong) transparent;scrollbar-width:thin;width:100%;min-width:0;max-width:100%;overflow-x:auto}._tableRegion_11ua9_75:focus-visible{outline:3px solid var(--owl-focus);outline-offset:2px}._tableScrollHint_11ua9_92{color:var(--owl-text-muted);background:var(--owl-surface-raised);border-bottom:1px solid var(--owl-border);white-space:nowrap;margin:0;padding:.5rem 1rem;font-size:.75rem;display:none}._table_11ua9_75{border-collapse:collapse;text-align:left;width:100%}._table_11ua9_75 th,._table_11ua9_75 td{border-bottom:1px solid var(--owl-border);vertical-align:top;padding:.75rem 1rem}._table_11ua9_75 th{z-index:1;color:var(--owl-text-muted);background:var(--owl-surface-raised);font-size:.8125rem;font-weight:600;position:sticky;top:0}._table_11ua9_75 td{color:var(--owl-text);background:var(--owl-surface)}._table_11ua9_75 tbody tr:last-child td{border-bottom:0}._table_11ua9_75 tbody tr:hover td{background:var(--owl-surface-hover)}._loading_11ua9_139{min-height:5rem;color:var(--owl-text-muted);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);align-items:center;gap:.75rem;padding:1rem 1.25rem;display:flex}._loadingIndicator_11ua9_151{border:2px solid var(--owl-border-strong);border-top-color:var(--owl-accent-strong);border-radius:50%;flex:none;width:1rem;height:1rem;animation:.7s linear infinite _loading-spin_11ua9_1}._empty_11ua9_161{background:var(--owl-surface);border:1px dashed var(--owl-border-strong);border-radius:var(--owl-radius-surface);justify-items:start;gap:.6rem;padding:2rem;display:grid}._empty_11ua9_161 h1,._empty_11ua9_161 h2,._empty_11ua9_161 h3,._empty_11ua9_161 p{margin:0}._empty_11ua9_161 p{max-width:40rem;color:var(--owl-text-muted)}._empty_11ua9_161>a{min-height:2.5rem;color:var(--owl-text);background:var(--owl-surface-raised);border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-control);align-items:center;padding:.5rem .875rem;font-weight:600;text-decoration:none;display:inline-flex}._empty_11ua9_161>a:hover{background:var(--owl-surface-hover)}._descriptionList_11ua9_200{border-top:1px solid var(--owl-border);grid-template-columns:minmax(8rem,12rem) minmax(0,1fr);margin:0;display:grid}._descriptionList_11ua9_200 dt,._descriptionList_11ua9_200 dd{border-bottom:1px solid var(--owl-border);min-width:0;margin:0;padding:.7rem 0}._descriptionList_11ua9_200 dt{color:var(--owl-text-muted);padding-right:1rem;font-size:.8125rem;font-weight:600}._descriptionList_11ua9_200 dd{overflow-wrap:anywhere}._tabs_11ua9_226{overscroll-behavior-inline:contain;border-bottom:1px solid var(--owl-border);scrollbar-color:var(--owl-border-strong) transparent;scrollbar-width:thin;gap:.25rem;margin-bottom:1.5rem;display:flex;overflow-x:auto}._tab_11ua9_75{min-height:2.75rem;color:var(--owl-text-muted);white-space:nowrap;border-bottom:2px solid #0000;align-items:center;padding:.5rem .75rem;font-weight:600;text-decoration:none;display:inline-flex}._tab_11ua9_75[aria-current=page]{color:var(--owl-text);border-bottom-color:var(--owl-accent)}._section_11ua9_254{margin-block:1.5rem}._sectionHeader_11ua9_258{justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:1rem;display:flex}._sectionHeader_11ua9_258 h2,._sectionHeader_11ua9_258 h3,._sectionHeader_11ua9_258 p{margin:0}._sectionHeader_11ua9_258 p{color:var(--owl-text-muted)}@keyframes _loading-spin_11ua9_1{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){._loadingIndicator_11ua9_151{border-top-color:var(--owl-border-strong);background:var(--owl-accent-strong);animation:none}}@media (width<=48rem){._tableScrollHint_11ua9_92{display:block}._pageHeader_11ua9_1,._sectionHeader_11ua9_258{flex-direction:column;align-items:stretch}._pageActions_11ua9_34>*{flex:auto}._descriptionList_11ua9_200{grid-template-columns:1fr}._descriptionList_11ua9_200 dt{border-bottom:0;padding-bottom:0}._descriptionList_11ua9_200 dd{padding-top:.25rem}}@media (width<=40rem){._tab_11ua9_75{padding-inline:.5rem;font-size:.875rem}._breadcrumbs_11ua9_41 ol{flex-wrap:nowrap;overflow:hidden}._breadcrumbs_11ua9_41 li:not(:last-child){display:none}._breadcrumbs_11ua9_41 li:last-child{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}}._button_2cp7j_1{border-radius:var(--owl-radius-control);cursor:pointer;border:1px solid #0000;justify-content:center;align-items:center;gap:.5rem;min-height:2.5rem;padding:.5rem .875rem;font-weight:600;line-height:1.25rem;text-decoration:none;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}._button_2cp7j_1:disabled,._button_2cp7j_1[aria-disabled=true]{cursor:not-allowed;opacity:.58}._primary_2cp7j_26{color:#fff;background:var(--owl-accent-strong);border-color:var(--owl-accent-strong)}._primary_2cp7j_26:not(:disabled):hover{background:var(--owl-accent-hover);border-color:var(--owl-accent-hover)}._secondary_2cp7j_37{color:var(--owl-text);background:var(--owl-surface-raised);border-color:var(--owl-border-strong)}._secondary_2cp7j_37:not(:disabled):hover,._quiet_2cp7j_44:not(:disabled):hover{background:var(--owl-surface-hover)}._quiet_2cp7j_44{color:var(--owl-text-muted);background:0 0;border-color:#0000}._danger_2cp7j_54{color:var(--owl-danger);background:var(--owl-danger-surface);border-color:var(--owl-danger-border)}._danger_2cp7j_54:not(:disabled):hover{color:#fff;background:var(--owl-danger-hover);border-color:var(--owl-danger-hover)}._fullWidth_2cp7j_66{width:100%}._iconOnly_2cp7j_70{width:2.75rem;min-width:2.75rem;padding-inline:0}._field_2cp7j_76{gap:.4rem;display:grid}._label_2cp7j_81{color:var(--owl-text);font-size:.875rem;font-weight:600}._description_2cp7j_87{color:var(--owl-text-muted);margin:0;font-size:.8125rem;line-height:1.3rem}._input_2cp7j_94,._select_2cp7j_95,._textarea_2cp7j_96{width:100%;min-height:2.5rem;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-control);padding:.55rem .7rem}._textarea_2cp7j_96{resize:vertical;min-height:6rem}._input_2cp7j_94::placeholder,._textarea_2cp7j_96::placeholder{color:var(--owl-text-subtle)}._input_2cp7j_94[aria-invalid=true],._select_2cp7j_95[aria-invalid=true],._textarea_2cp7j_96[aria-invalid=true]{border-color:var(--owl-danger)}._input_2cp7j_94:disabled,._select_2cp7j_95:disabled,._textarea_2cp7j_96:disabled{cursor:not-allowed;opacity:.68}._fieldError_2cp7j_129{color:var(--owl-danger);margin:0;font-size:.8125rem}._checkbox_2cp7j_135{min-height:2.5rem;color:var(--owl-text);align-items:flex-start;gap:.65rem;display:flex}._checkbox_2cp7j_135 input{width:1.125rem;height:1.125rem;accent-color:var(--owl-accent-strong);margin-top:.2rem}._alert_2cp7j_150{border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-control);color:var(--owl-text);background:var(--owl-surface-raised);grid-template-columns:auto 1fr;gap:.65rem;padding:.75rem .875rem;display:grid}._alert_2cp7j_150 p,._alert_2cp7j_150 ul{margin:0}._info_2cp7j_166{color:var(--owl-info);background:var(--owl-info-surface);border-color:var(--owl-info-border)}._success_2cp7j_172{color:var(--owl-success);background:var(--owl-success-surface);border-color:var(--owl-success-border)}._warning_2cp7j_178{color:var(--owl-warning);background:var(--owl-warning-surface);border-color:var(--owl-warning-border)}._alertDanger_2cp7j_184{color:var(--owl-danger);background:var(--owl-danger-surface);border-color:var(--owl-danger-border)}._alertIcon_2cp7j_190{margin-top:.08rem;display:inline-flex}._badge_2cp7j_195{border:1px solid var(--owl-border);min-height:1.625rem;color:var(--owl-text-muted);background:var(--owl-surface-raised);border-radius:999px;align-items:center;gap:.35rem;padding:.15rem .55rem;font-size:.75rem;font-weight:600;line-height:1.1rem;display:inline-flex;position:relative}._badge_2cp7j_195:before{content:"";background:currentColor;border-radius:50%;width:.42rem;height:.42rem}._badgeActive_2cp7j_219{color:var(--owl-success);background:var(--owl-accent-soft)}._badgePending_2cp7j_224{color:var(--owl-info);background:var(--owl-info-surface)}._badgeAttention_2cp7j_229{color:var(--owl-warning);background:var(--owl-warning-surface)}._badgeDanger_2cp7j_234{color:var(--owl-danger);background:var(--owl-danger-surface)}._notificationRegion_2cp7j_239{gap:.75rem;margin-bottom:1rem;display:grid}._toastRegion_2cp7j_245{z-index:120;pointer-events:none;gap:.75rem;width:min(24rem,100vw - 2rem);display:grid;position:fixed;bottom:1.25rem;right:1.25rem}._toast_2cp7j_245{border-radius:var(--owl-radius-surface);min-height:3.25rem;box-shadow:var(--owl-shadow-raised);pointer-events:auto;border:1px solid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.65rem;padding:.65rem .65rem .65rem .85rem;display:grid}._backdrop_2cp7j_269{z-index:100;background:var(--owl-backdrop);place-items:center;padding:1rem;display:grid;position:fixed;inset:0}._dialog_2cp7j_279{width:min(100%,32rem);max-height:calc(100vh - 2rem);color:var(--owl-text);background:var(--owl-surface-raised);border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-surface);box-shadow:var(--owl-shadow-raised);overflow:auto}._dialogHeader_2cp7j_290{justify-content:space-between;align-items:flex-start;gap:1rem;padding:1.25rem 1.25rem 0;display:flex}._dialogHeader_2cp7j_290 h2{margin:0;font-size:1.125rem}._dialogBody_2cp7j_303{padding:1.25rem}._dialogActions_2cp7j_307{flex-wrap:wrap;justify-content:flex-end;gap:.75rem;padding:0 1.25rem 1.25rem;display:flex}._sheetBackdrop_2cp7j_315{z-index:90;background:var(--owl-backdrop);position:fixed;inset:0}._sheet_2cp7j_315{inset-block:0;background:var(--owl-sidebar);border-right:1px solid var(--owl-border-strong);width:min(20rem,100vw - 2rem);box-shadow:var(--owl-shadow-raised);position:absolute;left:0;overflow-y:auto}._sheetRight_2cp7j_333{background:var(--owl-surface);border-right:0;border-left:1px solid var(--owl-border-strong);width:min(38rem,100vw - 2rem);left:auto;right:0}._srOnly_2cp7j_342{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;padding:0;position:absolute;overflow:hidden}@media (width<=30rem){._toastRegion_2cp7j_245{width:calc(100vw - 2rem);bottom:1rem;right:1rem}._backdrop_2cp7j_269{align-items:stretch;padding:0}._dialog_2cp7j_279{border-radius:0;width:100%;max-height:100vh}._dialogActions_2cp7j_307 ._button_2cp7j_1{flex:auto}}@media (forced-colors:active){._primary_2cp7j_26,._secondary_2cp7j_37,._quiet_2cp7j_44,._danger_2cp7j_54,._badge_2cp7j_195,._alert_2cp7j_150{border-color:buttontext}}._lockedCanvas_1a3cj_1{background:radial-gradient(circle at 50% 10%, #dff4efb8, transparent 30rem), var(--owl-canvas);place-items:center;min-height:100vh;padding:1rem;display:grid}._lockedPanel_1a3cj_10{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);width:min(100%,28rem);box-shadow:var(--owl-shadow-raised);padding:2rem}._wordmark_1a3cj_19{color:var(--owl-text);letter-spacing:-.01em;align-items:center;gap:.65rem;font-size:1rem;font-weight:700;display:flex}._mark_1a3cj_29{flex:none;width:1.8rem;height:1.8rem}._lockedPanel_1a3cj_10 h1{margin:1.5rem 0 .5rem;font-size:1.5rem}._lockedPanel_1a3cj_10>p{color:var(--owl-text-muted)}._lockedForm_1a3cj_44{gap:1rem;margin-top:1.5rem;display:grid}._workspace_1a3cj_50{background:var(--owl-canvas);grid-template:"sidebar topbar"3.75rem"sidebar main"minmax(0,1fr)/16rem minmax(0,1fr);min-height:100vh;display:grid}._sidebar_1a3cj_59{background:var(--owl-sidebar);border-right:1px solid var(--owl-border);flex-direction:column;grid-area:sidebar;height:100vh;padding:1.25rem 1rem 1rem;display:flex;position:sticky;top:0}._navigation_1a3cj_71{align-content:start;gap:.9rem;min-height:0;margin:1.5rem 0 1rem;display:grid;overflow-y:auto}._workspaceNavigation_1a3cj_80,._navigationGroup_1a3cj_81,._settingsNavigation_1a3cj_82{gap:.2rem;display:grid}._workspaceNavigation_1a3cj_80{gap:.35rem}._workspaceNavigation_1a3cj_80 a svg{flex:none}._navigationGroup_1a3cj_81,._settingsNavigation_1a3cj_82{border-top:1px solid var(--owl-border);padding-top:.8rem}._navigationLabel_1a3cj_101{color:var(--owl-text-subtle);padding:0 .75rem .25rem;font-size:.75rem;font-weight:650}._currentProject_1a3cj_108{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.5rem;min-width:0;padding:.2rem .5rem .35rem .75rem;display:grid}._currentProject_1a3cj_108 strong{color:var(--owl-text);text-overflow:ellipsis;white-space:nowrap;font-size:.9375rem;font-weight:650;display:block;overflow:hidden}._currentProject_1a3cj_108 button{min-height:2rem;padding:.25rem .45rem;font-size:.75rem}._navigation_1a3cj_71>:not(._currentProject_1a3cj_108) a{min-height:2.5rem;color:var(--owl-text-muted);border-radius:var(--owl-radius-control);border:1px solid #0000;align-items:center;padding:.5rem .75rem;font-weight:560;text-decoration:none;display:flex}._navigation_1a3cj_71 a:hover{color:var(--owl-text);background:var(--owl-surface-hover)}._navigation_1a3cj_71>:not(._currentProject_1a3cj_108) a[aria-current=page]{color:var(--owl-accent-hover);background:var(--owl-accent-soft);border-color:color-mix(in srgb, var(--owl-accent) 24%, var(--owl-border));box-shadow:inset 3px 0 var(--owl-accent);font-weight:650}._sidebarFooter_1a3cj_158{border-top:1px solid var(--owl-border);margin-top:auto;padding-top:1rem}._topbar_1a3cj_164{z-index:20;background:var(--owl-surface);border-bottom:1px solid var(--owl-border);box-shadow:var(--owl-shadow-subtle);grid-area:topbar;align-items:center;gap:1rem;padding:0 2rem;display:flex;position:sticky;top:0}._menuButton_1a3cj_178{display:none}._main_1a3cj_182{grid-area:main;width:100%;min-width:0;max-width:104rem;margin-inline:auto;padding:2rem}._skipLink_1a3cj_191{z-index:200;color:var(--owl-text);background:var(--owl-accent-strong);border-radius:var(--owl-radius-control);padding:.6rem .8rem;position:fixed;top:.5rem;left:.5rem;transform:translateY(-200%)}._skipLink_1a3cj_191:focus{transform:translateY(0)}._mobileOnly_1a3cj_207{display:none}@media (width<64rem){._workspace_1a3cj_50{grid-template:"topbar"3.75rem"main"minmax(0,1fr)/minmax(0,1fr)}._sidebar_1a3cj_59{display:none}._topbar_1a3cj_164{padding-inline:1.5rem}._menuButton_1a3cj_178,._mobileOnly_1a3cj_207{display:inline-flex}._main_1a3cj_182{padding:1.5rem}}@media (width<=40rem){._topbar_1a3cj_164,._main_1a3cj_182{padding-inline:1rem}._lockedPanel_1a3cj_10{padding:1.5rem}}._listField_16ujt_1{border:0;gap:.75rem;min-width:0;margin:0;padding:0;display:grid}._listField_16ujt_1 legend{color:var(--owl-text);padding:0;font-size:.875rem;font-weight:600}._fieldDescription_16ujt_17{color:var(--owl-text-muted);margin:-.4rem 0 0;font-size:.8125rem}._listRows_16ujt_23{gap:.65rem;display:grid}._listRow_16ujt_23{grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:.5rem;display:grid}._rowError_16ujt_35{color:var(--owl-danger);grid-column:1/-1;margin:-.25rem 0 0;font-size:.8125rem}._copyValue_16ujt_42,._copyValueBlock_16ujt_43{align-items:center;gap:.25rem;min-width:0;display:inline-flex}._copyValueBlock_16ujt_43{background:var(--owl-surface-raised);border:1px solid var(--owl-border);border-radius:var(--owl-radius-control);justify-content:space-between;width:100%;padding:.4rem .4rem .4rem .65rem}._copyValue_16ujt_42 code,._copyValueBlock_16ujt_43 code{overflow-wrap:anywhere;min-width:0;color:var(--owl-text-muted);-webkit-user-select:all;user-select:all}._form_10g3i_1{gap:.85rem;max-width:36rem;display:grid}._identityMutation_10g3i_7>._form_10g3i_1{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);margin-block:1rem;padding:1rem}._form_10g3i_1>label,._inlineForm_10g3i_16>label,._confirmation_10g3i_17>label{color:var(--owl-text);font-size:.875rem;font-weight:600}._form_10g3i_1 button{justify-self:start}._workspace_10g3i_27{gap:2rem;display:grid}._sectionHeader_10g3i_32,._actions_10g3i_33,._inlineForm_10g3i_16{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;display:flex}._sectionHeader_10g3i_32 h2,._sectionHeader_10g3i_32 h3,._sectionHeader_10g3i_32 p{margin:0}._sectionHeader_10g3i_32>div{gap:.25rem;min-width:0;display:grid}._filterToolbar_10g3i_54{border-bottom:1px solid var(--owl-border);grid-template-columns:repeat(12,minmax(0,1fr));align-items:end;gap:.75rem;width:100%;min-width:0;padding-bottom:1rem;display:grid}._filterFields_10g3i_65{display:contents}._filterField_10g3i_65,._searchField_10g3i_70{min-width:0;color:var(--owl-text);grid-column:span 2;gap:.35rem;font-size:.8125rem;font-weight:600;display:grid}._searchField_10g3i_70{grid-column:span 4}._filterError_10g3i_84{color:var(--owl-danger);font-size:.8125rem;font-weight:500}._filterActions_10g3i_90{grid-column:span 2;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem;min-width:0;display:grid}._filterActions_10g3i_90>button{width:100%}._panel_10g3i_102,._cards_10g3i_103>li{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);min-width:0;padding:1rem}._list_10g3i_111,._cards_10g3i_103{gap:.5rem;margin:0;padding:0;list-style:none;display:grid}._list_10g3i_111 button,._list_10g3i_111 a{width:100%;min-height:3rem;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-control);cursor:pointer;font:inherit;text-align:left;justify-content:space-between;align-items:center;gap:1rem;padding:.6rem .75rem;font-weight:600;text-decoration:none;display:flex}._list_10g3i_111 a:hover,._list_10g3i_111 button:not(:disabled):hover{background:var(--owl-surface-hover)}._list_10g3i_111 button:disabled{cursor:not-allowed;opacity:.58}._list_10g3i_111 button[aria-pressed=true]{background:var(--owl-accent-soft);box-shadow:inset 3px 0 var(--owl-accent)}._userName_10g3i_155{overflow-wrap:anywhere;min-width:0}._cards_10g3i_103>li{gap:.75rem;display:grid}._cards_10g3i_103>li>*{min-width:0}._emptyNote_10g3i_169{color:var(--owl-text-muted);background:var(--owl-surface-raised);border:1px dashed var(--owl-border);border-radius:var(--owl-radius-control);margin:0;padding:.75rem}._panel_10g3i_102>._sectionHeader_10g3i_32{flex-direction:column;align-items:stretch}._panel_10g3i_102 h3,._cards_10g3i_103 strong,._cards_10g3i_103 span{overflow-wrap:anywhere;min-width:0}._actions_10g3i_33,._inlineForm_10g3i_16{justify-content:flex-start}._inlineForm_10g3i_16{max-width:36rem}._inlineForm_10g3i_16>label{flex-basis:100%}._inlineForm_10g3i_16>input{flex:16rem;width:auto}._userInventory_10g3i_208{gap:1rem;width:100%;min-width:0;display:grid}._userGrid_10g3i_215,._sessionColumns_10g3i_216{grid-template-columns:minmax(12rem,1fr) minmax(0,3fr);gap:1rem;display:grid}._sessionColumns_10g3i_216{grid-template-columns:repeat(auto-fit,minmax(16rem,1fr))}._identityMutation_10g3i_7{border-top:1px solid var(--owl-border);gap:1rem;margin-top:1.5rem;padding-top:1.5rem;display:grid}._authority_10g3i_234,._confirmation_10g3i_17{background:var(--owl-surface-raised);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);gap:.65rem;padding:1rem;display:grid}._authority_10g3i_234 legend{font-weight:700}._planReview_10g3i_248{background:var(--owl-accent-soft);border:2px solid var(--owl-accent-strong);border-radius:var(--owl-radius-surface);gap:.75rem;padding:1rem;display:grid}._planGroup_10g3i_257{border-left:.2rem solid var(--owl-border-strong);gap:.25rem;padding-left:.75rem;display:grid}._planReview_10g3i_248 p,._planGroup_10g3i_257 p{margin:0}._machineValue_10g3i_269{overflow-wrap:anywhere;color:var(--owl-text-muted);font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:.8125rem;display:block}._disclosureIcon_10g3i_277,._disclosureIconExpanded_10g3i_278{transition:transform .15s}._disclosureIconExpanded_10g3i_278{transform:rotate(180deg)}._expandedDetailCell_10g3i_286{background:var(--owl-surface-raised)!important;padding:0 1rem 1rem!important}._expandedDetailCell_10g3i_286>*{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-control);padding:1rem}code{overflow-wrap:anywhere}@media (width<=72rem){._filterToolbar_10g3i_54{grid-template-columns:repeat(4,minmax(0,1fr))}._searchField_10g3i_70,._filterActions_10g3i_90{grid-column:1/-1}._filterField_10g3i_65{grid-column:span 1}}@media (width<=48rem){._userGrid_10g3i_215{grid-template-columns:1fr}._filterToolbar_10g3i_54{grid-template-columns:repeat(2,minmax(0,1fr))}._searchField_10g3i_70,._filterActions_10g3i_90{grid-column:1/-1}._sectionHeader_10g3i_32{flex-direction:column;align-items:stretch}}@media (width<=30rem){._filterToolbar_10g3i_54{grid-template-columns:minmax(0,1fr)}._filterField_10g3i_65,._searchField_10g3i_70,._filterActions_10g3i_90{grid-column:1}._filterActions_10g3i_90{grid-template-columns:minmax(0,1fr)}._actions_10g3i_33>*{flex:100%}}._page_1bfcl_1{margin-top:.5rem}._form_1bfcl_5{gap:1rem;max-width:36rem;display:grid}._formActions_1bfcl_11{flex-wrap:wrap;justify-content:flex-end;gap:.75rem;margin-top:.5rem;display:flex}._stack_1bfcl_19{gap:1rem;display:grid}._projectIdentity_1bfcl_24{flex-wrap:wrap;align-items:center;gap:.45rem;min-width:0;display:inline-flex}._projectIdentityLabel_1bfcl_32{color:var(--owl-text-subtle);font-size:.75rem;font-weight:650}._projectIdentity_1bfcl_24 code{overflow-wrap:anywhere;font-size:.8125rem}._dashboardGrid_1bfcl_43{grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:1rem;margin:0;padding:0;list-style:none;display:grid}._dashboardGrid_1bfcl_43>li{min-width:0}._dashboardCard_1bfcl_56{min-width:0;height:100%;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);gap:.65rem;padding:1.1rem;text-decoration:none;transition:background-color .15s,border-color .15s,box-shadow .15s;display:grid}._dashboardCard_1bfcl_56:hover{background:var(--owl-surface-hover);border-color:var(--owl-border-strong);box-shadow:var(--owl-shadow-subtle)}._dashboardCard_1bfcl_56:focus-visible{outline:3px solid var(--owl-focus);outline-offset:2px}._dashboardCardHeader_1bfcl_84{justify-content:space-between;align-items:center;gap:.75rem;min-width:0;display:flex}._dashboardCardHeader_1bfcl_84 h3{overflow-wrap:anywhere;margin:0;font-size:.9375rem}._dashboardCardHeader_1bfcl_84 svg{color:var(--owl-text-subtle);flex:none}._dashboardValue_1bfcl_103{font-variant-numeric:tabular-nums;font-size:2rem;line-height:1}._dashboardDetail_1bfcl_109{overflow-wrap:anywhere;min-width:0;color:var(--owl-text-muted);font-size:.8125rem}._directoryControls_1bfcl_116{background:var(--owl-surface-raised);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);flex-wrap:wrap;align-items:center;gap:.75rem;margin-bottom:.75rem;padding:.75rem;display:flex}._directorySearch_1bfcl_128{flex:24rem;min-width:min(18rem,100%);max-width:34rem}._directorySearch_1bfcl_128 input{background:var(--owl-surface);min-height:2.75rem}._directoryResultCount_1bfcl_139{color:var(--owl-text-muted);font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;font-size:.8125rem}._inactiveFilter_1bfcl_147{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:999px;flex:none;padding:.45rem .7rem}._inactiveFilter_1bfcl_147:focus-within{border-color:var(--owl-accent);box-shadow:0 0 0 3px var(--owl-accent-soft)}._inactiveFilter_1bfcl_147 label{white-space:nowrap;align-items:center;gap:.5rem;min-height:auto;font-size:.875rem;font-weight:600}._inactiveFilter_1bfcl_147 input{margin:0}._projectDirectory_1bfcl_173{gap:.75rem;margin:0;padding:0;list-style:none;display:grid}._projectDirectoryItem_1bfcl_181{min-width:0;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:1rem;padding:1rem 1.1rem;text-decoration:none;transition:background-color .15s,border-color .15s,box-shadow .15s;display:grid}a._projectDirectoryItem_1bfcl_181:hover{background:var(--owl-surface-hover);border-color:var(--owl-border-strong);box-shadow:var(--owl-shadow-subtle)}._projectDirectoryItemStatic_1bfcl_205{cursor:default}._projectDirectoryBody_1bfcl_209{gap:.2rem;min-width:0;display:grid}._projectDirectoryBody_1bfcl_209 strong{overflow-wrap:anywhere;font-size:1rem;font-weight:650}._projectDirectoryBody_1bfcl_209 code{overflow-wrap:anywhere;color:var(--owl-text-muted);font-size:.8125rem}._projectDirectoryMeta_1bfcl_227{color:var(--owl-text-subtle);align-items:center;gap:.75rem;display:inline-flex}._resourceLink_1bfcl_234{color:var(--owl-text);font-weight:600}._muted_1bfcl_239{color:var(--owl-text-muted)}._machineValue_1bfcl_243{overflow-wrap:anywhere;max-width:38rem;color:var(--owl-text-muted);font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:.8125rem;display:block}._credential_1bfcl_252{max-width:100%;color:var(--owl-text);background:var(--owl-canvas);border:1px solid var(--owl-border-strong);border-radius:var(--owl-radius-control);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all;margin-top:.75rem;padding:.75rem;font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,monospace;display:block;overflow-x:auto}._detailSurface_1bfcl_267{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);padding:1.25rem}._disclosureIcon_1bfcl_274,._disclosureIconExpanded_1bfcl_275{transition:transform .15s}._disclosureIconExpanded_1bfcl_275{transform:rotate(180deg)}._expandedDetailCell_1bfcl_283{background:var(--owl-surface-raised)!important;padding:0 1rem 1rem!important}._inlineDetail_1bfcl_288{background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-control);gap:1rem;padding:1rem;display:grid}._inlineDetail_1bfcl_288>p{margin:0}._inlineDangerZone_1bfcl_301{background:color-mix(in srgb, var(--owl-danger-surface) 55%, var(--owl-surface));border:1px solid var(--owl-danger-border);border-radius:var(--owl-radius-control);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:1rem;margin-top:.5rem;padding:1rem;display:flex}._inlineDangerZone_1bfcl_301 h3,._inlineDangerZone_1bfcl_301 p{margin:0}._dangerZone_1bfcl_319{background:color-mix(in srgb, var(--owl-danger-surface) 55%, var(--owl-surface));border:1px solid var(--owl-danger-border);border-radius:var(--owl-radius-surface);margin-top:3rem;padding:1.25rem}._dangerZone_1bfcl_319 h2,._dangerZone_1bfcl_319 p{margin-top:0}._actions_1bfcl_332{flex-wrap:wrap;gap:.75rem;display:flex}._providerChoices_1bfcl_338{gap:.75rem;margin-top:1rem;display:grid}._providerChoice_1bfcl_338{width:100%;color:var(--owl-text);background:var(--owl-surface);border:1px solid var(--owl-border);border-radius:var(--owl-radius-surface);cursor:pointer;text-align:left;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:.85rem;padding:1rem;display:grid}._providerChoice_1bfcl_338:hover{background:var(--owl-surface-hover);border-color:var(--owl-border-strong)}._providerChoice_1bfcl_338 svg{width:1.5rem;height:1.5rem;color:var(--owl-accent)}._providerChoice_1bfcl_338 span{gap:.2rem;display:grid}._providerChoice_1bfcl_338 small{color:var(--owl-text-muted);font-size:.8125rem}@media (width<=48rem){._projectDirectoryItem_1bfcl_181{grid-template-columns:1fr}._projectDirectoryMeta_1bfcl_227{justify-content:space-between}} diff --git a/crates/owlauth-server/web/dist/control/assets/control-C2nM8UOp.js b/crates/owlauth-server/web/dist/control/assets/control-IsQYFkbw.js similarity index 64% rename from crates/owlauth-server/web/dist/control/assets/control-C2nM8UOp.js rename to crates/owlauth-server/web/dist/control/assets/control-IsQYFkbw.js index b2feb16..a5f67f7 100644 --- a/crates/owlauth-server/web/dist/control/assets/control-C2nM8UOp.js +++ b/crates/owlauth-server/web/dist/control/assets/control-IsQYFkbw.js @@ -6,4 +6,4 @@ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r= `+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{ue=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?le(n):``}function fe(e,t){switch(e.tag){case 26:case 27:case 5:return le(e.type);case 16:return le(`Lazy`);case 13:return e.child!==t&&t!==null?le(`Suspense Fallback`):le(`Suspense`);case 19:return le(`SuspenseList`);case 0:case 15:return de(e.type,!1);case 11:return de(e.type.render,!1);case 1:return de(e.type,!0);case 31:return le(`Activity`);default:return``}}function pe(e){try{var t=``,n=null;do t+=fe(e,n),n=e,e=e.return;while(e);return t}catch(e){return` Error generating stack: `+e.message+` `+e.stack}}var me=Object.prototype.hasOwnProperty,he=t.unstable_scheduleCallback,ge=t.unstable_cancelCallback,_e=t.unstable_shouldYield,ve=t.unstable_requestPaint,ye=t.unstable_now,be=t.unstable_getCurrentPriorityLevel,xe=t.unstable_ImmediatePriority,Se=t.unstable_UserBlockingPriority,Ce=t.unstable_NormalPriority,we=t.unstable_LowPriority,Te=t.unstable_IdlePriority,Ee=t.log,De=t.unstable_setDisableYieldValue,Oe=null,ke=null;function Ae(e){if(typeof Ee==`function`&&De(e),ke&&typeof ke.setStrictMode==`function`)try{ke.setStrictMode(Oe,e)}catch{}}var je=Math.clz32?Math.clz32:Pe,Me=Math.log,Ne=Math.LN2;function Pe(e){return e>>>=0,e===0?32:31-(Me(e)/Ne|0)|0}var Fe=256,Ie=262144,Le=4194304;function Re(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function ze(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Re(n))):i=Re(o):i=Re(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Re(n))):i=Re(o)):i=Re(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Be(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Ve(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function He(){var e=Le;return Le<<=1,!(Le&62914560)&&(Le=4194304),e}function Ue(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function We(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Ge(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),rn=!1;if(nn)try{var an={};Object.defineProperty(an,"passive",{get:function(){rn=!0}}),window.addEventListener(`test`,an,an),window.removeEventListener(`test`,an,an)}catch{rn=!1}var on=null,sn=null,cn=null;function ln(){if(cn)return cn;var e,t=sn,n=t.length,r,i=`value`in on?on.value:on.textContent,a=i.length;for(e=0;e=Vn),Wn=` `,Gn=!1;function Kn(e,t){switch(e){case`keyup`:return zn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function qn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Jn=!1;function Yn(e,t){switch(e){case`compositionend`:return qn(t);case`keypress`:return t.which===32?(Gn=!0,Wn):null;case`textInput`:return e=t.data,e===Wn&&Gn?null:e;default:return null}}function Xn(e,t){if(Jn)return e===`compositionend`||!Bn&&Kn(e,t)?(e=ln(),cn=sn=on=null,Jn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=vr(n)}}function br(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?br(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function xr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=At(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=At(e.document)}return t}function Sr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Cr=nn&&`documentMode`in document&&11>=document.documentMode,wr=null,Tr=null,Er=null,Dr=!1;function Or(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Dr||wr==null||wr!==At(r)||(r=wr,`selectionStart`in r&&Sr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Er&&_r(Er,r)||(Er=r,r=Td(Tr,`onSelect`),0>=o,i-=o,bi=1<<32-je(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),ki&&Si(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),ki&&Si(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return ki&&Si(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),ki&&Si(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&xa(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ea(c,o),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}o.type===y?(c=si(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=oi(o.type,o.key,o.props,null,e.mode,c),Ea(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l){if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}c=ui(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=xa(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,J(o),c);if(o.$$typeof===C)return b(e,r,Xi(e,o),c);Da(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=ci(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{q=0;var i=b(e,t,n,r);return Ta=null,i}catch(t){if(t===ga||t===va)throw t;var a=ni(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var ka=Oa(!0),Aa=Oa(!1),ja=!1;function Ma(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Na(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Pa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Fa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,jl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=$r(e),Qr(e,null,n),t}return Yr(e,r,t,n),$r(e)}function Ia(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,qe(e,n)}}function La(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ra=!1;function za(){if(Ra){var e=sa;if(e!==null)throw e}}function Ba(e,t,n,r){Ra=!1;var i=e.updateQueue;ja=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Pl&f)===f:(r&f)===f){f!==0&&f===oa&&(Ra=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:ja=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Hl|=o,e.lanes=o,e.memoizedState=d}}function Va(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ha(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,ws(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Cs(e,t,ua(c,r),uu(e)):Cs(e,t,r,uu(e))}catch(n){Cs(e,t,{then:function(){},status:`rejected`,reason:n},uu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function ps(){}function ms(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=hs(e).queue;fs(e,a,t,R,n===null?ps:function(){return gs(e),n(r)})}function hs(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:R,baseState:R,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Do,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Do,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function gs(e){var t=hs(e);t.next===null&&(t=e.alternate.memoizedState),Cs(e,t.next.queue,{},uu())}function _s(){return Yi(Qf)}function vs(){return So().memoizedState}function ys(){return So().memoizedState}function bs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=uu();e=Pa(n);var r=Fa(t,e,n);r!==null&&(fu(r,t,n),Ia(r,t,n)),t={cache:na()},e.payload=t;return}t=t.return}}function xs(e,t,n){var r=uu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ts(e)?Es(t,n):(n=Xr(e,t,n,r),n!==null&&(fu(n,e,r),Ds(n,t,r)))}function Ss(e,t,n){Cs(e,t,n,uu())}function Cs(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ts(e))Es(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,gr(s,o))return Yr(e,t,i,0),Ml===null&&Jr(),!1}catch{}if(n=Xr(e,t,i,r),n!==null)return fu(n,e,r),Ds(n,t,r),!0}return!1}function ws(e,t,n,r){if(r={lane:2,revertLane:ld(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ts(e)){if(t)throw Error(i(479))}else t=Xr(e,n,r,2),t!==null&&fu(t,e,2)}function Ts(e){var t=e.alternate;return e===ro||t!==null&&t===ro}function Es(e,t){ao=io=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ds(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,qe(e,n)}}var Os={readContext:Yi,use:To,useCallback:fo,useContext:fo,useEffect:fo,useImperativeHandle:fo,useLayoutEffect:fo,useInsertionEffect:fo,useMemo:fo,useReducer:fo,useRef:fo,useState:fo,useDebugValue:fo,useDeferredValue:fo,useTransition:fo,useSyncExternalStore:fo,useId:fo,useHostTransitionStatus:fo,useFormState:fo,useActionState:fo,useOptimistic:fo,useMemoCache:fo,useCacheRefresh:fo};Os.useEffectEvent=fo;var ks={readContext:Yi,use:To,useCallback:function(e,t){return xo().memoizedState=[e,t===void 0?null:t],e},useContext:Yi,useEffect:$o,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),Zo(4194308,4,as.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Zo(4194308,4,e,t)},useInsertionEffect:function(e,t){Zo(4,2,e,t)},useMemo:function(e,t){var n=xo();t=t===void 0?null:t;var r=e();if(oo){Ae(!0);try{e()}finally{Ae(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=xo();if(n!==void 0){var i=n(t);if(oo){Ae(!0);try{n(t)}finally{Ae(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=xs.bind(null,ro,e),[r.memoizedState,e]},useRef:function(e){var t=xo();return e={current:e},t.memoizedState=e},useState:function(e){e=$(e);var t=e.queue,n=Ss.bind(null,ro,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ss,useDeferredValue:function(e,t){return us(xo(),e,t)},useTransition:function(){var e=$(!1);return e=fs.bind(null,ro,e.queue,!0,!1),xo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=ro,a=xo();if(ki){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Ml===null)throw Error(i(349));Pl&127||jo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,$o(No.bind(null,r,o,e),[e]),r.flags|=2048,Yo(9,{destroy:void 0},Mo.bind(null,r,o,n,t),null),n},useId:function(){var e=xo(),t=Ml.identifierPrefix;if(ki){var n=xi,r=bi;n=(r&~(1<<32-je(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=so++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[et]=t,o[tt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Cc(t)}}return Oc(t),wc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Cc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=ne.current,Ii(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Di,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[et]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||jd(e.nodeValue,n)),e||Ni(t,!0)}else e=Bd(e).createTextNode(r),e[et]=t,t.stateNode=e}return Oc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ii(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[et]=t}else Li(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Oc(t),e=!1}else n=Ri(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?($a(t),t):($a(t),null);if(t.flags&128)throw Error(i(558))}return Oc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ii(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[et]=t}else Li(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Oc(t),a=!1}else a=Ri(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?($a(t),t):($a(t),null)}return $a(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Ec(t,t.updateQueue),Oc(t),null);case 4:return ie(),e===null&&xd(t.stateNode.containerInfo),Oc(t),null;case 10:return Wi(t.type),Oc(t),null;case 19:if(H(eo),r=t.memoizedState,r===null)return Oc(t),null;if(a=!!(t.flags&128),o=r.rendering,o===null){if(a)Dc(r,!1);else{if(Vl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=to(e),o!==null){for(t.flags|=128,Dc(r,!1),e=o.updateQueue,t.updateQueue=e,Ec(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ai(n,e),n=n.sibling;return U(eo,eo.current&1|2),ki&&Si(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&ye()>Ql&&(t.flags|=128,a=!0,Dc(r,!1),t.lanes=4194304)}}else{if(!a){if(e=to(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Ec(t,e),Dc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!ki)return Oc(t),null}else 2*ye()-r.renderingStartTime>Ql&&n!==536870912&&(t.flags|=128,a=!0,Dc(r,!1),t.lanes=4194304)}r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Oc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=ye(),e.sibling=null,n=eo.current,U(eo,a?n&1|2:n&1),ki&&Si(t,r.treeForkCount),e);case 22:case 23:return $a(t),qa(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Oc(t),t.subtreeFlags&6&&(t.flags|=8192)):Oc(t),n=t.updateQueue,n!==null&&Ec(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&H(fa),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Wi(ta),Oc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Ac(e,t){switch(Ti(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Wi(ta),ie(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return oe(t),null;case 31:if(t.memoizedState!==null){if($a(t),t.alternate===null)throw Error(i(340));Li()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if($a(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Li()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(eo),null;case 4:return ie(),null;case 10:return Wi(t.type),null;case 22:case 23:return $a(t),qa(),e!==null&&H(fa),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Wi(ta),null;case 25:return null;default:return null}}function jc(e,t){switch(Ti(t),t.tag){case 3:Wi(ta),ie();break;case 26:case 27:case 5:oe(t);break;case 4:ie();break;case 31:t.memoizedState!==null&&$a(t);break;case 13:$a(t);break;case 19:H(eo);break;case 10:Wi(t.type);break;case 22:case 23:$a(t),qa(),e!==null&&H(fa);break;case 24:Wi(ta)}}function Mc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Hu(t,t.return,e)}}function Nc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Hu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Hu(t,t.return,e)}}function Pc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ha(t,n)}catch(t){Hu(e,e.return,t)}}}function Fc(e,t,n){n.props=Is(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Hu(e,t,n)}}function Ic(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Hu(e,t,n)}}function Lc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null){if(typeof r==`function`)try{r()}catch(n){Hu(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Hu(e,t,n)}else n.current=null}}function Rc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Hu(e,e.return,t)}}function zc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[tt]=t}catch(t){Hu(e,e.return,t)}}function Bc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Vc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Bc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Hc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=qt));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Hc(e,t,n),e=e.sibling;e!==null;)Hc(e,t,n),e=e.sibling}function Uc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Uc(e,t,n),e=e.sibling;e!==null;)Uc(e,t,n),e=e.sibling}function Wc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[et]=e,t[tt]=n}catch(t){Hu(e,e.return,t)}}var Gc=!1,Kc=!1,qc=!1,Jc=typeof WeakSet==`function`?WeakSet:Set,Yc=null;function Xc(e,t){if(e=e.containerInfo,Rd=sp,e=xr(e),Sr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,Yc=t;Yc!==null;)if(t=Yc,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Yc=e;else for(;Yc!==null;){switch(t=Yc,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[et]=e,pt(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=yr(s,h),v=yr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=ou,ou=null;var o=nu,s=iu;if(tu=0,ru=nu=null,iu=0,jl&6)throw Error(i(331));var c=jl;if(jl|=4,El(o.current),vl(o,o.current,s,n),jl=c,nd(0,!1),ke&&typeof ke.onPostCommitFiberRoot==`function`)try{ke.onPostCommitFiberRoot(Oe,o)}catch{}return!0}finally{L.p=a,I.T=r,Ru(e,t)}}function Vu(e,t,n){t=fi(n,t),t=Hs(e.stateNode,t,2),e=Fa(e,t,2),e!==null&&(We(e,2),td(e))}function Hu(e,t,n){if(e.tag===3)Vu(e,e,n);else for(;t!==null;){if(t.tag===3){Vu(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(eu===null||!eu.has(r))){e=fi(n,e),n=Us(2),r=Fa(t,n,2),r!==null&&(Ws(n,r,t,e),We(r,2),td(r));break}}t=t.return}}function Uu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Al;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(zl=!0,i.add(n),e=Wu.bind(null,e,t,n),t.then(e,e))}function Wu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Ml===e&&(Pl&n)===n&&(Vl===4||Vl===3&&(Pl&62914560)===Pl&&300>ye()-Xl?!(jl&2)&&yu(e,0):Wl|=n,Kl===Pl&&(Kl=0)),td(e)}function Gu(e,t){t===0&&(t=He()),e=Zr(e,t),e!==null&&(We(e,t),td(e))}function Ku(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Gu(e,n)}function qu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Gu(e,n)}function Ju(e,t){return he(e,t)}var Yu=null,Xu=null,Zu=!1,Qu=!1,$u=!1,ed=0;function td(e){e!==Xu&&e.next===null&&(Xu===null?Yu=Xu=e:Xu=Xu.next=e),Qu=!0,Zu||(Zu=!0,cd())}function nd(e,t){if(!$u&&Qu){$u=!0;do for(var n=!1,r=Yu;r!==null;){if(!t){if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-je(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,sd(r,a))}else a=Pl,a=ze(r,r===Ml?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Be(r,a)||(n=!0,sd(r,a))}r=r.next}while(n);$u=!1}}function rd(){id()}function id(){Qu=Zu=!1;var e=0;ed!==0&&Gd()&&(e=ed);for(var t=ye(),n=null,r=Yu;r!==null;){var i=r.next,a=ad(r,t);a===0?(r.next=null,n===null?Yu=i:n.next=i,i===null&&(Xu=n)):(n=r,(e!==0||a&3)&&(Qu=!0)),r=i}tu!==0&&tu!==5||nd(e,!1),ed!==0&&(ed=0)}function ad(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Mt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),pt(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Mt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Mt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Mt(n.imageSizes)+`"]`)):i+=`[href="`+Mt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),pt(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Mt(r)+`"][href="`+Mt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),pt(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=ft(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);pt(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=ft(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),pt(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=ft(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),pt(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=ne.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=ft(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=ft(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=ft(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Mt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),pt(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Mt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Mt(n.href)+`"]`);if(r)return t.instance=r,pt(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),pt(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,pt(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),pt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,pt(a),a):(r=n,(a=mf.get(o))&&(r=h({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),pt(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,pt(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),pt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,v=/^[\\/]{2}/;function y(e,t){return t+e.replace(/\\/g,`/`)}var b=`popstate`;function x(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function S(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return D(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:O(t)}return A(t,n,null,e)}function C(e,t){if(e===!1||e==null)throw Error(t)}function w(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function T(){return Math.random().toString(36).substring(2,10)}function E(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function D(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?k(t):t,state:n,key:t&&t.key||r||T(),mask:i}}function O({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function k(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function A(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=x(e)?e:D(h.location,e,t);n&&n(r,e),l=u()+1;let d=E(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=x(e)?e:D(h.location,e,t);n&&n(r,e),l=u();let i=E(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return j(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(b,d),c=e,()=>{i.removeEventListener(b,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function j(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),C(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:O(t);return i=i.replace(/ $/,`%20`),!n&&v.test(i)&&(i=r+i),new URL(i,r)}var M=c(u(),1),N=class{#e=new Map;constructor(e){if(e)for(let[t,n]of e)this.set(t,n)}get(e){if(this.#e.has(e))return this.#e.get(e);if(e.defaultValue!==void 0)return e.defaultValue;throw Error(`No value found for context`)}set(e,t){this.#e.set(e,t)}},P=new Set([`lazy`,`caseSensitive`,`path`,`id`,`index`,`children`]);function F(e){return P.has(e)}var I=new Set([`lazy`,`caseSensitive`,`path`,`id`,`index`,`middleware`,`children`]);function L(e){return I.has(e)}function R(e){return e.index===!0}function z(e){let t={};return e.Component&&Object.assign(t,{element:M.createElement(e.Component),Component:void 0}),e.HydrateFallback&&Object.assign(t,{hydrateFallbackElement:M.createElement(e.HydrateFallback),HydrateFallback:void 0}),e.ErrorBoundary&&Object.assign(t,{errorElement:M.createElement(e.ErrorBoundary),ErrorBoundary:void 0}),t}function B(e,t=z,n=[],r={},i=!1){return e.map((e,a)=>{let o=[...n,String(a)],s=typeof e.id==`string`?e.id:o.join(`-`);if(C(e.index!==!0||!e.children,`Cannot specify children on an index route`),C(i||!r[s],`Found a route id collision on id "${s}". Route id's must be globally unique within Data Router usages`),R(e)){let n={...e,id:s};return r[s]=V(n,t(n)),n}{let n={...e,id:s,children:void 0};return r[s]=V(n,t(n)),e.children&&(n.children=B(e.children,t,o,r,i)),n}})}function V(e,t){return Object.assign(e,{...t,...typeof t.lazy==`object`&&t.lazy!=null?{lazy:{...e.lazy,...t.lazy}}:{}})}function H(e,t,n=`/`){return U(e,t,n,!1)}function U(e,t,n,r,i){let a=be((typeof t==`string`?k(t):t).pathname||`/`,n);if(a==null)return null;let o=i??te(e),s=null,c=ye(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;C(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=Ae([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(C(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),ne(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:pe(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=ve(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of re(e.path))a(e,t,!0,n)}),t}function re(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=re(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function W(e){e.sort((e,t)=>e.score===t.score?me(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var ie=/^:[\w-]+$/,ae=/^:[\w-]+/,oe=3.5,se=3,ce=2,le=1,ue=10,de=-2,fe=e=>e===`*`;function pe(e,t){let n=e.split(`/`),r=n.length;return n.some(fe)&&(r+=de),t&&(r+=ce),n.filter(e=>!fe(e)).reduce((e,t)=>e+(ie.test(t)?se:ae.test(t)?oe:t===``?le:ue),r)}function me(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function he(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return e[t]=n&&!i?void 0:(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function ve(e,t=!1,n=!0){w(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(?=\/|$|\()/g,`(?:/$1)?`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function ye(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return w(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function be(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function xe({basename:e,pathname:t}){return t===`/`?e:Ae([e,t])}var Se=e=>_.test(e);function Ce(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?k(e):e,a;return n?(n=ke(n),a=n.startsWith(`/`)?we(n.substring(1),`/`):we(n,t)):a=t,{pathname:a,search:Ne(r),hash:Pe(i)}}function we(e,t){let n=je(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Te(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Ee(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function De(e){let t=Ee(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function Oe(e,t,n,r=!1){let i;typeof e==`string`?i=k(e):(i={...e},C(!i.pathname||!i.pathname.includes(`?`),Te(`?`,`pathname`,`search`,i)),C(!i.pathname||!i.pathname.includes(`#`),Te(`#`,`pathname`,`hash`,i)),C(!i.search||!i.search.includes(`#`),Te(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=Ce(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var ke=e=>e.replace(/[\\/]{2,}/g,`/`),Ae=e=>ke(e.join(`/`)),je=e=>e.replace(/\/+$/,``),Me=e=>je(e).replace(/^\/*/,`/`),Ne=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Pe=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Fe=[`EvalError`,`RangeError`,`ReferenceError`,`SyntaxError`,`TypeError`,`URIError`],Ie=class{status;statusText;data;error;internal;constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Le(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Re(e){return Ae(e.map(e=>e.route.path).filter(Boolean))||`/`}function ze(e,t){let n=new URL(typeof e==`string`||e instanceof URL?e:e.url),r=typeof t==`string`?k(t):t;if(n.pathname=r.pathname||`/`,r.search){let e=new URLSearchParams(r.search),t=e.getAll(`index`);e.delete(`index`);for(let n of t.filter(Boolean))e.append(`index`,n);let i=e.toString();n.search=i?`?${i}`:``}else n.search=``;return n.hash=r.hash||``,n}var Be=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ve(e,t){let n=e;if(typeof n!=`string`||!_.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Be)try{let e=new URL(window.location.href),r=v.test(n)?new URL(y(n,e.protocol)):new URL(n),a=be(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{w(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}var He=Symbol(`Uninstrumented`),Ue=new WeakMap;function We(e,t){let n={lazy:[],"lazy.loader":[],"lazy.action":[],"lazy.middleware":[],middleware:[],loader:[],action:[]};e.forEach(e=>e({id:t.id,index:t.index,path:t.path,instrument(e){e.lazy!=null&&n.lazy.push(e.lazy),e[`lazy.loader`]!=null&&n[`lazy.loader`].push(e[`lazy.loader`]),e[`lazy.action`]!=null&&n[`lazy.action`].push(e[`lazy.action`]),e[`lazy.middleware`]!=null&&n[`lazy.middleware`].push(e[`lazy.middleware`]),e.middleware!=null&&n.middleware.push(e.middleware),e.loader!=null&&n.loader.push(e.loader),e.action!=null&&n.action.push(e.action)}}));let r={};if(typeof t.lazy==`function`&&n.lazy.length>0){let e=t.lazy;r.lazy=async(...t)=>Xe(await Ze(n.lazy,void 0,()=>e(...t),Qe))}if(typeof t.lazy==`object`){let e=t.lazy;if(typeof e.middleware==`function`&&n[`lazy.middleware`].length>0){let t=e.middleware;r.lazy=Object.assign(r.lazy||{},{middleware:async(...e)=>Xe(await Ze(n[`lazy.middleware`],void 0,()=>t(...e),Qe))})}if(typeof e.loader==`function`&&n[`lazy.loader`].length>0){let t=e.loader;r.lazy=Object.assign(r.lazy||{},{loader:async(...e)=>Xe(await Ze(n[`lazy.loader`],void 0,()=>t(...e),Qe))})}if(typeof e.action==`function`&&n[`lazy.action`].length>0){let t=e.action;r.lazy=Object.assign(r.lazy||{},{action:async(...e)=>Xe(await Ze(n[`lazy.action`],void 0,()=>t(...e),Qe))})}}if(typeof t.loader==`function`&&n.loader.length>0){let e=Ke(t.loader),i=async(...t)=>Xe(await Ze(n.loader,$e(t[0]),()=>e(...t),Qe));e.hydrate===!0&&(i.hydrate=!0),qe(i,e),r.loader=i}if(typeof t.action==`function`&&n.action.length>0){let e=Ke(t.action),i=async(...t)=>Xe(await Ze(n.action,$e(t[0]),()=>e(...t),Qe));qe(i,e),r.action=i}return t.middleware&&t.middleware.length>0&&n.middleware.length>0&&(r.middleware=t.middleware.map(e=>{let t=Ke(e),r=async(...e)=>Xe(await Ze(n.middleware,$e(e[0]),()=>t(...e),Qe));return qe(r,t),r})),r}function Ge(e,t){let n={navigate:[],fetch:[]};if(t.forEach(e=>e({instrument(e){e.navigate!=null&&n.navigate.push(e.navigate),e.fetch!=null&&n.fetch.push(e.fetch)}})),n.navigate.length>0){let t=Ke(e.navigate),r=async(...r)=>{let[i,a]=r,o,s={to:typeof i==`number`||typeof i==`string`?i:i?O(i):`.`,...et(e,a??{})};return Xe(await Ze(n.navigate,s,async()=>{if(typeof i==`number`)return await t(...r);let n=Je(e,e=>{o=e});try{return await t(...r)}finally{n()}},e=>({...Qe(e),meta:o})))};qe(r,t),e.navigate=r}if(n.fetch.length>0){let t=Ke(e.fetch),r=async(...r)=>{let[i,a,o,s]=r,c;return Xe(await Ze(n.fetch,{href:o??`.`,fetcherKey:i,...et(e,s??{})},async()=>{let n=Je(e,e=>{c=e});try{return await t(...r)}finally{n()}},e=>({...Qe(e),meta:c})))};qe(r,t),e.fetch=r}return e}function Ke(e){return e[He]??e}function qe(e,t){e[He]=t}function Je(e,t){return Ue.set(e,t),()=>{Ue.get(e)===t&&Ue.delete(e)}}function Ye(e){let t=Ue.get(e);return Ue.delete(e),t}function Xe(e){if(e.type===`error`)throw e.value;return e.value}async function Ze(e,t,n,r,i={result:null,innerResult:null},a=e.length-1){let o=e[a];if(o){let s,c=async()=>(s?console.error(`You cannot call instrumented handlers more than once`):s=Ze(e,t,n,r,i,a-1),await s,C(i.innerResult,`Expected an inner result`),i.innerResult);try{await o(c,t)}catch(e){console.error(`An instrumentation function threw an error:`,e)}s||await c(),await s}else{try{i.result={type:`success`,value:await n()}}catch(e){i.result={type:`error`,value:e}}i.innerResult=r(i.result,t)}return i.result?i.result:(i.result={type:`error`,value:Error(`No result assigned in instrumentation chain.`)},i.innerResult=r(i.result,t),i.result)}function Qe(e){return e.type===`error`&&e.value instanceof Error?{status:`error`,error:e.value}:{status:`success`,error:void 0}}function $e(e){let{request:t,context:n,params:r}=e;return{...e,request:tt(t),params:{...r},context:nt(n)}}function et(e,t){return{currentUrl:O(e.state.location),...`formMethod`in t?{formMethod:t.formMethod}:{},...`formEncType`in t?{formEncType:t.formEncType}:{},...`formData`in t?{formData:t.formData}:{},...`body`in t?{body:t.body}:{}}}function tt(e){return{method:e.method,url:e.url,headers:{get:(...t)=>e.headers.get(...t)}}}function nt(e){return{get:t=>e.get(t)}}var rt=[`POST`,`PUT`,`PATCH`,`DELETE`],it=new Set(rt),at=[`GET`,...rt],ot=new Set(at),st=new Set([301,302,303,307,308]),ct=new Set([307,308]),lt={state:`idle`,location:void 0,matches:void 0,historyAction:void 0,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0},ut={state:`idle`,data:void 0,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0},dt={state:`unblocked`,proceed:void 0,reset:void 0,location:void 0},ft=`remix-router-transitions`,pt=Symbol(`ResetLoaderData`),mt=class{#e;#t;#n;#r;constructor(e){this.#e=e,this.#t=te(e)}get stableRoutes(){return this.#e}get activeRoutes(){return this.#n??this.#e}get branches(){return this.#r??this.#t}get hasHMRRoutes(){return this.#n!=null}setRoutes(e){this.#e=e,this.#t=te(e)}setHmrRoutes(e){this.#n=e,this.#r=te(e)}commitHmrRoutes(){this.#n&&(this.#e=this.#n,this.#t=this.#r,this.#n=void 0,this.#r=void 0)}};function ht(e){let t=e.window?e.window:typeof window<`u`?window:void 0,n=t!==void 0&&t.document!==void 0&&t.document.createElement!==void 0;C(e.routes.length>0,`You must provide a non-empty routes array to createRouter`);let r=e.hydrationRouteProperties||[],i=e.mapRouteProperties,a=i||(()=>({}));if(e.instrumentations){let t=e.instrumentations;a=e=>({...i?.(e),...We(t.map(e=>e.route).filter(Boolean),e)})}let o={},s=new mt(B(e.routes,a,void 0,o)),c=e.basename||`/`;c.startsWith(`/`)||(c=`/${c}`);let l=e.dataStrategy||Mt,u={...e.future},d=null,f=new Set,p=null,m=null,h=null,g=null,_=e.hydrationData!=null,v=U(s.activeRoutes,e.history.location,c,!1,s.branches),y=!1,b=null,x,S;if(v==null&&!e.patchRoutesOnNavigation){let t=nn(404,{pathname:e.history.location.pathname}),{matches:n,route:r}=tn(s.activeRoutes);x=!0,S=!x,v=n,b={[r.id]:t}}else if(v&&!e.hydrationData&&et(v,s.activeRoutes,e.history.location.pathname).active&&(v=null),!v){x=!1,S=!x,v=[];let t=et(null,s.activeRoutes,e.history.location.pathname);t.active&&t.matches&&(y=!0,v=t.matches)}else if(v.some(e=>e.route.lazy))x=!1,S=!x;else if(!v.some(e=>bt(e.route)))x=!0,S=!x;else{let t=e.hydrationData?e.hydrationData.loaderData:null,n=e.hydrationData?e.hydrationData.errors:null,r=v;if(n){let e=v.findIndex(e=>n[e.route.id]!==void 0);r=r.slice(0,e+1)}S=!1,x=!0,r.forEach(e=>{let r=xt(e.route,t,n);S||=r.renderFallback,x&&=!r.shouldLoad})}let T,E={historyAction:e.history.action,location:e.history.location,matches:v,initialized:x,renderFallback:S,navigation:lt,restoreScrollPosition:e.hydrationData==null&&null,preventScrollReset:!1,revalidation:`idle`,loaderData:e.hydrationData&&e.hydrationData.loaderData||{},actionData:e.hydrationData&&e.hydrationData.actionData||null,errors:e.hydrationData&&e.hydrationData.errors||b,fetchers:new Map,blockers:new Map},O=`POP`,A=null,M=!1,P,F=!1,I=new Map,L=null,R=!1,z=!1,V=new Set,H=new Map,te=0,ne=-1,re=new Map,W=new Set,ie=new Map,ae=new Map,oe=new Set,se=new Map,ce,le=null;function ue(){if(d=e.history.listen(({action:t,location:n,delta:r})=>{if(ce){ce(),ce=void 0;return}w(se.size===0||r!=null,"You are trying to use a blocker on a POP navigation to a location that was not created by @remix-run/router. This will fail silently in production. This can happen if you are navigating outside the router via `window.history.pushState`/`window.location.hash` instead of using router navigation APIs. This can also happen if you are using createHashRouter and the user manually changes the URL.");let i=qe({currentLocation:E.location,nextLocation:n,historyAction:t});if(i&&r!=null){let t=new Promise(e=>{ce=e});e.history.go(r*-1),Ke(i,{state:`blocked`,location:n,proceed(){Ke(i,{state:`proceeding`,proceed:void 0,reset:void 0,location:n}),t.then(()=>e.history.go(r))},reset(){let e=new Map(E.blockers);e.set(i,dt),pe({blockers:e})}}),A?.resolve(),A=null;return}return _e(t,n)}),n){On(t,I);let e=()=>kn(t,I);t.addEventListener(`pagehide`,e),L=()=>t.removeEventListener(`pagehide`,e)}return E.initialized||_e(`POP`,E.location,{initialHydration:!0}),T}function de(){d&&d(),L&&L(),f.clear(),P&&P.abort(),E.fetchers.forEach((e,t)=>Fe(E.fetchers,t)),E.blockers.forEach((e,t)=>Ue(t))}function fe(e){if(f.add(e),p){let{newErrors:t}=p;p=null,e(E,{deletedFetchers:[],newErrors:t,viewTransitionOpts:void 0,flushSync:!1})}return()=>f.delete(e)}function pe(e,t={}){e.matches&&=e.matches.map(e=>{let t=o[e.route.id],n=e.route;return n.element!==t.element||n.errorElement!==t.errorElement||n.hydrateFallbackElement!==t.hydrateFallbackElement?{...e,route:t}:e}),E={...E,...e};let n=[],r=[];E.fetchers.forEach((e,t)=>{e.state===`idle`&&(oe.has(t)?n.push(t):r.push(t))}),oe.forEach(e=>{!E.fetchers.has(e)&&!H.has(e)&&n.push(e)}),f.size===0&&(p={newErrors:e.errors??null}),[...f].forEach(r=>r(E,{deletedFetchers:n,newErrors:e.errors??null,viewTransitionOpts:t.viewTransitionOpts,flushSync:t.flushSync===!0})),n.forEach(e=>Fe(E.fetchers,e)),r.forEach(e=>E.fetchers.delete(e))}function me(t,n,{flushSync:r}={}){let i=E.actionData!=null&&E.navigation.formMethod!=null&&vn(E.navigation.formMethod)&&E.navigation.state===`loading`&&t.state?._isRedirect!==!0,a;a=n.actionData?Object.keys(n.actionData).length>0?n.actionData:null:i?E.actionData:null;let o=n.loaderData?Qt(E.loaderData,n.loaderData,n.matches||[],n.errors):E.loaderData,c=E.blockers;c.size>0&&!R&&(c=new Map(c),c.forEach((e,t)=>c.set(t,dt)));let l=!R&&$e(t,n.matches||E.matches),u=M===!0||E.navigation.formMethod!=null&&vn(E.navigation.formMethod)&&t.state?._isRedirect!==!0;s.commitHmrRoutes(),R||O===`POP`||(O===`PUSH`?e.history.push(t,t.state):O===`REPLACE`&&e.history.replace(t,t.state));let d;if(O===`POP`){let e=I.get(E.location.pathname);e&&e.has(t.pathname)?d={currentLocation:E.location,nextLocation:t}:I.has(t.pathname)&&(d={currentLocation:t,nextLocation:E.location})}else if(F){let e=I.get(E.location.pathname);e?e.add(t.pathname):(e=new Set([t.pathname]),I.set(E.location.pathname,e)),d={currentLocation:E.location,nextLocation:t}}pe({...n,actionData:a,loaderData:o,historyAction:O,location:t,initialized:!0,renderFallback:!1,navigation:lt,revalidation:`idle`,restoreScrollPosition:l,preventScrollReset:u,blockers:c},{viewTransitionOpts:d,flushSync:r===!0}),O=`POP`,M=!1,F=!1,R=!1,z=!1,A?.resolve(),A=null,le?.resolve(),le=null}async function he(t,n){if(A?.resolve(),A=null,typeof t==`number`){A||=An();let n=A.promise;return e.history.go(t),n}let r=Ye(T),{path:i,submission:a,error:o}=vt(!1,_t(E.location,E.matches,c,t,n?.fromRouteId,n?.relative),n),s;n?.mask&&(s={pathname:``,search:``,hash:``,...typeof n.mask==`string`?k(n.mask):{...E.location.mask,...n.mask}});let l=E.location,u=D(l,i,n&&n.state,void 0,s);u={...u,...e.history.encodeLocation(u)};let d=n&&n.replace!=null?n.replace:void 0,f=`PUSH`;d===!0?f=`REPLACE`:d===!1||a!=null&&vn(a.formMethod)&&a.formAction===E.location.pathname+E.location.search&&(f=`REPLACE`);let p=n&&`preventScrollReset`in n?n.preventScrollReset===!0:void 0,m=(n&&n.flushSync)===!0,h=qe({currentLocation:l,nextLocation:u,historyAction:f});if(h){Ke(h,{state:`blocked`,location:u,proceed(){Ke(h,{state:`proceeding`,proceed:void 0,reset:void 0,location:u}),he(t,n)},reset(){let e=new Map(E.blockers);e.set(h,dt),pe({blockers:e})}});return}await _e(f,u,{submission:a,pendingError:o,preventScrollReset:p,replace:n&&n.replace,enableViewTransition:n&&n.viewTransition,flushSync:m,callSiteDefaultShouldRevalidate:n&&n.defaultShouldRevalidate,instrumentationNavigateMetaReceiver:r})}function ge(){le||=An(),Ae(),pe({revalidation:`loading`});let e=le.promise;return E.navigation.state===`submitting`?e:E.navigation.state===`idle`?(_e(E.historyAction,E.location,{startUninterruptedRevalidation:!0}),e):(_e(O||E.historyAction,E.navigation.location,{overrideNavigation:E.navigation,enableViewTransition:F===!0}),e)}async function _e(t,n,r){P&&P.abort(),P=null,O=t,R=(r&&r.startUninterruptedRevalidation)===!0,Qe(E.location,E.matches),M=(r&&r.preventScrollReset)===!0,F=(r&&r.enableViewTransition)===!0;let i=s.activeRoutes,a=r?.initialHydration&&E.matches&&E.matches.length>0&&!y?E.matches:U(i,n,c,!1,s.branches),o=(r&&r.flushSync)===!0;if(a&&E.initialized&&!z&&on(E.location,n)&&!(r&&r.submission&&vn(r.submission.formMethod))){me(n,{matches:a},{flushSync:o});return}let l=et(a,i,n.pathname);if(l.active&&l.matches&&(a=l.matches),r?.instrumentationNavigateMetaReceiver){let t=xn(e.history,n,a);r.instrumentationNavigateMetaReceiver(t)}if(!a){let{error:e,notFoundMatches:t,route:r}=Je(n.pathname);me(n,{matches:t,loaderData:{},errors:{[r.id]:e}},{flushSync:o});return}let u=r&&r.overrideNavigation?{...r.overrideNavigation,matches:a,historyAction:t}:void 0;P=new AbortController;let d=qt(e.history,n,P.signal,r&&r.submission),f=e.getContext?await e.getContext():new N,p;if(r&&r.pendingError)p=[en(a).route.id,{type:`error`,error:r.pendingError}];else if(r&&r.submission&&vn(r.submission.formMethod)){let i=await ve(d,n,r.submission,a,t,f,l.active,r&&r.initialHydration===!0,{replace:r.replace,flushSync:o});if(i.shortCircuited)return;if(i.pendingActionResult){let[e,t]=i.pendingActionResult;if(dn(t)&&Le(t.error)&&t.error.status===404){P=null,me(n,{matches:i.matches,loaderData:{},errors:{[e]:t.error}});return}}a=i.matches||a,p=i.pendingActionResult,u=Cn(n,a,t,r.submission),o=!1,l.active=!1,d=qt(e.history,d.url,d.signal)}let{shortCircuited:m,matches:h,loaderData:g,errors:_,workingFetchers:v}=await ye(d,n,a,t,f,l.active,u,r&&r.submission,r&&r.fetcherSubmission,r&&r.replace,r&&r.initialHydration===!0,o,p,r&&r.callSiteDefaultShouldRevalidate);m||(P=null,me(n,{matches:h||a,...$t(p),loaderData:g,errors:_,...v?{fetchers:v}:{}}))}async function ve(t,n,i,l,u,d,f,p,m={}){if(Ae(),pe({navigation:wn(n,l,u,i)},{flushSync:m.flushSync===!0}),f){let e=await tt(l,n.pathname,t.signal);if(e.type===`aborted`)return{shortCircuited:!0};if(e.type===`error`){if(e.partialMatches.length===0){let{matches:t,route:n}=tn(s.activeRoutes);return{matches:t,pendingActionResult:[n.id,{type:`error`,error:e.error}]}}let t=en(e.partialMatches).route.id;return{matches:e.partialMatches,pendingActionResult:[t,{type:`error`,error:e.error}]}}if(e.matches)l=e.matches;else{let{notFoundMatches:e,error:t,route:r}=Je(n.pathname);return{matches:e,pendingActionResult:[r.id,{type:`error`,error:t}]}}}let h,g=bn(l,n);if(!g.route.action&&!g.route.lazy)h={type:`error`,error:nn(405,{method:t.method,pathname:n.pathname,routeId:g.route.id})};else{let e=await Oe(t,n,Rt(a,o,t,n,l,g,p?[]:r,d),d,null);if(h=e[g.route.id],!h){for(let t of l)if(e[t.route.id]){h=e[t.route.id];break}}if(t.signal.aborted)return{shortCircuited:!0}}if(fn(h)){let n;return n=m&&m.replace!=null?m.replace:Kt(h.response.headers.get(`Location`),new URL(t.url),c,e.history)===E.location.pathname+E.location.search,await De(t,h,!0,{submission:i,replace:n}),{shortCircuited:!0}}if(dn(h)){let e=en(l,g.route.id);return(m&&m.replace)!==!0&&(O=`PUSH`),{matches:l,pendingActionResult:[e.route.id,h,g.route.id]}}return{matches:l,pendingActionResult:[g.route.id,h]}}async function ye(t,n,i,l,u,d,f,p,m,h,g,_,v,y){let b=f||Cn(n,i,l,p),x=p||m||Sn(b),S=!R&&!g;if(d){if(S){let e=xe(v);pe({navigation:b,...e===void 0?{}:{actionData:e}},{flushSync:_})}let e=await tt(i,n.pathname,t.signal);if(e.type===`aborted`)return{shortCircuited:!0};if(e.type===`error`){if(e.partialMatches.length===0){let{matches:t,route:n}=tn(s.activeRoutes);return{matches:t,loaderData:{},errors:{[n.id]:e.error}}}let t=en(e.partialMatches).route.id;return{matches:e.partialMatches,loaderData:{},errors:{[t]:e.error}}}if(e.matches)i=e.matches;else{let{error:e,notFoundMatches:t,route:r}=Je(n.pathname);return{matches:t,loaderData:{},errors:{[r.id]:e}}}}let C=s.activeRoutes,{dsMatches:w,revalidatingFetchers:T}=yt(t,u,a,o,e.history,E,i,x,n,g?[]:r,g===!0,z,V,oe,ie,W,C,c,e.patchRoutesOnNavigation!=null,s.branches,v,y);if(ne=++te,!e.dataStrategy&&!w.some(e=>e.shouldLoad)&&!w.some(e=>e.route.middleware&&e.route.middleware.length>0)&&T.length===0){let e=new Map(E.fetchers),t=Be(e);return me(n,{matches:i,loaderData:{},errors:v&&dn(v[1])?{[v[0]]:v[1].error}:null,...$t(v),...t?{fetchers:e}:{}},{flushSync:_}),{shortCircuited:!0}}if(S){let e={};if(!d){e.navigation=b;let t=xe(v);t!==void 0&&(e.actionData=t)}T.length>0&&(e.fetchers=Ce(T)),pe(e,{flushSync:_})}T.forEach(e=>{Re(e.key),e.controller&&H.set(e.key,e.controller)});let D=()=>T.forEach(e=>Re(e.key));P&&P.signal.addEventListener(`abort`,D);let{loaderResults:O,fetcherResults:k}=await ke(w,T,t,n,u);if(t.signal.aborted)return{shortCircuited:!0};P&&P.signal.removeEventListener(`abort`,D),T.forEach(e=>H.delete(e.key));let A=rn(O);if(A)return await De(t,A.result,!0,{replace:h}),{shortCircuited:!0};if(A=rn(k),A)return W.add(A.key),await De(t,A.result,!0,{replace:h}),{shortCircuited:!0};let j=new Map(E.fetchers),{loaderData:M,errors:N}=Zt(E,i,O,v,T,k,j);g&&E.errors&&(N={...E.errors,...N});let F=Be(j),I=Ve(ne,j),L=F||I||T.length>0;return{matches:i,loaderData:M,errors:N,...L?{workingFetchers:j}:{}}}function xe(e){if(e&&!dn(e[1]))return{[e[0]]:e[1].data};if(E.actionData)return Object.keys(E.actionData).length===0?null:E.actionData}function Ce(e){let t=new Map(E.fetchers);return e.forEach(e=>{let n=t.get(e.key),r=Tn(void 0,n?n.data:void 0);t.set(e.key,r)}),t}async function we(t,n,r,i){Re(t);let a=(i&&i.flushSync)===!0,o=Ye(T),l=s.activeRoutes,u=_t(E.location,E.matches,c,r,n,i?.relative),d=U(l,u,c,!1,s.branches),f=et(d,l,u);if(f.active&&f.matches&&(d=f.matches),o&&o(xn(e.history,u,d)),!d){Me(t,n,nn(404,{pathname:u}),{flushSync:a});return}let{path:p,submission:m,error:h}=vt(!0,u,i);if(h){Me(t,n,h,{flushSync:a});return}let g=e.getContext?await e.getContext():new N,_=(i&&i.preventScrollReset)===!0;if(m&&vn(m.formMethod)){await Te(t,n,p,d,g,f.active,a,_,m,i&&i.defaultShouldRevalidate);return}ie.set(t,{routeId:n,path:p}),await Ee(t,n,p,d,g,f.active,a,_,m)}async function Te(t,n,i,l,u,d,f,p,m,h){Ae(),ie.delete(t),je(t,En(m,E.fetchers.get(t)),{flushSync:f});let g=new AbortController,_=qt(e.history,i,g.signal,m);if(d){let e=await tt(l,new URL(_.url).pathname,_.signal,t);if(e.type===`aborted`)return;if(e.type===`error`){Me(t,n,e.error,{flushSync:f});return}if(e.matches)l=e.matches;else{Me(t,n,nn(404,{pathname:i}),{flushSync:f});return}}let v=bn(l,i);if(!v.route.action&&!v.route.lazy){Me(t,n,nn(405,{method:m.formMethod,pathname:i,routeId:n}),{flushSync:f});return}H.set(t,g);let y=te,b=Rt(a,o,_,i,l,v,r,u),x=await Oe(_,i,b,u,t),S=x[v.route.id];if(!S){for(let e of b)if(x[e.route.id]){S=x[e.route.id];break}}if(_.signal.aborted){H.get(t)===g&&H.delete(t);return}if(oe.has(t)){if(fn(S)||dn(S)){je(t,Dn(void 0));return}}else{if(fn(S)){if(H.delete(t),ne>y){je(t,Dn(void 0));return}return W.add(t),je(t,Tn(m)),De(_,S,!1,{fetcherSubmission:m,preventScrollReset:p})}if(dn(S)){Me(t,n,S.error);return}}let w=E.navigation.location||E.location,T=qt(e.history,w,g.signal),D=s.activeRoutes,k=E.navigation.state===`idle`?E.matches:U(D,E.navigation.location,c,!1,s.branches);C(k,`Didn't find any matches after fetcher action`);let A=++te;re.set(t,A);let{dsMatches:j,revalidatingFetchers:M}=yt(T,u,a,o,e.history,E,k,m,w,r,!1,z,V,oe,ie,W,D,c,e.patchRoutesOnNavigation!=null,s.branches,[v.route.id,S],h),N=Tn(m,S.data),F=new Map(E.fetchers);F.set(t,N),M.filter(e=>e.key!==t).forEach(e=>{let t=e.key,n=F.get(t),r=Tn(void 0,n?n.data:void 0);F.set(t,r),Re(t),e.controller&&H.set(t,e.controller)}),pe({fetchers:F});let I=()=>M.forEach(e=>Re(e.key));g.signal.addEventListener(`abort`,I);let{loaderResults:L,fetcherResults:R}=await ke(j,M,T,w,u);if(g.signal.aborted)return;g.signal.removeEventListener(`abort`,I),re.delete(t),H.delete(t),M.forEach(e=>H.delete(e.key));let B=E.fetchers.has(t),ee=e=>{if(!B)return e;let n=new Map(e.fetchers);return n.set(t,Dn(S.data)),{...e,fetchers:n}},ae=rn(L);if(ae)return E=ee(E),De(T,ae.result,!1,{preventScrollReset:p});if(ae=rn(R),ae)return W.add(ae.key),E=ee(E),De(T,ae.result,!1,{preventScrollReset:p});let se=new Map(E.fetchers);B&&se.set(t,Dn(S.data));let{loaderData:ce,errors:le}=Zt(E,k,L,void 0,M,R,se);Ve(A,se),E.navigation.state===`loading`&&A>ne?(C(O,`Expected pending action`),P&&P.abort(),me(E.navigation.location,{matches:k,loaderData:ce,errors:le,fetchers:se})):(pe({errors:le,loaderData:Qt(E.loaderData,ce,k,le),fetchers:se}),z=!1)}async function Ee(t,n,i,s,c,l,u,d,f){let p=E.fetchers.get(t);je(t,Tn(f,p?p.data:void 0),{flushSync:u});let m=new AbortController,h=qt(e.history,i,m.signal);if(l){let e=await tt(s,new URL(h.url).pathname,h.signal,t);if(e.type===`aborted`)return;if(e.type===`error`){Me(t,n,e.error,{flushSync:u});return}if(e.matches)s=e.matches;else{Me(t,n,nn(404,{pathname:i}),{flushSync:u});return}}let g=bn(s,i);H.set(t,m);let _=te,v=await Oe(h,i,Rt(a,o,h,i,s,g,r,c),c,t),y=v[g.route.id];if(!y){for(let e of s)if(v[e.route.id]){y=v[e.route.id];break}}if(H.get(t)===m&&H.delete(t),!h.signal.aborted){if(oe.has(t)){je(t,Dn(void 0));return}if(fn(y)){if(ne>_){je(t,Dn(void 0));return}W.add(t),await De(h,y,!1,{preventScrollReset:d});return}if(dn(y)){Me(t,n,y.error);return}je(t,Dn(y.data))}}async function De(r,i,a,{submission:o,fetcherSubmission:s,preventScrollReset:l,replace:u}={}){a||(A?.resolve(),A=null),i.response.headers.has(`X-Remix-Revalidate`)&&(z=!0);let d=i.response.headers.get(`Location`);C(d,`Expected a Location header on the redirect Response`),d=Kt(d,new URL(r.url),c,e.history);let f=D(E.location,d,{_isRedirect:!0});if(n){let e=!1;if(i.response.headers.has(`X-Remix-Reload-Document`))e=!0;else if(Se(d)){let n=j(t,d,!0);e=n.origin!==t.location.origin||be(n.pathname,c)==null}if(e){u?t.location.replace(d):t.location.assign(d);return}}P=null;let p=u===!0||i.response.headers.has(`X-Remix-Replace`)?`REPLACE`:`PUSH`,{formMethod:m,formAction:h,formEncType:g}=E.navigation;!o&&!s&&m&&h&&g&&(o=Sn(E.navigation));let _=o||s;ct.has(i.response.status)&&_&&vn(_.formMethod)?await _e(p,f,{submission:{..._,formAction:d},preventScrollReset:l||M,enableViewTransition:a?F:void 0}):await _e(p,f,{overrideNavigation:Cn(f,[],p,o),fetcherSubmission:s,preventScrollReset:l||M,enableViewTransition:a?F:void 0})}async function Oe(e,t,n,r,i){let a,o={};try{a=await zt(l,e,t,n,i,r,!1)}catch(e){return n.filter(e=>e.shouldLoad).forEach(t=>{o[t.route.id]={type:`error`,error:e}}),o}if(e.signal.aborted)return o;if(!vn(e.method))for(let e of n){if(a[e.route.id]?.type===`error`)break;!a.hasOwnProperty(e.route.id)&&!E.loaderData.hasOwnProperty(e.route.id)&&(!E.errors||!E.errors.hasOwnProperty(e.route.id))&&e.shouldCallHandler()&&(a[e.route.id]={type:`error`,result:Error(`No result returned from dataStrategy for route ${e.route.id}`)})}for(let[t,r]of Object.entries(a))if(un(r)){let i=r.result;o[t]={type:`redirect`,response:Ut(i,e,t,n,c)}}else o[t]=await Ht(r);return o}async function ke(e,t,n,r,i){let a=Oe(n,r,e,i,null),o=Promise.all(t.map(async e=>{if(e.matches&&e.match&&e.request&&e.controller){let t=(await Oe(e.request,e.path,e.matches,i,e.key))[e.match.route.id];return{[e.key]:t}}return Promise.resolve({[e.key]:{type:`error`,error:nn(404,{pathname:e.path})}})}));return{loaderResults:await a,fetcherResults:(await o).reduce((e,t)=>Object.assign(e,t),{})}}function Ae(){z=!0,ie.forEach((e,t)=>{H.has(t)&&V.add(t),Re(t)})}function je(e,t,n={}){let r=new Map(E.fetchers);r.set(e,t),pe({fetchers:r},{flushSync:(n&&n.flushSync)===!0})}function Me(e,t,n,r={}){let i=en(E.matches,t),a=new Map(E.fetchers);Fe(a,e),pe({errors:{[i.route.id]:n},fetchers:a},{flushSync:(r&&r.flushSync)===!0})}function Ne(e){return ae.set(e,(ae.get(e)||0)+1),oe.has(e)&&oe.delete(e),E.fetchers.get(e)||ut}function Pe(e,t){Re(e,t?.reason),je(e,Dn(null))}function Fe(e,t){let n=E.fetchers.get(t);H.has(t)&&!(n&&n.state===`loading`&&re.has(t))&&Re(t),ie.delete(t),re.delete(t),W.delete(t),oe.delete(t),V.delete(t),e.delete(t)}function Ie(e){let t=(ae.get(e)||0)-1;t<=0?(ae.delete(e),oe.add(e)):ae.set(e,t),pe({fetchers:new Map(E.fetchers)})}function Re(e,t){let n=H.get(e);n&&(n.abort(t),H.delete(e))}function ze(e,t){for(let n of e){let e=t.get(n);C(e,`Expected fetcher: ${n}`);let r=Dn(e.data);t.set(n,r)}}function Be(e){let t=[],n=!1;for(let r of W){let i=e.get(r);C(i,`Expected fetcher: ${r}`),i.state===`loading`&&(W.delete(r),t.push(r),n=!0)}return ze(t,e),n}function Ve(e,t){let n=[];for(let[r,i]of re)if(i0}function He(e,t){let n=E.blockers.get(e)||dt;return se.get(e)!==t&&se.set(e,t),n}function Ue(e){E.blockers.delete(e),se.delete(e)}function Ke(e,t){let n=E.blockers.get(e)||dt;C(n.state===`unblocked`&&t.state===`blocked`||n.state===`blocked`&&t.state===`blocked`||n.state===`blocked`&&t.state===`proceeding`||n.state===`blocked`&&t.state===`unblocked`||n.state===`proceeding`&&t.state===`unblocked`,`Invalid blocker state transition: ${n.state} -> ${t.state}`);let r=new Map(E.blockers);r.set(e,t),pe({blockers:r})}function qe({currentLocation:e,nextLocation:t,historyAction:n}){if(se.size===0)return;se.size>1&&w(!1,`A router only supports one blocker at a time`);let r=Array.from(se.entries()),[i,a]=r[r.length-1],o=E.blockers.get(i);if(!(o&&o.state===`proceeding`)&&a({currentLocation:e,nextLocation:t,historyAction:n}))return i}function Je(e){let t=nn(404,{pathname:e}),n=s.activeRoutes,{matches:r,route:i}=tn(n);return{notFoundMatches:r,route:i,error:t}}function Xe(e,t,n){if(m=e,g=t,h=n||null,!_&&E.navigation===lt){_=!0;let e=$e(E.location,E.matches);e!=null&&pe({restoreScrollPosition:e})}return()=>{m=null,g=null,h=null}}function Ze(e,t){return h&&h(e,t.map(e=>ee(e,E.loaderData)))||e.key}function Qe(e,t){if(m&&g){let n=Ze(e,t);m[n]=g()}}function $e(e,t){if(m){let n=Ze(e,t),r=m[n];if(typeof r==`number`)return r}return null}function et(t,n,r){if(e.patchRoutesOnNavigation){let e=s.branches;if(!t)return{active:!0,matches:U(n,r,c,!0,e)||[]};if(Object.keys(t[0].params).length>0)return{active:!0,matches:U(n,r,c,!0,e)}}return{active:!1,matches:null}}async function tt(t,n,r,i){if(!e.patchRoutesOnNavigation)return{type:`success`,matches:t};let l=t;for(;;){let t=o;try{await e.patchRoutesOnNavigation({signal:r,path:n,matches:l,fetcherKey:i,patch:(e,n)=>{r.aborted||Tt(e,n,s,t,a,!1)}})}catch(e){return{type:`error`,error:e,partialMatches:l}}if(r.aborted)return{type:`aborted`};let u=s.branches,d=U(s.activeRoutes,n,c,!1,u),f=null;if(d&&(Object.keys(d[0].params).length===0||(f=U(s.activeRoutes,n,c,!0,u),!(f&&l.lengthe.route.id===t[n].route.id)}function rt(e){o={},s.setHmrRoutes(B(e,a,void 0,o))}function it(e,t,n=!1){Tt(e,t,s,o,a,n),s.hasHMRRoutes||pe({})}return T={get basename(){return c},get future(){return u},get state(){return E},get routes(){return s.stableRoutes},get branches(){return s.branches},get manifest(){return o},get window(){return t},initialize:ue,subscribe:fe,enableScrollRestoration:Xe,navigate:he,fetch:we,revalidate:ge,createHref:t=>e.history.createHref(t),encodeLocation:t=>e.history.encodeLocation(t),getFetcher:Ne,resetFetcher:Pe,deleteFetcher:Ie,dispose:de,getBlocker:He,deleteBlocker:Ue,patchRoutes:it,_internalFetchControllers:H,_internalSetRoutes:rt,_internalSetStateDoNotUseOrYouWillBreakYourApp(e){pe(e)}},e.instrumentations&&(T=Ge(T,e.instrumentations.map(e=>e.router).filter(Boolean))),T}function gt(e){return e!=null&&(`formData`in e&&e.formData!=null||`body`in e&&e.body!==void 0)}function _t(e,t,n,r,i,a){let o,s;if(i){o=[];for(let e of t)if(o.push(e),e.route.id===i){s=e;break}}else o=t,s=t[t.length-1];let c=Oe(r||`.`,De(o),be(e.pathname,n)||e.pathname,a===`path`);if(r??(c.search=e.search,c.hash=e.hash),(r==null||r===``||r===`.`)&&s){let e=yn(c.search);if(s.route.index&&!e)c.search=c.search?c.search.replace(/^\?/,`?index&`):`?index`;else if(!s.route.index&&e){let e=new URLSearchParams(c.search),t=e.getAll(`index`);e.delete(`index`),t.filter(e=>e).forEach(t=>e.append(`index`,t));let n=e.toString();c.search=n?`?${n}`:``}}return n!==`/`&&(c.pathname=xe({basename:n,pathname:c.pathname})),O(c)}function vt(e,t,n){if(!n||!gt(n))return{path:t};if(n.formMethod&&!_n(n.formMethod))return{path:t,error:nn(405,{method:n.formMethod})};let r=()=>({path:t,error:nn(400,{type:`invalid-body`})}),i=(n.formMethod||`get`).toUpperCase(),a=an(t);if(n.body!==void 0){if(n.formEncType===`text/plain`){if(!vn(i))return r();let e=typeof n.body==`string`?n.body:n.body instanceof FormData||n.body instanceof URLSearchParams?Array.from(n.body.entries()).reduce((e,[t,n])=>`${e}${t}=${n}\n`,``):String(n.body);return{path:t,submission:{formMethod:i,formAction:a,formEncType:n.formEncType,formData:void 0,json:void 0,text:e}}}if(n.formEncType===`application/json`){if(!vn(i))return r();try{let e=typeof n.body==`string`?JSON.parse(n.body):n.body;return{path:t,submission:{formMethod:i,formAction:a,formEncType:n.formEncType,formData:void 0,json:e,text:void 0}}}catch{return r()}}}C(typeof FormData==`function`,`FormData is not available in this environment`);let o,s;if(n.formData)o=Jt(n.formData),s=n.formData;else if(n.body instanceof FormData)o=Jt(n.body),s=n.body;else if(n.body instanceof URLSearchParams)o=n.body,s=Yt(o);else if(n.body==null)o=new URLSearchParams,s=new FormData;else try{o=new URLSearchParams(n.body),s=Yt(o)}catch{return r()}let c={formMethod:i,formAction:a,formEncType:n&&n.formEncType||`application/x-www-form-urlencoded`,formData:s,json:void 0,text:void 0};if(vn(c.formMethod))return{path:t,submission:c};let l=k(t);return e&&l.search&&yn(l.search)&&o.append(`index`,``),l.search=`?${o}`,{path:O(l),submission:c}}function yt(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h,g,_,v,y,b,x){let S=b?dn(b[1])?b[1].error:b[1].data:void 0,C=i.createURL(a.location),w=i.createURL(c),T;if(u&&a.errors){let e=Object.keys(a.errors)[0];T=o.findIndex(t=>t.route.id===e)}else if(b&&dn(b[1])){let e=b[0];T=o.findIndex(t=>t.route.id===e)-1}let E=b?b[1].statusCode:void 0,D=E&&E>=400,O={currentUrl:C,currentParams:a.matches[0]?.params||{},nextUrl:w,nextParams:o[0].params,...s,actionResult:S,actionStatus:E},k=Re(o),A=o.map((i,o)=>{let{route:s}=i,f=null;if(T!=null&&o>T)f=!1;else if(s.lazy)f=!0;else if(!bt(s))f=!1;else if(u){let{shouldLoad:e}=xt(s,a.loaderData,a.errors);f=e}else St(a.loaderData,a.matches[o],i)&&(f=!0);if(f!==null)return Lt(n,r,e,c,k,i,l,t,f);let p=!1;typeof x==`boolean`?p=x:D?p=!1:d||C.pathname+C.search===w.pathname+w.search?p=!0:C.search===w.search?Ct(a.matches[o],i)&&(p=!0):p=!0;let m={...O,defaultShouldRevalidate:p};return Lt(n,r,e,c,k,i,l,t,wt(i,m),m,x)}),j=[];return m.forEach((e,s)=>{if(u||!o.some(t=>t.route.id===e.routeId)||p.has(s))return;let c=a.fetchers.get(s),m=c&&c.state!==`idle`&&c.data===void 0,b=U(g,e.path,_??`/`,!1,y);if(!b){if(v&&m)return;j.push({key:s,routeId:e.routeId,path:e.path,matches:null,match:null,request:null,controller:null});return}if(h.has(s))return;let S=bn(b,e.path),C=new AbortController,w=qt(i,e.path,C.signal),T=null;if(f.has(s))f.delete(s),T=Rt(n,r,w,e.path,b,S,l,t);else if(m)d&&(T=Rt(n,r,w,e.path,b,S,l,t));else{let i;i=typeof x==`boolean`?x:!D&&d;let a={...O,defaultShouldRevalidate:i};wt(S,a)&&(T=Rt(n,r,w,e.path,b,S,l,t,a))}T&&j.push({key:s,routeId:e.routeId,path:e.path,matches:T,match:S,request:w,controller:C})}),{dsMatches:A,revalidatingFetchers:j}}function bt(e){return e.loader!=null||e.middleware!=null&&e.middleware.length>0}function xt(e,t,n){if(e.lazy)return{shouldLoad:!0,renderFallback:!0};if(!bt(e))return{shouldLoad:!1,renderFallback:!1};let r=t!=null&&e.id in t,i=n!=null&&n[e.id]!==void 0;if(!r&&i)return{shouldLoad:!1,renderFallback:!1};if(typeof e.loader==`function`&&e.loader.hydrate===!0)return{shouldLoad:!0,renderFallback:!r};let a=!r&&!i;return{shouldLoad:a,renderFallback:a}}function St(e,t,n){let r=!t||n.route.id!==t.route.id,i=!e.hasOwnProperty(n.route.id);return r||i}function Ct(e,t){let n=e.route.path;return e.pathname!==t.pathname||n!=null&&n.endsWith(`*`)&&e.params[`*`]!==t.params[`*`]}function wt(e,t){if(e.route.shouldRevalidate){let n=e.route.shouldRevalidate(t);if(typeof n==`boolean`)return n}return t.defaultShouldRevalidate}function Tt(e,t,n,r,i,a){let o;if(e){let t=r[e];C(t,`No route found to patch children into: routeId = ${e}`),t.children||=[],o=t.children}else o=n.activeRoutes;let s=[],c=[];if(t.forEach(e=>{let t=o.find(t=>Et(e,t));t?c.push({existingRoute:t,newRoute:e}):s.push(e)}),s.length>0){let t=B(s,i,[e||`_`,`patch`,String(o?.length||`0`)],r);o.push(...t)}if(a&&c.length>0)for(let e=0;et.children?.some(t=>Et(e,t)))??!1}var Dt=new WeakMap,Ot=({key:e,route:t,manifest:n,mapRouteProperties:r})=>{let i=n[t.id];if(C(i,`No route found in manifest`),!i.lazy||typeof i.lazy!=`object`)return;let a=i.lazy[e];if(!a)return;let o=Dt.get(i);o||(o={},Dt.set(i,o));let s=o[e];if(s)return s;let c=(async()=>{let t=F(e),n=i[e]!==void 0;if(t)w(!t,`Route property `+e+` is not a supported lazy route property. This property will be ignored.`),o[e]=Promise.resolve();else if(n)w(!1,`Route "${i.id}" has a static property "${e}" defined. The lazy property will be ignored.`);else{let t=await a();t!=null&&(Object.assign(i,{[e]:t}),Object.assign(i,r(i)))}typeof i.lazy==`object`&&(i.lazy[e]=void 0,Object.values(i.lazy).every(e=>e===void 0)&&(i.lazy=void 0))})();return o[e]=c,c},kt=new WeakMap;function At(e,t,n,r,i){let a=n[e.id];if(C(a,`No route found in manifest`),!e.lazy)return{lazyRoutePromise:void 0,lazyHandlerPromise:void 0};if(typeof e.lazy==`function`){let t=kt.get(a);if(t)return{lazyRoutePromise:t,lazyHandlerPromise:t};let n=(async()=>{C(typeof e.lazy==`function`,`No lazy route function found`);let t=await e.lazy(),n={};for(let e in t){let r=t[e];if(r===void 0)continue;let i=L(e),o=a[e]!==void 0;i?w(!i,`Route property `+e+` is not a supported property to be returned from a lazy route function. This property will be ignored.`):o?w(!o,`Route "${a.id}" has a static property "${e}" defined but its lazy function is also returning a value for this property. The lazy route property "${e}" will be ignored.`):n[e]=r}Object.assign(a,n),Object.assign(a,{...r(a),lazy:void 0})})();return kt.set(a,n),n.catch(()=>{}),{lazyRoutePromise:n,lazyHandlerPromise:n}}let o=Object.keys(e.lazy),s=[],c;for(let a of o){if(i&&i.includes(a))continue;let o=Ot({key:a,route:e,manifest:n,mapRouteProperties:r});o&&(s.push(o),a===t&&(c=o))}let l=s.length>0?Promise.all(s).then(()=>{}):void 0;return l?.catch(()=>{}),c?.catch(()=>{}),{lazyRoutePromise:l,lazyHandlerPromise:c}}async function jt(e){let t=e.matches.filter(e=>e.shouldLoad),n={};return(await Promise.all(t.map(e=>e.resolve()))).forEach((e,r)=>{n[t[r].route.id]=e}),n}async function Mt(e){return e.matches.some(e=>e.route.middleware)?Nt(e,()=>jt(e)):jt(e)}function Nt(e,t){return Pt(e,t,e=>{if(gn(e))throw e;return e},cn,n);async function n(t,n,r){if(r)return Object.assign(r.value,{[n]:{type:`error`,result:t}});{let{matches:r}=e,i=Math.min(Math.max(r.findIndex(e=>e.route.id===n),0),Math.max(r.findIndex(e=>e.shouldCallHandler()),0)),a=r[i].route.id;for(let e of r.slice(0,i+1))try{await e._lazyPromises?.route}catch{a=e.route.id;break}return{[en(r,a).route.id]:{type:`error`,result:t}}}}}async function Pt(e,t,n,r,i){let{matches:a,...o}=e;return await Ft(o,a.flatMap(e=>e.route.middleware?e.route.middleware.map(t=>[e.route.id,t]):[]),t,n,r,i)}async function Ft(e,t,n,r,i,a,o=0){let{request:s}=e;if(s.signal.aborted)throw s.signal.reason??Error(`Request aborted: ${s.method} ${s.url}`);let c=t[o];if(!c)return await n();let[l,u]=c,d,f=async()=>{if(d)throw Error("You may only call `next()` once per middleware");try{return d={value:await Ft(e,t,n,r,i,a,o+1)},d.value}catch(e){return d={value:await a(e,l,d)},d.value}};try{let t=await u(e,f),n=t==null?void 0:r(t);return i(n)?n:d?n??d.value:(d={value:await f()},d.value)}catch(e){return await a(e,l,d)}}function It(e,t,n,r,i){let a=Ot({key:`middleware`,route:r.route,manifest:t,mapRouteProperties:e}),o=At(r.route,vn(n.method)?`action`:`loader`,t,e,i);return{middleware:a,route:o.lazyRoutePromise,handler:o.lazyHandlerPromise}}function Lt(e,t,n,r,i,a,o,s,c,l=null,u){let d=!1,f=It(e,t,n,a,o);return{...a,_lazyPromises:f,shouldLoad:c,shouldRevalidateArgs:l,shouldCallHandler(e){return d=!0,l?typeof u==`boolean`?wt(a,{...l,defaultShouldRevalidate:u}):typeof e==`boolean`?wt(a,{...l,defaultShouldRevalidate:e}):wt(a,l):c},resolve(e){let{lazy:t,loader:o,middleware:l}=a.route,u=d||c||e&&!vn(n.method)&&(t||o),p=l&&l.length>0&&!o&&!t;return u&&(vn(n.method)||!p)?Bt({request:n,path:r,pattern:i,match:a,lazyHandlerPromise:f?.handler,lazyRoutePromise:f?.route,handlerOverride:e,scopedContext:s}):Promise.resolve({type:`data`,result:void 0})}}}function Rt(e,t,n,r,i,a,o,s,c=null){return i.map(l=>l.route.id===a.route.id?Lt(e,t,n,r,Re(i),l,o,s,!0,c):{...l,shouldLoad:!1,shouldRevalidateArgs:c,shouldCallHandler:()=>!1,_lazyPromises:It(e,t,n,l,o),resolve:()=>Promise.resolve({type:`data`,result:void 0})})}async function zt(e,t,n,r,i,a,o){r.some(e=>e._lazyPromises?.middleware)&&await Promise.all(r.map(e=>e._lazyPromises?.middleware));let s={request:t,url:ze(t,n),pattern:Re(r),params:r[0].params,context:a,matches:r},c=o?()=>{throw Error("You cannot call `runClientMiddleware()` from a static handler `dataStrategy`. Middleware is run outside of `dataStrategy` during SSR in order to bubble up the Response. You can enable middleware via the `respond` API in `query`/`queryRoute`")}:e=>{let t=s;return Nt(t,()=>e({...t,fetcherKey:i,runClientMiddleware:()=>{throw Error("Cannot call `runClientMiddleware()` from within an `runClientMiddleware` handler")}}))},l=await e({...s,fetcherKey:i,runClientMiddleware:c});try{await Promise.all(r.flatMap(e=>[e._lazyPromises?.handler,e._lazyPromises?.route]))}catch{}return l}async function Bt({request:e,path:t,pattern:n,match:r,lazyHandlerPromise:i,lazyRoutePromise:a,handlerOverride:o,scopedContext:s}){let c,l,u=vn(e.method),d=u?`action`:`loader`,f=i=>{let a,c=new Promise((e,t)=>a=t);l=()=>a(),e.signal.addEventListener(`abort`,l);let u=a=>typeof i==`function`?i({request:e,url:ze(e,t),pattern:n,params:r.params,context:s},...a===void 0?[]:[a]):Promise.reject(Error(`You cannot call the handler for a route which defines a boolean "${d}" [routeId: ${r.route.id}]`)),f=(async()=>{try{return{type:`data`,result:await(o?o(e=>u(e)):u())}}catch(e){return{type:`error`,result:e}}})();return Promise.race([f,c])};try{let t=u?r.route.action:r.route.loader;if(i||a){if(t){let e,[n]=await Promise.all([f(t).catch(t=>{e=t}),i,a]);if(e!==void 0)throw e;c=n}else{await i;let t=u?r.route.action:r.route.loader;if(t)[c]=await Promise.all([f(t),a]);else if(d===`action`){let t=new URL(e.url),n=t.pathname+t.search;throw nn(405,{method:e.method,pathname:n,routeId:r.route.id})}else return{type:`data`,result:void 0}}}else if(t)c=await f(t);else{let t=new URL(e.url);throw nn(404,{pathname:t.pathname+t.search})}}catch(e){return{type:`error`,result:e}}finally{l&&e.signal.removeEventListener(`abort`,l)}return c}async function Vt(e){let t=e.headers.get(`Content-Type`);return t&&/\bapplication\/json\b/.test(t)?e.body==null?null:e.json():e.text()}async function Ht(e){let{result:t,type:n}=e;if(mn(t)){let e;try{e=await Vt(t)}catch(e){return{type:`error`,error:e}}return n===`error`?{type:`error`,error:new Ie(t.status,t.statusText,e),statusCode:t.status,headers:t.headers}:{type:`data`,data:e,statusCode:t.status,headers:t.headers}}return n===`error`?pn(t)?t.data instanceof Error?{type:`error`,error:t.data,statusCode:t.init?.status,headers:t.init?.headers?new Headers(t.init.headers):void 0}:{type:`error`,error:sn(t),statusCode:Le(t)?t.status:void 0,headers:t.init?.headers?new Headers(t.init.headers):void 0}:{type:`error`,error:t,statusCode:Le(t)?t.status:void 0}:pn(t)?{type:`data`,data:t.data,statusCode:t.init?.status,headers:t.init?.headers?new Headers(t.init.headers):void 0}:{type:`data`,data:t}}function Ut(e,t,n,r,i){let a=e.headers.get(`Location`);if(C(a,`Redirects returned/thrown from loaders/actions must have a Location header`),!Se(a)){let o=r.slice(0,r.findIndex(e=>e.route.id===n)+1);a=_t(new URL(t.url),o,i,a),e.headers.set(`Location`,a)}return e}var Wt=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function Gt(e){try{return Wt.includes(new URL(e).protocol)}catch{return!1}}function Kt(e,t,n,r){if(Se(e)){let r=e,i=v.test(r)?new URL(y(r,t.protocol)):new URL(r);if(Gt(i.toString()))throw Error(`Invalid redirect location`);let a=be(i.pathname,n)!=null;if(i.origin===t.origin&&a)return ke(i.pathname)+i.search+i.hash}try{if(Gt(r.createURL(e).toString()))throw Error(`Invalid redirect location`)}catch{}return e}function qt(e,t,n,r){let i=e.createURL(an(t)).toString(),a={signal:n};if(r&&vn(r.formMethod)){let{formMethod:e,formEncType:t}=r;a.method=e.toUpperCase(),t===`application/json`?(a.headers=new Headers({"Content-Type":t}),a.body=JSON.stringify(r.json)):a.body=t===`text/plain`?r.text:t===`application/x-www-form-urlencoded`&&r.formData?Jt(r.formData):r.formData}return new Request(i,a)}function Jt(e){let t=new URLSearchParams;for(let[n,r]of e.entries())t.append(n,typeof r==`string`?r:r.name);return t}function Yt(e){let t=new FormData;for(let[n,r]of e.entries())t.append(n,r);return t}function Xt(e,t,n,r=!1,i=!1){let a={},o=null,s,c=!1,l={},u=n&&dn(n[1])?n[1].error:void 0;return e.forEach(n=>{if(!(n.route.id in t))return;let d=n.route.id,f=t[d];if(C(!fn(f),`Cannot handle redirect results in processLoaderData`),dn(f)){let t=f.error;if(u!==void 0&&(t=u,u=void 0),o||={},i)o[d]=t;else{let n=en(e,d);o[n.route.id]??(o[n.route.id]=t)}r||(a[d]=pt),c||(c=!0,s=Le(f.error)?f.error.status:500),f.headers&&(l[d]=f.headers)}else a[d]=f.data,f.statusCode&&f.statusCode!==200&&!c&&(s=f.statusCode),f.headers&&(l[d]=f.headers)}),u!==void 0&&n&&(o={[n[0]]:u},n[2]&&(a[n[2]]=void 0)),{loaderData:a,errors:o,statusCode:s||200,loaderHeaders:l}}function Zt(e,t,n,r,i,a,o){let{loaderData:s,errors:c}=Xt(t,n,r);return i.filter(e=>!e.matches||e.matches.some(e=>e.shouldLoad)).forEach(t=>{let{key:n,match:r,controller:i}=t;if(i&&i.signal.aborted)return;let s=a[n];if(C(s,`Did not find corresponding fetcher result`),dn(s)){let t=en(e.matches,r?.route.id);c&&c[t.route.id]||(c={...c,[t.route.id]:s.error}),o.delete(n)}else if(fn(s))C(!1,`Unhandled fetcher revalidation redirect`);else{let e=Dn(s.data);o.set(n,e)}}),{loaderData:s,errors:c}}function Qt(e,t,n,r){let i=Object.entries(t).filter(([,e])=>e!==pt).reduce((e,[t,n])=>(e[t]=n,e),{});for(let a of n){let n=a.route.id;if(!t.hasOwnProperty(n)&&e.hasOwnProperty(n)&&a.route.loader&&(i[n]=e[n]),r&&r.hasOwnProperty(n))break}return i}function $t(e){return e?dn(e[1])?{actionData:{}}:{actionData:{[e[0]]:e[1].data}}:{}}function en(e,t){return(t?e.slice(0,e.findIndex(e=>e.route.id===t)+1):[...e]).reverse().find(e=>e.route.ErrorBoundary!=null||e.route.errorElement!=null)||e[0]}function tn(e){let t=e.length===1?e[0]:e.find(e=>e.index||!e.path||e.path===`/`)||{id:`__shim-error-route__`};return{matches:[{params:{},pathname:``,pathnameBase:``,route:t}],route:t}}function nn(e,{pathname:t,routeId:n,method:r,type:i,message:a}={}){let o=`Unknown Server Error`,s=`Unknown @remix-run/router error`;return e===400?(o=`Bad Request`,r&&t&&n?s=`You made a ${r} request to "${t}" but did not provide a \`loader\` for route "${n}", so there is no way to handle the request.`:i===`invalid-body`&&(s=`Unable to encode submission body`)):e===403?(o=`Forbidden`,s=`Route "${n}" does not match URL "${t}"`):e===404?(o=`Not Found`,s=`No route matches URL "${t}"`):e===405&&(o=`Method Not Allowed`,r&&t&&n?s=`You made a ${r.toUpperCase()} request to "${t}" but did not provide an \`action\` for route "${n}", so there is no way to handle the request.`:r&&(s=`Invalid request method "${r.toUpperCase()}"`)),new Ie(e||500,o,Error(s),!0)}function rn(e){let t=Object.entries(e);for(let e=t.length-1;e>=0;e--){let[n,r]=t[e];if(fn(r))return{key:n,result:r}}}function an(e){return O({...typeof e==`string`?k(e):e,hash:``})}function on(e,t){return e.pathname!==t.pathname||e.search!==t.search?!1:e.hash===``?t.hash!==``:e.hash===t.hash||t.hash!==``}function sn(e){return new Ie(e.init?.status??500,e.init?.statusText??`Internal Server Error`,e.data)}function cn(e){return typeof e==`object`&&!!e&&Object.entries(e).every(([e,t])=>typeof e==`string`&&ln(t))}function ln(e){return typeof e==`object`&&!!e&&`type`in e&&`result`in e&&(e.type===`data`||e.type===`error`)}function un(e){return mn(e.result)&&st.has(e.result.status)}function dn(e){return e.type===`error`}function fn(e){return(e&&e.type)===`redirect`}function pn(e){return typeof e==`object`&&!!e&&`type`in e&&`data`in e&&`init`in e&&e.type===`DataWithResponseInit`}function mn(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.headers==`object`&&e.body!==void 0}function hn(e){return st.has(e)}function gn(e){return mn(e)&&hn(e.status)&&e.headers.has(`Location`)}function _n(e){return ot.has(e.toUpperCase())}function vn(e){return it.has(e.toUpperCase())}function yn(e){return new URLSearchParams(e).getAll(`index`).some(e=>e===``)}function bn(e,t){let n=typeof t==`string`?k(t).search:t.search;if(e[e.length-1].route.index&&yn(n||``))return e[e.length-1];let r=Ee(e);return r[r.length-1]}function xn(e,t,n){return{url:ze(e.createURL(t),t),pattern:n?Re(n):``,params:n?.[0]?.params?{...n[0].params}:{}}}function Sn(e){let{formMethod:t,formAction:n,formEncType:r,text:i,formData:a,json:o}=e;if(!(!t||!n||!r)){if(i!=null)return{formMethod:t,formAction:n,formEncType:r,formData:void 0,json:void 0,text:i};if(a!=null)return{formMethod:t,formAction:n,formEncType:r,formData:a,json:void 0,text:void 0};if(o!==void 0)return{formMethod:t,formAction:n,formEncType:r,formData:void 0,json:o,text:void 0}}}function Cn(e,t,n,r){return r?{state:`loading`,location:e,matches:t,historyAction:n,formMethod:r.formMethod,formAction:r.formAction,formEncType:r.formEncType,formData:r.formData,json:r.json,text:r.text}:{state:`loading`,location:e,matches:t,historyAction:n,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0}}function wn(e,t,n,r){return{state:`submitting`,location:e,matches:t,historyAction:n,formMethod:r.formMethod,formAction:r.formAction,formEncType:r.formEncType,formData:r.formData,json:r.json,text:r.text}}function Tn(e,t){return e?{state:`loading`,formMethod:e.formMethod,formAction:e.formAction,formEncType:e.formEncType,formData:e.formData,json:e.json,text:e.text,data:t}:{state:`loading`,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0,data:t}}function En(e,t){return{state:`submitting`,formMethod:e.formMethod,formAction:e.formAction,formEncType:e.formEncType,formData:e.formData,json:e.json,text:e.text,data:t?t.data:void 0}}function Dn(e){return{state:`idle`,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0,data:e}}function On(e,t){try{let n=e.sessionStorage.getItem(ft);if(n){let e=JSON.parse(n);for(let[n,r]of Object.entries(e||{}))r&&Array.isArray(r)&&t.set(n,new Set(r||[]))}}catch{}}function kn(e,t){if(t.size>0){let n={};for(let[e,r]of t)n[e]=[...r];try{e.sessionStorage.setItem(ft,JSON.stringify(n))}catch(e){w(!1,`Failed to save applied view transitions in sessionStorage (${e}).`)}}}function An(){let e,t,n=new Promise((r,i)=>{e=async e=>{r(e);try{await n}catch{}},t=async e=>{i(e);try{await n}catch{}}});return{promise:n,resolve:e,reject:t}}var jn=M.createContext(null);jn.displayName=`DataRouter`;var Mn=M.createContext(null);Mn.displayName=`DataRouterState`;var Nn=M.createContext(!1);function Pn(){return M.useContext(Nn)}var Fn=M.createContext({isTransitioning:!1});Fn.displayName=`ViewTransition`;var In=M.createContext(new Map);In.displayName=`Fetchers`;var Ln=M.createContext(null);Ln.displayName=`Await`;var Rn=M.createContext(null);Rn.displayName=`Navigation`;var zn=M.createContext(null);zn.displayName=`Location`;var Bn=M.createContext({outlet:null,matches:[],isDataRoute:!1});Bn.displayName=`Route`;var Vn=M.createContext(null);Vn.displayName=`RouteError`;var Hn=`REACT_ROUTER_ERROR`,Un=`REDIRECT`,Wn=`ROUTE_ERROR_RESPONSE`;function Gn(e){if(e.startsWith(`${Hn}:${Un}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function Kn(e){if(e.startsWith(`${Hn}:${Wn}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Ie(t.status,t.statusText,t.data)}catch{}}function qn(e,{relative:t}={}){C(Jn(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=M.useContext(Rn),{hash:i,pathname:a,search:o}=nr(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Ae([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Jn(){return M.useContext(zn)!=null}function Yn(){return C(Jn(),`useLocation() may be used only in the context of a component.`),M.useContext(zn).location}var Xn=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Zn(){let{isDataRoute:e}=M.useContext(Bn);return e?xr():Qn()}function Qn(){C(Jn(),`useNavigate() may be used only in the context of a component.`);let e=M.useContext(jn),{basename:t,navigator:n}=M.useContext(Rn),{matches:r}=M.useContext(Bn),{pathname:i}=Yn(),a=JSON.stringify(De(r)),o=M.useRef(!1);return M.useLayoutEffect(()=>{o.current=!0}),M.useCallback((r,s={})=>{if(w(o.current,Xn),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=Oe(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Ae([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var $n=M.createContext(null);function er(e){let t=M.useContext(Bn).outlet;return M.useMemo(()=>t&&M.createElement($n.Provider,{value:e},t),[t,e])}function tr(){let{matches:e}=M.useContext(Bn);return e[e.length-1]?.params??{}}function nr(e,{relative:t}={}){let{matches:n}=M.useContext(Bn),{pathname:r}=Yn(),i=JSON.stringify(De(n));return M.useMemo(()=>Oe(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function rr(e,t){return ir(e,t)}function ir(e,t,n){C(Jn(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=M.useContext(Rn),{matches:i}=M.useContext(Bn),a=i[i.length-1],o=a?a.params:{};a&&a.pathname;let s=a?a.pathnameBase:`/`;a&&a.route;let c=Yn(),l;if(t){let e=typeof t==`string`?k(t):t;C(s===`/`||e.pathname?.startsWith(s),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${s}" but pathname "${e.pathname}" was given in the \`location\` prop.`),l=e}else l=c;let u=l.pathname||`/`,d=u;if(s!==`/`){let e=s.replace(/^\//,``).split(`/`);d=`/`+u.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let f=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):H(e,{pathname:d}),p=dr(f&&f.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Ae([s,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?s:Ae([s,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&p?M.createElement(zn.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...l},navigationType:`POP`}},p):p}function ar(){let e=vr(),t=Le(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null;return M.createElement(M.Fragment,null,M.createElement(`h2`,null,`Unexpected Application Error!`),M.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?M.createElement(`pre`,{style:{padding:`0.5rem`,backgroundColor:`rgba(200,200,200, 0.5)`}},n):null,null)}var or=M.createElement(ar,null),sr=class extends M.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static contextType=Nn;static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=Kn(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:M.createElement(Bn.Provider,{value:this.props.routeContext},M.createElement(Vn.Provider,{value:e,children:this.props.component}));return this.context?M.createElement(lr,{error:e},t):t}},cr=new WeakMap;function lr({children:e,error:t}){let{basename:n}=M.useContext(Rn);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Gn(t.digest);if(e){let r=cr.get(t);if(r)throw r;let i=Ve(e.location,n),a=i.absoluteURL||i.to;if(Gt(a))throw Error(`Invalid redirect location`);if(Be&&!cr.get(t)){if(i.isExternal||e.reloadDocument)window.location.href=a;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw cr.set(t,n),n}}return M.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${a}`})}}return e}function ur({routeContext:e,match:t,children:n}){let r=M.useContext(jn);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),M.createElement(Bn.Provider,{value:e},n)}function dr(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);C(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Re(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||or,o&&(s<0&&c===0?(Cr(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?M.createElement(n.route.Component,null):n.route.element?n.route.element:e,M.createElement(ur,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?M.createElement(sr,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function fr(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function pr(e){let t=M.useContext(jn);return C(t,fr(e)),t}function mr(e){let t=M.useContext(Mn);return C(t,fr(e)),t}function hr(e){let t=M.useContext(Bn);return C(t,fr(e)),t}function gr(e){let t=hr(e),n=t.matches[t.matches.length-1];return C(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function _r(){return gr(`useRouteId`)}function vr(){let e=M.useContext(Vn),t=mr(`useRouteError`),n=gr(`useRouteError`);return e===void 0?t.errors?.[n]:e}var yr=0;function br(e){let{router:t,basename:n}=pr(`useBlocker`),r=mr(`useBlocker`),[i,a]=M.useState(``),o=M.useCallback(t=>{if(typeof e!=`function`)return!!e;if(n===`/`)return e(t);let{currentLocation:r,nextLocation:i,historyAction:a}=t;return e({currentLocation:{...r,pathname:be(r.pathname,n)||r.pathname},nextLocation:{...i,pathname:be(i.pathname,n)||i.pathname},historyAction:a})},[n,e]);return M.useEffect(()=>{let e=String(++yr);return a(e),()=>t.deleteBlocker(e)},[t]),M.useEffect(()=>{i!==``&&t.getBlocker(i,o)},[t,i,o]),i&&r.blockers.has(i)?r.blockers.get(i):dt}function xr(){let{router:e}=pr(`useNavigate`),t=gr(`useNavigate`),n=M.useRef(!1);return M.useLayoutEffect(()=>{n.current=!0}),M.useCallback(async(r,i={})=>{w(n.current,Xn),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Sr={};function Cr(e,t,n){!t&&!Sr[e]&&(Sr[e]=!0,w(!1,n))}var wr={};function Tr(e,t){!e&&!wr[t]&&(wr[t]=!0,console.warn(t))}var Er=[`HydrateFallback`,`hydrateFallbackElement`],Dr=class{status=`pending`;promise;resolve;reject;constructor(){this.promise=new Promise((e,t)=>{this.resolve=t=>{this.status===`pending`&&(this.status=`resolved`,e(t))},this.reject=e=>{this.status===`pending`&&(this.status=`rejected`,t(e))}})}};function Or({router:e,flushSync:t,onError:n,useTransitions:r}){r=Pn()||r;let[i,a]=M.useState(e.state),[o,s]=(0,M.useOptimistic)(i),[c,l]=M.useState(),[u,d]=M.useState({isTransitioning:!1}),[f,p]=M.useState(),[m,h]=M.useState(),[g,_]=M.useState(),v=M.useRef(new Map),y=M.useCallback((i,{deletedFetchers:o,newErrors:c,flushSync:u,viewTransitionOpts:g})=>{c&&n&&Object.values(c).forEach(e=>n(e,{location:i.location,params:i.matches[0]?.params??{},pattern:Re(i.matches)})),i.fetchers.forEach((e,t)=>{e.data!==void 0&&v.current.set(t,e.data)}),o.forEach(e=>v.current.delete(e)),Tr(u===!1||t!=null,'You provided the `flushSync` option to a router update, but you are not using the `` from `react-router/dom` so `ReactDOM.flushSync()` is unavailable. Please update your app to `import { RouterProvider } from "react-router/dom"` and ensure you have `react-dom` installed as a dependency to use the `flushSync` option.');let y=e.window!=null&&e.window.document!=null&&typeof e.window.document.startViewTransition==`function`;if(Tr(g==null||y,"You provided the `viewTransition` option to a router update, but you do not appear to be running in a DOM environment as `window.startViewTransition` is not available."),!g||!y){t&&u?t(()=>a(i)):r===!1?a(i):M.startTransition(()=>{r===!0&&s(e=>kr(e,i)),a(i)});return}if(t&&u){t(()=>{m&&(f?.resolve(),m.skipTransition()),d({isTransitioning:!0,flushSync:!0,currentLocation:g.currentLocation,nextLocation:g.nextLocation})});let n=e.window.document.startViewTransition(()=>{t(()=>a(i))});n.finished.finally(()=>{t(()=>{p(void 0),h(void 0),l(void 0),d({isTransitioning:!1})})}),t(()=>h(n));return}m?(f?.resolve(),m.skipTransition(),_({state:i,currentLocation:g.currentLocation,nextLocation:g.nextLocation})):(l(i),d({isTransitioning:!0,flushSync:!1,currentLocation:g.currentLocation,nextLocation:g.nextLocation}))},[e.window,t,m,f,r,s,n]);M.useLayoutEffect(()=>e.subscribe(y),[e,y]),M.useEffect(()=>{u.isTransitioning&&!u.flushSync&&p(new Dr)},[u]),M.useEffect(()=>{if(f&&c&&e.window){let t=c,n=f.promise,i=e.window.document.startViewTransition(async()=>{r===!1?a(t):M.startTransition(()=>{r===!0&&s(e=>kr(e,t)),a(t)}),await n});i.finished.finally(()=>{p(void 0),h(void 0),l(void 0),d({isTransitioning:!1})}),h(i)}},[c,f,e.window,r,s]),M.useEffect(()=>{f&&c&&o.location.key===c.location.key&&f.resolve()},[f,m,o.location,c]),M.useEffect(()=>{!u.isTransitioning&&g&&(l(g.state),d({isTransitioning:!0,flushSync:!1,currentLocation:g.currentLocation,nextLocation:g.nextLocation}),_(void 0))},[u.isTransitioning,g]);let b=M.useMemo(()=>({createHref:e.createHref,encodeLocation:e.encodeLocation,go:t=>e.navigate(t),push:(t,n,r)=>e.navigate(t,{state:n,preventScrollReset:r?.preventScrollReset}),replace:(t,n,r)=>e.navigate(t,{replace:!0,state:n,preventScrollReset:r?.preventScrollReset})}),[e]),x=e.basename||`/`,S=M.useMemo(()=>({router:e,navigator:b,static:!1,basename:x,onError:n}),[e,b,x,n]);return M.createElement(M.Fragment,null,M.createElement(jn.Provider,{value:S},M.createElement(Mn.Provider,{value:o},M.createElement(In.Provider,{value:v.current},M.createElement(Fn.Provider,{value:u},M.createElement(Pr,{basename:x,location:o.location,navigationType:o.historyAction,navigator:b,useTransitions:r},M.createElement(Ar,{routes:e.routes,manifest:e.manifest,future:e.future,state:o,isStatic:!1,onError:n})))))),null)}function kr(e,t){return{...e,navigation:t.navigation.state===`idle`?e.navigation:t.navigation,revalidation:t.revalidation===`idle`?e.revalidation:t.revalidation,actionData:t.navigation.state===`submitting`?e.actionData:t.actionData,fetchers:t.fetchers}}var Ar=M.memo(jr);function jr({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return ir(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Mr(e){return er(e.context)}function Nr(e){C(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Pr({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){C(!Jn(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=M.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=k(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=M.useMemo(()=>{let e=be(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return w(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:M.createElement(Rn.Provider,{value:c},M.createElement(zn.Provider,{children:t,value:h}))}function Fr({children:e,location:t}){return rr(Ir(e),t)}M.Component;function Ir(e,t=[]){let n=[];return M.Children.forEach(e,(e,r)=>{if(!M.isValidElement(e))return;let i=[...t,r];if(e.type===M.Fragment){n.push.apply(n,Ir(e.props.children,i));return}C(e.type===Nr,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `);let a=e.props;C(!a.index||!a.children,`An index route cannot have child routes.`);let o={id:a.id||i.join(`-`),caseSensitive:a.caseSensitive,element:a.element,Component:a.Component,index:a.index,path:a.path,middleware:a.middleware,loader:a.loader,action:a.action,hydrateFallbackElement:a.hydrateFallbackElement,HydrateFallback:a.HydrateFallback,errorElement:a.errorElement,ErrorBoundary:a.ErrorBoundary,shouldRevalidate:a.shouldRevalidate,handle:a.handle,lazy:a.lazy};a.children&&(o.children=Ir(a.children,i)),n.push(o)}),n}var Lr=`application/x-www-form-urlencoded`;function Rr(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function zr(e){return Rr(e)&&e.tagName.toLowerCase()===`button`}function Br(e){return Rr(e)&&e.tagName.toLowerCase()===`form`}function Vr(e){return Rr(e)&&e.tagName.toLowerCase()===`input`}function Hr(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Ur(e,t){return e.button===0&&(!t||t===`_self`)&&!Hr(e)}function Wr(e=``){return new URLSearchParams(typeof e==`string`||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let r=e[n];return t.concat(Array.isArray(r)?r.map(e=>[n,e]):[[n,r]])},[]))}function Gr(e,t){let n=Wr(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var Kr=null;function qr(){if(Kr===null)try{new FormData(document.createElement(`form`),0),Kr=!1}catch{Kr=!0}return Kr}var Jr=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Yr(e){return e!=null&&!Jr.has(e)?(w(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Lr}"`),null):e}function Xr(e,t){let n,r,i,a,o;if(Br(e)){let o=e.getAttribute(`action`);r=o?be(o,t):null,n=e.getAttribute(`method`)||`get`,i=Yr(e.getAttribute(`enctype`))||Lr,a=new FormData(e)}else if(zr(e)||Vr(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a - - ) : null} + + { - if (!submitting) setConfirmDisable(false); + if (!submitting) setLifecycleAction(null); }} actions={ <> @@ -422,31 +498,101 @@ export function ProjectSettingsPage() { variant="quiet" disabled={submitting} onClick={() => { - setConfirmDisable(false); + setLifecycleAction(null); }} > Cancel + {lifecycleAction === null ? null : ( + + )} + + } + > + {lifecycleError === null ? null : ( + + {lifecycleError} + + )} +

+ {lifecycleAction === "disable" ? "Disable" : "Enable"}{" "} + {project.display_name}? + {lifecycleAction === "disable" + ? " New authentication and configuration actions will be blocked until it is enabled again." + : " Runtime authentication and configuration actions will become available again."} +

+
+ { + if (!submitting) { + setConfirmDelete(false); + setDeleteConfirmation(""); + } + }} + actions={ + <> + } > - {disableError === null ? null : ( + {deleteError === null ? null : ( - {disableError} + {deleteError} )} -

- Disable {project.display_name}? New authentication and configuration - actions will be blocked. -

+ + Access stops immediately. This action cannot be undone. Provider key cleanup may continue + asynchronously before all live Project data is physically removed. + + void deleteProject(event)} + > + + { + setDeleteConfirmation(event.currentTarget.value); + }} + data-owl-initial-focus + /> + +
); diff --git a/crates/owlauth-server/web/src/control/pages/ProjectsPage.tsx b/crates/owlauth-server/web/src/control/pages/ProjectsPage.tsx index af3a21b..7d988ae 100644 --- a/crates/owlauth-server/web/src/control/pages/ProjectsPage.tsx +++ b/crates/owlauth-server/web/src/control/pages/ProjectsPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import type { SyntheticEvent } from "react"; import { Link, useNavigate } from "react-router"; @@ -6,17 +6,19 @@ import { ArrowRightIcon, PlusIcon } from "../../shared/icons/Icons"; import { EmptyState, LoadingState, PageHeader } from "../../shared/layout/Layout"; import { Button } from "../../shared/primitives/Button"; import { InlineAlert, StatusBadge } from "../../shared/primitives/Feedback"; -import { Field, Input } from "../../shared/primitives/Field"; +import { Checkbox, Field, Input } from "../../shared/primitives/Field"; import { Dialog } from "../../shared/primitives/Overlay"; import { useControl } from "../app/ControlContext"; import { UnsavedChangesGuard } from "../app/UnsavedChangesGuard"; -import { ControlRequestError, IdempotencyAttempt, requireData } from "../client"; +import { ControlRequestError, IdempotencyAttempt, type Project, requireData } from "../client"; import styles from "./pages.module.css"; export function ProjectsPage() { const { session, projects, loadingProjects, upsertProject, setMessage, handleError } = useControl(); const [creating, setCreating] = useState(false); + const [showInactiveProjects, setShowInactiveProjects] = useState(false); + const [projectSearch, setProjectSearch] = useState(""); const [createName, setCreateName] = useState(""); const [createError, setCreateError] = useState(null); const [createdProjectId, setCreatedProjectId] = useState(null); @@ -87,6 +89,20 @@ export function ProjectsPage() { } const createDirty = creating && createName !== ""; + const normalizedProjectSearch = projectSearch.trim().toLowerCase(); + const visibleProjects = useMemo( + () => + projects.filter((project) => { + if (!showInactiveProjects && project.status !== "active") return false; + if (normalizedProjectSearch === "") return true; + return [project.display_name, project.public_id].some((value) => + value.toLowerCase().includes(normalizedProjectSearch), + ); + }), + [normalizedProjectSearch, projects, showInactiveProjects], + ); + const hasInactiveProjects = projects.some((project) => project.status !== "active"); + const hasProjectSearch = normalizedProjectSearch !== ""; return (
@@ -108,32 +124,86 @@ export function ProjectsPage() { } /> {loadingProjects ? Loading projects : null} - {projects.length === 0 && !loadingProjects ? ( + {!loadingProjects && projects.length > 0 ? ( +
+
+ { + setProjectSearch(event.currentTarget.value); + }} + /> +
+ + {visibleProjects.length} {visibleProjects.length === 1 ? "Project" : "Projects"} + + {hasInactiveProjects ? ( +
+ { + setShowInactiveProjects(event.currentTarget.checked); + }} + > + Show inactive projects + +
+ ) : null} +
+ ) : null} + {visibleProjects.length === 0 && !loadingProjects ? ( ) : (
    - {projects.map((project) => ( + {visibleProjects.map((project) => (
  • - - - {project.display_name} - {project.public_id} - - - - + {project.status === "deleting" ? ( +
    + + + + + + +
    + ) : ( + + + + + + + - -
    - + + )}
  • ))}
@@ -182,6 +252,15 @@ export function ProjectsPage() { ); } +function ProjectDirectoryBody({ project }: { readonly project: Project }) { + return ( + + {project.display_name} + {project.public_id} + + ); +} + function text(fields: FormData, name: string): string { const value = fields.get(name); return typeof value === "string" ? value : ""; diff --git a/crates/owlauth-server/web/src/control/pages/pages.module.css b/crates/owlauth-server/web/src/control/pages/pages.module.css index b958355..6a73122 100644 --- a/crates/owlauth-server/web/src/control/pages/pages.module.css +++ b/crates/owlauth-server/web/src/control/pages/pages.module.css @@ -113,6 +113,63 @@ font-size: 0.8125rem; } +.directoryControls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; + padding: 0.75rem; + background: var(--owl-surface-raised); + border: 1px solid var(--owl-border); + border-radius: var(--owl-radius-surface); +} + +.directorySearch { + min-width: min(18rem, 100%); + max-width: 34rem; + flex: 1 1 24rem; +} + +.directorySearch input { + min-height: 2.75rem; + background: var(--owl-surface); +} + +.directoryResultCount { + flex: none; + color: var(--owl-text-muted); + font-size: 0.8125rem; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.inactiveFilter { + flex: none; + padding: 0.45rem 0.7rem; + background: var(--owl-surface); + border: 1px solid var(--owl-border); + border-radius: 999px; +} + +.inactiveFilter:focus-within { + border-color: var(--owl-accent); + box-shadow: 0 0 0 3px var(--owl-accent-soft); +} + +.inactiveFilter label { + align-items: center; + gap: 0.5rem; + min-height: auto; + font-size: 0.875rem; + font-weight: 600; + white-space: nowrap; +} + +.inactiveFilter input { + margin: 0; +} + .projectDirectory { display: grid; gap: 0.75rem; @@ -139,12 +196,16 @@ box-shadow 150ms ease; } -.projectDirectoryItem:hover { +a.projectDirectoryItem:hover { background: var(--owl-surface-hover); border-color: var(--owl-border-strong); box-shadow: var(--owl-shadow-subtle); } +.projectDirectoryItemStatic { + cursor: default; +} + .projectDirectoryBody { min-width: 0; display: grid; diff --git a/crates/owlauth-server/web/src/generated/control-openapi.ts b/crates/owlauth-server/web/src/generated/control-openapi.ts index 179ae1a..ef20714 100644 --- a/crates/owlauth-server/web/src/generated/control-openapi.ts +++ b/crates/owlauth-server/web/src/generated/control-openapi.ts @@ -78,7 +78,7 @@ export interface paths { get: operations["get_project"]; put?: never; post?: never; - delete?: never; + delete: operations["delete_project"]; options?: never; head?: never; patch: operations["update_project"]; @@ -388,6 +388,22 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/projects/{project_id}/enable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["enable_project"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/projects/{project_id}/identity-mutation-intents": { parameters: { query?: never; @@ -1790,7 +1806,7 @@ export interface components { /** @enum {string} */ ProjectServerKeyStatus: "active" | "revoked"; /** @enum {string} */ - ProjectStatus: "active" | "disabled"; + ProjectStatus: "active" | "disabled" | "deleting"; ProjectUser: { created_at: string; display_name?: string | null; @@ -2566,6 +2582,97 @@ export interface operations { }; }; }; + delete_project: { + parameters: { + query?: never; + header?: never; + path: { + project_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ExpectedSecurityRevision"]; + }; + }; + responses: { + /** @description Project deletion accepted and immediately fenced */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Project"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Missing or invalid operator API key */ + 401: { + headers: { + /** @description Bearer authentication challenge */ + "WWW-Authenticate"?: string; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description The request exceeded the Control listener time budget */ + 408: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Revision or state conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Stored authority data violated an invariant */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Required authority unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; update_project: { parameters: { query?: never; @@ -4848,6 +4955,97 @@ export interface operations { }; }; }; + enable_project: { + parameters: { + query?: never; + header?: never; + path: { + project_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ExpectedSecurityRevision"]; + }; + }; + responses: { + /** @description Enabled project */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Project"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Missing or invalid operator API key */ + 401: { + headers: { + /** @description Bearer authentication challenge */ + "WWW-Authenticate"?: string; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description The request exceeded the Control listener time budget */ + 408: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Revision, state, idempotency, or capacity conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Stored authority data violated an invariant */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Required authority unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; create_identity_mutation_intent: { parameters: { query?: never; diff --git a/crates/owlauth-types/src/control.rs b/crates/owlauth-types/src/control.rs index ba3147c..4a61234 100644 --- a/crates/owlauth-types/src/control.rs +++ b/crates/owlauth-types/src/control.rs @@ -98,6 +98,8 @@ pub fn get_system() -> SystemCapabilities { crate::control_resources::get_project_policy, crate::control_resources::update_project_policy, crate::control_resources::disable_project, + crate::control_resources::enable_project, + crate::control_resources::delete_project, crate::control_resources::list_project_server_keys, crate::control_resources::get_project_server_key, crate::control_resources::create_project_server_key, diff --git a/crates/owlauth-types/src/control_resources.rs b/crates/owlauth-types/src/control_resources.rs index 6d2b8cd..a19a87e 100644 --- a/crates/owlauth-types/src/control_resources.rs +++ b/crates/owlauth-types/src/control_resources.rs @@ -34,6 +34,7 @@ where pub enum ProjectStatus { Active, Disabled, + Deleting, } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] @@ -1672,6 +1673,33 @@ control_path!( body = ExpectedSecurityRevision, params(("project_id" = String, Path)) ); +control_path!( + enable_project, + post, + "/v1/projects/{project_id}/enable", + Project, + "Enabled project", + body = ExpectedSecurityRevision, + params(("project_id" = String, Path)) +); +#[utoipa::path( + delete, + path = "/v1/projects/{project_id}", + request_body = ExpectedSecurityRevision, + responses( + (status = 202, description = "Project deletion accepted and immediately fenced", body = Project), + (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"), + (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))), + (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"), + (status = 409, description = "Revision or state conflict", body = ProblemDetails, content_type = "application/problem+json"), + (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"), + (status = 503, description = "Required authority unavailable", body = ProblemDetails, content_type = "application/problem+json") + ), + params(("project_id" = String, Path)), + security(("operator_api_key" = [])) +)] +#[doc(hidden)] +pub fn delete_project() {} control_path!( list_project_server_keys, get, diff --git a/docs/guide/agent-integrations.md b/docs/guide/agent-integrations.md index 94ead5b..43291c8 100644 --- a/docs/guide/agent-integrations.md +++ b/docs/guide/agent-integrations.md @@ -71,6 +71,15 @@ owlauth --profile production project create \ owlauth --profile production project disable PROJECT_ID \ --expected-security-revision 7 \ --yes + +owlauth --profile production project enable PROJECT_ID \ + --expected-security-revision 8 \ + --yes + +# Permanent deletion immediately fences the Project and cannot be undone. +owlauth --profile production project delete PROJECT_ID \ + --expected-security-revision 9 \ + --yes ``` Use `owlauth COMMAND --help` at each command level for the complete typed surface. Server-key creation reveals credential material once; capture that JSON directly into approved backend secret custody and do not paste it into shell arguments, logs, tickets, or agent context. Rotate a server key by creating and acknowledging a replacement, deploying it, and then revoking the predecessor. Resource secrets accepted by provider or webhook commands are also read from explicitly named environment variables and may not reuse the operator credential. diff --git a/spec/02-domain-and-crate-boundaries.md b/spec/02-domain-and-crate-boundaries.md index 5b814f5..23ff894 100644 --- a/spec/02-domain-and-crate-boundaries.md +++ b/spec/02-domain-and-crate-boundaries.md @@ -135,7 +135,9 @@ Every Project-owned table has `project_id` directly or reaches it through a cons A Runtime request resolves Project and Application before provider, user, session, ticket, or token lookup. A Server API request authenticates one Project server key, requires the route Project to match that exact Project, and invokes only the read-only Server query boundary. A Control request first authenticates the deployment operator API key and then invokes a Project-bound command. A valid key grants all Control commands; `belongs_to` does not replace Project qualification and is never an authorization check. -Project disablement cannot transfer child resources to another Project. A disabled Project rejects new login, handoff, refresh, current-user, and signing operations while preserving identifiers and durable state for audit and controlled recovery. The Control model exposes no hard-delete transition for Projects, Applications, providers, or users. +Project disablement cannot transfer child resources to another Project. Project status follows `active ⇄ disabled`; either transition advances `security_revision`, and a disabled Project rejects new login, handoff, refresh, current-user, Server API, and signing operations while preserving identifiers and durable state for controlled re-enablement. + +Permanent Project deletion is the separate irreversible `active|disabled → deleting → absent` lifecycle. The first revision-checked Control transaction advances `security_revision`, records the deletion request, and fences every Runtime, Server API, and ordinary Project-scoped Control operation. `deleting` cannot transition back to `active` or `disabled`. Bounded maintenance destroys provider-backed signing material and crypto-erases other live material before one Project-qualified finalizer physically removes the complete Project-owned PostgreSQL graph. Applications, providers, users, and other child resources expose no independent hard-delete transition. ## Ports diff --git a/spec/03-project-auth-flows-and-security-invariants.md b/spec/03-project-auth-flows-and-security-invariants.md index feb02d2..a017130 100644 --- a/spec/03-project-auth-flows-and-security-invariants.md +++ b/spec/03-project-auth-flows-and-security-invariants.md @@ -144,7 +144,7 @@ Reuse is not an authentication method in the allowed provider/email snapshot and - Local identity lookup uses `(project_id, provider_issuer, provider_subject)`, never email, display name, login name, or avatar URL. - For `login`, an unknown verified identity creates a Project user and linked identity atomically, and the same provider identity in another Project creates/resolves an independent user. For `identity_mutation`, an unknown identity creates only short-term candidate evidence; durable identity creation/attachment waits for final Control confirmation. `managed_reauthorization` requires the exact frozen existing user, linked identity, provider configuration, and connection generation; a different or unknown issuer/subject terminalizes it without creating or moving anything. - Matching email never silently links users. Link or merge requires explicit proof and Project-bound domain preconditions. -- A disabled Project or user cannot produce a handoff ticket, mutation receipt, or managed-reauthorization successor. +- A disabled or deleting Project, or a disabled user, cannot produce a handoff ticket, mutation receipt, or managed-reauthorization successor. - Callback completion revalidates that the provider registration remains actively assigned to the captured Application; an assignment revision mismatch terminalizes the exact login, mutation slot, or managed reauthorization. - Provider access tokens are transient and discarded after the bounded identity/profile call. Ordinary `login` may request the adapter-declared least managed scopes when current policy enables synchronization; `managed_reauthorization` must request exactly those frozen managed scopes and requires a valid renewable successor. Either class may retain only that renewable credential under the connection lifecycle, encryption, rotation, and stale-result rules in spec 11; login-only providers retain none. An `identity_mutation` slot requests only the adapter-declared non-renewable authentication/profile-proof scope subset, never offline access or managed scopes, and discards any renewable credential returned unexpectedly after extracting the bounded proof result. - No provider access/refresh token is returned downstream, stored in Project profile data, exposed through a generic provider API, or usable for Application-requested scopes. Provider credentials and full provider payloads never enter redirects, user projections, webhooks, audit context or ordinary logs. @@ -258,7 +258,7 @@ Application logout and Project browser logout use different credential and DTO c - Project browser logout atomically marks the current Project browser session terminated. Derived Application access tokens retain offline expiry, but every refresh checks the referenced browser-session status/revision and fails after termination. - A user may choose Application-only logout without terminating other Applications in the Project. - User disablement invalidates all of that Project user's browser/Application sessions, handoff tickets, and refresh families through the user security revision. -- Project disablement invalidates every Runtime operation in the Project through the Project security revision. +- Project disablement, re-enablement, and accepted deletion each advance the Project security revision. Disabled and deleting Projects invalidate every Runtime operation; only a revision-checked disabled Project may be re-enabled, while deleting is terminal. - Application disablement invalidates only that Application's Runtime credentials and pending handoffs. - Revocation responses do not reveal whether an unrelated Project, user, session, or token exists. diff --git a/spec/04-storage-and-migrations.md b/spec/04-storage-and-migrations.md index 7a757b4..374ba06 100644 --- a/spec/04-storage-and-migrations.md +++ b/spec/04-storage-and-migrations.md @@ -676,7 +676,12 @@ erDiagram - Canonical `(project_id, provider_issuer, provider_subject)` is unique for linked identities across all provider registrations in that Project. - Application/provider public keys are unique in a namespace that cannot resolve to another Project accidentally. - Project transfer does not exist. Changing `belongs_to` changes external metadata, not Project identity or child ownership. -- Project disablement advances `security_revision`; all Runtime operations compare their revision snapshot and fail closed. +- Project disablement and re-enablement each advance `security_revision`; all Runtime and Server API operations compare current Project status and their revision snapshot and fail closed. +- Project lifecycle is `active ⇄ disabled` and `active|disabled → deleting → absent`. A revision-checked delete request atomically sets `deleting`, advances `security_revision`, records `deletion_requested_at`, emits `project.deletion_requested`, converts every signing-key provisioning operation to provider-cleanup authority, clears stale cleanup leases, and crypto-erases Project configuration-secret envelopes that have no external destroy obligation. The same transaction locks every managed-provider connection and credential generation, clears all locally renewable credential ciphertext, terminalizes prepared/submitted renewal operations, and clears connection leases and future work so stale provider responses cannot restore credential authority. No ordinary Project-scoped write admits `deleting`, and no transition can restore it. +- Provider-backed signing handles are destroyed through the exact stable provisioning operation and `SigningKeyProvisioner::destroy` contract. Retryable or ambiguous failures remain durable cleanup work; unsupported or non-retryable failures remain `cleanup_blocked`. Neither case permits physical deletion or a false completed response. +- Project deletion treats renewable managed-provider credentials as local custody, not as a remote revocation workflow: accepted deletion immediately crypto-erases every ciphertext and terminalizes its leases before responding, so OwlAuth can no longer use or disclose the credential. It does not delay Project deletion on an upstream provider's optional token-revocation endpoint; provider-side copies follow that provider's own expiry/revocation policy. An operator that requires confirmed upstream revocation must revoke the managed connection before permanently deleting the Project. +- A bounded deletion finalizer may act only after every Project protected-material row is terminally erased, every managed-provider credential ciphertext is absent, and no signing cleanup remains pending, leased, or blocked. Physical deletion is performed only by an owner-owned `SECURITY DEFINER` function with exact fixed `search_path` and no `PUBLIC` execution grant. Direct Project DELETE is trigger-rejected outside that function. The caller explicitly selects READ COMMITTED before the transaction's first query; URL/session defaults cannot weaken that premise. The short transaction locks deleting Project candidates before evaluating cleanup blockers in a fresh statement snapshot, skips blocked candidates, installs an owner-only transaction-local context naming exactly the selected Project, removes its complete FK-closed graph including normally append-only audit/history/idempotency rows, deletes the Project row, and appends one minimal deployment-scoped `project.deleted` audit event. Project-owned foreign keys prevent new blocker rows from passing the locked root, and the post-lock proof observes blockers committed while candidate locking was in progress. Trigger exceptions require the finalizer's transaction-local exact Project context; OwlAuth exposes no caller-selected SQL or context-setting surface. +- Permanent deletion removes the live PostgreSQL authority and provider material; it does not claim immediate removal from retained backups, WAL/PITR archives, replicas, provider backups, or operator-exported copies. Those historical copies follow operator retention and restore procedures; restore must run deletion cleanup before admitting Project traffic. ## Entity constraints @@ -784,10 +789,10 @@ erDiagram - The single Control API key is loaded from `OWLAUTH_CONTROL_API_KEY`, remains only in process configuration, and has no PostgreSQL row, digest, identifier, permission set, expiry, or lifecycle endpoint. - Control idempotency is deployment-operator-scoped: `idempotency_key` is unique across the deployment. Project-bound records also carry the target Project. Reuse with another request digest is a conflict. - An idempotency record for creation of a durable resource remains as a replay/tombstone record for at least the lifetime of that resource and MUST NOT expire into permission to execute the same key again; its `expires_at` is null while lifetime retention applies. Other eligible command records have a documented retention window longer than every supported client retry and reconciliation window. After expiry, an unknown create outcome is reconciliation-required rather than automatically replayable. Backup/restore preserves each record consistently with its committed resource transaction. -- Audit events are append-only. Security mutations and their audit event commit in the same PostgreSQL transaction. Every Control event has the fixed actor kind `deployment_operator`; OwlAuth stores no server-side operator identity. +- Audit events are append-only during ordinary operation. Security mutations and their audit event commit in the same PostgreSQL transaction. Every Control event has the fixed actor kind `deployment_operator`; OwlAuth stores no server-side operator identity. The sole exception is the exact transaction-local Project-deletion finalizer described above, which removes that deleting Project's audit rows and leaves one detached deployment-level completion event. - Project-scoped events carry `project_id`; deployment-level events use a constrained null Project and cannot be confused with a Project event. - `safe_context` follows action-specific schemas and excludes arbitrary payloads and credentials. -- Generic retention maintenance never deletes `audit_events`, `key_state_events`, identity-mutation/managed-reauthorization create-result authority, durable-resource idempotency tombstones, merge tombstones, or live protected-material/resource generations. Those authorities require their own reviewed retention or archival contract; an elapsed interaction deadline alone is not permission to remove them. +- Generic retention maintenance never deletes `audit_events`, `key_state_events`, identity-mutation/managed-reauthorization create-result authority, durable-resource idempotency tombstones, merge tombstones, or live protected-material/resource generations. Those authorities require their own reviewed retention or archival contract; an elapsed interaction deadline alone is not permission to remove them. The exact deleting-Project finalizer is a separate reviewed whole-aggregate teardown and is not a generic retention path. ## Authoritative transaction boundaries diff --git a/spec/05-http-contract-and-surface-boundaries.md b/spec/05-http-contract-and-surface-boundaries.md index 2e6f6db..435f761 100644 --- a/spec/05-http-contract-and-surface-boundaries.md +++ b/spec/05-http-contract-and-surface-boundaries.md @@ -179,7 +179,7 @@ Control resources are rooted at `/v1/` and Project-owned operations always carry | Resource family | Representative operations | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/projects` | create/read/update/disable; read/write/filter exact `belongs_to`; list/create/revoke Project server keys with one-time create reveal | +| `/projects` | create/read/update, revisioned disable, enable, and permanent delete; read/write/filter exact `belongs_to`; list/create/revoke Project server keys with one-time create reveal | | `/projects/{project}/applications` | register/disable app; origins, redirects, publishable keys | | `/projects/{project}/providers` | configure/disable provider client registrations, obtain secret-free server-owned registration guidance for custom OIDC and named adapters, resource-keyed reconcile of interrupted secret provisioning with write-only secret re-entry, and assign Applications | | `/projects/{project}/provider-egress-policy` | read or revision-CAS the Project's custom-provider origin policy; default allow-all mode or recommended exact-origin mode | @@ -196,6 +196,8 @@ Control resources are rooted at `/v1/` and Project-owned operations always carry | `/system` | validate the operator key and return bounded Console/client capabilities | | Control health | safe probe response under the configured probe policy | +Project status is `active`, `disabled`, or `deleting`. `POST /v1/projects/{project_id}/disable` and `POST /v1/projects/{project_id}/enable` require the observed security revision and return the committed Project. `DELETE /v1/projects/{project_id}` accepts the observed security revision; it is valid from active or disabled, returns `202 Accepted` with the now-terminal `deleting` Project, and is idempotent while that row remains deleting. Once finalization removes the row, exact reads and repeated deletes return ordinary `404`. Every other Project-scoped Control mutation rejects deleting. The API exposes no restore, cancellation, tombstone, or provider-cleanup bypass operation. + The webhook resource returns only safe endpoint/secret-version/delivery metadata. Secret input is write-only, and replay names an existing immutable event plus its existing endpoint; Control has no arbitrary event-send route. Webhook payload/signature headers are an Application integration contract but endpoint configuration/replay remain Control operations. The exact `v1` HMAC grammar and duplicate/out-of-order receiver fixtures are generated and versioned with public contracts. ### Provider onboarding contract diff --git a/spec/07-cli-and-mcp-boundaries.md b/spec/07-cli-and-mcp-boundaries.md index 5da4edf..b25fdd0 100644 --- a/spec/07-cli-and-mcp-boundaries.md +++ b/spec/07-cli-and-mcp-boundaries.md @@ -123,7 +123,7 @@ The self-hosted adapter: - requires a validated/pinned self-hosted descriptor, exact Control endpoint, and TLS verification; - treats every invocation as the fixed deployment operator; -- sends explicit Project/target identifiers and expected revisions for destructive commands; +- sends explicit Project/target identifiers and expected revisions for destructive commands; Project disable, enable, and permanent delete require deliberate local `--yes` confirmation; - emits a Project server credential only from the original successful create response and exposes a separate revision-fenced, idempotent `server-key acknowledge` command that requires explicit confirmation after automation has durably stored it; - uses deployment-operator-scoped idempotency for eligible retries; - shows the selected profile/endpoint and a safe summary before destructive commands; @@ -167,7 +167,9 @@ The authenticated `/v1` Control OpenAPI contract is the MCP administration catal A generated tool exposes path and query parameters as top-level arguments, maps `Idempotency-Key` to `idempotency_key`, and places a JSON request entity under `body`. The model cannot supply a method, route template, arbitrary header, external URL, or undeclared argument. Dispatch occurs only against the in-process Control `/v1` router after the enclosing MCP request has authenticated the operator key; it never loops through the network or bypasses Control DTO, Project ownership, revision, state-transition, idempotency, custody, or application/domain validation. -The operator key deliberately grants the MCP caller the same complete deployment authority as Control HTTP. MCP adds no read-only role, attenuated scope, preview/commit table, or adapter-specific confirmation ceremony. A Control operation's server-enforced `confirm`, expected-revision, and idempotency fields remain mandatory exactly when its public contract requires them. CLI-only `--yes` prompts are local human UX and do not constrain another fully authorized Control adapter. Tool annotations accurately describe read-only, destructive, idempotent, and open-world hints but never replace server enforcement. +Project enable and permanent delete are exposed through their reviewed Control operations like every other generated tool. Delete is annotated destructive and idempotent at the protocol level: replay while the Project is `deleting` returns the current terminal state, while replay after physical removal returns not found. It requires the expected security revision and never invents an MCP-only authority, preview token, or restore path. + +The operator key deliberately grants the MCP caller the same complete deployment authority as Control HTTP. MCP adds no read-only role, attenuated scope, preview/commit table, or adapter-specific confirmation ceremony. A Control operation's expected-revision and idempotency fields remain mandatory exactly when its public contract requires them. CLI-only `--yes` prompts and Console dialogs are local human UX and do not constrain another fully authorized Control adapter. Tool annotations accurately describe read-only, destructive, idempotent, and open-world hints but never replace server enforcement. Tools MUST NOT provide raw SQL, repository access, arbitrary HTTP/OpenAPI forwarding, caller-selected path/method/header invocation, CLI/shell/filesystem execution, Runtime or Server API dispatch, or unrestricted bulk mutation. Full Control parity does include the reviewed secret-bearing operations: provider, SMTP, and webhook secrets are write-only Control request fields, while Project server-key creation returns its credential exactly once. Those values therefore enter model-visible MCP arguments or results when an operator invokes those tools; reviewed identity queries may likewise return model-visible identity presentation fields. Operators MUST use only a trusted MCP host/model and approved secret and personal-data handling for such calls. The Control operator key itself remains transport-only and MUST NOT enter prompt text, tool arguments/schemas/results, protocol errors, URLs, session IDs, logs, or agent-plugin configuration visible to the model. Provider tokens, handoff/session credentials, signing private keys, stored protected material, and repository rows remain unexported. diff --git a/spec/08-consistency-resilience-and-plane-separation.md b/spec/08-consistency-resilience-and-plane-separation.md index 3afe98b..2690906 100644 --- a/spec/08-consistency-resilience-and-plane-separation.md +++ b/spec/08-consistency-resilience-and-plane-separation.md @@ -36,14 +36,15 @@ Runtime consults PostgreSQL for mutable Project/Application/user/session/key fac - Every row lock, uniqueness check, conditional update, and idempotency record includes the authoritative Project where applicable. - A request cannot combine Project A route context with Application/user/session/provider/key state from Project B. - Global UUID uniqueness is defense-in-depth, not a replacement for Project predicates. -- Project disablement revision invalidates Runtime work prepared from an earlier snapshot before final commit. +- Project disablement, re-enablement, and deletion-request revisions invalidate Runtime work prepared from an earlier snapshot before final commit. A deleting Project is terminal and remains fully fenced until physical removal. - `belongs_to` changes do not move children or change Runtime credentials; every external-gateway mutation compares the previously observed Project `metadata_revision` in the same transaction as its child effect. ## Security-change visibility After Control commit: -- disabled Project rejects all new login, callback completion, handoff, refresh, current-user, and signing operations; +- disabled Project rejects all new login, callback completion, handoff, refresh, current-user, Server API, and signing operations, while a revision-checked enable restores only future operations under the new Project security revision; +- accepted Project deletion commits `deleting` before responding, rejects every new Runtime/Server API/ordinary Project Control operation, and remains inspectable only for safe Control lifecycle status until bounded provider cleanup and physical finalization complete; - disabled Application rejects its login/handoff/refresh and invalidates its Application sessions, while Project browser sessions and other Applications remain valid; - removed redirect/origin rejects new use and pending completion through Application revision mismatch; - disabled provider rejects login/callback for that Project/provider only; @@ -68,10 +69,10 @@ Already issued self-contained Project access tokens remain subject to signed exp | Long-term email-PII or managed-credential AEAD key unavailable | affected identity/profile/sync capability is unready or requires explicit destructive recovery | exact email lookup or affected projection field fails closed rather than returning guessed/partial protected data | key inventory/recovery remains available without plaintext read-back | old key cannot retire without proven re-encryption; no silent data loss or credential reuse | | Projection verified-email persisted version unavailable locally | affected projection return/repair fails closed | affected Application projection read/introspection fails closed | identity confirmation that would fan out an affected binding remains ready and rolls back without receipt consumption | no fallback version; operators keep every referenced version through external backfill, cutover, and retirement | | Configuration-secret opener/material unavailable or invalid | affected Project/provider login, SMTP claim, or webhook signing fails closed; guarded durable work backs off | unaffected directory reads; introspection still requires signing verification authority | metadata remains manageable; sealing/provider failure is operation-specific | no plaintext guess, cross-Project/generation fallback, stale profile commit, or erased-material resurrection | -| Remote signing-key provisioner has an ambiguous create result | unrelated Runtime continues; no new key is selected | directory reads continue; introspection uses only committed verification authority | durable operation remains pending and reconciles the same stable provider identity | no duplicate/untracked external key and no publish before handle/public-key consistency is proven | +| Remote signing-key provisioner has an ambiguous create or destroy result | unrelated Runtime continues; no new key is selected | directory reads continue; introspection uses only committed verification authority | durable operation remains pending and reconciles the same stable provider identity | no duplicate/untracked external key and no publish before handle/public-key consistency is proven | | SMTP unavailable/permanent rejection or generation disabled/compromised | generic login start/method picker remain independent; challenge responses remain enumeration-safe, and jobs retry only while the pinned generation/revision is eligible and useful | unaffected | metadata/lifecycle remains manageable and test reports safe failure class | completion revalidates pinned eligibility in PostgreSQL; compromise commit denies later proof even after in-flight delivery | | Webhook endpoint unavailable, duplicate, or out of order | login/current-user/profile mutation success is unchanged | committed directory/projection reads remain available | safe delivery health/replay remains available | immutable event persists; receiver deduplicates event ID and compares Application-specific `projection_revision` | -| Worker crash or lease expiry | another worker may repeat bounded external attempt | request processing is independent of workers | same durable state is inspectable | PostgreSQL challenge/generation/event state prevents duplicate identity effects | +| Worker crash or lease expiry | another worker may repeat bounded external attempt, including deleting-Project signing cleanup | request processing is independent of workers | same durable state is inspectable | PostgreSQL challenge/generation/event state prevents duplicate identity effects | | Control endpoint unavailable or restarting for operator-key rotation | continues in split-process topology; redundant `all` rollout may preserve capacity; single-instance `all` restart interrupts Auth too | continues in split-process topology; single-instance `all` restart interrupts Auth too | unavailable | Auth has no Control RPC dependency or credential crossover; process topology still determines restart availability | | Auth endpoint unavailable | unavailable | unavailable | Control continues subject to its dependencies | Runtime and Server API share one transport lifecycle; Control does not accept Auth credentials or proxy Auth requests | | Crash during transaction | no committed success unless commit completed | read fails without an alternate result | same | PostgreSQL atomicity defines outcome | diff --git a/spec/12-product-ui-and-interaction-design.md b/spec/12-product-ui-and-interaction-design.md index d873fe7..8593104 100644 --- a/spec/12-product-ui-and-interaction-design.md +++ b/spec/12-product-ui-and-interaction-design.md @@ -202,7 +202,7 @@ These routes organize existing or contract-backed workflows; they do not require ### Project directory and overview -The Project directory is a navigation collection with one primary action per item, so it uses a compact semantic list of whole-row links rather than a comparison table. Each linked row displays the display name, public ID, and user-facing status, stacks safely at narrow widths, and exposes the public ID and status as accessible description without replacing the display name as the link name. The `belongs_to` extension pointer and optimistic-concurrency revisions are API-level integration details and MUST NOT appear in ordinary lists, summaries, creation, or editing. If the directory gains comparison-oriented columns or independent row actions, it MUST become a semantic table instead of nesting controls inside the whole-row link. +The Project directory is a navigation collection with one primary action per item, so it uses a compact semantic list of whole-row links rather than a comparison table. Active Projects are shown by default. One page-memory **Show inactive projects** checkbox includes disabled and deleting Projects; it is never persisted in browser storage or encoded as authority. Each linked row displays the display name, public ID, and user-facing status, stacks safely at narrow widths, and exposes the public ID and status as accessible description without replacing the display name as the link name. Deleting rows are status-only and cannot enter ordinary Project management routes. The `belongs_to` extension pointer and optimistic-concurrency revisions are API-level integration details and MUST NOT appear in ordinary lists, summaries, creation, or editing. If the directory gains comparison-oriented columns or independent row actions, it MUST become a semantic table instead of nesting controls inside the whole-row link. Project creation opens a focused dialog or narrow side sheet instead of placing a permanent create form above the inventory. The surface contains only the display name, preserves idempotency, and moves to the created Project overview after success. @@ -276,7 +276,7 @@ The generated Project server key is the one explicit exception: its non-dismissi High-impact actions use an OwlAuth-owned accessible confirmation dialog rather than `window.confirm`. The dialog displays the exact safe target name and consequence. Routine reversible actions require one clear confirmation; irreversible or broad actions MAY require typing a bounded display name when the owning behavior calls for stronger intent. Confirmation text never asks the operator to paste a secret or internal identifier. -Destructive controls are not styled as the page's primary teal action. Project and Application disablement, Project server-key revocation, provider disconnect, session revocation, identity mutation, key retirement, and webhook replay retain their existing expected-revision, idempotency, exact-plan, and audit semantics. The Project settings page places irreversible or deployment-affecting actions in a visually separated **Danger zone** after ordinary settings. +Destructive controls are not styled as the page's primary teal action. Project and Application disablement, Project server-key revocation, provider disconnect, session revocation, identity mutation, key retirement, and webhook replay retain their existing expected-revision, idempotency, exact-plan, and audit semantics. The Project settings page places lifecycle controls in a visually separated **Danger zone** after ordinary settings: an active Project offers **Disable Project**, a disabled Project offers **Enable Project**, and either offers **Permanently delete Project** without requiring disablement first. Permanent deletion requires typing the exact Project display name, explains that access stops immediately and provider cleanup may continue asynchronously, sends the current security revision, and returns to the Project directory after acceptance. Deleting Projects expose no enable or cancel action. ## Hosted Authentication UI diff --git a/spec/13-server-api-and-project-server-keys.md b/spec/13-server-api-and-project-server-keys.md index 5c8cb5d..daa8960 100644 --- a/spec/13-server-api-and-project-server-keys.md +++ b/spec/13-server-api-and-project-server-keys.md @@ -100,7 +100,7 @@ Verification: 5. verifies that Project and key remain active in the same authoritative read; 6. returns one private typed Server actor containing Project/key identity, never raw credential bytes. -Unknown, malformed, revoked, wrong-Project, disabled-Project, wrong-version, and wrong-digest credentials return the same bounded `401` response and `WWW-Authenticate: Bearer` challenge. Core performs no source- or credential-window traffic limiting before or after key resolution; a SaaS or operator-owned ingress may apply generic traffic controls, but those controls cannot authenticate a key. +Unknown, malformed, revoked, wrong-Project, disabled-or-deleting-Project, wrong-version, and wrong-digest credentials return the same bounded `401` response and `WWW-Authenticate: Bearer` challenge. Core performs no source- or credential-window traffic limiting before or after key resolution; a SaaS or operator-owned ingress may apply generic traffic controls, but those controls cannot authenticate a key. A successful request may advance `last_used_at` at most once per fifteen-minute bucket through a best-effort update guarded by `status = active` and an older stored bucket. Usage metadata is lifecycle-neutral: it does not advance the key revision, cannot make an expected-revision revoke conflict, and cannot update a row after revocation wins. It never authorizes, revokes, or delays the request and has no per-request audit cardinality. @@ -179,7 +179,7 @@ User/projection responses use `private, no-store`; key lifecycle responses use C OwlAuth Core does not add Server API IP, route, Project, key, global, bot/risk, product-quota, or billing limits. A SaaS or operator-owned ingress may enforce those controls for customer backend traffic and owns any generic `429` contract it introduces. Core continues to enforce request shape, deadlines, connection/in-flight capacity, PostgreSQL pool bounds, and provider/worker concurrency as local resource-safety boundaries. -Project disablement and key revocation affect the next authoritative request. Server-key authentication, user/email/projection reads, and token introspection consult PostgreSQL authority on every request and are never accepted from a process response cache in v1. This keeps externally observable Server reads at the latest committed PostgreSQL state without inventing a directory-cache consistency protocol. Customer backends may cache minimized directory responses under their own product policy, but must not represent them as online revocation checks; Server introspection is the authoritative online token/session path. +Project disablement, re-enablement, accepted deletion, and key revocation affect the next authoritative request. Server-key authentication, user/email/projection reads, and token introspection consult PostgreSQL authority on every request and are never accepted from a process response cache in v1. This keeps externally observable Server reads at the latest committed PostgreSQL state without inventing a directory-cache consistency protocol. Customer backends may cache minimized directory responses under their own product policy, but must not represent them as online revocation checks; Server introspection is the authoritative online token/session path. ## OpenAPI and implementation ownership