{"openapi":"3.1.0","info":{"title":"QuickHost API","version":"0.1.0"},"paths":{"/v1/auth/login":{"post":{"tags":["auth"],"summary":"Login","description":"Authenticate with email + password and set session cookies.\n\nThe response JSON includes ``csrf_token`` so that SPAs hosted on a\ndifferent origin (app.qh.localhost vs api.qh.localhost) can obtain the\nCSRF token without having to read the ``qh_csrf`` cookie (I5).\n\nRate-limited (A3): 5/min per client IP AND 20/hour per normalized email.\nThe per-email limit is what stops distributed guessing against one\ntarget; the per-IP limit stops a single source from spraying many\nemails. Checked before any DB/Argon2 work, so a rate-limited response is\ncheap. The 429 doesn't leak whether the email exists — the bucket key is\nthe raw normalized email regardless of account existence.\n\nArgs:\n    body: Login credentials.\n    request: Incoming request (for client IP).\n    response: FastAPI response for setting cookies.\n\nReturns:\n    User JSON plus ``csrf_token`` on success.\n\nRaises:\n    QuickhostError: ``rate_limited`` (429) if either limit is exhausted.\n    QuickhostError: ``invalid_credentials`` (401) on any auth failure.","operationId":"login_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Login V1 Auth Login Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","description":"Clear session and CSRF cookies.\n\nArgs:\n    response: FastAPI response for clearing cookies.\n    _user: Current authenticated user (ensures login).\n\nReturns:\n    ``{\"status\": \"ok\"}``","operationId":"logout_v1_auth_logout_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Logout V1 Auth Logout Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/logout-all":{"post":{"tags":["auth"],"summary":"Logout All","description":"Invalidate all existing session cookies for this user by bumping session_epoch.\n\nThis is a CSRF-protected endpoint. After a successful call, any previously\nissued session cookie (including the current one) will be rejected by\n_parse_cookie_value.\n\nArgs:\n    response: FastAPI response for clearing cookies.\n    user: The currently authenticated user.\n\nReturns:\n    ``{\"status\": \"ok\"}``","operationId":"logout_all_v1_auth_logout_all_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Logout All V1 Auth Logout All Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/csrf":{"get":{"tags":["auth"],"summary":"Get Csrf Token","description":"Return the current CSRF token in JSON for SPA bootstrap after redirect-based logins.\n\nAfter a Google OAuth callback (which sets session cookies via redirect), the\nSPA cannot read the CSRF cookie if the API is on a different subdomain.\nThe SPA should call this endpoint immediately after the OAuth redirect lands\nto obtain the CSRF token and store it in memory for subsequent requests.\n\nIdempotent by design: if the request already carries a ``__Host-qh_csrf``\ncookie, we return *that* token (re-setting the same cookie) rather than\nminting a new one. Bootstrap is called on every auth-state change and can run concurrently\nacross tabs / focus refetches; minting a fresh token each time would rotate\nthe single shared cookie out from under another tab's in-memory token and\ncause \"CSRF token mismatch\" on the next mutation. Reusing the existing token\nmakes bootstrap a \"sync in-memory ← cookie\" operation.\n\nArgs:\n    response: FastAPI response for refreshing cookies.\n    user: The currently authenticated user.\n    csrf_cookie: The CSRF token already on the request's cookie, if any.\n        Only the ``__Host-`` cookie counts: a value under the old name may\n        have been planted by a sibling origin, so it is never carried\n        forward — a caller holding one gets a freshly minted token instead.\n\nReturns:\n    ``{\"csrf_token\": \"...\"}``","operationId":"get_csrf_token_v1_auth_csrf_get","parameters":[{"name":"__Host-qh_csrf","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"  Host-Qh Csrf"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Get Csrf Token V1 Auth Csrf Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/magic-link":{"post":{"tags":["auth"],"summary":"Request Magic Link","description":"Request a magic login link (always returns 202 to avoid enumeration).\n\nRate-limited (A3): 3/hour per target email. When allowed, behavior is\nunchanged (always 202, whether or not the account exists).\n\nArgs:\n    body: Request body with ``email``.\n    background_tasks: FastAPI background tasks.\n\nReturns:\n    ``{\"status\": \"sent\"}``\n\nRaises:\n    QuickhostError: ``rate_limited`` (429) if the per-email limit is exhausted.","operationId":"request_magic_link_v1_auth_magic_link_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Request Magic Link V1 Auth Magic Link Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/magic-link/verify":{"post":{"tags":["auth"],"summary":"Verify Magic Link","description":"Verify a magic link token and set session cookies.\n\nUses a SELECT ... FOR UPDATE row lock to prevent concurrent double-spend (I1).\nReturns uniform ``invalid_token`` for unknown/used/expired tokens (I3).\nIncludes ``csrf_token`` in the response body (I5).\n\nArgs:\n    body: Contains the raw token.\n    response: FastAPI response for setting cookies.\n\nReturns:\n    User JSON plus ``csrf_token`` on success.\n\nRaises:\n    QuickhostError: ``invalid_token`` (401) for any token validation failure.","operationId":"verify_magic_link_v1_auth_magic_link_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkVerifyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Verify Magic Link V1 Auth Magic Link Verify Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/password-reset":{"post":{"tags":["auth"],"summary":"Request Password Reset","description":"Request a password reset email (always 202).\n\nRate-limited (A3): 3/hour per target email. When allowed, behavior is\nunchanged (always 202, whether or not the account exists).\n\nArgs:\n    body: Request body with ``email``.\n    background_tasks: FastAPI background tasks.\n\nReturns:\n    ``{\"status\": \"sent\"}``\n\nRaises:\n    QuickhostError: ``rate_limited`` (429) if the per-email limit is exhausted.","operationId":"request_password_reset_v1_auth_password_reset_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Request Password Reset V1 Auth Password Reset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/password-reset/confirm":{"post":{"tags":["auth"],"summary":"Confirm Password Reset","description":"Consume a password reset token and update the user's password.\n\nUses a SELECT ... FOR UPDATE row lock to prevent concurrent double-spend (I1).\nBumps session_epoch to invalidate all existing sessions (I4).\nReturns uniform ``invalid_token`` for unknown/used/expired tokens (I3).\n\nArgs:\n    body: Contains ``token`` and new ``password``.\n\nReturns:\n    ``{\"status\": \"ok\"}``\n\nRaises:\n    QuickhostError: ``invalid_token`` (401) for any token validation failure.\n    QuickhostError: ``validation_error`` (422) if password too short.","operationId":"confirm_password_reset_v1_auth_password_reset_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetConfirmRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Confirm Password Reset V1 Auth Password Reset Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/password/change":{"post":{"tags":["auth"],"summary":"Change Password","description":"Change the authenticated user's password and refresh session (I4).\n\nBumps session_epoch to invalidate all *other* sessions, then issues a new\nsession cookie reflecting the new epoch so the caller stays logged in.\n\nArgs:\n    body: Contains ``current_password`` and ``new_password``.\n    response: FastAPI response for refreshing cookies.\n    user: The currently authenticated user.\n\nReturns:\n    ``{\"status\": \"ok\", \"csrf_token\": \"...\"}``\n\nRaises:\n    QuickhostError: ``invalid_credentials`` (401) if current password wrong.\n    QuickhostError: ``validation_error`` (422) if new password too short.","operationId":"change_password_v1_auth_password_change_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordChangeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Change Password V1 Auth Password Change Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/google":{"get":{"tags":["auth"],"summary":"Google Login","description":"Initiate Google OAuth2 authorization code flow.\n\nRequires SessionMiddleware to be registered on the application so that the\nOAuth ``state`` parameter can be stored and validated in the callback (C1).\n\nArgs:\n    request: The incoming HTTP request (session must be available).\n\nReturns:\n    Redirect to Google authorization endpoint.\n\nRaises:\n    QuickhostError: ``oauth_not_configured`` (503) if credentials missing.","operationId":"google_login_v1_auth_google_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Google Login V1 Auth Google Get"}}}}}}},"/v1/auth/google/link":{"get":{"tags":["auth"],"summary":"Google Link Start","description":"Initiate Google OAuth to link a Google account to the logged-in user.\n\nSame authorization-code flow as ``/google``, but marks the cookie session\nwith a link intent so the shared callback attaches the Google identity to\nthe authenticated user instead of signing someone in. This is the\nexplicit, safe counterpart to the C2 auto-link rule: the caller has\nalready proven account ownership via their session, so linking any\nverified Google identity they control is legitimate.\n\nBrowser-navigation endpoint: unauthenticated callers are redirected to\nthe SPA login page rather than given a JSON 401.\n\nArgs:\n    request: The incoming HTTP request (session must be available).\n\nReturns:\n    Redirect to the Google authorization endpoint, or to the SPA login\n    page when unauthenticated.\n\nRaises:\n    QuickhostError: ``oauth_not_configured`` (503) if credentials missing.","operationId":"google_link_start_v1_auth_google_link_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Google Link Start V1 Auth Google Link Get"}}}}}}},"/v1/auth/google/callback":{"get":{"tags":["auth"],"summary":"Google Callback","description":"Handle Google OAuth2 callback (login mode, or link mode via /google/link).\n\nSecurity checks applied (C2):\n- ``email_verified`` must be True; otherwise 403.\n- If google_sub matches an existing user → login (normal case).\n- Else if email matches an existing user: auto-link ONLY if that user has\n  no password_hash AND no google_sub (pure invited-never-set-password\n  account). Any other case → 403 \"account exists — sign in with your\n  password\".\n- If no account exists but valid pending invites do → provision the\n  account and accept every pending invite (L4; safe because there is no\n  existing account to take over and the email is Google-verified).\n- Otherwise → 403 (invite-only system).\n\nLink mode (L1, started by ``/google/link``): the link intent is popped\nfrom the cookie session unconditionally — success or failure — so it can\nnever bleed into a later login callback. Outcomes are reported by\nredirecting to the SPA settings page (``google_linked=1`` /\n``google_link_error=<code>``) because the callback is a top-level browser\nnavigation, not an XHR.\n\nThe callback cannot return JSON (it is a browser redirect), so the SPA\nmust call GET /v1/auth/csrf after landing to obtain the CSRF token (I5).\n\nArgs:\n    request: The incoming HTTP request with OAuth state/code.\n    response: FastAPI response for setting cookies.\n\nReturns:\n    303 redirect to APP_URL (login) or the settings page (link).\n\nRaises:\n    QuickhostError: ``forbidden`` (403) on any login-mode auth failure.","operationId":"google_callback_v1_auth_google_callback_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/auth/google/unlink":{"post":{"tags":["auth"],"summary":"Google Unlink","description":"Disconnect the linked Google account from the authenticated user.\n\nSession-only (no Bearer): this is an account-security mutation driven\nfrom the SPA settings page. Refused when the account has no password —\nremoving the only sign-in method would lock the user out.\n\nArgs:\n    user: The session-authenticated user.\n\nReturns:\n    ``{\"status\": \"ok\"}``\n\nRaises:\n    QuickhostError: ``conflict`` (409) if nothing is linked or the\n        account has no password to fall back on.","operationId":"google_unlink_v1_auth_google_unlink_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Google Unlink V1 Auth Google Unlink Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/invites/accept":{"post":{"tags":["auth"],"summary":"Accept Invite","description":"Accept a workspace invitation.\n\nNew-account branch: creates user, adds membership, sets session cookies (unchanged).\n\nExisting-account branch (I7):\n- If the request is session-authenticated as the invite's email owner →\n  add membership only, return 200 with no new session.\n- If unauthenticated (or authenticated as a different user) → 409\n  ``account_exists`` telling the client to log in first.\n\nReturns uniform ``invalid_token`` for unknown/used/expired tokens (I3).\nUses SELECT ... FOR UPDATE row lock to prevent concurrent double-spend (I1).\nIncludes ``csrf_token`` in the new-account branch response body (I5).\n\nArgs:\n    body: Invite acceptance data (token, name, optional password).\n    request: Incoming HTTP request (needed for session-auth check).\n    response: FastAPI response for setting cookies.\n\nReturns:\n    User JSON plus ``csrf_token`` (new-account branch) or user JSON (existing-account).\n\nRaises:\n    QuickhostError: Various error codes on failure.","operationId":"accept_invite_v1_auth_invites_accept_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteAcceptRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Accept Invite V1 Auth Invites Accept Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/verify-email/resend":{"post":{"tags":["auth"],"summary":"Resend Verification","description":"Send a fresh confirmation link.\n\nAlways 202, whether or not the address exists and whether or not it is\nalready verified — the same non-disclosure rule the magic-link route\nfollows. An endpoint that answered differently would be an account-existence\noracle available to anyone, which is worse than the inconvenience it saves.\n\nArgs:\n    body: Request body with ``email``.\n    background_tasks: FastAPI background tasks.\n\nReturns:\n    ``{\"status\": \"sent\"}``\n\nRaises:\n    QuickhostError: ``rate_limited`` (429) when the per-address limit is spent.","operationId":"resend_verification_v1_auth_verify_email_resend_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailResendRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Resend Verification V1 Auth Verify Email Resend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/verify-email/confirm":{"post":{"tags":["auth"],"summary":"Confirm Verification","description":"Redeem a confirmation token.\n\nDeliberately does **not** issue a session. A verification link proves control\nof a mailbox, not intent to sign in on this device — and a link that logged\nyou in would turn a forwarded confirmation email into account takeover. The\ntoken's ``purpose`` makes that structurally impossible; this route just\ndeclines to undo it.\n\nArgs:\n    body: Request body with ``token``.\n\nReturns:\n    ``{\"status\": \"verified\", \"email\": …}``\n\nRaises:\n    QuickhostError: ``invalid_token`` (400) when unknown, expired, already\n        used, or minted for a different purpose.","operationId":"confirm_verification_v1_auth_verify_email_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailConfirmRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Confirm Verification V1 Auth Verify Email Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/signup":{"post":{"tags":["auth"],"summary":"Signup","description":"Create an account, a workspace, and a trial.\n\n**Always 202, and never a session.** The response is identical whether or\nnot the address already has an account: returning 201-with-session for a new\none and something else for an existing one would make this an\naccount-existence oracle for anybody. An address that is already registered\ngets a \"someone tried to sign up with your address\" email instead of a\nverification link, so a real owner learns about it and an attacker learns\nnothing.\n\nNot signing anyone in is the other half of that. It costs a step — the user\nverifies, then signs in with the password they just chose — and it is what\nlets both branches return the same bytes.\n\nThe trial is Studio for 7 days, per the published promise, so the workspace\nis created on the Studio plan with ``trial_ends_at`` set rather than on some\nseparate trial tier.\n\nArgs:\n    body: Email, password, and optional display name.\n    background_tasks: For the outgoing email.\n\nReturns:\n    ``{\"status\": \"check_your_email\"}``\n\nRaises:\n    QuickhostError: ``validation_error`` (422) on a weak password;\n        ``rate_limited`` (429) per address.","operationId":"signup_v1_auth_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Signup V1 Auth Signup Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me":{"get":{"tags":["auth"],"summary":"Get Me","description":"Return the authenticated user and their workspaces.\n\nScoped credentialed CORS (Comments v2 P4a, extended by P4b): when the\nrequest ``Origin`` is a hosted-project subdomain that\n``services.authz.trusted_viewer_entry_for_origin`` resolves to a trusted\nentry, the response reflects ``Access-Control-Allow-Origin``/\n``-Credentials``/``Vary: Origin`` and the body is a MINIMAL\n``{\"user\": {\"name\", \"email\"}, \"comment_token\": ...}`` — a short-lived\nsigned token (see ``services.security.issue_comment_token``) that viewer\ncomment-app JS can forward on ``POST /_qh/comments`` to comment as a\nverified identity. Two kinds of origin earn that trust:\n\n- A **rendered-viewer** kind (markdown/text/pdf/image/video/data —\n  QuickHost chrome that runs no author-controlled JS).\n- **(P4b)** A ``kind=site`` origin that is *currently framed*\n  (``comments_enabled`` and ``html_display_mode`` in\n  ``{\"auto\", \"framed\"}``) — i.e. the code actually running at that\n  top-level origin is the QuickHost wrapper (``routers.serve``), and the\n  author's HTML/JS is isolated inside an opaque-origin sandboxed\n  iframe that never sees this reflected CORS or the token.\n\nEvery other origin — the SPA (handled entirely by the global\n``CORSMiddleware``), a **raw**-served ``kind=site`` origin (comments off,\nor ``html_display_mode=\"raw\"`` — author JS running directly at the\ntop-level origin, the exact hazard this scoping exists to exclude), or any\nunknown/hostile/lookalike origin — gets neither the scoped headers nor a\ntoken.\n\nThe scoped branch is ALSO skipped for any Bearer-authenticated request,\neven one carrying a trusted-viewer Origin header: the scoped identity/\ntoken exists for browser cookie-session viewers only, and a Bearer/CLI\ncaller can set arbitrary headers, so honoring a spoofed Origin there\nwould hand out ACAO + a comment_token to a non-browser caller with no\nsame-origin-policy backstop.\n\nThis handler resolves auth via ``current_user_optional`` (not the\nraising ``current_user`` dependency) and returns an explicit\n``Response`` so the scoped CORS headers can be attached to BOTH the 200\nand the 401 outcome — a dependency-raised ``QuickhostError`` is turned\ninto a response by the global exception handler, which has no way to see\n(and would drop) headers set on a Response built inside this function.\n\nArgs:\n    request: The incoming HTTP request.\n\nReturns:\n    For a scoped trusted-viewer origin (cookie session only): 200 with\n    ``{\"user\": {\"name\", \"email\"}, \"comment_token\": \"...\"}``. Otherwise:\n    200 with the full ``{\"user\": {...}, \"workspaces\": [...]}`` payload,\n    or a 401 ``{\"error\": {\"code\": \"auth_required\", ...}}`` when\n    unauthenticated. Each workspace carries an ``is_owner`` flag and the\n    caller's own (home) workspace sorts first.","operationId":"get_me_v1_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"tags":["deletion"],"summary":"Delete Account","description":"Schedule this account, and the workspaces only it uses, for erasure.\n\n202 rather than 204: nothing has been destroyed, and the body carries the\ndate on which it will be.\n\nArgs:\n    body: Password confirmation.\n    user: The authenticated caller.\n    _csrf: CSRF guard.\n    session: Owner session.\n\nReturns:\n    What was scheduled, and when it happens.\n\nRaises:\n    QuickhostError: ``forbidden`` (403) on a failed re-authentication;\n        ``conflict`` (409) when a shared workspace blocks it.","operationId":"delete_account_v1_me_delete","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Account V1 Me Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/device":{"post":{"tags":["device"],"summary":"Initiate Device Flow","description":"Initiate a device authorization flow.\n\nThe ``user_code`` uses the XXXXX-XXXX (9-char body, 10 chars total with\ndash) format from an unambiguous alphabet, giving ~45 bits of entropy (I2).\n\nReturns:\n    device_code, user_code, verification_url, interval, expires_in.","operationId":"initiate_device_flow_v1_auth_device_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Initiate Device Flow V1 Auth Device Post"}}}}}}},"/v1/auth/device/token":{"post":{"tags":["device"],"summary":"Poll Device Token","description":"Poll for a device grant result.\n\nUses SELECT ... FOR UPDATE to atomically consume an approved grant (I1).\nReturns uniform ``invalid_token`` for unknown/expired/consumed grants (I3).\n\nArgs:\n    body: Contains ``device_code``.\n\nReturns:\n    ``{\"status\": \"pending\"}`` or ``{\"status\": \"approved\", \"token\": \"qh_…\"}``.\n\nRaises:\n    QuickhostError: ``device_denied`` (400), ``invalid_token`` (401), or\n        ``rate_limited`` (429).","operationId":"poll_device_token_v1_auth_device_token_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Poll Device Token V1 Auth Device Token Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/device/approve":{"post":{"tags":["device"],"summary":"Approve Device","description":"Approve a pending device grant.\n\nApplies per-user in-process rate limiting (I2): max 10 attempts per minute.\n\nArgs:\n    body: Contains ``user_code``.\n    background_tasks: For background email.\n    user: The session-authenticated user.\n\nReturns:\n    ``{\"status\": \"approved\"}``\n\nRaises:\n    QuickhostError: If grant not found, already resolved, or rate limit exceeded.","operationId":"approve_device_v1_auth_device_approve_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceUserCodeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Approve Device V1 Auth Device Approve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/device/deny":{"post":{"tags":["device"],"summary":"Deny Device","description":"Deny a pending device grant.\n\nApplies per-user in-process rate limiting (I2): max 10 attempts per minute.\n\nArgs:\n    body: Contains ``user_code``.\n    user: The session-authenticated user.\n\nReturns:\n    ``{\"status\": \"denied\"}``","operationId":"deny_device_v1_auth_device_deny_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceUserCodeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Deny Device V1 Auth Device Deny Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["oauth"],"summary":"Authorization Server Metadata","description":"OAuth 2.0 Authorization Server Metadata (RFC 8414).","operationId":"authorization_server_metadata__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Authorization Server Metadata  Well Known Oauth Authorization Server Get"}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"tags":["oauth"],"summary":"Protected Resource Metadata","description":"OAuth 2.0 Protected Resource Metadata (RFC 9728) — points at the AS.","operationId":"protected_resource_metadata__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Protected Resource Metadata  Well Known Oauth Protected Resource Get"}}}}}}},"/oauth/register":{"post":{"tags":["oauth"],"summary":"Register Client","description":"Register a public OAuth client (PKCE, no secret).\n\nReturns the standard RFC 7591 client information response.","operationId":"register_client_oauth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/oauth/authorize/info":{"get":{"tags":["oauth"],"summary":"Authorize Info","description":"Return client display info for the consent screen (requires app session).","operationId":"authorize_info_v1_oauth_authorize_info_get","parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string","default":"mcp","title":"Scope"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Authorize Info V1 Oauth Authorize Info Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/oauth/authorize":{"post":{"tags":["oauth"],"summary":"Authorize","description":"Approve (or deny) an authorization request and mint a one-time code.\n\nValidates the client + redirect_uri + PKCE, then returns ``{redirect_to}``\nfor the SPA to navigate the browser back to the client. Invalid client or\nredirect_uri raises an error (never redirects — open-redirect guard).","operationId":"authorize_v1_oauth_authorize_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorizeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Authorize V1 Oauth Authorize Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/token":{"post":{"tags":["oauth"],"summary":"Token","description":"OAuth token endpoint: authorization_code (code+PKCE) and refresh_token grants.","operationId":"token_oauth_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/tokens":{"get":{"tags":["tokens"],"summary":"List Tokens","description":"List all API tokens for the current user (no hashes).\n\nArgs:\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    List of token dicts.","operationId":"list_tokens_v1_tokens_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Tokens V1 Tokens Get"}}}}}},"post":{"tags":["tokens"],"summary":"Create Token","description":"Create a new API token (session auth only; returns raw token once).\n\nArgs:\n    body: Contains ``label``.\n    user: Session-authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    Token dict with ``token`` field containing the raw value (shown once).","operationId":"create_token_v1_tokens_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenCreateRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Create Token V1 Tokens Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/tokens/{token_id}":{"delete":{"tags":["tokens"],"summary":"Revoke Token","description":"Revoke an API token by ID.\n\nArgs:\n    token_id: UUID of the token to revoke.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"status\": \"revoked\"}``\n\nRaises:\n    QuickhostError: ``not_found`` (404) if token doesn't exist or isn't owned.","operationId":"revoke_token_v1_tokens__token_id__delete","parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Token Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Revoke Token V1 Tokens  Token Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/workspace":{"get":{"tags":["workspace"],"summary":"Get Workspace","description":"Get a workspace with member count and usage.\n\nArgs:\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace. A workspace the caller doesn't belong to\n        yields 404.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    Workspace dict.","operationId":"get_workspace_v1_workspace_get","parameters":[{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Workspace V1 Workspace Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["workspace"],"summary":"Update Workspace","description":"Update the workspace name (admin only).\n\nArgs:\n    body: Contains new ``name``.\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    Updated workspace dict.","operationId":"update_workspace_v1_workspace_patch","parameters":[{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspacePatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Workspace V1 Workspace Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["deletion"],"summary":"Delete Workspace","description":"Schedule one workspace for erasure, leaving everyone's accounts alone.\n\nUnlike the account route this does not refuse over other members: an admin\ndeleting a workspace acts *as* the workspace, which is theirs to decide. The\nlive-link count in the response is what the confirmation in front of it\nshould have said.\n\nArgs:\n    body: Workspace and password confirmation.\n    user: The authenticated caller (must be an admin of it).\n    _csrf: CSRF guard.\n    session: Owner session.\n\nReturns:\n    What was scheduled, and when it happens.\n\nRaises:\n    QuickhostError: ``forbidden`` (403) for non-admins or a failed\n        re-authentication; ``not_found`` (404) when the row is gone.","operationId":"delete_workspace_v1_workspace_delete","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteWorkspaceRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Workspace V1 Workspace Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/workspace/members":{"get":{"tags":["workspace"],"summary":"List Members","description":"List members of a workspace (the folder-share picker source).\n\nArgs:\n    workspace: Optional workspace id; defaults to the caller's primary\n        (team) workspace. Non-members see an empty list (RLS-filtered).\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    List of member dicts.","operationId":"list_members_v1_workspace_members_get","parameters":[{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Members V1 Workspace Members Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/workspace/members/{target_user_id}":{"delete":{"tags":["workspace"],"summary":"Remove Member","description":"Remove a member from the workspace (admin only).\n\nCannot remove the last admin.\n\nArgs:\n    target_user_id: UUID of the member to remove.\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace.\n    user: Authenticated user (must be admin).\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"status\": \"removed\"}``\n\nRaises:\n    QuickhostError: Various error codes.","operationId":"remove_member_v1_workspace_members__target_user_id__delete","parameters":[{"name":"target_user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Target User Id"}},{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Remove Member V1 Workspace Members  Target User Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/workspace/invites":{"get":{"tags":["workspace"],"summary":"List Invites","description":"List pending workspace invites (admin only).\n\nAn ownership-granting invite is **never** listed here — see\n``_ORDINARY_INVITE``. It belongs to the admin panel that issued it.\n\nArgs:\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    List of invite dicts.","operationId":"list_invites_v1_workspace_invites_get","parameters":[{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response List Invites V1 Workspace Invites Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["workspace"],"summary":"Create Invite","description":"Create a workspace invite and send an invitation email (admin only).\n\n**At most one live invitation per address.** Re-inviting someone with an\ninvitation already outstanding replaces it rather than adding a second, so\nthe newest link is the only one that works and the row carries the role just\nchosen. Without this, three clicks made three live links and — because\npending invitations occupy seats (``services/seats.py``) — burned three\nseats of the members cap on one person. The admin panel's ``add_member``\nhas always replaced; these two surfaces now agree.\n\nAlready-members are refused outright: an invitation they can only\nno-op on is a confusing email and a seat held for nothing.\n\nArgs:\n    body: Contains ``email`` and ``role``.\n    background_tasks: For background email delivery.\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace.\n    user: Authenticated user (must be admin).\n    session: RLS-scoped DB session.\n\nReturns:\n    Invite dict.\n\nRaises:\n    QuickhostError: ``validation_error`` (422) on a bad role or an address\n        that already belongs to a member; ``conflict`` (409) when an\n        ownership-granting invite for that address is outstanding;\n        ``quota_exceeded`` (402) when the workspace has no seat left.","operationId":"create_invite_v1_workspace_invites_post","parameters":[{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Invite V1 Workspace Invites Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/workspace/invites/{invite_id}":{"delete":{"tags":["workspace"],"summary":"Cancel Invite","description":"Cancel a pending workspace invite (admin only).\n\nArgs:\n    invite_id: UUID of the invite to cancel.\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace.\n    user: Authenticated user (must be admin).\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"status\": \"cancelled\"}``","operationId":"cancel_invite_v1_workspace_invites__invite_id__delete","parameters":[{"name":"invite_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invite Id"}},{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Cancel Invite V1 Workspace Invites  Invite Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/workspace/invites/{invite_id}/resend":{"post":{"tags":["workspace"],"summary":"Resend Invite","description":"Replace a pending invite with a fresh one and re-send the email (admin only).\n\nOne-click recovery for expired invitations: the old invite row is\ndeleted, a new invite with the same email and role — but a fresh token\nand a new 7-day expiry — is created, and the invitation email goes out\nagain. Works on any un-accepted invite; the SPA offers it on expired\nrows.\n\nAn ownership-granting invite is not resendable here (404) — see\n``_ORDINARY_INVITE``. ``grants_ownership`` is still copied onto the\nreplacement, so the filter and the copy each independently prevent the\ndemotion bug rather than one relying on the other.\n\nArgs:\n    invite_id: UUID of the invite to replace.\n    background_tasks: For background email delivery.\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace.\n    user: Authenticated user (must be admin).\n    session: RLS-scoped DB session.\n\nReturns:\n    The new invite dict (same shape as create).\n\nRaises:\n    QuickhostError: ``not_found`` (404) if the invite doesn't exist in\n        this workspace or was already accepted.","operationId":"resend_invite_v1_workspace_invites__invite_id__resend_post","parameters":[{"name":"invite_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invite Id"}},{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resend Invite V1 Workspace Invites  Invite Id  Resend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/billing/plans":{"get":{"tags":["billing"],"summary":"List Plans","description":"What can actually be bought right now.\n\nDerived from which price ids are configured rather than from the plan\ndefinitions, so a half-finished Stripe setup shows the buyer three options\nand fails on two. An unconfigured plan simply is not offered.\n\nReturns:\n    ``{\"billing_enabled\": bool, \"plans\": {plan: [intervals]}}``","operationId":"list_plans_v1_billing_plans_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Plans V1 Billing Plans Get"}}}}}}},"/v1/billing/checkout":{"post":{"tags":["billing"],"summary":"Start Checkout","description":"Open a Stripe Checkout session and hand back its URL.\n\nGrants nothing. A workspace's plan changes when the webhook says Stripe\ncharged someone — not when a browser reaches a success URL, which can be\nreplayed, forged, or never visited at all.\n\nThe workspace is named explicitly in the body rather than inferred from the\nuser, because a person can belong to several and billing is per workspace.\nOnly an admin of *that* workspace may start it.\n\nArgs:\n    body: Workspace, plan and interval.\n    user: The authenticated caller.\n    _csrf: CSRF guard.\n    session: Owner session — this crosses into billing state.\n\nReturns:\n    ``{\"url\": …}`` to redirect the browser to.\n\nRaises:\n    QuickhostError: ``validation_error`` (503/422) when billing is\n        unconfigured or the plan is not purchasable; ``forbidden`` (403)\n        when the caller is not an admin of the workspace.","operationId":"start_checkout_v1_billing_checkout_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Start Checkout V1 Billing Checkout Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/billing/subscription":{"get":{"tags":["billing"],"summary":"Get Subscription","description":"What this workspace is on, and what it can do about it.\n\nOne request behind the whole billing panel, so the page does not have to\nassemble its own answer from the plan table, the trial clock and Stripe's\nstatus — three sources that disagree in ways a UI should never have to\nreconcile. ``state`` is the same :func:`plans.billing_state` the serving\npath enforces with, so what the customer is told matches what happens.\n\n``links_down_on`` is the deadline the trial emails quote. It exists here so\nthe page and the email cannot drift into naming different dates.\n\nArgs:\n    workspace: The workspace id (``?workspace=``).\n    user: The authenticated caller (must be an admin of it).\n    session: Owner session.\n\nReturns:\n    Plan, state, deadlines, and whether checkout and the portal are usable.","operationId":"get_subscription_v1_billing_subscription_get","parameters":[{"name":"workspace","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Subscription V1 Billing Subscription Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/billing/portal":{"post":{"tags":["billing"],"summary":"Open Portal","description":"Hand back a Stripe billing-portal URL for this workspace.\n\nGrants and changes nothing on our side, exactly like ``/checkout``: whatever\nthe customer does in the portal comes back as a webhook, which is the only\nthing that moves a plan.\n\nArgs:\n    body: The workspace to manage.\n    user: The authenticated caller (must be an admin of it).\n    _csrf: CSRF guard.\n    session: Owner session.\n\nReturns:\n    ``{\"url\": …}`` — single-use and short-lived, so it is fetched per click\n    rather than rendered into the page.\n\nRaises:\n    QuickhostError: ``validation_error`` (503) when billing is unconfigured\n        or the Stripe portal has no dashboard configuration; ``not_found``\n        (404) when the workspace has never been a Stripe customer.","operationId":"open_portal_v1_billing_portal_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Open Portal V1 Billing Portal Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/workspace/export":{"post":{"tags":["export"],"summary":"Request Export","description":"Queue an export of one workspace.\n\nReturns 202 and nothing else useful. The archive is built by\n``quickhost-export`` and the download link is emailed — see the module\ndocstring for why it is not in this response.\n\nArgs:\n    body: Workspace and password confirmation.\n    user: The authenticated caller.\n    _csrf: CSRF guard.\n    session: Owner session.\n\nReturns:\n    The queued row's public shape.\n\nRaises:\n    QuickhostError: ``forbidden`` (403) for non-admins or a failed\n        re-authentication; ``validation_error`` (429) when one is already\n        queued or one was taken today.","operationId":"request_export_v1_workspace_export_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Request Export V1 Workspace Export Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["export"],"summary":"Get Export Status","description":"Where the most recent export of this workspace got to.\n\nArgs:\n    workspace: The workspace id (``?workspace=``).\n    user: The authenticated caller (must be an admin of it).\n    session: Owner session.\n\nReturns:\n    The latest export's public shape, or ``{\"status\": null}``.","operationId":"get_export_status_v1_workspace_export_get","parameters":[{"name":"workspace","in":"query","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Export Status V1 Workspace Export Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/exports/{export_id}/download":{"get":{"tags":["export"],"summary":"Download Export","description":"Stream a finished export to whoever holds a valid token.\n\nNo session required, deliberately: the link is emailed and has to work in a\ndifferent browser from the one that asked for it. The token is signed, names\nthis specific export, and carries the same expiry as the row — so a leaked\nlink dies on schedule rather than whenever someone notices.\n\nEvery failure — bad token, wrong export, expired, swept, still building —\ncollapses to the same 404. Distinguishing them would let a holder of one\nvalid link enumerate the existence of others.\n\nArgs:\n    export_id: From the path.\n    token: The signed download token.\n    session: Owner session.\n\nReturns:\n    The archive, streamed.\n\nRaises:\n    QuickhostError: ``not_found`` (404) for anything that is not a live,\n        ready export matching this token.","operationId":"download_export_v1_exports__export_id__download_get","parameters":[{"name":"export_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Export Id"}},{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/deletion-impact":{"get":{"tags":["deletion"],"summary":"Get Deletion Impact","description":"What deleting this account would take down.\n\nRead-only. The confirmation dialog is built from this, and the ``DELETE``\nbelow decides from the same function — so the count someone is shown is the\ncount that actually happens.\n\nArgs:\n    user: The authenticated caller.\n    session: Owner session — this crosses workspaces the caller may not\n        administer.\n\nReturns:\n    The impact assessment.","operationId":"get_deletion_impact_v1_me_deletion_impact_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Deletion Impact V1 Me Deletion Impact Get"}}}}}}},"/v1/admin/deletions/{target_type}/{target_id}/restore":{"post":{"tags":["deletion"],"summary":"Restore Deletion","description":"Cancel a pending deletion. The only way back, and support-only.\n\nArgs:\n    target_type: ``user`` or ``workspace``.\n    target_id: Which one.\n    admin: The platform administrator acting.\n    _csrf: CSRF guard.\n    session: Owner session.\n\nReturns:\n    ``{\"restored\": bool}``.\n\nRaises:\n    QuickhostError: ``not_found`` (404) for an unknown type or id.","operationId":"restore_deletion_v1_admin_deletions__target_type___target_id__restore_post","parameters":[{"name":"target_type","in":"path","required":true,"schema":{"type":"string","title":"Target Type"}},{"name":"target_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Target Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Restore Deletion V1 Admin Deletions  Target Type   Target Id  Restore Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/waitlist":{"post":{"tags":["waitlist"],"summary":"Join Waitlist","description":"Record an address for launch notification.\n\nArgs:\n    request: Carries a JSON or form-encoded body with ``email`` and\n        optionally ``name`` and ``note``.\n\nReturns:\n    JSON ``{\"ok\": true}`` for fetch callers, or a 303 back to the marketing\n    site for plain form posts. The response is identical whether or not the\n    address was new — see the module docstring.","operationId":"join_waitlist_v1_waitlist_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/admin/overview":{"get":{"tags":["admin"],"summary":"Get Overview","description":"Return system-wide totals.\n\nArgs:\n    _admin: The authenticated admin (gate only).\n    session: Owner/system session.\n\nReturns:\n    Totals for workspaces, users, projects, storage, and admins.","operationId":"get_overview_v1_admin_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Overview V1 Admin Overview Get"}}}}}}},"/v1/admin/workspaces":{"get":{"tags":["admin"],"summary":"List Workspaces","description":"List every workspace with member/project/storage usage, owner, and quotas.\n\nUses set-based queries merged in Python by ``workspace_id`` (no N+1),\nincluding one batch lookup of owner users and one of pending\nownership-granting invites for interim-held customer workspaces.\n\nArgs:\n    _admin: The authenticated admin (gate only).\n    session: Owner/system session.\n\nReturns:\n    ``{\"workspaces\": [row, ...]}`` ordered by creation time.","operationId":"list_workspaces_v1_admin_workspaces_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Workspaces V1 Admin Workspaces Get"}}}}}},"post":{"tags":["admin"],"summary":"Create Workspace","description":"Create a workspace, in one call.\n\nTwo flows, selected by ``owner_email``:\n\n- **Classic** (no ``owner_email``): the calling admin becomes the owner + an\n  admin member — an admin's own workspace.\n- **Customer** (``owner_email`` given): the named user owns it and the admin\n  is neither owner nor member. An existing account becomes owner + admin\n  member immediately; a brand-new email leaves the workspace owned by the\n  creating admin *on an interim basis* and gets an ownership-granting\n  invite (magic link) — ownership transfers on accept. The admin still\n  supports it through this cross-tenant panel.\n\nQuotas are applied as given (a number, ``null`` for unlimited, or omitted to\nkeep the standard default). Extra ``members`` join immediately if they have\nan account, else they're invited.\n\nArgs:\n    payload: name + optional owner_email + optional quotas + optional members.\n    background_tasks: For background invite-email delivery.\n    admin: The authenticated admin (the creator).\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The created workspace row (admin-table shape) plus ``invited_emails``\n    (member invites sent) and ``owner_pending_email`` (the owner-invite\n    address, or ``null`` when the owner already existed / classic flow),\n    status 201.\n\nRaises:\n    QuickhostError: ``validation_error`` (422) empty name / bad role /\n        negative quota.","operationId":"create_workspace_v1_admin_workspaces_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Workspace V1 Admin Workspaces Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/workspaces/{workspace_id}/quota":{"patch":{"tags":["admin"],"summary":"Update Workspace Quota","description":"Override a workspace's quotas. A ``null`` value means unlimited.\n\n``quota_members`` is here because the members cap is the one quota the\npricing page promises to *sell past*: Studio and Agency are published as\n\"5 / 15 members, flat — then $5 each\". Seat billing is unbuilt, so until it\nexists this is how an operator honours that promise — raise the cap by hand\nfor a workspace that has paid for extra seats. Without it the only way to\ngrant a sixth Studio seat was an UPDATE against production.\n\nArgs:\n    workspace_id: The workspace to update.\n    payload: New quota values. An explicit ``None`` = unlimited; an omitted\n        field is left as it is.\n    _admin: The authenticated admin (gate only).\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The updated workspace row (admin-table shape).\n\nRaises:\n    QuickhostError: ``validation_error`` (422) on negative values;\n        ``not_found`` (404) if the workspace doesn't exist.","operationId":"update_workspace_quota_v1_admin_workspaces__workspace_id__quota_patch","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Workspace Quota V1 Admin Workspaces  Workspace Id  Quota Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/admins":{"get":{"tags":["admin"],"summary":"List Admins","description":"List all global administrators.\n\nArgs:\n    _admin: The authenticated admin (gate only).\n    session: Owner/system session.\n\nReturns:\n    ``{\"admins\": [{\"id\", \"email\", \"name\"}, ...]}`` ordered by email.","operationId":"list_admins_v1_admin_admins_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Admins V1 Admin Admins Get"}}}}}},"post":{"tags":["admin"],"summary":"Grant Admin","description":"Grant global admin to the user with *email* (idempotent).\n\nArgs:\n    payload: ``{\"email\": ...}`` (matched case-insensitively via CITEXT).\n    _admin: The authenticated admin (gate only).\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The granted user row.\n\nRaises:\n    QuickhostError: ``not_found`` (404) if no user has that email.","operationId":"grant_admin_v1_admin_admins_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantAdmin"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Grant Admin V1 Admin Admins Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/admins/{user_id}":{"delete":{"tags":["admin"],"summary":"Revoke Admin","description":"Revoke global admin from *user_id*.\n\nGuard: a caller cannot revoke their *own* access. This single rule also\nguarantees at least one admin always remains — the only way to reach zero\nadmins is to revoke the final one, who is necessarily the caller (any other\nadmin target implies a second admin still exists), so self-revoke blocks it.\n\nArgs:\n    user_id: The admin to demote.\n    admin: The authenticated caller.\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    ``{\"status\": \"ok\"}``.\n\nRaises:\n    QuickhostError: ``conflict`` (409) on self-revoke; ``not_found`` (404)\n        if the target isn't an admin.","operationId":"revoke_admin_v1_admin_admins__user_id__delete","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Revoke Admin V1 Admin Admins  User Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/workspaces/{workspace_id}":{"patch":{"tags":["admin"],"summary":"Rename Workspace","description":"Rename a workspace.\n\nArgs:\n    workspace_id: The workspace to rename.\n    payload: ``{\"name\": ...}``.\n    _admin: The authenticated admin (gate only).\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The updated workspace row.\n\nRaises:\n    QuickhostError: ``validation_error`` (422) empty name; ``not_found`` (404).","operationId":"rename_workspace_v1_admin_workspaces__workspace_id__patch","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRename"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Rename Workspace V1 Admin Workspaces  Workspace Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/workspaces/{workspace_id}/members":{"post":{"tags":["admin"],"summary":"Add Member","description":"Add a member by email, invite a new one, or update an existing role.\n\nIf the email belongs to an existing account, they're added (or their role is\nupdated) immediately and the response is tagged ``status=\"member\"``. If no\naccount exists yet, a pending invite is created and an invitation email is\nsent (``status=\"invited\"``); the invitee joins on acceptance (invite link or\na matching Google sign-in).\n\nArgs:\n    workspace_id: The workspace.\n    payload: ``{\"email\": ..., \"role\": \"admin\"|\"member\"}``.\n    background_tasks: For background invite-email delivery.\n    _admin: The authenticated admin (gate only).\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The member row (``status=\"member\"``) or the invite row\n    (``status=\"invited\"``).\n\nRaises:\n    QuickhostError: ``validation_error`` (422) bad role; ``not_found`` (404)\n        unknown workspace.","operationId":"add_member_v1_admin_workspaces__workspace_id__members_post","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberAdd"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Add Member V1 Admin Workspaces  Workspace Id  Members Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/workspaces/{workspace_id}/members/{user_id}":{"delete":{"tags":["admin"],"summary":"Remove Member","description":"Remove a member from a workspace (cannot remove its last admin).\n\nArgs:\n    workspace_id: The workspace.\n    user_id: The member to remove.\n    _admin: The authenticated admin (gate only).\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    ``{\"status\": \"removed\"}``.\n\nRaises:\n    QuickhostError: ``not_found`` (404) if not a member; ``conflict`` (409)\n        if it would remove the workspace's last admin.","operationId":"remove_member_v1_admin_workspaces__workspace_id__members__user_id__delete","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}},{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Remove Member V1 Admin Workspaces  Workspace Id  Members  User Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/invites":{"get":{"tags":["admin"],"summary":"List All Invites","description":"Every un-accepted invite in the system, newest first.\n\nThe per-workspace list answers \"who is pending here?\", which is only useful\nonce you already know where to look. This answers the question support\nactually gets — \"I never got my invite\" — from someone who does not mention\na workspace and may not know its name. Without it, finding one invitation\nmeant opening workspaces one at a time.\n\nExpired invites are included, and are most of the point: an expired invite\nis precisely the row this panel exists to act on.\n\nArgs:\n    _admin: The authenticated admin (gate only).\n    session: Owner/system session.\n\nReturns:\n    ``{\"invites\": [...]}`` — ``_invited_row`` shape plus ``workspace_id``,\n    ``workspace_name`` and ``grants_ownership``, since a row here has to say\n    which tenant it belongs to and whether re-sending it hands over a\n    workspace.","operationId":"list_all_invites_v1_admin_invites_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"type":"object","title":"Response List All Invites V1 Admin Invites Get"}}}}}}},"/v1/admin/workspaces/{workspace_id}/invites":{"get":{"tags":["admin"],"summary":"List Workspace Invites","description":"List a workspace's un-accepted invites, newest first.\n\nEvery pending invite is returned, expired ones included, because an expired\ninvite is precisely the row an admin opens this panel to act on — it is what\n``resend`` exists for. Accepted invites are omitted: that invitee is a member\nnow and belongs in the member list, not here.\n\nArgs:\n    workspace_id: The workspace.\n    _admin: The authenticated admin (gate only).\n    session: Owner/system session.\n\nReturns:\n    ``{\"invites\": [...]}`` — each row in ``_invited_row`` shape, carrying the\n    ``invite_url`` the panel copies to the clipboard.\n\nRaises:\n    QuickhostError: ``not_found`` (404) for an unknown workspace.","operationId":"list_workspace_invites_v1_admin_workspaces__workspace_id__invites_get","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","additionalProperties":true}},"title":"Response List Workspace Invites V1 Admin Workspaces  Workspace Id  Invites Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/workspaces/{workspace_id}/invites/{invite_id}/resend":{"post":{"tags":["admin"],"summary":"Resend Workspace Invite","description":"Replace a pending invite with a fresh one and re-send its email.\n\nRecovery for an invitation that expired or never arrived: the old row is\nreplaced by one with the same email and role but a new token and a new\n7-day expiry, and the invitation email goes out again. The old link stops\nworking, which is the point — one live link per invitee.\n\n``grants_ownership`` is carried across deliberately. Re-sending the\nownership-granting invite of an interim-held customer workspace must not\nquietly demote it to an ordinary membership invite, or the workspace would\nstay stuck with its interim owner after the invitee accepts. The workspace\nrouter's resend used to drop it, which is what migration 0033 exists to\nmake unrepeatable.\n\nArgs:\n    workspace_id: The workspace.\n    invite_id: The invite to replace.\n    background_tasks: For background invite-email delivery.\n    admin: The authenticated admin (recorded as the new invite's creator).\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The new invite row (``_invited_row`` shape), with a fresh ``invite_url``.\n\nRaises:\n    QuickhostError: ``not_found`` (404) for an unknown workspace, an invite\n        that is not in it, or one that was already accepted.","operationId":"resend_workspace_invite_v1_admin_workspaces__workspace_id__invites__invite_id__resend_post","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}},{"name":"invite_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invite Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resend Workspace Invite V1 Admin Workspaces  Workspace Id  Invites  Invite Id  Resend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/workspaces/{workspace_id}/invites/{invite_id}":{"delete":{"tags":["admin"],"summary":"Cancel Workspace Invite","description":"Withdraw a pending invitation. Its link stops working immediately.\n\n**An ownership-granting invite cannot be cancelled**, and that refusal is\nload-bearing rather than cautious. Nothing but ``create_workspace`` issues\none, so cancelling it destroys the only path by which an interim-held\ncustomer workspace ever reaches its real owner — the workspace would be\nstuck with the provisioning admin forever, which is the exact class of\nunrecoverable state migration 0033 exists to remove. Re-send it instead\n(that rotates the token and the expiry, which is what \"cancel and re-issue\"\nwas reaching for anyway). Note this leaves no way to re-point an owner\ninvite at a *corrected* address — a real gap, and a worse one to fix by\nallowing the dead end.\n\nArgs:\n    workspace_id: The workspace the invite belongs to.\n    invite_id: The invite to withdraw.\n    _admin: The authenticated admin (gate only).\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    ``{\"status\": \"cancelled\"}``.\n\nRaises:\n    QuickhostError: ``not_found`` (404) for an unknown workspace, an invite\n        not in it, or one already accepted; ``conflict`` (409) for an\n        ownership-granting invite.","operationId":"cancel_workspace_invite_v1_admin_workspaces__workspace_id__invites__invite_id__delete","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}},{"name":"invite_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invite Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"},"title":"Response Cancel Workspace Invite V1 Admin Workspaces  Workspace Id  Invites  Invite Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/projects/{slug}/suspend":{"post":{"tags":["admin"],"summary":"Suspend Project","description":"Take a single project offline immediately.\n\nArgs:\n    slug: The project to suspend.\n    payload: Carries the required reason.\n    admin: The authenticated admin, recorded as the actor.\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session — required, since the 0024 trigger rejects\n        suspension writes from the app role.\n\nReturns:\n    The project's suspension state.\n\nRaises:\n    QuickhostError: ``not_found`` (404) if no such project.","operationId":"suspend_project_v1_admin_projects__slug__suspend_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuspendPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Suspend Project V1 Admin Projects  Slug  Suspend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/projects/{slug}/unsuspend":{"post":{"tags":["admin"],"summary":"Unsuspend Project","description":"Restore a suspended project. No republish required.\n\nArgs:\n    slug: The project to restore.\n    admin: The authenticated admin, recorded as the actor.\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The project's suspension state.\n\nRaises:\n    QuickhostError: ``not_found`` (404) if no such project.","operationId":"unsuspend_project_v1_admin_projects__slug__unsuspend_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Unsuspend Project V1 Admin Projects  Slug  Unsuspend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/workspaces/{workspace_id}/suspend":{"post":{"tags":["admin"],"summary":"Suspend Workspace","description":"Take every project in a workspace offline immediately.\n\nArgs:\n    workspace_id: The workspace to suspend.\n    payload: Carries the required reason.\n    admin: The authenticated admin, recorded as the actor.\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The workspace's suspension state and how many projects it covers.\n\nRaises:\n    QuickhostError: ``not_found`` (404) if no such workspace.","operationId":"suspend_workspace_v1_admin_workspaces__workspace_id__suspend_post","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuspendPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Suspend Workspace V1 Admin Workspaces  Workspace Id  Suspend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/workspaces/{workspace_id}/unsuspend":{"post":{"tags":["admin"],"summary":"Unsuspend Workspace","description":"Restore a suspended workspace.\n\nProjects suspended on their own merits stay suspended — workspace\nsuspension never wrote to their rows, so lifting it cannot silently\nun-suspend them.\n\nArgs:\n    workspace_id: The workspace to restore.\n    admin: The authenticated admin, recorded as the actor.\n    _csrf: CSRF guard for session auth.\n    session: Owner/system session.\n\nReturns:\n    The workspace's suspension state and how many projects it covers.\n\nRaises:\n    QuickhostError: ``not_found`` (404) if no such workspace.","operationId":"unsuspend_workspace_v1_admin_workspaces__workspace_id__unsuspend_post","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Workspace Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Unsuspend Workspace V1 Admin Workspaces  Workspace Id  Unsuspend Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/folders":{"get":{"tags":["folders"],"summary":"List Folders","description":"List every folder the caller can see.","operationId":"list_folders_v1_folders_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Folders V1 Folders Get"}}}}}},"post":{"tags":["folders"],"summary":"Create Folder","description":"Create a folder owned by the caller in one of their workspaces.","operationId":"create_folder_v1_folders_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Folder V1 Folders Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/folders/{folder_id}":{"patch":{"tags":["folders"],"summary":"Update Folder","description":"Rename, re-share, or reorder a folder (owner or workspace admin).","operationId":"update_folder_v1_folders__folder_id__patch","parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderPatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Folder V1 Folders  Folder Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["folders"],"summary":"Delete Folder","description":"Delete an empty folder (owner or workspace admin).","operationId":"delete_folder_v1_folders__folder_id__delete","parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}":{"put":{"tags":["projects"],"summary":"Put Project","description":"Create or replace a project via a single raw-body request.\n\nThe Content-Type header determines the kind:\n- ``text/html`` → site (single index.html)\n- ``text/markdown`` → markdown\n- ``text/csv`` → data\n- ``application/json`` → data\n- ``application/pdf`` → pdf\n- ``application/zip`` → site (zip extraction)\n- ``image/*`` → image\n- ``video/*`` → video\n- ``text/plain`` → text\n\nCreates the project if it doesn't exist (query params: visibility,\nexpires_in_days, title), otherwise replaces the content.\n\nArgs:\n    slug: The project slug.\n    request: The FastAPI request containing the raw body.\n    visibility: Visibility for new projects (query param).\n    expires_in_days: Expiry for new projects in days (query param).\n    title: Title for new projects (query param).\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"slug\", \"url\", \"kind\", \"version\"}`` with 200 or 201.","operationId":"put_project_v1_projects__slug__put","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"visibility","in":"query","required":false,"schema":{"type":"string","default":"private","title":"Visibility"}},{"name":"expires_in_days","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires In Days"}},{"name":"expires_at","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"}},{"name":"title","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"}},{"name":"folder","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder"}},{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Project V1 Projects  Slug  Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["projects"],"summary":"Get Project","description":"Get project detail including version list.\n\nArgs:\n    slug: The project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    Full project detail dict.","operationId":"get_project_v1_projects__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Project V1 Projects  Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["projects"],"summary":"Update Project","description":"Update project metadata, rename slug, manage visibility/password.\n\nSlug rename moves the storage directory atomically.\nPassword set/change increments ``password_generation``.\nExplicit ``password: null`` clears the password when leaving password mode.\n\nSwitching *into* ``visibility=\"password\"`` without supplying a password\ngenerates one, so the owner always has something to read back and share.\nOmit ``password`` entirely to leave the existing one alone; an explicit\n``password: null`` still clears it, even in password mode.\n\nArgs:\n    slug: The current project slug.\n    body: Partial update payload.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    Updated project detail dict.","operationId":"update_project_v1_projects__slug__patch","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectPatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Project V1 Projects  Slug  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["projects"],"summary":"Delete Project","description":"Soft-delete a project and remove its storage.\n\nArgs:\n    slug: The project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.","operationId":"delete_project_v1_projects__slug__delete","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/files":{"post":{"tags":["projects"],"summary":"Add Files","description":"Add or replace files in a new version.\n\nFor a ``site`` project this builds the new version from the current version's\nfiles plus the uploads (a superset). Non-site projects replace content\nwholesale, and their new ``kind``/``primary_filename`` are derived from the\nupload exactly as the initial publish does.\n\nArgs:\n    slug: The project slug.\n    request: The FastAPI request.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"slug\", \"url\", \"kind\", \"version\"}``.","operationId":"add_files_v1_projects__slug__files_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Add Files V1 Projects  Slug  Files Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/download":{"get":{"tags":["projects"],"summary":"Download Project Zip","description":"Download all files of the current version as a zip (attachment).\n\nStreams the current version's objects from storage into a temp zip, then\nserves it and deletes it.\n\nArgs:\n    slug: The project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    A zip ``FileResponse`` with ``Content-Disposition: attachment``.","operationId":"download_project_zip_v1_projects__slug__download_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/download/{path}":{"get":{"tags":["projects"],"summary":"Download Project File","description":"Download a single file from the current version (attachment).\n\nStreams the object from storage with a ``Content-Disposition: attachment``\nheader.\n\nArgs:\n    slug: The project slug.\n    path: File path relative to the current version root.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    A ``StreamingResponse`` with ``Content-Disposition: attachment``.","operationId":"download_project_file_v1_projects__slug__download__path__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/files/{path}":{"delete":{"tags":["projects"],"summary":"Delete File","description":"Remove a file from a site project by creating a new version without it.\n\nSite kind only.  Builds a new version as a copy of the current version\nminus the named file, runs the publish pipeline, and swaps the symlink.\nRefuses to remove the last file or a sole ``index.html``.\n\nArgs:\n    slug: The project slug.\n    path: Relative path of the file to delete within the current version.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"slug\", \"version\"}`` on success.","operationId":"delete_file_v1_projects__slug__files__path__delete","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"path","in":"path","required":true,"schema":{"type":"string","title":"Path"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete File V1 Projects  Slug  Files  Path  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/available":{"get":{"tags":["projects"],"summary":"Slug Available","description":"Report whether *slug* can be claimed, across the **whole** slug namespace.\n\nThe publish dialog used to infer this from a 404 on ``GET /v1/projects/{slug}``,\nwhich answers a different question: \"is there a project *you can see* here?\" Slugs\nare hostname labels and unique globally, so a slug held by another user's folder —\nincluding a teammate in your own workspace — read as available and then failed the\nINSERT. This endpoint asks the same global question the constraint does, so the\ngreen check and the publish result agree.\n\nAuthenticated (the dialog is), so it is not an open slug-enumeration oracle; what it\nreveals to a signed-in user is in any case already public via ``<slug>.<base_domain>``.\n\nArgs:\n    slug: The candidate slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"slug\", \"available\", \"reason\"}`` — ``reason`` is ``\"invalid\"``, ``\"reserved\"``,\n    ``\"taken\"``, or ``None`` when available.","operationId":"slug_available_v1_projects__slug__available_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Slug Available V1 Projects  Slug  Available Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/ready":{"get":{"tags":["projects"],"summary":"Project Ready","description":"Report whether a published project's hosted URL actually works yet.\n\nThe publish UI polls this so it reveals the link/QR/Open button only once\nthe URL is usable. The check is server-side, so the browser never queries\nthe project URL early and can't negative-cache a miss — which would\notherwise delay the real click.\n\nReadiness is decided by a data-plane probe: an HTTPS request to the hosted\nURL must actually get a response. \"A record exists at Cloudflare\" is not the\nsame as \"a public resolver can resolve it\", so the probe is what avoids the\n\"DNS error on first click\" race during propagation. With the zone-wide\n``*.<base_domain>`` wildcard serving production the name resolves before the\nproject row is even committed, so this normally returns True on the first\npoll — see :func:`quickhost_api.services.cloudflare_dns.is_ready`.\n\nArgs:\n    slug: The project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"ready\": bool}``. Always ready when DNS management is off (dev/test,\n    where ``*.<base_domain>`` resolves locally with no provisioning step).","operationId":"project_ready_v1_projects__slug__ready_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"boolean"},"title":"Response Project Ready V1 Projects  Slug  Ready Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/link":{"get":{"tags":["projects"],"summary":"Project Link","description":"Return the project's hosted link, **200 only once it's confirmed live**.\n\nThe async counterpart to publishing: ``POST``/``PUT`` create the project and\nschedule its DNS, then return immediately; an API/MCP consumer polls this\nendpoint until it gets a 200, at which point the URL resolves and the page\nloads (DNS record exists *and* the hosted URL responds). While the record is\nstill propagating this returns **202** with ``ready: false`` so the caller\nkeeps polling. Mirrors what the UI does, without blocking a worker.\n\nArgs:\n    slug: The project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``200 {\"url\", \"ready\": true}`` when live, else\n    ``202 {\"url\", \"ready\": false, \"status\": \"provisioning\"}``.","operationId":"project_link_v1_projects__slug__link_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/og-image":{"post":{"tags":["projects"],"summary":"Upload Og Image","description":"Upload a custom OG image for the project.\n\nAccepts PNG, JPEG, WebP, or GIF files up to 2 MB.  The image is stored in\nthe project's ``meta/`` directory and the viewer index.html is re-rendered\nin place to include the absolute ``og:image`` URL.\n\nArgs:\n    slug: The project slug.\n    request: The FastAPI request (multipart ``file`` field).\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"og_image_url\": \"<absolute URL>\"}`` on success.","operationId":"upload_og_image_v1_projects__slug__og_image_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Upload Og Image V1 Projects  Slug  Og Image Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["projects"],"summary":"Delete Og Image","description":"Remove the custom OG image for the project.\n\nDeletes the stored asset, clears ``og_image_path``, and re-renders the\nviewer in place (removing the ``og:image`` meta tag).\n\nArgs:\n    slug: The project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.","operationId":"delete_og_image_v1_projects__slug__og_image_delete","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/favicon":{"post":{"tags":["projects"],"summary":"Upload Favicon","description":"Upload a custom favicon for the project.\n\nAccepts PNG, ICO, or SVG files up to 256 KB.  The favicon is stored in the\nproject's ``meta/`` directory.  ``/_qh/favicon.ico`` serves it when present.\n\nArgs:\n    slug: The project slug.\n    request: The FastAPI request (multipart ``file`` field).\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"favicon_url\": \"<absolute URL>\"}`` on success.","operationId":"upload_favicon_v1_projects__slug__favicon_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Upload Favicon V1 Projects  Slug  Favicon Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["projects"],"summary":"Delete Favicon","description":"Remove the custom favicon, reverting to the QuickHost brand default.\n\nArgs:\n    slug: The project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.","operationId":"delete_favicon_v1_projects__slug__favicon_delete","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects":{"get":{"tags":["projects"],"summary":"List Projects","description":"List every visible folder, project, and workspace (with usage).\n\nRLS scopes all three to what the caller may access — folders via\n``fn_my_folder_ids``, projects via their folder, workspaces via membership.\n\nArgs:\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"folders\", \"projects\", \"workspaces\", \"usage\"}``.","operationId":"list_projects_v1_projects_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Projects V1 Projects Get"}}}}}},"post":{"tags":["projects"],"summary":"Create Project","description":"Create a new project from multipart file upload.\n\nForm fields: slug (optional → random), title, visibility (default private),\npassword (for visibility=password), expires_at / expires_in_days, noindex\n(**defaults to true** — send ``noindex=false`` to allow search indexing),\ncomments_enabled (defaults to false), comment_emails (project-scope comment\nallowlist — repeat the field per address, or send one separated string).\n\n``comment_emails`` is stored regardless of ``comments_enabled`` — it is a\nseparate setting that simply has no effect until comments are on, so a\ncaller that turns comments on later keeps the list it already supplied.\nTurning comments on without it is still valid: workspace members can\nalways comment, and folder/workspace defaults still apply\n(``comment_allowlist_inherit`` defaults to true).\n\nArgs:\n    request: The FastAPI request (used to parse multipart form data).\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"slug\", \"url\", \"kind\", \"version\"}`` with status 201.","operationId":"create_project_v1_projects_post","parameters":[{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Project V1 Projects Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/share-links":{"get":{"tags":["share_links"],"summary":"List Share Links","description":"List all share links for a project (token hidden).\n\nArgs:\n    slug: Project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    Dict with ``share_links`` list.","operationId":"list_share_links_v1_projects__slug__share_links_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Share Links V1 Projects  Slug  Share Links Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["share_links"],"summary":"Create Share Link","description":"Create a new share link.  Raw token returned ONCE.\n\nArgs:\n    slug: Project slug.\n    body: Create request payload.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    Dict with share link data including raw ``token`` and ``share_url``.","operationId":"create_share_link_v1_projects__slug__share_links_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareLinkCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Create Share Link V1 Projects  Slug  Share Links Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/share-links/{link_id}":{"patch":{"tags":["share_links"],"summary":"Update Share Link","description":"Update a share link's label, expiry, or notification setting.\n\nRLS ensures the link belongs to a project in the user's workspace.\n\nArgs:\n    link_id: The share link UUID.\n    body: Partial update payload.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    Updated share link dict.","operationId":"update_share_link_v1_share_links__link_id__patch","parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Link Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareLinkPatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Update Share Link V1 Share Links  Link Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["share_links"],"summary":"Revoke Share Link","description":"Revoke a share link by setting revoked_at to now.\n\nAlso touches project.updated_at so the authz cache sweeps the change.\n\nArgs:\n    link_id: The share link UUID.\n    user: Authenticated user.\n    session: RLS-scoped DB session.","operationId":"revoke_share_link_v1_share_links__link_id__delete","parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Link Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/analytics":{"get":{"tags":["analytics"],"summary":"Get Project Analytics","description":"Return aggregated analytics for a project.\n\nArgs:\n    slug: Project URL slug.\n    days: Number of calendar days to include in the window (1–90, default 30).\n    user: Authenticated user (injected by dependency).\n    session: RLS-scoped database session (injected by dependency).\n\nReturns:\n    Analytics payload with totals, daily series, and share-link summaries.\n\nRaises:\n    QuickhostError: 404 if the project is not found or not in caller's workspace.","operationId":"get_project_analytics_v1_projects__slug__analytics_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Project Analytics V1 Projects  Slug  Analytics Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/authz":{"get":{"tags":["internal"],"summary":"Authz","description":"nginx auth_request target.\n\nReads X-Original-Host (set by nginx; falls back to Host header).\nReads X-Gate-Cookie (nginx passes ``$cookie_qh_gate``; falls back to\nCookie header so ASGI test clients work without extra headers).\n\nReturns:\n    204 → serve; 401 → gate needed; 403 → blocked (404/410/denied).\n\nNo authentication required on this endpoint.  Public access is blocked\nby the nginx ``api.qh.localhost`` vhost.","operationId":"authz_internal_authz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/projects/{slug}/comments":{"get":{"tags":["comments"],"summary":"List Comments","description":"List non-deleted, threaded comments for a project.\n\nArgs:\n    slug: Project slug.\n    status: Filter — ``\"open\"``, ``\"resolved\"``, or ``\"all\"`` (default).\n        Applies to top-level comments only; matching replies always ride\n        along with their root.\n    file_path: Optional relpath filter, applied to top-level comments only.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"comments\": [OwnerNode, ...], \"current_version_n\": int | None}`` —\n    comments newest-first by created_at; ``current_version_n`` is the\n    project's live ``ProjectVersion.n`` (``None`` if it has no committed\n    version yet), letting the dashboard identify needs-review comments\n    (``anchor_status == \"orphaned\"`` OR ``created_version_n <\n    current_version_n``) without a separate per-comment server field.","operationId":"list_comments_v1_projects__slug__comments_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Status"}},{"name":"file_path","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Comments V1 Projects  Slug  Comments Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/comments/mentions":{"get":{"tags":["comments"],"summary":"List Mention Candidates","description":"List who may be @-mentioned on a project, for the dashboard reply box.\n\nUnlike the viewer route this returns addresses: the dashboard already shows\nevery commenter's email to members who can see the project.\n\nArgs:\n    slug: Project slug.\n    user: Authenticated user (excluded from the list).\n    session: RLS-scoped DB session — the project must be visible.\n\nReturns:\n    ``{\"candidates\": [{\"handle\", \"name\", \"email\"}]}`` sorted by name.\n\nRaises:\n    QuickhostError: ``not_found`` (404) if the project isn't visible.","operationId":"list_mention_candidates_v1_projects__slug__comments_mentions_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Mention Candidates V1 Projects  Slug  Comments Mentions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/comments/{comment_id}/resolve":{"post":{"tags":["comments"],"summary":"Resolve Comment","description":"Resolve a comment (idempotent); resolving a root also resolves its replies.\n\nArgs:\n    comment_id: UUID of the comment.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"comment\": OwnerNode}`` with the updated comment and its (now also\n    resolved, if it's a root) replies.","operationId":"resolve_comment_v1_comments__comment_id__resolve_post","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Comment Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Resolve Comment V1 Comments  Comment Id  Resolve Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/comments/{comment_id}/replies":{"post":{"tags":["comments"],"summary":"Reply To Comment","description":"Create an owner-authored reply to a comment (thread root or another reply).\n\nEmails the thread's author and anyone the reply tags — after committing.\n\nArgs:\n    comment_id: UUID of the comment being replied to.\n    body: Reply payload.\n    background_tasks: FastAPI background tasks for notification emails.\n    user: Authenticated (verified) user.\n    session: RLS-scoped DB session.\n\nReturns:\n    201 ``{\"comment\": OwnerNode}`` for the new reply.","operationId":"reply_to_comment_v1_comments__comment_id__replies_post","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Comment Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplyCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reply To Comment V1 Comments  Comment Id  Replies Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/comments/{comment_id}":{"patch":{"tags":["comments"],"summary":"Edit Comment","description":"Edit the body of the caller's own comment.\n\nAuthor-only and window-bounded — being able to *see* a comment (RLS) is not\nenough to rewrite it. See the module docstring for why this bar differs\nfrom ``DELETE``'s. People the edit newly tags are emailed, after committing.\n\nArgs:\n    comment_id: UUID of the comment.\n    body: Replacement body payload, optionally with mention handles.\n    background_tasks: FastAPI background tasks for mention emails.\n    user: Authenticated user, who must be the comment's author.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"comment\": OwnerNode}`` with the updated comment and its replies.","operationId":"edit_comment_v1_comments__comment_id__patch","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Comment Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentEditRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Edit Comment V1 Comments  Comment Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["comments"],"summary":"Delete Comment","description":"Soft-delete a comment; deleting a root also soft-deletes its replies.\n\nTwo bars on one route, selected by the caller:\n\n* Default (``author_only=false``) — **owner moderation**. Any RLS-visible\n  member may remove any comment on a project they can see. This is what\n  the dashboard's Delete button does, and it is unchanged.\n* ``author_only=true`` — **authorship**. Only the comment's own author,\n  and only inside the edit window. The agent surfaces (MCP\n  ``delete_comment``, ``qh comments --delete``) opt in to this so an agent\n  can retract its own reply but can never erase a client's feedback.\n\nThe flag is opt-in rather than the default so an existing caller can't\nsilently lose the moderation power it already has.\n\nArgs:\n    comment_id: UUID of the comment.\n    author_only: Apply the authorship bar instead of owner moderation.\n    user: Authenticated user.\n    session: RLS-scoped DB session.","operationId":"delete_comment_v1_comments__comment_id__delete","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Comment Id"}},{"name":"author_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Author Only"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/projects/{slug}/comment-allowlist":{"get":{"tags":["comment-allowlist"],"summary":"Get Project Allowlist","description":"Return the project's comment-access mode and its allowlist (own + inherited).\n\nArgs:\n    slug: Project slug.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"comment_access\", \"comment_allowlist_inherit\", \"emails\", \"inherited_emails\"}``.","operationId":"get_project_allowlist_v1_projects__slug__comment_allowlist_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Project Allowlist V1 Projects  Slug  Comment Allowlist Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["comment-allowlist"],"summary":"Put Project Allowlist","description":"Replace the project's own allowlist entries and comment-access settings.\n\nReflects the new ``comment_access`` into the authz cache immediately (like\n``comments_enabled``) so ``GET /_qh/comments/config`` doesn't lag the\nbackground sweep.\n\nArgs:\n    slug: Project slug.\n    body: New ``comment_access``, ``comment_allowlist_inherit``, and the\n        full replacement ``emails`` list for this project's own scope.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    The same shape as the GET.\n\nRaises:\n    QuickhostError: ``validation_error`` (422) for a bad ``comment_access``\n        or an invalid/oversized email list.","operationId":"put_project_allowlist_v1_projects__slug__comment_allowlist_put","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectAllowlistPutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Project Allowlist V1 Projects  Slug  Comment Allowlist Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/folders/{folder_id}/comment-allowlist":{"get":{"tags":["comment-allowlist"],"summary":"Get Folder Allowlist","description":"Return the folder's default comment allowlist (owner or workspace admin only).\n\nArgs:\n    folder_id: Folder UUID string.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"emails\": [...]}``.","operationId":"get_folder_allowlist_v1_folders__folder_id__comment_allowlist_get","parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Folder Allowlist V1 Folders  Folder Id  Comment Allowlist Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["comment-allowlist"],"summary":"Put Folder Allowlist","description":"Replace the folder's default comment allowlist (owner or workspace admin only).\n\nArgs:\n    folder_id: Folder UUID string.\n    body: The full replacement ``emails`` list for this folder's scope.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"emails\": [...]}``.","operationId":"put_folder_allowlist_v1_folders__folder_id__comment_allowlist_put","parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","title":"Folder Id"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailsPutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Folder Allowlist V1 Folders  Folder Id  Comment Allowlist Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/workspace/comment-allowlist":{"get":{"tags":["comment-allowlist"],"summary":"Get Workspace Allowlist","description":"Return the workspace's default comment allowlist (any member may view).\n\nArgs:\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace.\n    user: Authenticated user.\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"emails\": [...]}``.","operationId":"get_workspace_allowlist_v1_workspace_comment_allowlist_get","parameters":[{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Workspace Allowlist V1 Workspace Comment Allowlist Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["comment-allowlist"],"summary":"Put Workspace Allowlist","description":"Replace the workspace's default comment allowlist (admin only).\n\nArgs:\n    body: The full replacement ``emails`` list for this workspace's scope.\n    workspace_id: Optional workspace id (``?workspace=``); defaults to the\n        caller's primary workspace.\n    user: Authenticated user (must be admin in the resolved workspace).\n    session: RLS-scoped DB session.\n\nReturns:\n    ``{\"emails\": [...]}``.","operationId":"put_workspace_allowlist_v1_workspace_comment_allowlist_put","parameters":[{"name":"workspace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace"}},{"name":"X-CSRF-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Csrf-Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailsPutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Workspace Allowlist V1 Workspace Comment Allowlist Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/llms.txt":{"get":{"tags":["agent-docs"],"summary":"Llms Txt","description":"Concise plain-text overview for LLM context windows.\n\nReturns:\n    Plain-text string describing the QuickHost API.","operationId":"llms_txt_llms_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/docs.md":{"get":{"tags":["agent-docs"],"summary":"Docs Md","description":"Fuller Markdown API reference.\n\nReturns:\n    Markdown string with full API reference.","operationId":"docs_md_docs_md_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/mcp.md":{"get":{"tags":["agent-docs"],"summary":"Mcp Md","description":"Per-client instructions for connecting to the QuickHost MCP server.\n\nWritten to be executed, not read: an agent that fetches this should be able\nto configure its own host without asking the user for anything except a\ntoken. Every client here was checked against its vendor's own docs, and the\nones that *cannot* connect say so — a config that looks plausible and fails\nsilently costs a user more than an honest \"not supported yet\".\n\nReturns:\n    Plain-text Markdown.","operationId":"mcp_md_mcp_md_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/.well-known/agent.md":{"get":{"tags":["agent-docs"],"summary":"Well Known Agent Md","description":"Short agent-onboarding guide.\n\nReturns:\n    Markdown string for agent onboarding.","operationId":"well_known_agent_md__well_known_agent_md_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/healthz":{"get":{"tags":["meta"],"summary":"Healthz","description":"Health check endpoint (liveness — static, no dependency checks).","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Healthz Healthz Get"}}}}}}},"/healthz/deep":{"get":{"tags":["meta"],"summary":"Healthz Deep","description":"Deep health check (readiness) — verifies DB and object storage.\n\nThis is the endpoint an external uptime pinger should hit; ``/healthz``\nstays static so it can't flap on a transient dependency blip.\n\nReturns:\n    200 with ``{\"status\": \"ok\", \"db\": \"ok\", \"storage\": \"ok\"}``, or 503\n    naming which component failed.","operationId":"healthz_deep_healthz_deep_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AuthorizeRequest":{"properties":{"client_id":{"type":"string","title":"Client Id"},"redirect_uri":{"type":"string","title":"Redirect Uri"},"code_challenge":{"type":"string","title":"Code Challenge"},"code_challenge_method":{"type":"string","title":"Code Challenge Method","default":"S256"},"scope":{"type":"string","title":"Scope","default":"mcp"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"resource":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resource"},"approve":{"type":"boolean","title":"Approve","default":true}},"type":"object","required":["client_id","redirect_uri","code_challenge"],"title":"AuthorizeRequest","description":"Consent decision posted by the SPA."},"CheckoutRequest":{"properties":{"workspace_id":{"type":"string","format":"uuid","title":"Workspace Id"},"plan":{"type":"string","title":"Plan"},"interval":{"type":"string","title":"Interval","default":"monthly"}},"type":"object","required":["workspace_id","plan"],"title":"CheckoutRequest","description":"Which plan, billed how often, for which workspace."},"CommentEditRequest":{"properties":{"body":{"type":"string","title":"Body"},"mentions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Mentions"}},"type":"object","required":["body"],"title":"CommentEditRequest","description":"Body for ``PATCH /v1/comments/{id}``.\n\nBody-only by construction: there is no field here for ``anchor``,\n``file_path``, or ``created_version_n``, so an edit can never move a pin or\nre-target a file even if a client tries. ``mentions`` rides along because\nwho a comment tags is part of its text."},"DeleteRequest":{"properties":{"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"}},"type":"object","title":"DeleteRequest","description":"Confirm it is really you.\n\n``password`` is required when the account has one. A Google-only account has\nnone to check; for those the confirmation in the UI is the whole gate, which\nis weaker — but the window is the real protection here, not the prompt, and\nthirty days of \"everything is dark, email support\" is recoverable in a way a\nstronger prompt would not make it."},"DeleteWorkspaceRequest":{"properties":{"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"workspace_id":{"type":"string","format":"uuid","title":"Workspace Id"}},"type":"object","required":["workspace_id"],"title":"DeleteWorkspaceRequest","description":"Which workspace, and confirm it is really you."},"DeviceTokenRequest":{"properties":{"device_code":{"type":"string","title":"Device Code"}},"type":"object","required":["device_code"],"title":"DeviceTokenRequest","description":"Poll request body."},"DeviceUserCodeRequest":{"properties":{"user_code":{"type":"string","title":"User Code"}},"type":"object","required":["user_code"],"title":"DeviceUserCodeRequest","description":"Approve/deny request body."},"EmailsPutRequest":{"properties":{"emails":{"items":{"type":"string"},"type":"array","title":"Emails"}},"type":"object","required":["emails"],"title":"EmailsPutRequest","description":"Body for the folder- and workspace-scope PUT endpoints."},"ExportRequest":{"properties":{"workspace_id":{"type":"string","format":"uuid","title":"Workspace Id"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"}},"type":"object","required":["workspace_id"],"title":"ExportRequest","description":"Confirm it is really you.\n\n``password`` is required when the account has one and ignored when it does\nnot: a Google-only account has no password to check, and refusing those\nwould make the feature unreachable for them. Their protection is the same\none everyone else gets — the link is emailed, never returned here."},"FolderCreateRequest":{"properties":{"workspace_id":{"type":"string","title":"Workspace Id"},"name":{"type":"string","title":"Name"},"share_mode":{"type":"string","title":"Share Mode","default":"private"},"member_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Member Ids"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color"}},"type":"object","required":["workspace_id","name"],"title":"FolderCreateRequest","description":"Body for ``POST /v1/folders``."},"FolderPatchRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color"},"share_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Share Mode"},"member_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Member Ids"},"position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position"}},"type":"object","title":"FolderPatchRequest","description":"Body for ``PATCH /v1/folders/{id}`` (all fields optional)."},"GrantAdmin":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"GrantAdmin","description":"Grant-admin payload."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InviteAcceptRequest":{"properties":{"token":{"type":"string","title":"Token"},"name":{"type":"string","title":"Name"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"}},"type":"object","required":["token","name"],"title":"InviteAcceptRequest","description":"Invite acceptance body."},"InviteCreateRequest":{"properties":{"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role","default":"member"}},"type":"object","required":["email"],"title":"InviteCreateRequest","description":"Invite creation body."},"LoginRequest":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest","description":"Login credentials."},"MagicLinkRequest":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"MagicLinkRequest","description":"Magic link request body."},"MagicLinkVerifyRequest":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"MagicLinkVerifyRequest","description":"Magic link token verification body."},"MemberAdd":{"properties":{"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role","default":"member"}},"type":"object","required":["email"],"title":"MemberAdd","description":"Add-member payload."},"MemberSpec":{"properties":{"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role","default":"member"}},"type":"object","required":["email"],"title":"MemberSpec","description":"A member to add when creating a workspace."},"PasswordChangeRequest":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"PasswordChangeRequest","description":"Password change body (authenticated user)."},"PasswordResetConfirmRequest":{"properties":{"token":{"type":"string","title":"Token"},"password":{"type":"string","title":"Password"}},"type":"object","required":["token","password"],"title":"PasswordResetConfirmRequest","description":"Password reset confirmation body."},"PasswordResetRequest":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"PasswordResetRequest","description":"Password reset initiation body."},"PortalRequest":{"properties":{"workspace_id":{"type":"string","format":"uuid","title":"Workspace Id"}},"type":"object","required":["workspace_id"],"title":"PortalRequest","description":"Which workspace's billing to manage."},"ProjectAllowlistPutRequest":{"properties":{"comment_access":{"type":"string","title":"Comment Access"},"comment_allowlist_inherit":{"type":"boolean","title":"Comment Allowlist Inherit"},"emails":{"items":{"type":"string"},"type":"array","title":"Emails"}},"type":"object","required":["comment_access","comment_allowlist_inherit","emails"],"title":"ProjectAllowlistPutRequest","description":"Body for ``PUT /v1/projects/{slug}/comment-allowlist``."},"ProjectPatchRequest":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"visibility":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visibility"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"noindex":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Noindex"},"comments_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Comments Enabled"},"html_display_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Html Display Mode"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"}},"type":"object","title":"ProjectPatchRequest","description":"Partial update body for PATCH /v1/projects/{slug}."},"QuotaPatch":{"properties":{"quota_projects":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota Projects"},"quota_storage_bytes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota Storage Bytes"},"quota_upload_bytes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota Upload Bytes"},"quota_members":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota Members"}},"type":"object","title":"QuotaPatch","description":"Quota override payload.\n\nAn explicit ``null`` means unlimited; a field left out is left alone. The\nthree original fields were mandatory, which is why ``quota_members`` could\nnot simply be added to them — every existing caller sends three keys and\nwould have started failing. Omission-means-unchanged is also what\n:class:`WorkspaceCreate` already does, so the two payloads now read alike."},"RegisterRequest":{"properties":{"redirect_uris":{"items":{"type":"string"},"type":"array","title":"Redirect Uris"},"client_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Name"},"grant_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Grant Types"},"response_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Response Types"},"token_endpoint_auth_method":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Endpoint Auth Method"}},"type":"object","required":["redirect_uris"],"title":"RegisterRequest","description":"Subset of RFC 7591 client metadata we accept."},"ReplyCreateRequest":{"properties":{"body":{"type":"string","title":"Body"},"mentions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Mentions"}},"type":"object","required":["body"],"title":"ReplyCreateRequest","description":"Body for ``POST /v1/comments/{id}/replies``."},"ShareLinkCreateRequest":{"properties":{"label":{"type":"string","title":"Label","default":"Share link"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"expires_in_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires In Days"},"notify_on_first_open":{"type":"boolean","title":"Notify On First Open","default":false}},"type":"object","title":"ShareLinkCreateRequest","description":"Request body for creating a share link."},"ShareLinkPatchRequest":{"properties":{"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"notify_on_first_open":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notify On First Open"}},"type":"object","title":"ShareLinkPatchRequest","description":"Partial update body for a share link."},"SignupRequest":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["email","password"],"title":"SignupRequest","description":"Public account creation."},"SuspendPayload":{"properties":{"reason":{"type":"string","maxLength":1000,"minLength":3,"title":"Reason"}},"type":"object","required":["reason"],"title":"SuspendPayload","description":"Suspension payload.\n\n``reason`` is required and non-empty on purpose. It is the only part of a\ntakedown that is still useful three months later, when the suspended user\nemails to appeal and nobody remembers the page. Making it optional means it\nis blank exactly when the decision was made in a hurry — which is exactly\nwhen it matters."},"TokenCreateRequest":{"properties":{"label":{"type":"string","title":"Label"}},"type":"object","required":["label"],"title":"TokenCreateRequest","description":"Token creation body."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyEmailConfirmRequest":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"VerifyEmailConfirmRequest","description":"Redeem a confirmation link."},"VerifyEmailResendRequest":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"VerifyEmailResendRequest","description":"Request a fresh confirmation link."},"WorkspaceCreate":{"properties":{"name":{"type":"string","title":"Name"},"owner_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Email"},"quota_projects":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota Projects"},"quota_storage_bytes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota Storage Bytes"},"quota_upload_bytes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota Upload Bytes"},"members":{"items":{"$ref":"#/components/schemas/MemberSpec"},"type":"array","title":"Members"}},"type":"object","required":["name"],"title":"WorkspaceCreate","description":"Create-workspace payload — name plus optional quotas, owner, and members.\n\nEach quota is ``null`` for unlimited or a number; when a quota field is\nomitted entirely the workspace keeps the standard-plan default.\n\n``owner_email`` selects the customer-workspace flow: the named user owns the\nworkspace and the creating admin is neither owner nor member. If that email\nhas no account yet, the workspace is held by the creating admin as its\n**interim** owner and an ownership-granting invite is emailed; ownership\ntransfers on acceptance and the admin panel manages it cross-tenant\nmeanwhile. Omit ``owner_email`` for the classic flow where the creating\nadmin becomes the owner + admin member. ``members`` are extra members added\nalongside (existing accounts join immediately; unknown emails are invited)."},"WorkspacePatchRequest":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"WorkspacePatchRequest","description":"Workspace update body."},"WorkspaceRename":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"WorkspaceRename","description":"Rename-workspace payload."}}}}