openapi: 3.0.3 # TrustNet — RP management plane (/api/v1/rp/*). # # Audience: the `me.trustnet.app` portal area (and the `_TEST` suite). This is a # FIRST-PARTY, owner-facing plane — a normal Kratos session cookie, same-origin, # no CORS — and it is deliberately separate from the RP machine plane # (`rp.yaml`, `/api/rp/v1/*`, bearer/introspection only): the two must not share # an authentication path (ADR-0014 §6). # # Enforcement: internal/apispec validates every handler-test exchange against # this file, and cmd/persona-api's coverage guard fails if a mounted # `/api/v1/rp/*` route is neither documented here nor on the exclusion list. # # What this document does NOT do: it describes shapes, not security rules. # Owner-only authorization, `no-store`, "the secret is shown once" and the # locked protocol invariants are controls held by the handler tests, not by a # description. info: title: TrustNet RP management API version: 1.0.0 description: | The operator-side RP lifecycle (ADR-0014): register, manage and delete a relying party as a first-class entity. Every operation resolves the account from the Kratos cookie server-side (G18); `account_id` never appears on the wire. - **Owner-only.** The registry store enforces ownership on every read and write; a non-owner is refused. - **`Cache-Control: no-store` + `Vary: Cookie` on every response.** - **Errors never echo input.** `error` is a fixed code from a closed set. - **The client secret is shown once.** It is returned only by registration and reissue, and never retrievable again. - **Locked invariants.** `subject_type=pairwise`, `access_token_strategy= opaque`, `response_types=["code"]` and a required `backchannel_logout_uri` are set by TrustNet and are not operator-editable (G19, ADR-0014 §5). - **Management-plane guard (M8.12, ADR-0014 §6).** Every request requires an **AAL2** session authenticated within the step-up freshness window (`RP_STEP_UP_MAX_AGE`, default 15 m) — else `403 step_up_required`. Every state-changing request must additionally pass Fetch Metadata (`Sec-Fetch-Site: same-origin`), an exact `Origin` match against the portal origin, and a custom `X-Trustnet-CSRF: 1` header — else `403 csrf_rejected`. tags: - name: tenants description: The account's tenants — the grouping entity RPs attach to. - name: rps description: Register, read, update and lifecycle-manage relying parties. - name: domains description: | Account-scoped redirect-host ownership proofs (ADR-0014 §4): issue a one-time token, publish it at the registrable domain, then verify. - name: logos description: | Uploaded RP logos (M8.13, ADR-0014 §6 hardening; S3 P8): the bytes live in the environment's **public Space** (ADR-0016 decision 13) and are served from the CDN, so no customer-controlled image is hot-linked onto the session origin. Upload is guarded and returns the public `logo_url`. - name: terms description: The operator terms an RP owner accepts at registration. paths: /api/v1/rp/tenants: get: tags: [tenants] operationId: getRPTenants summary: List the account's tenants responses: '200': description: The account's active tenants, oldest first. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/TenantList' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } post: tags: [tenants] operationId: postRPTenant summary: Create a tenant requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CreateTenantRequest' } responses: '201': description: The created tenant. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/Tenant' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/rps: get: tags: [rps] operationId: getRPRPs summary: List the account's RPs responses: '200': description: Every non-purged RP the account owns, oldest first. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/RPList' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } post: tags: [rps] operationId: postRPRP summary: Register an RP (instant self-service) description: | Validates, verifies redirect-host ownership, creates the Hydra client, inserts the registry row as `active`, and returns the `client_id` plus a one-time `client_secret`. If the registry insert fails, the Hydra client is deleted so the two halves never diverge (ADR-0014 §1). requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/RegisterRequest' } responses: '201': description: The RP was created; `client_secret` is shown once. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/RegisterResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/rps/{rp_id}: parameters: - $ref: '#/components/parameters/RPPath' get: tags: [rps] operationId: getRPRP summary: Read one RP description: | The registry lifecycle joined to the full Hydra client config (M8.14): consent-surface metadata, redirect/logout URIs, scopes and the locked protocol invariants. responses: '200': description: The full RP view. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/RPDetail' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } patch: tags: [rps] operationId: patchRPRP summary: Update RP settings description: | Consent-surface metadata and logout config (which live on the Hydra client). A redirect-URI change is accepted only when the registrable domain set is unchanged: a redirect-host change is refused as `identity_break`. (The sector is the RP's tenant, ADR-0001, so this is a redirect-safety gate, not a PPID re-derivation.) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SettingsRequest' } responses: '200': description: The updated RP, as the full detail view. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/RPDetail' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/rps/{rp_id}/secret/reissue: parameters: - $ref: '#/components/parameters/RPPath' post: tags: [rps] operationId: postRPRPSecretReissue summary: Reissue the client secret description: | Mints a new secret, replaces it on the Hydra client (the old one is invalidated), and returns it once. There is no automatic rotation. responses: '200': description: The new secret, shown once. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/SecretResponse' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/rps/{rp_id}/suspend: parameters: - $ref: '#/components/parameters/RPPath' post: tags: [rps] operationId: postRPRPSuspend summary: Suspend an RP responses: '200': description: The suspended RP. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/RP' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/rps/{rp_id}/reinstate: parameters: - $ref: '#/components/parameters/RPPath' post: tags: [rps] operationId: postRPRPReinstate summary: Reinstate a suspended RP responses: '200': description: The reinstated RP. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/RP' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/rps/{rp_id}/delete: parameters: - $ref: '#/components/parameters/RPPath' post: tags: [rps] operationId: postRPRPDelete summary: Delete an RP (global teardown) description: | Ordered teardown across every account with a live consent at the RP (ADR-0014 §8). The registry row becomes a `deleted` tombstone; its audit tree is retained. responses: '200': description: The deleted (tombstoned) RP. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/RP' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/domains: get: tags: [domains] operationId: getRPDomains summary: List the account's domain proofs responses: '200': description: The account's ownership proofs, newest first. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/DomainList' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } post: tags: [domains] operationId: postRPDomain summary: Issue an ownership-proof token description: | Canonicalizes the registrable domain, mints a one-time token and stores only its hash. The clear token and where to publish it are returned once. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CreateDomainRequest' } responses: '201': description: The stored (pending) proof plus the token to publish. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/DomainChallenge' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/domains/{domain_id}/verify: parameters: - $ref: '#/components/parameters/DomainPath' post: tags: [domains] operationId: postRPDomainVerify summary: Verify an ownership proof description: | Runs the live check (DNS TXT preferred, `.well-known` fallback). A missing proof is a 200 with `status: pending`/`lapsed`, never an error. responses: '200': description: The proof after the check. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/Domain' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/logo: post: tags: [logos] operationId: postRPLogo summary: Upload an RP logo description: | Stores a small image for use as an RP's consent-surface logo (M8.13). The type is decided by sniffing the bytes, not by the client's declared part type: `image/png`, `image/jpeg` and `image/webp` only, SVG refused. The bytes are written to the environment's **public Space** (ADR-0016 decision 13); the response carries the `logo_id` to reference from registration/PATCH and the public `logo_url` (the CDN URL Hydra's `logo_uri` becomes — never an operator URL). requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: '201': description: The stored logo; reference it by `logo_id`. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/Logo' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/terms: get: tags: [terms] operationId: getRPOperatorTerms summary: Read the current operator terms description: | The operator ToS an RP owner accepts at registration (M8.14, ADR-0014 §9). The register flow renders this text and posts its `version` back as `operator_terms_version`. Guarded like every management-plane read. responses: '200': description: The current operator terms revision. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/OperatorTerms' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/terms/status: get: tags: [terms] operationId: getRPTermsStatus summary: Whether the owner must re-accept the Operator Terms description: | Post-login re-acceptance check for the Operator Terms (F9). Compares the current operator version with the newest version this account accepted (registration counts). Guarded like every portal read. responses: '200': description: The operator re-acceptance state. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/OperatorTermsStatus' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/terms/reacceptance: post: tags: [terms] operationId: postRPTermsReacceptance summary: Re-accept the current Operator Terms after a version bump description: | Records the owner's acceptance of the current Operator Terms. The posted version must be current; a stale version is `409 conflict`. Source is `re-acceptance` server-side; idempotent per version. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/OperatorTermsAcceptanceRequest' } responses: '204': description: Recorded (or already recorded). headers: Cache-Control: { $ref: '#/components/headers/NoStore' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalError' } '502': { $ref: '#/components/responses/UpstreamError' } /api/v1/rp/rps/{rp_id}/export: parameters: - $ref: '#/components/parameters/RPPath' - name: prev_sth in: query required: false description: | An RFC3339 timestamp of a previous head the caller holds; the bundle then carries a consistency proof from it to the export head. schema: { type: string } get: tags: [rps] operationId: getRPRPExport summary: Download the RP's verifiable bundle description: | The owner-facing per-RP audit export (M8.14): the same verifiable bundle the machine plane serves (`POST /api/export/v1/rp`), reached with the owner's session cookie. Owner-only; a non-owner never reaches the builder. Returns a gzip tar archive. responses: '200': description: The verifiable bundle (tar.gz). headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/gzip: schema: { type: string, format: binary } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/NoSession' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalError' } '503': { $ref: '#/components/responses/ExportNotReady' } components: headers: NoStore: description: Always `no-store`. See the transport rules in `info`. schema: type: string parameters: RPPath: name: rp_id in: path required: true description: The opaque immutable registry key (first-party only). schema: type: string DomainPath: name: domain_id in: path required: true description: The opaque ownership-proof key (first-party only). schema: type: string schemas: ErrorResponse: type: object additionalProperties: false required: [status, error] properties: status: type: string enum: [error] error: type: string enum: - invalid_request - no_session - forbidden - not_found - quota_exceeded - conflict - unverified_host - local_development_non_loopback - locked_invariant - not_allowed - verification_failed - identity_break - step_up_required - csrf_rejected - logo_too_large - logo_type_unsupported - terms_not_accepted - export_not_ready - upstream_error - internal_error Tenant: type: object additionalProperties: false required: [tenant_id, label, created_at] properties: tenant_id: { type: string } label: { type: string } created_at: { type: string } TenantList: type: object additionalProperties: false required: [tenants] properties: tenants: type: array items: { $ref: '#/components/schemas/Tenant' } RP: type: object additionalProperties: false required: [rp_id, tenant_id, client_id, local_development, status, version, created_at, updated_at] properties: rp_id: { type: string } tenant_id: { type: string } client_id: { type: string } local_development: { type: boolean } status: type: string enum: [active, suspended, deleted, purged] version: { type: integer } created_at: { type: string } updated_at: { type: string } deleted_at: { type: string } retention_until: { type: string } RPListItem: description: | One RP in the list (M8.14): the registry lifecycle fields plus the Hydra-sourced presentation the portal shows. `client_name` and `redirect_hosts` are joined from Hydra at read time — the registry does not duplicate them (ADR-0014 §1). type: object additionalProperties: false required: [rp_id, tenant_id, client_id, local_development, status, version, created_at, updated_at, client_name, redirect_hosts] properties: rp_id: { type: string } tenant_id: { type: string } client_id: { type: string } local_development: { type: boolean } status: type: string enum: [active, suspended, deleted, purged] version: { type: integer } created_at: { type: string } updated_at: { type: string } deleted_at: { type: string } retention_until: { type: string } client_name: { type: string } redirect_hosts: type: array items: { type: string } RPDetail: description: | One RP's full view (M8.14): the list fields plus the consent-surface and protocol config Hydra holds for the client. Every config field is read back from Hydra, never from the registry (ADR-0014 §1). type: object additionalProperties: false required: [rp_id, tenant_id, client_id, local_development, status, version, created_at, updated_at, client_name, redirect_hosts, redirect_uris, backchannel_logout_uri, grant_types, scopes, subject_type, access_token_strategy, response_types] properties: rp_id: { type: string } tenant_id: { type: string } client_id: { type: string } local_development: { type: boolean } status: type: string enum: [active, suspended, deleted, purged] version: { type: integer } created_at: { type: string } updated_at: { type: string } deleted_at: { type: string } retention_until: { type: string } client_name: { type: string } redirect_hosts: type: array items: { type: string } logo_uri: { type: string } client_uri: { type: string } policy_uri: { type: string } tos_uri: { type: string } contacts: type: array items: { type: string } redirect_uris: type: array items: { type: string } post_logout_redirect_uris: type: array items: { type: string } backchannel_logout_uri: { type: string } webhook_url: type: string description: | Optional RP purge-webhook endpoint (ADR-0015). HTTPS, and its host must be one of this RP's redirect hosts (loopback http allowed for local_development RPs). Empty/absent = no webhook. A registry-owned setting, not a Hydra client field. grant_types: type: array items: { type: string } scopes: type: array items: { type: string } subject_type: { type: string } access_token_strategy: { type: string } response_types: type: array items: { type: string } claims: type: array items: { $ref: '#/components/schemas/ClaimDecl' } RPList: type: object additionalProperties: false required: [rps] properties: rps: type: array items: { $ref: '#/components/schemas/RPListItem' } OperatorTerms: type: object additionalProperties: false required: [version, published_at, text_hash, body] properties: version: { type: integer } published_at: { type: string } text_hash: { type: string } body: { type: string } OperatorTermsStatus: type: object additionalProperties: false required: [current_version, accepted_version, needs_acceptance] properties: current_version: { type: integer, minimum: 1 } accepted_version: { type: integer, minimum: 0 } needs_acceptance: { type: boolean } OperatorTermsAcceptanceRequest: type: object additionalProperties: false required: [version] properties: version: { type: integer, minimum: 1 } NewTenant: type: object required: [label] properties: label: { type: string, minLength: 1 } ClaimDecl: type: object required: [claim, reason] properties: claim: type: string description: A requestable OIDC claim (storeable catalog or a derived boolean). reason: type: string minLength: 1 description: The RP's GDPR justification, shown to the user at consent. RegisterRequest: type: object required: [client_name, redirect_uris, backchannel_logout_uri, scopes, grant_types, operator_terms_version] properties: tenant_id: { type: string } new_tenant: { $ref: '#/components/schemas/NewTenant' } client_name: { type: string, minLength: 1 } operator_terms_version: type: integer minimum: 1 description: | The current operator terms version the owner accepted (GET /rp/terms). Must equal the current version or registration is refused with `terms_not_accepted`. logo_id: type: string description: An uploaded logo id (POST /rp/logo). Never a URL. client_uri: { type: string, description: Must be an absolute https URL (loopback http allowed for local_development RPs). } policy_uri: { type: string, description: Must be an absolute https URL (loopback http allowed for local_development RPs). } tos_uri: { type: string, description: Must be an absolute https URL (loopback http allowed for local_development RPs). } contacts: type: array items: { type: string } redirect_uris: type: array items: { type: string } post_logout_redirect_uris: type: array items: { type: string } backchannel_logout_uri: { type: string, minLength: 1 } webhook_url: type: string description: | Optional RP purge-webhook endpoint (ADR-0015). HTTPS, and its host must be one of this RP's redirect hosts (loopback http allowed for local_development RPs). Absent = no webhook. scopes: type: array items: { type: string } grant_types: type: array items: { type: string } claims: type: array items: { $ref: '#/components/schemas/ClaimDecl' } local_development: { type: boolean } RegisterResponse: type: object additionalProperties: false required: [rp_id, tenant_id, client_id, client_secret, local_development, status, version, created_at, updated_at] properties: rp_id: { type: string } tenant_id: { type: string } client_id: { type: string } client_secret: type: string description: Shown once. Never retrievable again; only reissued. local_development: { type: boolean } status: { type: string } version: { type: integer } created_at: { type: string } updated_at: { type: string } SettingsRequest: type: object required: [version] properties: version: { type: integer, minimum: 1 } client_name: { type: string, minLength: 1 } logo_id: type: string description: An uploaded logo id (POST /rp/logo), or empty to clear. Never a URL. client_uri: { type: string, description: Must be an absolute https URL (loopback http allowed for local_development RPs). } policy_uri: { type: string, description: Must be an absolute https URL (loopback http allowed for local_development RPs). } tos_uri: { type: string, description: Must be an absolute https URL (loopback http allowed for local_development RPs). } contacts: type: array items: { type: string } redirect_uris: type: array items: { type: string } post_logout_redirect_uris: type: array items: { type: string } backchannel_logout_uri: { type: string, minLength: 1 } webhook_url: type: string description: | The RP purge-webhook endpoint (ADR-0015). HTTPS, and its host must be one of this RP's redirect hosts. Empty string clears it; omitted leaves it unchanged. claims: type: array items: { $ref: '#/components/schemas/ClaimDecl' } scopes: type: array items: { type: string } description: | The RP's granted OAuth2 scopes (allow-list, ADR-0014 §5). `openid` is always required. Scopes are editable after registration so an RP can add scope-gated capabilities as they become available; omitted leaves them unchanged. SecretResponse: type: object additionalProperties: false required: [client_id, client_secret] properties: client_id: { type: string } client_secret: type: string description: Shown once. The previous secret is invalidated. Logo: type: object additionalProperties: false required: [logo_id, logo_url] properties: logo_id: type: string description: Reference this from `logo_id` on registration/PATCH. logo_url: type: string description: | The public CDN URL for the logo (what Hydra `logo_uri` becomes). Immutable and cacheable; served from the environment's public Space. CreateTenantRequest: type: object required: [label] properties: label: { type: string, minLength: 1 } CreateDomainRequest: type: object required: [registrable_domain, method] properties: registrable_domain: { type: string, minLength: 1 } method: type: string enum: [dns_txt, well_known] Domain: type: object additionalProperties: false required: [domain_id, registrable_domain, method, status, created_at] properties: domain_id: { type: string } registrable_domain: { type: string } method: type: string enum: [dns_txt, well_known] status: type: string enum: [pending, verified, lapsed] verified_at: { type: string } last_checked_at: { type: string } created_at: { type: string } DomainList: type: object additionalProperties: false required: [domains] properties: domains: type: array items: { $ref: '#/components/schemas/Domain' } DomainChallenge: type: object additionalProperties: false required: [domain, token, dns_name, well_known_url] properties: domain: { $ref: '#/components/schemas/Domain' } token: type: string description: Shown once. Publish it at `dns_name` or `well_known_url`. dns_name: { type: string } well_known_url: { type: string } responses: BadRequest: description: | `invalid_request` | `not_allowed` | `unverified_host` | `local_development_non_loopback` | `locked_invariant` | `logo_too_large` | `logo_type_unsupported` | `terms_not_accepted`. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } NoSession: description: '`no_session` — the Kratos session is gone.' headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } Forbidden: description: | `forbidden` — the account does not own the referenced object; `step_up_required` — the session is not AAL2 or is older than the step-up freshness window (the UI drives Kratos step-up); `csrf_rejected` — a state-changing request failed the Fetch Metadata / Origin / custom-header checks (M8.12). headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } NotFound: description: '`not_found` — no such object for this account.' headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } Conflict: description: | `quota_exceeded` | `conflict` | `identity_break` | `verification_failed`. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } UpstreamError: description: '`upstream_error` — Hydra admin failed.' headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } InternalError: description: '`internal_error` — ours.' headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } ExportNotReady: description: | `export_not_ready` — no signed head covers the tree yet (the STH job has not caught up); retry shortly. headers: Cache-Control: { $ref: '#/components/headers/NoStore' } content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' }