{"openapi":"3.1.0","info":{"title":"Astrabase Control Plane","version":"1.1.0","description":"Partial documented control-plane API; does not describe backend data-plane endpoints."},"servers":[{"url":"https://cloud.astrabase.fr"}],"components":{"securitySchemes":{"apiToken":{"type":"http","scheme":"bearer","description":"Astrabase API token"}},"parameters":{"project":{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}}}},"paths":{"/api/auth/mfa":{"get":{"summary":"Local console MFA status, no secrets returned","responses":{"200":{"description":"MFA status and remaining recovery code count"},"401":{"description":"Unauthenticated"}}}},"/api/auth/mfa/enroll":{"post":{"summary":"Start local TOTP enrollment, requires account password","responses":{"200":{"description":"One-time setup secret and otpauth URI"},"403":{"description":"Wrong password"},"409":{"description":"Already enabled"},"501":{"description":"External identity mode"}}}},"/api/auth/mfa/confirm":{"post":{"summary":"Confirm TOTP enrollment and generate one-time recovery codes","responses":{"200":{"description":"MFA enabled and recovery codes returned once"},"400":{"description":"Invalid or expired TOTP"}}}},"/api/auth/mfa/recovery/rotate":{"post":{"summary":"Rotate recovery codes after password and MFA verification","responses":{"200":{"description":"New recovery codes shown once"},"403":{"description":"Invalid factor"}}}},"/api/auth/mfa/disable":{"post":{"summary":"Disable MFA with password and factor, revoke all browser sessions","responses":{"200":{"description":"MFA disabled, sessions revoked"},"403":{"description":"Invalid factor"}}}},"/api/auth/sessions/revoke-all":{"post":{"summary":"Revoke all local browser sessions, requires account password","responses":{"200":{"description":"All browser sessions invalidated"},"403":{"description":"Password not valid"},"501":{"description":"External identity mode"}}}},"/api/health":{"get":{"summary":"Health","security":[],"responses":{"200":{"description":"Healthy"}}}},"/api/config":{"get":{"summary":"Public client configuration","security":[],"responses":{"200":{"description":"Client settings"}}}},"/api/ops/monitor/alerts":{"get":{"summary":"Operator-only persistent local Monitor alert inbox; off by default, no notifications","responses":{"200":{"description":"Alert policy, counts and bounded incidents"},"403":{"description":"Not the configured operator"}}}},"/api/ops/monitor/alerts/insights":{"get":{"summary":"Operator-only 7/30-day aggregate counts for retained local confirmed incidents; not uptime or a SLA","responses":{"200":{"description":"Incident counts by signal and window"},"403":{"description":"Configured operator required"}}}},"/api/ops/monitor/alerts/policy":{"put":{"summary":"Operator-only opt-in rules and thresholds, optimistic policy version required","responses":{"200":{"description":"Updated opt-in policy"},"400":{"description":"Invalid thresholds"},"403":{"description":"Operator required"},"409":{"description":"Version conflict"}}}},"/api/ops/monitor/alerts/{incidentId}/acknowledge":{"post":{"summary":"Operator acknowledges a confirmed local incident; does not close or notify","responses":{"200":{"description":"Acknowledged incident"},"403":{"description":"Operator required"},"404":{"description":"Incident not found"},"409":{"description":"Already acknowledged or resolved"}}}},"/api/ops/monitor/history":{"get":{"summary":"Operator-only sampled host/app history and 24h/7d observation coverage; not independently measured uptime","responses":{"200":{"description":"History and bounded warning episodes (sampling only)"},"403":{"description":"Operator role required"},"503":{"description":"Monitor persistence temporarily unavailable"}}}},"/api/ops/readiness":{"get":{"summary":"Private node readiness","responses":{"200":{"description":"Readiness"}}}},"/api/ops/metrics.prom":{"get":{"summary":"Operator-only Prometheus exporter; bearer authentication required","responses":{"200":{"description":"Prometheus text metrics"},"403":{"description":"Operator required"}}}},"/api/ops/traffic":{"get":{"summary":"Operator-only process traffic counters","responses":{"200":{"description":"Aggregate metrics"},"403":{"description":"Operator permission required"}}}},"/api/projects/{id}/cloud/storage/buckets/{bucket}/objects":{"get":{"summary":"List project-bucket objects with optional MIME kind and size band filters (project cloud role required)","description":"Backward compatible: without limit returns previous object array. Paged usage: limit 1-100; offset 0-1000000; optional q at most 80 chars, kind=image|pdf|text|other, size_band=small (<1 MiB)|medium (1-10 MiB)|large (>=10 MiB). Filtering reads recorded metadata only, not disk content. Size and MIME are declared metadata, not verified evidence of file safety. Returns per-project and per-bucket scope with count and pagination.","responses":{"200":{"description":"Filtered scoped object metadata list or legacy array"},"400":{"description":"Invalid or unsupported filter"},"403":{"description":"Project authorization required"},"404":{"description":"Bucket not found"}}}},"/api/projects/{id}/sql-studio":{"get":{"summary":"Administrator-only saved SQL library, shared queries and fingerprint-only history","responses":{"200":{"description":"Private/shared snippets and execution history"},"403":{"description":"Project admin required"},"501":{"description":"Local storage required"}}}},"/api/projects/{id}/sql-studio/import":{"post":{"summary":"Import 1-60 explicitly saved SQL snippets atomically, private to the importing administrator, no SQL execution","responses":{"201":{"description":"All snippets imported as private version-1 copies"},"400":{"description":"Invalid or oversized portable library"},"403":{"description":"Project administrator required"},"409":{"description":"Per-project or per-author quota exceeded"},"501":{"description":"Local storage required"}}}},"/api/projects/{id}/sql-studio/queries":{"post":{"summary":"Save bounded SQL explicitly, private by default; may share with project administrators","responses":{"201":{"description":"Saved snippet version one"},"400":{"description":"Invalid SQL or metadata"},"409":{"description":"Quota reached"}}}},"/api/projects/{id}/sql-studio/queries/{queryId}":{"patch":{"summary":"Optimistically update own saved SQL (expected_version required)","responses":{"200":{"description":"Saved updated snippet"},"403":{"description":"Not the author"},"409":{"description":"Version conflict"}}},"delete":{"summary":"Delete own saved SQL; unlinks corresponding history references","responses":{"200":{"description":"Deleted"},"403":{"description":"Not the author"}}}},"/api/projects/{id}/sql-studio/queries/{queryId}/revisions":{"get":{"summary":"Owner-only last five previous plaintext revisions of an explicitly saved query; does not execute SQL","responses":{"200":{"description":"Bounded author-only SQL revisions"},"403":{"description":"Not the saved-query author"},"404":{"description":"Saved query missing"},"501":{"description":"Local storage required"}}}},"/api/projects/{id}/sql-studio/queries/{queryId}/revisions/restore":{"post":{"summary":"Owner-only optimistic version restoration, atomically archive current version first; never execute SQL","responses":{"200":{"description":"Saved query restored as a new revision"},"400":{"description":"Invalid version"},"403":{"description":"Not the author"},"404":{"description":"Revision missing"},"409":{"description":"Expected version stale"}}}},"/api/projects/{id}/sql-studio/execute":{"post":{"summary":"Execute SQL as existing project SQL role; require additional explicit confirmation for non-SELECT operations; audit fingerprint, no SQL in history","responses":{"200":{"description":"Bounded result plus duration"},"403":{"description":"Project admin required"},"409":{"description":"Execution confirmation required"},"502":{"description":"Backend SQL execution failure"}}}},"/api/projects/{id}/studio":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Get current saved Site Studio multipage draft (project viewer)","responses":{"200":{"description":"Saved draft"},"403":{"description":"Project access denied"},"501":{"description":"Storage mode not supported"}}},"put":{"summary":"Save validated Studio draft, preserving revisions (project developer)","responses":{"200":{"description":"New saved draft version"},"409":{"description":"Concurrent modification or foreign media"}}}},"/api/projects/{id}/seo/report":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Read saved local SEO artifact audit and comparison, project viewer","responses":{"200":{"description":"Current audit and comparison, potentially null"},"403":{"description":"Project denied"}}}},"/api/projects/{id}/secrets/rotation-report":{"get":{"summary":"Private project-admin review of Secrets rotation metadata, without reading secret values","description":"Return per-secret age from an explicit rotation timestamp, else initial creation. Advisories at 30 and 90 days; unknown dates fail closed. Does not decrypt, expire, or rotate secrets, or verify external credentials. Max 100 visible entries; totals cover the project. No query string, local mode only.","responses":{"200":{"description":"Secret rotation review counts and metadata-only recommendations"},"400":{"description":"Query parameters refused"},"403":{"description":"Project administrator required"},"501":{"description":"Local storage only"}}}},"/api/projects/{id}/analytics/report.csv":{"get":{"summary":"Download 31 UTC days of private project-level aggregate static HTTP response counts and transferred bytes","description":"Viewer+ permission required. A fixed header and exactly 31 chronological CSV rows; dates and finite nonnegative integer aggregate counters only. Never visitor identifiers, URLs, paths, cookies, raw headers, clients or user agents. No query parameters. No-store and nosniff.","responses":{"200":{"description":"CSV of per-day static HTTP aggregate counts"},"400":{"description":"Query parameters forbidden"},"403":{"description":"Viewer role required"}}}},"/api/projects/{id}/gateway/history":{"get":{"summary":"Admin-only seven UTC days of per-project API GET quota reservations and quota denials","description":"Daily successful reservations and refused quota attempts, split into Flags, Errors and Analytics, and refusal reason (project total vs route cap). Aggregated per project/UTC day without IP, users, URLs, request headers or bodies. Denials do not indicate malicious traffic or actual API response outcomes. Local mode only.","responses":{"200":{"description":"Daily allowed reservation counts and denied counters per route/reason"},"403":{"description":"Project admin required"},"501":{"description":"Local storage required"}}}},"/api/projects/{id}/seo/trends":{"get":{"summary":"Project private trends of ten retained local static SEO scans and recurring findings; no Google metrics","responses":{"200":{"description":"Ten bounded historical points and recurring warning codes"},"403":{"description":"Project access denied"},"501":{"description":"Local storage required"}}}},"/api/projects/{id}/seo/history":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Latest ten project-scoped local SEO scans","responses":{"200":{"description":"Audit history"},"403":{"description":"Project denied"}}}},"/api/projects/{id}/seo/audit":{"parameters":[{"$ref":"#/components/parameters/project"}],"post":{"summary":"Run bounded read-only scan of currently published STATIC deployment (project developer, one per minute)","responses":{"201":{"description":"Saved SEO V2 report"},"403":{"description":"Project denied"},"409":{"description":"No ready production artifact"},"422":{"description":"Only static sites supported"},"429":{"description":"Scan throttle"}}}},"/api/projects/{id}/studio/quality":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Analyze saved Site Studio draft SEO metadata, links and content (project viewer; no network calls)","responses":{"200":{"description":"Advisory SEO report with version, issues and summary"},"403":{"description":"Project access denied"},"501":{"description":"Storage mode not supported"}}}},"/api/projects/{id}/mail/config":{"get":{"summary":"Mail transactional SMTP readiness and operator sender (project viewer)","responses":{"200":{"description":"Configured status and fixed sender"}}}},"/api/projects/{id}/mail/deliveries":{"get":{"summary":"Last 40 project-scoped transactional Mail jobs (project viewer)","responses":{"200":{"description":"Job status without message body"}}}},"/api/projects/{id}/mail/send":{"post":{"summary":"Queue text template email with verified Reply-To; project admin, SMTP opt-in","responses":{"200":{"description":"Idempotent replay"},"202":{"description":"Pending, not delivered"},"403":{"description":"Reply-To domain unverified"},"429":{"description":"Quota exceeded"},"503":{"description":"SMTP not configured"}}}},"/api/projects/{id}/mail/templates":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"List project-scoped Mail templates; authenticated project viewer","responses":{"200":{"description":"Text templates"},"404":{"description":"Project not found"}}}},"/api/projects/{id}/mail/templates/{name}":{"parameters":[{"$ref":"#/components/parameters/project"},{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"put":{"summary":"Save a text-only Mail template; project developer required","responses":{"200":{"description":"Updated"},"201":{"description":"Created"},"400":{"description":"Invalid input"},"403":{"description":"Forbidden"},"409":{"description":"25-template quota"}}},"delete":{"summary":"Delete Mail template; project developer required","responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/api/projects/{id}/mail/templates/{name}/preview":{"post":{"summary":"Render text preview only; sends no email; project developer required","responses":{"200":{"description":"Subject and text preview"},"400":{"description":"Invalid variables"},"404":{"description":"Not found"}}}},"/api/projects/{id}/cache/queue/jobs":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"List at most 100 durable local queue job metadata for project viewers; never disclose payload or lease tokens","responses":{"200":{"description":"Private queue metadata"},"403":{"description":"Project access denied"},"501":{"description":"Local mode required"}}},"post":{"summary":"Idempotently enqueue bounded text job; requires project developer; does not execute job","responses":{"200":{"description":"Same idempotency key and contents"},"201":{"description":"Created"},"400":{"description":"Invalid shape, ID, value or attempts"},"409":{"description":"Conflict or 100-job quota"}}}},"/api/projects/{id}/cache/queue/claim":{"post":{"summary":"Atomically lease one queued job to a project developer, return opaque secret receipt and payload; never execute it","responses":{"200":{"description":"Claim or null"},"400":{"description":"Invalid worker or lease TTL"},"403":{"description":"Project denied"},"501":{"description":"Local only"}}}},"/api/projects/{id}/cache/queue/jobs/{jobId}/ack":{"post":{"summary":"Mark leased job complete with current private lease token only; stale tokens rejected","responses":{"200":{"description":"Acknowledged"},"400":{"description":"Invalid lease token"},"409":{"description":"Expired, wrong or replaced lease"}}}},"/api/projects/{id}/cache/queue/jobs/{jobId}/retry":{"post":{"summary":"Release current lease for bounded delayed retry or dead-letter once max attempts is exhausted","responses":{"200":{"description":"Pending or dead"},"409":{"description":"Stale lease"}}}},"/api/projects/{id}/cache/queue/jobs/{jobId}/renew":{"post":{"summary":"Extend currently valid lease, bounded to 5-300 seconds from server clock","responses":{"200":{"description":"Lease renewed"},"409":{"description":"Stale lease"}}}},"/api/projects/{id}/cache/mutate-batch":{"parameters":[{"$ref":"#/components/parameters/project"}],"post":{"summary":"Atomic local Cache 1-20 revision-conditional write/delete operations. All conditions validated before persistence; developer+ only.","responses":{"200":{"description":"All requested modifications committed"},"400":{"description":"Invalid operations"},"403":{"description":"Project denied"},"409":{"description":"Revision or capacity conflict; zero operations applied"},"501":{"description":"Local transactional storage required"}}}},"/api/projects/{id}/cache/maintenance":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Preview tenant-only expired KV entries and terminal queue jobs older than 7 days; admin required, no mutation","responses":{"200":{"description":"Redacted eligible counts"},"403":{"description":"Project admin access required"},"501":{"description":"Local mode only"}}},"post":{"summary":"Explicit admin confirmation required to atomically purge only eligible tenant Cache records","responses":{"200":{"description":"Deleted counts, no payloads"},"400":{"description":"Strict body {confirm:true} required"},"403":{"description":"Project admin access required"},"501":{"description":"Local mode only"}}}},"/api/projects/{id}/cache/batch":{"parameters":[{"$ref":"#/components/parameters/project"}],"post":{"summary":"Read 1-20 active project cache keys in one authenticated viewer request, no values outside the project","responses":{"200":{"description":"Active entries with values and missing keys"},"400":{"description":"Invalid or duplicate keys"},"403":{"description":"Project access denied"},"501":{"description":"Local storage required"}}}},"/api/projects/{id}/cache/{key}/revision":{"get":{"summary":"Read current opaque revision of active project Cache key (viewer+); legacy keys may return null","responses":{"200":{"description":"Current entry plus UUID revision"},"403":{"description":"Project denied"},"404":{"description":"Missing or expired key"}}}},"/api/projects/{id}/cache/{key}/compare-revision":{"post":{"summary":"Atomic revision-based conditional cache write (developer+), detecting changes even if the value returns to the same text","responses":{"200":{"description":"Revision-matched update"},"201":{"description":"Absent key created"},"400":{"description":"Invalid revision or value"},"403":{"description":"Project denied"},"409":{"description":"Revision changed, missing, expired or duplicate create"}}}},"/api/projects/{id}/cache/{key}/compare-and-set":{"parameters":[{"$ref":"#/components/parameters/project"},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"post":{"summary":"Atomic conditional cache write for a project developer; expected_value null means create only if absent","responses":{"200":{"description":"Existing key updated"},"201":{"description":"New key created"},"400":{"description":"Invalid value or TTL"},"403":{"description":"Project denied"},"409":{"description":"Condition failed without changing value or TTL"}}}},"/api/projects/{id}/errors":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"List authenticated project error groups without PII","responses":{"200":{"description":"Structured incidents"},"404":{"description":"Project not found"}}},"post":{"summary":"Report structured non-PII error codes (project developer); no arbitrary messages/stacks","responses":{"200":{"description":"Duplicate grouped"},"201":{"description":"New group"},"400":{"description":"Unsafe or invalid fields"},"409":{"description":"Group limit"},"429":{"description":"Daily quota"}}}},"/api/projects/{id}/errors/deployments":{"get":{"summary":"Read-only 2h temporal proximity between error observations and project deployments, not proof of causation","responses":{"200":{"description":"Bounded redacted correlations"},"403":{"description":"Project viewer access required"},"501":{"description":"Local storage required"}}}},"/api/projects/{id}/errors/alerts":{"get":{"summary":"Project-only internal Errors alert ledger, viewer+, disabled by default","description":"No external delivery; 30-day bounded structured history, no free text or PII, private no-store.","responses":{"200":{"description":"Internal alert policy and retained events"},"403":{"description":"Viewer role required"},"501":{"description":"Local mode only"}}}},"/api/projects/{id}/errors/alerts/{alertId}/acknowledge":{"post":{"summary":"Acknowledge one internal Errors alert (developer+, project scoped, audited)","description":"Marks a retained synthetic structured alert as acknowledged, without resolving its incident, notifying anyone, or sending external network requests. Legacy alerts without status are open. Repeat gives 409.","responses":{"200":{"description":"Alert acknowledged"},"403":{"description":"Developer access required"},"404":{"description":"Alert not found within project or outside retention"},"409":{"description":"Already acknowledged"},"501":{"description":"Local mode only"}}}},"/api/projects/{id}/errors/alerts/policy":{"put":{"summary":"Opt-in internal Errors alert policy with expected_version (developer+)","description":"Set enabled, min_severity, cooldown_minutes 1–1440, max_alerts_per_day 1–50. No email, SMS or webhooks.","responses":{"200":{"description":"Updated policy"},"400":{"description":"Invalid policy"},"403":{"description":"Developer required"},"409":{"description":"Version conflict"},"501":{"description":"Local mode only"}}}},"/api/projects/{id}/errors/triage":{"get":{"summary":"Read-only project-scoped prioritized structured incident hints (viewer+, local mode)","description":"Deterministic urgency from severity, environment and reopening count, excluding resolved and events outside 30 days. No messages, PII, root-cause attribution, outbound notifications or SLA promise. Max 30 results.","responses":{"200":{"description":"30-day bounded privacy-safe triage with counts"},"400":{"description":"No query string permitted"},"403":{"description":"Project viewer access required"},"501":{"description":"Local storage only"}}}},"/api/projects/{id}/errors/activity":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Read private 7/30-day aggregated structured error counts and severity without PII (project viewer)","responses":{"200":{"description":"30 UTC daily buckets, 7/30-day summary and unclassified historic counts"},"403":{"description":"Project denied"},"501":{"description":"Local storage required"}}}},"/api/projects/{id}/errors/{errorId}/resolve":{"patch":{"summary":"Resolve an incident group (project developer)","responses":{"200":{"description":"Resolved"},"404":{"description":"Incident not found"}}}},"/api/projects/{id}/servers":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Private VPS metadata inventory for project viewers (no SSH)","responses":{"200":{"description":"Server inventory"},"404":{"description":"Project not found"}}},"post":{"summary":"Add VPS inventory entry; project admin required","responses":{"201":{"description":"Created"},"400":{"description":"Unsafe or invalid metadata"},"409":{"description":"Duplicate or quota exceeded"}}}},"/api/projects/{id}/servers/{serverId}":{"parameters":[{"$ref":"#/components/parameters/project"},{"name":"serverId","in":"path","required":true,"schema":{"type":"string"}}],"put":{"summary":"Update metadata only; project admin required","responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"summary":"Remove inventory entry only; does not delete an actual VPS","responses":{"200":{"description":"Removed"},"404":{"description":"Not found"}}}},"/api/services":{"get":{"summary":"Authenticated cloud module catalog (truthful deployment status)","responses":{"200":{"description":"15 cloud modules and implementation status"},"401":{"description":"Authentication required"}}}},"/api/projects/{id}/flags":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"List feature flags scoped to an authorized project","responses":{"200":{"description":"Flags"},"401":{"description":"Unauthorized"},"404":{"description":"Project not found"}}}},"/api/projects/{id}/flags/{key}":{"parameters":[{"$ref":"#/components/parameters/project"},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"put":{"summary":"Upsert a project flag (developer role)","responses":{"200":{"description":"Updated"},"201":{"description":"Created"},"400":{"description":"Invalid input"},"403":{"description":"Insufficient role"},"409":{"description":"Quota exceeded"}}},"delete":{"summary":"Delete a project flag","responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/api/projects/{id}/flags/{key}/targeting":{"put":{"summary":"Configure flag environment/channel + optional plan/platform audience targeting (developer+, project scoped, optimistic version)","description":"Use expected_version and targeting:{environments:[production|staging|development|test],channels:[stable|beta|internal],audience?:{plans:[free|pro|team|enterprise],platforms:[web|windows|linux|macos|android|ios]}}. Omit audience to retain an existing rule; supply empty arrays to clear it. Empty arrays permit any, missing context fails closed for targeted values. Subject IDs must remain opaque; never submit email/IP/device identifiers.","responses":{"200":{"description":"New version of the flag"},"400":{"description":"Invalid targeting"},"403":{"description":"Insufficient project role"},"404":{"description":"Flag missing"},"409":{"description":"Version conflict"}}}},"/api/projects/{id}/flags/{key}/revisions":{"get":{"summary":"Read last 20 configuration revisions of a project flag (viewer+)","responses":{"200":{"description":"Current version and bounded historical configuration"},"404":{"description":"Flag unavailable"}}}},"/api/projects/{id}/flags/{key}/restore":{"post":{"summary":"Restore a retained Flags configuration with explicit confirmation and optimistic concurrency (developer+)","responses":{"200":{"description":"New version created"},"400":{"description":"Invalid version or confirmation"},"403":{"description":"Insufficient role"},"404":{"description":"Revision unavailable"},"409":{"description":"Concurrent update"}}}},"/api/projects":{"get":{"summary":"Accessible projects","responses":{"200":{"description":"Projects"}}},"post":{"summary":"Create project","responses":{"201":{"description":"Created"}}}},"/api/projects/{id}":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Project details","responses":{"200":{"description":"Project"},"404":{"description":"Not found"}}},"patch":{"summary":"Update project (admin)","responses":{"200":{"description":"Updated"}}},"delete":{"summary":"Delete project (admin)","responses":{"200":{"description":"Deleted"}}}},"/api/projects/{id}/insights":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Redacted project insights","responses":{"200":{"description":"Insights"}}}},"/api/projects/{id}/audit":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Recent project audit events","responses":{"200":{"description":"Events"}}}},"/api/projects/{id}/audit.csv":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Export recent project audit events","responses":{"200":{"description":"CSV"}}}},"/api/projects/{id}/previews":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Immutable deployed release previews","responses":{"200":{"description":"Preview URLs"}}}},"/api/projects/{id}/deployments":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"Recent deployments","responses":{"200":{"description":"Deployments"}}}},"/api/projects/{id}/deployments/{deploymentId}/integrity":{"get":{"summary":"Verify deployment artifact checksum; legacy deployments may lack a recorded digest","responses":{"200":{"description":"Integrity status"},"404":{"description":"Deployment inaccessible"}}}},"/api/projects/{id}/github-webhook/rotate":{"post":{"summary":"Create/rotate signed GitHub webhook secret (project admin)","responses":{"200":{"description":"Secret returned once"},"403":{"description":"Admin required"}}}},"/hooks/github/{id}":{"post":{"summary":"Signed GitHub push webhook (SHA256 HMAC); delivery replay protection is per-process","security":[],"responses":{"202":{"description":"Queued or ignored"},"401":{"description":"Invalid signature"}}}},"/api/projects/{id}/cloud/database/rls":{"get":{"summary":"Inspect RLS policies in project schema (admin)","responses":{"200":{"description":"RLS status"}}}},"/api/projects/{id}/cloud/database/migrations":{"get":{"summary":"Migration history (admin)","responses":{"200":{"description":"Migration history"}}},"post":{"summary":"Apply one restricted prepared SQL migration (admin)","responses":{"200":{"description":"Applied"},"409":{"description":"Duplicate name"}}}},"/api/projects/{id}/cloud/storage/s3/credentials":{"get":{"summary":"List project S3 key IDs (admin)","responses":{"200":{"description":"Key IDs"}}},"post":{"summary":"Create S3 access and secret keys, returned once (admin)","responses":{"200":{"description":"New S3 keys"}}}},"/api/projects/{id}/deploy-git":{"parameters":[{"$ref":"#/components/parameters/project"}],"post":{"summary":"Queue Git deploy","responses":{"202":{"description":"Queued"}}}},"/api/projects/{id}/deploy":{"parameters":[{"$ref":"#/components/parameters/project"}],"post":{"summary":"Upload ZIP deploy","requestBody":{"required":true,"content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"Deployed"}}}},"/api/projects/{id}/domains/{domainId}/readiness":{"get":{"summary":"Read-only project-scoped domain ownership TXT and A/CNAME readiness report","description":"Returns expected TXT challenge for this registered domain, checks against recursive DNS, and tests existing Astrabase routing. DNS lookup is bounded; no TLS probe, DNS provider write or certificate issuance. Requires viewer access.","responses":{"200":{"description":"Domain readiness report"},"403":{"description":"Project role denied"},"404":{"description":"Domain not in project"}}}},"/api/projects/{id}/domains/{domainId}/challenge":{"post":{"summary":"Issue a stable cryptographic TXT challenge for an unverified legacy domain","description":"Project admin only; cannot overwrite a verified domain or change an existing token. Existing verified domains are preserved.","responses":{"200":{"description":"TXT challenge record"},"403":{"description":"Project admin required"},"404":{"description":"Domain not found"},"409":{"description":"Already verified"}}}},"/api/projects/{id}/domains/{domainId}/verify":{"post":{"summary":"Verify domain DNS ownership TXT AND Astrabase A/CNAME routing before activation","description":"Project admin only; domain must have an exact project-bound TXT challenge on _astrabase-challenge.hostname, as well as a valid routing record. Existing verified legacy hosts are not revoked.","responses":{"200":{"description":"Verified and activated"},"403":{"description":"Project admin required"},"409":{"description":"Challenge or routing not verified"}}}},"/api/projects/{id}/domains":{"parameters":[{"$ref":"#/components/parameters/project"}],"get":{"summary":"List custom domains","responses":{"200":{"description":"Domains"}}}},"/api/tokens":{"get":{"summary":"List API tokens","responses":{"200":{"description":"Tokens"}}},"post":{"summary":"Create expiring token","responses":{"201":{"description":"Created"}}}}},"security":[{"apiToken":[]}]}