{"openapi":"3.1.0","info":{"title":"EchoAtlas Observatory API","version":"1.0.0","description":"Public v1 surface for the EchoAtlas Observatory honeypot. Auto-derived from the manifest at /api/observatory/v1/manifest — see also the interactive catalog at /observatory/api.","contact":{"name":"EchoAtlas","url":"https://echo-atlas.com"},"license":{"name":"MIT"}},"servers":[{"url":"https://echoatlas-observatory.vercel.app"}],"tags":[{"name":"meta"},{"name":"dashboard"},{"name":"tenants"},{"name":"alerts"},{"name":"events"},{"name":"clusters"},{"name":"investigate"},{"name":"fleet"},{"name":"ops"},{"name":"test"},{"name":"empire"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"eatok_*","description":"Scoped bearer token issued at /observatory/tokens. Each token carries an explicit scope list (e.g. read:dashboard)."}},"schemas":{"Error":{"type":"object","required":["schema","reason","human"],"properties":{"schema":{"const":"echoatlas-error.v1"},"reason":{"type":"string"},"human":{"type":"string"}}}}},"paths":{"/api/observatory/v1/manifest":{"get":{"tags":["meta"],"summary":"API manifest","description":"Returns the canonical capability index for /api/observatory/v1. Lists every endpoint with method, params, response schema, example payload, and required scope. Drift-free single source of truth.","operationId":"observatory_manifest","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-manifest.v1","generatedAt":"2026-05-25T12:00:00.000Z","baseUrl":"https://echoatlas-observatory.vercel.app","scopes":["read:dashboard","read:events","read:tenants","read:alerts","write:alerts","read:clusters","read:intel","write:tokens","write:tenants","write:test-event","read:empire"],"tags":["meta","dashboard","events","tenants","alerts","clusters","investigate","fleet","intel","ops","test","empire"],"endpoints":[]}}}}}}},"/api/observatory/v1/openapi.json":{"get":{"tags":["meta"],"summary":"OpenAPI v3.1 document","description":"OpenAPI v3.1 representation of the manifest. Importable into Postman, Insomnia, openapi-generator, swagger-ui, and Stainless. Auto-derived from the same Zod schemas the runtime uses for validation.","operationId":"observatory_openapi","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"openapi":"3.1.0","info":{"title":"EchoAtlas Observatory","version":"1.0.0"}}}}}}}},"/api/observatory/v1/honeypot/dashboard":{"get":{"tags":["dashboard"],"summary":"Dashboard","description":"KPI strip, UA-class breakdown, top paths/UAs/countries, hourly buckets, top verified vendors. Same payload the /observatory dashboard renders.","operationId":"observatory_dashboard","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-dashboard.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenant":"landing","window":"24h","kpis":{"totalEvents":1024,"uniqueFingerprints":87,"liarCount":6,"liarRatePct":0.6,"knownVerifiedCount":412,"attributionCoveragePct":91.2},"uaClassBreakdown":[{"uaClass":"KNOWN_LLM_AGENT","count":412}],"topPaths":[{"path":"/honeypot/topics/seed-1","count":122}],"topCountries":[{"countryCode":"US","count":540}],"topUas":[{"uaRaw":"GPTBot/1.0","count":88}],"topVerifiedVendors":[{"uaRaw":"GPTBot/1.0","count":88}],"eventsPerHour":[{"bucketIso":"2026-05-25T11:00:00.000Z","count":42}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/tenants":{"get":{"tags":["tenants"],"summary":"List tenants","description":"Every tenant registered for the honeypot fanout. Includes disabled tenants. Excludes the per-tenant webhook signature secret — fetch via the rotate endpoint when needed.","operationId":"observatory_tenants_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-tenants.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenants":[{"id":"tnt_abc","slug":"landing","name":"Echo-Atlas Landing","createdAt":"2026-04-01T00:00:00.000Z","disabledAt":null,"notes":null}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/alerts":{"get":{"tags":["alerts"],"summary":"List alert rules","description":"All alert rules for a tenant, enabled and disabled. Predicate JSON and destinations array follow the same shape the rule editor and cron evaluator consume.","operationId":"observatory_alerts_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-alerts.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenant":"landing","rules":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/recent":{"get":{"tags":["events"],"summary":"Recent events","description":"Newest honeypot events captured for a tenant. Pagination via `limit` (max 100). The same source the dashboard \"Recent\" panel renders.","operationId":"observatory_recent","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-recent.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenant":"landing","events":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/clusters":{"get":{"tags":["clusters"],"summary":"Fingerprint clusters","description":"Groups fingerprints by (ja4, uaClass, asn). Ranks by event count. Phase 18.2 deliverable — sibling page lives at /observatory/honeypot/clusters.","operationId":"observatory_clusters","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-clusters.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenant":"landing","window":"24h","clusters":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/investigate":{"get":{"tags":["investigate"],"summary":"Fingerprint investigation","description":"Full timeline for one fingerprint id: events, country trail, paths trail, JA4 drift, verified-vendor verdict. Phase 18.3 sibling page lives at /observatory/honeypot/investigate.","operationId":"observatory_investigate","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-investigate.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenant":"landing","fingerprintId":"fp_demo","found":false,"fingerprint":null,"events":[],"countriesSeen":[],"pathsSeen":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/fleet":{"get":{"tags":["fleet"],"summary":"Tenant fleet overview","description":"Cross-tenant KPIs: 24h/7d/30d event counts, last seen, cadence alarm. Phase 18.6 sibling page lives at /observatory/fleet.","operationId":"observatory_fleet","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-fleet.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenants":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/integrations/status":{"get":{"tags":["ops"],"summary":"Integration status","description":"Reports which downstream integrations are currently configured on this deployment (Slack alerts, Datadog forwarder, GitHub Actions dispatch, CSV incremental fetch). Secret values are never returned — only the configuration flag and notes.","operationId":"observatory_integrations_status","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-integrations-status.v1","generatedAt":"2026-05-25T12:00:00.000Z","integrations":[{"id":"slack","configured":true,"notes":"Per-rule webhooks."},{"id":"datadog","configured":false,"notes":"No-op without DATADOG_API_KEY."},{"id":"github_dispatch","configured":false,"notes":"No-op without GITHUB_DISPATCH_TOKEN."},{"id":"csv_since","configured":true,"notes":"?since= incremental fetch."}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/integrations/slack/test":{"post":{"tags":["ops"],"summary":"Test a Slack webhook","description":"Sends a one-line test message to a Slack incoming-webhook URL. Validates the webhook before saving it on an alert rule. The URL is never persisted by this endpoint — it is only forwarded once.","operationId":"observatory_integrations_slack_test","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-slack-test.v1","generatedAt":"2026-05-25T12:00:00.000Z","ok":true,"status":200}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/replay":{"get":{"tags":["ops"],"summary":"Deterministic replay comparator","description":"Re-runs the ingest derivation against the stored replay envelope for one eventId and reports the diff against the persisted row. With ?list=1, enumerates the most recently stored envelopes instead.","operationId":"observatory_replay","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-replay.v1","generatedAt":"2026-05-25T12:00:00.000Z","eventId":"hpe_abcdef0123456789abcdef01","matches":true,"fields":[],"envelope":{"schemaVersion":1,"receivedAt":"2026-05-25T11:00:00.000Z","sourceTenant":"landing","bodyBytes":1024}}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/ops/ingest":{"get":{"tags":["ops"],"summary":"Ingest SLO","description":"Latency percentiles (p50/p95/p99) + outcome rates for /api/observatory/honeypot/ingest over the requested window (1h, 24h, or 7d). Backed by per-request IngestSample rows.","operationId":"observatory_ops_ingest","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-ops-ingest.v1","generatedAt":"2026-05-25T12:00:00.000Z","window":"24h","windowStartIso":"2026-05-24T12:00:00.000Z","totalSamples":4221,"okCount":4198,"okRatePct":99.46,"signatureFailureCount":12,"signatureFailureRatePct":0.28,"p50Ms":38,"p95Ms":92,"p99Ms":154,"outcomeBreakdown":[{"outcome":"ok","count":4198}],"perTenantCount":[{"tenant":"landing","count":4221}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/ops/db-health":{"get":{"tags":["ops"],"summary":"DB health","description":"Row counts, recency, and the wall-clock duration of the heaviest dashboard-style query. Used to monitor Postgres free-space and query budget.","operationId":"observatory_ops_db-health","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-ops-db-health.v1","generatedAt":"2026-05-25T12:00:00.000Z","honeypotEventCount":41221,"fingerprintCount":612,"ingestSampleCount":4221,"honeypotEvent24hCount":1024,"oldestSampleIso":"2026-05-04T12:00:00.000Z","newestSampleIso":"2026-05-25T12:00:00.000Z","dashboardQueryMs":23}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/ops/cron-health":{"get":{"tags":["ops"],"summary":"Cron health","description":"Latest run per scheduled job + the most recent N runs across all jobs. Outcome, duration, stats, and error message preserved per row.","operationId":"observatory_ops_cron-health","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-ops-cron-health.v1","generatedAt":"2026-05-25T12:00:00.000Z","latestPerJob":[],"recent":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/ops/audit":{"get":{"tags":["ops"],"summary":"Operator audit log","description":"Recent operator actions (newest first). Includes verb, target, surface, and before/after JSON for state-changing actions.","operationId":"observatory_ops_audit","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-ops-audit.v1","generatedAt":"2026-05-25T12:00:00.000Z","rows":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/ops/heartbeat":{"get":{"tags":["ops"],"summary":"Tenant heartbeat alerts","description":"Tenants whose last-hour event count is zero against a non-zero 14-day baseline. Surfaced as alerts so operators can investigate silent fanout sources.","operationId":"observatory_ops_heartbeat","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-ops-heartbeat.v1","generatedAt":"2026-05-25T12:00:00.000Z","silentTenants":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/tarpits":{"get":{"tags":["events"],"summary":"Active auto-tarpits","description":"Fingerprints currently under auto-tarpit. A fingerprint enters tarpit when it emits >= LIAR_THRESHOLD events of UA class LIAR within a 10-minute window. Landing's honeypot router consults the bridge endpoint /api/observatory/honeypot/tarpit?fp=<id> and serves 8s-delayed responses while active.","operationId":"observatory_tarpits","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-tarpits.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenant":"landing","rows":[{"active":true,"fingerprintId":"fp_demo","tenant":"landing","triggeredAt":"2026-05-25T11:55:00.000Z","expiresAt":"2026-05-25T12:55:00.000Z","delayMs":8000,"reason":"liar-flood:9/10min","liarEventCount":9}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/lures":{"get":{"tags":["events"],"summary":"Lure registry","description":"Canonical bait-family catalogue with 24h / 7d hit counts, unique IPs, unique fingerprints, top UA class, and last-hit timestamp per family. Drives the /observatory/honeypot/lures operator page.","operationId":"observatory_lures","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-lures.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenant":"landing","totals":{"families":26,"hits24h":412,"hits7d":1840},"families":[{"id":"wp-admin","title":"WordPress admin","kind":"cms-admin","intent":"WordPress dashboard takeover / login brute force.","pathLike":"/wp-admin%","hits24h":122,"hits7d":612,"uniqueIpHashes24h":34,"uniqueFingerprints24h":18,"topUaClass":"UNKNOWN","lastHitAt":"2026-05-25T11:58:00.000Z"}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/sitemap":{"get":{"tags":["events"],"summary":"Bait sitemap stats","description":"Aggregates the three generated bait roots (/honeypot/topics/, /q/, /agents/) by 24h / 7d hit counts plus the top single path per bucket. Drives the /observatory/honeypot/sitemap operator page.","operationId":"observatory_sitemap","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-sitemap.v1","generatedAt":"2026-05-25T12:00:00.000Z","tenant":"landing","buckets":[{"rootPath":"/honeypot/topics/","hits24h":88,"hits7d":410,"uniqueIpHashes24h":22,"topPath":"/honeypot/topics/evaluation-pipeline-a1b2c3d4","topPathCount":11}],"uniqueGeneratedUrlsSeen7d":612,"totalGeneratedHits7d":1840}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/honeypot/test-event":{"post":{"tags":["test"],"summary":"Emit a synthetic test event","description":"Pushes one synthetic signed honeypot event through the full ingest pipeline. Used by the new-tenant SetupHint to light up the dashboard in <30s.","operationId":"observatory_test-event","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-test-event.v1","ok":true,"tenant":"landing","eventId":"evt_demo","path":"/honeypot/topics/synthetic","uaClass":"KNOWN_LLM_AGENT","acceptedAt":"2026-05-25T12:00:00.000Z"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/tokens":{"get":{"tags":["meta"],"summary":"List API tokens","description":"Tokens the calling session has issued. Each token is hashed at rest; only the prefix and metadata are returned. The plaintext is only ever shown once at creation.","operationId":"observatory_tokens_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-tokens.v1","generatedAt":"2026-05-25T12:00:00.000Z","tokens":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]},"post":{"tags":["meta"],"summary":"Create API token","description":"Issues a new scoped bearer token for programmatic access. Returns the plaintext exactly once. Hash is stored server-side; subsequent reads return only the prefix.","operationId":"observatory_tokens_create","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-token-create.v1","id":"tok_demo","name":"CI ingest probe","scopes":["read:dashboard"],"token":"eatok_xxxxxxxx","prefix":"eatok_xx","createdAt":"2026-05-25T12:00:00.000Z"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/{toolset}/dashboard":{"get":{"tags":["empire"],"summary":"Empire toolset dashboard","description":"Per-toolset KPI strip + event-type breakdown + per-tenant breakdown + hourly buckets for the empire event substrate. Toolset segment is dynamic; metrics config is resolved from the empire metrics registry. Sibling UI page lives at /observatory/empire/<toolset>.","operationId":"empire_dashboard","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-dashboard.v1","generatedAt":"2026-05-25T12:00:00.000Z","toolset":"EchoAtlasCodex","tenant":null,"window":"24h","blurb":"Topic generation, scoring, and corpus stewardship for the empire.","kpis":[{"id":"totalEvents","label":"Events","unit":null,"value":0,"precision":0}],"eventTypeBreakdown":[],"tenantBreakdown":[],"eventsPerHour":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/{toolset}/alerts":{"get":{"tags":["empire"],"summary":"List empire alert rules","description":"Empire-scoped alert rules for a toolset. Predicates use the empire_count_gte / cross_toolset_join variants; destinations share the honeypot fanout. Sibling UI lives at /observatory/empire/<toolset>/alerts.","operationId":"empire_alerts_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-alerts.v1","generatedAt":"2026-05-25T12:00:00.000Z","toolset":"EchoAtlasCodex","tenant":"landing","rules":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Create empire alert rule","description":"Creates an alert rule scoped to a single toolset. Predicate JSON is validated against the empire predicate union; destinations follow the same shape as honeypot rules.","operationId":"empire_alerts_create","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-alert-create.v1","ok":true,"id":"alr_demo","toolset":"EchoAtlasCodex"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/notes":{"get":{"tags":["empire"],"summary":"Read empire investigation note","description":"Fetches the pinned investigation note for a given empire event id.","operationId":"empire_notes_read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-note.v1","generatedAt":"2026-05-25T12:00:00.000Z","eventId":"evt_demo","tenant":"landing","note":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Upsert empire investigation note","description":"Pins an investigation note to an empire event id. Same lifecycle semantics as the honeypot fingerprint notes; the row carries a targetKind discriminator.","operationId":"empire_notes_upsert","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-note-upsert.v1","ok":true,"id":"inv_demo","eventId":"evt_demo","updatedAt":"2026-05-25T12:00:00.000Z"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/triage/proposals":{"get":{"tags":["empire"],"summary":"List triage proposals","description":"Recent AI-classified proposals (pending / accepted / rejected / auto-rejected). Filter via ?status=, ?kind=, ?toolset=. Driven by the active TriageAgent + Phase 21 prompt versioning.","operationId":"empire_triage_proposals_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-proposals.v1","generatedAt":"2026-05-25T12:00:00.000Z","proposals":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Trigger triage pass","description":"Operator-triggered one-off triage run. Hands the last N minutes of empire events to the active TriageAgent and persists the resulting proposals. Use windowMinutes to override the default 15m window; stub=true returns a deterministic empty pass for tests.","operationId":"empire_triage_proposals_run","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-run.v1","ok":true,"runId":"tr_demo","proposalCount":0,"autoRejected":0,"windowStart":"2026-05-25T11:45:00.000Z","windowEnd":"2026-05-25T12:00:00.000Z","agentVersion":"incident-classifier-v1"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/triage/decide":{"post":{"tags":["empire"],"summary":"Decide on a triage proposal","description":"Operator accept/reject for a single proposal. Accepting an incident proposal also opens the EmpireIncident with the proposal payload and seeds the postmortem with the LLM-authored draft when present.","operationId":"empire_triage_decide","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-decide.v1","ok":true,"id":"trp_demo","decision":"accepted","openedIncidentId":"inc_demo","createdSavedViewId":null,"createdAlertRuleId":null}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/triage/investigations":{"get":{"tags":["empire"],"summary":"List triage investigation proposals","description":"Recent AI-proposed saved investigations. Each payload carries a name, a 3-bullet rationale, and a fixed-shape filter set the SavedView loader understands. Accepting an investigation writes a SavedView row.","operationId":"empire_triage_investigations_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-investigations.v1","generatedAt":"2026-05-25T12:00:00.000Z","proposals":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Trigger investigation proposer","description":"Operator-triggered investigation proposal pass. When focalEventId is set, the proposer narrows around that event; otherwise it emits up to three cluster-level investigations.","operationId":"empire_triage_investigations_run","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-investigation-run.v1","ok":true,"runId":"tr_demo","proposalCount":0,"autoRejected":0,"windowStart":"2026-05-25T11:45:00.000Z","windowEnd":"2026-05-25T12:00:00.000Z","focalEventId":null}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/triage/rules":{"get":{"tags":["empire"],"summary":"List triage alert-rule proposals","description":"Recent AI-proposed alert rules. Each proposal carries a predicate + backtest counts (TP/FP/meanLeadTimeSec) computed against the last 30 days of resolved incidents. Accepting a proposal via the decide endpoint requires destinations.","operationId":"empire_triage_rules_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-rules.v1","generatedAt":"2026-05-25T12:00:00.000Z","proposals":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Trigger alert-rule proposer","description":"Operator-triggered rule-proposer pass. Walks resolved EmpireIncidents in the last lookbackDays (default 30); the LLM proposes predicates and the backtest harness scores each one. Continuous learning: recent accepted/rejected decisions are fed back as few-shot examples (cap 10 each).","operationId":"empire_triage_rules_run","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-rule-run.v1","ok":true,"runId":"tr_demo","proposalCount":0,"autoRejected":0,"lookbackDays":30}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/triage/rules/bulk-accept":{"post":{"tags":["empire"],"summary":"Bulk-accept alert-rule proposals","description":"Accept K rule proposals at once. Each becomes an AlertRule with the supplied destinations. The decision is recorded against every proposal; failures (e.g. predicate schema invalid) surface in the per-id results array without aborting the batch.","operationId":"empire_triage_rules_bulk-accept","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-bulk-accept.v1","ok":true,"acceptedCount":0,"results":[]}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/incidents/[id]/auto-draft":{"post":{"tags":["empire"],"summary":"LLM-draft incident postmortem","description":"Asks the postmortem writer to emit a 5-section postmortem for the named EmpireIncident. The lineage walk + incident summary are passed to the LLM; the anti-confabulation guard strips claims that reference event ids outside the lineage. The draft is returned but only persisted when body.persist=true.","operationId":"empire_incidents_auto-draft","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-incident-auto-draft.v1","ok":true,"id":"inc_demo","runId":"tr_demo","draft":"## Summary\n...","rawDraft":"## Summary\n...","citedEventIds":[],"strippedEventIds":[],"persisted":false}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/forecast":{"get":{"tags":["empire"],"summary":"Per-toolset anomaly forecast","description":"Most recent forecast row per toolset. Score 0..1 with level derived as <0.4=none, 0.4..0.7=watch, >0.7=alert. Cron-populated every 15 minutes; 0.7 score sustained for 3 consecutive runs opens a minor EmpireIncident.","operationId":"empire_forecast_read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-forecast.v1","generatedAt":"2026-05-25T12:00:00.000Z","forecasts":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Trigger anomaly forecaster","description":"Operator-triggered forecaster pass. Walks the last 7 days of per-toolset hourly buckets, hands the digest to the LLM, persists one EmpireForecast row per toolset, and applies the 3-run alert latch.","operationId":"empire_forecast_run","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-forecast-run.v1","ok":true,"runId":"tr_demo","forecastsCreated":0,"latchedIncidentIds":[]}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/triage/replay/[runId]":{"get":{"tags":["empire"],"summary":"Triage run replay comparator","description":"Fetches the persisted replay envelope for the named TriageRun and reports the deterministic digest over (systemPromptHash, userMessage, llmText). Drift on the digest across replays means the orchestrator inputs or the LLM response have changed; same envelope re-fetches return byte-identical digests.","operationId":"empire_triage_replay","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-replay.v1","generatedAt":"2026-05-25T12:00:00.000Z","runId":"tr_demo","digest":"<sha256>","schemaVersion":1,"agentVersion":"incident-classifier-v1","systemPromptHash":"<sha256>","userMessageBytes":0,"llmResponseBytes":0,"proposalIds":[],"startedAt":"2026-05-25T12:00:00.000Z","finishedAt":"2026-05-25T12:00:00.000Z"}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/triage/metrics":{"get":{"tags":["empire"],"summary":"Triage agent trust metrics","description":"Per-agent metrics derived from the TriageProposal log: acceptedRate, precisionPct, recallPct, meanTimeToDecisionSec, badge. The auto-rollback gate uses the same numbers: when an active agent dips below 30% acceptedRate over 24h of decided proposals, the next cron run reverts to the previous version automatically.","operationId":"empire_triage_metrics","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-metrics.v1","generatedAt":"2026-05-25T12:00:00.000Z","agents":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/triage/budget":{"get":{"tags":["empire"],"summary":"Triage cost budget","description":"Per-month, per-toolset LLM cost budget rows. Recomputes spend from TriageRun token totals on every GET. When spent >= budget the cron defers non-safety prompts (rule proposer, investigation proposer, forecaster, postmortem writer); the incident classifier always runs.","operationId":"empire_triage_budget_read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-budget.v1","generatedAt":"2026-05-25T12:00:00.000Z","month":"2026-05","budgets":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Operator override on the triage budget","description":"Raise budgetUsdCents and/or set degradedUntil (ISO-8601 or null) for a budget row. Defaults to the empire-wide bucket (toolset=*) and the current month.","operationId":"empire_triage_budget_update","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-triage-budget.v1","ok":true,"month":"2026-05","toolset":"*","budgetUsdCents":20000,"spentUsdCents":0,"pctSpent":0,"isDegraded":false,"degradedUntil":null}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/slo":{"get":{"tags":["empire"],"summary":"Empire ingest SLO","description":"Cross-toolset p50/p95/p99 latency + OK rate over a window. Same shape as the honeypot ingest SLO but partitioned by toolset.","operationId":"empire_slo","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-slo.v1","generatedAt":"2026-05-25T12:00:00.000Z","window":"24h","windowStartIso":"2026-05-24T12:00:00.000Z","rows":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/{toolset}/replay":{"get":{"tags":["empire"],"summary":"Empire replay comparator","description":"Diff a stored replay envelope against the live EmpireEvent row for one event. With ?list=1, enumerates the most recently stored envelopes for the toolset.","operationId":"empire_replay","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-replay.v1","generatedAt":"2026-05-25T12:00:00.000Z","toolset":"EchoAtlasCodex","eventId":"evt_demo","matches":true,"fields":[],"envelope":{"schemaVersion":1,"receivedAt":"2026-05-25T11:00:00.000Z","sourceTenant":"landing","eventType":"topic.scored","bodyBytes":256,"causedBy":null}}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/lineage/{eventId}":{"get":{"tags":["empire"],"summary":"Empire event lineage","description":"Walks the causedBy chain upstream + downstream from a focal event id. Returns the node list the UI lineage page renders.","operationId":"empire_lineage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-lineage.v1","generatedAt":"2026-05-25T12:00:00.000Z","focal":null,"upstream":[],"downstream":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/overview":{"get":{"tags":["empire"],"summary":"Federated empire overview","description":"Operator-only alias for /empire/overview?federated=1. Fans out the cross-toolset overview to every active region via the bridge token, then deterministically merges by toolset (24h + 7d sums, lastEventAt = max, per-region breakdown). Failed regions are captured (not thrown) so partial-fleet visibility is always available.","operationId":"empire_federated-overview","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-federated-overview.v1","generatedAt":"2026-05-26T00:00:00.000Z","participatingRegions":["us-east"],"failedRegions":[],"toolsets":[{"toolset":"EchoAtlasCodex","blurb":"Topic generation, scoring, and corpus stewardship for the empire.","events24h":0,"events7d":0,"lastEventAt":null,"sparkline24h":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"perRegion":{"us-east":0}}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/compliance/bundle":{"post":{"tags":["empire"],"summary":"Build a compliance evidence bundle","description":"Generates the per-control evidence artifacts (JSON snapshot, CSV row dump, PDF summary) for the latest evaluation of every control under a framework. Returns a manifest with per-artifact sha256 + pre-signed Blob URLs the caller can fetch and zip.","operationId":"empire_compliance_bundle","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-bundle.v1","framework":"soc2-2017","generatedAt":"2026-05-25T12:00:00.000Z","artifacts":[],"manifestText":"","manifestSha256":""}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/graph/replay/{materialisationId}":{"get":{"tags":["empire"],"summary":"Knowledge graph replay comparator","description":"Loads the gzipped graph snapshot stored at cron-tick time, hashes the live graph with graphDigest(), and reports the diff. Returns matches=true when the stored digest equals the live digest; otherwise lists added/removed node keys + edge keys (capped at 50 each).","operationId":"empire_graph_replay","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-graph-replay.v1","materialisationId":"crn_demo","matches":true,"storedDigest":"d986dab01803db7eec7ca293405bc19ddbe2ff33b8b05d86a520de7481802e80","liveDigest":"d986dab01803db7eec7ca293405bc19ddbe2ff33b8b05d86a520de7481802e80","storedAt":"2026-05-25T12:00:00.000Z","liveAt":"2026-05-25T12:05:00.000Z","addedNodes":[],"removedNodes":[],"addedEdges":[],"removedEdges":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/graph/slo":{"get":{"tags":["empire"],"summary":"Knowledge graph SLO","description":"Materialisation lag per node kind + the worst-case overall lag. Used by the lag-based graph_lag_gte alert predicate and the operator badge on /observatory/empire/graph/materialiser.","operationId":"empire_graph_slo","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-graph-slo.v1","generatedAt":"2026-05-25T12:00:00.000Z","worstLagSec":null,"worstLagKind":null,"overallBadge":"unknown","perKind":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/graph/materialiser":{"get":{"tags":["empire"],"summary":"Knowledge graph materialiser status","description":"Status surface for the /api/cron/knowledge-graph-materialise cron. Returns the latest run + recent runs, per-kind node + edge counts, and the oldest materialisedAt lag. Sibling UI lives at /observatory/empire/graph.","operationId":"empire_graph_materialiser","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-graph-materialiser.v1","generatedAt":"2026-05-25T12:00:00.000Z","latestRun":null,"recentRuns":[],"totals":{"nodes":0,"edges":0},"nodesByKind":[],"edgesByKind":[],"oldestMaterialisedAt":null,"oldestLagSec":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/graph/query":{"post":{"tags":["empire"],"summary":"Run a knowledge graph DSL query","description":"Executes a read-only DSL query against the empire knowledge graph. Body accepts `query` (the DSL string), optional `limit`, and optional `savedName` to replay a named saved query. Returns the QueryEnvelope: rows + projected nodes + edges + the parsed AST.","operationId":"empire_graph_query","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-graph-query.v1","query":"(p:postmortem) RETURN p LIMIT 10","ast":null,"rowCount":0,"rows":[],"nodes":[],"edges":[],"generatedAt":"2026-05-25T12:00:00.000Z"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/graph/saved-queries":{"get":{"tags":["empire"],"summary":"List saved knowledge graph queries","description":"Returns every saved query visible to the caller: workspace + public + the caller's own private rows. Sorted by name. Sibling UI lives at /observatory/empire/graph (left sidebar).","operationId":"empire_graph_saved-queries_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-graph-saved-queries.v1","generatedAt":"2026-05-25T12:00:00.000Z","rows":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Save a knowledge graph query","description":"Upserts a named saved query for the caller. The DSL is validated before persistence — invalid queries are rejected with the parser's reason. Visibility ∈ private | workspace | public.","operationId":"empire_graph_saved-queries_save","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-graph-saved-queries.v1","ok":true}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/compliance/subprocessors":{"get":{"tags":["empire"],"summary":"Subprocessor registry list","description":"Operator view of the empire subprocessor registry (GDPR Art.28 / SOC 2 vendor-management). ?includeArchived=1 returns archived rows too. POST upserts; per-row archive/review actions go to /subprocessors/{id}.","operationId":"empire_compliance_subprocessors","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-subprocessors.v1","generatedAt":"2026-05-25T12:00:00.000Z","rows":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Upsert a subprocessor","description":"Add or update a subprocessor row by unique name. markReviewed=true also stamps lastReviewedAt to now and records the calling operator as reviewer. Requires write:alerts scope.","operationId":"empire_compliance_subprocessors_upsert","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-subprocessor.v1","ok":true,"row":{}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/compliance/subprocessors/{id}":{"post":{"tags":["empire"],"summary":"Per-row subprocessor action","description":"Body { action: \"review\" | \"archive\" }. review stamps lastReviewedAt + reviewerOperatorId. archive sets archivedAt to now so the predicate stops counting it.","operationId":"empire_compliance_subprocessors_action","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-subprocessor.v1","ok":true}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/regions":{"get":{"tags":["empire"],"summary":"List empire regions","description":"Returns the active EmpireRegion registry plus the deploy's currentRegion(). Used by the federation layer + the partner self-service region picker. Falls back to a built-in seed when the table is empty.","operationId":"empire_regions_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-regions.v1","generatedAt":"2026-05-26T00:00:00.000Z","currentRegion":"us-east","rows":[{"key":"us-east","displayName":"US East (Virginia)","vercelProjectSlug":"echoatlas-observatory","postgresHost":"","latencyTierSec":0.18,"status":"active","isPrimary":true}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/region-digest":{"get":{"tags":["empire"],"summary":"Local region digest tuple","description":"Returns the local deploy's (regionKey, migrationDigest, graphDigest, complianceDigest) tuple. The federator fans this out across regions and asserts migration parity before merging.","operationId":"empire_region-digest_local","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-region-digest-local.v1","generatedAt":"2026-05-26T00:00:00.000Z","region":"us-east","tuple":{"regionKey":"us-east","migrationDigest":"b16de63c6bcb696d0d25a9816a98ae75884627d43282b3a5435f6515dfe2133c","graphDigest":"d986dab01803db7eec7ca293405bc19ddbe2ff33b8b05d86a520de7481802e80","complianceDigest":"cdd9a95de13a8d5265498694ecb4cdb084c0994b62eee370476b8bb7863991da"}}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Federated region digest comparison","description":"Operator-only. Fans the region-digest call out to every active region via the bridge token, asserts migration parity, and returns the federation digest. The empire-wide determinism gate pins the digest under docs/empire-determinism/federation/.","operationId":"empire_region-digest_federate","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-region-digest.v1","generatedAt":"2026-05-26T00:00:00.000Z","participating":["us-east"],"failed":[],"tuples":[{"regionKey":"us-east","migrationDigest":"b16de63c6bcb696d0d25a9816a98ae75884627d43282b3a5435f6515dfe2133c","graphDigest":"d986dab01803db7eec7ca293405bc19ddbe2ff33b8b05d86a520de7481802e80","complianceDigest":"cdd9a95de13a8d5265498694ecb4cdb084c0994b62eee370476b8bb7863991da"}],"federationDigest":"e97b00f7f54cbc4311e5a531c1bc331b7a8fab0a59224e0205630c9d30365817","migrationParity":"matched","migrationDriftDetails":[]}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/federation-replay":{"post":{"tags":["empire"],"summary":"Federation determinism replay","description":"Takes a previously-captured federation digest (and optionally the per-region tuple list that produced it) and reports whether the live empire still yields the same digest. Used by the cross-repo hash chain to assert federation parity across runs.","operationId":"empire_federation-replay","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-federation-replay.v1","generatedAt":"2026-05-26T00:00:00.000Z","pinnedDigest":"e97b00f7f54cbc4311e5a531c1bc331b7a8fab0a59224e0205630c9d30365817","liveDigest":"e97b00f7f54cbc4311e5a531c1bc331b7a8fab0a59224e0205630c9d30365817","matches":true,"participating":["us-east"],"failed":[],"migrationParity":"matched","pinnedTuplesDigest":null,"driftDetail":[]}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/trust/snapshot":{"get":{"tags":["empire"],"summary":"Trust & Safety snapshot","description":"Single composed envelope over compliance posture, federation integrity, graph freshness, open incidents, operator audit, and per-region drift. The same shape the /observatory/trust page renders. Used by operator dashboards + the trust MCP tools.","operationId":"empire_trust_snapshot","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-trust-snapshot.v1","generatedAt":"2026-05-27T00:00:00.000Z","overallBadge":"green","dimensions":[{"key":"posture","title":"Compliance posture","badge":"green","summary":"4 green / 0 not green across 4 frameworks.","drillIn":"/observatory/compliance","apiDrillIn":"/api/observatory/v1/compliance/posture"}],"numbers":{"postureGreen":4,"postureFailing":0,"federationDigest":"e97b00f7f54cbc4311e5a531c1bc331b7a8fab0a59224e0205630c9d30365817","migrationParity":"matched","graphLagSec":45,"openIncidents":0,"recentAudits24h":12,"driftEvents24h":0},"score":{"score":100,"badge":"green","breakdown":[{"dimension":"integrity","weight":30,"badge":"green","contribution":30},{"dimension":"posture","weight":25,"badge":"green","contribution":25},{"dimension":"freshness","weight":15,"badge":"green","contribution":15},{"dimension":"incidents","weight":15,"badge":"green","contribution":15},{"dimension":"audit","weight":10,"badge":"green","contribution":10},{"dimension":"drift","weight":5,"badge":"green","contribution":5}]}}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/trust/drift":{"get":{"tags":["empire"],"summary":"Trust Drift Inbox list","description":"Returns the current trust drift inbox: cross-system divergence events auto-opened by five detectors (graph-drift, replay-drift, region-parity, compliance-fail, integrity-mismatch). Filter via ?status=open|acknowledged|resolved|auto-resolved|wont-fix|all (default open), ?kind=<one of the five>, ?limit=1-500 (default 100), ?sinceHours=N (default unlimited).","operationId":"empire_trust_drift_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-trust-drift-list.v1","generatedAt":"2026-05-27T00:00:00.000Z","filter":{"status":"open","kind":null,"limit":100,"sinceHours":null},"events":[{"id":"cuid_example","kind":"graph-drift","source":"graph-slo","sourceId":"kind:event","description":"Graph kind event lag 120s exceeds 60s threshold.","severity":"minor","status":"open","openedAt":"2026-05-27T00:00:00.000Z","acknowledgedAt":null,"resolvedAt":null,"resolvedBy":null,"resolutionNotes":"","payload":{"lagSec":120,"kind":"event"}}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Trust Drift Inbox auto-open scan","description":"Runs the five auto-open detectors (graph SLO, federation digest, compliance posture - inline; replay + integrity from upstream consumers) and inserts a TrustDriftEvent per fresh breach. Idempotent: re-running on a stable empire produces zero new rows. The Phase 25.4 auto-remediator cron invokes this every 5 minutes.","operationId":"empire_trust_drift_auto-open","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-trust-drift-auto-open.v1","scannedAt":"2026-05-27T00:00:00.000Z","openedNow":[],"alreadyOpen":0,"detectorErrors":[]}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/trust/drift/{id}/ack":{"post":{"tags":["empire"],"summary":"Acknowledge a drift event","description":"Marks a TrustDriftEvent as acknowledged. Stamps acknowledgedAt; the dedupe partial unique index releases the (kind, source, sourceId) slot so a fresh auto-open can fire if the underlying drift reappears later.","operationId":"empire_trust_drift_ack","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-trust-drift-ack.v1","ok":true}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/trust/drift/{id}/resolve":{"post":{"tags":["empire"],"summary":"Resolve a drift event","description":"Marks a TrustDriftEvent as resolved (manual fix), auto-resolved (Phase 25.4 cron mechanically cleared it), or wont-fix (expected divergence). Body { status?: \"resolved\" | \"auto-resolved\" | \"wont-fix\", notes?: string }.","operationId":"empire_trust_drift_resolve","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-trust-drift-resolve.v1","ok":true}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/account/passkeys":{"get":{"tags":["ops"],"summary":"Operator passkey inventory","description":"Lists the calling operator's WebAuthn passkeys. Each row exposes the credential id prefix, device label, transports, and lifecycle timestamps. No public key material is returned.","operationId":"account_passkeys_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-passkeys-list.v1","generatedAt":"2026-05-28T00:00:00.000Z","passkeys":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/account/passkeys/register/start":{"post":{"tags":["ops"],"summary":"Begin operator passkey registration","description":"Returns a WebAuthn registration challenge envelope for the calling operator. The client passes `options` to @simplewebauthn/browser startRegistration() and POSTs the resulting attestation to /register/finish.","operationId":"account_passkeys_register_start","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-passkeys-register-start.v1","challengeId":"cuid_example","options":{"challenge":"base64url...","rp":{"id":"example.com","name":"EchoAtlas"}}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/account/passkeys/register/finish":{"post":{"tags":["ops"],"summary":"Finish operator passkey registration","description":"Validates the attestation against the challenge issued by /register/start, persists the new Passkey credential, and writes an OperatorAudit row with verb passkey.register.","operationId":"account_passkeys_register_finish","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-passkeys-register-finish.v1","ok":true,"passkey":{"id":"cuid_example","credentialIdPrefix":"AAEC...","deviceLabel":"MacBook","createdAt":"2026-05-28T00:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/account/passkeys/{id}/revoke":{"post":{"tags":["ops"],"summary":"Revoke an operator passkey","description":"Stamps revokedAt on the calling operator's passkey. Revoked credentials are kept in the table for audit but cannot complete assertions any more.","operationId":"account_passkeys_revoke","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-passkeys-revoke.v1","ok":true,"id":"cuid_example"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/account/step-up/required":{"get":{"tags":["ops"],"summary":"List step-up-gated routes","description":"Returns the canonical catalogue of routes currently gated by Phase 26.3 step-up. Agents can pre-plan their flow by reading this catalogue before they POST to a gated route; the same shape backs the empire CLI `echoatlas step-up --list` command.","operationId":"account_step-up_required","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-step-up-required-list.v1","generatedAt":"2026-05-28T00:00:00.000Z","routes":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/empire/region-slo":{"get":{"tags":["empire"],"summary":"Per-region SLO + lag","description":"Per-region rollup of p95 ingest latency, OK rate, graph materialisation lag, and migration-digest parity. ?local=1 returns only the calling deploy's row; the default fans out across active regions via the bridge token.","operationId":"empire_region-slo","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-empire-region-slo.v1","generatedAt":"2026-05-26T00:00:00.000Z","rows":[{"regionKey":"us-east","displayName":"US East (Virginia)","isPrimary":true,"status":"active","p95LatencyMs":180,"okRatePct":99.8,"graphLagSec":45,"replayParity":"matched","migrationDigest":"b16de63c6bcb696d0d25a9816a98ae75884627d43282b3a5435f6515dfe2133c","worstLagSec":45,"badge":"green"}],"failed":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/compliance/replay/{evaluationId}":{"get":{"tags":["empire"],"summary":"Compliance evaluation replay","description":"Loads a persisted ComplianceEvaluation row and reports whether replaying its observed JSON yields the same outcome. The persisted observed payload is the predicate's deterministic input, so this is a digest-style integrity check rather than a live re-evaluation.","operationId":"empire_compliance_replay","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-replay.v1","evaluationId":"cev_demo","framework":"soc2-2017","controlId":"CC7.4","predicateKey":"postmortem-required-major","storedOutcome":"pass","replayedOutcome":"pass","matches":true,"runAt":"2026-05-25T12:00:00.000Z","storedSummary":"0 resolved major/critical incidents missing postmortem (of 5)","replayedSummary":"0 resolved major/critical incidents missing postmortem (of 5)","observed":{"totalResolved":5,"offenderIds":[]}}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/compliance/narrator":{"post":{"tags":["empire"],"summary":"Run the compliance narrator","description":"Produces a one-paragraph plain-text summary of the current compliance posture. Strictly non-authoritative: the confabulation guard strips any sentence referencing a framework / controlId not present in the posture input. GET forces deterministic stub-mode (no ANTHROPIC_API_KEY required); POST runs the live narrator when configured.","operationId":"empire_compliance_narrator","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-narrator.v1","text":"EchoAtlas continuously evaluates...","scrubbedText":"EchoAtlas continuously evaluates...","citedFrameworks":[],"strippedFrameworks":[],"citedControlIds":[],"strippedControlIds":[],"runId":"cnar_demo","modelOrStub":"stub","replayUrl":null,"inputDigest":""}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/compliance/frameworks/{framework}":{"get":{"tags":["empire"],"summary":"Compliance per-framework drill-in","description":"Returns every control in the framework with its latest evaluation (outcome, runAt, summary, evidence URL). Used by the /observatory/compliance/[framework] operator drill-in page.","operationId":"empire_compliance_framework","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-framework.v1","generatedAt":"2026-05-25T12:00:00.000Z","framework":"soc2-2017","title":"SOC 2 Type II (Trust Services Criteria)","version":"2017-Common Criteria","status":"active","controls":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/compliance/override":{"post":{"tags":["empire"],"summary":"Record a manual compliance override","description":"Operator-filed manual-review evaluation when the underlying predicate is wrong or insufficient. Recorded as ComplianceEvaluation row with evaluatedBy=operator:<userId> + a required note. Audit-tracked via OperatorAudit. Requires write:alerts scope.","operationId":"empire_compliance_override","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-override.v1","ok":true,"evaluationId":"cev_demo"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/compliance/posture":{"get":{"tags":["empire"],"summary":"Compliance posture snapshot","description":"Latest per-framework posture (totalControls, passing, failing, manualReview, nA, asOf) plus a green/yellow/red badge derived from the passing rate against (total - nA). Refreshed hourly by the compliance-evaluate cron. Sibling UI lives at /observatory/compliance.","operationId":"empire_compliance_posture","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-compliance-posture.v1","generatedAt":"2026-05-25T12:00:00.000Z","frameworks":[{"framework":"soc2-2017","title":"SOC 2 Type II (Trust Services Criteria)","version":"2017-Common Criteria","totalControls":8,"passing":7,"failing":0,"manualReview":1,"nA":0,"asOf":"2026-05-25T11:55:00.000Z","badge":"green","ratePct":87.5}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/analytics/query":{"get":{"tags":["empire"],"summary":"Analytics warehouse query","description":"Single-metric, date-range query against the Phase 28.0 analytics warehouse. Required query params: metric=<key>, from=<ISO date>, to=<ISO date> (exclusive). Optional: dims=<csv of workspaceId|toolset|severity|region>, workspace=<id>, toolset=<name>, severity=<value>, region=<id>. Returns dimDate-asc series + totals. Same warehouse state -> byte-identical envelope (excluding generatedAt).","operationId":"empire_analytics_query","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-analytics-query.v1","generatedAt":"2026-05-27T00:00:00.000Z","metric":"incidents-opened-daily","from":"2026-04-01T00:00:00.000Z","to":"2026-05-01T00:00:00.000Z","dims":["severity"],"filters":{},"total":{"value":3,"count":3},"series":[{"dimDate":"2026-04-14","dimWorkspaceId":"","dimToolset":"","dimSeverity":"minor","dimRegion":"","value":2,"count":2},{"dimDate":"2026-04-22","dimWorkspaceId":"","dimToolset":"","dimSeverity":"major","dimRegion":"","value":1,"count":1}],"unknownMetric":false,"invalidDims":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/analytics/metrics":{"get":{"tags":["empire"],"summary":"Analytics metric catalog","description":"Lists every metric registered in the Phase 28.0 analytics catalog with its title, description, and declared dimensions. Lets agents introspect what they can query.","operationId":"empire_analytics_metrics","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-analytics-metrics.v1","generatedAt":"2026-05-27T00:00:00.000Z","metrics":[{"key":"incidents-opened-daily","title":"Empire incidents opened per day","description":"Count of EmpireIncident rows whose startedAt falls in each UTC day, broken down by severity. Dimensions: severity.","dimensions":["severity"]}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/analytics/cohort":{"get":{"tags":["empire"],"summary":"Analytics cohort retention curve","description":"Per-day retention buckets for a registered cohort. Required query params: key=<cohortKey>, from=<ISO date>, to=<ISO date> (exclusive). Returns buckets[] sorted by day ascending with retained + pct columns. Phase 28.11 ships two cohorts: developer-token-7d (signup -> first token mint) and incident-postmortem-7d (incident opened -> postmortem published).","operationId":"empire_analytics_cohort","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-analytics-cohort.v1","generatedAt":"2026-05-27T00:00:00.000Z","key":"developer-token-7d","anchorLabel":"developer signup","followLabel":"first token mint","from":"2026-02-26T00:00:00.000Z","to":"2026-05-26T00:00:00.000Z","cohortSize":12,"buckets":[{"day":0,"retained":4,"pct":0.3333},{"day":1,"retained":7,"pct":0.5833},{"day":3,"retained":9,"pct":0.75},{"day":7,"retained":10,"pct":0.8333},{"day":14,"retained":10,"pct":0.8333},{"day":30,"retained":11,"pct":0.9167}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/cost/rollup":{"get":{"tags":["empire"],"summary":"Cost Center rollup","description":"Per-(toolset|vendor|sku) spend rollup over a [from, to) window. Required query params: from=<ISO date>, to=<ISO date> (exclusive), group=toolset|vendor|sku (default toolset). Optional filters: workspace, toolset, vendor, region. Returns rows sorted by amountUsd desc with deterministic key-asc tiebreak.","operationId":"empire_cost_rollup","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-cost-rollup.v1","generatedAt":"2026-05-27T12:00:00.000Z","from":"2026-04-27T00:00:00.000Z","to":"2026-05-27T00:00:00.000Z","group":"toolset","filters":{},"total":{"amountUsd":4.83,"rows":3},"rows":[{"key":"EchoAtlasObservatory","amountUsd":3.4,"units":34000,"dayCount":1},{"key":"EchoAtlasCodex","amountUsd":1.25,"units":12500,"dayCount":1},{"key":"EchoAtlasLanding","amountUsd":0.18,"units":1.8,"dayCount":1}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/cost/by-toolset":{"get":{"tags":["empire"],"summary":"Cost Center per-toolset curve","description":"Per-day spend curve for a single toolset over a [from, to) window. Required query params: toolset=<name>, from=<ISO date>, to=<ISO date> (exclusive). Returns series sorted by dimDate ascending.","operationId":"empire_cost_by-toolset","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-cost-by-toolset.v1","generatedAt":"2026-05-27T12:00:00.000Z","toolset":"EchoAtlasObservatory","from":"2026-04-27T00:00:00.000Z","to":"2026-05-27T00:00:00.000Z","total":{"amountUsd":102.5},"series":[{"dimDate":"2026-05-26","amountUsd":3.4},{"dimDate":"2026-05-27","amountUsd":3.6}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/cost/budgets":{"get":{"tags":["empire"],"summary":"Cost Center budget summary","description":"Lists every registered CostBudget alongside its observed-vs-budget spend for the current calendar month and the alertAtPct thresholds the spend has already crossed. Drives the budget alert engine in Phase 29.5.","operationId":"empire_cost_budgets","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-cost-budgets.v1","generatedAt":"2026-05-27T12:00:00.000Z","budgets":[{"id":"bud_empire_demo","scope":"empire","scopeKey":"","monthlyUsd":1500,"alertAtPct":[50,80,100],"observedUsd":845.42,"observedPct":56.36,"monthBucket":"2026-05","crossedThresholds":[50]}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/cost/alerts":{"get":{"tags":["empire"],"summary":"Cost Center alert inbox","description":"Lists CostAlert rows fired by the budget evaluator cron. Default status=open. Status transitions: open -> acknowledged -> resolved. Idempotent via (budgetId, monthBucket, thresholdPct) unique constraint; the evaluator fires at most one alert per threshold per month.","operationId":"empire_cost_alerts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-cost-alerts.v1","generatedAt":"2026-05-27T12:00:00.000Z","status":"open","openCount":1,"alerts":[{"id":"alrt_demo_80","budgetId":"bud_empire_demo","scope":"empire","scopeKey":"","monthBucket":"2026-05","thresholdPct":80,"monthlyUsd":1500,"observedUsd":1220.5,"status":"open","firedAt":"2026-05-27T06:30:00.000Z","ackedAt":null,"ackedBy":null,"resolvedAt":null}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/cost/alerts/{id}/ack":{"post":{"tags":["empire"],"summary":"Cost Center alert acknowledge","description":"Acknowledge a fired CostAlert. Stamps ackedAt + ackedBy; status transitions to \"acknowledged\". Idempotent: a second ack call refreshes the timestamp but does not change the state machine.","operationId":"empire_cost_alerts_ack","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-cost-alert-ack.v1","ok":true,"alert":{"id":"alrt_demo_80","status":"acknowledged"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/cost/alerts/{id}/resolve":{"post":{"tags":["empire"],"summary":"Cost Center alert resolve","description":"Resolve a fired CostAlert. Stamps resolvedAt; status transitions to \"resolved\". Auto-stamps ackedAt when resolving directly from open.","operationId":"empire_cost_alerts_resolve","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-cost-alert-resolve.v1","ok":true,"alert":{"id":"alrt_demo_80","status":"resolved"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/backups/ladder":{"get":{"tags":["empire"],"summary":"Backup verification ladder","description":"Last N days of RestoreVerification rows + per-database posture (last verification, last ok verification, hours-since-last-ok, 30-day outcome totals). The ladder is the empire's answer to \"did last night restore work\" at a glance. Outcomes: ok | drift | error.","operationId":"empire_backups_ladder","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-backup-ladder.v1","generatedAt":"2026-05-28T03:00:00.000Z","windowDays":30,"rows":[{"id":"rv_demo","snapshotId":"snap_demo","database":"echoatlas_landing","neonSnapshotId":"neon_snap_2026_05_28","takenAt":"2026-05-28T02:00:00.000Z","verifiedAt":"2026-05-28T03:00:12.000Z","durationMs":12000,"outcome":"ok","errorText":null,"restoredRowSha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","determinismDigest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}],"databases":[{"database":"echoatlas_landing","last":null,"lastOk":null,"hoursSinceLastOk":0,"totals30d":{"ok":30,"drift":0,"error":0}}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/backups/snapshots":{"get":{"tags":["empire"],"summary":"Backup snapshot list","description":"Lists every observed BackupSnapshot per database. Each row has rowCountsSha256 + byTableJson so the verifier (Phase 30.3) can replay the row-count digest against a cloned branch.","operationId":"empire_backups_snapshots","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-backup-snapshots.v1","generatedAt":"2026-05-28T03:00:00.000Z","database":null,"snapshots":[{"id":"snap_demo","database":"echoatlas_landing","neonSnapshotId":"neon_snap_2026_05_28","takenAt":"2026-05-28T02:00:00.000Z","retentionUntil":"2026-06-04T02:00:00.000Z","sizeBytes":52428800,"rowCountsSha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","byTable":{"User":12,"ApiToken":7},"source":"neon","observedAt":"2026-05-28T02:00:30.000Z"}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/backups/verify":{"post":{"tags":["empire"],"summary":"Manual restore verification","description":"Operator-triggered manual verification of a BackupSnapshot. Body: {snapshotId}. Until Phase 30.3 lands the Neon adapter the verifier uses a synthetic BackupSource that returns the snapshot's own row counts so the round-trip always reports outcome=ok. Use it to confirm auth + persistence + response shape before the real driver lands. Operator-only (write:alerts).","operationId":"empire_backups_verify","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-backup-verify.v1","ok":true,"verification":{"id":"rv_demo","snapshotId":"snap_demo","verifiedAt":"2026-05-28T03:00:00.000Z","durationMs":1200,"outcome":"ok","determinismDigest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/backups/drills":{"get":{"tags":["empire"],"summary":"Region failover drill history","description":"Last N weeks of RegionFailoverDrill rows. Each row is one drill outcome: the picked non-primary region, observed federation badge, federation lag, outcome (ok|drift|error). The weekly cron at /api/cron/region-failover-drill landed these on the Monday 04:00 UTC schedule.","operationId":"empire_backups_drills","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-region-failover-drill-list.v1","generatedAt":"2026-05-28T04:00:00.000Z","drills":[{"id":"rfd_demo","region":"eu-west","startedAt":"2026-05-25T04:00:00.000Z","endedAt":"2026-05-25T04:01:00.000Z","observedBadge":"green","federationLagSec":0,"outcome":"ok","errorText":null}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/backups/drills/run":{"post":{"tags":["empire"],"summary":"Manual region failover drill","description":"Operator-triggered manual run of the region failover drill. Same orchestrator the weekly cron uses; same pre-flight gate (refuse if unresolved error drill from the last 24h). Operator-only (write:alerts).","operationId":"empire_backups_drills_run","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-region-failover-drill.v1","generatedAt":"2026-05-28T04:00:00.000Z","skipped":false,"skipReason":null,"drill":{"id":"rfd_demo","region":"eu-west","startedAt":"2026-05-28T04:00:00.000Z","endedAt":"2026-05-28T04:01:00.000Z","observedBadge":"green","federationLagSec":0,"outcome":"ok","errorText":null}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/backups/cross-toolset":{"get":{"tags":["empire"],"summary":"Cross-toolset restore coordination","description":"Walks the toolset registry, pings each toolset's INTERNAL_BRIDGE_TOKEN-gated /api/internal/v1/backup/verify endpoint, and aggregates the per-toolset outcomes into an empire-wide green/yellow/red rollup. Drift + error rows auto-open a TrustDriftEvent. Operator-only.","operationId":"empire_backups_cross-toolset","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-cross-toolset-restore.v1","generatedAt":"2026-05-28T05:00:00.000Z","totalToolsets":3,"overall":"green","counts":{"ok":3,"drift":0,"error":0,"skipped":0},"rows":[{"toolset":"EchoAtlasCodex","database":"codex_main","vercelProject":"dan-shives-projects/echoatlas-codex","outcome":"ok","latestSnapshotAt":"2026-05-28T04:00:00.000Z","lastVerifiedAt":"2026-05-28T04:30:00.000Z","errorText":null,"badge":"green"}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/audit/chain":{"get":{"tags":["empire"],"summary":"Audit chain list","description":"Paginated AuditFact rows in seqNo-desc order. Filter by source (OperatorAudit | TrustDriftEvent | RestoreVerification | RegionFailoverDrill | ComplianceEvaluation | CostAlert), kind (operator | drift | backup | region-drill | compliance | cost-alert-fired | cost-alert-acknowledged | cost-alert-resolved), actor, or ts range (since/until ISO8601). Each row carries seqNo + prevHash + entryHash; tampering with row N invalidates row N+1's prevHash. The response includes the current chain head so consumers can pin against it.","operationId":"empire_audit_chain","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-audit-chain-list.v1","generatedAt":"2026-05-28T06:00:00.000Z","facts":[{"id":"af_demo","seqNo":"42","source":"OperatorAudit","sourceId":"oa_demo","kind":"operator","actor":"op_demo","ts":"2026-05-28T05:59:00.000Z","prevHash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","entryHash":"4c7a24f6343341e371df997d49724e4e66c34897bca932979a9b76f487878516"}],"head":{"seqNo":"42","headHash":"4c7a24f6343341e371df997d49724e4e66c34897bca932979a9b76f487878516","observedAt":"2026-05-28T06:00:00.000Z"},"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/audit/chain/{id}":{"get":{"tags":["empire"],"summary":"Audit fact + Merkle proof","description":"Returns a single AuditFact (by AuditFact.id or decimal seqNo) plus a Merkle proof to the current chain head root. The proof is the sibling-hash path from the leaf entryHash up to a sha256 binary-tree root over every fact 1..head. The verifier replays sha256(left || right) at each level using the `side` field to recover the root. The head field carries (seqNo, headHash, observedAt) so an external auditor can pin against the public anchor.","operationId":"empire_audit_chain_fact","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-audit-fact.v1","generatedAt":"2026-05-28T06:00:00.000Z","fact":{"id":"af_demo","seqNo":"42","source":"OperatorAudit","sourceId":"oa_demo","kind":"operator","actor":"op_demo","ts":"2026-05-28T05:59:00.000Z","prevHash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","entryHash":"4c7a24f6343341e371df997d49724e4e66c34897bca932979a9b76f487878516","payload":{"kind":"operator","verb":"backup.verify","target":"snap_demo"}},"proof":{"leafIndex":41,"leafCount":42,"leafHash":"4c7a24f6343341e371df997d49724e4e66c34897bca932979a9b76f487878516","rootHash":"aa48f9b2c6a6b29a5b0d8d51b8b85e6c9a5d3e8c7a8f9b0c1d2e3f4a5b6c7d8e","siblings":[{"hash":"bb48f9b2c6a6b29a5b0d8d51b8b85e6c9a5d3e8c7a8f9b0c1d2e3f4a5b6c7d8e","side":"left"}]},"head":{"seqNo":"42","headHash":"4c7a24f6343341e371df997d49724e4e66c34897bca932979a9b76f487878516","observedAt":"2026-05-28T06:00:00.000Z"}}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/audit/chain/verify":{"post":{"tags":["empire"],"summary":"Audit chain end-to-end verification","description":"Operator-triggered chain walk. Reads every AuditFact in seqNo order, recomputes each row's canonical entryHash, and confirms the prevHash linkage. Returns intact:true when the chain is unbroken; on divergence returns the seqNo of the first row that fails (with reason prev-mismatch | entry-mismatch). Logs an OperatorAudit row at verb=audit.verify-chain. Operator-only (write:alerts).","operationId":"empire_audit_chain_verify","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-audit-chain-verify.v1","generatedAt":"2026-05-28T06:00:00.000Z","intact":true,"walkedCount":42,"divergence":null,"head":{"seqNo":"42","headHash":"4c7a24f6343341e371df997d49724e4e66c34897bca932979a9b76f487878516","observedAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/chaos/experiments":{"get":{"tags":["empire"],"summary":"Chaos experiment registry","description":"Paginated ChaosExperiment registry in createdAt-desc order. Each row carries the slug, fault kind (one of the five whitelisted categories: synthetic-latency | bridge-token-rotate-rehearsal | cron-skip-one | feature-flag-flip | prisma-readonly-window), target surface, blast radius, recovery SLO, cooldown, severity, and status (active | paused | retired). Filter by status or kind. Cursor is the createdAt ISO of the last row in the prior page.","operationId":"empire_chaos_experiments","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-chaos-experiments.v1","generatedAt":"2026-05-28T06:00:00.000Z","experiments":[{"id":"ce_demo","slug":"latency-honeypot-dashboard","kind":"synthetic-latency","target":"/api/observatory/v1/honeypot/dashboard","description":"Inject 800ms latency into the honeypot dashboard route.","blastRadius":"single-route","recoverySloSec":60,"severity":"minor","status":"active","cooldownMinutes":60,"ownerUserId":"op_demo","createdAt":"2026-05-28T05:59:00.000Z","updatedAt":"2026-05-28T05:59:00.000Z"}],"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Chaos experiment register","description":"Register a new ChaosExperiment. Body requires slug (1..128 chars [a-z0-9-]), kind (one of the whitelisted categories), target (1..256 chars). Optional: description, recoverySloSec (defaults to category default; capped at category maxWindowSec), cooldownMinutes (1..1440), severity (minor | major). Returns {ok:true, experiment} on success or {ok:false, reason, human} with HTTP 400 on validation failure. Logs an OperatorAudit row at verb=chaos.register.","operationId":"empire_chaos_experiments_register","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-chaos-experiment.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","experiment":{"id":"ce_demo","slug":"latency-honeypot-dashboard","kind":"synthetic-latency","target":"/api/observatory/v1/honeypot/dashboard","description":"","blastRadius":"single-route","recoverySloSec":60,"severity":"minor","status":"active","cooldownMinutes":60,"ownerUserId":"op_demo","createdAt":"2026-05-28T06:00:00.000Z","updatedAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/chaos/runs":{"get":{"tags":["empire"],"summary":"Chaos run history","description":"Paginated ChaosRun rows in startedAt-desc order. Each row carries the parent experiment slug + kind (hydrated server-side), the observed recovery time, the outcome (ok | sla-breach | aborted | partial), and any errorText or abortReason. Filter by experimentId or outcome. Cursor is the startedAt ISO of the last row in the prior page.","operationId":"empire_chaos_runs","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-chaos-runs.v1","generatedAt":"2026-05-28T06:00:00.000Z","runs":[{"id":"cr_demo","experimentId":"ce_demo","experimentSlug":"latency-honeypot-dashboard","experimentKind":"synthetic-latency","startedAt":"2026-05-28T05:45:00.000Z","endedAt":"2026-05-28T05:45:01.000Z","outcome":"ok","observedRecoverySec":1,"errorText":null,"abortReason":null}],"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/chaos/freeze":{"post":{"tags":["empire"],"summary":"Chaos schedule freeze switch","description":"GET returns the current freeze state. POST toggles it; body {frozen:boolean, reason?:string}. When frozen=true the /api/cron/chaos-schedule cron no-ops without selecting an experiment. Implicit pause also fires when any open critical TrustDriftEvent is present. Operators set this during incidents or planned maintenance. POST logs an OperatorAudit row at verb=chaos.freeze (frozen=true) or chaos.thaw (frozen=false).","operationId":"empire_chaos_freeze","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-chaos-freeze.v1","generatedAt":"2026-05-28T06:00:00.000Z","freeze":{"scope":"global","frozen":true,"reason":"incident i_demo open","updatedBy":"op_demo","updatedAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/provenance/keys":{"get":{"tags":["empire"],"summary":"Provenance key registry","description":"Paginated ToolsetSigningKey registry in createdAt-desc order. Each row carries the toolset, raw 32-byte Ed25519 publicKeyB64 (base64), status (active | retired), ownerUserId, createdAt, and retiredAt (set when status=retired). Filter by toolset or status. Cursor is the createdAt ISO of the last row in the prior page.","operationId":"empire_provenance_keys_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-provenance-keys.v1","generatedAt":"2026-05-28T06:00:00.000Z","keys":[{"id":"tk_demo","toolset":"EchoAtlasObservatory","publicKeyB64":"3GgNxZ7dPunmC4aNMUMklpIaMhmiChQRbE6ZdnoscP4=","status":"active","ownerUserId":"op_demo","createdAt":"2026-05-28T05:59:00.000Z","retiredAt":null}],"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Provenance key register","description":"Register a new ToolsetSigningKey. Body requires toolset (3..128 chars [A-Za-z][A-Za-z0-9-]) and publicKeyB64 (canonical RFC 4648 base64 of a 32-byte Ed25519 public key). The active key set for a toolset can have more than one row during rotation windows; the most recently created active row wins admission in steady state. Returns {ok:true, key} or {ok:false, reason, human} with HTTP 400 on validation failure. Logs OperatorAudit at verb=provenance.register.","operationId":"empire_provenance_keys_register","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-provenance-key.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","key":{"id":"tk_demo","toolset":"EchoAtlasObservatory","publicKeyB64":"3GgNxZ7dPunmC4aNMUMklpIaMhmiChQRbE6ZdnoscP4=","status":"active","ownerUserId":"op_demo","createdAt":"2026-05-28T06:00:00.000Z","retiredAt":null}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/provenance/keys/{id}/retire":{"post":{"tags":["empire"],"summary":"Provenance key retire","description":"Retire a ToolsetSigningKey by id. Sets status='retired' + retiredAt=now. Idempotent: re-retiring an already-retired key returns the existing row. Future signatures from the retired key remain verifiable only for events whose ts is at or before retiredAt; anything later fails admission with rejectionReason='key-retired-before-event'. Logs OperatorAudit at verb=provenance.retire.","operationId":"empire_provenance_keys_retire","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-provenance-key.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","key":{"id":"tk_demo","toolset":"EchoAtlasObservatory","publicKeyB64":"3GgNxZ7dPunmC4aNMUMklpIaMhmiChQRbE6ZdnoscP4=","status":"retired","ownerUserId":"op_demo","createdAt":"2026-05-28T05:59:00.000Z","retiredAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/provenance/attestations":{"get":{"tags":["empire"],"summary":"Provenance attestation history","description":"Paginated ProvenanceAttestation rows in acceptedAt-desc order. Each row carries the toolset + (source, sourceId, kind) tuple, eventHash, signatureB64 (null when compat-mode admitted), toolsetKeyId (the registered key that verified the signature, when any), attestedAt (null for compat-mode + reject paths), acceptedAt, compatMode flag, and rejectionReason (null for admits). Filters: toolset, source, compatMode (true|false), rejected (true|false). Cursor is the acceptedAt ISO of the last row.","operationId":"empire_provenance_attestations_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-provenance-attestations.v1","generatedAt":"2026-05-28T06:00:00.000Z","attestations":[{"id":"pa_demo","toolset":"EchoAtlasObservatory","source":"OperatorAudit","sourceId":"audit_demo","kind":"operator","eventHash":"f598d6ed589a72ae05a0c2d6c11748a407c6e6db7422c5e6df5ec2090cbfb92f","signatureB64":null,"toolsetKeyId":null,"attestedAt":null,"acceptedAt":"2026-05-28T06:00:00.000Z","compatMode":true,"rejectionReason":null}],"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/provenance/compat-mode":{"post":{"tags":["empire"],"summary":"Provenance compat-mode switch","description":"GET returns the singleton ProvenanceCompatMode row (default enabled=true during rollout). POST toggles it; body {enabled:boolean, reason?:string}. When enabled=true the audit mirror admits unsigned events with attestedAt=null + compatMode=true so the provenance-compat-mode-off predicate can count the gap. When enabled=false the mirror rejects unsigned events with rejectionReason=provenance-required. Logs OperatorAudit at verb=provenance.compat-mode.","operationId":"empire_provenance_compat-mode","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-provenance-compat-mode.v1","generatedAt":"2026-05-28T06:00:00.000Z","compatMode":{"scope":"global","enabled":true,"reason":"rollout window","updatedBy":"op_demo","updatedAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/knowledge/articles":{"get":{"tags":["empire"],"summary":"Knowledge article registry","description":"Paginated KnowledgeArticle registry in updatedAt-desc order. Each row carries id, slug, kind (runbook | decision | postmortem-link | how-to), title, status (draft | published | archived), visibility (operator-only | public), ownerUserId, activeRevisionId, createdAt, updatedAt. Filters: kind, status, visibility, ownerUserId. Cursor is the updatedAt ISO of the last row in the prior page.","operationId":"empire_knowledge_articles_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-knowledge-articles.v1","generatedAt":"2026-05-28T06:00:00.000Z","articles":[{"id":"ka_demo","slug":"incident-replay-runbook","kind":"runbook","title":"Incident replay runbook","status":"published","visibility":"operator-only","ownerUserId":"op_demo","activeRevisionId":"kr_demo","createdAt":"2026-05-28T05:59:00.000Z","updatedAt":"2026-05-28T06:00:00.000Z"}],"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Knowledge article create","description":"Create a new KnowledgeArticle plus its first KnowledgeRevision in one transaction. Body requires slug (1..128 chars [a-z0-9-]), kind, title (3..256 chars), and bodyMd; visibility defaults to operator-only. Articles always land in status=draft; promotion to published goes through the /status endpoint with a passkey step-up. Logs OperatorAudit at verb=knowledge.create. Returns {ok:true, article, activeRevision} or {ok:false, reason, human} with HTTP 400 on validation failure.","operationId":"empire_knowledge_articles_create","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-knowledge-article.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","article":{"id":"ka_demo","slug":"incident-replay-runbook","kind":"runbook","title":"Incident replay runbook","status":"draft","visibility":"operator-only","ownerUserId":"op_demo","activeRevisionId":"kr_demo","createdAt":"2026-05-28T06:00:00.000Z","updatedAt":"2026-05-28T06:00:00.000Z"},"activeRevision":{"id":"kr_demo","articleId":"ka_demo","version":1,"bodyMd":"# Incident replay\n\n1. open mission control...","authorUserId":"op_demo","summary":"initial draft","createdAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/knowledge/articles/{id}":{"get":{"tags":["empire"],"summary":"Knowledge article fetch","description":"Fetch a KnowledgeArticle by id with its active revision body and every KnowledgeBacklink in createdAt-asc order. Returns 404 when the article does not exist. The active revision wins reads; older revisions remain in /revisions (Phase 34.3) for rollback.","operationId":"empire_knowledge_articles_fetch","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-knowledge-article.v1","generatedAt":"2026-05-28T06:00:00.000Z","article":{"id":"ka_demo","slug":"incident-replay-runbook","kind":"runbook","title":"Incident replay runbook","status":"published","visibility":"operator-only","ownerUserId":"op_demo","activeRevisionId":"kr_demo","createdAt":"2026-05-28T05:59:00.000Z","updatedAt":"2026-05-28T06:00:00.000Z"},"activeRevision":{"id":"kr_demo","articleId":"ka_demo","version":2,"bodyMd":"# Incident replay\n\n1. open mission control...","authorUserId":"op_demo","summary":"second pass","createdAt":"2026-05-28T06:00:00.000Z"},"backlinks":[]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/knowledge/articles/{id}/edit":{"post":{"tags":["empire"],"summary":"Knowledge article edit","description":"Append a new KnowledgeRevision to an existing KnowledgeArticle. The new revision becomes the active one; prior revisions remain in the append-only history for rollback. Body: { bodyMd, summary? }. Returns {ok:true, article, activeRevision} or {ok:false, reason, human}. Logs OperatorAudit at verb=knowledge.edit.","operationId":"empire_knowledge_articles_edit","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-knowledge-article.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","article":{"id":"ka_demo","slug":"incident-replay-runbook","kind":"runbook","title":"Incident replay runbook","status":"draft","visibility":"operator-only","ownerUserId":"op_demo","activeRevisionId":"kr_demo_v3","createdAt":"2026-05-28T05:59:00.000Z","updatedAt":"2026-05-28T06:00:00.000Z"},"activeRevision":{"id":"kr_demo_v3","articleId":"ka_demo","version":3,"bodyMd":"# Incident replay\n\n1. open mission control... (v3)","authorUserId":"op_demo","summary":"tighten the step-3 region check","createdAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/knowledge/articles/{id}/status":{"post":{"tags":["empire"],"summary":"Knowledge article status transition","description":"Transition a KnowledgeArticle through the status matrix (draft -> published | archived, published -> draft | archived, archived -> draft). Promoting to published from an interactive operator session is gated on a fresh Phase 26.3 passkey step-up; bearer eatok_* tokens with write:alerts bypass step-up so headless flows still work. Body: { to:\"draft\" | \"published\" | \"archived\" }. Logs OperatorAudit at verb=knowledge.publish (for published transitions) or knowledge.status.","operationId":"empire_knowledge_articles_status","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-knowledge-article.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","article":{"id":"ka_demo","slug":"incident-replay-runbook","kind":"runbook","title":"Incident replay runbook","status":"published","visibility":"operator-only","ownerUserId":"op_demo","activeRevisionId":"kr_demo","createdAt":"2026-05-28T05:59:00.000Z","updatedAt":"2026-05-28T06:00:00.000Z"},"activeRevision":{"id":"kr_demo","articleId":"ka_demo","version":1,"bodyMd":"# Incident replay\n\n1. open mission control...","authorUserId":"op_demo","summary":"initial draft","createdAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/knowledge/articles/{id}/rollback":{"post":{"tags":["empire"],"summary":"Knowledge article rollback","description":"Flip activeRevisionId back to a prior KnowledgeRevision by version number. Rollback is append-only with respect to the revision table; newer revisions remain on disk for re-roll-forward. Body: { toVersion:number }. Logs OperatorAudit at verb=knowledge.rollback.","operationId":"empire_knowledge_articles_rollback","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-knowledge-article.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","article":{"id":"ka_demo","slug":"incident-replay-runbook","kind":"runbook","title":"Incident replay runbook","status":"published","visibility":"operator-only","ownerUserId":"op_demo","activeRevisionId":"kr_demo_v1","createdAt":"2026-05-28T05:59:00.000Z","updatedAt":"2026-05-28T06:00:00.000Z"},"activeRevision":{"id":"kr_demo_v1","articleId":"ka_demo","version":1,"bodyMd":"# Incident replay\n\n1. open mission control...","authorUserId":"op_demo","summary":"initial draft","createdAt":"2026-05-28T05:59:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/knowledge/articles/{id}/backlinks":{"post":{"tags":["empire"],"summary":"Knowledge article backlink attach","description":"Attach a typed KnowledgeBacklink to a KnowledgeArticle. Idempotent: re-adding the same (articleId, targetKind, targetId) tuple returns the existing row. Body: { targetKind, targetId }. targetKind must be one of incident, postmortem, audit-fact, chaos-run, drift-event, graph-node. Logs OperatorAudit at verb=knowledge.backlink.","operationId":"empire_knowledge_articles_backlinks","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-knowledge-backlink.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","backlink":{"id":"kb_demo","articleId":"ka_demo","targetKind":"postmortem","targetId":"pm_demo","createdAt":"2026-05-28T06:00:00.000Z"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/intel-exchange/signals":{"get":{"tags":["empire"],"summary":"Threat intel signal registry","description":"Paginated ThreatIntelSignal registry in lastSeenAt-desc order. Each row carries id, producerToolset, signalKind (ja4 | ip | ua-class | attack-pattern | verified-scraper), indicator, severity (minor | major | critical), confidence in [0,1], firstSeenAt, lastSeenAt, observedCount, signatureB64, signatureKeyId, attrs. Filters: signalKind, minSeverity, producerToolset, since (ISO timestamp). Cursor is the lastSeenAt ISO of the last row in the prior page.","operationId":"empire_intel-exchange_signals_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-signals.v1","generatedAt":"2026-05-28T06:00:00.000Z","signals":[{"id":"ti_demo","producerToolset":"EchoAtlasHoneypot","signalKind":"ja4","indicator":"t13d1516h2_8daaf6152771_b186095e22b6","severity":"major","confidence":0.8,"firstSeenAt":"2026-05-28T05:00:00.000Z","lastSeenAt":"2026-05-28T06:00:00.000Z","observedCount":17,"signatureB64":"sig_demo","signatureKeyId":"tsk_demo","attrs":{},"createdAt":"2026-05-28T05:00:00.000Z"}],"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Threat intel signal register","description":"Register a new ThreatIntelSignal. Dedup-upsert on the (producerToolset, signalKind, indicator) unique key bumps observedCount + lastSeenAt and climbs to the strictest severity seen for that indicator. Body: { producerToolset, signalKind, indicator, severity?, confidence?, observedCount?, signatureB64?, signatureKeyId?, attrs? }. Returns {ok:true, signal, created} or {ok:false, reason, human} with HTTP 400 on validation failure. Logs OperatorAudit at verb=intel-exchange.signal.register.","operationId":"empire_intel-exchange_signals_register","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-signal.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","signal":{"id":"ti_demo","producerToolset":"EchoAtlasHoneypot","signalKind":"ja4","indicator":"t13d1516h2_8daaf6152771_b186095e22b6","severity":"major","confidence":0.8,"firstSeenAt":"2026-05-28T06:00:00.000Z","lastSeenAt":"2026-05-28T06:00:00.000Z","observedCount":1,"signatureB64":null,"signatureKeyId":null,"attrs":{},"createdAt":"2026-05-28T06:00:00.000Z"},"created":true}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/intel-exchange/signals/{id}":{"get":{"tags":["empire"],"summary":"Threat intel signal fetch","description":"Fetch a ThreatIntelSignal by id. Returns 404 when the signal does not exist. The signed-attestation pair (signatureB64 + signatureKeyId) is suitable for end-to-end verification via the Phase 33 /api/public/v1/provenance/keys anchor.","operationId":"empire_intel-exchange_signals_fetch","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-signal.v1","generatedAt":"2026-05-28T06:00:00.000Z","signal":{"id":"ti_demo","producerToolset":"EchoAtlasHoneypot","signalKind":"ja4","indicator":"t13d1516h2_8daaf6152771_b186095e22b6","severity":"major","confidence":0.8,"firstSeenAt":"2026-05-28T05:00:00.000Z","lastSeenAt":"2026-05-28T06:00:00.000Z","observedCount":17,"signatureB64":"sig_demo","signatureKeyId":"tsk_demo","attrs":{},"createdAt":"2026-05-28T05:00:00.000Z"}}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/intel-exchange/subscriptions":{"get":{"tags":["empire"],"summary":"Threat intel subscriptions list","description":"Lists the calling workspace's ThreatIntelSubscription rows in createdAt-desc order. Each row carries id, workspaceId, signalKinds[], minSeverity, createdAt, lastDeliveredAt. workspaceId derives from the resolved auth userId.","operationId":"empire_intel-exchange_subscriptions_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-subscriptions.v1","generatedAt":"2026-05-28T06:00:00.000Z","subscriptions":[{"id":"tis_demo","workspaceId":"op_demo","signalKinds":["ja4","ip"],"minSeverity":"major","createdAt":"2026-05-28T05:00:00.000Z","lastDeliveredAt":null}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Threat intel subscription create","description":"Create a ThreatIntelSubscription scoped to the caller's workspace. Body: { signalKinds:string[], minSeverity? }. signalKinds must be a non-empty whitelist of {ja4, ip, ua-class, attack-pattern, verified-scraper}. minSeverity defaults to 'minor'. Returns {ok:true, subscription} or {ok:false, reason, human} (HTTP 400 on validation failure). Logs OperatorAudit at verb=intel-exchange.subscription.create.","operationId":"empire_intel-exchange_subscriptions_create","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-subscription.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","subscription":{"id":"tis_demo","workspaceId":"op_demo","signalKinds":["ja4","ip"],"minSeverity":"major","createdAt":"2026-05-28T06:00:00.000Z","lastDeliveredAt":null}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/intel-exchange/subscriptions/{id}":{"delete":{"tags":["empire"],"summary":"Threat intel subscription drop","description":"Drops a ThreatIntelSubscription owned by the calling workspace. Returns 404 when the subscription is missing OR owned by a different workspace (we never leak subscription ids across workspaces). Logs OperatorAudit at verb=intel-exchange.subscription.drop.","operationId":"empire_intel-exchange_subscriptions_drop","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-subscription.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","id":"tis_demo"}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/intel-exchange/privacy-budget":{"get":{"tags":["empire"],"summary":"Threat intel privacy budget read","description":"Read the global ThreatIntelPrivacyBudget singleton. Defaults: kAnonThreshold=5, dailyShareCap=1000. The k-anonymity threshold gates per-workspace raw-count exposure on the partner surface; signals with k < kAnonThreshold are released only via the empire-attested aggregate.","operationId":"empire_intel-exchange_privacy-budget_get","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-privacy-budget.v1","generatedAt":"2026-05-28T06:00:00.000Z","budget":{"scope":"global","kAnonThreshold":5,"dailyShareCap":1000,"updatedAt":"2026-05-28T06:00:00.000Z","updatedBy":null}}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Threat intel privacy budget set","description":"Operator-only adjustment of the global ThreatIntelPrivacyBudget. Body: { kAnonThreshold?:number, dailyShareCap?:number }. kAnonThreshold is floored at 1, dailyShareCap at 0; non-finite numbers are dropped. Every change lands an OperatorAudit row at verb=intel-exchange.privacy-budget.set so the partner surface can prove honour of the gate.","operationId":"empire_intel-exchange_privacy-budget_set","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-privacy-budget.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","budget":{"scope":"global","kAnonThreshold":5,"dailyShareCap":1000,"updatedAt":"2026-05-28T06:00:00.000Z","updatedBy":"op_demo"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/intel-exchange/stix/import":{"post":{"tags":["empire"],"summary":"Threat intel STIX 2.1 import","description":"Operator-only ingest of an external STIX 2.1 bundle. Every projected indicator becomes a ThreatIntelSignal row with producerToolset='external'; the upstream pattern types we accept are x-ja4-fingerprint, ipv4-addr, ipv6-addr, x-ua-class, x-attack-pattern, x-verified-scraper. Indicators with unrecognised patterns are dropped; the response lists accepted + failures so the caller knows the partial-success picture. Logs one OperatorAudit row per accepted indicator at verb=intel-exchange.signal.register.","operationId":"empire_intel-exchange_stix_import","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-threat-intel-stix-import.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","accepted":[{"signalKind":"ip","indicator":"198.51.100.4","id":"ti_imported_demo"}],"failures":[]}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/search":{"get":{"tags":["empire"],"summary":"Empire Search query","description":"BM25 + semantic search over the unified empire index. Indexed kinds: incident, postmortem, drift-event, audit, doc-page, help-page, graph-edge, trust-snapshot. Query syntax: free text plus operators kind:<value>, severity:<minor|major|critical>, since:<24h|7d|30d|ISO-timestamp>. Mode: bm25 (default), semantic (cosine over text-embedding-3-small with a seeded-local fallback), hybrid (alpha-blended). Returns per-result envelope with sourceKind, sourceId, title, snippet, score, drillIn (UI path), apiDrillIn (JSON path). Cursor pagination via the prior page nextCursor.","operationId":"empire_search_query","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-search.v1","generatedAt":"2026-05-26T00:00:00.000Z","query":"kind:audit yubikey","mode":"bm25","alpha":null,"parsed":{"terms":["yubikey"],"kinds":["audit"],"severities":[],"sinceMs":null},"total":1,"byKind":{"incident":0,"postmortem":0,"drift-event":0,"audit":1,"doc-page":0,"help-page":0,"graph-edge":0,"trust-snapshot":0},"nextCursor":null,"hits":[{"id":"cuid_demo","sourceKind":"audit","sourceId":"audit_42","title":"passkey revoked","snippet":"...revoked yubikey 5C nano for operator alice...","score":1.234,"drillIn":"/observatory/operator-audit","apiDrillIn":"/api/observatory/v1/operator-audit","attrs":{"verb":"account.passkeys.revoke"},"indexedAt":"2026-05-26T00:00:00.000Z"}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/edge/latency":{"get":{"tags":["empire"],"summary":"Edge latency overview","description":"Per-endpoint p50/p95/p99 rollup over the trailing window (default 24h). Reads EdgeLatencySample buckets. Same DB state -> same envelope. Used by the trust card to surface the migration SLA.","operationId":"empire_edge_latency_overview","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-edge-latency-overview.v1","generatedAt":"2026-05-27T12:00:00.000Z","windowMs":86400000,"rows":[{"endpoint":"/api/public/v1/manifest","count":1024,"p50Ms":18.4,"p95Ms":31.2,"p99Ms":47.8,"buckets":288}]}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/edge/backplane":{"get":{"tags":["empire"],"summary":"Edge backplane queue depth","description":"Pending + errored counts in EdgeBackplaneEvent plus the age of the oldest still-pending row. Drains via /api/cron/edge-backplane-drain at one-minute cadence; the trust card alerts if oldestPendingAgeMs > 120000.","operationId":"empire_edge_backplane_queue-depth","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-edge-backplane-depth.v1","generatedAt":"2026-05-27T12:00:00.000Z","pending":3,"errored":0,"oldestPendingAt":"2026-05-27T11:59:30.000Z","oldestPendingAgeMs":30000}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/edge/fallback":{"post":{"tags":["empire"],"summary":"Edge fallback toggle (operator)","description":"Operator-only verb that records the intended EDGE_FALLBACK posture in OperatorAudit and returns the Vercel CLI command to apply. The env var is the runtime enforcer; this verb captures the operator intent so the trust card can flag drift.","operationId":"empire_edge_fallback_set","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-edge-fallback-set.v1","ok":true,"intended":true,"currentlyActive":false,"applyCommand":"vercel env add EDGE_FALLBACK 1 production","note":"The fallback flag is enforced by EDGE_FALLBACK env at request time. This audit row records the operator intent; flip the env via the command above to actually take effect."}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/lineage-vault/bundles":{"get":{"tags":["empire"],"summary":"Sealed lineage bundle registry","description":"Paginated LineageBundle registry in sealedAt-desc order. Each row carries id, sealedAt, fromTs, toTs, workspaceFilter (null = empire-wide), sha256, signatureB64, signatureKeyId, blobUrlPrimary, blobUrlSecondary, rowCounts. The response also includes the vault posture: totals, hours-since-most-recent seal, active legal-hold count, replication ratio over the trailing 30d.","operationId":"empire_lineage-vault_bundles_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-observatory-lineage-vault.v1","generatedAt":"2026-05-28T06:00:00.000Z","posture":{"schema":"echoatlas-lineage-vault-posture.v1","generatedAt":"2026-05-28T06:00:00.000Z","totals":{"bundles":1,"bundles30d":1,"bundlesFullyReplicated30d":1,"legalHoldsActive":0},"mostRecentSealedAt":"2026-05-28T04:00:00.000Z","hoursSinceMostRecent":2},"bundles":[{"id":"lvb_20260527","sealedAt":"2026-05-28T04:00:00.000Z","fromTs":"2026-05-27T00:00:00.000Z","toTs":"2026-05-28T00:00:00.000Z","workspaceFilter":null,"sha256":"1d45144ac4306fab4057bf728f2f01391c188af1640a788d5305c9f8430bd7ad","signatureB64":"AAA","signatureKeyId":"tsk_vault_2026_05","blobUrlPrimary":"https://blob.vercel-storage.com/lineage-vault/lvb_20260527.json","blobUrlSecondary":"https://acct.r2.cloudflarestorage.com/echoatlas-vault/lineage-vault/lvb_20260527.json","rowCounts":{"audit":24,"provenance":18,"threatIntel":3}}],"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/lineage-vault/legal-hold":{"get":{"tags":["empire"],"summary":"Legal-hold registry","description":"Paginated LegalHold registry in openedAt-desc order. Filters: status (active|closed|all), workspaceId. Each row carries id, workspaceId, openedAt, closedAt, openedByUserId, closedByUserId, reason, externalCaseId.","operationId":"empire_lineage-vault_legal-hold_list","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-legal-holds.v1","generatedAt":"2026-05-28T06:00:00.000Z","holds":[{"id":"lh_demo","workspaceId":"ws_demo","openedAt":"2026-05-28T03:00:00.000Z","closedAt":null,"openedByUserId":"op_alice","closedByUserId":null,"reason":"Subpoena CASE-2026-04-001 for 2026-04 records","externalCaseId":"CASE-2026-04-001"}],"nextCursor":null}}}},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"parameters":[],"security":[{"bearerAuth":[]}]},"post":{"tags":["empire"],"summary":"Open legal hold","description":"Opens a LegalHold on a workspace. Hot deletes against AuditFact / ProvenanceAttestation / ThreatIntelSignal for that workspaceId fail closed via assertNotOnHold() until the hold is closed. Idempotent: an existing open hold for the same workspace returns that row with idempotent:true. Logs OperatorAudit at verb=lineage-vault.legal-hold.open.","operationId":"empire_lineage-vault_legal-hold_open","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-legal-hold.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","hold":{"id":"lh_demo","workspaceId":"ws_demo","openedAt":"2026-05-28T03:00:00.000Z","closedAt":null,"openedByUserId":"op_alice","closedByUserId":null,"reason":"Subpoena CASE-2026-04-001","externalCaseId":"CASE-2026-04-001"},"idempotent":false}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{}}}},"security":[{"bearerAuth":[]}]}},"/api/observatory/v1/lineage-vault/legal-hold/{id}":{"post":{"tags":["empire"],"summary":"Close legal hold","description":"Closes an open LegalHold. The empire still retains the rows under empire-wide 7-year retention; closing the hold lifts the assertNotOnHold() gate against future deletes. Logs OperatorAudit at verb=lineage-vault.legal-hold.close.","operationId":"empire_lineage-vault_legal-hold_close","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{},"example":{"schema":"echoatlas-legal-hold.v1","ok":true,"generatedAt":"2026-05-28T06:00:00.000Z","hold":{"id":"lh_demo","workspaceId":"ws_demo","openedAt":"2026-05-28T03:00:00.000Z","closedAt":"2026-05-28T06:00:00.000Z","openedByUserId":"op_alice","closedByUserId":"op_alice","reason":"Subpoena CASE-2026-04-001","externalCaseId":"CASE-2026-04-001"}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Missing or invalid bearer token / session."},"403":{"description":"Token missing required scope."}},"security":[{"bearerAuth":[]}]}}},"x-echoatlas-scopes":["read:dashboard","read:events","read:tenants","read:alerts","write:alerts","read:clusters","read:intel","write:tokens","write:tenants","write:test-event","read:empire"]}