{
 "openapi": "3.1.0",
 "info": {
  "title": "Throndar Verification API",
  "version": "1.0.0",
  "summary": "Verify a Throndar answer and generate AI Provenance Records.",
  "description": "Public, no-auth, CORS-enabled, stateless endpoints for verifying a Throndar answer's post-quantum signature, turning a verified proof into an AI Provenance Record, and fetching the published keys.\n\nVerification runs against the public keys embedded in the proof bundle, so a caller never has to trust Throndar's servers at check time. Nothing is stored.\n\nHONEST FRAMING: a passing verdict attests an answer's ORIGIN and INTEGRITY (it came from Throndar, unaltered), NOT its factual ACCURACY. `signerRecognized:true` additionally confirms the signing key is one Throndar publishes. Algorithm names denote the public standards the primitives are based on (ML-DSA-87 / FIPS 204, ML-KEM-1024 / FIPS 203; Falcon / FN-DSA, FIPS 206 forthcoming), not a FIPS-140 / CMVP validation.",
  "contact": {
   "name": "Throndar",
   "url": "https://throndar.ai/developers"
  },
  "license": {
   "name": "See throndar.ai/terms",
   "url": "https://throndar.ai/terms"
  }
 },
 "servers": [
  {
   "url": "https://throndar.ai",
   "description": "Production"
  }
 ],
 "tags": [
  {
   "name": "verification",
   "description": "Check a proof bundle."
  },
  {
   "name": "evidence",
   "description": "Produce an AI Provenance Record."
  },
  {
   "name": "keys",
   "description": "Published public keys."
  },
  {
   "name": "inventory",
   "description": "Cryptographic Bill of Materials (CBOM)."
  },
  {
   "name": "transparency",
   "description": "Public append-only transparency ledger (RFC 6962)."
  },
  {
   "name": "status",
   "description": "Live health of the product, as shown on /status."
  }
 ],
 "paths": {
  "/api/v1/verify": {
   "post": {
    "tags": [
     "verification"
    ],
    "summary": "Verify a proof, transcript, or Autopilot run bundle",
    "description": "Runs the same verification the browser uses. `verdict.overallOk` means the embedded signatures verify against the embedded keys (INTEGRITY). `signerRecognized` means the signing key is a published Throndar key (ORIGIN) — only `signerRecognized:true` means \"produced by Throndar\". Both are statements about the ANSWER. A bundle may also carry a `prompt` (the question) and an `excerpt` (a pinned quote), which no signature covers by itself: the question is attested only when `verdict.receipt.questionBinding` is `entire-input` or `current-message`, and `verdict.promptUnverified:true` means NO signature covers the question shown (a legacy receipt, reported but not failed — check it before treating the pair as evidence). A bound question that does not match, or an `excerpt` that is not an exact slice of the answer (`verdict.excerpt.ok:false`), fails the bundle. For a transcript the same fields appear per turn under `verdict.turns[].verdict`. An AI Provenance Record (the /api/v1/evidence body) may be posted as-is: its embedded `proof` is verified, its `attested` summary is recomputed from that proof and must match (else 422), and the response carries `record`. No network calls; nothing stored.",
    "operationId": "verifyBundle",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "oneOf": [
         {
          "$ref": "#/components/schemas/ProofBundle"
         },
         {
          "$ref": "#/components/schemas/TranscriptBundle"
         },
         {
          "$ref": "#/components/schemas/RunProofBundle"
         },
         {
          "$ref": "#/components/schemas/ExportArchive"
         },
         {
          "$ref": "#/components/schemas/SthBundle"
         },
         {
          "$ref": "#/components/schemas/ConsistencyBundle"
         },
         {
          "$ref": "#/components/schemas/AiProvenanceRecord"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The structured verdict.",
      "headers": {
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "description": "*"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/VerifyResponse"
        },
        "example": {
         "service": "Throndar",
         "kind": "throndar-proof",
         "created": 1782000000,
         "verdict": {
          "overallOk": true,
          "checks": 1,
          "receipt": {
           "present": true,
           "answerHashOk": true,
           "sigOk": true,
           "keyId": "0986d89fa3c74566",
           "councilGrounded": true,
           "promptBound": true,
           "promptOk": true,
           "questionBinding": "entire-input"
          },
          "signerRecognized": true
         },
         "signerRecognized": true,
         "disclaimer": "verdict.overallOk means the bundle's embedded signatures verify against its embedded keys (INTEGRITY). signerRecognized additionally confirms those signing keys are keys Throndar publishes (ORIGIN)..."
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadJson"
     },
     "413": {
      "$ref": "#/components/responses/TooLarge"
     },
     "422": {
      "$ref": "#/components/responses/NotABundle"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   },
   "options": {
    "tags": [
     "verification"
    ],
    "summary": "CORS preflight",
    "operationId": "verifyPreflight",
    "responses": {
     "204": {
      "description": "No Content"
     }
    }
   }
  },
  "/api/v1/evidence": {
   "post": {
    "tags": [
     "evidence"
    ],
    "summary": "Generate an AI Provenance Record",
    "description": "Turns a proof bundle into an AI Provenance Record: the verified proof embedded as the trust root, a factual summary derived from verification, and the published EU-AI-Act obligation mapping. Factual evidence — never a declaration of conformity. A bundle that doesn't verify still returns a record that honestly reports `attested.signed:false`.",
    "operationId": "generateEvidenceRecord",
    "parameters": [
     {
      "name": "org",
      "in": "query",
      "required": false,
      "description": "Optional subject organisation stamped into the record (max 200 chars).",
      "schema": {
       "type": "string",
       "maxLength": 200
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "oneOf": [
         {
          "$ref": "#/components/schemas/ProofBundle"
         },
         {
          "$ref": "#/components/schemas/RunProofBundle"
         }
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The AI Provenance Record (save the body as the .json artifact).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AiProvenanceRecord"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadJson"
     },
     "413": {
      "$ref": "#/components/responses/TooLarge"
     },
     "422": {
      "$ref": "#/components/responses/NotABundle"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   },
   "options": {
    "tags": [
     "evidence"
    ],
    "summary": "CORS preflight",
    "operationId": "evidencePreflight",
    "responses": {
     "204": {
      "description": "No Content"
     }
    }
   }
  },
  "/api/provenance/pubkey": {
   "get": {
    "tags": [
     "keys"
    ],
    "summary": "Published answer-receipt public keys",
    "description": "The ML-DSA-87 public key(s) used to verify answer-provenance receipts. Returns an array so historical key_ids keep resolving across rotations. Cached one hour.",
    "operationId": "getPublicKeys",
    "responses": {
     "200": {
      "description": "The published keys.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/PubkeysResponse"
        },
        "example": {
         "algo": "ML-DSA-87",
         "context": "throndar-provenance-v1",
         "configured": true,
         "keys": [
          {
           "key_id": "0986d89fa3c74566",
           "public_key_hex": "…",
           "context": "throndar-provenance-v1",
           "algo": "ML-DSA-87"
          }
         ]
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/api/cbom": {
   "get": {
    "tags": [
     "inventory"
    ],
    "summary": "Cryptographic Bill of Materials",
    "description": "A public, machine-readable inventory of every cryptographic primitive Throndar relies on, mapped to its NIST standard, role, and status (CycloneDX-CBOM flavoured; TNO/AIVD/CWI PQC Migration Handbook §2.3.4). Algorithm names denote the public standards the primitives are based on — NOT a CMVP/FIPS-140 validation. ML-DSA-87 (FIPS 204) is the sole load-bearing signature; Falcon/FN-DSA (FIPS 206 draft) is a NON-authoritative diversity co-signature. Cached one hour.",
    "operationId": "getCbom",
    "responses": {
     "200": {
      "description": "The CBOM document.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Cbom"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/api/transparency/ledger": {
   "get": {
    "tags": [
     "transparency"
    ],
    "summary": "Current Signed Tree Head",
    "description": "The current Signed Tree Head of the append-only RFC 6962 Merkle log of every signed Throndar answer (hashes only, never content): tree size + Merkle root, with an ML-DSA-87 `signed_tree_head` (domain 'throndar-sth-v1'). Returns `{enabled:false}` when the ledger is off.",
    "operationId": "getLedgerSth",
    "responses": {
     "200": {
      "description": "The signed tree head.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/LedgerSth"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "503": {
      "description": "Ledger temporarily unavailable."
     }
    }
   },
   "options": {
    "tags": [
     "transparency"
    ],
    "summary": "CORS preflight",
    "operationId": "ledgerPreflight",
    "responses": {
     "204": {
      "description": "No Content"
     }
    }
   }
  },
  "/api/transparency/ledger/inclusion": {
   "post": {
    "tags": [
     "transparency"
    ],
    "summary": "Prove an answer is in the ledger",
    "description": "Returns the RFC 6962 inclusion (audit) path for an answer, which the CALLER re-verifies in-browser against the signed root — a '✓ included' verdict trusts the math, not the server. Supply one of `answer`, `answer_sha256`, or `leaf`; optionally pin `tree_size`.",
    "operationId": "ledgerInclusion",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/InclusionRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Inclusion result (found + proof, or found:false).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/InclusionResponse"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadJson"
     },
     "413": {
      "$ref": "#/components/responses/TooLarge"
     },
     "422": {
      "description": "No usable answer/answer_sha256/leaf supplied."
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "503": {
      "description": "Ledger temporarily unavailable."
     }
    }
   },
   "options": {
    "tags": [
     "transparency"
    ],
    "summary": "CORS preflight",
    "operationId": "inclusionPreflight",
    "responses": {
     "204": {
      "description": "No Content"
     }
    }
   }
  },
  "/api/transparency/ledger/consistency": {
   "get": {
    "tags": [
     "transparency"
    ],
    "summary": "Append-only consistency proof",
    "description": "A consistency proof that the log at size `first` is an exact prefix of the log at size `second` (default: current) — i.e. the ledger only appended, never rewrote or deleted history. Re-verify in-browser.",
    "operationId": "ledgerConsistency",
    "parameters": [
     {
      "name": "first",
      "in": "query",
      "required": true,
      "schema": {
       "type": "integer",
       "minimum": 1
      }
     },
     {
      "name": "second",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The consistency proof.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ConsistencyResponse"
        }
       }
      }
     },
     "422": {
      "description": "first/second out of range."
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "503": {
      "description": "Ledger temporarily unavailable."
     }
    }
   },
   "options": {
    "tags": [
     "transparency"
    ],
    "summary": "CORS preflight",
    "operationId": "consistencyPreflight",
    "responses": {
     "204": {
      "description": "No Content"
     }
    }
   }
  },
  "/api/transparency/ledger/history": {
   "get": {
    "tags": [
     "transparency"
    ],
    "summary": "Signed-tree-head witness history",
    "description": "Recent Throndar-signed tree heads (the CT-style gossip record). `consistent:false` with `forked_sizes` would expose a split-view (two roots for one tree_size); `append_only_verified` recomputes each checkpoint against the live log.",
    "operationId": "ledgerHistory",
    "parameters": [
     {
      "name": "verifiable",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "1"
       ]
      },
      "description": "1 to include signed_checkpoints: the newest 20 checkpoints with their ML-DSA-87 receipts and keys, each verifiable offline."
     }
    ],
    "responses": {
     "200": {
      "description": "The witness history.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/LedgerHistory"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "503": {
      "description": "History temporarily unavailable."
     }
    }
   },
   "options": {
    "tags": [
     "transparency"
    ],
    "summary": "CORS preflight",
    "operationId": "historyPreflight",
    "responses": {
     "204": {
      "description": "No Content"
     }
    }
   }
  },
  "/api/v1/evidence-signer": {
   "get": {
    "summary": "Evidence Pack signer public keys",
    "description": "The pinned PUBLIC keys (ML-DSA-87, FIPS 204; hybrid leg SLH-DSA-SHA2-256f, FIPS 205) that sign paid Evidence Packs. Buyers cross-check the signer.pub.json delivered with their pack against this endpoint, then verify offline with @trelyan/verify-pqc. Key ids are SHA-256 fingerprints of the raw public keys. No auth; CORS-open.",
    "tags": [
     "verification"
    ],
    "responses": {
     "200": {
      "description": "Signer identity: alg, per-key publicKeyHex + fingerprint, verification instructions, and the self-attested (not a certification) notice.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "role": {
           "type": "string"
          },
          "issuer": {
           "type": "string"
          },
          "alg": {
           "type": "string"
          },
          "keys": {
           "type": "object"
          },
          "verify": {
           "type": "string"
          },
          "notice": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/v1/provenance/pubkeys": {
   "get": {
    "tags": [
     "keys"
    ],
    "summary": "Model-plane artifact-signing public keys",
    "description": "The bridge's ML-DSA-87 (primary) and Falcon-1024 (cosign) public keys, plus the canonicalization spec, proxied same-origin so a browser or an offline verifier can check the dual signature on any answer without trusting this server. Distinct from /api/provenance/pubkey, which publishes the WEB tier's answer-receipt key: an answer carries both, and a full verification needs both. No auth - public keys are public by definition. Rate limited to 120/min/IP because it proxies a bridge call. Returns {signer, keys: []} when the bridge is not configured, rather than an error, so a verifier can distinguish 'no keys published' from 'request failed'.",
    "operationId": "getBridgePublicKeys",
    "responses": {
     "200": {
      "description": "The published model-plane keys.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "signer": {
           "type": "string",
           "description": "Identifier of the signing party."
          },
          "keys": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "role": {
              "type": "string",
              "description": "primary | cosign"
             },
             "alg": {
              "type": "string",
              "description": "ML-DSA-87 or Falcon-1024"
             },
             "key_id": {
              "type": "string",
              "description": "First 8 bytes of SHA-256 over the public key, hex."
             },
             "public_key_b64": {
              "type": "string",
              "description": "Raw public key, base64."
             }
            }
           }
          }
         }
        },
        "example": {
         "signer": "apex-jarvis",
         "keys": [
          {
           "role": "primary",
           "alg": "ML-DSA-87",
           "key_id": "ea66c20067cbf449"
          }
         ]
        }
       }
      }
     },
     "429": {
      "description": "Rate limit exceeded."
     }
    }
   }
  },
  "/api/status/council": {
   "get": {
    "tags": [
     "status"
    ],
    "summary": "Which council seats are answering right now",
    "description": "The live per-seat probe shown on /status. Every seat is asked a one-word question by the model plane; this reports who answered and how fast. Cached: one probe per five minutes per server instance, shared by every caller, plus a one-minute edge cache. Failure reasons are never returned. No auth; no request body.",
    "operationId": "councilHealth",
    "responses": {
     "200": {
      "description": "The cached probe.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "description": "public, max-age=60, s-maxage=60"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CouncilHealth"
        }
       }
      }
     },
     "503": {
      "description": "The model plane could not be probed (not configured, unreachable, or timed out). Not cached.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "ok"
         ],
         "properties": {
          "ok": {
           "type": "boolean",
           "enum": [
            false
           ]
          }
         }
        }
       }
      }
     }
    }
   }
  }
 },
 "components": {
  "responses": {
   "BadJson": {
    "description": "Body is not valid JSON.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "TooLarge": {
    "description": "Payload over 2 MB.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "NotABundle": {
    "description": "Not a recognised Throndar bundle (or too many transcript turns).",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "RateLimited": {
    "description": "Per-IP rate limit exceeded — honour the Retry-After header. Limits vary by endpoint: the v1 verify/evidence POSTs are ~40/min; the public read endpoints (pubkey, CBOM, ledger) are ~60–120/min.",
    "headers": {
     "Retry-After": {
      "schema": {
       "type": "string"
      }
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "string"
     }
    },
    "required": [
     "error"
    ]
   },
   "ReceiptEnvelope": {
    "type": "object",
    "description": "The signed answer receipt (ML-DSA-87 over a canonical core).",
    "properties": {
     "v": {
      "type": "integer"
     },
     "model": {
      "type": "string",
      "example": "throndar-council"
     },
     "answer_sha256": {
      "type": "string",
      "description": "SHA-256 (hex) of the exact answer text."
     },
     "governance_flag": {
      "type": "string",
      "example": "PASS"
     },
     "ts": {
      "type": "integer",
      "description": "Unix seconds the receipt was signed."
     },
     "council_grounded": {
      "type": "boolean",
      "description": "false ⇒ a single-model fallback (bound into the signature; only serialized when false)."
     },
     "sig": {
      "type": "string",
      "description": "hex signature"
     },
     "key_id": {
      "type": "string",
      "description": "hex thumbprint of the public key"
     },
     "context": {
      "type": "string",
      "example": "throndar-provenance-v1"
     },
     "algo": {
      "type": "string",
      "example": "ML-DSA-87"
     }
    },
    "required": [
     "v",
     "model",
     "answer_sha256",
     "ts",
     "sig",
     "key_id",
     "context",
     "algo"
    ]
   },
   "ReceiptKey": {
    "type": "object",
    "properties": {
     "key_id": {
      "type": "string"
     },
     "public_key_hex": {
      "type": "string"
     },
     "algo": {
      "type": "string"
     },
     "context": {
      "type": "string"
     }
    },
    "required": [
     "key_id",
     "public_key_hex",
     "algo",
     "context"
    ]
   },
   "ProofBundle": {
    "type": "object",
    "description": "A portable, self-verifying proof of one answer (the \"Download proof\" JSON).",
    "properties": {
     "v": {
      "type": "integer"
     },
     "kind": {
      "type": "string",
      "const": "throndar-proof"
     },
     "created": {
      "type": "integer"
     },
     "answer": {
      "type": "string",
      "description": "The exact signed answer text."
     },
     "prompt": {
      "type": "string",
      "description": "Optional: the QUESTION the answer was given. Not signed by itself — it is checked against the receipt's `prompt_sha256` when the receipt carries one, and is otherwise UNVERIFIABLE (the verdict then reports `promptUnverified:true`). Any present value counts as a claimed question, whatever its JSON type."
     },
     "receipt": {
      "$ref": "#/components/schemas/ReceiptEnvelope"
     },
     "receiptKey": {
      "$ref": "#/components/schemas/ReceiptKey"
     },
     "attestation": {
      "type": "object",
      "description": "Optional model-plane dual-signature (ML-DSA-87 + Falcon-1024)."
     },
     "bridgeKeys": {
      "type": "array",
      "items": {
       "type": "object"
      }
     },
     "excerpt": {
      "type": "object",
      "description": "Optional pinned QUOTE (what \"Share quote\" emits): character offsets into `answer` plus the exact text. Not signed — it verifies only as an exact slice of the signed answer, reported as `verdict.excerpt`. A quote that is not that slice fails the bundle.",
      "properties": {
       "start": {
        "type": "integer"
       },
       "end": {
        "type": "integer"
       },
       "quote": {
        "type": "string"
       }
      }
     }
    },
    "required": [
     "kind",
     "answer"
    ]
   },
   "TranscriptBundle": {
    "type": "object",
    "description": "A whole-conversation proof: an ordered list of signed turns.",
    "properties": {
     "kind": {
      "type": "string",
      "const": "throndar-transcript"
     },
     "title": {
      "type": "string"
     },
     "turns": {
      "type": "array",
      "items": {
       "type": "object"
      },
      "maxItems": 200
     }
    },
    "required": [
     "kind",
     "turns"
    ]
   },
   "ProofVerdict": {
    "type": "object",
    "properties": {
     "overallOk": {
      "type": "boolean",
      "description": "True only if every present layer verifies AND at least one layer was checked, a bound question (if carried) matches, and a pinned quote (if carried) is an exact slice of the answer. It does NOT cover an unbound question — see promptUnverified."
     },
     "checks": {
      "type": "integer"
     },
     "receipt": {
      "type": "object",
      "properties": {
       "present": {
        "type": "boolean"
       },
       "answerHashOk": {
        "type": "boolean"
       },
       "sigOk": {
        "type": "boolean"
       },
       "keyId": {
        "type": "string"
       },
       "councilGrounded": {
        "type": "boolean",
        "description": "false ⇒ single-model fallback (trustworthy only when sigOk)."
       },
       "promptBound": {
        "type": "boolean",
        "description": "The receipt BINDS a question (carries prompt_sha256). false ⇒ a legacy receipt: any question beside the answer is unsigned."
       },
       "promptOk": {
        "type": "boolean",
        "description": "Meaningful only when promptBound AND the bundle carries a `prompt`: the supplied question hashes to the signed value (and, for a v:2 receipt, its framing verified). false ⇒ the question was altered and the bundle does not verify. Defaults to true when no prompt is supplied — read questionBinding to know whether a question was checked at all."
       },
       "questionBinding": {
        "type": "string",
        "enum": [
         "none",
         "entire-input",
         "current-message"
        ],
        "description": "What the signature covers for the question this bundle CARRIES. `entire-input`: the question was the whole input. `current-message`: the question was the current message of a larger input, disclosed by `framing`. `none`: the receipt binds no question — the question shown is unsigned. Absent when the bundle carries no question."
       },
       "framing": {
        "type": "object",
        "description": "v:2 receipts with a question: the separately signed disclosure of what else the council read. The flags are reported only when it verified (`ok:true`); contents are never disclosed.",
        "properties": {
         "present": {
          "type": "boolean"
         },
         "ok": {
          "type": "boolean"
         },
         "persona": {
          "type": "integer",
          "enum": [
           0,
           1
          ],
          "description": "1 ⇒ the account's custom instructions were part of the input."
         },
         "voice": {
          "type": "integer",
          "enum": [
           0,
           1
          ],
          "description": "1 ⇒ a live-voice style note was part of the input."
         },
         "attachment": {
          "type": "integer",
          "enum": [
           0,
           1
          ],
          "description": "1 ⇒ an attached file was part of the input."
         },
         "historyTurns": {
          "type": "integer",
          "description": "How many earlier turns of the conversation were folded into the input."
         }
        }
       }
      }
     },
     "promptUnverified": {
      "type": "boolean",
      "description": "Present and true when the bundle CARRIES a question that no bound receipt verified — a legacy receipt (reported, not failed), a question that did not match, or no receipt at all. `overallOk` and `signerRecognized` say nothing about such a question: treat only the answer as attested."
     },
     "excerpt": {
      "type": "object",
      "description": "Present when the bundle pins a quote (`excerpt`). `ok` ⇒ the quote is an exact slice of `answer`; meaningful only beside overallOk. `ok:false` fails the bundle.",
      "properties": {
       "present": {
        "type": "boolean"
       },
       "ok": {
        "type": "boolean"
       }
      }
     },
     "bridge": {
      "type": "object",
      "properties": {
       "present": {
        "type": "boolean"
       },
       "primaryOk": {
        "type": "boolean"
       },
       "secondaryOk": {
        "type": "boolean"
       },
       "keyId": {
        "type": "string"
       }
      }
     },
     "signerRecognized": {
      "type": "boolean",
      "description": "Signing key matches a published Throndar key (ORIGIN)."
     }
    },
    "required": [
     "overallOk",
     "checks"
    ]
   },
   "VerifyResponse": {
    "type": "object",
    "properties": {
     "service": {
      "type": "string",
      "const": "Throndar"
     },
     "kind": {
      "type": "string"
     },
     "created": {
      "type": [
       "integer",
       "null"
      ]
     },
     "verdict": {
      "$ref": "#/components/schemas/ProofVerdict"
     },
     "signerRecognized": {
      "type": "boolean"
     },
     "record": {
      "type": "object",
      "description": "Present only when the posted body was an AI Provenance Record: `verdict` is then the record's embedded proof's, and the record's `attested` summary was recomputed from that proof and matched (a record whose summary claims more than its proof supports is a 422, never a 200). The one claim that can be neither confirmed nor contradicted is ORIGIN, since it depends on the verifier's pinned key set: see `originClaimUnconfirmed`, and decide origin from the top-level `signerRecognized`.",
      "properties": {
       "schema": {
        "type": "string",
        "const": "throndar-ai-provenance-record"
       },
       "summaryOk": {
        "type": "boolean",
        "const": true
       },
       "originClaimUnconfirmed": {
        "type": "boolean",
        "const": true,
        "description": "Optional; present only when the record claims `signer_recognized: true` over a proof that verified against a key this verifier does not pin (what a genuine record issued during a key rotation looks like). `signerRecognized` is then false: origin is UNCONFIRMED, not disproved."
       }
      }
     },
     "disclaimer": {
      "type": "string"
     }
    },
    "required": [
     "service",
     "verdict",
     "signerRecognized",
     "disclaimer"
    ]
   },
   "AiProvenanceRecord": {
    "type": "object",
    "description": "A contextual wrapper around a signed proof: the trust root is the embedded `proof`; the summary is recomputable from it. Not a new signature, not a conformity declaration.",
    "properties": {
     "schema": {
      "type": "string",
      "const": "throndar-ai-provenance-record"
     },
     "schema_version": {
      "type": "integer",
      "const": 1
     },
     "generated": {
      "type": "integer"
     },
     "subject": {
      "type": "object",
      "properties": {
       "organisation": {
        "type": "string"
       }
      }
     },
     "attested": {
      "type": "object",
      "properties": {
       "signed": {
        "type": "boolean"
       },
       "signer_recognized": {
        "type": [
         "boolean",
         "null"
        ]
       },
       "answer_sha256": {
        "type": "string"
       },
       "algorithms": {
        "type": "array",
        "items": {
         "type": "string"
        }
       },
       "governance_flag": {
        "type": "string"
       },
       "council_grounded": {
        "type": [
         "boolean",
         "null"
        ]
       },
       "signed_at": {
        "type": [
         "integer",
         "null"
        ]
       },
       "receipt_key_id": {
        "type": "string",
        "description": "Thumbprint of the key the receipt signature verified against. Absent unless it did verify."
       },
       "question_attested": {
        "type": [
         "boolean",
         "null"
        ],
        "description": "Whether the QUESTION carried in the embedded proof is covered by the signature. true ⇒ bound and matched. false ⇒ a question is present and nothing attests it (a legacy receipt, a proof that did not verify, no receipt, or a bound receipt proving the question was swapped) — `notes` says which. null ⇒ the proof carries no question. `signed:true` beside `question_attested:false` means ONLY the answer is evidence."
       },
       "question_binding": {
        "type": [
         "string",
         "null"
        ],
        "enum": [
         "entire-input",
         "current-message",
         "none",
         null
        ],
        "description": "What an attested question binding covers: the whole input, or the current message of a disclosed larger input. null when no question is attested."
       },
       "quote_attested": {
        "type": [
         "boolean",
         "null"
        ],
        "description": "Whether the pinned QUOTE carried in the embedded proof (`proof.excerpt`) is an exact slice of an answer that verified. false ⇒ the quoted text is not in the answer (the proof then reports signed:false) or the answer did not verify. null ⇒ the proof pins no quote."
       }
      }
     },
     "ai_act_support": {
      "type": "array",
      "items": {
       "type": "object"
      },
      "description": "Published EU-AI-Act obligation mapping (illustrative, not a legal determination)."
     },
     "proof": {
      "$ref": "#/components/schemas/ProofBundle"
     },
     "verify_instructions": {
      "type": "string"
     },
     "disclaimer": {
      "type": "string"
     },
     "notes": {
      "type": "array",
      "items": {
       "type": "string"
      }
     }
    },
    "required": [
     "schema",
     "schema_version",
     "generated",
     "attested",
     "proof",
     "disclaimer"
    ]
   },
   "PubkeysResponse": {
    "type": "object",
    "properties": {
     "algo": {
      "type": "string"
     },
     "context": {
      "type": "string"
     },
     "configured": {
      "type": "boolean"
     },
     "keys": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ReceiptKey"
      }
     },
     "slh_diversity": {
      "type": "object",
      "description": "Optional SLH-DSA-256f diversity public key (present only when enabled)."
     }
    },
    "required": [
     "algo",
     "context",
     "configured",
     "keys"
    ]
   },
   "Cbom": {
    "type": "object",
    "description": "CycloneDX-CBOM-flavoured cryptographic inventory. A transparency document, not a certification.",
    "properties": {
     "bomFormat": {
      "type": "string"
     },
     "specVersion": {
      "type": "string"
     },
     "metadata": {
      "type": "object"
     },
     "cryptography": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string"
        },
        "standard": {
         "type": "string"
        },
        "type": {
         "type": "string"
        },
        "role": {
         "type": "string"
        },
        "status": {
         "type": "string"
        }
       }
      }
     },
     "posture": {
      "type": "object"
     },
     "disclaimer": {
      "type": "string"
     }
    },
    "required": [
     "bomFormat",
     "cryptography",
     "disclaimer"
    ]
   },
   "LedgerSth": {
    "type": "object",
    "description": "A Signed Tree Head (or {enabled:false} when the ledger is off).",
    "properties": {
     "enabled": {
      "type": "boolean"
     },
     "log": {
      "type": "string",
      "example": "throndar-answers-v1"
     },
     "algo": {
      "type": "string",
      "example": "RFC6962-SHA256"
     },
     "leaf_hashing": {
      "type": "string"
     },
     "tree_size": {
      "type": "integer"
     },
     "root_hash": {
      "type": "string"
     },
     "signed_tree_head": {
      "type": [
       "object",
       "null"
      ],
      "description": "ML-DSA-87 signature over {log,tree_size,root_hash,timestamp} under 'throndar-sth-v1'."
     },
     "sth_withheld": {
      "type": "string",
      "enum": [
       "rollback",
       "fork",
       "unverifiable"
      ],
      "description": "Present only when the server REFUSED to sign the current head because of its own published history: rollback (the log is now smaller than a head it published), fork (the published prefix no longer recomputes, or another root is recorded at the current size — history was rewritten, or the log shows a split view), or unverifiable (the published history could not be read — not evidence of either). The same state reports the same reason whichever read observed it. signed_tree_head is null whenever this is set."
     },
     "note": {
      "type": "string"
     }
    },
    "required": [
     "enabled"
    ]
   },
   "InclusionRequest": {
    "type": "object",
    "description": "Supply exactly one of answer / answer_sha256 / leaf.",
    "properties": {
     "answer": {
      "type": "string"
     },
     "answer_sha256": {
      "type": "string",
      "description": "SHA-256 (hex) of the answer."
     },
     "leaf": {
      "type": "string",
      "description": "Precomputed Merkle leaf hash (hex)."
     },
     "tree_size": {
      "type": "integer",
      "description": "Optional: pin the proof to a specific tree size."
     }
    }
   },
   "InclusionResponse": {
    "type": "object",
    "properties": {
     "found": {
      "type": "boolean"
     },
     "leaf": {
      "type": "string"
     },
     "index": {
      "type": "integer"
     },
     "tree_size": {
      "type": "integer"
     },
     "root_hash": {
      "type": "string"
     },
     "proof": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "RFC 6962 inclusion (audit) path."
     }
    },
    "required": [
     "found"
    ]
   },
   "ConsistencyResponse": {
    "type": "object",
    "properties": {
     "first": {
      "type": "integer"
     },
     "second": {
      "type": "integer"
     },
     "old_root": {
      "type": "string"
     },
     "new_root": {
      "type": "string"
     },
     "proof": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "RFC 6962 consistency proof."
     },
     "sth_withheld": {
      "type": "string",
      "enum": [
       "rollback",
       "fork",
       "unverifiable"
      ],
      "description": "Present, with the same values and meaning as on GET /api/transparency/ledger, when the log is CURRENTLY refusing to sign its own head. Carried on BOTH shapes this route can return (this proof and the ?signed=1 bundle), so which one a caller asked for cannot change the verdict. The proof itself is unaffected: it is about two historical checkpoints, not about the log's present height."
     },
     "bundle_withheld": {
      "type": "string",
      "enum": [
       "timestamp-order"
      ],
      "description": "Present only when ?signed=1 was asked for and both checkpoints were published, but they are NOT served as a signed bundle. timestamp-order: the recorded head at `second` carries an earlier timestamp than the one at `first` (the servers that minted them disagreed about the time, or raced), and every verifier would refuse that pair as a rollback. The unsigned proof between old_root and new_root is still served. This field is not evidence of a rollback, and says nothing about the log's current head (that is `sth_withheld`)."
     }
    },
    "required": [
     "first",
     "second",
     "old_root",
     "new_root",
     "proof"
    ]
   },
   "LedgerHistory": {
    "type": "object",
    "properties": {
     "enabled": {
      "type": "boolean"
     },
     "consistent": {
      "type": [
       "boolean",
       "null"
      ],
      "description": "true: no split-view, no truncation, and every recorded root recomputes from the live log — every check ran. false: evidence of a rewrite (a split view needs no leaves, so it is reported even when they cannot be read). null: the checks against the live log could not run (see consistent_unknown) — NOT a pass."
     },
     "consistent_unknown": {
      "type": "string",
      "enum": [
       "leaves_unreadable",
       "leaf_check_incomplete"
      ],
      "description": "Present only when consistent is null: why the checks against the live log did not complete."
     },
     "forked_sizes": {
      "type": "array",
      "items": {
       "type": "integer"
      }
     },
     "append_only_verified": {
      "type": [
       "boolean",
       "null"
      ]
     },
     "inconsistent_sizes": {
      "type": "array",
      "items": {
       "type": "integer"
      }
     },
     "count": {
      "type": "integer"
     },
     "checkpoints": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "tree_size": {
         "type": "integer"
        },
        "root_hash": {
         "type": "string"
        },
        "signed": {
         "type": "string"
        },
        "recorded": {
         "type": "integer"
        }
       }
      }
     },
     "signed_checkpoints": {
      "type": [
       "array",
       "null"
      ],
      "description": "Only with ?verifiable=1. The newest 20 checkpoints, each a self-contained signed tree head {signed, receipt, key} that verifies offline like GET /api/transparency/ledger's signed_tree_head. key is null when the signing key is no longer published. null when they could not be produced.",
      "items": {
       "type": "object"
      }
     },
     "note": {
      "type": "string"
     }
    },
    "required": [
     "enabled"
    ]
   },
   "RunProofBundle": {
    "type": "object",
    "description": "An Autopilot run EVIDENCE PACK — a portable, self-verifying proof of a whole guardrailed run (the \"Download evidence pack\" JSON). The run carries only signed fields; the receipt is ML-DSA-87 under the domain-separated autopilot context.",
    "properties": {
     "v": {
      "type": "integer"
     },
     "kind": {
      "type": "string",
      "const": "throndar-autopilot-run"
     },
     "created": {
      "type": "integer"
     },
     "run": {
      "type": "object",
      "description": "The sanitized run: goal, per-step outputs + verification flags, summary, budget accounting. No account data.",
      "properties": {
       "id": {
        "type": "string"
       },
       "goal": {
        "type": "string"
       },
       "status": {
        "type": "string"
       },
       "budgetCredits": {
        "type": "integer"
       },
       "spentCredits": {
        "type": "integer"
       },
       "maxSteps": {
        "type": "integer"
       },
       "summary": {
        "type": "string"
       },
       "stopReason": {
        "type": "string"
       },
       "steps": {
        "type": "array",
        "items": {
         "type": "object"
        }
       }
      },
      "required": [
       "id",
       "goal",
       "steps"
      ]
     },
     "receipt": {
      "$ref": "#/components/schemas/ReceiptEnvelope"
     },
     "receiptKey": {
      "$ref": "#/components/schemas/ReceiptKey"
     }
    },
    "required": [
     "kind",
     "run"
    ]
   },
   "ExportArchive": {
    "type": "object",
    "description": "A SIGNED conversation export (the \"Download signed export\" JSON from GET /api/conversations/export?signed=1). The nested proof (proof.kind = 'throndar-export') carries an ML-DSA-87 receipt over a manifest that binds a SHA-256 of the whole conversations payload, under the domain-separated export context. Verification proves the archive is an unaltered, Throndar-signed export of exactly these conversations — not that the content is correct.",
    "properties": {
     "service": {
      "type": "string"
     },
     "exported": {
      "type": "string"
     },
     "count": {
      "type": "integer"
     },
     "conversations": {
      "type": "array",
      "description": "The signed payload — the exact conversations the manifest's payload_sha256 commits to.",
      "items": {
       "type": "object"
      }
     },
     "signed_manifest": {
      "type": "string"
     },
     "proof": {
      "type": "object",
      "properties": {
       "v": {
        "type": "integer"
       },
       "kind": {
        "type": "string",
        "const": "throndar-export"
       },
       "context": {
        "type": "string",
        "description": "Carried for display only; the verifier PINS the export context and never trusts this."
       },
       "created": {
        "type": "integer"
       },
       "manifest": {
        "type": "string",
        "description": "JSON string binding payload_sha256 + conversation/message counts; this exact string is what the receipt signs."
       },
       "receipt": {
        "$ref": "#/components/schemas/ReceiptEnvelope"
       },
       "receiptKey": {
        "$ref": "#/components/schemas/ReceiptKey"
       }
      },
      "required": [
       "kind",
       "manifest",
       "receipt",
       "receiptKey"
      ]
     }
    },
    "required": [
     "conversations",
     "proof"
    ]
   },
   "SthBundle": {
    "type": "object",
    "description": "A Signed Tree Head — a transparency-log checkpoint: submit the `signed_tree_head` object from GET /api/transparency/ledger ITSELF, bare. The full ledger response (a wrapper carrying a nested `signed_tree_head`) is refused (kind \"unknown\", HTTP 422): the verdict is about the object you submit, and a wrapper has no signature of its own — accepting it would report the whole document as Throndar-signed on the strength of a public checkpoint anyone can copy in. The ML-DSA-87 receipt signs the tree-head string under the domain-separated STH context. Verification proves it is a genuine, unaltered Throndar commitment to (tree_size, root) at a time — append-only-ness itself needs a consistency proof between two checkpoints.",
    "properties": {
     "signed": {
      "type": "string",
      "description": "JSON string { log, tree_size, root_hash, timestamp } — the exact bytes the receipt signs."
     },
     "receipt": {
      "$ref": "#/components/schemas/ReceiptEnvelope"
     },
     "key": {
      "$ref": "#/components/schemas/ReceiptKey"
     }
    },
    "required": [
     "signed",
     "receipt",
     "key"
    ]
   },
   "ConsistencyBundle": {
    "type": "object",
    "description": "A consistency bundle from GET /api/transparency/ledger/consistency?signed=1 — two Throndar-signed tree heads plus the RFC 6962-bis consistency proof between them. Verification proves BOTH checkpoints are genuine AND the older signed root is an exact prefix of the newer (the log only appended between them — no rewrite or split-view), entirely offline. It also requires the two SIGNED timestamps to be ordered like the sizes: an `old` head signed after the `new` one says the log was smaller later — a rollback presented as growth — and is refused whatever the Merkle proof says. The verdict reports that rule on its own as `timestampOrderOk` (present when both heads are genuine), so two genuine heads in rollback order can be told from a failed signature or proof. Verifiers built before this rule (throndar-verify 1.0.x) do not apply it.",
    "properties": {
     "kind": {
      "type": "string",
      "const": "throndar-consistency"
     },
     "old": {
      "$ref": "#/components/schemas/SthBundle"
     },
     "new": {
      "$ref": "#/components/schemas/SthBundle"
     },
     "proof": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "RFC 6962-bis consistency proof — hex Merkle node hashes relating the two signed roots."
     },
     "sth_withheld": {
      "type": "string",
      "enum": [
       "rollback",
       "fork",
       "unverifiable"
      ],
      "description": "Present BESIDE the bundle (never inside a signed head) when the log is currently refusing to sign its own head — the same field, with the same values, GET /api/transparency/ledger reports. The bundle's verdict stays true and unchanged: both heads are genuine checkpoints this log published and the older root is a prefix of the newer. This says that the tree has since moved below or away from a head it published, which no proof between two past checkpoints can show. Verifiers do not read it; monitors and auditors should."
     }
    },
    "required": [
     "kind",
     "old",
     "new",
     "proof"
    ]
   },
   "CouncilSeatHealth": {
    "type": "object",
    "required": [
     "id",
     "label",
     "model",
     "ok",
     "keyed"
    ],
    "properties": {
     "id": {
      "type": "string",
      "example": "openai"
     },
     "label": {
      "type": "string",
      "example": "OpenAI"
     },
     "model": {
      "type": "string",
      "example": "gpt-6-astra"
     },
     "ok": {
      "type": "boolean",
      "description": "Answered the one-word probe."
     },
     "keyed": {
      "type": "boolean",
      "description": "Has a key at all; an unkeyed seat is not enabled, not down."
     },
     "ms": {
      "type": "integer",
      "description": "Probe latency in milliseconds; absent for an unkeyed seat."
     }
    }
   },
   "CouncilHealth": {
    "type": "object",
    "required": [
     "ok",
     "defined",
     "configured",
     "working",
     "probed_at",
     "seats"
    ],
    "properties": {
     "ok": {
      "type": "boolean",
      "enum": [
       true
      ]
     },
     "defined": {
      "type": "integer",
      "description": "Seats the council defines."
     },
     "configured": {
      "type": "integer",
      "description": "Seats with a key."
     },
     "working": {
      "type": "integer",
      "description": "Seats that answered the probe."
     },
     "probed_at": {
      "type": "string",
      "format": "date-time"
     },
     "seats": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CouncilSeatHealth"
      }
     }
    }
   }
  }
 }
}
