{
  "openapi": "3.1.1",
  "info": {
    "title": "Verifable.Server | v1",
    "version": "1.0.0"
  },
  "paths": {
    "/wallet/config.js": {
      "get": {
        "tags": [
          "Verifable.Server"
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/oauth/atproto/client-metadata.json": {
      "get": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "Serves the OAuth client metadata document whose absolute URL is this client's `client_id`.\r\nPublic, cacheable bootstrap data — authorization servers fetch it to learn the redirect URI,\r\ngrant types, scope, and that tokens must be DPoP-bound.",
        "operationId": "AtProtocolClientMetadata",
        "responses": {
          "200": {
            "description": "The metadata JSON."
          }
        }
      }
    },
    "/did/resolve": {
      "get": {
        "tags": [
          "Verifable.Server"
        ],
        "parameters": [
          {
            "name": "did",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/login": {
      "post": {
        "tags": [
          "Program"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/ssf/poll/{streamId}": {
      "post": {
        "tags": [
          "Program"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "streamId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/ssf/receive": {
      "post": {
        "tags": [
          "Program"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/status/{listId}": {
      "get": {
        "tags": [
          "Program"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "listId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/secdsa/activate": {
      "post": {
        "tags": [
          "Verifable.Server"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/secdsa/instruct": {
      "post": {
        "tags": [
          "Verifable.Server"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/{path}": {
      "get": {
        "tags": [
          "Program"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Program"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/.well-known/openid-credential-issuer/{rest}": {
      "get": {
        "tags": [
          "Verifable.Server"
        ],
        "parameters": [
          {
            "name": "rest",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server/{rest}": {
      "get": {
        "tags": [
          "Verifable.Server"
        ],
        "parameters": [
          {
            "name": "rest",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource/{rest}": {
      "get": {
        "tags": [
          "Verifable.Server"
        ],
        "parameters": [
          {
            "name": "rest",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/oid4vci/offer": {
      "post": {
        "tags": [
          "FlowInitiationApi"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/oid4vp/request": {
      "post": {
        "tags": [
          "FlowInitiationApi"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/oid4vci/offer/{offerId}/status": {
      "get": {
        "tags": [
          "FlowInitiationApi"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/connect/{segment}/oid4vp/request/{parHandle}/status": {
      "get": {
        "tags": [
          "FlowInitiationApi"
        ],
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "parHandle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/resolver/{identifier}": {
      "get": {
        "tags": [
          "DppResolver"
        ],
        "summary": "Resolves a DID-path or opaque identifier: a did:web product id\r\n(`did:web:&lt;host&gt;:product:&lt;id&gt;`), or the bare product id. The trailing\r\n`product:&lt;id&gt;` segment is the lookup key.",
        "description": "Dereferences a did:web product id (or the bare product id) to the passport's RFC 9264 linkset: the signed passport (dpp.json), the DID document (did.json), and a human-page placeholder, with matching Link headers. Content negotiation redirects (302) to one representation, or returns 300 Multiple Choices with the linkset body (the default).",
        "operationId": "ResolveDppIdentifier",
        "parameters": [
          {
            "name": "identifier",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/01/{gtin}": {
      "get": {
        "tags": [
          "DppResolver"
        ],
        "summary": "Resolves the GS1 Digital Link-style HTTP path `/01/{gtin}` (AI 01 = GTIN). The GTIN is\r\ncanonicalised to GTIN-14 and its check digit validated (bool Gs1DigitalLink.TryNormaliseGtin(ReadOnlySpan&lt;char&gt; raw, out string gtin14));\r\na digit-shaped-but-invalid value (wrong length or check digit) refuses with a\r\n`gs1-digital-link-syntax` problem+json 400, and a syntactically valid but unindexed GTIN with\r\na `gs1-key-unknown` problem+json 404. A value that never looked like a GTIN attempt at all —\r\ne.g. an opaque product id, this route's ORIGINAL contract before GS1 syntax was understood — still\r\nresolves through the SAME lookup as a bare product id, so that original contract keeps working;\r\nit is a bare 404 when unknown, not a GS1-syntax refusal (it never claimed to be GS1 syntax).\r\nResolves to the same linkset as the DID path — CIRPASS \"one system, two ways\".",
        "description": "The HTTP access path (GS1 Digital Link AI 01 = GTIN). The GTIN is canonicalised to GTIN-14 and its check digit is validated server-side; a digit-shaped-but-invalid value (wrong length or check digit) refuses with an RFC 9457 problem+json 400 (type .../gs1-digital-link-syntax), and a syntactically valid but unindexed GTIN with a problem+json 404 (type .../gs1-key-unknown). Resolves to the same linkset as the DID path — CIRPASS 'one system, two ways'.",
        "operationId": "ResolveDppGtin",
        "parameters": [
          {
            "name": "gtin",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/01/{gtin}/21/{serial}": {
      "get": {
        "tags": [
          "DppResolver"
        ],
        "summary": "Resolves the two-key GS1 Digital Link path `/01/{gtin}/21/{serial}` (AI 01 = GTIN, AI 21 =\r\nthe serialised item). Both segments are validated before lookup — a route with no legacy contract\r\nto preserve, so an invalid GTIN or serial always refuses with a `gs1-digital-link-syntax`\r\nproblem+json 400, and a syntactically valid but unindexed pair with a `gs1-key-unknown`\r\nproblem+json 404. Resolves to the same linkset shape as the bare-GTIN and DID paths, anchored at\r\nthis two-key canonical DL URI.",
        "description": "The two-key GS1 Digital Link path (AI 01 = GTIN, AI 21 = serial number). Both segments are canonicalised and validated server-side before lookup; either segment failing syntax refuses with an RFC 9457 problem+json 400 (type .../gs1-digital-link-syntax), and a syntactically valid but unindexed GTIN/serial pair with a problem+json 404 (type .../gs1-key-unknown). Resolves to the same linkset as the bare /01/{gtin} and DID paths — CIRPASS 'one system, two ways'.",
        "operationId": "ResolveDppGtinSerial",
        "parameters": [
          {
            "name": "gtin",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "serial",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/resolve/{productId}": {
      "get": {
        "tags": [
          "DppResolver"
        ],
        "summary": "Resolves a product identifier to its ISO/IEC 18975 typed link-set: `anchor` = the product DID,\r\nwith a `dpp` / `dcc` / `dte` link array per issued UNTP credential. An optional\r\n`?linkType=` filter narrows the result to one relation (or `all`). Returns 200 with the\r\n`application/linkset+json` body and matching RFC 8288 `Link` headers when the product exists;\r\n404 with an empty link-set when the product id is unknown (no crash, no leak).",
        "description": "Dereferences a product id to an RFC 9264 application/linkset+json body: anchor = the product DID, with a dpp/dcc/dte link array per issued UNTP credential (each a vc+jwt the link target serves), plus matching RFC 8288 Link headers. An optional ?linkType= filter narrows to one relation (or 'all'). 404 with an empty link-set when the product id is unknown.",
        "operationId": "ResolveUntpProductLinkset",
        "parameters": [
          {
            "name": "productId",
            "in": "path",
            "description": "The product id to resolve (the trailing `product:&lt;id&gt;` segment).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/resolve/credential/{credentialReference}": {
      "get": {
        "tags": [
          "DppResolver"
        ],
        "summary": "    Serves a single issued UNTP credential by the opaque reference a resolver link target carries,\r\n    TIER-GATED so the public consumer never receives the Annex-XIII-restricted DPP fields (UNTP\r\n    Decentralised Access Control, first slice). The tier is resolved EXACTLY as the `dpp.json` PEP\r\n    resolves it — ONLY from the authenticated session cookie's `dpp.stakeholder` scope\r\n    (bool DppGateway.IsStakeholder(HttpContext httpContext)), never from a client-supplied header or query parameter — so a\r\n    public caller cannot escalate the tier by tampering the request.\r\n    * A stakeholder caller gets the full SIGNED vc+jwt JWS (application/vc+jwt), as\r\n    before — the verifiable credential.\r\n* A non-stakeholder caller fetching a DPP gets the UNSIGNED tier projection\r\n    (application/json): the masked fields (manufacturingPlace) are removed byte-absent and the\r\n    signed proof is dropped, reusing the SAME DppTieredAccess tiering as dpp.json. The\r\n    restricted field and its signed proof are NEVER on the wire for the public tier.\r\n* A non-stakeholder caller fetching a DCC/DTE gets the full signed credential, as before — no\r\n    DPP-tier restriction is defined for those types yet (per-type DCC/DTE tiering is a later slice).\r\n    FAIL-CLOSED: an indeterminate type, or a payload that cannot be decoded for projection, defaults to the\r\n    masked DPP projection (or 404) rather than risk serving restricted bytes. Returns 404 when no credential\r\n    is served for the reference.",
        "description": "The target the ISO/IEC 18975 link-set's dpp/dcc/dte href values dereference. TIER-GATED (UNTP Decentralised Access Control): a stakeholder (authenticated, dpp.stakeholder scope) gets the full signed application/vc+jwt JWS a cold verifier can fetch and verify; a non-stakeholder fetching a DPP gets the unsigned application/json public-tier projection (the Annex-XIII-restricted manufacturingPlace removed byte-absent, the proof dropped) so the restricted field is never on the wire; a DCC/DTE is served in full (no DPP-tier restriction yet). The tier comes ONLY from the session cookie scope, never a header or query parameter. 404 when no credential is served for the reference.",
        "operationId": "ResolveUntpCredential",
        "parameters": [
          {
            "name": "credentialReference",
            "in": "path",
            "description": "The opaque credential reference from a resolver link target.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/config.js": {
      "get": {
        "tags": [
          "SiteConfig"
        ],
        "summary": "Serves the stamped environment module. The body is composed from the startup-resolved options through\r\nthe source-gen JSON string serializer (each value is a JSON string literal, so the module is valid\r\nESM regardless of the configured values).",
        "description": "Returns `export const config = { appBase, channel, region, version };` as text/javascript with Cache-Control: no-cache. The values are stamped from configuration (Site:AppBase / Site:Channel / Site:Region) with environment-appropriate defaults; appBase always ends with a trailing slash. The client imports this through its import map instead of inferring the environment from location.",
        "operationId": "SiteConfig",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/atproto/account": {
      "get": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "Reads the wallet account's connection (no token material).",
        "operationId": "AtProtocolAccount",
        "responses": {
          "200": {
            "description": "The account response."
          }
        }
      },
      "delete": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "Disconnects the account: discards the server-held tokens and DPoP key. Wallet data is untouched.\r\nThe grant itself remains listed at the person's PDS until revoked there; server-side revocation\r\nagainst the issuer's revocation endpoint is a tracked follow-up.",
        "operationId": "AtProtocolDisconnect",
        "responses": {
          "200": {
            "description": "The now-empty account response."
          }
        }
      }
    },
    "/api/atproto/connect": {
      "post": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "Begins an OAuth connection from a handle; returns the authorization URL or an in-band error.",
        "operationId": "AtProtocolConnect",
        "responses": {
          "200": {
            "description": "The begin outcome."
          }
        }
      }
    },
    "/api/atproto/callback": {
      "get": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "The OAuth callback. State is single-use (a replay fails closed), the code is exchanged DPoP-bound,\r\nthe token subject must match the resolved DID, and the browser returns to the wallet with the\r\noutcome in the query — never with token material.",
        "operationId": "AtProtocolCallback",
        "responses": {
          "200": {
            "description": "A redirect to the wallet."
          }
        }
      }
    },
    "/api/atproto/feed": {
      "get": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "One Network reader section. `following` proxies the timeline; `opportunities` and\r\n`consultations` derive from the statement log by epistemic kind; `relevant` and\r\n`things` derive from DPP-linked statements. Derived sections work without a connection.",
        "operationId": "AtProtocolFeed",
        "responses": {
          "200": {
            "description": "The feed page."
          }
        }
      }
    },
    "/api/atproto/thread": {
      "get": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "Reads one thread.",
        "operationId": "AtProtocolThread",
        "responses": {
          "200": {
            "description": "The thread response."
          }
        }
      }
    },
    "/api/atproto/post": {
      "post": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "Publishes a standard Bluesky-compatible post under the wallet's publication contract: length-capped\r\ntext, explicit disclosure confirmation for passport-linked content, agent drafts referenced for\r\naudit, and the publication recorded as a classified public statement — never as passport evidence.",
        "operationId": "AtProtocolPublish",
        "responses": {
          "200": {
            "description": "The publish outcome."
          }
        }
      }
    },
    "/api/atproto/conversations": {
      "get": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "The public conversation recorded around one passport, as classified statements.",
        "operationId": "AtProtocolConversations",
        "responses": {
          "200": {
            "description": "The conversations response."
          }
        }
      }
    },
    "/api/atproto/agent-draft": {
      "post": {
        "tags": [
          "AtProtocol"
        ],
        "summary": "Prepares an agent draft for a passport. Draft-only by mandate default: the draft is stored for\r\nreview and publish-time audit; nothing is published here and the agent holds no publish authority.",
        "operationId": "AtProtocolAgentDraft",
        "responses": {
          "200": {
            "description": "The draft response."
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "HealthCheck"
        ],
        "operationId": "HealthCheck",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/{segment}/documents/{id}": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Read a document.",
        "description": "AuthZEN-gated: permitted for any authenticated subject.",
        "operationId": "ReadDocument",
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "put": {
        "tags": [
          "Documents"
        ],
        "summary": "Write a document.",
        "description": "AuthZEN-gated: permitted only for the resource owner.",
        "operationId": "WriteDocument",
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/assets/federation/{id}": {
      "get": {
        "tags": [
          "FederationAssets"
        ],
        "summary": "Serves a cached federation branding asset by its content-addressed id. The bytes were fetched once\r\nand content-checked at ingest; this only ever serves our local copy, never a foreign resource.",
        "description": "Returns a logo fetched once from a federation entity and now self-hosted (content-addressed, immutable); the system never depends on the foreign resource at request time.",
        "operationId": "GetFederationAsset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/api/clients": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Lists the registered clients/tenants.",
        "operationId": "ListAdminClients",
        "responses": {
          "200": {
            "description": "The client summaries as JSON."
          }
        }
      }
    },
    "/admin/api/clients/{segment}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "The full per-tenant AS configuration the admin console shows and edits. The host resolves the\r\nregistration cache-aside (DB-aware), so it works with or without a database and does not depend\r\non the routing table being warm.",
        "operationId": "GetAdminClient",
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "description": "The tenant segment.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The tenant configuration as JSON, or 404 when the segment is unknown."
          }
        }
      },
      "put": {
        "tags": [
          "Admin"
        ],
        "summary": "Updates one tenant's AS configuration.",
        "operationId": "UpdateAdminClient",
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "description": "The tenant segment.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The updated configuration as JSON, 400 on a malformed body, or 404 when unknown."
          }
        }
      }
    },
    "/admin/api/clients/{segment}/deactivate": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Deactivates one client in the database store.",
        "operationId": "DeactivateAdminClient",
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "description": "The tenant segment.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "204 on deactivation, 404 when unknown, or 503 when no database is configured."
          }
        }
      }
    },
    "/ops/api/decisions": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "Recent decisions, newest first, with optional tenant/subject/decision filters — the help-desk\r\nquery. Console-only: deny rows carry the reason + contributing signals.",
        "operationId": "ListOpsDecisions",
        "responses": {
          "200": {
            "description": "The decision rows as JSON."
          }
        }
      }
    },
    "/ops/api/decisions/{traceId}": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "The headline help-desk workflow: every decision row for one trace id (the handle the PEP\r\nsurfaces on its error page / the X-Verifable-Trace-Id response header).",
        "operationId": "GetOpsDecisionsByTrace",
        "parameters": [
          {
            "name": "traceId",
            "in": "path",
            "description": "The trace id to look up.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The decision rows as JSON."
          }
        }
      }
    },
    "/ops/api/streams": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "Stream inventory joined with the per-stream delivery counters (delivered / failed / dead-letter\r\ncounts + last completed_at from the SET-delivery tables).",
        "operationId": "ListOpsStreams",
        "responses": {
          "200": {
            "description": "The stream rows as JSON."
          }
        }
      }
    },
    "/ops/api/events": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "Recent SETs (the Events panel), newest first, optional tenant/status filters.",
        "operationId": "ListOpsEvents",
        "responses": {
          "200": {
            "description": "The event rows as JSON."
          }
        }
      }
    },
    "/ops/api/dead-letters": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "Recent dead letters across all streams — the investigation queue for deliveries that failed and\r\nwere retained rather than dropped. Optional tenant filter.",
        "operationId": "ListOpsDeadLetters",
        "responses": {
          "200": {
            "description": "The dead-letter rows as JSON."
          }
        }
      }
    },
    "/ops/api/requests": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "The IGA access-request read surface for the operator console — recorded deny→request intents\r\n(newest first, optional subject/status filter). The Decisions panel's deny rows can link here so\r\nan operator sees which denies turned into requests. Reads the same\r\nAccessRequestStore the IGA API records into; carries NO reason (the\r\nreason-non-disclosure rule holds on this projection too).",
        "operationId": "ListOpsAccessRequests",
        "responses": {
          "200": {
            "description": "The access-request rows as JSON."
          }
        }
      }
    },
    "/ops/api/traces": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "The trace list: recent traces summarized from the local spans store, newest first — the\r\ndashboard's entry point. A locally running server (the dev origin, or the test host under a\r\ndebugger) lists the traces it just recorded.",
        "operationId": "ListOpsTraces",
        "responses": {
          "200": {
            "description": "The trace summary rows as JSON."
          }
        }
      }
    },
    "/ops/api/action-events": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "Recent action-event ledger rows, newest first, with optional action/tenant/actor/outcome\r\nfilters — the Actions panel and the metering/audit investigation query. Every row carries its\r\nchain position (seq + hash pair), so the panel shows the tamper-evident ledger as it is.",
        "operationId": "ListOpsActionEvents",
        "responses": {
          "200": {
            "description": "The ledger rows as JSON."
          }
        }
      }
    },
    "/ops/api/contracts": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "The contracts overview: the billing-contract version in force per tenant — which tenants are\r\ncontracted, at which version, enumerating which charge types.",
        "operationId": "ListOpsContracts",
        "responses": {
          "200": {
            "description": "The contract rows as JSON."
          }
        }
      }
    },
    "/ops/api/charges": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "One tenant's charges view — the charge-type join: the tenant's ledger accrual (succeeded rows\r\nonly; refusals are audit, not billing) classified by the contract version in force. Each charge\r\ntype carries its exact per-action aggregates from the whole ledger plus the tenant's recent\r\nevents bucketed under it, every event traceable through its trace id. Actions no charge type\r\nclaims surface in the unclassified remainder — accrual the contract does not cover is made\r\nvisible, never dropped. A tenant without a contract answers with a null contract and its whole\r\naccrual unclassified.",
        "operationId": "GetOpsCharges",
        "responses": {
          "200": {
            "description": "The charges view as JSON, or 400 without a tenant."
          }
        }
      }
    },
    "/ops/api/trace/{traceId}": {
      "get": {
        "tags": [
          "Operations"
        ],
        "summary": "The Traces panel's joined view: the span rows for a trace id (the waterfall), the decision rows\r\nit produced, the action-event ledger rows it recorded, plus the SET rows that correlate to it.\r\nWith no trace-id column on the SET record yet, the SET side is matched by the subjects the\r\ndecision rows name (the available cross-table correlation — the header says so).",
        "operationId": "GetOpsTrace",
        "parameters": [
          {
            "name": "traceId",
            "in": "path",
            "description": "The trace id to look up.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The joined trace view as JSON."
          }
        }
      }
    },
    "/iga/api/requests": {
      "post": {
        "tags": [
          "Iga"
        ],
        "summary": "Records a new access request. The body is read manually through the source-gen context. The\r\ntrace id the deny surfaced links the request back to the deny decision-log row (the audit\r\nchain).",
        "description": "The deny envelope's request_url target: records the named entitlement request as pending, linked to the deny decision row by the supplied trace id. Requires an authenticated session; a missing subject or entitlement is a 400.",
        "operationId": "CreateAccessRequest",
        "responses": {
          "200": {
            "description": "201 with the recorded request, or 400 on a malformed/incomplete submission."
          }
        }
      },
      "get": {
        "tags": [
          "Iga"
        ],
        "summary": "Reads recent access requests, filterable by subject and/or status. An absent subject filter\r\nreturns every subject's requests — an operator view, which is why the read sits behind the\r\nsystem-admin policy.",
        "operationId": "ListAccessRequests",
        "responses": {
          "200": {
            "description": "The requests as JSON."
          }
        }
      }
    },
    "/iga/api/requests/{id}": {
      "get": {
        "tags": [
          "Iga"
        ],
        "summary": "Reads one access request by id.",
        "operationId": "GetAccessRequest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The request id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request as JSON, or 404 when the id is unknown."
          }
        }
      }
    },
    "/webauthn/register/options": {
      "post": {
        "tags": [
          "WebAuthn"
        ],
        "summary": "Begins a registration ceremony: authorizes the enrollment (an existing `admin.system` session,\r\nor the single-use bootstrap token for the seeded admin's FIRST passkey), then issues fresh\r\n`PublicKeyCredentialCreationOptions` (with a new single-use challenge) for the named admin and\r\nrecords the challenge — with its authorization mode — as short-lived ceremony state. An unauthorized\r\nrequest is refused with a 403 problem before any ceremony state is created.",
        "description": "Body { \"username\"?, \"displayName\"?, \"bootstrapToken\"? } (all optional; an empty body targets the seeded admin). Enrollment is authorized, not open: the request must ride an authenticated admin.system session, or — for the FIRST enrollment only — present the out-of-band single-use bootstrap token for the seeded admin username; otherwise 403 (enrollment_not_authorized). Returns the W3C PublicKeyCredentialCreationOptions JSON (rp, user with base64url id, base64url challenge, pubKeyCredParams, excludeCredentials, authenticatorSelection, attestation) for navigator.credentials.create. The challenge is single-use and expires.",
        "operationId": "WebAuthnRegisterOptions",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/webauthn/register/verify": {
      "post": {
        "tags": [
          "WebAuthn"
        ],
        "summary": "Completes a registration ceremony: consumes the challenge (single-use, replay-protected),\r\nRE-ENFORCES the options leg's enrollment authorization (a session-authorized ceremony requires the\r\n`admin.system` session again; a bootstrap ceremony atomically consumes the single-use token —\r\nfirst writer wins), verifies the attestation, and on success persists the credential. Returns the\r\nverdict in-band; a malformed body, an unknown/used/expired challenge, or a failed attestation is a\r\n400, and a failed enrollment authorization is a 403.",
        "description": "Body is the registration credential JSON { id, rawId, type:\"public-key\", response: { attestationObject, clientDataJSON } } (all base64url). The options leg's enrollment authorization is re-enforced before persistence (session required again; a bootstrap ceremony consumes the single-use token atomically) — a failed authorization is 403. Returns 200 { verified:true, credentialId, attestationTier, error:null } on success; a malformed body, an unknown/used/expired challenge, or a failed attestation returns 400.",
        "operationId": "WebAuthnRegisterVerify",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/webauthn/authenticate/options": {
      "post": {
        "tags": [
          "WebAuthn"
        ],
        "summary": "Begins an authentication ceremony: issues fresh `PublicKeyCredentialRequestOptions` (with a new\r\nsingle-use challenge), narrowed to the named admin's registered credentials when the username is known.",
        "description": "Body { \"username\"? } (optional; narrows allowCredentials to that admin's passkeys, else discoverable). Returns the W3C PublicKeyCredentialRequestOptions JSON (base64url challenge, rpId, allowCredentials, userVerification) for navigator.credentials.get. The challenge is single-use and expires.",
        "operationId": "WebAuthnAuthenticateOptions",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/webauthn/authenticate/verify": {
      "post": {
        "tags": [
          "WebAuthn"
        ],
        "summary": "Completes an authentication ceremony: consumes the challenge (single-use, replay-protected), verifies\r\nthe assertion against the stored credential, advances its signature counter, and — on success — issues\r\nthe admin session cookie (subject = user handle, scope `admin.system`, the attestation-tier\r\nmarker). Returns the verdict in-band; any failure is a 400 and no session is issued.",
        "description": "Body is the assertion credential JSON { id, rawId, type:\"public-key\", response: { authenticatorData, clientDataJSON, signature, userHandle } } (all base64url; userHandle MAY be null). On a verified assertion the response Set-Cookie carries the admin session (subject=user handle, scope admin.system, attestation-tier marker) and the body (200) is { verified:true, subject, attestationTier, error:null }; any failure returns 400.",
        "operationId": "WebAuthnAuthenticateVerify",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/webauthn/session": {
      "get": {
        "tags": [
          "WebAuthn"
        ],
        "summary": "Reports the current admin session (anonymous-readable): whether a session is present, its subject (the\r\nadmin user handle), the attestation-tier marker, and whether it carries the `admin.system` scope.\r\nA page reads this to render signed-in state without parsing the opaque session cookie.",
        "description": "Returns { authenticated, subject, attestationTier, isSystemAdmin }. Anonymous-readable: reports authenticated:false when no session is present, so a page can render signed-in state without parsing the opaque session cookie.",
        "operationId": "WebAuthnSession",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/cbor/inspect": {
      "get": {
        "tags": [
          "CborInspect"
        ],
        "summary": "Inspects a CBOR document given as hex: returns the offset-annotated item tree plus the library's\r\nEDN rendering. Decode failures are part of the diagnosis and are reported in-band with HTTP 200.",
        "description": "Parses the hex-encoded CBOR and returns each item with its byte offset/length, so a client can highlight the connection between the binary and the Extended Diagnostic Notation. Decode errors are reported in-band — the diagnosis is the product.",
        "operationId": "InspectCbor",
        "parameters": [
          {
            "name": "hex",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/vc/issue": {
      "post": {
        "tags": [
          "Credentials"
        ],
        "summary": "Signs the credential in the request body with the playground's did:key identity\r\n(`eddsa-jcs-2022` Data Integrity proof) and returns the secured credential.",
        "description": "The request body is the unsigned credential JSON. The issuer is overwritten with the playground's did:key and the credential is secured with an eddsa-jcs-2022 Data Integrity proof (RFC 8785 canonicalization — every property is signed, no JSON-LD context is fetched). Document problems are reported in-band.",
        "operationId": "IssueCredential",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/vc/verify": {
      "post": {
        "tags": [
          "Credentials"
        ],
        "summary": "Verifies the signed credential in the request body: resolves the issuer's did:key in-process\r\nand checks the Data Integrity proof. Tampering shows up as `IsValid: false` with\r\n`SignatureInvalid`.",
        "description": "The request body is the signed credential JSON. The issuer's did:key is resolved in-process and the eddsa-jcs-2022 proof is checked; a failed check is a verdict (isValid: false with a failure reason such as SignatureInvalid), not an error. Issuers with other DID methods are reported as unsupported rather than fetched.",
        "operationId": "VerifyCredential",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/vc/validate": {
      "post": {
        "tags": [
          "Credentials"
        ],
        "summary": "Validates the credential in the request body against one of the embedded workbench schemas\r\n(`?schema=credential|battery-passport`; defaults to the VCDM envelope). Findings carry\r\nRFC 6901 JSON Pointers into the credential and the schema. No remote `$ref` is resolved.",
        "description": "The request body is the credential JSON; ?schema selects the embedded schema (credential = the VCDM 2.0 structural envelope, battery-passport = the EU battery passport attribute clusters). Findings carry RFC 6901 JSON Pointers into both the credential and the schema. Remote $ref resolution is off — validation never fetches an external resource.",
        "operationId": "ValidateCredential",
        "parameters": [
          {
            "name": "schema",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/vc/bbs/issue": {
      "post": {
        "tags": [
          "DataIntegrityBbs"
        ],
        "summary": "Issues a `bbs-2023` base proof over the submitted credential, binding the request's mandatory\r\npointers. The playground tenant's BBS public key is published as the proof's `verificationMethod`.",
        "description": "The request body is {credential, mandatoryPointers}: the unsecured credential JSON and the RFC 6901 JSON pointers to always-disclosed claims. The credential is secured with a bbs-2023 base proof under the playground tenant's BLS12-381 G2 BBS key, whose public half is published as the proof's verificationMethod did:key Multikey. Document problems are reported in-band.",
        "operationId": "IssueBbs2023Credential",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/vc/bbs/derive": {
      "post": {
        "tags": [
          "DataIntegrityBbs"
        ],
        "summary": "Derives a `bbs-2023` presentation from the submitted base credential, disclosing only the\r\nrequest's selective pointers (plus the mandatory pointers the base proof bound) under the supplied\r\npresentation header.",
        "description": "The request body is {credential, selectivePointers, presentationHeaderHex}: the bbs-2023 base credential, the RFC 6901 JSON pointers the verifier wants disclosed, and the presentation header (the verifier nonce / challenge) as hex. The derived credential discloses only the mandatory + requested claims; the undisclosed claims are removed. Document problems are reported in-band.",
        "operationId": "DeriveBbs2023Presentation",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/vc/bbs/verify": {
      "post": {
        "tags": [
          "DataIntegrityBbs"
        ],
        "summary": "Verifies a `bbs-2023` proof (base or derived) in the request body against the playground tenant's\r\nBBS public key. A failed check is a verdict (`IsValid: false` + reason), not an error.",
        "description": "The request body is the secured credential JSON (base or derived). The proof is checked against the playground tenant's BBS public key; a failed check is a verdict (isValid: false with a failure reason such as SignatureInvalid), not an error.",
        "operationId": "VerifyBbs2023Proof",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/rp/create": {
      "post": {
        "tags": [
          "RelyingPartyRegistry"
        ],
        "summary": "Registers a relying party: validates the submission, stamps the server-assigned fields, and stores it.\r\nAdmin-gated — an unauthenticated caller is 401, an authenticated non-admin 403.",
        "description": "The request body is a relying-party registration (orgIdentifier {scheme, value}, name, businessNames, contact {email, phone}, intendedUses[], entitlement, publicSectorBody) in the CIR (EU) 2025/848 Annex I shape. The server validates it, assigns the id, timestamps and version, and publishes it. Gated on the passkey-minted admin.system session: an unauthenticated caller is 401, an authenticated non-admin 403; an invalid submission is a 400 problem+json.",
        "operationId": "CreateRelyingParty",
        "responses": {
          "200": {
            "description": "201 with the stored entry, 400 on an invalid submission, or the admin-gate denial."
          }
        }
      }
    },
    "/rp/list": {
      "get": {
        "tags": [
          "RelyingPartyRegistry"
        ],
        "summary": "The public, unauthenticated list of active (non-blocked) register entries — the CIR machine-readable\r\nregister.",
        "description": "Returns the active (non-blocked) register entries and their count. Public and unauthenticated, per the CIR requirement that the register be machine-readable and queryable without authentication.",
        "operationId": "ListRelyingParties",
        "responses": {
          "200": {
            "description": "200 with the active entries and their count."
          }
        }
      }
    },
    "/rp/list/signed": {
      "get": {
        "tags": [
          "RelyingPartyRegistry"
        ],
        "summary": "The public, unauthenticated signed register: the active entries enveloped as a JWS-signed\r\n`RelyingPartyRegisterCredential` under the registry's `did:key`. This is the \"be the registry\"\r\npayoff — a counterparty fetches the register and verifies it cryptographically (the key is self-certified\r\nin the JWS `kid`). Served as `application/jwt`.",
        "description": "Returns the active register entries enveloped as a JWS-signed RelyingPartyRegisterCredential under the registry's did:key (application/jwt). The verification key is self-certified in the JWS kid, so a counterparty fetches and verifies the register without a side channel — the 'be the registry' contract.",
        "operationId": "ListSignedRelyingParties",
        "responses": {
          "200": {
            "description": "200 with the compact JWS over the active register."
          }
        }
      }
    },
    "/rp/{id}": {
      "get": {
        "tags": [
          "RelyingPartyRegistry"
        ],
        "summary": "The public, unauthenticated read of one active register entry by id. A blocked (withdrawn) or unknown\r\nentry is 404, so the public surface never serves a blocked relying party.",
        "description": "Returns the register entry for the id, or 404 when it is unknown or has been blocked (a blocked entry is withdrawn from the public register).",
        "operationId": "GetRelyingParty",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The entry id.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "200 with the entry, or 404 when it is unknown or blocked."
          }
        }
      }
    },
    "/rp/{id}/block": {
      "post": {
        "tags": [
          "RelyingPartyRegistry"
        ],
        "summary": "Blocks a register entry, withdrawing it from the public register. Behind the\r\nstring AuthorizationPolicies.SystemAdmin policy (declared at the mapping) — an\r\nunauthenticated caller is 401, an authenticated non-admin 403.",
        "description": "Withdraws the entry from the public register (it stops appearing in the list and reads 404). Gated on the admin.system session: an unauthenticated caller is 401, an authenticated non-admin 403; an unknown id is 404.",
        "operationId": "BlockRelyingParty",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The entry id to block.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "200 with the blocked entry, or 404 when the id is unknown."
          }
        }
      }
    },
    "/onboarding/inspect": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Inspects the issuer in `?issuer=`: resolves its OIDC discovery, runs the onboarding\r\nchecks, and returns whether it is ready to federate. The outbound fetch threads an empty\r\nExchangeContext — the SSRF policy lives in the fetch delegate itself.",
        "description": "The ?issuer query parameter is the upstream identity provider's issuer URL. The server resolves its OIDC discovery document and JWKS (server-side, behind SSRF egress guards) and runs the onboarding check battery: issuer is https, discovery reachable and parses, the document's issuer matches (OIDC §3.3), the required endpoints are present and https, PKCE S256 is supported, and the JWKS publishes keys. The provider is ready only when every check passes; verdicts are data.",
        "operationId": "InspectIdentityProvider",
        "parameters": [
          {
            "name": "issuer",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/onboarding/{segment}/providers": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Attaches the upstream provider named in the request body to the segment\r\ntenant. The issuer is re-inspected server-side (the client-supplied descriptor is never\r\ntrusted); only a provider that passes the whole onboarding battery is attached and persisted.\r\nA provider that is not ready is rejected with its inspection verdicts so the caller sees why.",
        "description": "Re-inspects the issuer in the request body server-side (behind the SSRF egress guards); when it passes the onboarding battery, the verified issuer is attached to the tenant named by the {segment} route and persisted per-tenant. A provider that is not ready is rejected (422) with the inspection verdicts so the caller sees which checks blocked it.",
        "operationId": "AttachUpstreamIdentityProvider",
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Lists the upstream providers currently attached to the segment tenant.",
        "description": "Returns the upstream identity providers currently attached to the tenant named by the {segment} route, in attach order.",
        "operationId": "ListAttachedIdentityProviders",
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/onboarding/{segment}/contract": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Creates the segment tenant's next billing-contract version from the charge\r\ntypes in the request body — the tenant's first contract, or an appended successor superseding\r\nthe version in force (never an edit). The created version lands on the action-event ledger as\r\nstring ActionEventActions.BillingContractCreated, so the commercial act that starts\r\nmetering classification is itself a metered, trace-correlated event.",
        "description": "Creates a billing contract for the tenant named by the {segment} route: the tenant's first version, or an appended successor version superseding the one in force (contract versions are immutable — terms change by supersession, never by edit). Each charge type names the action-event ledger actions it classifies; within one contract an action maps to at most one charge type, so every metered event classifies deterministically. Prices may be zero — the classification capability is the point.",
        "operationId": "CreateTenantBillingContract",
        "parameters": [
          {
            "name": "segment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/product/passports": {
      "post": {
        "tags": [
          "ProductPassports"
        ],
        "summary": "Mints a product passport from the credential in the request body. Before signing, the\r\ncredentialSubject is validated against the product group's SHACL shape (the `group` query\r\nparameter, defaulting to battery); a non-conforming payload is refused with HTTP 422 and the SHACL\r\nreport in the body (a validation surface, not a secret). On conformance the product gets a\r\ndid:web under the request's host as the credential subject's id, the credential's id becomes the\r\npassport's serving URL, the result is signed with the playground identity, and the documents are\r\nstored (write-through to the durable store when one is configured). Document problems are reported\r\nin-band with HTTP 200.",
        "description": "The request body is the (edited) credential JSON. The product gets a did:web:<host>:product:<id> identity as the credential subject's id, the credential's id becomes the passport's serving URL, and the result is signed with the playground's did:key issuer. The DID document and the signed passport are then served at stable URLs, so the product DID resolves — by third-party resolvers when the origin is public, and through this server's own resolver locally. Document problems are reported in-band.",
        "operationId": "MintProductPassport",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/product/passports/list": {
      "get": {
        "tags": [
          "ProductPassports"
        ],
        "summary": "The product workspace's passport list: one summary row per minted passport, in mint order. Reads\r\nthrough Task&lt;IReadOnlyList&lt;ProductPassportListEntry&gt;&gt; ProductPassportStore.ListAsync(CancellationToken cancellationToken) (durable when a database is configured) and,\r\nfor each row, the credential (for the display name/headline) and the latest registration event\r\n(for the verified/draft status) — both cache-aside reads the store already performs for the\r\nsingle-item routes. Never empty-guarded: a freshly provisioned tenant genuinely returns `[]`.",
        "operationId": "ListProductPassports",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/product/passports/{id}": {
      "get": {
        "tags": [
          "ProductPassports"
        ],
        "summary": "The passport page's document read: the minted passport's credential projected into display\r\nsections, its serving URLs, a LIVE-computed signature verification verdict (re-checked at read\r\ntime, not a cached verdict from mint), its EU DPP registry registration event (when any), and the\r\npublic-tier restricted field paths so the page can render its tier ribbon without a PDP round-trip.\r\nThis is the product workspace's OWN read of its passport — unlike Task&lt;IResult&gt; ProductPassportApi.GetPassport(string id, HttpContext httpContext, VerifableOAuthHost host, ProductPassportStore store, DppRegistrationStore registrations, OriginOptions origin, CancellationToken cancellationToken), it is\r\nnot tier-gated; every field is always present, and string[] ProductPassportDocument.RestrictedFieldPaths\r\nis metadata describing what a PUBLIC caller would not see, not a live mask applied here.",
        "operationId": "GetProductPassportDocument",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/product/{id}/did.json": {
      "get": {
        "tags": [
          "ProductPassports"
        ],
        "summary": "Serves a minted product's DID document (the did:web resolution target).",
        "operationId": "GetProductDidDocument",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/product/{id}/dpp.json": {
      "get": {
        "tags": [
          "ProductPassports"
        ],
        "summary": "    Serves a minted product's passport, tier-gated through the AuthZEN PDP (slice C3, the DPP\r\n    tiered-access gateway). The PEP resolves the caller's tier from the AUTHENTICATED session and routes\r\n    the read through the PDP; the PDP permits the read and, for a masked tier, attaches a `mask`\r\n    obligation (the credentialSubject field paths the public consumer must not see). The PEP honors it:\r\n    * the STAKEHOLDER tier (authenticated, carrying the stakeholder scope) gets the full SIGNED\r\n    credential, unchanged — the verifiable passport;\r\n* the PUBLIC tier gets the UNSIGNED tier projection (strategy B): the masked fields are removed\r\n    (byte-absent) and the signature proof is dropped, because masking a field out of an\r\n    eddsa-jcs-2022-signed credential breaks its proof — so the public body is a declared projection\r\n    (annotated \"masked\": true), never a signed credential with fields silently stripped.",
        "operationId": "GetProductPassport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/product/{id}/credential-offer": {
      "get": {
        "tags": [
          "ProductPassports"
        ],
        "summary": "Mints an OID4VCI Credential Offer for a product's battery passport (slice C2): the resolver linkset's\r\n`credentialOffer` link points here, so dereferencing it yields the §4.1.1 offer the wallet\r\nredeems for the passport credential. The offer is minted against the issuing tenant (the seeded\r\n`test` tenant in this slice — multi-tenant attenuation is deferred), bound to the product whose\r\nalready-minted passport the issuance branch resolves. A non-existent product (or an unconfigured\r\nissuing tenant) yields 404 rather than an empty offer. The body is the upstream Credential Offer\r\nJSON; the wallet GETs this URL to recreate the offer.",
        "description": "Dereferencing the resolver linkset's credentialOffer link mints a pre-authorized-code Credential Offer for the verifable.dpp.battery.1 configuration, bound to this product's already-minted passport. The wallet redeems the offer through the same OID4VCI endpoints it uses for any credential; the issued credential's subject IS the product passport, signed by the tenant's CredentialIssuance key. 404 when the product is unknown.",
        "operationId": "GetProductCredentialOffer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/product/{id}/register": {
      "post": {
        "tags": [
          "ProductPassports"
        ],
        "summary": "Registers a minted product's passport with the EU DPP registry (slice C4) — the ESPR Art. 12/13\r\nregister-before-market obligation. Operator-gated end to end: an unauthenticated caller is 401\r\nBEFORE the PDP (register is never anonymously permittable, unlike the tiered public read), and the\r\nPDP's `register` arm permits only a session the PEP marked as a responsible economic operator.\r\nThe handler submits the composed registration to the configured registry, fetches the\r\nProof-of-Registration JWT, verifies it against the registry's JWKS (verify-then-persist), records\r\nthe registration event, and drives the charge seam. A proof/JWKS fetch or verification failure\r\ndoes NOT fail the request — the registry-side registration already happened, so the state persists\r\nunverified rather than desyncing from the registry. A failed local persist is the one in-band\r\nerror: the response still carries the registry-assigned id so the operator can reconcile.",
        "description": "ESPR Articles 12/13: the responsible economic operator registers the passport with the Commission's EU DPP registry before placing the product on the market. The handler gates on an authenticated operator session (401 unauthenticated, PDP-denied 403), submits the registration metadata, fetches the registry's Proof-of-Registration JWT and verifies it against the registry JWKS, and records the registration event. The Commission has published no normative registry API, so the wire contract is the non-normative CIRPASS-2 shape, kept behind a delegate seam the real registry binding replaces.",
        "operationId": "RegisterProductPassport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/product/{id}/registration": {
      "get": {
        "tags": [
          "ProductPassports"
        ],
        "summary": "Serves a product's EU registry registration state (slice C4): the LATEST registration event, with\r\nthe Proof-of-Registration JWT verbatim so a third party can re-verify it against the registry's\r\nJWKS. Reads permit universally today — anonymous public reads are a legal requirement for the DPP\r\nsurfaces, and the PoR claims are business data, not personal data (watch: the Battery Art. 77(9)\r\naccess-rights implementing act may tier this later).",
        "operationId": "GetProductRegistration",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/untp/verify": {
      "post": {
        "tags": [
          "Untp"
        ],
        "summary": "Verifies the UNTP credential in the request body. Returns the signature/schema verdicts, the resolved\r\nissuer, and the credential types (in-band, HTTP 200). An untrusted/unresolved issuer, a malformed\r\nenvelope, or a failed signature surfaces `signatureValid=false` (fail closed) rather than an\r\nerror; only a missing or non-JSON body is a 400.",
        "description": "The request body is { \"credential\": \"<compact vc+jwt JWS>\" }. The credential's issuer.id selects the trust anchor: an in-process tenant's published CredentialIssuance key, then an out-of-band-enrolled foreign issuer key, then — for a did:web issuer — a LIVE resolution of its published DID document through the SSRF-guarded outbound transport. The compact JWS signature is verified, and only on a verified signature is the credential validated against its pinned UNTP schema (DigitalProductPassport / DigitalConformityCredential / DigitalTraceabilityEvent / DigitalIdentityAnchor / DigitalFacilityRecord). An unresolved/untrusted issuer, a malformed envelope, a did:web that does not resolve, or a failed signature returns signatureValid=false (fail closed).",
        "operationId": "VerifyUntpCredential",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/render/credential/{credentialReference}": {
      "get": {
        "tags": [
          "Untp"
        ],
        "summary": "Renders a single issued UNTP credential to HTML by its opaque reference. Resolves the served\r\n`vc+jwt` JWS, decodes the signed payload, and expands the credential's `renderMethod` HTML\r\ntemplate against that payload. Returns 200 `text/html` with the rendered view when the reference\r\nresolves to a credential that carries an HTML render template; returns 404 when no credential is served\r\nfor the reference, the envelope is malformed, or the credential carries no HTML render template (no\r\ncrash, no leak).",
        "description": "Dereferences the SAME opaque credential reference the ISO/IEC 18975 link-set's dpp/dcc/dte href values carry, decodes the signed vc+jwt payload, and expands the credential's own issuer-signed RenderTemplate2024 (text/html) renderMethod template against that payload — so the rendered HTML and the machine's JSON claims are the SAME signed bytes (human-machine parity). Every value substituted from the credential is HTML-escaped (no injection), the template is never executed, and only public DPP fields are referenced (no tier-restricted field is rendered). 404 when no credential is served for the reference, the envelope is malformed, or the credential carries no HTML render template.",
        "operationId": "RenderUntpCredential",
        "parameters": [
          {
            "name": "credentialReference",
            "in": "path",
            "description": "The opaque credential reference from a resolver link target.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/rdf/inspect": {
      "post": {
        "tags": [
          "RdfInspect"
        ],
        "summary": "Inspects an RDF document (request body) in the given format (turtle, trig,\r\nnquads; defaults to turtle): returns the parsed triples plus coded diagnostics with source spans.\r\nParse problems are part of the diagnosis and are reported in-band with HTTP 200.",
        "description": "Parses the document and returns the triples plus coded diagnostics carrying source spans (byte offsets and line/column), so a client can highlight the connection between the source text and each finding. Parse problems are reported in-band — the diagnosis is the product.",
        "operationId": "InspectRdf",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/shacl/validate": {
      "post": {
        "tags": [
          "ShaclValidate"
        ],
        "summary": "Validates an RDF data document against a SHACL shapes document. The body is JSON\r\n`{ data, shapes, format? }` (format defaults to turtle and applies to both documents),\r\ndeserialized explicitly with the source-generated context. Problems are part of the diagnosis\r\nand are reported in-band with HTTP 200.",
        "description": "Parses both documents, loads the shapes with the built-in SHACL constraint components, and returns the validation report (conforms + per-result focus node, path, severity, source shape, constraint component). Parse problems return the span-carrying diagnostics in-band — the diagnosis is the product.",
        "operationId": "ValidateShacl",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/sparql": {
      "get": {
        "tags": [
          "Sparql"
        ],
        "summary": "SPARQL Protocol: `GET /sparql?query=…`.",
        "description": "Executes a SELECT or ASK query against the in-memory store and returns application/sparql-results+json. Malformed queries return 400 with span-bearing parser diagnostics.",
        "operationId": "SparqlQueryGet",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Sparql"
        ],
        "summary": "SPARQL Protocol: `POST /sparql` with either an `application/sparql-query` body or a\r\nform-encoded `query=` field.",
        "operationId": "SparqlQueryPost",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/sparql/data": {
      "put": {
        "tags": [
          "Sparql"
        ],
        "summary": "Loads an RDF document into the store (replace semantics) — the workbench's \"load into the\r\ndatabase\" operation. Parse problems are reported in-band with the span-bearing diagnostics and\r\nleave the store unchanged.",
        "description": "Parses the body (turtle/trig/nquads via ?format=) and replaces the store contents. Parse problems return the span-bearing diagnostics in-band and leave the store unchanged.",
        "operationId": "SparqlLoadData",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Verifable.Server"
    },
    {
      "name": "AtProtocol"
    },
    {
      "name": "Program"
    },
    {
      "name": "FlowInitiationApi"
    },
    {
      "name": "DppResolver"
    },
    {
      "name": "SiteConfig"
    },
    {
      "name": "HealthCheck"
    },
    {
      "name": "Documents"
    },
    {
      "name": "FederationAssets"
    },
    {
      "name": "Admin"
    },
    {
      "name": "Operations"
    },
    {
      "name": "Iga"
    },
    {
      "name": "WebAuthn"
    },
    {
      "name": "CborInspect"
    },
    {
      "name": "Credentials"
    },
    {
      "name": "DataIntegrityBbs"
    },
    {
      "name": "RelyingPartyRegistry"
    },
    {
      "name": "Onboarding"
    },
    {
      "name": "ProductPassports"
    },
    {
      "name": "Untp"
    },
    {
      "name": "RdfInspect"
    },
    {
      "name": "ShaclValidate"
    },
    {
      "name": "Sparql"
    }
  ]
}