openapi: 3.1.0
info:
  title: Sendense Central Appliance (SCA) API
  version: 0.1.0
  summary: CSP/MSP control plane + tenant front door for the Sendense platform.
  description: |-
    The **Sendense Central Appliance (SCA)** is the control plane that federates customer
    **Sendense Hub Appliances (SHAs)** over outbound SSH reverse tunnels. Backup data never
    transits the SCA. Two surfaces:

    - **MSP/CSP control plane** (staff realm): manage tenants, appliance enrollment, license
      pools + operational quota, resell pricing/billing exports, fleet health, and
      customer-granted delegated operations (enforced ON each SHA).
    - **Tenant front door** (tenant realm, CSP edition): an end tenant authenticates at the
      SCA and is proxied into THEIR OWN single-tenant SHA (SILO 1:1). Reads and writes use a
      short-lived, role-scoped token minted by the SHA via token-exchange — the SHA's own RBAC
      is the authorization gate (design D1).

    ### Realms
    `staffAuth` and `tenantAuth` are SEPARATE JWT realms (distinct derived signing keys): a
    tenant token can never validate on a staff route or vice versa.

    ### Editions
    Routes marked *CSP edition only* return **403** when `SCA_EDITION` is `msp`.

    ### Base path & environments
    `/health` is at the server root; every other path is under `/api/v1` (shown in full below).
    The base URL is your SCA deployment's public endpoint (`SCA_PUBLIC_URL`, normally the edge
    proxy on `:443`). Staging/evaluation appliances typically use a self-signed certificate —
    pass `-k`/`--insecure` to curl or pin the appliance certificate.

    ### Quickstart — zero to first call
    ```bash
    SCA=https://<your-sca-host>          # e.g. the staging edge
    # 1. Staff login (bootstrap admin is created at install; SCA_BOOTSTRAP_ADMIN_PASSWORD)
    TOKEN=$(curl -sk $SCA/api/v1/auth/login \
      -d '{"email":"admin@example.com","password":"<password>"}' | jq -r .access_token)
    # 2. First authenticated call — list your tenants
    curl -sk -H "Authorization: Bearer $TOKEN" $SCA/api/v1/tenants
    # 3. Tenant-realm call (CSP edition): tenant user logs in, reads through to THEIR SHA
    TEN=$(curl -sk $SCA/api/v1/tenant/auth/login \
      -d '{"tenant_code":"acme","email":"user@acme.com","password":"<password>"}' | jq -r .access_token)
    curl -sk -H "Authorization: Bearer $TEN" $SCA/api/v1/tenant/proxy/vm-contexts
    ```

    ### Tokens & lifetimes
    `Authorization: Bearer <token>` on every authenticated route. Staff access tokens live
    **15 minutes** (configurable, `SCA_ACCESS_TTL`) with a **30-day** refresh token
    (`SCA_REFRESH_TTL`) redeemed at `POST /api/v1/auth/refresh`. Tenant sessions use the same
    access lifetime but have **no refresh flow** — on `401`, re-authenticate at
    `POST /api/v1/tenant/auth/login`. Tenant suspension is re-checked on **every** request, so
    a suspended tenant's calls fail `403` immediately, mid-session.

    ### Errors, rate limits & retries
    Every error is `{"error": "<human-readable message>"}` with a conventional status code
    (per-route codes below). The tenant login and federated-callback routes are rate-limited per
    client IP (fixed window: **10 requests/minute**) and answer `429` with `Retry-After: 60`;
    staff logins are not rate-limited but every attempt is security-audited. Reads are
    safe to retry with backoff; delegated-operation writes are idempotent via `idempotency_key`
    (unique per appliance — replaying the same key returns the original operation, so retry on
    timeout is safe). Other writes are not idempotent unless a route says so — on a timeout,
    read back before retrying.

    ### Async operations
    Delegated writes (`POST /api/v1/tenants/{id}/shas/{sha}/operations`) return an operation
    record immediately and execute on the tenant appliance. Poll
    `GET /api/v1/operations/{op_id}` (reconciled from the appliance on read); lifecycle:
    `pending → dispatched → running → succeeded | failed` (`unknown` = dispatch timed out and
    will reconcile on the next appliance heartbeat). Poll every few seconds; there are no
    webhooks today. Zero-touch onboarding is likewise async: start a run, then poll
    `GET /api/v1/tenants/{id}/onboarding-runs/{run_id}` to a terminal `complete`/`failed`.

    ### Pagination & list conventions
    List endpoints return the complete collection unless the route documents a limit/filter
    parameter (e.g. usage series and CSV exports are capped per-route). Filters are plain query
    parameters (`?tenant_id=…`); there is no cursor pagination on this API surface today.

    ### Versioning & changes
    The API is path-versioned (`/api/v1`); changes within v1 are additive (new routes/fields,
    never repurposed ones) and breaking changes would ship as a new path version. This document
    is generated from the route table in `sendense-sca/api/server.go` and drift-checked in CI on
    every change, so the spec version in `info.version` tracks the shipped surface.
servers:
- url: https://{sca-host}
  description: Your SCA deployment's public endpoint (SCA_PUBLIC_URL; edge proxy on 443 demuxes TLS API
    + SSH tunnels).
  variables:
    sca-host:
      default: sca.example.com
- url: https://45.130.45.69
  description: Sendense staging/evaluation SCA (self-signed cert — curl -k).
tags:
- name: Health
  description: Liveness probe (server root, unauthenticated).
- name: Auth
  description: MSP/CSP staff login + sessions (staff realm).
- name: Tenant Front Door
  description: End-tenant login + the allowlisted proxy into the tenant's own SHA (tenant realm, CSP edition).
- name: Enrollment
  description: SHA pairing → Ed25519 challenge/verify → approval → tunnel provisioning.
- name: Tenants
  description: Tenant lifecycle + per-tenant rollups.
- name: MSP Users
  description: Staff accounts + per-user tenant scoping.
- name: Storage
  description: Partner-hosted storage offerings (registration/metadata only — backup data never transits
    the SCA).
- name: Fleet
  description: Fleet health, capacity, per-SHA detail + usage series, alerts.
- name: Onboarding
- name: Billing
  description: Rate cards, per-tenant overrides, draft + finalized monthly statements.
- name: Licensing
  description: License pools + per-tenant operational quota.
- name: Operations
  description: Delegated operations dispatched to tenant SHAs (grant-enforced ON the SHA).
- name: Read-through
  description: Staff read-through mirror of a tenant SHA's API over the tunnel.
- name: SHA-side (called by the SCA)
  description: Companion endpoints each tenant SHA serves for the SCA; not part of the SCA's own surface.
x-tagGroups:
- name: Platform
  tags:
  - Health
  - Auth
- name: Tenant front door (CSP)
  tags:
  - Tenant Front Door
- name: Fleet & enrollment
  tags:
  - Enrollment
  - Fleet
  - Onboarding
- name: Tenant management & operations
  tags:
  - Tenants
  - MSP Users
  - Operations
  - Read-through
- name: Commercials
  tags:
  - Licensing
  - Billing
  - Storage
- name: SHA-side companion
  tags:
  - SHA-side (called by the SCA)
paths:
  /health:
    get:
      tags:
      - Health
      summary: Liveness — DB-independent, stays 200 during DB outages.
      operationId: getHealth
      security: []
      responses:
        '200':
          description: Success
  /api/v1/auth/login:
    post:
      tags:
      - Auth
      summary: MSP/CSP staff login → staff-realm access+refresh tokens.
      operationId: login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: MSP staff credentials.
              properties:
                email:
                  type: string
                  format: email
                  example: admin@sendense.local
                password:
                  type: string
                  format: password
                  example: S3cr3t!pass
              required:
              - email
              - password
            example:
              email: admin@sendense.local
              password: S3cr3t!pass
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
              example:
                access_token: eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJ1c3ItOWYzYSJ9.sig
                refresh_token: rt_c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
                expires_at: '2026-07-02T15:40:12Z'
                user:
                  id: usr-9f3a
                  email: admin@sendense.local
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid credentials/token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/auth/refresh:
    post:
      tags:
      - Auth
      summary: Exchange a refresh token for a new staff access token.
      operationId: refreshToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: A valid, unexpired, unrevoked refresh token.
              properties:
                refresh_token:
                  type: string
                  example: rt_c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
              required:
              - refresh_token
            example:
              refresh_token: rt_c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefreshResponse'
              example:
                access_token: eyJhbGciOiJIUzI1NiJ9.new.sig
                refresh_token: rt_a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4
                expires_at: '2026-07-02T15:55:12Z'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid credentials/token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/auth/me:
    get:
      tags:
      - Auth
      summary: The authenticated staff user + roles.
      operationId: getCurrentUser
      security:
      - staffAuth: &id001 []
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
              example:
                user_id: usr-9f3a
                email: admin@sendense.local
                roles:
                - admin
                permissions:
                - tenants.read
                - tenants.write
                - users.manage
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/auth/logout:
    post:
      tags:
      - Auth
      summary: Invalidate the current staff session.
      operationId: logout
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: The refresh token to revoke. Body is optional/best-effort — an absent or empty
                refresh_token still returns 200; only a present token is revoked.
              properties:
                refresh_token:
                  type: string
                  example: rt_c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
            example:
              refresh_token: rt_c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
              example:
                status: logged_out
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenant/auth/login:
    post:
      tags:
      - Tenant Front Door
      summary: End-tenant login (tenant_code+email+password) → tenant-realm token bound to the tenant.
        Per-IP rate-limited. CSP edition only.
      operationId: tenantLogin
      x-required-edition: csp
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Tenant front-door login by tenant code + email + password (design 6.2). Public
                route (CSP-edition-gated + rate-limited).
              properties:
                tenant_code:
                  type: string
                  description: The tenant's code (resolves the tenant; existence is never leaked).
                  example: ACME
                email:
                  type: string
                  format: email
                  example: ops@acme.example.com
                password:
                  type: string
                  format: password
                  example: s3cr3t-passphrase
              required:
              - tenant_code
              - email
              - password
            example:
              tenant_code: ACME
              email: ops@acme.example.com
              password: s3cr3t-passphrase
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantSession'
              example:
                access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbnRfaWQiOiJhY21lLWNvcnAifQ.sig
                tenant_id: acme-corp
                email: ops@acme.example.com
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid credentials/token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limited — retry after the `Retry-After` header (seconds)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenant/auth/federated/{code}/callback:
    post:
      tags:
      - Tenant Front Door
      summary: 'OIDC federated tenant login: the CSP IdP posts an id_token, the SCA validates it (JWKS)
        and issues a tenant session. Rate-limited. CSP edition only.'
      operationId: tenantFederatedCallback
      x-required-edition: csp
      security: []
      parameters:
      - name: code
        in: path
        required: true
        schema:
          type: string
        description: Federation config code.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Public OIDC callback (design 6.2 W1-e). The CSP portal/IdP posts an id_token
                for the tenant identified by {code} in the path. The SCA validates it against the tenant's
                TenantIdPConfig (signature via jwks_uri, iss/aud/exp) and issues a tenant-realm session
                from the mapped email claim.
              properties:
                id_token:
                  type: string
                  description: OIDC id_token (JWT) minted by the tenant's configured IdP. Required.
                  example: eyJraWQiOiJrMSIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiJodHRwczovL2xvZ2luLmFjbWUuZXhhbXBsZS5jb20vIn0.sig
              required:
              - id_token
            example:
              id_token: eyJraWQiOiJrMSIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiJodHRwczovL2xvZ2luLmFjbWUuZXhhbXBsZS5jb20vIn0.sig
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantSession'
              example:
                access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbnRfaWQiOiJhY21lLWNvcnAifQ.sig
                tenant_id: acme-corp
                email: alice@acme.example.com
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid credentials/token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limited — retry after the `Retry-After` header (seconds)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenant/auth/me:
    get:
      tags:
      - Tenant Front Door
      summary: The authenticated end tenant (tenant_id, email, user_id). CSP edition only.
      operationId: getTenantMe
      x-required-edition: csp
      security:
      - tenantAuth: &id002 []
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantMe'
              example:
                tenant_id: acme-corp
                email: ops@acme.example.com
                user_id: 3f2c1b7a-90de-4a11-8c33-0a1b2c3d4e5f
                sha_role: backup-operator
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenant/proxy/{shaPath}:
    get:
      tags:
      - Tenant Front Door
      summary: Read-through into the caller's OWN SHA (SILO 1:1; instance resolved from the session, never
        a param). Exact SHA path allowlist (default-deny). The GUI rewrites SHA /api/v1 GETs here. Served
        via a token-exchanged, role-scoped SHA token — the SHA's RBAC is the gate. CSP edition only.
      operationId: tenantProxyRead
      x-required-edition: csp
      security:
      - tenantAuth: *id002
      parameters:
      - name: shaPath
        in: path
        required: true
        schema:
          type: string
        description: The SHA API sub-path (e.g. vm-contexts, backups). Allowlisted.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProxyResponse'
              example:
                jobs:
                - id: backup-acme-01
                  status: running
                  type: backup
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Tenant Front Door
      summary: 'Tenant self-service WRITE into the caller''s own SHA (D1 + D13/CSP-W8). Method+template
        allowlist (default-deny): backups (Backup Now), restore/mount, restore/full-vm, protection-patterns
        create + per-pattern groups/execute/force-full/secondary-copy, machine-groups create + membership,
        schedules create/enable/trigger/preview. repositories, encryption and restore-to-server writes
        are EXCLUDED. Feature-gated per rule (403 feature_not_licensed); idempotent via the Idempotency-Key
        header (a key binds ONE request — the same key with a different method/path/body is refused, 409);
        on-behalf-of attributed. Authorized ONLY by the exchanged SHA role''s RBAC. CSP edition only.'
      operationId: tenantProxyWrite
      x-required-edition: csp
      security:
      - tenantAuth: *id002
      parameters:
      - name: shaPath
        in: path
        required: true
        schema:
          type: string
        description: The SHA API sub-path (e.g. vm-contexts, backups). Allowlisted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: 'OPAQUE control payload forwarded to the tenant''s SHA (design 5.1 D1: Backup
                Now / restore). The body is read raw (max 1 MiB) and relayed to the exact-path-allowlisted
                SHA write endpoint identified by {shaPath}; its shape is defined by that SHA endpoint,
                not the SCA. Send an ''Idempotency-Key'' header to make the write idempotent. Not msp_access_grants
                (staff-only) - the SHA''s RBAC is the authz gate.'
              additionalProperties: true
              example:
                disk_ids:
                - disk-0
                consistency: crash
            example:
              disk_ids:
              - disk-0
              consistency: crash
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProxyResponse'
              example:
                operation_id: op-7f3a
                status: accepted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
      - Tenant Front Door
      summary: 'Tenant self-service config UPDATE on the caller''s own SHA (D13/CSP-W8): protection-patterns/{id}
        (incl. repository choice in the body; capability fields secondary_copy/app_aware in the body are
        feature-gated), machine-groups/{id} + /pattern, schedules/{id}, protection-patterns/{id}/secondary-copy.
        Same allowlist/feature/idempotency semantics as POST. CSP edition only.'
      operationId: tenantProxyPut
      x-required-edition: csp
      security:
      - tenantAuth: *id002
      parameters:
      - name: shaPath
        in: path
        required: true
        schema:
          type: string
        description: The SHA API sub-path (e.g. vm-contexts, backups). Allowlisted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: OPAQUE config payload forwarded to the tenant's SHA; its shape is owned by
                the targeted SHA endpoint (e.g. a protection-pattern update carrying repository_id).
              additionalProperties: true
              example:
                name: web tier
            example:
              disk_ids:
              - disk-0
              consistency: crash
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProxyResponse'
              example:
                operation_id: op-7f3a
                status: accepted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Tenant Front Door
      summary: Partial update variant of PUT for protection-patterns/{id} (the reused SHA GUI's ILR toggle).
        Same allowlist/feature/idempotency semantics. CSP edition only.
      operationId: tenantProxyPatch
      x-required-edition: csp
      security:
      - tenantAuth: *id002
      parameters:
      - name: shaPath
        in: path
        required: true
        schema:
          type: string
        description: The SHA API sub-path (e.g. vm-contexts, backups). Allowlisted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: OPAQUE partial-update payload forwarded to the tenant's SHA (protection-pattern
                PATCH).
              additionalProperties: true
              example:
                name: web tier
            example:
              disk_ids:
              - disk-0
              consistency: crash
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProxyResponse'
              example:
                operation_id: op-7f3a
                status: accepted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Tenant Front Door
      summary: 'Tenant self-service DELETE on the caller''s own SHA (D13/CSP-W8): protection-patterns/{id}
        (+ /groups/{groupId}, /secondary-copy), machine-groups/{id} (+ /vms/{vmId}), schedules/{id}, restore/{mountId}
        (unmount — file-recovery feature). Same allowlist/feature/idempotency semantics. CSP edition only.'
      operationId: tenantProxyDelete
      x-required-edition: csp
      security:
      - tenantAuth: *id002
      parameters:
      - name: shaPath
        in: path
        required: true
        schema:
          type: string
        description: The SHA API sub-path (e.g. vm-contexts, backups). Allowlisted.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProxyResponse'
              example:
                operation_id: op-7f3a
                status: accepted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenant/features:
    get:
      tags:
      - Tenant Front Door
      summary: 'The caller''s effective feature set (design §17, D7): the full catalog with per-tenant
        enabled state. Advisory for portal nav-gating; the front door''s per-request gate is the boundary.
        CSP edition only.'
      operationId: getMyTenantFeatures
      x-required-edition: csp
      security:
      - tenantAuth: *id002
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantFeaturesResponse'
              example:
                features:
                - key: replication
                  label: Replication & DR
                  enabled: false
                - key: offsite-copy
                  label: Off-site copy
                  enabled: true
                - key: file-recovery
                  label: File-level recovery
                  enabled: true
                - key: app-aware
                  label: Application-aware recovery
                  enabled: true
                - key: clean-rooms
                  label: Clean-room validation
                  enabled: true
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/enroll:
    post:
      tags:
      - Enrollment
      summary: SHA submits an enrollment request (pairing code + ed25519 pubkey).
      operationId: initiateApplianceEnrollment
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InitiateEnrollmentRequest'
            example:
              pairing_code: 7F3K-9Q2M-XR4T
              appliance_name: acme-sha-prod-01
              appliance_hostname: sha-prod-01.acme.internal
              appliance_version: 4.3.520
              public_key_ed25519: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH0k... sha-prod-01
              hardware_fingerprint: hw:9f2a1c7e4b
              current_ip: 10.3.2.32
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InitiateEnrollmentResponse'
              example:
                enrollment_id: 3b1e9c2a-8f4d-4a77-9c11-6d2e0f5b7a90
                challenge: Zk8xY2p3b3JkY2hhbGxlbmdlbm9uY2U=
                expires_at: '2026-07-02T15:30:00Z'
                status: challenge_sent
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/enroll/{id}/verify:
    post:
      tags:
      - Enrollment
      summary: SHA completes the challenge/response to verify key possession.
      operationId: verifyApplianceEnrollmentChallenge
      security: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: The SHA's base64 Ed25519 signature over the enrollment challenge, proving possession
                of the private key.
              properties:
                signature:
                  type: string
                  description: Base64-encoded Ed25519 signature over the challenge issued at enroll.
                  example: MEUCIQDf8x...base64signature...
              required:
              - signature
            example:
              signature: MEUCIQDf8x...base64signature...
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - challenge_verified
                    example: challenge_verified
                required:
                - status
              example:
                status: challenge_verified
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/enroll/{id}/status:
    get:
      tags:
      - Enrollment
      summary: Poll enrollment status (pending/approved/rejected).
      operationId: getApplianceEnrollmentStatus
      security: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrollmentStatusResponse'
              example:
                status: approved
                proposed_access_tier: backup
                appliance_id: c4f7a2b9-1e3d-4c56-8a90-2b1f6e7d0c34
                ssh_user: sha_tunnel_007
                reverse_api_port: 20007
                diag_port: 29007
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/{id}/heartbeat:
    post:
      tags:
      - Enrollment
      summary: Signed heartbeat + §6.4 usage snapshot ingest (Ed25519-verified against the enrolled key;
        monotonic replay watermark).
      operationId: postApplianceHeartbeat
      security: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HeartbeatRequest'
            example:
              version: 4.3.520
              tenancy_mode: dedicated
              vm_count: 142
              protected_vm_count: 138
              storage_used_gb: 48213.5
              sna_count: 4
              sna_healthy: 4
              failed_jobs_24h: 2
              dr_readiness_pct: 97.5
              last_backup_at: '2026-07-02T13:05:00Z'
              snapshot: true
              access_tier: backup
              granted_actions:
              - backup.run
              - backup.read
              - restore.file
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - ok
                    example: ok
                required:
                - status
              example:
                status: ok
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid credentials/token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/{id}/read-credential:
    post:
      tags:
      - Enrollment
      summary: SHA pushes its per-relationship sca-service read token (ed25519-signed over the token).
      operationId: setApplianceReadCredential
      security: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: A just-approved SHA pushes the per-relationship sca-service read bearer it
                minted for the SCA (§8.1), authenticated by an Ed25519 signature over the token bytes.
              properties:
                token:
                  type: string
                  description: The per-relationship sca-service read bearer token the SHA minted for the
                    SCA. Escrowed on the SHA row (never re-serialized).
                  example: scasvc_rd_2b9f...longopaquetoken...
                signature:
                  type: string
                  description: Base64 Ed25519 signature over the raw token bytes, verified against the
                    SHA's enrollment public key.
                  example: MEQCIF3a...base64signature...
              required:
              - token
              - signature
            example:
              token: scasvc_rd_2b9f...longopaquetoken...
              signature: MEQCIF3a...base64signature...
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - read_credential_set
                    example: read_credential_set
                required:
                - status
              example:
                status: read_credential_set
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/pairing-codes/generate:
    post:
      tags:
      - Enrollment
      summary: Mint a one-time pairing code for a new SHA enrollment.
      operationId: generateAppliancePairingCode
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GeneratePairingCodeRequest'
            example:
              tenant_id: tenant_acme
              proposed_access_tier: backup
              expires_in_minutes: 60
              notes: Prod SHA for Acme datacenter
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GeneratePairingCodeResponse'
              example:
                code: 7F3K-9Q2M-XR4T
                expires_at: '2026-07-02T15:30:00Z'
                tenant_id: tenant_acme
                proposed_access_tier: backup
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/pending:
    get:
      tags:
      - Enrollment
      summary: List SHAs awaiting approval.
      operationId: listPendingApplianceEnrollments
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                description: Wrapper around the pending-approval enrollment queue.
                properties:
                  pending:
                    type: array
                    items:
                      $ref: '#/components/schemas/EnrollmentSession'
                    description: Enrollment sessions awaiting MSP-admin review.
                required:
                - pending
              example:
                pending:
                - id: 3b1e9c2a-8f4d-4a77-9c11-6d2e0f5b7a90
                  pairing_code_id: pc_8821
                  appliance_name: acme-sha-prod-01
                  appliance_hostname: sha-prod-01.acme.internal
                  appliance_type: SHA
                  appliance_version: 4.3.520
                  source_ip: 10.3.2.32
                  public_key_ed25519: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH0k... sha-prod-01
                  key_fingerprint: SHA256:9f2a1c7e4b...
                  hardware_fingerprint: hw:9f2a1c7e4b
                  challenge_verified: true
                  status: challenge_verified
                  expires_at: '2026-07-02T15:30:00Z'
                  created_at: '2026-07-02T14:31:00Z'
                  updated_at: '2026-07-02T14:31:05Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/{id}/approve:
    post:
      tags:
      - Enrollment
      summary: Approve a pending SHA (allocates a tunnel slot + reverse API port; assigns the tenant).
        Enforces the licensed-scale cap + SILO 1:1.
      operationId: approveApplianceEnrollment
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApproveRequest'
            example:
              tenant_id: tenant_acme
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SHA'
              example:
                id: c4f7a2b9-1e3d-4c56-8a90-2b1f6e7d0c34
                name: acme-sha-prod-01
                hostname: sha-prod-01.acme.internal
                version: 4.3.520
                administered_by: tenant
                key_fingerprint: SHA256:9f2a1c7e4b...
                hardware_fingerprint: hw:9f2a1c7e4b
                ip_address: 10.3.2.32
                tunnel_slot: 7
                ssh_user: sha_tunnel_007
                reverse_api_port: 20007
                diag_port: 29007
                enrollment_session_id: 3b1e9c2a-8f4d-4a77-9c11-6d2e0f5b7a90
                tenancy_mode: dedicated
                vm_count: 0
                protected_vm_count: 0
                storage_used_gb: 0
                sna_count: 0
                sna_healthy: 0
                failed_jobs_24h: 0
                dr_readiness_pct: 0
                status: pending
                created_at: '2026-07-02T14:32:00Z'
                updated_at: '2026-07-02T14:32:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/{id}/reject:
    post:
      tags:
      - Enrollment
      summary: Reject a pending SHA enrollment.
      operationId: rejectApplianceEnrollment
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Optional rejection reason. Body is optional.
              properties:
                reason:
                  type: string
                  description: Free-text reason recorded on the enrollment session.
                  example: Unrecognized appliance / not authorized for this tenant
            example:
              reason: Unrecognized appliance / not authorized for this tenant
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - rejected
                    example: rejected
                required:
                - status
              example:
                status: rejected
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/{id}/revoke:
    post:
      tags:
      - Enrollment
      summary: Disenroll a SHA (removes the tunnel user/key, clears the read token, deletes the tenancy
        link).
      operationId: revokeAppliance
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - revoked
                    example: revoked
                required:
                - status
              example:
                status: revoked
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/{id}/rotate-key:
    post:
      tags:
      - Enrollment
      summary: Rotate a SHA's enrollment key.
      operationId: rotateApplianceTunnelKey
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Admin-gated swap of a SHA's tunnel authorized key to a new Ed25519 public key.
              properties:
                new_public_key:
                  type: string
                  description: The new Ed25519 public key (SSH authorized_keys wire form) to install for
                    the SHA's tunnel user and persist.
                  example: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINewKey... sha-prod-01
              required:
              - new_public_key
            example:
              new_public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINewKey... sha-prod-01
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - key_rotated
                    example: key_rotated
                required:
                - status
              example:
                status: key_rotated
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants:
    get:
      tags:
      - Tenants
      summary: List tenants in scope (admin sees all; staff sees assigned).
      operationId: listTenants
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantListResponse'
              example:
                tenants:
                - id: 9f1c2d3e-4b5a-6789-0abc-def012345678
                  name: Acme Corporation
                  code: acme
                  status: active
                  contact_email: ops@acme.example.com
                  created_at: '2026-06-01T09:00:00Z'
                  updated_at: '2026-07-01T12:30:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Tenants
      summary: Create a tenant (admin only).
      operationId: createTenant
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TenantCreateRequest'
            example:
              name: Acme Corporation
              code: acme
              status: active
              contact_email: ops@acme.example.com
              notes: Onboarded Q1; dedicated SILO.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tenant'
              example:
                id: 9f1c2d3e-4b5a-6789-0abc-def012345678
                name: Acme Corporation
                code: acme
                status: active
                contact_email: ops@acme.example.com
                notes: Onboarded Q1; dedicated SILO.
                created_at: '2026-07-02T14:05:00Z'
                updated_at: '2026-07-02T14:05:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}:
    get:
      tags:
      - Tenants
      summary: Get one tenant.
      operationId: getTenant
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tenant'
              example:
                id: 9f1c2d3e-4b5a-6789-0abc-def012345678
                name: Acme Corporation
                code: acme
                status: active
                contact_email: ops@acme.example.com
                created_at: '2026-06-01T09:00:00Z'
                updated_at: '2026-07-01T12:30:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
      - Tenants
      summary: Update a tenant (name/status/contact/notes). Suspension takes effect on the tenant's next
        request.
      operationId: updateTenant
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TenantUpdateRequest'
            example:
              status: suspended
              notes: Moved to suspended pending renewal.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tenant'
              example:
                id: 9f1c2d3e-4b5a-6789-0abc-def012345678
                name: Acme Corporation
                code: acme
                status: suspended
                contact_email: ops@acme.example.com
                notes: Moved to suspended pending renewal.
                created_at: '2026-06-01T09:00:00Z'
                updated_at: '2026-07-02T14:10:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Tenants
      summary: Delete a tenant (admin only; blocked while a live SHA is enrolled).
      operationId: deleteTenant
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteStatusResponse'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/summary:
    get:
      tags:
      - Tenants
      summary: Tenant KPI summary (appliances, protected VMs, storage, alerts).
      operationId: getTenantSummary
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantSummary'
              example:
                tenant:
                  id: 9f1c2d3e-4b5a-6789-0abc-def012345678
                  name: Acme Corporation
                  code: acme
                  status: active
                  contact_email: ops@acme.example.com
                  created_at: '2026-06-01T09:00:00Z'
                  updated_at: '2026-07-01T12:30:00Z'
                sha_count: 2
                online_sha_count: 1
                protected_vm_count: 42
                storage_used_gb: 1536.5
                failed_jobs_24h: 3
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/shas:
    get:
      tags:
      - Tenants
      summary: The tenant's enrolled SHAs.
      operationId: listTenantShas
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantSHAsResponse'
              example:
                shas:
                - id: sha-7c3f9a12
                  name: acme-hub-01
                  status: online
                  version: 4.3.520
                  last_heartbeat_at: '2026-07-02T13:59:00Z'
                  protected_vm_count: 42
                  storage_used_gb: 1536.5
                  failed_jobs_24h: 3
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/usage:
    get:
      tags:
      - Tenants
      summary: Tenant usage time series (usage_snapshots), optional from/to; format=json|csv.
      operationId: getTenantUsage
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantUsageResponse'
              example:
                tenant_id: 9f1c2d3e-4b5a-6789-0abc-def012345678
                points:
                - captured_at: '2026-07-02T12:00:00Z'
                  vm_count: 60
                  protected_vm_count: 42
                  storage_used_gb: 1536.5
                  failed_jobs_24h: 3
                  dr_readiness_pct: 98.5
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/users:
    get:
      tags:
      - Tenant Front Door
      summary: List the tenant's end-tenant login identities. CSP edition only.
      operationId: listTenantUsers
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: array
                description: Bare JSON array of the tenant's login identities, ordered by email. Password
                  hashes are never included. writeJSON emits the slice directly (no wrapper object).
                items:
                  $ref: '#/components/schemas/TenantUser'
              example:
              - id: 3f2c1b7a-90de-4a11-8c33-0a1b2c3d4e5f
                tenant_id: acme-corp
                email: ops@acme.example.com
                sha_role: backup-operator
                status: active
                created_at: '2026-06-01T09:15:00Z'
                updated_at: '2026-06-01T09:15:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Tenant Front Door
      summary: Create an end-tenant login (email/password + assigned SHA role). CSP edition only.
      operationId: createTenantUser
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Create an SCA-local end-tenant login identity (design 6.2). Staff route. {id}
                = tenant id; the new user is scoped to it. Email is normalized/lowercased; uniqueness
                is (tenant_id, email). sha_role is required.
              properties:
                email:
                  type: string
                  format: email
                  example: ops@acme.example.com
                password:
                  type: string
                  format: password
                  description: Plaintext password; stored bcrypt-hashed.
                  example: s3cr3t-passphrase
                sha_role:
                  type: string
                  description: Role this user gets on their SHA (validated against the SHA's tenant-assignable
                    roles at token-exchange time, not here). Required.
                  example: backup-operator
              required:
              - email
              - password
              - sha_role
            example:
              email: ops@acme.example.com
              password: s3cr3t-passphrase
              sha_role: backup-operator
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantUserCreated'
              example:
                id: 3f2c1b7a-90de-4a11-8c33-0a1b2c3d4e5f
                tenant_id: acme-corp
                email: ops@acme.example.com
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/users/{uid}:
    delete:
      tags:
      - Tenant Front Door
      summary: Delete an end-tenant login. CSP edition only.
      operationId: deleteTenantUser
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: uid
        in: path
        required: true
        schema:
          type: string
        description: Tenant-user id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                description: No response body. Returns 204 No Content on success. Deletion is scoped to
                  {id} so a caller authorized for tenant A can never delete tenant B's user by id.
                additionalProperties: false
              example: {}
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/federation:
    get:
      tags:
      - Tenant Front Door
      summary: Get the tenant's OIDC federation config. CSP edition only.
      operationId: getTenantFederation
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantIdPConfig'
              example:
                id: b21e77a0-5c44-4d0e-9a2b-77c9f0e1a234
                tenant_id: acme-corp
                protocol: oidc
                issuer: https://login.acme.example.com/
                client_id: sendense-portal
                jwks_uri: https://login.acme.example.com/.well-known/jwks.json
                email_claim: email
                default_sha_role: backup-operator
                enabled: true
                created_at: '2026-06-01T09:15:00Z'
                updated_at: '2026-06-01T09:15:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
      - Tenant Front Door
      summary: Set the tenant's OIDC IdP (issuer/JWKS/default SHA role). CSP edition only.
      operationId: setTenantFederation
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TenantIdPConfig'
            example:
              protocol: oidc
              issuer: https://login.acme.example.com/
              client_id: sendense-portal
              jwks_uri: https://login.acme.example.com/.well-known/jwks.json
              email_claim: email
              default_sha_role: backup-operator
              enabled: true
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantIdPConfig'
              example:
                id: b21e77a0-5c44-4d0e-9a2b-77c9f0e1a234
                tenant_id: acme-corp
                protocol: oidc
                issuer: https://login.acme.example.com/
                client_id: sendense-portal
                jwks_uri: https://login.acme.example.com/.well-known/jwks.json
                email_claim: email
                default_sha_role: backup-operator
                enabled: true
                created_at: '2026-06-01T09:15:00Z'
                updated_at: '2026-06-01T09:15:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/features:
    get:
      tags:
      - Tenant Front Door
      summary: A tenant's effective feature entitlements (design §17, D7). CSP edition only.
      operationId: getTenantFeatures
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantFeaturesResponse'
              example:
                tenant_id: acme-corp
                features:
                - key: replication
                  label: Replication & DR
                  enabled: false
                - key: offsite-copy
                  label: Off-site copy
                  enabled: true
                - key: file-recovery
                  label: File-level recovery
                  enabled: true
                - key: app-aware
                  label: Application-aware recovery
                  enabled: true
                - key: clean-rooms
                  label: Clean-room validation
                  enabled: true
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
      - Tenant Front Door
      summary: Set a tenant's feature switches (admin; partial update; enforced live at the front door).
        CSP edition only.
      operationId: setTenantFeatures
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTenantFeaturesRequest'
            example:
              features:
                replication: false
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantFeaturesResponse'
              example:
                tenant_id: acme-corp
                features:
                - key: replication
                  label: Replication & DR
                  enabled: false
                - key: offsite-copy
                  label: Off-site copy
                  enabled: true
                - key: file-recovery
                  label: File-level recovery
                  enabled: true
                - key: app-aware
                  label: Application-aware recovery
                  enabled: true
                - key: clean-rooms
                  label: Clean-room validation
                  enabled: true
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users:
    get:
      tags:
      - MSP Users
      summary: List MSP staff users (admin only).
      operationId: listUsers
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
              example:
                users:
                - id: usr-9f3a
                  email: admin@sendense.local
                  full_name: Site Admin
                  status: active
                  roles:
                  - admin
                  is_admin: true
                  tenant_ids: []
                  created_at: '2026-05-14T09:12:00Z'
                - id: usr-4b2c
                  email: jane.doe@sendense.local
                  full_name: Jane Doe
                  status: active
                  roles:
                  - staff
                  is_admin: false
                  tenant_ids:
                  - tnt-acme
                  - tnt-globex
                  created_at: '2026-06-01T11:30:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - MSP Users
      summary: Create an MSP staff user with a role (admin only).
      operationId: createUser
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: New MSP user. role is 'admin' (fleet-wide) or 'staff' (scoped to assigned tenants);
                defaults to 'staff' if omitted. full_name is optional.
              properties:
                email:
                  type: string
                  format: email
                  example: new.staff@sendense.local
                password:
                  type: string
                  format: password
                  example: Welcome1!
                full_name:
                  type: string
                  example: New Staff
                role:
                  type: string
                  enum:
                  - admin
                  - staff
                  description: Defaults to 'staff' when empty. Any other value is rejected with 400.
                  example: staff
              required:
              - email
              - password
            example:
              email: new.staff@sendense.local
              password: Welcome1!
              full_name: New Staff
              role: staff
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedUserResponse'
              example:
                id: usr-7d1e
                email: new.staff@sendense.local
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users/{id}:
    delete:
      tags:
      - MSP Users
      summary: Delete an MSP staff user (clears roles + tenant scopes first).
      operationId: deleteUser
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users/{id}/tenants:
    post:
      tags:
      - MSP Users
      summary: Assign a staff user to a tenant scope.
      operationId: assignUserTenant
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Scope the MSP staff user {id} to a tenant.
              properties:
                tenant_id:
                  type: string
                  example: tnt-acme
              required:
              - tenant_id
            example:
              tenant_id: tnt-acme
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
              example:
                status: assigned
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users/{id}/tenants/{tenantId}:
    delete:
      tags:
      - MSP Users
      summary: Unassign a staff user from a tenant.
      operationId: unassignUserTenant
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: tenantId
        in: path
        required: true
        schema:
          type: string
        description: Tenant id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
              example:
                status: unassigned
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/partner-repositories:
    get:
      tags:
      - Storage
      summary: List partner storage offerings (backup targets; data flows SHA→endpoint, never via the
        SCA).
      operationId: listPartnerRepositories
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRepositoryListResponse'
              example:
                repositories:
                - id: a1b2c3d4-5e6f-7089-abcd-1234567890ef
                  name: EU-West Object Store
                  endpoint: https://s3.eu-west-1.partner.example.com
                  kind: s3
                  region: eu-west-1
                  bucket: tenant-backups
                  notes: Primary offering for EU tenants
                  created_at: '2026-07-02T10:15:30Z'
                  updated_at: '2026-07-02T10:15:30Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Storage
      summary: Register a partner storage offering (admin only).
      operationId: createPartnerRepository
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerRepositoryCreateRequest'
            example:
              name: EU-West Object Store
              endpoint: https://s3.eu-west-1.partner.example.com
              kind: s3
              region: eu-west-1
              bucket: tenant-backups
              notes: Primary offering for EU tenants
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRepository'
              example:
                id: a1b2c3d4-5e6f-7089-abcd-1234567890ef
                name: EU-West Object Store
                endpoint: https://s3.eu-west-1.partner.example.com
                kind: s3
                region: eu-west-1
                bucket: tenant-backups
                notes: Primary offering for EU tenants
                created_at: '2026-07-02T10:15:30Z'
                updated_at: '2026-07-02T10:15:30Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/partner-repositories/{id}:
    delete:
      tags:
      - Storage
      summary: Delete a partner storage offering (admin only).
      operationId: deletePartnerRepository
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedStatusResponse'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/providers:
    get:
      tags:
      - Storage
      summary: List managed storage providers + probed capabilities (§18.2). Admin credentials never serialize.
        CSP edition only.
      operationId: listStorageProviders
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedStorageProviderListResponse'
              example:
                providers:
                - id: 9a1c…
                  name: CSP MinIO EU
                  endpoint: http://minio:9000
                  kind: minio
                  status: active
                  capabilities:
                    bucket_quota: true
                    object_lock: true
                    usage_metrics: true
                    policy_separation: true
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Storage
      summary: 'Register a managed provider (admin): AES-GCM-vault the admin credential + probe capabilities
        — both fail closed (§18.3 step 1). kind=minio (SCA-managed) or external (adopt a customer-run
        bucket). CSP edition only.'
      operationId: registerStorageProvider
      x-required-edition: csp
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterStorageProviderRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedStorageProvider'
              example:
                id: 9a1c…
                name: CSP MinIO EU
                endpoint: http://minio:9000
                kind: minio
                status: active
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/providers/{id}/probe:
    post:
      tags:
      - Storage
      summary: Re-probe provider reachability/capabilities and update its status (admin). CSP edition
        only.
      operationId: probeStorageProvider
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageProviderProbeResponse'
              example:
                status: active
                capabilities:
                  bucket_quota: true
                  object_lock: true
                  usage_metrics: true
                  policy_separation: true
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/providers/{id}:
    delete:
      tags:
      - Storage
      summary: Delete a provider with no live managed buckets (409 otherwise; admin). CSP edition only.
      operationId: deleteStorageProvider
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedStatusResponse'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets:
    get:
      tags:
      - Storage
      summary: List managed buckets (?tenant_id=). One bucket = one EBA repo = one dedup domain per tenant.
        CSP edition only.
      operationId: listManagedBuckets
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucketListResponse'
              example:
                buckets:
                - id: b1…
                  tenant_id: t1…
                  bucket_name: sdt-acme-1a2b3c4d
                  state: assigned
                  sha_repo_ref: eba-repo-1751500000
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Storage
      summary: Create a per-tenant bucket (+optional hard quota/object-lock) at the provider — state ready;
        no credential minted yet (D10; admin). CSP edition only.
      operationId: createManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateManagedBucketRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucket'
              example:
                id: b1…
                bucket_name: sdt-acme-1a2b3c4d
                state: ready
                quota_bytes: 2147483648
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}:
    delete:
      tags:
      - Storage
      summary: Delete a NEVER-ASSIGNED bucket (creating/ready — crash recovery; 409 otherwise; assigned
        buckets ride reclaim→purge; admin). CSP edition only.
      operationId: deleteManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedStatusResponse'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}/assign:
    post:
      tags:
      - Storage
      summary: Mint a bucket-scoped credential (ledgered by key id), deliver it to the tenant's SHA (storage.assign_repository
        over sca/operations — the SHA registers the EBA repo), record metadata + DROP the plaintext (D10;
        admin). Retries reconcile the prior attempt first — a landed-but-unconfirmed assign folds in,
        never double-mints. CSP edition only.
      operationId: assignManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssignManagedBucketRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucket'
              example:
                id: b1…
                state: assigned
                sha_repo_ref: eba-repo-1751500000
                access_key_id: AKIA…
                assign_operation_id: op…
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}/rotate:
    post:
      tags:
      - Storage
      summary: 'Rotate the assigned bucket''s per-tenant credential (§18.3.4): mint a FRESH key (ledgered),
        deliver it to the tenant''s SHA (storage.rotate_repository_credential over sca/operations — the
        SHA swaps the repo credential in place), then revoke the OLD key. Retries reconcile the prior
        attempt first; unknown outcomes keep both keys ledgered. D10 throughout (admin). CSP edition only.'
      operationId: rotateManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RotateManagedBucketRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucket'
              example:
                id: b1…
                state: assigned
                sha_repo_ref: eba-repo-1751500000
                access_key_id: AKIA…NEW
                credential_rotated_at: '2026-07-03T11:00:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}/reclaim:
    post:
      tags:
      - Storage
      summary: 'Start §18.6 offboarding: revoke the tenant credential at the provider; state reclaim_pending
        (data-return window; admin). CSP edition only.'
      operationId: reclaimManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucket'
              example:
                id: b1…
                state: reclaim_pending
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}/exported:
    post:
      tags:
      - Storage
      summary: 'Record the §18.6 data-return outcome: reclaim_pending → exported (admin). CSP edition
        only.'
      operationId: exportManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExportManagedBucketRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucket'
              example:
                id: b1…
                state: exported
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}/unassign:
    post:
      tags:
      - Storage
      summary: '§18.6 offboarding TAIL: remove the dead managed EBA repository row from the tenant''s
        SHA (storage.unassign_repository over sca/operations — the SHA''s native delete-safety guard refuses
        while restorable backups remain; already-absent folds as success). Exported/purged buckets only;
        purge auto-attempts this, the endpoint retries an owed one. CSP edition only.'
      operationId: unassignManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucket'
              example:
                id: b1…
                state: purged
                sha_repo_ref: ''
                notes: 'unassigned: SHA repository eba-repo-1751500000 removed (§18.6)'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}/purge:
    post:
      tags:
      - Storage
      summary: 'Terminal §18.6 purge: capture deletion evidence, force-delete objects + bucket at the
        provider, state purged (admin; requires confirm=purge). CSP edition only.'
      operationId: purgeManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PurgeManagedBucketRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PurgeManagedBucketResponse'
              example:
                status: purged
                evidence:
                  bucket_name: sdt-acme-1a2b3c4d
                  object_count: 12345
                  total_bytes: 987654321
                  usage_source: list
                  purged_by: admin@sendense.local
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}/usage:
    get:
      tags:
      - Storage
      summary: Provider-side usage next to the SHA-reported per-repo figure — the §18.4 reconciliation
        pair. CSP edition only.
      operationId: managedBucketUsage
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucketUsageResponse'
              example:
                bucket_id: b1…
                bucket_name: sdt-acme-1a2b3c4d
                state: assigned
                provider:
                  bytes: 5368709120
                  objects: 1042
                  source: list
                sha_reported:
                  stored_bytes: 5368709120
                  captured_at: '2026-07-03T10:00:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/buckets/{id}/detach:
    post:
      tags:
      - Storage
      summary: '§18.8: release an ADOPTED/EXTERNAL bucket back to CSP control WITHOUT touching data (the
        data-preserving sibling of purge; the only terminal path for external buckets). Reclaim/export
        first; writes action=detached evidence + best-effort SHA unassign (admin). CSP edition only.'
      operationId: detachManagedBucket
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DetachManagedBucketRequest'
            example:
              confirm: detach
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetachManagedBucketResponse'
              example:
                status: detached
                evidence:
                  bucket_name: csp-owned-bucket
                  action: detached
                  usage_source: unavailable
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/offerings:
    get:
      tags:
      - Storage
      summary: 'List storage offerings with bucket/tenant counts (§18.8): mode (managed_create|managed_adopt|external),
        provider, defaults, per-offering rate card. CSP edition only.'
      operationId: listStorageOfferings
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageOfferingListResponse'
              example:
                offerings:
                - id: o1…
                  name: Standard S3 (UK)
                  provisioning_mode: managed_create
                  rate_per_tb_month: 25
                  bucket_count: 3
                  assigned_count: 3
                  tenant_count: 3
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Storage
      summary: 'Create a storage offering (admin): provisioning mode must match the provider kind (external
        mode ⇔ kind=external). rate_per_tb_month (optional) prices this offering''s D11 statement line
        instead of the card rate. CSP edition only.'
      operationId: createStorageOffering
      x-required-edition: csp
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StorageOfferingRequest'
            example:
              name: Standard S3 (UK)
              provisioning_mode: managed_create
              provider_id: p1…
              default_quota_gb: 100
              rate_per_tb_month: 25
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageOffering'
              example:
                id: o1…
                name: Standard S3 (UK)
                status: active
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/offerings/{id}:
    put:
      tags:
      - Storage
      summary: 'Edit an offering (admin): name/description/defaults/rate/notes mutable; status flips active|retired
        (retired = hidden from assignment). Mode + provider are IMMUTABLE. CSP edition only.'
      operationId: updateStorageOffering
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StorageOfferingRequest'
            example:
              name: Standard S3 (UK)
              provisioning_mode: managed_create
              provider_id: p1…
              rate_per_tb_month: 22
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageOffering'
              example:
                id: o1…
                rate_per_tb_month: 22
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Storage
      summary: Delete an offering NO bucket references (409 with history — retire instead; admin). CSP
        edition only.
      operationId: deleteStorageOffering
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedStatusResponse'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/storage/offerings/{id}/assign:
    post:
      tags:
      - Storage
      summary: '§18.8 ONE-CLICK assign: instantiate the offering for a tenant per its mode (create/adopt/record
        the bucket) and run the D10 assign in one call. Idempotent (an existing live bucket under the
        offering returns/resumes; new_bucket=true forces an additional one). External mode takes the CSP-minted
        bucket credential in the body — delivered to the SHA and dropped, never at rest (admin). CSP edition
        only.'
      operationId: assignStorageOffering
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssignStorageOfferingRequest'
            example:
              tenant_id: 3e63...
              new_bucket: false
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedBucket'
              example:
                id: b1…
                state: assigned
                offering_id: o1…
                provisioning_mode: managed_create
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts:
    get:
      tags:
      - Fleet
      summary: List placement hosts (tins) with derived allocated-set counts (design §15.2).
      operationId: listHosts
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HostsResponse'
              example:
                hosts:
                - id: b7c1…
                  name: tin-fra-01
                  kind: tin
                  address: 10.20.0.11
                  cpu_cores: 128
                  mem_gb: 1024
                  disk_gb: 32000
                  max_sets: 40
                  health: unknown
                  allocated_sets: 12
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Fleet
      summary: Register a placement host (admin; name, capacity incl. max_sets — 0 = no cap).
      operationId: createHost
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateHostRequest'
            example:
              name: tin-fra-01
              max_sets: 40
              cpu_cores: 128
              mem_gb: 1024
              disk_gb: 32000
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
              example:
                id: b7c1…
                name: tin-fra-01
                kind: tin
                address: 10.20.0.11
                cpu_cores: 128
                mem_gb: 1024
                disk_gb: 32000
                max_sets: 40
                health: unknown
                allocated_sets: 12
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/recommendation:
    get:
      tags:
      - Fleet
      summary: Least-loaded registered host with free capacity (404 when none) — the §15.2 default placement.
      operationId: recommendHost
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
              example:
                id: b7c1…
                name: tin-fra-01
                kind: tin
                address: 10.20.0.11
                cpu_cores: 128
                mem_gb: 1024
                disk_gb: 32000
                max_sets: 40
                health: unknown
                allocated_sets: 12
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/{id}:
    get:
      tags:
      - Fleet
      summary: One placement host + derived allocation.
      operationId: getHost
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
              example:
                id: b7c1…
                name: tin-fra-01
                kind: tin
                address: 10.20.0.11
                cpu_cores: 128
                mem_gb: 1024
                disk_gb: 32000
                max_sets: 40
                health: unknown
                allocated_sets: 12
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Fleet
      summary: Delete an EMPTY placement host (409 while appliances are placed on it; admin).
      operationId: deleteHost
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                example:
                  status: deleted
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/appliances/{id}/host:
    put:
      tags:
      - Fleet
      summary: Associate an enrolled SHA's tenancy with a registered host (admin; capacity-checked — 409
        at max_sets; empty host_id clears).
      operationId: setApplianceHost
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetApplianceHostRequest'
            example:
              host_id: b7c1…
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  sha_id:
                    type: string
                  host_id:
                    type: string
              example:
                sha_id: 05cc0afc…
                host_id: b7c1…
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/agent-tokens:
    post:
      tags:
      - Fleet
      summary: Mint a one-time host-agent install token bound to a host name/kind (or host_id for reinstall);
        returns the plaintext token ONCE (stored hashed). Admin; CSP edition.
      operationId: mintHostAgentToken
      x-required-edition: csp
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MintHostAgentTokenRequest'
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      tags:
      - Fleet
      summary: List host-agent install tokens (hashes never serialize). CSP edition.
      operationId: getHostsAgenttokens
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/agent-tokens/{id}:
    delete:
      tags:
      - Fleet
      summary: Revoke a PENDING install token (409 if already redeemed/revoked; admin). CSP edition.
      operationId: deleteHostsAgenttokensById
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/agent/enroll:
    post:
      tags:
      - Fleet
      summary: 'Host-agent enrollment: redeem an install token + register the agent''s Ed25519 key as
        the host''s durable identity (the token is the bearer of trust — no staff JWT). Single-use; fail-closed.
        CSP edition.'
      operationId: enrollHostAgent
      x-required-edition: csp
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostAgentEnrollRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid credentials/token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/{id}/agent/heartbeat:
    post:
      tags:
      - Fleet
      summary: 'Signed host-agent heartbeat (X-SCA-Signature + X-SCA-Timestamp over the host''s registered
        agent key): refresh host capacity + per-set state, and RETURN pending container-set commands.
        CSP edition.'
      operationId: postHostsAgentHeartbeatById
      x-required-edition: csp
      security: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/{id}/agent/ack:
    post:
      tags:
      - Fleet
      summary: 'Signed command-outcome report from the host agent: success advances the set; failure re-queues
        until the cap then fails the set. CSP edition.'
      operationId: hostAgentAck
      x-required-edition: csp
      security: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostAgentAckRequest'
      responses:
        '200':
          description: Success
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/{id}/container-sets:
    post:
      tags:
      - Fleet
      summary: 'Provision a container-set for a tenant on this host: capacity-check, set up per-set networking,
        mint the tenant-bound auto_provision pairing code, render the per-set compose bundle, and queue
        a create_set command. network_mode defaults to `ip` (locked D9) — a dedicated per-set IP so the
        tenant''s SNAs dial it on :443; `set_ip` is REQUIRED in ip mode (400 otherwise). The `port` fallback
        allocates a per-set high-port block on a shared host IP. Admin; CSP edition.'
      operationId: createContainerSet
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContainerSetRequest'
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/container-sets:
    get:
      tags:
      - Fleet
      summary: List container-sets (?host_id= / ?tenant_id=). CSP edition.
      operationId: getContainersets
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/container-sets/{id}/start:
    post:
      tags:
      - Fleet
      summary: Queue a start_set command for the host agent (admin). CSP edition.
      operationId: postContainersetsStartById
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/container-sets/{id}/stop:
    post:
      tags:
      - Fleet
      summary: Queue a stop_set command (admin). CSP edition.
      operationId: postContainersetsStopById
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/container-sets/{id}/remove:
    post:
      tags:
      - Fleet
      summary: Queue a remove_set command (compose down -v; admin). CSP edition.
      operationId: postContainersetsRemoveById
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/provisioning/domains:
    get:
      tags:
      - Enrollment
      summary: CloudStack domains (§16.7 tenant-binding picker; root-admin credential, admin-only). CSP
        edition only.
      operationId: listProvisioningDomains
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        path:
                          type: string
              example:
                domains:
                - id: d1
                  name: acme
                  path: /acme
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/provisioning/accounts:
    get:
      tags:
      - Enrollment
      summary: CloudStack accounts in a domain (?domain_id=). CSP edition only.
      operationId: listProvisioningAccounts
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  accounts:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        domain_id:
                          type: string
              example:
                accounts:
                - id: a1
                  name: acme
                  domain_id: d1
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/provisioning/resources:
    get:
      tags:
      - Enrollment
      summary: Account-scoped zone/template/offering/network pickers (?domain_id=&account=) — fetched
        IN the tenant account context (D12), never root's. CSP edition only.
      operationId: listProvisioningResources
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  zones:
                    type: array
                  templates:
                    type: array
                  service_offerings:
                    type: array
                  networks:
                    type: array
              example:
                zones:
                - id: z1
                  name: zone1
                templates: []
                service_offerings: []
                networks: []
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/provisioning/records:
    get:
      tags:
      - Enrollment
      summary: 'Provisioning evidence trail (?tenant_id=): pairing code, provider VM id, expected D8 IPs,
        state (provisioning|enrolled|expired|failed). CSP edition only.'
      operationId: listProvisioningRecords
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  records:
                    type: array
                    items:
                      $ref: '#/components/schemas/ProvisioningRecord'
              example:
                records:
                - id: pr-1
                  tenant_id: acme
                  kind: sha
                  pairing_code_id: pc-9
                  provider_vm_id: vm-42
                  expected_ips: 10.1.1.5,203.0.113.9
                  state: provisioning
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/provisioning/cloudstack-credential:
    get:
      tags:
      - Enrollment
      summary: CloudStack credential METADATA only (state active|pending, api_url, api_key id, timestamps)
        — the sealed secret is NEVER returned (§16.5). Admin. CSP edition only.
      operationId: getCloudStackCredential
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudStackCredentialListResponse'
              example:
                vault_configured: true
                credentials:
                - id: c1
                  state: active
                  api_url: https://cs/client/api
                  api_key: AKIA…
                  verify_ssl: true
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
      - Enrollment
      summary: 'Vault a CloudStack root-admin credential (AES-GCM at rest, §16.5): first set becomes active;
        with an active present it is stored PENDING (a rotation candidate, unused until verified). Write-only.
        Admin. CSP edition only.'
      operationId: setCloudStackCredential
      x-required-edition: csp
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetCloudStackCredentialRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudStackCredential'
              example:
                id: c2
                state: pending
                api_url: https://cs/client/api
                api_key: AKIB…
                verify_ssl: true
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Enrollment
      summary: Cancel a pending credential rotation (admin). CSP edition only.
      operationId: cancelCloudStackRotation
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedStatusResponse'
              example:
                status: pending rotation cancelled
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/provisioning/cloudstack-credential/verify:
    post:
      tags:
      - Enrollment
      summary: Probe the PENDING credential (read-only ListDomains) and promote it to active on success
        — the §16.5 old-kept-until-first-success rotation; the active is untouched on failure (admin).
        CSP edition only.
      operationId: verifyCloudStackRotation
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudStackRotationVerifyResponse'
              example:
                status: rotated
                credential:
                  id: c2
                  state: active
                  api_key: AKIB…
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/cloudstack:
    put:
      tags:
      - Enrollment
      summary: Bind a tenant to its CloudStack {domain, account} (D12 — instances are created UNDER this
        account; empty values clear). Admin. CSP edition only.
      operationId: setTenantCloudStack
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetTenantCloudStackRequest'
            example:
              cloudstack_domain_id: d1
              cloudstack_account: acme
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenant_id:
                    type: string
                  cloudstack_domain_id:
                    type: string
                  cloudstack_account:
                    type: string
                  enroll_endpoint:
                    type: string
                    description: Echoed when set on the tenant.
              example:
                tenant_id: acme
                cloudstack_domain_id: d1
                cloudstack_account: acme
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/provision:
    post:
      tags:
      - Enrollment
      summary: 'Provision a CSP-owned SHA appliance for the tenant (§16.3): mints a single-use auto_provision
        pairing code, deploys the VM in the tenant''s OWN account (D12) with enrollment userdata, records
        the D8 evidence. The appliance enrolls itself; auto-approve fires only on auto_provision code
        + provider-IP match (Ed25519 always runs), else the manual queue. Admin. CSP edition only.'
      operationId: provisionTenantAppliance
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProvisionTenantRequest'
            example:
              zone_id: z1
              template_id: tpl-sha
              offering_id: off-4c8g
              network_id: net-acme
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisioningRecord'
              example:
                id: pr-1
                tenant_id: acme
                kind: sha
                pairing_code_id: pc-9
                provider_vm_id: vm-42
                expected_ips: 10.1.1.5,203.0.113.9
                state: provisioning
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/onboarding-runs:
    post:
      tags:
      - Onboarding
      summary: 'CSP-W10 §16.9 zero-touch onboarding: configure a tenant SHA end-to-end from ONE POST (provision/attach
        → repo assign → site → SNA provision → source creds → discovery) as a durable, resumable state
        machine. Secret-bearing specs are vault-sealed at rest (D10). Admin. CSP edition only.'
      operationId: startOnboardingRun
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OnboardingSpec'
            example:
              attach_sha_id: sha-abc123
              site:
                name: HQ
              sources:
              - source_name: acme-cs
                api_host: cs.acme.example
                api_key: …
                secret_key: …
                zone_name: zone1
              discovery:
                discover_cloudstack: true
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingRun'
              example:
                id: 9c1e...
                tenant_id: 3e63...
                sha_id: sha-abc123
                status: pending
                step: site
                ledger: '{"sha_id":"sha-abc123","site_id":"site-9","source_op_ids":["op-1"],"discovery_execution_id":"exec-77"}'
                attempts: 0
                created_at: '2026-07-07T09:00:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      tags:
      - Onboarding
      summary: List the tenant's onboarding runs (tenant-scoped; 404 out of scope). CSP edition only.
      operationId: listOnboardingRuns
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingRunListResponse'
              example:
                runs:
                - id: 9c1e...
                  tenant_id: 3e63...
                  sha_id: sha-abc123
                  status: running
                  step: discovery_wait
                  ledger: '{"sha_id":"sha-abc123","site_id":"site-9","source_op_ids":["op-1"],"discovery_execution_id":"exec-77"}'
                  attempts: 0
                  created_at: '2026-07-07T09:00:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/onboarding-runs/{rid}:
    get:
      tags:
      - Onboarding
      summary: Get one onboarding run — status/step/ledger progress (the sealed spec is never returned).
        CSP edition only.
      operationId: getOnboardingRun
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: rid
        in: path
        required: true
        schema:
          type: string
        description: rid path parameter.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingRun'
              example:
                id: 9c1e...
                tenant_id: 3e63...
                sha_id: sha-abc123
                status: running
                step: discovery_wait
                ledger: '{"sha_id":"sha-abc123","site_id":"site-9","source_op_ids":["op-1"],"discovery_execution_id":"exec-77"}'
                attempts: 0
                created_at: '2026-07-07T09:00:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/onboarding-runs/{rid}/cancel:
    post:
      tags:
      - Onboarding
      summary: 'Abort a non-terminal onboarding run: it flips to `cancelled` and the stepper stops advancing
        it (committed steps are idempotent and recorded in the ledger — cancel stops FURTHER steps, it
        does not roll back). 409 if the run is already terminal. Admin. CSP edition only.'
      operationId: postTenantsOnboardingrunsCancelByIdRid
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: rid
        in: path
        required: true
        schema:
          type: string
        description: rid path parameter.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingRun'
              example:
                id: 9c1e...
                tenant_id: 3e63...
                sha_id: sha-abc123
                status: cancelled
                step: discovery_wait
                ledger: '{"sha_id":"sha-abc123","site_id":"site-9","source_op_ids":["op-1"],"discovery_execution_id":"exec-77"}'
                attempts: 0
                created_at: '2026-07-07T09:00:00Z'
                last_error: cancelled by admin@sendense.local
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/onboarding-runs/{rid}/retry:
    post:
      tags:
      - Onboarding
      summary: 'Re-open a failed or cancelled onboarding run: the stepper drives it again from its current
        step (safe — per-step idempotency keys make a replay read back the original resource rather than
        duplicate it). The TTL window restarts. 409 if the run is not failed/cancelled. Admin. CSP edition
        only.'
      operationId: postTenantsOnboardingrunsRetryByIdRid
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: rid
        in: path
        required: true
        schema:
          type: string
        description: rid path parameter.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingRun'
              example:
                id: 9c1e...
                tenant_id: 3e63...
                sha_id: sha-abc123
                status: pending
                step: discovery_wait
                ledger: '{"sha_id":"sha-abc123","site_id":"site-9","source_op_ids":["op-1"],"discovery_execution_id":"exec-77"}'
                attempts: 0
                created_at: '2026-07-07T09:00:00Z'
                retried_at: '2026-07-07T09:20:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /provision/seed/{token}/seed.iso:
    get:
      tags:
      - Enrollment
      summary: Serve a rendered §16.8 seed ISO behind a short-TTL one-time token (the SSVM/KVM pulls it
        during provisioning); the code-bearing bytes are dropped after the pull (D10).
      operationId: getProvisionSeedSeed.isoByToken
      security: []
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
        description: token path parameter.
      responses:
        '200':
          description: Success
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/hosts/agent/install.sh:
    get:
      tags:
      - Fleet
      summary: 'D9 host-agent one-line installer (§15.2): returns a bash script (token-gated via ?token=<install-token>;
        ?insecure=1 for a self-signed edge) that downloads the agent binary, enrolls into the SCA (redeeming
        the token), installs a systemd unit, and starts it. Unauthenticated by necessity — the one-time
        install token is the credential. Usage: curl -fsSL ''<sca>/api/v1/hosts/agent/install.sh?token=TOKEN''
        | sudo bash.'
      operationId: getHostsAgentInstall.sh
      security: []
      responses:
        '200':
          description: Success
  /api/v1/hosts/agent/binary:
    get:
      tags:
      - Fleet
      summary: Serve the baked-in D9 host-agent binary (application/octet-stream). Downloaded by the install.sh
        script; not a secret (trust is in the enroll token).
      operationId: getHostsAgentBinary
      security: []
      responses:
        '200':
          description: Success
  /api/v1/rate-cards:
    get:
      tags:
      - Billing
      summary: List rate cards (admin only). CSP edition only.
      operationId: listRateCards
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                description: 'Wrapper returned by writeJSON: a single ''rate_cards'' array, ordered by
                  effective_from descending. ADMIN-only.'
                properties:
                  rate_cards:
                    type: array
                    items:
                      schemaRef: RateCard
                required:
                - rate_cards
              example:
                rate_cards:
                - id: 7c3e0a2f-9b41-4d2e-8f1a-2b6d5c4e1a90
                  name: Standard 2026
                  currency: USD
                  license_per_vm_month: 5
                  storage_per_tb_month: 12.5
                  overage_per_vm_month: 8
                  effective_from: '2026-01-01T00:00:00Z'
                  created_at: '2026-01-15T09:30:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Billing
      summary: Create a rate card (per-VM/month, per-TB/month, currency, effective_from). CSP edition
        only.
      operationId: createRateCard
      x-required-edition: csp
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Inline request struct decoded in handleCreateRateCard. name is required and
                non-empty; rates must be non-negative; currency defaults to USD when empty; effective_from
                defaults to now when omitted/unparseable.
              properties:
                name:
                  type: string
                  description: Required, non-empty price book name.
                  example: Standard 2026
                currency:
                  type: string
                  description: ISO currency; defaults to USD when empty.
                  example: USD
                license_per_vm_month:
                  type: number
                  description: Non-negative per-VM monthly license rate.
                  example: 5
                storage_per_tb_month:
                  type: number
                  description: Non-negative per-TB monthly storage rate.
                  example: 12.5
                overage_per_vm_month:
                  type: number
                  description: Non-negative per-VM monthly overage rate.
                  example: 8
                effective_from:
                  type: string
                  format: date-time
                  description: Effective date; parsed via parseTimeParam. Defaults to server now if omitted/invalid.
                  example: '2026-01-01T00:00:00Z'
              required:
              - name
            example:
              name: Standard 2026
              currency: USD
              license_per_vm_month: 5
              storage_per_tb_month: 12.5
              overage_per_vm_month: 8
              effective_from: '2026-01-01T00:00:00Z'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateCard'
              example:
                id: 7c3e0a2f-9b41-4d2e-8f1a-2b6d5c4e1a90
                name: Standard 2026
                currency: USD
                license_per_vm_month: 5
                storage_per_tb_month: 12.5
                overage_per_vm_month: 8
                effective_from: '2026-01-01T00:00:00Z'
                created_at: '2026-01-15T09:30:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/rate-cards/{id}:
    delete:
      tags:
      - Billing
      summary: Delete a rate card. CSP edition only.
      operationId: deleteRateCard
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedStatus'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/billing/statement:
    get:
      tags:
      - Billing
      summary: DRAFT statement for a period (high-water VMs + avg TB × active rate card). CSP edition
        only.
      operationId: getTenantStatementDraft
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DraftStatement'
              example:
                tenant_id: acme-corp
                period: 2026-05
                currency: USD
                rate_card: Standard 2026
                line_items:
                - description: Protected VMs (monthly high-water)
                  quantity: 42
                  unit: VM·month
                  rate: 5
                  amount: 210
                - description: Storage (monthly average)
                  quantity: 18.4
                  unit: TB·month
                  rate: 12.5
                  amount: 230
                - description: Overage — protected VMs above the allocated quota
                  quantity: 2
                  unit: VM·month
                  rate: 8
                  amount: 16
                  quota: 40
                total: 456
                quota: 40
                basis: 'consumed: monthly high-water protected VMs + average storage TB; overage above
                  quota billable (§9.7, D4)'
                evidence_window:
                  from: '2026-05-01T00:00:00Z'
                  to: '2026-05-31T23:59:59Z'
                draft: true
                rate_overrides_applied: true
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/billing/statements:
    get:
      tags:
      - Billing
      summary: List finalized statements. CSP edition only.
      operationId: listTenantStatements
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                description: 'Wrapper returned by writeJSON: a ''statements'' array of compact summaries,
                  ordered by period descending. Scoped to the caller''s assigned tenants.'
                properties:
                  statements:
                    type: array
                    items:
                      schemaRef: StatementSummary
                required:
                - statements
              example:
                statements:
                - id: 1f8b7c6d-5e4a-3b2c-1d0e-9f8a7b6c5d4e
                  period: 2026-05
                  currency: USD
                  rate_card: Standard 2026
                  total: 456
                  finalized_by: admin@sendense.local
                  finalized_at: '2026-06-02T10:15:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Billing
      summary: Finalize a period (snapshots the computation into an immutable record; 409 if already finalized
        or no active card). CSP edition only.
      operationId: finalizeTenantStatement
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: 'Inline request struct decoded in handleFinalizeStatement: just the period
                to finalize.'
              properties:
                period:
                  type: string
                  description: Billing period to finalize, YYYY-MM.
                  example: 2026-05
              required:
              - period
            example:
              period: 2026-05
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FinalizedStatement'
              example:
                id: 1f8b7c6d-5e4a-3b2c-1d0e-9f8a7b6c5d4e
                tenant_id: acme-corp
                period: 2026-05
                currency: USD
                rate_card: Standard 2026
                rate_card_id: 7c3e0a2f-9b41-4d2e-8f1a-2b6d5c4e1a90
                line_items:
                - description: Protected VMs (monthly high-water)
                  quantity: 42
                  unit: VM·month
                  rate: 5
                  amount: 210
                - description: Storage (monthly average)
                  quantity: 18.4
                  unit: TB·month
                  rate: 12.5
                  amount: 230
                total: 440
                high_water_vms: 42
                avg_tb: 18.4
                quota: 40
                evidence_window:
                  from: '2026-05-01T00:00:00Z'
                  to: '2026-05-31T23:59:59Z'
                finalized_by: admin@sendense.local
                finalized_at: '2026-06-02T10:15:00Z'
                draft: false
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/billing/statements/{sid}:
    get:
      tags:
      - Billing
      summary: Drill into a finalized statement's line items + evidence window. CSP edition only.
      operationId: getTenantStatement
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: sid
        in: path
        required: true
        schema:
          type: string
        description: Statement id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FinalizedStatement'
              example:
                id: 1f8b7c6d-5e4a-3b2c-1d0e-9f8a7b6c5d4e
                tenant_id: acme-corp
                period: 2026-05
                currency: USD
                rate_card: Standard 2026
                rate_card_id: 7c3e0a2f-9b41-4d2e-8f1a-2b6d5c4e1a90
                line_items:
                - description: Protected VMs (monthly high-water)
                  quantity: 42
                  unit: VM·month
                  rate: 5
                  amount: 210
                - description: Storage (monthly average)
                  quantity: 18.4
                  unit: TB·month
                  rate: 12.5
                  amount: 230
                total: 440
                high_water_vms: 42
                avg_tb: 18.4
                quota: 40
                evidence_window:
                  from: '2026-05-01T00:00:00Z'
                  to: '2026-05-31T23:59:59Z'
                finalized_by: admin@sendense.local
                finalized_at: '2026-06-02T10:15:00Z'
                draft: false
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Billing
      summary: Un-finalize (pre-close redo). CSP edition only.
      operationId: deleteTenantStatement
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: sid
        in: path
        required: true
        schema:
          type: string
        description: Statement id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedStatus'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/rate-overrides:
    get:
      tags:
      - Billing
      summary: The tenant's per-tenant rate overrides (D4), 404 when none. Admin only. CSP edition only.
      operationId: getTenantRateOverrides
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantRateOverride'
              example:
                tenant_id: acme-corp
                license_per_vm_month: 4.5
                overage_per_vm_month: 6
                updated_by: admin@sendense.local
                created_at: '2026-02-01T12:00:00Z'
                updated_at: '2026-03-01T12:00:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
      - Billing
      summary: Replace the tenant's rate overrides wholesale (nil field = card rate applies). Future drafts/finalizations
        only — finalized statements never re-price. Admin only. CSP edition only.
      operationId: putTenantRateOverrides
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: 'Inline request struct decoded in handlePutRateOverrides. PUT semantics: an
                omitted/null field means ''no override'' (clears any prior value), NOT ''keep existing''.
                Each rate is a nullable pointer; if provided it must be non-negative. At least one of
                the three must be set (else 400).'
              properties:
                license_per_vm_month:
                  type: number
                  nullable: true
                  description: Override for license_per_vm_month; null/omitted clears it.
                  example: 4.5
                storage_per_tb_month:
                  type: number
                  nullable: true
                  description: Override for storage_per_tb_month; null/omitted clears it.
                  example: 10
                overage_per_vm_month:
                  type: number
                  nullable: true
                  description: Override for overage_per_vm_month; null/omitted clears it.
                  example: 6
            example:
              license_per_vm_month: 4.5
              overage_per_vm_month: 6
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantRateOverride'
              example:
                tenant_id: acme-corp
                license_per_vm_month: 4.5
                overage_per_vm_month: 6
                updated_by: admin@sendense.local
                created_at: '2026-02-01T12:00:00Z'
                updated_at: '2026-03-01T12:00:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Billing
      summary: Remove the tenant's rate overrides. Admin only. CSP edition only.
      operationId: deleteTenantRateOverrides
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletedStatus'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/license-pools:
    get:
      tags:
      - Licensing
      summary: List license pools (entitlements + derived allocated/remaining). CSP edition only.
      operationId: listLicensePools
      x-required-edition: csp
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListLicensePoolsResponse'
              example:
                pools:
                - id: 9f1c2b3a-4d5e-6f70-8a9b-0c1d2e3f4a5b
                  entitlement_id: ent-ENTERPRISE-2026
                  edition: enterprise
                  quota: 500
                  allocated: 90
                  remaining: 410
                  expiry: '2027-01-01T00:00:00Z'
                  notes: Q1 enterprise purchase
                  allocations:
                  - tenant_id: tenant-acme
                    tenant_name: Acme Corp
                    quota: 50
                    used: 37
                    state: active
                    notes: initial allocation
                  - tenant_id: tenant-globex
                    tenant_name: Globex
                    quota: 40
                    used: 12
                    state: planned
                    notes: ''
                note: Operational pool + quota planning only (spec §9.4/§9.5). Cryptographic license issuance
                  requires the sendense-licensing §9.2 partner endpoints, which are not built yet — the
                  SCA is a pool manager + viewer, never a signer.
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Licensing
      summary: Record a purchased license pool. CSP edition only.
      operationId: createLicensePool
      x-required-edition: csp
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateLicensePoolRequest'
            example:
              entitlement_id: ent-ENTERPRISE-2026
              edition: enterprise
              quota: 500
              expiry: '2027-01-01T00:00:00Z'
              notes: Q1 enterprise purchase
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePool'
              example:
                id: 9f1c2b3a-4d5e-6f70-8a9b-0c1d2e3f4a5b
                entitlement_id: ent-ENTERPRISE-2026
                edition: enterprise
                quota: 500
                expiry: '2027-01-01T00:00:00Z'
                notes: Q1 enterprise purchase
                created_at: '2026-07-02T10:15:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/license-pools/{id}:
    delete:
      tags:
      - Licensing
      summary: Delete a license pool. CSP edition only.
      operationId: deleteLicensePool
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusMessage'
              example:
                status: deleted
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/license-pools/{id}/allocations:
    post:
      tags:
      - Licensing
      summary: Allocate operational quota to a tenant (sum ≤ pool quota; §9.5 operational, NOT cryptographic).
        CSP edition only.
      operationId: allocateLicense
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AllocateLicenseRequest'
            example:
              tenant_id: tenant-acme
              quota: 50
              state: active
              notes: initial allocation
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantLicense'
              example:
                id: 1a2b3c4d-5e6f-7081-9a0b-1c2d3e4f5061
                pool_id: 9f1c2b3a-4d5e-6f70-8a9b-0c1d2e3f4a5b
                tenant_id: tenant-acme
                quota: 50
                state: active
                notes: initial allocation
                created_at: '2026-07-02T10:20:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/license-pools/{id}/allocations/{tenantId}:
    delete:
      tags:
      - Licensing
      summary: Reclaim a tenant's allocation. CSP edition only.
      operationId: reclaimLicense
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: tenantId
        in: path
        required: true
        schema:
          type: string
        description: Tenant id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusMessage'
              example:
                status: reclaimed
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/license:
    get:
      tags:
      - Licensing
      summary: A tenant's allocations + measured utilization (30-day high-water). CSP edition only.
      operationId: getTenantLicense
      x-required-edition: csp
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantLicenseResponse'
              example:
                tenant_id: tenant-acme
                allocations:
                - pool_id: 9f1c2b3a-4d5e-6f70-8a9b-0c1d2e3f4a5b
                  edition: enterprise
                  quota: 50
                  state: active
                  notes: initial allocation
                total_quota: 50
                used: 37
                utilization: 74
                basis: used = 30-day high-water protected VMs (usage_snapshots); quota = operational (§9.5)
                note: Operational pool + quota planning only (spec §9.4/§9.5). Cryptographic license issuance
                  requires the sendense-licensing §9.2 partner endpoints, which are not built yet — the
                  SCA is a pool manager + viewer, never a signer.
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not permitted (edition/scope)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/fleet/capacity:
    get:
      tags:
      - Fleet
      summary: Licensed-scale capacity (enrolled vs SCA_LICENSED_MAX_SHAS).
      operationId: getFleetCapacity
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/license:
    get:
      tags:
      - Licensing
      summary: 'Active product-license status (design §7.6/D5): licensed flag, status, the derived SCA
        edition (csp|msp) + whether it came from the license or the interim SCA_EDITION, VM entitlement
        (max_protected_vms — the G23 CSP pool capacity), and validity window. Edition-neutral.'
      operationId: getLicense
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                description: Product-license status + derived edition (design §7.6/D5).
                properties:
                  licensed:
                    type: boolean
                  status:
                    type: string
                    description: ACTIVE | GRACE | EXPIRED | UNLICENSED …
                  edition:
                    type: string
                    description: 'The SCA''s active edition: csp | msp.'
                  edition_from_license:
                    type: boolean
                    description: true = derived from the applied license SKU; false = the interim SCA_EDITION.
                  license_id:
                    type: string
                  license_type:
                    type: string
                  sku_edition:
                    type: string
                    description: The edition field on the license Document.
                  max_protected_vms:
                    type: integer
                    description: VM entitlement — the CSP's pool capacity (G23).
                  current_protected_vms:
                    type: integer
                  over_limit:
                    type: boolean
                  starts_at:
                    type: string
                  expires_at:
                    type: string
                  grace_until:
                    type: string
                  support_until:
                    type: string
                  sca_eligible:
                    type: boolean
                    description: true = partner sales channel AND an SCA-capable product tier (Enterprise
                      / Enterprise Plus).
                  partner_channel:
                    type: string
                    description: csp | msp | empty. Derived ONLY from the signed Document's sales_channel
                      (CSP_SERVICE / CSP_MARKETPLACE → csp, MSP_RESALE → msp) — never from the channel-neutral
                      SKU code.
                  sales_channel:
                    type: string
                    description: The raw sales channel on the license Document.
                  eligibility_reason:
                    type: string
                    description: Human-readable reason when the license is not SCA-partner-eligible.
              example:
                licensed: true
                status: ACTIVE
                edition: csp
                edition_from_license: true
                license_id: 5d4476b6-6255-44ae-96a8-def217deaed7
                license_type: CSP
                sku_edition: csp
                max_protected_vms: 100
                current_protected_vms: 12
                over_limit: false
                starts_at: '2026-07-06T00:00:00Z'
                expires_at: '2026-07-20T21:21:00Z'
                grace_until: ''
                support_until: ''
                sca_eligible: true
                partner_channel: csp
                sales_channel: CSP_SERVICE
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/license/activate:
    post:
      tags:
      - Licensing
      summary: 'Activate an MSP/CSP product license (design §7.6/D5): apply an activation token → central
        sendense-licensing verifies + returns the Ed25519-signed Document → the SCA verifies locally,
        persists, and DERIVES the edition from the SKU, flipping the CSP gate live. The SCA is never a
        signer. Admin. 400 on a missing token; 502 when central rejects/fails the token.'
      operationId: postLicenseActivate
      security:
      - staffAuth: *id001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - activation_token
              properties:
                activation_token:
                  type: string
                  description: The one-time activation token from the license (issued by central sendense-licensing).
            example:
              activation_token: CVt1uyK5JRi0F1pfRZzjYnRYnVFhaHSv1TOr-cwrAA0
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                description: Product-license status + derived edition (design §7.6/D5).
                properties:
                  licensed:
                    type: boolean
                  status:
                    type: string
                    description: ACTIVE | GRACE | EXPIRED | UNLICENSED …
                  edition:
                    type: string
                    description: 'The SCA''s active edition: csp | msp.'
                  edition_from_license:
                    type: boolean
                    description: true = derived from the applied license SKU; false = the interim SCA_EDITION.
                  license_id:
                    type: string
                  license_type:
                    type: string
                  sku_edition:
                    type: string
                    description: The edition field on the license Document.
                  max_protected_vms:
                    type: integer
                    description: VM entitlement — the CSP's pool capacity (G23).
                  current_protected_vms:
                    type: integer
                  over_limit:
                    type: boolean
                  starts_at:
                    type: string
                  expires_at:
                    type: string
                  grace_until:
                    type: string
                  support_until:
                    type: string
                  sca_eligible:
                    type: boolean
                    description: true = partner sales channel AND an SCA-capable product tier (Enterprise
                      / Enterprise Plus).
                  partner_channel:
                    type: string
                    description: csp | msp | empty. Derived ONLY from the signed Document's sales_channel
                      (CSP_SERVICE / CSP_MARKETPLACE → csp, MSP_RESALE → msp) — never from the channel-neutral
                      SKU code.
                  sales_channel:
                    type: string
                    description: The raw sales channel on the license Document.
                  eligibility_reason:
                    type: string
                    description: Human-readable reason when the license is not SCA-partner-eligible.
              example:
                licensed: true
                status: ACTIVE
                edition: csp
                edition_from_license: true
                license_id: 5d4476b6-6255-44ae-96a8-def217deaed7
                license_type: CSP
                sku_edition: csp
                max_protected_vms: 100
                current_protected_vms: 12
                over_limit: false
                starts_at: '2026-07-06T00:00:00Z'
                expires_at: '2026-07-20T21:21:00Z'
                grace_until: ''
                support_until: ''
                sca_eligible: true
                partner_channel: csp
                sales_channel: CSP_SERVICE
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Service unavailable — a required dependency (database, vault, tunnel) is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/fleet/health:
    get:
      tags:
      - Fleet
      summary: Fleet health rollup (scoped).
      operationId: getFleetHealth
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/fleet/shas/{id}:
    get:
      tags:
      - Fleet
      summary: Rich SHA detail (current state minus secrets + owning tenants + connection telemetry).
      operationId: getFleetShasById
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/fleet/shas/{id}/usage:
    get:
      tags:
      - Fleet
      summary: A SHA's usage time series (tenant-filtered for non-admins).
      operationId: getFleetShasUsageById
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/fleet/alerts:
    get:
      tags:
      - Fleet
      summary: Fleet alerts (scoped).
      operationId: getFleetAlerts
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/alerts/{id}/ack:
    post:
      tags:
      - Fleet
      summary: Acknowledge an alert.
      operationId: postAlertsAckById
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/alerts/{id}/snooze:
    post:
      tags:
      - Fleet
      summary: Snooze an alert.
      operationId: snoozeAlert
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SnoozeAlertRequest'
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/alerts/{id}/resolve:
    post:
      tags:
      - Fleet
      summary: Resolve an alert.
      operationId: postAlertsResolveById
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/operations:
    get:
      tags:
      - Operations
      summary: List delegated operations (scoped).
      operationId: listOperations
      security:
      - staffAuth: *id001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationListResponse'
              example:
                operations:
                - id: op-55
                  tenant_id: 3e63...
                  sha_id: sha-abc123
                  msp_user: admin@sendense.local
                  action: backup.run
                  target_ref: '{"vm_name":"QUAD-FA-01"}'
                  status: running
                  sha_job_id: backup-20260707-1
                  created_at: '2026-07-07T09:05:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/operations/{op_id}:
    get:
      tags:
      - Operations
      summary: Get one delegated operation (reconciled from the SHA on read).
      operationId: getOperation
      security:
      - staffAuth: *id001
      parameters:
      - name: op_id
        in: path
        required: true
        schema:
          type: string
        description: Operation id.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteOperation'
              example:
                id: op-55
                tenant_id: 3e63...
                sha_id: sha-abc123
                msp_user: admin@sendense.local
                action: backup.run
                target_ref: '{"vm_name":"QUAD-FA-01"}'
                status: succeeded
                sha_job_id: backup-20260707-1
                created_at: '2026-07-07T09:05:00Z'
                completed_at: '2026-07-07T09:12:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/shas/{sha}/operations:
    post:
      tags:
      - Operations
      summary: Dispatch a delegated WRITE (backup/restore/replication/failover) to a tenant SHA. The SHA
        grant-checks the action (msp_access_grants) — staff on-behalf-of only. Idempotent.
      operationId: createDelegatedOperation
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: sha
        in: path
        required: true
        schema:
          type: string
        description: SHA id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOperationRequest'
            example:
              action: backup.run
              target:
                vm_name: QUAD-FA-01
              idempotency_key: csp-req-8f2a
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteOperation'
              example:
                id: op-55
                tenant_id: 3e63...
                sha_id: sha-abc123
                msp_user: admin@sendense.local
                action: backup.run
                target_ref: '{"vm_name":"QUAD-FA-01"}'
                status: running
                sha_job_id: backup-20260707-1
                created_at: '2026-07-07T09:05:00Z'
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden — edition, scope, tenant status, or feature gate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/shas/{sha}/proxy/{shaPath}:
    get:
      tags:
      - Read-through
      summary: 'Operator read-through mirror: forward a GET to a tenant SHA''s /api/v1/{shaPath} (allowlisted),
        scoped to the operator''s tenants.'
      operationId: getTenantsShasProxyByIdShaShapath
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: sha
        in: path
        required: true
        schema:
          type: string
        description: SHA id.
      - name: shaPath
        in: path
        required: true
        schema:
          type: string
        description: The SHA API sub-path (e.g. vm-contexts, backups). Allowlisted.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tenants/{id}/shas/{sha}/{resource}:
    get:
      tags:
      - Read-through
      summary: Convenience read-through for common SHA resources (vms→vm-contexts, jobs→backups).
      operationId: getTenantsShasByIdShaResource
      security:
      - staffAuth: *id001
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Resource id.
      - name: sha
        in: path
        required: true
        schema:
          type: string
        description: SHA id.
      - name: resource
        in: path
        required: true
        schema:
          type: string
        description: Read-through resource alias.
      responses:
        '200':
          description: Success
        '401':
          description: Missing/invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Bad gateway — the appliance did not serve the forwarded request (tunnel down or
            path not served)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/sca/token-exchange:
    post:
      tags:
      - SHA-side (called by the SCA)
      summary: Mint a short-lived, role-scoped tenant session token. Gated by token.exchange (reach) +
        role.TenantAssignable (the real gate). Used by the CSP tenant front door.
      operationId: sha_postScaTokenexchange
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
  /api/v1/sca/operations:
    post:
      tags:
      - SHA-side (called by the SCA)
      summary: Execute a delegated MSP operation; the handler enforces msp_access_grants per action. The
        enforcement point for staff-dispatched writes.
      operationId: sha_postScaOperations
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
  /api/v1/sca/operations/{remote_op_id}:
    get:
      tags:
      - SHA-side (called by the SCA)
      summary: Status of a delegated operation (for SCA reconciliation).
      operationId: sha_getScaOperationsByRemoteopid
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      parameters:
      - name: remote_op_id
        in: path
        required: true
        description: Remote operation id.
        schema:
          type: string
      responses:
        '200':
          description: Success
  /api/v1/sca/usage:
    get:
      tags:
      - SHA-side (called by the SCA)
      summary: Lean usage snapshot for the §6.4 heartbeat (inventory.read).
      operationId: sha_getScaUsage
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
  /api/v1/sca/grant:
    get:
      tags:
      - SHA-side (called by the SCA)
      summary: The resolved effective grant (allow-list) for the heartbeat.
      operationId: sha_getScaGrant
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
  /api/v1/sca/access-grants:
    get:
      tags:
      - SHA-side (called by the SCA)
      summary: Read the customer grant config (LOCAL SHA admin only; sca-service denied).
      operationId: sha_getScaAccessgrants
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
    put:
      tags:
      - SHA-side (called by the SCA)
      summary: Set the customer grant tier/overrides (LOCAL SHA admin only).
      operationId: sha_putScaAccessgrants
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
  /api/v1/sca/approvals:
    get:
      tags:
      - SHA-side (called by the SCA)
      summary: List operations pending local approval (LOCAL admin).
      operationId: sha_getScaApprovals
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
  /api/v1/sca/approvals/{id}/approve:
    post:
      tags:
      - SHA-side (called by the SCA)
      summary: Approve a pending delegated operation (step-up reauth).
      operationId: sha_postScaApprovalsApproveById
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      parameters:
      - name: id
        in: path
        required: true
        description: Resource id.
        schema:
          type: string
      responses:
        '200':
          description: Success
  /api/v1/sca/approvals/{id}/deny:
    post:
      tags:
      - SHA-side (called by the SCA)
      summary: Deny a pending delegated operation.
      operationId: sha_postScaApprovalsDenyById
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      parameters:
      - name: id
        in: path
        required: true
        description: Resource id.
        schema:
          type: string
      responses:
        '200':
          description: Success
  /api/v1/sca/activity:
    get:
      tags:
      - SHA-side (called by the SCA)
      summary: MSP-action activity log (LOCAL admin).
      operationId: sha_getScaActivity
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
  /api/v1/sca/notifications:
    get:
      tags:
      - SHA-side (called by the SCA)
      summary: Operator notification feed (notify_on_msp_action).
      operationId: sha_getScaNotifications
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      responses:
        '200':
          description: Success
  /api/v1/sca/notifications/{id}/ack:
    post:
      tags:
      - SHA-side (called by the SCA)
      summary: Acknowledge a notification.
      operationId: sha_postScaNotificationsAckById
      x-served-by: tenant-sha
      security:
      - scaServiceToken: []
      parameters:
      - name: id
        in: path
        required: true
        description: Resource id.
        schema:
          type: string
      responses:
        '200':
          description: Success
components:
  securitySchemes:
    staffAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: MSP/CSP staff-realm JWT (from /api/v1/auth/login).
    tenantAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: End-tenant-realm JWT (from /api/v1/tenant/auth/login). Distinct signing key from staffAuth.
    scaServiceToken:
      type: http
      scheme: bearer
      description: Per-relationship sca-service bearer the SHA mints for the SCA at enrollment (SHA-side
        endpoints only).
  schemas:
    LoginResponse:
      type: object
      description: Successful MSP staff login. The user object carries only id + email (roles/permissions
        are fetched separately via GET /auth/me).
      properties:
        access_token:
          type: string
          description: 'Short-lived JWT bearer used in the Authorization: Bearer header for subsequent
            SCA API calls.'
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJ1c3ItOWYzYSIsImVtYWlsIjoiYWRtaW5Ac2VuZGVuc2UubG9jYWwiLCJyb2xlcyI6WyJhZG1pbiJdfQ.sig
        refresh_token:
          type: string
          description: Opaque long-lived refresh token; exchange at POST /auth/refresh for a new access
            token. Store securely.
          example: rt_c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
        expires_at:
          type: string
          format: date-time
          description: Absolute expiry of the access_token (RFC3339). Refresh before this time.
          example: '2026-07-02T15:40:12Z'
        user:
          type: object
          description: Minimal identity of the authenticated MSP user.
          properties:
            id:
              type: string
              description: MSP user id.
              example: usr-9f3a
            email:
              type: string
              format: email
              example: admin@sendense.local
          required:
          - id
          - email
      required:
      - access_token
      - refresh_token
      - expires_at
      - user
    RefreshResponse:
      type: object
      description: 'New token pair minted from a valid refresh token. Note: no user object is returned
        here (unlike login).'
      properties:
        access_token:
          type: string
          description: Newly issued access JWT.
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.new.sig
        refresh_token:
          type: string
          description: Rotated refresh token; the previous one is consumed. Replace your stored copy with
            this value.
          example: rt_a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4
        expires_at:
          type: string
          format: date-time
          description: Absolute expiry of the new access_token (RFC3339).
          example: '2026-07-02T15:55:12Z'
      required:
      - access_token
      - refresh_token
      - expires_at
    MeResponse:
      type: object
      description: The authenticated MSP staff user's identity and effective authorization, derived from
        the validated access token.
      properties:
        user_id:
          type: string
          description: MSP user id (from the token's uid claim).
          example: usr-9f3a
        email:
          type: string
          format: email
          example: admin@sendense.local
        roles:
          type: array
          description: Role names assigned to the user. 'admin' grants fleet-wide MSP privileges (user
            management, all tenants); 'staff' is scoped to assigned tenants.
          items:
            type: string
          example:
          - admin
        permissions:
          type: array
          description: Flattened effective permission strings the user holds via their roles.
          items:
            type: string
          example:
          - tenants.read
          - tenants.write
          - users.manage
      required:
      - user_id
      - email
      - roles
      - permissions
    StatusResponse:
      type: object
      description: Generic single-field status acknowledgement returned by logout and tenant assignment/removal
        endpoints.
      properties:
        status:
          type: string
          description: 'Outcome keyword. Values observed: ''logged_out'' (logout), ''deleted'' (user delete),
            ''assigned'' (tenant assign), ''unassigned'' (tenant unassign).'
          example: logged_out
      required:
      - status
    MspUser:
      type: object
      description: An MSP staff user as returned in the admin user list. Never includes the password hash.
      properties:
        id:
          type: string
          description: MSP user id.
          example: usr-4b2c
        email:
          type: string
          format: email
          example: jane.doe@sendense.local
        full_name:
          type: string
          description: Display name. Omitted from JSON when empty (omitempty).
          example: Jane Doe
        status:
          type: string
          description: Account status (e.g. 'active', 'disabled').
          example: active
        roles:
          type: array
          description: Role names assigned to the user.
          items:
            type: string
          example:
          - staff
        is_admin:
          type: boolean
          description: 'Convenience flag: true if ''admin'' is among roles (fleet-wide privileges).'
          example: false
        tenant_ids:
          type: array
          description: Tenant ids this staff user is scoped to. Empty array for admins or unassigned staff.
          items:
            type: string
          example:
          - tnt-acme
          - tnt-globex
        created_at:
          type: string
          format: date-time
          description: User creation timestamp (RFC3339).
          example: '2026-05-14T09:12:00Z'
      required:
      - id
      - email
      - status
      - roles
      - is_admin
      - tenant_ids
      - created_at
    UserListResponse:
      type: object
      description: Wrapper for the admin MSP-user listing.
      properties:
        users:
          type: array
          items:
            $ref: '#/components/schemas/MspUser'
      required:
      - users
    CreatedUserResponse:
      type: object
      description: Minimal identity of a newly created MSP user (returned with HTTP 201).
      properties:
        id:
          type: string
          example: usr-7d1e
        email:
          type: string
          format: email
          example: new.staff@sendense.local
      required:
      - id
      - email
    ErrorResponse:
      type: object
      description: 'Uniform error envelope (writeError → {"error": msg}) used for all non-2xx responses.'
      properties:
        error:
          type: string
          example: tenant not found
      required:
      - error
    TenantUser:
      type: object
      description: An SCA-local end-tenant login identity for the CSP front door (design 6.2). Distinct
        from MSP staff users. The bcrypt password hash is never serialized (json:"-"). List rows are ordered
        by email.
      properties:
        id:
          type: string
          description: UUID primary key of the tenant user.
          example: 3f2c1b7a-90de-4a11-8c33-0a1b2c3d4e5f
        tenant_id:
          type: string
          description: Owning tenant id. Unique together with email.
          example: acme-corp
        email:
          type: string
          format: email
          description: Login email (normalized/lowercased). Unique per tenant.
          example: ops@acme.example.com
        sha_role:
          type: string
          description: Role this user is granted on their SHA (design 6.4). SCA owns assignment; SHA owns
            the role definition/enforcement. Carried into the SHA at proxy time via token-exchange.
          example: backup-operator
        status:
          type: string
          description: Account status. Only 'active' users may log in.
          example: active
        created_at:
          type: string
          format: date-time
          example: '2026-06-01T09:15:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-06-01T09:15:00Z'
      required:
      - id
      - tenant_id
      - email
      - sha_role
      - status
      - created_at
      - updated_at
    TenantIdPConfig:
      type: object
      description: A tenant's identity-federation config for the CSP front door (design 6.2). No secrets
        are stored (all fields are public OIDC metadata). Used to validate an OIDC id_token minted by
        the CSP IdP and issue a tenant-realm session. This is both the PUT request body and the GET/PUT
        response body.
      properties:
        id:
          type: string
          description: UUID primary key. Server-assigned; ignored/overwritten on Set.
          example: b21e77a0-5c44-4d0e-9a2b-77c9f0e1a234
        tenant_id:
          type: string
          description: Owning tenant id (unique - one IdP config per tenant). On PUT this is set from
            the path {id}.
          example: acme-corp
        protocol:
          type: string
          description: Federation protocol. Defaults to 'oidc' (saml is future).
          example: oidc
        issuer:
          type: string
          description: Expected id_token 'iss' claim (OIDC issuer URL).
          example: https://login.acme.example.com/
        client_id:
          type: string
          description: Expected id_token 'aud' claim (OIDC client id).
          example: sendense-portal
        jwks_uri:
          type: string
          format: uri
          description: IdP signing-key (JWKS) endpoint used to verify the id_token signature.
          example: https://login.acme.example.com/.well-known/jwks.json
        email_claim:
          type: string
          description: id_token claim to read the user's email from. Defaults to 'email'.
          example: email
        default_sha_role:
          type: string
          description: 'SHA role (job sheet CSP-W2) granted to every tenant user who authenticates via
            this IdP. MVP: one role per tenant IdP config.'
          example: backup-operator
        enabled:
          type: boolean
          description: Whether federated login is enabled for this tenant. Defaults to false; the callback
            returns 403 if disabled.
          example: true
        created_at:
          type: string
          format: date-time
          example: '2026-06-01T09:15:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-06-01T09:15:00Z'
      required:
      - id
      - tenant_id
      - protocol
      - issuer
      - client_id
      - jwks_uri
      - email_claim
      - default_sha_role
      - enabled
      - created_at
      - updated_at
    TenantSession:
      type: object
      description: The tenant-realm session issued by a successful tenant login or federated callback.
        The access_token is a tenant-realm JWT (HS256) bound to the tenant_id; it is NOT a staff token
        and can never act as staff. Present it as a Bearer token to the /tenant/* front-door routes.
      properties:
        access_token:
          type: string
          description: 'Tenant-realm bearer token. Send as ''Authorization: Bearer <token>''.'
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbnRfaWQiOiJhY21lLWNvcnAifQ.sig
        tenant_id:
          type: string
          description: Tenant the session is bound to.
          example: acme-corp
        email:
          type: string
          format: email
          description: Authenticated user's email.
          example: ops@acme.example.com
      required:
      - access_token
      - tenant_id
      - email
    TenantMe:
      type: object
      description: The authenticated tenant caller derived from the tenant-realm bearer token. tenant_id
        comes ONLY from the token (never a param). sha_role is the caller's own exchanged SHA role, advisory
        for GUI rendering; the SHA's RBAC remains the real gate.
      properties:
        tenant_id:
          type: string
          example: acme-corp
        email:
          type: string
          format: email
          example: ops@acme.example.com
        user_id:
          type: string
          description: Subject id of the session. For federated sessions this is 'oidc:<subject>'.
          example: 3f2c1b7a-90de-4a11-8c33-0a1b2c3d4e5f
        sha_role:
          type: string
          description: Caller's own SHA role (advisory).
          example: backup-operator
      required:
      - tenant_id
      - email
      - user_id
      - sha_role
    TenantUserCreated:
      type: object
      description: Minimal identity echo returned on tenant-user creation (201).
      properties:
        id:
          type: string
          example: 3f2c1b7a-90de-4a11-8c33-0a1b2c3d4e5f
        tenant_id:
          type: string
          example: acme-corp
        email:
          type: string
          format: email
          example: ops@acme.example.com
      required:
      - id
      - tenant_id
      - email
    ProxyResponse:
      type: object
      description: OPAQUE passthrough. The front door resolves the tenant's own SHA (1:1 silo) from the
        session token, exchanges a role-scoped SHA token, and relays the SHA's response VERBATIM (already
        JSON) with the SHA's status code. The shape is whatever the targeted SHA endpoint (identified
        by {shaPath}) returns - it is NOT owned or defined by the SCA. Treat as an arbitrary JSON object/array.
        Front-door-level failures are returned as an ErrorResponse instead.
      additionalProperties: true
    InitiateEnrollmentRequest:
      type: object
      description: Payload a SHA sends to begin enrollment into the SCA. It presents the pairing code
        it was given and its dedicated Ed25519 public key (the identity the tunnel + all later signature
        auth is bound to).
      properties:
        pairing_code:
          type: string
          description: The tenant-bound pairing code minted by the MSP admin.
          example: 7F3K-9Q2M-XR4T
        appliance_name:
          type: string
          description: Human-readable SHA name.
          example: acme-sha-prod-01
        appliance_hostname:
          type: string
          description: SHA hostname.
          example: sha-prod-01.acme.internal
        appliance_version:
          type: string
          description: SHA software version.
          example: 4.3.520
        public_key_ed25519:
          type: string
          description: The SHA's dedicated Ed25519 public key in SSH authorized_keys wire form ('ssh-ed25519
            <base64> <name>'). Used for the challenge, the tunnel authorized_keys, and all later signature
            auth (heartbeat, read-credential).
          example: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH0k... sha-prod-01
        hardware_fingerprint:
          type: string
          description: Stable hardware fingerprint captured at enrollment.
          example: hw:9f2a1c7e4b
        mac_address:
          type: string
          description: Optional MAC address (used for SNA VM MOID discovery; unused for SHA).
          example: 52:54:00:ab:cd:ef
        current_ip:
          type: string
          description: Optional current IP for dynamic IP tracking (separate from name).
          example: 10.3.2.32
      required:
      - pairing_code
      - appliance_name
      - public_key_ed25519
    InitiateEnrollmentResponse:
      type: object
      description: Server response opening an enrollment session. Returns the non-enumerable enrollment_id
        handle and the random challenge the SHA must sign to prove possession of its private key.
      properties:
        enrollment_id:
          type: string
          description: Server-minted, non-enumerable handle scoping this enrollment session. Used in the
            /verify and /status paths.
          example: 3b1e9c2a-8f4d-4a77-9c11-6d2e0f5b7a90
        challenge:
          type: string
          description: Base64 random nonce the SHA must sign with its enrollment private key and submit
            to /verify.
          example: Zk8xY2p3b3JkY2hhbGxlbmdlbm9uY2U=
        expires_at:
          type: string
          format: date-time
          description: When this enrollment session (and challenge) expires.
          example: '2026-07-02T15:30:00Z'
        status:
          type: string
          description: Session status after initiation.
          example: challenge_sent
      required:
      - enrollment_id
      - challenge
      - expires_at
      - status
    GeneratePairingCodeRequest:
      type: object
      description: 'MSP admin request to mint a tenant-bound pairing code with a proposed access tier.
        NOTE: generated_by is set server-side from the authenticated caller and is ignored if sent.'
      properties:
        tenant_id:
          type: string
          description: Tenant this SHA will be siloed to (required). Must reference an existing tenant.
          example: tenant_acme
        proposed_access_tier:
          type: string
          enum:
          - billing_only
          - monitor
          - backup
          - full_ops
          description: Access tier proposed to the enrolling SHA (consent step). Defaults to 'monitor'
            if empty.
          example: backup
        expires_in_minutes:
          type: integer
          nullable: true
          description: Optional code lifetime in minutes; server default applied if omitted.
          example: 60
        notes:
          type: string
          nullable: true
          description: Optional free-text note stored on the pairing code.
          example: Prod SHA for Acme datacenter
      required:
      - tenant_id
    GeneratePairingCodeResponse:
      type: object
      description: The minted pairing code echoed back with the tenant/tier it is bound to.
      properties:
        code:
          type: string
          description: The pairing code the operator hands to the SHA.
          example: 7F3K-9Q2M-XR4T
        expires_at:
          type: string
          format: date-time
          description: Code expiry.
          example: '2026-07-02T15:30:00Z'
        tenant_id:
          type: string
          description: Tenant the code is bound to.
          example: tenant_acme
        proposed_access_tier:
          type: string
          enum:
          - billing_only
          - monitor
          - backup
          - full_ops
          description: Access tier bound to the code.
          example: backup
        pairing_code_id:
          type: string
          description: The stored pairing_codes row id (used to record the D8 expected IP for an SNA).
      required:
      - code
      - expires_at
      - tenant_id
      - proposed_access_tier
    EnrollmentStatusResponse:
      type: object
      description: Poll-able status the enrolling SHA reads. Before approval only status (+ proposed tier)
        is set; once approved it carries the allocated tunnel identity the SHA needs to bring up its reverse
        tunnel.
      properties:
        status:
          type: string
          enum:
          - initiated
          - challenge_sent
          - challenge_verified
          - approved
          - rejected
          - expired
          - cancelled
          description: Current enrollment session status.
          example: approved
        proposed_access_tier:
          type: string
          enum:
          - billing_only
          - monitor
          - backup
          - full_ops
          description: Access tier proposed for local consent. Omitted if no SCA pairing metadata is found.
          example: backup
        appliance_id:
          type: string
          description: Allocated SHA id (only present once approved).
          example: c4f7a2b9-1e3d-4c56-8a90-2b1f6e7d0c34
        ssh_user:
          type: string
          description: Restricted tunnel user provisioned for this SHA (sha_tunnel_NNN); present once
            approved.
          example: sha_tunnel_007
        reverse_api_port:
          type: integer
          description: SCA->SHA reverse API loopback port (base 20000); present once approved.
          example: 20007
        diag_port:
          type: integer
          description: Optional remote-diag port (base 29000); present once approved and if allocated.
          example: 29007
      required:
      - status
    HeartbeatRequest:
      type: object
      description: §6.4 rollup payload the SHA publisher POSTs. All fields optional/omitempty; the body
        is signed via the X-SCA-Signature / X-SCA-Timestamp headers over (appliance_id + timestamp + sha256(body)).
        A 'snapshot:true' beat also appends a usage_snapshots time-series row per tenant.
      properties:
        version:
          type: string
          description: SHA software version (updates the stored value if non-empty).
          example: 4.3.520
        tenancy_mode:
          type: string
          enum:
          - dedicated
          - shared
          description: Current tenancy mode.
          example: dedicated
        vm_count:
          type: integer
          description: Total VMs known to the SHA.
          example: 142
        protected_vm_count:
          type: integer
          description: VMs with an active protection policy.
          example: 138
        storage_used_gb:
          type: number
          description: Backup storage consumed, in GB.
          example: 48213.5
        sna_count:
          type: integer
          description: SNAs attached to this SHA.
          example: 4
        sna_healthy:
          type: integer
          description: Healthy SNAs.
          example: 4
        failed_jobs_24h:
          type: integer
          description: Backup/replication jobs failed in the last 24h.
          example: 2
        dr_readiness_pct:
          type: number
          description: DR readiness percentage.
          example: 97.5
        last_backup_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the most recent successful backup.
          example: '2026-07-02T13:05:00Z'
        snapshot:
          type: boolean
          description: If true, this beat also writes a usage_snapshots time-series row per associated
            tenant (the 15-min metering rollup).
          example: true
        access_tier:
          type: string
          description: '§7 grant sync: the SHA''s currently resolved access tier.'
          example: backup
        granted_actions:
          type: array
          items:
            type: string
          description: '§7 grant sync: advisory allow-list of delegated action keys the SHA currently
            permits. Cached on every tenancy of this SHA. nil (omitted) = leave cache as-is.'
          example:
          - backup.run
          - backup.read
          - restore.file
        per_repository:
          type: array
          description: §18.4 / CSP-W7 consumed-only per-repo storage-billing basis (D11); present on snapshot
            beats.
          items:
            type: object
            additionalProperties: true
    EnrollmentSession:
      type: object
      description: An enrollment session row as returned in the pending-approval queue.
      properties:
        id:
          type: string
          description: Enrollment session id.
          example: 3b1e9c2a-8f4d-4a77-9c11-6d2e0f5b7a90
        pairing_code_id:
          type: string
          description: Id of the pairing code used.
          example: pc_8821
        appliance_name:
          type: string
          description: SHA name submitted at enrollment.
          example: acme-sha-prod-01
        appliance_hostname:
          type: string
          nullable: true
          description: SHA hostname.
          example: sha-prod-01.acme.internal
        appliance_type:
          type: string
          description: Appliance type (always 'SHA' for the SCA).
          example: SHA
        appliance_version:
          type: string
          nullable: true
          description: SHA version.
          example: 4.3.520
        source_ip:
          type: string
          nullable: true
          description: IP the enrollment initiation came from.
          example: 10.3.2.32
        mac_address:
          type: string
          nullable: true
          description: MAC address (SNA-only; usually null for SHA).
          example: null
        public_key_ed25519:
          type: string
          description: The SHA's submitted Ed25519 public key.
          example: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH0k... sha-prod-01
        key_fingerprint:
          type: string
          description: Fingerprint of the submitted key.
          example: SHA256:9f2a1c7e4b...
        hardware_fingerprint:
          type: string
          nullable: true
          description: Hardware fingerprint.
          example: hw:9f2a1c7e4b
        challenge_nonce:
          type: string
          nullable: true
          description: The issued challenge nonce.
          example: Zk8xY2p3b3JkY2hhbGxlbmdlbm9uY2U=
        challenge_signature:
          type: string
          nullable: true
          description: The SHA's submitted signature over the challenge.
          example: MEUCIQD...
        challenge_verified:
          type: boolean
          description: Whether the challenge was verified.
          example: true
        status:
          type: string
          enum:
          - initiated
          - challenge_sent
          - challenge_verified
          - approved
          - rejected
          - expired
          - cancelled
          description: Session status. Pending queue entries are typically 'challenge_verified'.
          example: challenge_verified
        expires_at:
          type: string
          format: date-time
          description: Session expiry.
          example: '2026-07-02T15:30:00Z'
        reviewed_by:
          type: string
          nullable: true
          description: Email of the MSP admin who reviewed.
          example: null
        reviewed_at:
          type: string
          format: date-time
          nullable: true
          description: Review timestamp.
          example: null
        review_notes:
          type: string
          nullable: true
          description: Reviewer notes.
          example: null
        rejection_reason:
          type: string
          nullable: true
          description: Reason if rejected.
          example: null
        created_appliance_id:
          type: string
          nullable: true
          description: Id of the SHA created on approval.
          example: null
        created_at:
          type: string
          format: date-time
          example: '2026-07-02T14:31:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-07-02T14:31:05Z'
      required:
      - id
      - pairing_code_id
      - appliance_name
      - appliance_type
      - public_key_ed25519
      - key_fingerprint
      - challenge_verified
      - status
      - expires_at
      - created_at
      - updated_at
    SHA:
      type: object
      description: An enrolled SHA connection record (the `shas` table). Returned by approve. Secret fields
        (public_key_ed25519, read_service_token) are json:"-" and NEVER serialized.
      properties:
        id:
          type: string
          description: SHA id.
          example: c4f7a2b9-1e3d-4c56-8a90-2b1f6e7d0c34
        name:
          type: string
          example: acme-sha-prod-01
        hostname:
          type: string
          example: sha-prod-01.acme.internal
        version:
          type: string
          example: 4.3.520
        administered_by:
          type: string
          enum:
          - tenant
          - partner
          description: 'Federation model: tenant-administered (T1) vs partner-administered (T2/T3).'
          example: tenant
        key_fingerprint:
          type: string
          description: Fingerprint of the enrollment public key.
          example: SHA256:9f2a1c7e4b...
        hardware_fingerprint:
          type: string
          description: Hardware fingerprint.
          example: hw:9f2a1c7e4b
        ip_address:
          type: string
          description: Last-known IP.
          example: 10.3.2.32
        tunnel_slot:
          type: integer
          nullable: true
          description: Allocated tunnel slot (unique). Nulled on revoke.
          example: 7
        ssh_user:
          type: string
          description: Restricted tunnel user (sha_tunnel_NNN).
          example: sha_tunnel_007
        reverse_api_port:
          type: integer
          description: SCA->SHA API loopback port (base 20000).
          example: 20007
        diag_port:
          type: integer
          nullable: true
          description: Optional remote-diag port (base 29000); off by default.
          example: 29007
        enrollment_session_id:
          type: string
          description: Back-link to the enrollment_sessions row (audit trail).
          example: 3b1e9c2a-8f4d-4a77-9c11-6d2e0f5b7a90
        tenancy_mode:
          type: string
          enum:
          - dedicated
          - shared
          description: Current tenancy mode.
          example: dedicated
        vm_count:
          type: integer
          example: 0
        protected_vm_count:
          type: integer
          example: 0
        storage_used_gb:
          type: number
          example: 0
        sna_count:
          type: integer
          example: 0
        sna_healthy:
          type: integer
          example: 0
        failed_jobs_24h:
          type: integer
          example: 0
        dr_readiness_pct:
          type: number
          example: 0
        last_backup_at:
          type: string
          format: date-time
          nullable: true
          example: null
        status:
          type: string
          enum:
          - pending
          - online
          - stale
          - offline
          - revoked
          description: Lifecycle status. Starts 'pending' after approve; flips 'online' on first heartbeat.
          example: pending
        last_heartbeat_at:
          type: string
          format: date-time
          nullable: true
          example: null
        created_at:
          type: string
          format: date-time
          example: '2026-07-02T14:32:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-07-02T14:32:00Z'
      required:
      - id
      - name
      - hostname
      - version
      - administered_by
      - key_fingerprint
      - reverse_api_port
      - status
      - vm_count
      - protected_vm_count
      - storage_used_gb
      - sna_count
      - sna_healthy
      - failed_jobs_24h
      - dr_readiness_pct
      - created_at
      - updated_at
    StatusMessageResponse:
      type: object
      description: Simple status acknowledgement envelope ({"status":"..."}).
      properties:
        status:
          type: string
          description: Fixed status string for the operation.
          example: ok
      required:
      - status
    Tenant:
      type: object
      description: A customer/organization managed by the MSP from the SCA (spec §7). Backing table `tenants`.
      properties:
        id:
          type: string
          description: Server-assigned UUID (generated on create).
          example: 9f1c2d3e-4b5a-6789-0abc-def012345678
        name:
          type: string
          description: Display name.
          example: Acme Corporation
        code:
          type: string
          description: Unique short code (uniqueIndex; used for federated-login callback URLs).
          example: acme
        status:
          type: string
          enum:
          - active
          - suspended
          - trial
          - offboarding
          description: Tenant lifecycle status. Defaults to `active`.
          example: active
        contact_email:
          type: string
          description: Primary contact email (omitted when empty).
          example: ops@acme.example.com
        notes:
          type: string
          description: Free-text notes (omitted when empty).
          example: Onboarded Q1; dedicated SILO.
        created_at:
          type: string
          format: date-time
          example: '2026-06-01T09:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-07-01T12:30:00Z'
      required:
      - id
      - name
      - code
      - status
      - created_at
      - updated_at
    TenantCreateRequest:
      type: object
      description: Request body for POST /tenants (tenant.CreateRequest). `name` and `code` are required;
        `code` must be unique.
      properties:
        name:
          type: string
          description: Display name (required, trimmed).
          example: Acme Corporation
        code:
          type: string
          description: Unique short code (required, trimmed).
          example: acme
        status:
          type: string
          enum:
          - active
          - suspended
          - trial
          - offboarding
          description: Optional; defaults to `active` when empty. Any other value is rejected with 400.
          example: active
        contact_email:
          type: string
          description: Optional primary contact email.
          example: ops@acme.example.com
        notes:
          type: string
          description: Optional free-text notes.
          example: Onboarded Q1; dedicated SILO.
      required:
      - name
      - code
    TenantUpdateRequest:
      type: object
      description: 'Request body for PUT /tenants/{id} (tenant.UpdateRequest). PATCH semantics: every
        field is optional; omitted fields are left unchanged. `name` cannot be set to empty; `status`
        must be one of the allowed values.'
      properties:
        name:
          type: string
          nullable: true
          description: New display name (rejected with 400 if blank).
          example: Acme Corp (renamed)
        status:
          type: string
          nullable: true
          enum:
          - active
          - suspended
          - trial
          - offboarding
          description: New status; invalid value returns 400.
          example: suspended
        contact_email:
          type: string
          nullable: true
          description: New contact email.
          example: billing@acme.example.com
        notes:
          type: string
          nullable: true
          description: New notes.
          example: Moved to suspended pending renewal.
    TenantSummary:
      type: object
      description: Tenant rollup card (tenant.Summary) aggregating the tenant's non-revoked enrolled SHAs.
      properties:
        tenant:
          $ref: '#/components/schemas/Tenant'
        sha_count:
          type: integer
          description: Count of non-revoked SHAs tenanted to this tenant.
          example: 2
        online_sha_count:
          type: integer
          description: Count of those SHAs whose status is `online`.
          example: 1
        protected_vm_count:
          type: integer
          description: Sum of protected_vm_count across the tenant's SHAs.
          example: 42
        storage_used_gb:
          type: number
          description: Sum of storage_used_gb across the tenant's SHAs.
          example: 1536.5
        failed_jobs_24h:
          type: integer
          description: Sum of failed backup jobs in the last 24h across the tenant's SHAs.
          example: 3
      required:
      - tenant
      - sha_count
      - online_sha_count
      - protected_vm_count
      - storage_used_gb
      - failed_jobs_24h
    SHAHealth:
      type: object
      description: A fleet-row projection of an enrolled SHA (fleet.SHAHealth) — the safe subset shown
        in fleet/tenant views (secrets like tokens/keys are stripped).
      properties:
        id:
          type: string
          description: SHA appliance id.
          example: sha-7c3f9a12
        name:
          type: string
          example: acme-hub-01
        status:
          type: string
          enum:
          - pending
          - online
          - stale
          - offline
          - revoked
          description: Heartbeat-derived health state.
          example: online
        version:
          type: string
          example: 4.3.520
        last_heartbeat_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the last accepted heartbeat (omitted if never).
          example: '2026-07-02T13:59:00Z'
        protected_vm_count:
          type: integer
          example: 42
        storage_used_gb:
          type: number
          example: 1536.5
        failed_jobs_24h:
          type: integer
          example: 3
      required:
      - id
      - name
      - status
      - version
      - protected_vm_count
      - storage_used_gb
      - failed_jobs_24h
    UsagePoint:
      type: object
      description: One point in a tenant's usage time series (fleet.UsagePoint), aggregated per capture
        time across the tenant's SHAs.
      properties:
        captured_at:
          type: string
          format: date-time
          description: Rollup-heartbeat capture time (15-min cadence).
          example: '2026-07-02T12:00:00Z'
        vm_count:
          type: integer
          example: 60
        protected_vm_count:
          type: integer
          example: 42
        storage_used_gb:
          type: number
          example: 1536.5
        failed_jobs_24h:
          type: integer
          example: 3
        dr_readiness_pct:
          type: number
          description: Average DR readiness percentage across the tenant's SHAs at this capture.
          example: 98.5
      required:
      - captured_at
      - vm_count
      - protected_vm_count
      - storage_used_gb
      - failed_jobs_24h
      - dr_readiness_pct
    TenantListResponse:
      type: object
      description: List wrapper returned by GET /tenants.
      properties:
        tenants:
          type: array
          items:
            $ref: '#/components/schemas/Tenant'
      required:
      - tenants
    TenantSHAsResponse:
      type: object
      description: List wrapper returned by GET /tenants/{id}/shas.
      properties:
        shas:
          type: array
          items:
            $ref: '#/components/schemas/SHAHealth'
      required:
      - shas
    TenantUsageResponse:
      type: object
      description: JSON response returned by GET /tenants/{id}/usage (default, i.e. without ?format=csv).
        With ?format=csv the response is instead a text/csv attachment with header row `captured_at,vm_count,protected_vm_count,storage_used_gb,failed_jobs_24h,dr_readiness_pct`.
      properties:
        tenant_id:
          type: string
          description: Echoes the {id} path parameter.
          example: 9f1c2d3e-4b5a-6789-0abc-def012345678
        points:
          type: array
          items:
            $ref: '#/components/schemas/UsagePoint'
      required:
      - tenant_id
      - points
    DeleteStatusResponse:
      type: object
      description: Body returned by DELETE /tenants/{id} on success.
      properties:
        status:
          type: string
          enum:
          - deleted
          example: deleted
      required:
      - status
    RateCard:
      type: object
      description: The MSP's global sell-price book (commercial data, not tenant-scoped). Effective-dated;
        the active card for a period is the latest with effective_from <= period end. Serialized directly
        by GORM struct json tags.
      properties:
        id:
          type: string
          description: UUID assigned on create.
          example: 7c3e0a2f-9b41-4d2e-8f1a-2b6d5c4e1a90
        name:
          type: string
          description: Human-readable price book name.
          example: Standard 2026
        currency:
          type: string
          description: ISO currency code; defaults to USD when omitted on create.
          example: USD
        license_per_vm_month:
          type: number
          description: Per-protected-VM monthly license rate (billed against monthly high-water VM count).
          example: 5
        storage_per_tb_month:
          type: number
          description: Per-TB monthly storage rate (billed against monthly average TB).
          example: 12.5
        overage_per_vm_month:
          type: number
          description: 'Rate billed per protected VM ABOVE the tenant''s operational quota (D4: overage
            is billable, not merely alerted). 0 = overage shown on statement but free.'
          example: 8
        effective_from:
          type: string
          format: date-time
          description: Date this card takes effect; card selection for a statement is the latest effective_from
            <= period end.
          example: '2026-01-01T00:00:00Z'
        created_at:
          type: string
          format: date-time
          example: '2026-01-15T09:30:00Z'
      required:
      - id
      - name
      - currency
      - license_per_vm_month
      - storage_per_tb_month
      - overage_per_vm_month
      - effective_from
      - created_at
    TenantRateOverride:
      type: object
      description: 'Per-tenant commercial override of the global rate card (D4). One row per tenant. Each
        rate field is NULLABLE: a null/omitted field means ''no override — the active card''s rate applies'';
        a set field replaces that rate for this tenant on drafts and finalized statements alike. Serialized
        directly by GORM struct json tags (nil rate fields are omitted from the response due to omitempty).'
      properties:
        tenant_id:
          type: string
          description: Tenant this override applies to (primary key).
          example: acme-corp
        license_per_vm_month:
          type: number
          nullable: true
          description: Overrides the card's license_per_vm_month for this tenant. null/omitted = inherit
            the card rate.
          example: 4.5
        storage_per_tb_month:
          type: number
          nullable: true
          description: Overrides the card's storage_per_tb_month for this tenant. null/omitted = inherit
            the card rate.
          example: 10
        overage_per_vm_month:
          type: number
          nullable: true
          description: Overrides the card's overage_per_vm_month for this tenant. null/omitted = inherit
            the card rate.
          example: 6
        updated_by:
          type: string
          description: Email of the admin who last wrote the override (omitted when empty).
          example: admin@sendense.local
        created_at:
          type: string
          format: date-time
          description: First-write timestamp; preserved across PUT upserts.
          example: '2026-02-01T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-03-01T12:00:00Z'
      required:
      - tenant_id
      - created_at
      - updated_at
    StatementLineItem:
      type: object
      description: One charge line on a statement (§9.7). The base statement always contains a license
        line and a storage line; a third OVERAGE line is appended (and carries an extra 'quota' field)
        only when the tenant has quota>0 and the monthly high-water exceeded it. quantity is an integer
        for VM lines and a rounded number for the storage TB line.
      properties:
        description:
          type: string
          example: Protected VMs (monthly high-water)
        quantity:
          type: number
          description: 'VM·month lines: integer VM count. Storage line: rounded average TB (may be fractional).'
          example: 42
        unit:
          type: string
          description: Unit of the quantity.
          example: VM·month
        rate:
          type: number
          description: Per-unit rate applied (after any per-tenant override).
          example: 5
        amount:
          type: number
          description: quantity × rate, rounded to 2 decimals.
          example: 210
        quota:
          type: integer
          description: ONLY present on the overage line — the operational quota the excess was measured
            against.
          example: 40
        offering_id:
          type: string
          description: The managed storage offering the line was metered against (storage lines).
      required:
      - description
      - quantity
      - unit
      - rate
      - amount
    DraftStatement:
      type: object
      description: An on-the-fly DRAFT billing statement (draft=true), computed from full-window usage
        × the active rate card with per-tenant overrides applied. Assembled as an inline map by computedStatement.toMap(true)
        — not a stored record, so it has no id/finalized_by/finalized_at. 'rate_overrides_applied' is
        present (true) only when an override actually changed a rate; 'note' is present only when no active
        rate card exists for the period (amounts are zero).
      properties:
        tenant_id:
          type: string
          example: acme-corp
        period:
          type: string
          description: Billing period, YYYY-MM.
          example: 2026-05
        currency:
          type: string
          description: From the active rate card (USD if no card).
          example: USD
        rate_card:
          type: string
          description: Name of the active rate card (empty string when no card exists for the period).
          example: Standard 2026
        line_items:
          type: array
          items:
            $ref: '#/components/schemas/StatementLineItem'
        total:
          type: number
          description: Sum of line-item amounts, rounded to 2 decimals.
          example: 735
        quota:
          type: integer
          description: Tenant operational quota (SUM of tenant_licenses allocations) the overage was measured
            against; 0 if none.
          example: 40
        basis:
          type: string
          description: Fixed explanation of the billing basis.
          example: 'consumed: monthly high-water protected VMs + average storage TB; overage above quota
            billable (§9.7, D4)'
        evidence_window:
          type: object
          description: The usage-evidence window (full calendar month) the basis was measured over.
          properties:
            from:
              type: string
              format: date-time
              example: '2026-05-01T00:00:00Z'
            to:
              type: string
              format: date-time
              example: '2026-05-31T23:59:59Z'
          required:
          - from
          - to
        draft:
          type: boolean
          description: Always true for this endpoint.
          example: true
        rate_overrides_applied:
          type: boolean
          description: Present (true) only when a per-tenant override replaced at least one card rate.
          example: true
        note:
          type: string
          description: Present only when there is no active rate card for the period.
          example: no active rate card for this period — amounts are zero until one is set
      required:
      - tenant_id
      - period
      - currency
      - rate_card
      - line_items
      - total
      - quota
      - basis
      - evidence_window
      - draft
    FinalizedStatement:
      type: object
      description: A FINALIZED (immutable) statement record (draft=false), rendered by renderStatement
        from a stored BillingStatement. Line items are re-parsed from the JSON snapshot. 'rate_overrides_applied'
        is present (true) only when overrides were applied at finalize time.
      properties:
        id:
          type: string
          description: Statement UUID.
          example: 1f8b7c6d-5e4a-3b2c-1d0e-9f8a7b6c5d4e
        tenant_id:
          type: string
          example: acme-corp
        period:
          type: string
          description: YYYY-MM.
          example: 2026-05
        currency:
          type: string
          example: USD
        rate_card:
          type: string
          description: Snapshotted rate-card name.
          example: Standard 2026
        rate_card_id:
          type: string
          description: Snapshotted rate-card id.
          example: 7c3e0a2f-9b41-4d2e-8f1a-2b6d5c4e1a90
        line_items:
          type: array
          items:
            $ref: '#/components/schemas/StatementLineItem'
        total:
          type: number
          description: Snapshotted total amount.
          example: 735
        high_water_vms:
          type: integer
          description: Monthly high-water protected VM count that was billed.
          example: 42
        avg_tb:
          type: number
          description: Monthly average storage TB that was billed (rounded to 2 decimals at finalize).
          example: 18.4
        quota:
          type: integer
          description: Operational quota snapshot the overage was measured against.
          example: 40
        evidence_window:
          type: object
          properties:
            from:
              type: string
              format: date-time
              example: '2026-05-01T00:00:00Z'
            to:
              type: string
              format: date-time
              example: '2026-05-31T23:59:59Z'
          required:
          - from
          - to
        finalized_by:
          type: string
          description: Email of the admin who finalized the statement (empty string if unknown).
          example: admin@sendense.local
        finalized_at:
          type: string
          format: date-time
          example: '2026-06-02T10:15:00Z'
        draft:
          type: boolean
          description: Always false for a finalized record.
          example: false
        rate_overrides_applied:
          type: boolean
          description: Present (true) only when per-tenant overrides were applied at finalize time.
          example: true
      required:
      - id
      - tenant_id
      - period
      - currency
      - rate_card
      - rate_card_id
      - line_items
      - total
      - high_water_vms
      - avg_tb
      - quota
      - evidence_window
      - finalized_by
      - finalized_at
      - draft
    StatementSummary:
      type: object
      description: A compact finalized-statement row in the history list (list endpoint only — not the
        full drill-down).
      properties:
        id:
          type: string
          example: 1f8b7c6d-5e4a-3b2c-1d0e-9f8a7b6c5d4e
        period:
          type: string
          description: YYYY-MM.
          example: 2026-05
        currency:
          type: string
          example: USD
        rate_card:
          type: string
          example: Standard 2026
        total:
          type: number
          example: 735
        finalized_by:
          type: string
          example: admin@sendense.local
        finalized_at:
          type: string
          format: date-time
          example: '2026-06-02T10:15:00Z'
      required:
      - id
      - period
      - currency
      - rate_card
      - total
      - finalized_by
      - finalized_at
    DeletedStatus:
      type: object
      description: Generic delete acknowledgement.
      properties:
        status:
          type: string
          example: deleted
      required:
      - status
    LicensePool:
      type: object
      description: SCA-local OPERATIONAL mirror of an MSP pool entitlement (spec §8.2/§9.4). Not a cryptographic
        license — the SCA is a pool viewer + operational-quota planner. Returned verbatim by POST /license-pools.
        allocated/remaining are NOT stored on this model; they are derived only in the list view.
      properties:
        id:
          type: string
          description: Server-generated UUID for the pool.
          example: 9f1c2b3a-4d5e-6f70-8a9b-0c1d2e3f4a5b
        entitlement_id:
          type: string
          description: Optional reference to the central sendense-licensing entitlement.
          example: ent-ENTERPRISE-2026
        edition:
          type: string
          description: Product edition the pool grants (required on create).
          example: enterprise
        quota:
          type: integer
          description: Total protected-VM capacity purchased for this pool.
          example: 500
        expiry:
          type: string
          format: date-time
          nullable: true
          description: Pool expiry timestamp; omitted (null) when the pool has no expiry.
          example: '2027-01-01T00:00:00Z'
        notes:
          type: string
          description: Free-form operational notes.
          example: Q1 enterprise purchase
        created_at:
          type: string
          format: date-time
          description: When the pool record was created.
          example: '2026-07-02T10:15:00Z'
      required:
      - id
      - edition
      - quota
      - notes
      - created_at
    TenantLicense:
      type: object
      description: OPERATIONAL per-tenant quota allocation drawn from a pool (spec §8.2/§9.5). No signed
        cryptographic document is issued. Returned verbatim by POST /license-pools/{id}/allocations (201
        on create, 200 on in-place update). The (pool_id, tenant_id) pair is unique.
      properties:
        id:
          type: string
          description: Server-generated UUID for the allocation.
          example: 1a2b3c4d-5e6f-7081-9a0b-1c2d3e4f5061
        pool_id:
          type: string
          description: Parent pool UUID.
          example: 9f1c2b3a-4d5e-6f70-8a9b-0c1d2e3f4a5b
        tenant_id:
          type: string
          description: Tenant the quota is allocated to.
          example: tenant-acme
        quota:
          type: integer
          description: Operational protected-VM quota allocated to the tenant from this pool.
          example: 50
        state:
          type: string
          description: Planning metadata; defaults to 'planned' when omitted on create.
          enum:
          - planned
          - active
          example: planned
        notes:
          type: string
          description: Free-form operational notes.
          example: initial allocation
        created_at:
          type: string
          format: date-time
          description: When the allocation was created.
          example: '2026-07-02T10:20:00Z'
      required:
      - id
      - pool_id
      - tenant_id
      - quota
      - state
      - notes
      - created_at
    LicensePoolView:
      type: object
      description: A pool as rendered in the admin list view (handleListLicensePools). Built as an inline
        map — NOT the raw LicensePool model. Adds DERIVED allocated/remaining and an expanded per-tenant
        allocations array with measured utilization.
      properties:
        id:
          type: string
          example: 9f1c2b3a-4d5e-6f70-8a9b-0c1d2e3f4a5b
        entitlement_id:
          type: string
          example: ent-ENTERPRISE-2026
        edition:
          type: string
          example: enterprise
        quota:
          type: integer
          description: Total pool capacity purchased.
          example: 500
        allocated:
          type: integer
          description: 'DERIVED: sum of quota across all tenant allocations in this pool.'
          example: 90
        remaining:
          type: integer
          description: 'DERIVED: quota - allocated.'
          example: 410
        expiry:
          type: string
          format: date-time
          nullable: true
          description: Pool expiry; null when unset.
          example: '2027-01-01T00:00:00Z'
        notes:
          type: string
          example: Q1 enterprise purchase
        allocations:
          type: array
          description: Per-tenant operational allocations with measured usage.
          items:
            $ref: '#/components/schemas/PoolAllocationView'
      required:
      - id
      - entitlement_id
      - edition
      - quota
      - allocated
      - remaining
      - notes
      - allocations
    PoolAllocationView:
      type: object
      description: 'One tenant''s allocation as shown inside a pool''s allocations array (list view).
        Inline map: adds tenant_name (looked up) and measured ''used'' (30-day high-water protected VMs).'
      properties:
        tenant_id:
          type: string
          example: tenant-acme
        tenant_name:
          type: string
          description: Resolved tenant display name; empty string if not resolvable.
          example: Acme Corp
        quota:
          type: integer
          description: Operational quota allocated to this tenant.
          example: 50
        used:
          type: integer
          description: 'MEASURED: 30-day high-water protected-VM count from usage_snapshots; 0 if no series.'
          example: 37
        state:
          type: string
          enum:
          - planned
          - active
          example: active
        notes:
          type: string
          example: initial allocation
      required:
      - tenant_id
      - tenant_name
      - quota
      - used
      - state
      - notes
    ListLicensePoolsResponse:
      type: object
      description: Wrapper returned by GET /api/v1/license-pools (writeJSON of a map with 'pools' + 'note').
      properties:
        pools:
          type: array
          items:
            $ref: '#/components/schemas/LicensePoolView'
        note:
          type: string
          description: 'Constant §9.2 boundary disclaimer: SCA records operational pool/quota state only
            and never issues cryptographic licenses.'
          example: Operational pool + quota planning only (spec §9.4/§9.5). Cryptographic license issuance
            (signed DELEGATED child documents, central quota decrement) requires the sendense-licensing
            §9.2 partner endpoints (POST /partner/pools/{id}/allocate, /partner/licenses/{id}/revoke),
            which are not built yet — the SCA is a pool manager + viewer, never a signer.
      required:
      - pools
      - note
    CreateLicensePoolRequest:
      type: object
      description: Body for POST /api/v1/license-pools. 'edition' is required; 'quota' must be >= 0; 'expiry'
        is parsed as a timestamp (optional).
      properties:
        entitlement_id:
          type: string
          description: Optional central licensing entitlement reference.
          example: ent-ENTERPRISE-2026
        edition:
          type: string
          description: REQUIRED product edition.
          example: enterprise
        quota:
          type: integer
          description: Total protected-VM capacity purchased; must be >= 0.
          example: 500
        expiry:
          type: string
          format: date-time
          description: Optional pool expiry; parsed via parseTimeParam, ignored if empty/unparseable.
          example: '2027-01-01T00:00:00Z'
        notes:
          type: string
          example: Q1 enterprise purchase
      required:
      - edition
    AllocateLicenseRequest:
      type: object
      description: Body for POST /api/v1/license-pools/{id}/allocations. 'tenant_id' is required and must
        exist; 'quota' must be >= 0; enforces sum(allocations in pool) <= pool.quota (else 409). Re-posting
        the same tenant updates the allocation in place.
      properties:
        tenant_id:
          type: string
          description: REQUIRED existing tenant to allocate to.
          example: tenant-acme
        quota:
          type: integer
          description: Operational quota to allocate; must be >= 0.
          example: 50
        state:
          type: string
          description: Planning state; defaults to 'planned' if empty.
          enum:
          - planned
          - active
          example: active
        notes:
          type: string
          example: initial allocation
      required:
      - tenant_id
    TenantLicenseAllocationView:
      type: object
      description: One allocation entry inside GET /api/v1/tenants/{id}/license. Inline map joining the
        allocation to its pool edition.
      properties:
        pool_id:
          type: string
          example: 9f1c2b3a-4d5e-6f70-8a9b-0c1d2e3f4a5b
        edition:
          type: string
          description: Edition of the parent pool.
          example: enterprise
        quota:
          type: integer
          example: 50
        state:
          type: string
          enum:
          - planned
          - active
          example: active
        notes:
          type: string
          example: initial allocation
      required:
      - pool_id
      - edition
      - quota
      - state
      - notes
    TenantLicenseResponse:
      type: object
      description: 'Response for GET /api/v1/tenants/{id}/license: a tenant''s operational quota allocations
        plus measured utilization.'
      properties:
        tenant_id:
          type: string
          example: tenant-acme
        allocations:
          type: array
          items:
            $ref: '#/components/schemas/TenantLicenseAllocationView'
        total_quota:
          type: integer
          description: Sum of quota across all of this tenant's allocations.
          example: 50
        used:
          type: integer
          description: 'MEASURED: 30-day high-water protected-VM count.'
          example: 37
        utilization:
          type: number
          description: used/total_quota as a percentage, rounded to 1 decimal place; 0 when quota is 0.
          example: 74
        basis:
          type: string
          description: Constant explanation of the used/quota basis.
          example: used = 30-day high-water protected VMs (usage_snapshots); quota = operational (§9.5)
        note:
          type: string
          description: Constant §9.2 boundary disclaimer.
          example: Operational pool + quota planning only (spec §9.4/§9.5). Cryptographic license issuance
            ... the SCA is a pool manager + viewer, never a signer.
      required:
      - tenant_id
      - allocations
      - total_quota
      - used
      - utilization
      - basis
      - note
    StatusMessage:
      type: object
      description: Simple status acknowledgement returned by pool/allocation deletes.
      properties:
        status:
          type: string
          example: deleted
      required:
      - status
    PartnerRepository:
      type: object
      description: An MSP-offered storage endpoint (EBA/S3-compatible) that tenant SHAs connect to directly.
        The SCA only RECORDS the offering for onboarding + metering; backup data flows SHA->endpoint and
        NEVER through the SCA. NO credentials are stored on this object — they live in the SHA vault.
      properties:
        id:
          type: string
          description: Server-generated UUID primary key.
          example: a1b2c3d4-5e6f-7089-abcd-1234567890ef
        name:
          type: string
          description: Human-friendly display name of the offering.
          example: EU-West Object Store
        endpoint:
          type: string
          description: S3-compatible endpoint URL/host the tenant SHA connects to.
          example: https://s3.eu-west-1.partner.example.com
        kind:
          type: string
          description: 'Storage backend type. Defaults to "s3" when omitted on create. Documented values:
            s3 | minio | other.'
          example: s3
        region:
          type: string
          description: Optional region hint. Omitted from JSON when empty (json:",omitempty").
          example: eu-west-1
        bucket:
          type: string
          description: Optional bucket name. Omitted from JSON when empty (json:",omitempty").
          example: tenant-backups
        notes:
          type: string
          description: Optional free-text notes. Omitted from JSON when empty (json:",omitempty").
          example: Primary offering for EU tenants
        created_at:
          type: string
          format: date-time
          description: Creation timestamp.
          example: '2026-07-02T10:15:30Z'
        updated_at:
          type: string
          format: date-time
          description: Last-update timestamp.
          example: '2026-07-02T10:15:30Z'
      required:
      - id
      - name
      - endpoint
      - kind
      - created_at
      - updated_at
    PartnerRepositoryCreateRequest:
      type: object
      description: Registers a partner storage offering. Records the offering only — NO credentials/secrets
        are accepted or stored (credentials live in the SHA vault). Backup data never flows through the
        SCA.
      properties:
        name:
          type: string
          description: Required. Display name (whitespace-only rejected with 400).
          example: EU-West Object Store
        endpoint:
          type: string
          description: Required. S3-compatible endpoint URL/host (whitespace-only rejected with 400).
          example: https://s3.eu-west-1.partner.example.com
        kind:
          type: string
          description: 'Optional storage backend type. Defaults to "s3" server-side when empty. Documented
            values: s3 | minio | other.'
          example: s3
        region:
          type: string
          description: Optional region hint.
          example: eu-west-1
        bucket:
          type: string
          description: Optional bucket name.
          example: tenant-backups
        notes:
          type: string
          description: Optional free-text notes.
          example: Primary offering for EU tenants
      required:
      - name
      - endpoint
    PartnerRepositoryListResponse:
      type: object
      description: Wrapper object returned by the list endpoint. Repositories are ordered by name ascending.
      properties:
        repositories:
          type: array
          description: All registered partner storage offerings, ordered by name asc.
          items:
            $ref: '#/components/schemas/PartnerRepository'
      required:
      - repositories
    DeletedStatusResponse:
      type: object
      description: Confirmation returned on successful delete.
      properties:
        status:
          type: string
          description: Always the literal "deleted".
          example: deleted
      required:
      - status
    ApproveRequest:
      type: object
      description: Approve a pending SHA enrollment, assigning it to a tenant (SILO 1:1).
      properties:
        tenant_id:
          type: string
          description: The tenant to bind this SHA to on approval.
          example: tnt-acme
      required:
      - tenant_id
    TenantFeature:
      type: object
      required:
      - key
      - label
      - enabled
      properties:
        key:
          type: string
          description: Catalog feature key (replication | offsite-copy | file-recovery | app-aware | clean-rooms).
        label:
          type: string
          description: Human-readable feature name.
        enabled:
          type: boolean
          description: Effective state for the tenant. Absence of a stored switch means enabled (default-on).
    TenantFeaturesResponse:
      type: object
      required:
      - features
      properties:
        tenant_id:
          type: string
          description: Present on the staff views only.
        features:
          type: array
          items:
            $ref: '#/components/schemas/TenantFeature'
          description: The FULL catalog in catalog order with per-tenant effective state.
    UpdateTenantFeaturesRequest:
      type: object
      required:
      - features
      properties:
        features:
          type: object
          additionalProperties:
            type: boolean
          description: 'feature_key → enabled. PARTIAL update: only the keys present change. Unknown keys
            are refused wholesale (400).'
    Host:
      type: object
      required:
      - id
      - name
      - kind
      - health
      properties:
        id:
          type: string
        name:
          type: string
        kind:
          type: string
          description: tin (default) or a future host kind.
        address:
          type: string
        agent_fingerprint:
          type: string
          description: Future D9 host-agent identity.
        cpu_cores:
          type: integer
        mem_gb:
          type: integer
        disk_gb:
          type: integer
        max_sets:
          type: integer
          description: Container-set capacity; 0 = no cap. Placement past this is refused (409).
        health:
          type: string
        notes:
          type: string
        last_seen_at:
          type: string
          format: date-time
        allocated_sets:
          type: integer
          description: DERIVED count of non-revoked appliances placed here (never stored).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        agent_version:
          type: string
          description: Reported by the host agent after D9 enrollment.
        agent_enrolled_at:
          type: string
          format: date-time
    CreateHostRequest:
      type: object
      required:
      - name
      properties:
        name:
          type: string
        kind:
          type: string
          default: tin
        address:
          type: string
        cpu_cores:
          type: integer
        mem_gb:
          type: integer
        disk_gb:
          type: integer
        max_sets:
          type: integer
        notes:
          type: string
    SetApplianceHostRequest:
      type: object
      required:
      - host_id
      properties:
        host_id:
          type: string
          description: Registered host id; empty string clears the association.
    HostsResponse:
      type: object
      required:
      - hosts
      properties:
        hosts:
          type: array
          items:
            $ref: '#/components/schemas/Host'
    ProvisioningRecord:
      type: object
      required:
      - id
      - tenant_id
      - kind
      - state
      properties:
        id:
          type: string
        tenant_id:
          type: string
        kind:
          type: string
          description: sha (Topology A); sna arrives with §16.4.
        pairing_code_id:
          type: string
        provider_vm_id:
          type: string
        expected_ips:
          type: string
          description: 'Comma-separated D8 comparands: VM NIC IPs + the account''s source-NAT egress IPs.'
        state:
          type: string
          enum:
          - provisioning
          - enrolled
          - expired
          - failed
        sha_id:
          type: string
        error:
          type: string
        enrolled_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ProvisionTenantRequest:
      type: object
      required:
      - zone_id
      - template_id
      - offering_id
      - network_id
      properties:
        kind:
          type: string
          description: sha (Topology A) | sna (§16.4 — enrolls to a parent SHA).
          default: sha
          enum:
          - sha
          - sna
        name:
          type: string
        zone_id:
          type: string
        template_id:
          type: string
        offering_id:
          type: string
        network_id:
          type: string
          description: The TENANT's network (account-scoped picker) the appliance joins.
        parent_sha_id:
          type: string
          description: '§16.4 SNA: the parent SHA the SNA enrolls TO (required when kind=sna).'
        site_id:
          type: string
          description: '§16.4 SNA: inventory site the SNA is bound to.'
        delivery:
          type: string
          description: 'Enrollment-payload transport: seed_iso (default, needs no VR) | userdata (needs
            a VR/config-drive).'
          default: seed_iso
          enum:
          - seed_iso
          - userdata
        enroll_endpoint:
          type: string
          description: Operator-overridable address the appliance dials to enroll (§16.8.3). URL or host[:port].
            Empty = tenant default > SCA_PUBLIC_URL.
        static_ip:
          type: string
          description: 'Static NIC baked into the seed (§16.8.1): CIDR (10.0.0.5/24) or plain IP (+ static_netmask).
            Empty = DHCP.'
        static_netmask:
          type: string
          description: Dotted netmask when static_ip is a plain IP.
        static_gateway:
          type: string
        static_dns:
          type: array
          items:
            type: string
          description: DNS server IPs.
        static_domain:
          type: string
          description: DNS search suffix (e.g. corp.example.com).
        static_iface:
          type: string
          description: default eth0
    SetTenantCloudStackRequest:
      type: object
      required:
      - cloudstack_domain_id
      - cloudstack_account
      properties:
        cloudstack_domain_id:
          type: string
        cloudstack_account:
          type: string
          description: Both set together, or both empty to clear.
        enroll_endpoint:
          type: string
          description: 'Tenant default appliance-facing enroll address (§16.8.3). A *string: absent leaves
            the stored value, empty clears it.'
    ManagedStorageProvider:
      type: object
      description: 'A centrally-managed EBA storage provider (design §18.2, CSP-W7). v1 manages kind=minio;
        other kinds keep the decentralized §9.6 partner-repositories path. The admin credential is WRITE-ONLY:
        it is AES-GCM-vaulted at registration and never returned by any endpoint.'
      properties:
        id:
          type: string
          example: 9a1c…
        name:
          type: string
          example: CSP MinIO EU
        endpoint:
          type: string
          example: http://minio:9000
        kind:
          type: string
          enum:
          - minio
          example: minio
        region:
          type: string
          example: eu-west-1
        admin_access_key_id:
          type: string
          description: Display metadata only — the paired secret lives sealed in the vault.
          example: sca-lifecycle
        status:
          type: string
          enum:
          - active
          - unreachable
          - disabled
          example: active
        capabilities:
          type: object
          description: 'Probed at registration: bucket_quota, object_lock, usage_metrics, policy_separation
            (the §18.2 admin-vs-data separation caveat carrier).'
        notes:
          type: string
        created_at:
          type: string
          format: date-time
        data_endpoint:
          type: string
          description: SHA-reachable S3 DATA endpoint (empty = same as endpoint). The tenant SHA writes
            backup data direct here; the admin API stays on the internal endpoint (§18.2).
          example: https://minio.tenant.example
        insecure_skip_verify:
          type: boolean
          description: Skip TLS cert verification for a self-signed endpoint (§18.2).
    ManagedStorageProviderListResponse:
      type: object
      properties:
        providers:
          type: array
          items:
            $ref: '#/components/schemas/ManagedStorageProvider'
    RegisterStorageProviderRequest:
      type: object
      required:
      - name
      - endpoint
      - access_key_id
      - secret_access_key
      description: Registers a managed provider (§18.3 step 1). The admin credential should be a DEDICATED
        lifecycle account denied s3:GetObject/PutObject (MinIO supports the separation) — keeping “no
        SCA data path” structural. Fails closed 503 until SCA_STORAGE_VAULT_KEY is set; the probe must
        pass.
      properties:
        name:
          type: string
          example: CSP MinIO EU
        endpoint:
          type: string
          example: http://minio:9000
          description: ADMIN endpoint the SCA reaches (keep SCA-internal, e.g. http://minio:9000 — the
            admin API must not be public, §18.2).
        kind:
          type: string
          enum:
          - minio
          - external
          default: minio
          description: ' kind=external (§18.8): endpoint metadata ONLY — no admin credential (refused
            if supplied), nothing probed; bucket credentials are CSP-supplied per assignment.'
        region:
          type: string
        access_key_id:
          type: string
        secret_access_key:
          type: string
          description: Vaulted AES-GCM at rest; never returned.
        notes:
          type: string
        data_endpoint:
          type: string
          description: SHA-reachable S3 DATA endpoint delivered to the tenant SHA for backup writes (empty
            = same as endpoint). Set this when the SHA reaches MinIO at a different address than the SCA
            (e.g. an externally-published https://host:443).
        insecure_skip_verify:
          type: boolean
          default: false
          description: Skip TLS cert verification (self-signed endpoint). A trusted cert is preferred.
    StorageProviderProbeResponse:
      type: object
      properties:
        status:
          type: string
          enum:
          - active
          - unreachable
        capabilities:
          type: object
        error:
          type: string
    ManagedBucket:
      type: object
      description: 'A per-tenant managed bucket (design §18.2): one bucket → one EBA repository → one
        dedup domain per tenant. Credential fields are METADATA only (D10 — the secret is minted at assign,
        delivered to the SHA, dropped). quota_bytes is ENFORCEMENT only (D11 — never a billable quantity).
        States: creating→ready→assigning→assigned→reclaim_pending→exported→purged.'
      properties:
        id:
          type: string
        provider_id:
          type: string
        tenant_id:
          type: string
        bucket_name:
          type: string
          description: S3 bucket name. Terminal rows (purged/detached) carry a tombstoned form name#<id8>
            — the name slot is RELEASED for re-onboarding; the original survives in the deletion-evidence
            record.
        quota_bytes:
          type: integer
          format: int64
        object_lock:
          type: boolean
        state:
          type: string
          enum:
          - creating
          - ready
          - assigning
          - assigned
          - rotating
          - reclaim_pending
          - exported
          - purging
          - purged
          - detached
        access_key_id:
          type: string
          description: Key-id metadata; the secret never rests on the SCA (D10).
        credential_created_at:
          type: string
          format: date-time
        credential_rotated_at:
          type: string
          format: date-time
        sha_id:
          type: string
        sha_repo_ref:
          type: string
          description: The EBA repository id on the tenant SHA once assigned.
          example: eba-repo-1751500000
        assign_operation_id:
          type: string
          description: remote_operations audit ref for the assign dispatch.
        notes:
          type: string
        created_at:
          type: string
          format: date-time
        offering_id:
          type: string
          description: StorageOffering the bucket was assigned under (§18.8). Empty for direct/pre-offering
            buckets.
        provisioning_mode:
          type: string
          enum:
          - managed_create
          - managed_adopt
          - external
          description: 'How the bucket came under management (§18.8). Empty = managed_create (legacy rows).
            Gates the destructive paths: adopted data needs delete_adopted_data to purge; external buckets
            never purge (detach).'
        assigned_repo_ref:
          type: string
          description: 'DURABLE copy of the SHA repo id (billing/history join key): set at assign, never
            cleared — captured consumption keeps billing after offboarding clears sha_repo_ref.'
    ManagedBucketListResponse:
      type: object
      properties:
        buckets:
          type: array
          items:
            $ref: '#/components/schemas/ManagedBucket'
    CreateManagedBucketRequest:
      type: object
      required:
      - provider_id
      - tenant_id
      description: Creates the per-tenant bucket at the provider (§18.3 step 2). No credential is minted
        here — that happens at assign (D10). quota_gb applies a provider-side HARD quota where supported
        (enforcement only, D11).
      properties:
        provider_id:
          type: string
        tenant_id:
          type: string
        bucket_name:
          type: string
          description: Optional; defaulted to sdt-{tenant_code}-{rand}. 3-63 chars, lowercase/digits/hyphens.
        quota_gb:
          type: integer
          format: int64
        object_lock:
          type: boolean
        adopt_existing:
          type: boolean
          description: '§18.8 managed_adopt: the bucket already exists at the managed provider (CSP-created)
            — verified via a metadata stat, never created; object-lock is READ from the bucket. Requires
            bucket_name.'
        offering_id:
          type: string
          description: Stamps the offering the bucket is provisioned under (normally set by the offering
            assign composite).
    AssignManagedBucketRequest:
      type: object
      description: Optional EBA repo settings passed through to the SHA-side registration (compression/dedup
        default true). Encryption stays the SHA's own concern — tenant keys never reach the SCA.
      properties:
        repo_name:
          type: string
          description: Defaults to managed-{bucket_name}.
        compression_enabled:
          type: boolean
        dedup_enabled:
          type: boolean
        access_key_id:
          type: string
          description: 'EXTERNAL buckets only (§18.8): the CSP-minted bucket-scoped key id. Refused for
            managed buckets (those always mint).'
        secret_access_key:
          type: string
          format: password
          description: 'EXTERNAL buckets only: the paired secret — delivered to the tenant SHA and DROPPED
            (D10; redacted in every persisted record).'
    ExportManagedBucketRequest:
      type: object
      properties:
        note:
          type: string
          description: Which §18.6 data-return path applied (recorded in the bucket notes).
    PurgeManagedBucketRequest:
      type: object
      required:
      - confirm
      properties:
        confirm:
          type: string
          enum:
          - purge
          description: Literal "purge" — destructive confirmation.
        delete_adopted_data:
          type: boolean
          description: '§18.8 second confirm for ADOPTED buckets: their data predates SCA management —
            without this the purge is refused (use detach to release intact).'
    BucketDeletionEvidence:
      type: object
      description: The §18.6 deletion-evidence record written at purge, retained under the G7 evidence
        policy (survives bucket-row pruning).
      properties:
        id:
          type: string
        bucket_id:
          type: string
        provider_id:
          type: string
        tenant_id:
          type: string
        bucket_name:
          type: string
        object_count:
          type: integer
          format: int64
        total_bytes:
          type: integer
          format: int64
        usage_source:
          type: string
          enum:
          - list
          - provider_metrics
          - unavailable
          description: Where the pre-purge figures came from (provider crawler metrics can lag).
        purged_by:
          type: string
        purged_at:
          type: string
          format: date-time
        action:
          type: string
          enum:
          - ''
          - detached
          description: Empty = purged (data deleted); detached = released intact to CSP control (§18.8).
    PurgeManagedBucketResponse:
      type: object
      properties:
        status:
          type: string
          enum:
          - purged
        evidence:
          $ref: '#/components/schemas/BucketDeletionEvidence'
    ManagedBucketUsageResponse:
      type: object
      description: 'The §18.4 reconciliation pair: provider-side usage metadata next to the latest SHA-reported
        per-repo snapshot.'
      properties:
        bucket_id:
          type: string
        bucket_name:
          type: string
        state:
          type: string
        provider:
          type: object
          description: '{bytes, objects, source} from the provider admin/list API.'
        provider_error:
          type: string
        sha_reported:
          type: object
          description: Latest repo_usage_snapshots row for the bucket's sha_repo_ref (heartbeat per_repository[]
            series).
    CloudStackCredential:
      type: object
      description: Vaulted CloudStack root-admin credential METADATA (design §16.5). The sealed secret
        key is NEVER present — this is the write-only credential's public face. At most one active + one
        pending.
      properties:
        id:
          type: string
        state:
          type: string
          enum:
          - active
          - pending
          description: active = in use; pending = a rotation candidate, unused until verified.
        api_url:
          type: string
          example: https://cloudstack.example/client/api
        api_key:
          type: string
          description: The API key id (metadata) — NOT the secret.
          example: AKIA…
        verify_ssl:
          type: boolean
        created_by:
          type: string
        notes:
          type: string
        activated_at:
          type: string
          format: date-time
        last_verified_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    CloudStackCredentialListResponse:
      type: object
      properties:
        vault_configured:
          type: boolean
          description: false ⇒ SCA_VAULT_KEY unset; credential ops fail closed (503).
        credentials:
          type: array
          items:
            $ref: '#/components/schemas/CloudStackCredential'
    SetCloudStackCredentialRequest:
      type: object
      required:
      - api_url
      - api_key
      - secret_key
      description: A CloudStack root-admin credential to vault (§16.5). The secret_key is AES-GCM-sealed
        at rest and never returned. First set → active; with an active present → pending (rotation).
      properties:
        api_url:
          type: string
        api_key:
          type: string
        secret_key:
          type: string
          description: Sealed at rest; write-only.
        verify_ssl:
          type: boolean
          default: true
        notes:
          type: string
        force:
          type: boolean
          default: false
          description: 'Break-glass replace (§16.5 recovery): write DIRECTLY as active, replacing a BROKEN
            active WITHOUT a probe — for when the active secret was rotated/revoked out-of-band AND CloudStack
            is unreachable. Admin-only + audited; skips verification, so use only to recover.'
    CloudStackRotationVerifyResponse:
      type: object
      properties:
        status:
          type: string
          enum:
          - rotated
        credential:
          $ref: '#/components/schemas/CloudStackCredential'
    StorageOffering:
      type: object
      description: '§18.8 (CSP-W9): a CSP-facing storage PRODUCT — provisioning mode + provider + defaults
        + rate card. Assigning it to a tenant instantiates a managed bucket per the mode in one step.
        Offerings hold NO credentials.'
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        provisioning_mode:
          type: string
          enum:
          - managed_create
          - managed_adopt
          - external
        provider_id:
          type: string
        default_quota_gb:
          type: integer
          format: int64
          description: Seeds the bucket hard quota at assign (0 = none; ADVISORY for external buckets).
        object_lock:
          type: boolean
        rate_per_tb_month:
          type: number
          nullable: true
          description: 'Per-offering D11 rate card (binary-TiB-month, labeled TB): prices this offering''s
            statement line INSTEAD of the global card storage rate (and instead of tenant storage-rate
            overrides — a product rate). null = card rate.'
        status:
          type: string
          enum:
          - active
          - retired
        notes:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    StorageOfferingWithCounts:
      allOf:
      - $ref: '#/components/schemas/StorageOffering'
      - type: object
        properties:
          bucket_count:
            type: integer
            description: Non-terminal buckets under this offering.
          assigned_count:
            type: integer
            description: Buckets currently serving a tenant SHA (assigned/rotating).
          tenant_count:
            type: integer
            description: Distinct tenants with a non-terminal bucket.
    StorageOfferingListResponse:
      type: object
      properties:
        offerings:
          type: array
          items:
            $ref: '#/components/schemas/StorageOfferingWithCounts'
    StorageOfferingRequest:
      type: object
      required:
      - name
      - provisioning_mode
      - provider_id
      description: 'Create/update a storage offering. provisioning_mode must match the provider kind (external
        ⇔ kind=external). UPDATE has PATCH semantics: absent fields keep their values, present fields
        change them, and an explicit rate_per_tb_month:null clears the rate back to the card rate; a status
        field flips active|retired — everything validates first and applies as ONE write. Mode + provider
        are IMMUTABLE on update. The required fields apply to CREATE only.'
      properties:
        name:
          type: string
        description:
          type: string
        provisioning_mode:
          type: string
          enum:
          - managed_create
          - managed_adopt
          - external
        provider_id:
          type: string
        default_quota_gb:
          type: integer
          format: int64
        object_lock:
          type: boolean
        rate_per_tb_month:
          type: number
          nullable: true
        notes:
          type: string
        status:
          type: string
          enum:
          - active
          - retired
          description: Update only.
    AssignStorageOfferingRequest:
      type: object
      required:
      - tenant_id
      description: '§18.8 one-click composite: instantiate the offering for the tenant per its mode +
        run the D10 assign. Idempotent by default (an existing live bucket under the offering returns/resumes).'
      properties:
        tenant_id:
          type: string
        bucket_name:
          type: string
          description: REQUIRED for managed_adopt (which existing bucket) and external (the CSP-created
            bucket's name); optional for managed_create.
        quota_gb:
          type: integer
          format: int64
          description: Overrides the offering default when >0.
        access_key_id:
          type: string
          description: 'External mode only: CSP-minted bucket-scoped key id.'
        secret_access_key:
          type: string
          format: password
          description: 'External mode only: delivered to the tenant SHA and DROPPED (D10).'
        repo_name:
          type: string
          description: EBA repository name on the tenant SHA (default managed-{bucket}).
        new_bucket:
          type: boolean
          description: Force an ADDITIONAL bucket when the tenant already has a live one under this offering.
    DetachManagedBucketResponse:
      type: object
      properties:
        status:
          type: string
          enum:
          - detached
        evidence:
          $ref: '#/components/schemas/BucketDeletionEvidence'
    RotateManagedBucketRequest:
      type: object
      description: 'Optional body — EXTERNAL buckets only (§18.8): the CSP-minted REPLACEMENT credential
        (required there; refused for managed buckets, which always mint). Delivered to the tenant SHA
        and dropped (D10). After the swap the OLD key must be revoked at the provider (CSP action — the
        SCA cannot).'
      properties:
        access_key_id:
          type: string
        secret_access_key:
          type: string
          format: password
    DetachManagedBucketRequest:
      type: object
      required:
      - confirm
      properties:
        confirm:
          type: string
          enum:
          - detach
          description: Literal "detach" — irreversible release confirmation (data stays intact).
    OnboardingSiteSpec:
      type: object
      required:
      - name
      properties:
        name:
          type: string
        description:
          type: string
        location:
          type: string
    OnboardingSNASpec:
      type: object
      description: '§16.4 SNA leg: provision a SEPARATE SNA VM into the run''s site. The orchestrator
        fills kind/parent_sha_id/site_id from the ledger; the caller supplies the VM shape (+ enroll_endpoint
        = the SHA the SNA dials, auto-derived from the parent SHA''s IP if omitted).'
      properties:
        provision:
          $ref: '#/components/schemas/ProvisionTenantRequest'
    OnboardingSourceSpec:
      type: object
      required:
      - source_name
      - api_host
      - api_key
      - secret_key
      - zone_name
      description: A CloudStack source platform to add to the SHA (secret is vault-sealed at rest; redacted
        in audit).
      properties:
        source_name:
          type: string
        api_host:
          type: string
        api_key:
          type: string
        secret_key:
          type: string
          description: Vault-sealed at rest; never returned.
        zone_name:
          type: string
        bind_to_site:
          type: boolean
          description: Create the source against the run's created site (default true when a site is created).
    OnboardingDiscoverySpec:
      type: object
      properties:
        discover_cloudstack:
          type: boolean
        discover_vmware:
          type: boolean
        site_scoped:
          type: boolean
          description: Scope discovery to the created site (default true when a site was created).
    OnboardingSpec:
      type: object
      description: 'CSP-W10 §16.9 zero-touch onboarding: configure a tenant SHA end-to-end from ONE POST.
        Exactly one of attach_sha_id or provision drives how the run obtains an appliance; the remaining
        fields sequence the config steps (storage repo assign, site create, SNA provision, source creds,
        discovery). Any secret-bearing field is vault-sealed at rest (D10).'
      properties:
        attach_sha_id:
          type: string
          description: Target an ALREADY-ENROLLED SHA (mutually exclusive with provision).
        provision:
          $ref: '#/components/schemas/ProvisionTenantRequest'
          description: Deploy + orchestrator-approve a fresh SHA (§16.8 seed-ISO). Mutually exclusive
            with attach_sha_id.
        site:
          $ref: '#/components/schemas/OnboardingSiteSpec'
          description: Register a NEW inventory site (mutually exclusive with existing_site_id).
        existing_site_id:
          type: string
          description: Configure against an ALREADY-PRESENT site (which already has an SNA).
        sna:
          $ref: '#/components/schemas/OnboardingSNASpec'
        sources:
          type: array
          items:
            $ref: '#/components/schemas/OnboardingSourceSpec'
        discovery:
          $ref: '#/components/schemas/OnboardingDiscoverySpec'
        storage_offering_id:
          type: string
          description: Assign a managed storage offering (the backup repo) to the SHA as a run step (first
            among the config steps). Empty = don't assign a repo.
    OnboardingRun:
      type: object
      description: A durable onboarding-run state machine record. The request Spec is vault-sealed and
        NEVER returned; progress is read from status/step/ledger.
      properties:
        id:
          type: string
        tenant_id:
          type: string
        sha_id:
          type: string
          description: The appliance the run configures (once known).
        status:
          type: string
          description: pending | running | complete | failed
          enum:
          - pending
          - running
          - complete
          - failed
        step:
          type: string
          description: The NEXT step to run (e.g. provision, await_enroll, wait_online, storage, site,
            sna_provision, sna_wait, sources, discovery, discovery_wait), or empty on a terminal run.
        ledger:
          type: string
          description: 'JSON string: the accumulated output ids (provisioning_record_id, sha_id, site_id,
            sna_provisioning_record_id, source_op_ids, discovery_execution_id, repo_id).'
        last_error:
          type: string
        attempts:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    OnboardingRunListResponse:
      type: object
      properties:
        runs:
          type: array
          items:
            $ref: '#/components/schemas/OnboardingRun'
    CreateOperationRequest:
      type: object
      required:
      - action
      description: Dispatch a delegated MSP operation to a tenant SHA (the SHA enforces msp_access_grants
        per action).
      properties:
        action:
          type: string
          description: Delegated action key (e.g. site.create, source.add_cloudstack_source, discovery.execute,
            appliances.mint_sna_pairing_code). Required.
        target:
          type: object
          description: Arbitrary JSON target for the action (e.g. the VM/backup/site payload).
          additionalProperties: true
        idempotency_key:
          type: string
          description: Caller dedup key; a repeat (sha, key) returns the original record without re-dispatch.
    RemoteOperation:
      type: object
      description: The SCA's idempotency + audit record of a dispatched delegated operation. Secret target
        fields are redacted in target_ref.
      properties:
        id:
          type: string
        tenant_id:
          type: string
        sha_id:
          type: string
        msp_user:
          type: string
          description: on-behalf-of email
        action:
          type: string
        target_ref:
          type: string
          description: JSON target (secrets redacted).
        request_hash:
          type: string
        idempotency_key:
          type: string
        sha_job_id:
          type: string
        status:
          type: string
          enum:
          - pending
          - dispatched
          - succeeded
          - failed
        grant_advisory:
          type: string
          description: advisory pre-check note
        error:
          type: string
        response_status:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
    OperationListResponse:
      type: object
      properties:
        operations:
          type: array
          items:
            $ref: '#/components/schemas/RemoteOperation'
    HostAgentEnrollRequest:
      type: object
      required:
      - install_token
      - agent_public_key
      description: 'D9 host-agent enrollment: a docker-privileged tin enrolls with a mint-token + its
        ed25519 pubkey.'
      properties:
        install_token:
          type: string
        agent_public_key:
          type: string
          description: SSH authorized_keys wire form (ssh-ed25519 …).
        agent_version:
          type: string
        address:
          type: string
        cpu_cores:
          type: integer
        mem_gb:
          type: integer
        disk_gb:
          type: integer
        max_sets:
          type: integer
    MintHostAgentTokenRequest:
      type: object
      description: Mint a one-time host-agent install token.
      properties:
        host_name:
          type: string
        kind:
          type: string
        host_id:
          type: string
        expires_minutes:
          type: integer
    CreateContainerSetRequest:
      type: object
      required:
      - tenant_id
      description: Provision a per-tenant container set (a SHA) on a host agent. host_id comes from the
        path.
      properties:
        tenant_id:
          type: string
        network_mode:
          type: string
          description: ip (default, locked D9 — dedicated per-set IP, SNAs on :443) | port
          enum:
          - port
          - ip
        set_ip:
          type: string
          description: required for ip mode
        volume_root:
          type: string
          description: default /var/lib/sendense-sets/<set>
        bundle_version:
          type: string
          description: image tag; default latest
        registry:
          type: string
        access_tier:
          type: string
          description: proposed tier for the set's SHA (default full_ops)
        expose_api:
          type: boolean
          description: publish the hub API on the host (diagnostics)
    HostAgentAckRequest:
      type: object
      required:
      - command_id
      description: Signed host-agent acknowledgement of a dispatched command result.
      properties:
        command_id:
          type: string
        success:
          type: boolean
        result:
          type: string
        error:
          type: string
    SnoozeAlertRequest:
      type: object
      description: Snooze an alert until a time (default +1h). Provide 'until' OR 'snooze_minutes'.
      properties:
        until:
          type: string
          format: date-time
        snooze_minutes:
          type: integer
