{
  "openapi": "3.1.0",
  "info": {
    "title": "Cloud Pricing Calculators federation API",
    "version": "1.0.0",
    "summary": "The parent site's thin service layer over the Azure, AWS and GCP calculator engines.",
    "description": "Served as the Static Web App managed API under `/api/*` on the parent origin (spine §2).\nThe API is same-origin with the web app, so no CORS is needed.\n\n**Identity.** It comes only from the SWA-injected `x-ms-client-principal` header, or from a\nserver-side email session (`cpc_session` cookie, ADR 0012), both resolved on the server and\nnever from a request body (spine §6). The API issues no JWT; the session cookie is an opaque\nrandom value whose sha256 is the only thing stored.\n\n**Cross-site requests.** Every `/api/auth/*` route, and any POST, PUT or DELETE that a\nsigned-in cookie authenticates, must come from the site's own origin: `Origin` is one of\n`FRONTEND_URL`, `PUBLIC_ORIGIN`, `AUTH_ALLOWED_ORIGINS` or the request's own host, or it is\nabsent and `Sec-Fetch-Site` is `same-origin`. Otherwise `403 cross_site_refused`.\n\n**Builds.** Every endpoint that triggers a build returns `202` within 5 seconds. Clients then\npoll `GET /api/jobs/{id}` or `GET /api/groups/{id}`, which poll the sister engine lazily\n(spine §3–4). Nothing is held open, so Cloudflare's 100 s limit and the managed API's 45 s\nlimit are never reached.\n\n**Fail closed.** If the counter store is unavailable, any spending endpoint answers\n`503 store_unavailable` (spine §7).\n\n**Schemas.** Document shapes live in `../schema/*.json` (JSON Schema 2020-12) and are\nreferenced here, so there is one source of truth. The build bundles them into the published\n`/openapi.json`.\n\n**Correlation.** Every response carries `X-Correlation-Id` and the same value as\n`x-cpc-correlation-id` (architecture §3, §7). A caller may send either header; otherwise\nthe server generates one. It is forwarded to the sisters as `X-CPC-Correlation-Id`.\n\n**Client IP** (ADR 0010). With `TRUST_CF_HEADER=true` (production, with the SWA locked to\nCloudflare ranges) the client IP is `cf-connecting-ip`. Otherwise it is the first\n`x-forwarded-for` entry, port stripped and validated. Only sha256(ip + `IP_HASH_SALT`),\ntruncated to 16 hex characters, is stored or logged.\n\n**Ownership of jobs and groups.** Each job and group records its owner keys (spine §12):\n`user:<id>`, `agent:<keyHash>`, or, for an unsigned visitor, `sid:<hash of the cpc_sid\ncookie>`. The API sets `cpc_sid` (HttpOnly, Secure, SameSite=Lax, Path=/api) on the first\nbuild response of a visitor who has none; such a cookie-less first request is also owned\nby `ip:<ipHash>`, so a client without a cookie jar can still read what it created. Reads\nby anyone else get 404. This closes, on the parent, the gap sister agent-quote polling\nhas (OD-8 finding 7).\n\n**Analytics (fail-open).** The parent counts visits and builds in the sisters' analytics\ndatabase so ChatBotAdmin's Overview shows it as a fourth site (`page: 'cloudpricing'`,\ndocs/tracking-integration.md). Writes start after the response is decided, are never\nawaited, and their failures are logged, never returned: this is the one write path that\nfails open, and no response depends on it. Nothing written carries an email, IP address,\nsession id or request text; visitors are `cpc_u_` / `cpc_s_` / `cpc_ip_` pseudonyms.\n\n**Unknown legacy fields.** Request bodies from older clients may still carry fields this\ndocument no longer lists; the API drops those few known fields before validation and\nignores them, so such requests are not refused.\n"
  },
  "servers": [
    {
      "url": "https://cloudpricingcalculators.com",
      "description": "Production (apex is canonical)"
    },
    {
      "url": "https://staging.cloudpricingcalculators.com",
      "description": "Staging"
    },
    {
      "url": "http://localhost:4280",
      "description": "Local (SWA CLI)"
    }
  ],
  "tags": [
    {
      "name": "quotes"
    },
    {
      "name": "mirror"
    },
    {
      "name": "import"
    },
    {
      "name": "canvases"
    },
    {
      "name": "shares"
    },
    {
      "name": "account"
    },
    {
      "name": "system"
    },
    {
      "name": "auth"
    }
  ],
  "security": [
    {},
    {
      "swaPrincipal": []
    },
    {
      "emailSession": []
    },
    {
      "agentKey": []
    }
  ],
  "paths": {
    "/api/quote": {
      "post": {
        "tags": [
          "quotes"
        ],
        "operationId": "createQuote",
        "summary": "Start a quote build on one engine, or on all three (\"All three\" fan-out).",
        "description": "Charges the caller's daily build limit before contacting any engine. An unsigned\ncaller is charged per IP, a signed-in caller per user, an agent per key; `all`\ncounts as one unit per provider actually started. The per-provider parent ceiling and the estimated-spend ceiling are\ncharged too. With `provider: all`, a provider that is already exhausted\nis listed in `skipped[]` and not charged (ADR 0010), so the UI can say so up front.\nWith a single provider, that case answers 429 `scope: provider`. An unsigned caller whose\nIP cannot be determined gets 400 `client_ip_unavailable` (fail closed).\n`providers: [..]` (2–3 providers) is the subset form of `all`: same group, charging and\n`skipped[]` rules, for just those engines.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the returned URL.",
            "headers": {
              "X-Correlation-Id": {
                "$ref": "#/components/headers/CorrelationId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/jobs/{jobId}": {
      "get": {
        "tags": [
          "quotes"
        ],
        "operationId": "getJob",
        "summary": "Job status. When complete, includes the normalised quote and details.",
        "description": "Each call may make at most one poll of the sister job (at most one every 3 s per\njob). Results are cached on the job document, which expires after 24 h. Not\ncharged. Only the job's owner can read it (`ownerKey`, see the overview); anyone\nelse gets 404.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          },
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^job_[a-z0-9]{20}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current job state.",
            "headers": {
              "X-Correlation-Id": {
                "$ref": "#/components/headers/CorrelationId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/groups/{groupId}": {
      "get": {
        "tags": [
          "quotes"
        ],
        "operationId": "getGroup",
        "summary": "Status of a fan-out or mirror group, one entry per provider.",
        "description": "Triggers the same lazy polling as `GET /api/jobs/{id}` for each unfinished member job.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          },
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^grp_[a-z0-9]{20}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Group state.",
            "headers": {
              "X-Correlation-Id": {
                "$ref": "#/components/headers/CorrelationId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/groups/{groupId}/share": {
      "post": {
        "tags": [
          "shares"
        ],
        "operationId": "shareGroup",
        "summary": "Create a parent share holding the group's finished nodes, laid out as a comparison group.",
        "description": "Added for the MCP `compare_cloud_quote` tool (OD-13), which has no canvas of its own.\nOwner only (the group's `ownerKey`).\nThe server builds the snapshot from the completed member jobs. Unfinished or failed\nmembers appear as nodes in their current status. The share follows the same rules\nas `POST /api/shares` (unsigned callers and agents get 7 days, fixed). Charged\nagainst the share limit.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          },
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^grp_[a-z0-9]{20}$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Share created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`no_completed_jobs`: no member job has completed yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/mirror": {
      "post": {
        "tags": [
          "mirror"
        ],
        "operationId": "mirrorQuote",
        "summary": "Price the same workload on other clouds via the canonical workload description.",
        "description": "Uses `sourceNode.canonical` if present. Otherwise it derives the canonical workload\ndeterministically from `sourceNode.quote` (spine §4; no model call). For each target\nit checks the mirror cache, keyed by sha256(canonicalJSON | target | UTC\ndate). A cache hit returns a job that is already `complete` with `cached: true`, and\ncharges nothing. Each miss is charged as one build and creates a job. The target\nengine chooses SKUs; the federation never maps SKUs or prices.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MirrorRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted.",
            "headers": {
              "X-Correlation-Id": {
                "$ref": "#/components/headers/CorrelationId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MirrorAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/canonical": {
      "post": {
        "tags": [
          "mirror"
        ],
        "operationId": "deriveCanonical",
        "summary": "Derive the canonical workload from a quote. A pure function; no spend, no limit charged.",
        "description": "Deterministic, with no model call in v1 (spine §4). `CANONICAL_ENRICH` must stay `off`;\n`model` is reserved for a later optional enricher that keeps this deterministic\nresult as its fallback (ADR 0003). After a fan-out, the client calls this once the\nfirst member completes and stores the result on the group (ADR 0004).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "quote",
                  "provider"
                ],
                "properties": {
                  "quote": {
                    "$ref": "#/components/schemas/NormalizedQuote"
                  },
                  "provider": {
                    "$ref": "#/components/schemas/Provider"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The canonical workload and its hash.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "canonical",
                    "canonicalHash"
                  ],
                  "properties": {
                    "canonical": {
                      "$ref": "#/components/schemas/CanonicalWorkload"
                    },
                    "canonicalHash": {
                      "type": "string",
                      "pattern": "^[a-f0-9]{64}$"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          }
        }
      }
    },
    "/api/refresh": {
      "post": {
        "tags": [
          "quotes"
        ],
        "operationId": "refreshQuote",
        "summary": "Re-price a node on the same provider as a new build (spine §4, Refresh price).",
        "description": "Renders the request from `canonical`, deriving it from `quote` if absent, and\nsubmits it to the same provider. It is charged as one build and bypasses the mirror\ncache. The sisters' `*-price-check` endpoints are deliberately not used.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/import": {
      "get": {
        "tags": [
          "import"
        ],
        "operationId": "importUrl",
        "summary": "Resolve a sister or parent share link or AI link into canvas content or a build plan.",
        "description": "`url` must be on an allowed host: the three sister domains with or without `www.`,\nor `cloudpricingcalculators.com`, `www.` or `staging.` (spine §9). The federation\nnever fetches the supplied URL. It parses it and, for sister shares only, calls the\nknown sister API base for that host.\n\nFor sister shares, the sister `pageType` maps to a provider: `azurepricing` maps to\nazure, except from the AWS host where it means aws; `awspricing` maps to aws;\n`gcppricing` maps to gcp.\n\nShare-fetch mode `none` returns `409 import_unavailable`.\n\nFor AI links the result is a `plan`; the client then submits the builds through\n`POST /api/quote`. Charged against the import limit (per IP).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          },
          {
            "name": "url",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri",
              "maxLength": 2000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportResult"
                }
              }
            }
          },
          "400": {
            "description": "The URL is not on an allowed host or not a recognised link (`unsupported_url`), the AI link failed to parse (`bad_ailink`, with `reason`), or the caller's IP could not be determined for the per-IP import limit (`client_ip_unavailable`, fail closed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`import_unavailable`: this sister does not expose share payloads to the parent yet (share-fetch mode `none`). The UI offers \"Open on the sister site\" using `openUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportError"
                }
              }
            }
          },
          "410": {
            "$ref": "#/components/responses/Gone"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The sister API answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/canvases": {
      "get": {
        "tags": [
          "canvases"
        ],
        "operationId": "listCanvases",
        "summary": "List the signed-in user's canvases (\"My canvases\").",
        "security": [
          {
            "swaPrincipal": []
          },
          {
            "emailSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "Canvas summaries, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "maxItems": 200,
                      "items": {
                        "$ref": "#/components/schemas/CanvasSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "post": {
        "tags": [
          "canvases"
        ],
        "operationId": "createCanvas",
        "summary": "Create a canvas. Also used to upload the browser canvas on first sign-in.",
        "security": [
          {
            "swaPrincipal": []
          },
          {
            "emailSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CanvasSnapshot"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CanvasDocument"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/canvases/{canvasId}": {
      "parameters": [
        {
          "name": "canvasId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "canvases"
        ],
        "operationId": "getCanvas",
        "security": [
          {
            "swaPrincipal": []
          },
          {
            "emailSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "The canvas.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CanvasDocument"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Not found, or owned by someone else. The two cases are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "put": {
        "tags": [
          "canvases"
        ],
        "operationId": "updateCanvas",
        "summary": "Replace the canvas content (autosave, debounced 1.5 s, and Save). Optimistic concurrency via ETag.",
        "security": [
          {
            "swaPrincipal": []
          },
          {
            "emailSession": []
          }
        ],
        "parameters": [
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "description": "The ETag from the last GET, POST or PUT response.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CanvasSnapshot"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved. A new ETag is returned.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CanvasDocument"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "404": {
            "description": "Not found, or owned by someone else.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "`precondition_failed`: the ETag is stale; someone saved from another tab. The client reloads and merges by node id, last writer wins per node (ADR 0006).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "428": {
            "description": "`precondition_required`: If-Match is missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "canvases"
        ],
        "operationId": "deleteCanvas",
        "security": [
          {
            "swaPrincipal": []
          },
          {
            "emailSession": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "404": {
            "description": "Not found, or owned by someone else.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/shares": {
      "post": {
        "tags": [
          "shares"
        ],
        "operationId": "createShare",
        "summary": "Create an immutable share of a canvas snapshot.",
        "description": "An unsigned caller's share gets `expiresAt` = now + 7 days, fixed (OD-7). A signed-in\ncaller's share never expires. `recipientEmail` requires sign-in (otherwise 401) and\nrestricts opening to that email; no email is sent (spine §5). `scope` picks the whole\nsnapshot or a node subset; edges and groups are trimmed to the chosen nodes.\n`details.raw` is removed when the snapshot is over 1 MB. Charged against the share\nlimit (per IP, or per user when signed in).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShareRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/shares/{shareId}": {
      "get": {
        "tags": [
          "shares"
        ],
        "operationId": "getShare",
        "summary": "Open a share, read-only. Never writes.",
        "parameters": [
          {
            "name": "shareId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The share.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareView"
                }
              }
            }
          },
          "401": {
            "description": "The share is restricted to a recipient and the caller is not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The share is restricted to a different recipient.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "410": {
            "$ref": "#/components/responses/Gone"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "shares"
        ],
        "operationId": "deleteShare",
        "summary": "Revoke a share. Owner only; signed-in shares only, since unsigned shares have no owner.",
        "security": [
          {
            "swaPrincipal": []
          },
          {
            "emailSession": []
          }
        ],
        "parameters": [
          {
            "name": "shareId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked (deleted). The link returns 404 from then on."
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "404": {
            "description": "Not found, or not the caller's share.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/me": {
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "getMe",
        "summary": "The signed-in user (created on first call for an SWA sign-in), and the sign-in methods on offer.",
        "description": "Signed in means an SWA principal (`/.auth/login/{aad|google}`) or a valid email session\n(`cpc_session`, ADR 0012). `authProviders` comes from `AUTH_PROVIDERS` (default `email`);\n`loginUrls` lists only the OAuth providers it enables. A session cookie that does not\nresolve is cleared with `Set-Cookie: cpc_session=; Max-Age=0`. The user projection never\ncarries a password hash, token or session id.\n",
        "responses": {
          "200": {
            "description": "Signed-in state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResponse"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/limits": {
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "getLimits",
        "summary": "The caller's remaining builds today and per-provider availability. Not charged.",
        "responses": {
          "200": {
            "description": "Current allowances.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LimitsResponse"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": [
          "system"
        ],
        "operationId": "getHealth",
        "summary": "Reachability of each sister engine and of the store, without spending any allowance.",
        "description": "For each sister it calls `GET {SISTER_HEALTH_BASE}/agent-quote` with no `jobId`, which\nreturns the contract JSON and costs nothing (agent_quote_core.py :311-351).\n`SISTER_HEALTH_BASE` defaults to `SISTER_API_BASE`. A 200 that is not the JSON contract\n(for example a site's `index.html` fallback) counts as unreachable. Results are cached for 60 s.\n",
        "responses": {
          "200": {
            "description": "Health report. `status` is `degraded` when any provider (`reachable` false) or the store is unreachable; a cold provider (`reachable` null) does not degrade it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/ai-link": {
      "get": {
        "tags": [
          "system"
        ],
        "operationId": "getAiLinkContract",
        "summary": "The parent AI-link contract as data (ADR 0005), for agents such as the MCP compose_quote_link parent form.",
        "description": "Built from the vendored sister vocabulary (`packages/ai-link`), so it cannot drift from\nthe parser. Not charged; touches no store and calls no sister. Cacheable for an hour.\nThe web build may also publish the same object as the static `/ai-link.json`.\n",
        "responses": {
          "200": {
            "description": "The contract.",
            "headers": {
              "X-Correlation-Id": {
                "$ref": "#/components/headers/CorrelationId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiLinkContract"
                }
              }
            }
          }
        }
      }
    },
    "/api/track": {
      "post": {
        "tags": [
          "system"
        ],
        "operationId": "trackVisit",
        "summary": "The web app's once-per-session visit beacon. Anonymous; always 204 for a well-formed body.",
        "description": "Sent with `navigator.sendBeacon` once per browser session (never under\n`navigator.webdriver`). Touches the caller's `AnalyticsUserSessions` row for the UTC\nday, after the response, with a pseudonymous `userId` only. Counted per IP against\n`LIMIT_TRACK_IP_DAILY`; past the limit, or when the counter store is unavailable, the\nbeacon is dropped and still answered `204` (a beacon never fails closed, and it\ncharges and starts nothing). Sets no cookie.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrackRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Received (counted",
            "or dropped by the limit or a store outage).": null
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          }
        }
      }
    },
    "/api/converse": {
      "post": {
        "tags": [
          "quotes"
        ],
        "operationId": "converseTurn",
        "summary": "One turn of Guided mode's clarifying conversation (ADR 0011). Asks; never prices, never starts a build.",
        "description": "The browser sends the whole exchange so far (`messages`, alternating visitor and\nassistant, starting and ending with the visitor). The parent asks a small Claude model\n(app setting `CONVERSE_MODEL`, default `claude-haiku-5-5`) for the next clarifying\nquestion with suggested answers, or, once the brief is complete, for a self-contained\nplain-language `request` (at most 1000 characters, accepted by the same validator as\n`POST /api/quote`). The browser then sends that request through `POST /api/quote`; this\nendpoint never triggers a build.\n\n**The model never prices.** Any reply, option or request carrying a currency amount or\na link is rejected and the model is asked once more; a second failure answers a neutral\nquestion instead. Visitor text is wrapped as data in the prompt; no tools, no fetching.\n\n**Limits, fail closed.** At most 6 visitor turns per conversation (`429 scope:\nconversation`). Each turn is charged before the model is called: per IP\n(`LIMIT_CONVERSE_IP_DAILY`, default 40, `scope: converse_ip`) or per signed-in user\n(`LIMIT_CONVERSE_USER_DAILY`, default 200, `scope: converse_user`), then against\n`SPEND_CEILING_USD_DAILY` at `EST_COST_PER_TURN_USD` (default 0.003). A counter-store\nerror is `503 store_unavailable`; no `ANTHROPIC_API_KEY`, or no answer from the model,\nis `503 converse_unavailable` (the UI falls back to Quick mode). On the 6th turn the\nanswer is always `ready`.\n\n**Nothing is stored.** The log line has the turn number, attempts, outcome, latency and\ntoken counts, never text. The whole turn is answered in about 15 s at most, well under\nthe managed API's 45 s.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CorrelationId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConverseRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The next question (with `options`), or `ready` with the request to price.",
            "headers": {
              "X-Correlation-Id": {
                "$ref": "#/components/headers/CorrelationId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConverseResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/auth/signup": {
      "post": {
        "tags": [
          "auth"
        ],
        "operationId": "authSignup",
        "summary": "Start email sign-up. Mails a single-use set-password link (24 h). Always the same 202.",
        "description": "ADR 0012. A new address gets a \"Set your password\" mail with a link to\n`/reset-password?token=…` (valid 24 h, single use). An address that already has an\naccount gets a \"You already have an account\" mail with a 1-hour reset link instead, and\nthe response is **identical**, so the endpoint never reveals whether an address exists.\n\nRefused cross-site (`403 cross_site_refused`: `Origin` must be one of the site's own\norigins, or absent with `Sec-Fetch-Site: same-origin`). Limits (UTC hour, fail closed):\n10 per IP (`auth_ip`), and 3 mails per address shared with `/api/auth/forgot`\n(`auth_email`). No email settings: `503 email_unavailable`. A send that ACS refuses:\n`502 email_send_failed` (for either kind of mail, so it reveals nothing either).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthEmailRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. The body never depends on whether the address has an account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthBadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "`email_send_failed`: the email service did not accept the message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/auth/forgot": {
      "post": {
        "tags": [
          "auth"
        ],
        "operationId": "authForgot",
        "summary": "\"Forgot password\". Mails a 1-hour reset link to an account's address. Always the same 202.",
        "description": "ADR 0012. An address with a password gets a reset link (1 h, single use, as on the\nsister); any other address gets nothing. The answer is the same either way, and a send\nfailure is logged, never returned. Refused cross-site. Limits: 10 per IP per hour\n(`auth_ip`) and the 3-mails-per-address hour shared with sign-up (`auth_email`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthEmailRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. The body never depends on whether the address has an account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthBadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/auth/password": {
      "post": {
        "tags": [
          "auth"
        ],
        "operationId": "authSetPassword",
        "summary": "Set a password with an emailed token (first set or reset), and sign in.",
        "description": "ADR 0012. Both emailed links land here. The token is consumed atomically (a conditional\ndelete), so it works once; an unknown, forged, expired, already used or superseded token\nall get the same `400 invalid_token`. A password that fails the policy is\n`400 weak_password` and leaves the link usable. On success the account is created (first\nset) or updated, every other session of the account is revoked, and a new session cookie\nis set (`cpc_session`, HttpOnly, Secure, SameSite=Lax, Path=/, 30 days).\n\nRefused cross-site. Limits: 10 per IP per hour (`auth_ip`), 10 per address per hour\n(`auth_email`), and 8 bad tokens per IP per hour block that IP for an hour\n(`auth_lockout`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthPasswordRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password set; signed in.",
            "headers": {
              "Set-Cookie": {
                "$ref": "#/components/headers/SessionCookie"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSignedIn"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthBadRequest"
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "409": {
            "description": "`conflict`: the account changed while the password was being written. Try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/auth/login": {
      "post": {
        "tags": [
          "auth"
        ],
        "operationId": "authLogin",
        "summary": "Sign in with email and password. Every failure is the same 401.",
        "description": "ADR 0012. Success rotates the session: any presented `cpc_session` is deleted and a new\none is set. An unknown address, an account without a password and a wrong password all\nget the same `401 invalid_credentials`, after the same scrypt work.\n\nRefused cross-site. Limits: 20 attempts per IP per hour (`auth_ip`). Lockout\n(`auth_lockout`, 1 hour): 8 failures in an hour for one IP and address, or 20 for one\naddress from any IP. Failures are counted whether or not the address has an account.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthLoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in.",
            "headers": {
              "Set-Cookie": {
                "$ref": "#/components/headers/SessionCookie"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSignedIn"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthBadRequest"
          },
          "401": {
            "description": "`invalid_credentials`, always with the same message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/auth/logout": {
      "post": {
        "tags": [
          "auth"
        ],
        "operationId": "authLogout",
        "summary": "Sign out of an email session. The session is deleted server-side and the cookie cleared.",
        "description": "ADR 0012. Always clears `cpc_session` (Max-Age=0), even if the store is unreachable\n(then `503`, and the server-side session expires within its 30 days). An SWA sign-in\n(Microsoft or Google) signs out at `/.auth/logout` instead. Refused cross-site.\n",
        "responses": {
          "204": {
            "description": "Signed out (or there was no session).",
            "headers": {
              "Set-Cookie": {
                "$ref": "#/components/headers/SessionCookie"
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/api/auth/change-password": {
      "post": {
        "tags": [
          "auth"
        ],
        "operationId": "authChangePassword",
        "summary": "Change the signed-in email user's password. Revokes every other session.",
        "description": "ADR 0012. Needs an email session (`401` without one, `409 oauth_account` for a\nMicrosoft or Google sign-in). `401 incorrect_password` when the current password is\nwrong; `400 weak_password` / `same_password` for the new one. Success sets a fresh\nsession cookie; every other session of the account stops working. Refused cross-site.\nLimit: 10 per account per hour (`auth_email`).\n",
        "security": [
          {
            "emailSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthChangePasswordRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed; signed in with a fresh session.",
            "headers": {
              "Set-Cookie": {
                "$ref": "#/components/headers/SessionCookie"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthSignedIn"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthBadRequest"
          },
          "401": {
            "description": "`unauthenticated` (no email session) or `incorrect_password`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/CrossSite"
          },
          "409": {
            "description": "`oauth_account`: a Microsoft or Google account has no password here; or `conflict`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "swaPrincipal": {
        "type": "apiKey",
        "in": "header",
        "name": "x-ms-client-principal",
        "description": "Base64 JSON `{identityProvider, userId, userDetails, userRoles}`, injected by Azure\nStatic Web Apps after `/.auth/login/{aad|google}`. Clients never set it. The managed\nAPI is reachable only through the SWA, which strips any client-supplied copy.\n"
      },
      "agentKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Agent-Key",
        "description": "An optional key for agents such as the MCP server (OD-13). Keys are listed, hashed,\nin app setting `AGENT_KEYS_SHA256`. A registered key is charged against\n`LIMIT_AGENT_KEY_DAILY` (default 100) instead of the per-IP unsigned limit. An\nunknown key is ignored, and the caller is treated as an unsigned visitor.\n"
      },
      "emailSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "cpc_session",
        "description": "A server-side email session (ADR 0012), set by `/api/auth/login`, `/api/auth/password`\nand `/api/auth/change-password`: 32 random bytes, HttpOnly, Secure, SameSite=Lax, Path=/,\nMax-Age 30 days. Only its sha256 is stored (Cosmos `sessions`, TTL 30 days). Not a JWT:\nnothing in it can be read or forged. Resolved once per request; a cookie that does not\nresolve is cleared. State-changing requests it authenticates must come from the site's\nown origin (`403 cross_site_refused` otherwise).\n"
      }
    },
    "parameters": {
      "CorrelationId": {
        "name": "X-Correlation-Id",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9-]{8,64}$"
        }
      }
    },
    "headers": {
      "CorrelationId": {
        "schema": {
          "type": "string"
        }
      },
      "RetryAfter": {
        "description": "Seconds. On job polls, the suggested poll interval.",
        "schema": {
          "type": "integer"
        }
      },
      "ETag": {
        "description": "An opaque version of the canvas (the Cosmos _etag).",
        "schema": {
          "type": "string"
        }
      },
      "SessionCookie": {
        "description": "`cpc_session=<32 random bytes, base64url>; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax`, or the same name with an empty value and Max-Age=0 to clear it.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "CrossSite": {
        "description": "`cross_site_refused`: a state-changing request that did not come from the site's own origin (ADR 0012). Sent for every auth route, and for any POST, PUT or DELETE a signed-in cookie authenticates.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AuthBadRequest": {
        "description": "`invalid_json`, `validation_failed`, `invalid_email`, `invalid_token` (unknown, forged,\nexpired, used or superseded: one message for all), `weak_password` (the message states the\npolicy), `same_password`, or `client_ip_unavailable`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Validation failed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationError"
            }
          }
        }
      },
      "Unauthenticated": {
        "description": "Sign-in required.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Gone": {
        "description": "`share_expired`: the share has passed `expiresAt`, even if the TTL sweep has not removed it yet.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "`payload_too_large`: a canvas over 1.5 MB, or a share snapshot still over 1 MB after `details.raw` is removed (ADR 0006), or a request body over the endpoint's byte cap (quote and converse 16 KiB; mirror, canonical and refresh 512 KiB).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "A parent limit is spent (spine §7). Sister 429s do not appear here; they become job status `rate_limited`.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitedError"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "The spend ceiling is reached, the counter store is down (fail closed), or a sister is unreachable on a synchronous step.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ServiceUnavailableError"
            }
          }
        }
      }
    },
    "schemas": {
      "Provider": {
        "$ref": "#/components/schemas/schema_quote/$defs/Provider"
      },
      "PriceSource": {
        "$ref": "#/components/schemas/schema_quote/$defs/PriceSource"
      },
      "Role": {
        "$ref": "#/components/schemas/schema_quote/$defs/Role"
      },
      "LineItem": {
        "$ref": "#/components/schemas/schema_quote/$defs/LineItem"
      },
      "NormalizedQuote": {
        "$ref": "#/components/schemas/schema_quote/$defs/NormalizedQuote"
      },
      "QuoteDetails": {
        "$ref": "#/components/schemas/schema_quote/$defs/QuoteDetails"
      },
      "SisterRef": {
        "$ref": "#/components/schemas/schema_quote/$defs/SisterRef"
      },
      "CanonicalWorkload": {
        "$ref": "#/components/schemas/schema_canonicalWorkload"
      },
      "NodeDocument": {
        "$ref": "#/components/schemas/schema_nodeDocument"
      },
      "CanvasDocument": {
        "$ref": "#/components/schemas/schema_canvasDocument"
      },
      "CanvasSnapshot": {
        "$ref": "#/components/schemas/schema_canvasDocument/$defs/CanvasSnapshot"
      },
      "ShareDocument": {
        "$ref": "#/components/schemas/schema_shareDocument"
      },
      "UserDocument": {
        "$ref": "#/components/schemas/schema_userDocument"
      },
      "ProviderOrAll": {
        "type": "string",
        "enum": [
          "azure",
          "aws",
          "gcp",
          "all"
        ]
      },
      "JobStatus": {
        "type": "string",
        "enum": [
          "queued",
          "running",
          "complete",
          "needs_more_detail",
          "rate_limited",
          "failed"
        ],
        "description": "`queued`: created; the sister has not accepted yet, or accepted but not started.\n`running`: the sister is building. `complete`: a result is attached.\n`needs_more_detail`: the sister answered 200 needs_more_detail; `message` and `hint`\nsay what to add. `rate_limited`: the sister answered 429, or the parent's ceiling\nfor this provider is spent; see `retryAfterSeconds`. `failed`: sister 4xx/5xx,\ntimeout (no sister progress for 180 s), or an unparseable result.\n"
      },
      "QuoteRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Send exactly one of `provider` (one engine, or `all`) and `providers` (a 2–3 provider subset, fanned out like `all`).",
        "required": [
          "request"
        ],
        "anyOf": [
          {
            "required": [
              "provider"
            ]
          },
          {
            "required": [
              "providers"
            ]
          }
        ],
        "not": {
          "required": [
            "provider",
            "providers"
          ]
        },
        "properties": {
          "provider": {
            "$ref": "#/components/schemas/ProviderOrAll"
          },
          "providers": {
            "type": "array",
            "minItems": 2,
            "maxItems": 3,
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/Provider"
            },
            "description": "A subset fan-out (added for the MCP compare_cloud_quote subset form): creates a group for exactly these providers, with the same rules, charging and skipped[] as `provider: all`."
          },
          "request": {
            "type": "string",
            "minLength": 12,
            "maxLength": 1000,
            "description": "Plain-language workload description. Whitespace is collapsed before the length check, matching the sister rule (agent_quote_core.py :542-556)."
          },
          "quoteName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "canvasContext": {
            "type": "string",
            "maxLength": 600,
            "description": "A short summary of what is already on the canvas (\"knows everything already on\nit\"). It is appended to `request` only if the total stays at or under 1000\ncharacters. Otherwise it is dropped whole and the job records\n`contextDropped: true` (ADR 0002). The request itself is never trimmed.\n"
          },
          "parentNodeId": {
            "type": "string",
            "maxLength": 64,
            "description": "Child lineage from the + handle; echoed on the job."
          }
        }
      },
      "QuoteAccepted": {
        "type": "object",
        "additionalProperties": false,
        "description": "A single provider returns `jobId` + `pollUrl`. `all` (or a `providers` subset) returns `groupId` + `pollUrl` (the group URL) + `jobs`.",
        "required": [
          "pollUrl",
          "pollAfterSeconds"
        ],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "groupId": {
            "type": "string"
          },
          "pollUrl": {
            "type": "string",
            "description": "Relative URL: /api/jobs/{id} or /api/groups/{id}."
          },
          "pollAfterSeconds": {
            "type": "integer",
            "minimum": 1,
            "default": 3
          },
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobSummary"
            }
          },
          "skipped": {
            "type": "array",
            "description": "Fan-out and mirror only. Providers not started, and not charged.",
            "items": {
              "$ref": "#/components/schemas/SkippedProvider"
            }
          }
        }
      },
      "SkippedProvider": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "provider",
          "reason",
          "message"
        ],
        "properties": {
          "provider": {
            "$ref": "#/components/schemas/Provider"
          },
          "reason": {
            "type": "string",
            "enum": [
              "provider_limit",
              "sister_rate_limited",
              "spend_ceiling"
            ]
          },
          "message": {
            "type": "string"
          },
          "retryAfterSeconds": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "JobSummary": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "provider",
          "jobId",
          "status",
          "cached"
        ],
        "properties": {
          "provider": {
            "$ref": "#/components/schemas/Provider"
          },
          "jobId": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "cached": {
            "type": "boolean"
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "JobResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "quote",
          "details",
          "sister",
          "canonical"
        ],
        "properties": {
          "quote": {
            "$ref": "#/components/schemas/NormalizedQuote"
          },
          "details": {
            "$ref": "#/components/schemas/QuoteDetails"
          },
          "sister": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/SisterRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "canonical": {
            "description": "The canonical workload derived from this result. For mirror jobs, the source canonical that produced the request.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/CanonicalWorkload"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "Job": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "jobId",
          "kind",
          "provider",
          "status",
          "cached",
          "createdAt",
          "updatedAt",
          "pollAfterSeconds"
        ],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "quote",
              "fanout",
              "mirror",
              "refresh",
              "import"
            ]
          },
          "provider": {
            "$ref": "#/components/schemas/Provider"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "groupId": {
            "type": [
              "string",
              "null"
            ]
          },
          "parentNodeId": {
            "type": [
              "string",
              "null"
            ]
          },
          "quoteName": {
            "type": [
              "string",
              "null"
            ]
          },
          "request": {
            "type": "string",
            "description": "The exact request text sent to the sister."
          },
          "cached": {
            "type": "boolean",
            "description": "True when served from the mirror cache; no allowance spent."
          },
          "contextDropped": {
            "type": "boolean",
            "description": "True when canvasContext was dropped to keep the request within 1000 characters."
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "description": "The sister's or parent's user-facing message for needs_more_detail",
            "rate_limited or failed.": null
          },
          "hint": {
            "type": [
              "string",
              "null"
            ]
          },
          "retryAfterSeconds": {
            "type": [
              "integer",
              "null"
            ]
          },
          "sisterAllowanceRemaining": {
            "type": [
              "integer",
              "null"
            ],
            "description": "From the sister's rateLimit block when it reported one."
          },
          "previousTotal": {
            "type": [
              "number",
              "null"
            ],
            "description": "Refresh jobs only: the totalMonthlyCost of the quote being refreshed, for the delta display (test-plan T-P3-09). Null otherwise."
          },
          "result": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/JobResult"
              },
              {
                "type": "null"
              }
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "pollAfterSeconds": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "Group": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "groupId",
          "kind",
          "name",
          "jobs",
          "createdAt"
        ],
        "properties": {
          "groupId": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "fanout",
              "mirror"
            ]
          },
          "name": {
            "type": "string",
            "maxLength": 80
          },
          "sourceProvider": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "azure",
              "aws",
              "gcp",
              null
            ]
          },
          "canonical": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CanonicalWorkload"
              },
              {
                "type": "null"
              }
            ]
          },
          "canonicalHash": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[a-f0-9]{64}$"
          },
          "jobs": {
            "type": "array",
            "minItems": 0,
            "maxItems": 3,
            "items": {
              "$ref": "#/components/schemas/JobSummary"
            }
          },
          "skipped": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SkippedProvider"
            }
          },
          "complete": {
            "type": "boolean",
            "description": "True when every member is in a terminal status."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MirrorRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "sourceNode",
          "targetProviders"
        ],
        "properties": {
          "sourceNode": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "provider",
              "quote"
            ],
            "properties": {
              "id": {
                "type": "string",
                "maxLength": 64
              },
              "provider": {
                "$ref": "#/components/schemas/Provider"
              },
              "quote": {
                "$ref": "#/components/schemas/NormalizedQuote"
              },
              "canonical": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/CanonicalWorkload"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "targetProviders": {
            "type": "array",
            "minItems": 1,
            "maxItems": 2,
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/Provider"
            },
            "description": "Must not include `sourceNode.provider` (400 `invalid_target`)."
          }
        }
      },
      "MirrorAccepted": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "groupId",
          "canonical",
          "canonicalHash",
          "pollUrl",
          "jobs"
        ],
        "properties": {
          "groupId": {
            "type": "string"
          },
          "canonical": {
            "$ref": "#/components/schemas/CanonicalWorkload"
          },
          "canonicalHash": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$"
          },
          "pollUrl": {
            "type": "string"
          },
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobSummary"
            }
          },
          "skipped": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SkippedProvider"
            }
          }
        }
      },
      "RefreshRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "provider",
          "quote"
        ],
        "properties": {
          "nodeId": {
            "type": "string",
            "maxLength": 64
          },
          "provider": {
            "$ref": "#/components/schemas/Provider"
          },
          "quote": {
            "$ref": "#/components/schemas/NormalizedQuote"
          },
          "canonical": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CanonicalWorkload"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "AiLinkPlanService": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "slug",
          "known",
          "canonicalName",
          "pairs",
          "request"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "known": {
            "type": "boolean",
            "description": "False for slugs outside the vocabulary; they pass through, as on the sisters."
          },
          "canonicalName": {
            "type": "string"
          },
          "pairs": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "request": {
            "type": "string",
            "maxLength": 1000,
            "description": "Rendered by the vendored render.js: \"Price ONLY X for this request…\"."
          }
        }
      },
      "AiLinkPlan": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "comparison",
          "blocks"
        ],
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60
          },
          "comparison": {
            "type": "boolean",
            "description": "True for a multi-block parent link; the client lands the results as a comparison group."
          },
          "blocks": {
            "type": "array",
            "minItems": 1,
            "maxItems": 3,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "provider",
                "services"
              ],
              "properties": {
                "provider": {
                  "$ref": "#/components/schemas/Provider"
                },
                "services": {
                  "type": "array",
                  "minItems": 1,
                  "maxItems": 3,
                  "items": {
                    "$ref": "#/components/schemas/AiLinkPlanService"
                  }
                }
              }
            }
          }
        }
      },
      "ImportResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "kind",
          "origin"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "sister-share",
              "sister-ailink",
              "parent-share",
              "parent-ailink"
            ]
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "azure",
              "aws",
              "gcp",
              null
            ],
            "description": "Set for sister links and single-block AI links."
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "nodes": {
            "type": "array",
            "description": "For share kinds. Nodes carry `origin` (site and shareId) so the user can jump back.",
            "items": {
              "$ref": "#/components/schemas/NodeDocument"
            }
          },
          "edges": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/schema_canvasDocument/$defs/Edge"
            }
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/schema_canvasDocument/$defs/Group"
            }
          },
          "plan": {
            "description": "For AI-link kinds.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/AiLinkPlan"
              },
              {
                "type": "null"
              }
            ]
          },
          "origin": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "site",
              "url"
            ],
            "properties": {
              "site": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "shareId": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      },
      "CanvasSummary": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "name",
          "updatedAt",
          "nodeCount",
          "providers"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "nodeCount": {
            "type": "integer",
            "minimum": 0
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Provider"
            }
          }
        }
      },
      "ShareRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "snapshot",
          "scope"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "snapshot": {
            "$ref": "#/components/schemas/CanvasSnapshot",
            "description": "The client's current canvas (the server cannot see an unsigned browser canvas)."
          },
          "scope": {
            "description": "`canvas` shares everything in the snapshot; `{nodeIds}` shares that subset. Unknown ids give 400 `invalid_scope`.",
            "oneOf": [
              {
                "const": "canvas"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "nodeIds"
                ],
                "properties": {
                  "nodeIds": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 200,
                    "uniqueItems": true,
                    "items": {
                      "type": "string",
                      "maxLength": 64
                    }
                  }
                }
              }
            ]
          },
          "recipientEmail": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "sourceCanvasId": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ShareCreated": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "shareId",
          "url",
          "signed",
          "expiresAt"
        ],
        "properties": {
          "shareId": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "https://cloudpricingcalculators.com/?share=<uuid>, using the serving environment origin."
          },
          "signed": {
            "type": "boolean"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "recipientRestricted": {
            "type": "boolean"
          },
          "mailto": {
            "type": [
              "string",
              "null"
            ],
            "description": "A prefilled mailto: link when recipientEmail was given. No email is sent."
          }
        }
      },
      "ShareView": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "shareId",
          "name",
          "createdAt",
          "expiresAt",
          "readOnly",
          "recipientRestricted",
          "snapshot"
        ],
        "properties": {
          "shareId": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "readOnly": {
            "const": true
          },
          "recipientRestricted": {
            "type": "boolean"
          },
          "isOwner": {
            "type": "boolean"
          },
          "ownerDisplay": {
            "type": "string",
            "description": "The text \"a Cloud Pricing Calculators user\", or the display name the owner opted to show."
          },
          "recipientEmailMasked": {
            "type": [
              "string",
              "null"
            ],
            "description": "For example j***@example.com. Never the raw email."
          },
          "snapshot": {
            "$ref": "#/components/schemas/CanvasSnapshot"
          }
        }
      },
      "AuthProviders": {
        "description": "The sign-in methods the browser offers, from app setting `AUTH_PROVIDERS` (default\n`email` only, ADR 0012). With only `email`, the Microsoft and Google buttons are hidden\neverywhere. An existing SWA sign-in still works whatever the list says.\n",
        "type": "array",
        "minItems": 1,
        "maxItems": 3,
        "uniqueItems": true,
        "items": {
          "type": "string",
          "enum": [
            "email",
            "google",
            "aad"
          ]
        }
      },
      "PublicUser": {
        "description": "The only projection of a user document any response carries. Never a password hash, token or session id.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "provider",
          "email",
          "displayName",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "`aad:<id>`, `google:<id>`, or `email:<sha256 of the normalised address>`."
          },
          "provider": {
            "type": "string",
            "enum": [
              "aad",
              "google",
              "email"
            ]
          },
          "email": {
            "type": "string"
          },
          "displayName": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MeResponse": {
        "oneOf": [
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "signedIn",
              "authProviders",
              "loginUrls"
            ],
            "properties": {
              "signedIn": {
                "const": false
              },
              "authProviders": {
                "$ref": "#/components/schemas/AuthProviders"
              },
              "loginUrls": {
                "type": "object",
                "additionalProperties": false,
                "description": "The SWA login URL of each OAuth provider `authProviders` enables; empty by default.",
                "properties": {
                  "aad": {
                    "type": "string",
                    "example": "/.auth/login/aad"
                  },
                  "google": {
                    "type": "string",
                    "example": "/.auth/login/google"
                  }
                }
              }
            }
          },
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "signedIn",
              "user",
              "authProviders",
              "loginUrls"
            ],
            "properties": {
              "signedIn": {
                "const": true
              },
              "authProviders": {
                "$ref": "#/components/schemas/AuthProviders"
              },
              "loginUrls": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "aad": {
                    "type": "string"
                  },
                  "google": {
                    "type": "string"
                  }
                }
              },
              "user": {
                "$ref": "#/components/schemas/PublicUser"
              }
            }
          }
        ]
      },
      "AuthEmailRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254,
            "description": "Trimmed and lower-cased before use."
          }
        }
      },
      "AuthLoginRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254
          },
          "password": {
            "type": "string",
            "maxLength": 1024
          }
        }
      },
      "AuthPasswordRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "token",
          "password"
        ],
        "properties": {
          "token": {
            "type": "string",
            "maxLength": 256,
            "description": "The `token` query parameter of the emailed /reset-password link."
          },
          "password": {
            "type": "string",
            "maxLength": 1024,
            "description": "10 to 72 characters (72 UTF-8 bytes); not the address or its local part; not a common password (the sister's policy)."
          }
        }
      },
      "AuthChangePasswordRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "currentPassword",
          "newPassword"
        ],
        "properties": {
          "currentPassword": {
            "type": "string",
            "maxLength": 1024
          },
          "newPassword": {
            "type": "string",
            "maxLength": 1024
          }
        }
      },
      "AuthAccepted": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "status",
          "message"
        ],
        "properties": {
          "status": {
            "const": "sent"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "AuthSignedIn": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "signedIn",
          "user"
        ],
        "properties": {
          "signedIn": {
            "const": true
          },
          "user": {
            "$ref": "#/components/schemas/PublicUser"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "LimitsResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "scope",
          "limit",
          "remaining",
          "resetsAt",
          "providers",
          "spend"
        ],
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "ip",
              "user",
              "agent_key"
            ]
          },
          "limit": {
            "type": "integer"
          },
          "remaining": {
            "type": "number",
            "description": "Builds left today for this caller. An \"All three\" costs 3."
          },
          "resetsAt": {
            "type": "string",
            "format": "date-time",
            "description": "Next 00:00 UTC."
          },
          "providers": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "azure",
              "aws",
              "gcp"
            ],
            "properties": {
              "azure": {
                "$ref": "#/components/schemas/ProviderAllowance"
              },
              "aws": {
                "$ref": "#/components/schemas/ProviderAllowance"
              },
              "gcp": {
                "$ref": "#/components/schemas/ProviderAllowance"
              }
            }
          },
          "spend": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "ceilingUsd",
              "exhausted"
            ],
            "properties": {
              "ceilingUsd": {
                "type": "number"
              },
              "exhausted": {
                "type": "boolean",
                "description": "True when the estimated sister spend today has reached the ceiling. The used amount is not exposed publicly."
              }
            }
          }
        }
      },
      "ProviderAllowance": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "available",
          "mode"
        ],
        "properties": {
          "available": {
            "type": "boolean",
            "description": "False when the parent ceiling for this provider is spent today, or the sister last returned 429 with a retry-after that has not yet passed."
          },
          "mode": {
            "type": "string",
            "enum": [
              "agent",
              "trusted",
              "fixture"
            ],
            "description": "`fixture` = recorded responses for offline development and e2e; never set in staging or prod."
          },
          "retryAfterSeconds": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "status",
          "version",
          "store",
          "providers",
          "checkedAt"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded",
              "down"
            ]
          },
          "version": {
            "type": "string",
            "description": "Build version (git sha)."
          },
          "environment": {
            "type": "string",
            "enum": [
              "local",
              "staging",
              "prod"
            ]
          },
          "store": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "reachable"
            ],
            "properties": {
              "reachable": {
                "type": "boolean"
              },
              "latencyMs": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "providers": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "azure",
              "aws",
              "gcp"
            ],
            "properties": {
              "azure": {
                "$ref": "#/components/schemas/ProviderHealth"
              },
              "aws": {
                "$ref": "#/components/schemas/ProviderHealth"
              },
              "gcp": {
                "$ref": "#/components/schemas/ProviderHealth"
              }
            }
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AiLinkContract": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "version",
          "sisterVersion",
          "origin",
          "grammar",
          "limits",
          "providers",
          "errorReasons",
          "examples"
        ],
        "properties": {
          "version": {
            "type": "string",
            "description": "Parent grammar version, e.g. parent-1.0.0."
          },
          "sisterVersion": {
            "type": "string",
            "description": "The vendored sister AI_LINK_VERSION."
          },
          "origin": {
            "type": "string",
            "description": "The serving environment origin."
          },
          "grammar": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "prefix",
              "single",
              "multi",
              "providerTokens",
              "services",
              "specs",
              "rules"
            ],
            "properties": {
              "prefix": {
                "const": "/q/"
              },
              "single": {
                "type": "string"
              },
              "multi": {
                "type": "string"
              },
              "providerTokens": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Provider"
                }
              },
              "services": {
                "type": "string"
              },
              "specs": {
                "type": "string"
              },
              "rules": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "limits": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "maxServices",
              "maxPathChars",
              "maxPairsPerSpec",
              "maxNameChars"
            ],
            "properties": {
              "maxServices": {
                "type": "integer",
                "const": 3
              },
              "maxPathChars": {
                "type": "integer",
                "const": 800
              },
              "maxPairsPerSpec": {
                "type": "integer"
              },
              "maxNameChars": {
                "type": "integer"
              }
            }
          },
          "providers": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "azure",
              "aws",
              "gcp"
            ],
            "properties": {
              "azure": {
                "$ref": "#/components/schemas/AiLinkVocabulary"
              },
              "aws": {
                "$ref": "#/components/schemas/AiLinkVocabulary"
              },
              "gcp": {
                "$ref": "#/components/schemas/AiLinkVocabulary"
              }
            }
          },
          "errorReasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "examples": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "path",
                "valid",
                "note"
              ],
              "properties": {
                "path": {
                  "type": "string"
                },
                "valid": {
                  "type": "boolean"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "AiLinkVocabulary": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "display",
          "sisterOrigin",
          "services",
          "specKeys",
          "regions"
        ],
        "properties": {
          "display": {
            "type": "string"
          },
          "sisterOrigin": {
            "type": "string",
            "format": "uri"
          },
          "services": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "slug",
                "name",
                "aliases"
              ],
              "properties": {
                "slug": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "aliases": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "specKeys": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "key",
                "meaning",
                "example"
              ],
              "properties": {
                "key": {
                  "type": "string"
                },
                "meaning": {
                  "type": "string"
                },
                "example": {
                  "type": "string"
                }
              }
            }
          },
          "regions": {
            "type": "object",
            "description": "Link region token → the display the engine reads (render.js REGION_DISPLAY).",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "ProviderHealth": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "reachable",
          "mode",
          "shareFetchMode"
        ],
        "properties": {
          "reachable": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`null` with `error: cold` when the probe timed out (usually a cold sister instance). A timeout reports `false` with `error: timeout` only when the sister last answered its contract more than 15 minutes earlier (or this instance has probed it for 15 minutes without an answer). `error: not_contract` is a 200 that was not the JSON contract."
          },
          "latencyMs": {
            "type": [
              "integer",
              "null"
            ]
          },
          "mode": {
            "type": "string",
            "enum": [
              "agent",
              "trusted",
              "fixture"
            ]
          },
          "shareFetchMode": {
            "type": "string",
            "enum": [
              "payload",
              "claim",
              "none"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "TrackRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "page",
          "path"
        ],
        "properties": {
          "page": {
            "type": "string",
            "enum": [
              "canvas",
              "share",
              "import",
              "ai-link"
            ],
            "description": "Which SPA view loaded. The analytics row's own `page` is always the site identifier (`cloudpricing`)."
          },
          "path": {
            "type": "string",
            "enum": [
              "/",
              "/q",
              "/import"
            ],
            "description": "The SPA route, reduced to its prefix. Never a query string, share id or link."
          }
        }
      },
      "ConverseMessage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "role",
          "text"
        ],
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant"
            ]
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000,
            "description": "Whitespace is collapsed first. Treated as data, never as instructions."
          }
        }
      },
      "ConverseRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "messages"
        ],
        "not": {
          "required": [
            "provider",
            "providers"
          ]
        },
        "properties": {
          "messages": {
            "type": "array",
            "minItems": 1,
            "maxItems": 13,
            "items": {
              "$ref": "#/components/schemas/ConverseMessage"
            },
            "description": "The exchange so far, alternating user and assistant, first and last `user`. At most 6 user turns (a 7th is 429 with scope `conversation`) and 8000 characters in all."
          },
          "provider": {
            "$ref": "#/components/schemas/ProviderOrAll"
          },
          "providers": {
            "type": "array",
            "minItems": 2,
            "maxItems": 3,
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/Provider"
            }
          }
        }
      },
      "ConverseResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "reply",
          "options",
          "ready",
          "request",
          "turn",
          "maxTurns",
          "turnsLeft"
        ],
        "properties": {
          "reply": {
            "type": "string",
            "minLength": 1,
            "maxLength": 600,
            "description": "The next question, or a one-line note when ready. Plain text; never a price."
          },
          "options": {
            "type": "array",
            "maxItems": 6,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 80
            },
            "description": "Suggested answers to show as chips. Empty when ready. Free text is always allowed."
          },
          "ready": {
            "type": "boolean"
          },
          "request": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 12,
            "maxLength": 1000,
            "description": "When ready, the self-contained request to send to POST /api/quote; otherwise null."
          },
          "turn": {
            "type": "integer",
            "minimum": 1,
            "maximum": 6,
            "description": "The visitor turn just answered."
          },
          "maxTurns": {
            "type": "integer",
            "const": 6
          },
          "turnsLeft": {
            "type": "integer",
            "minimum": 0,
            "maximum": 5
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A machine code, e.g. `not_found`, `share_expired`, `unauthenticated`, `forbidden`,\n`precondition_failed`, `precondition_required`, `payload_too_large`,\n`no_completed_jobs`, `upstream_error`.\n"
          },
          "message": {
            "type": "string",
            "description": "A user-facing sentence. Never echoes user input or secrets."
          },
          "correlationId": {
            "type": "string"
          }
        }
      },
      "ValidationError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "enum": [
                  "validation_failed",
                  "request_too_short",
                  "request_too_long",
                  "invalid_provider",
                  "invalid_target",
                  "invalid_json",
                  "invalid_scope",
                  "client_ip_unavailable"
                ]
              },
              "field": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        ]
      },
      "ImportError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "enum": [
                  "unsupported_url",
                  "bad_ailink",
                  "import_unavailable",
                  "client_ip_unavailable"
                ]
              },
              "reason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "For `bad_ailink`, a reason code: the sister codes (`bad_encoding`, `too_long`,\n`bad_shape`, `empty_spec`, `too_many_services`, `count_mismatch`, `bad_token`,\n`too_many_pairs`, `bad_name`) plus parent codes `bad_provider` and\n`duplicate_provider`.\n"
              },
              "openUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "For import_unavailable, the original link to open on the sister site."
              }
            }
          }
        ]
      },
      "RateLimitedError": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error",
          "scope",
          "limit",
          "retryAfterSeconds",
          "message"
        ],
        "properties": {
          "error": {
            "const": "rate_limited"
          },
          "scope": {
            "type": "string",
            "enum": [
              "ip",
              "user",
              "agent_key",
              "provider",
              "import",
              "share",
              "converse_ip",
              "converse_user",
              "conversation",
              "auth_ip",
              "auth_email",
              "auth_lockout"
            ],
            "description": "Counter scopes from ADR 0010 (ip, user, agent, provider, import, share), ADR 0011 (converse_ip, converse_user, and the per-conversation turn cap) and ADR 0012 (auth_ip and auth_email hourly limits, and the one-hour auth_lockout)."
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "azure",
              "aws",
              "gcp",
              null
            ]
          },
          "limit": {
            "type": "integer"
          },
          "retryAfterSeconds": {
            "type": "integer"
          },
          "message": {
            "type": "string"
          },
          "correlationId": {
            "type": "string"
          }
        }
      },
      "ServiceUnavailableError": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "spend_ceiling",
              "store_unavailable",
              "upstream_unavailable",
              "converse_unavailable",
              "email_unavailable"
            ]
          },
          "message": {
            "type": "string"
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "azure",
              "aws",
              "gcp",
              null
            ]
          },
          "correlationId": {
            "type": "string"
          }
        }
      },
      "schema_canonicalWorkload": {
        "title": "CanonicalWorkload",
        "description": "A provider-neutral description of a priced quote (spine §4, ADR 0003). Pure code derives it from a NormalizedQuote; there is no model call. Serialisation is deterministic: keys in schema order, `services` sorted by (role, label, quantity), numbers without trailing zeros. sha256 of that serialisation keys the mirror cache. `renderRequest(canonical, provider)` turns it into a sister `request` of at most 1000 characters.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "version",
          "name",
          "sourceProvider",
          "regionIntent",
          "services"
        ],
        "properties": {
          "version": {
            "const": 1
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "The source quote's name."
          },
          "sourceProvider": {
            "$ref": "#/components/schemas/schema_quote/$defs/Provider"
          },
          "regionIntent": {
            "type": "string",
            "enum": [
              "us-east",
              "us-west",
              "us-central",
              "eu-west",
              "eu-central",
              "uk",
              "asia-east",
              "asia-southeast",
              "australia",
              "canada",
              "us-gov",
              "other"
            ],
            "description": "The dominant region across services."
          },
          "services": {
            "type": "array",
            "minItems": 1,
            "maxItems": 12,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "role",
                "label",
                "quantity",
                "commitment"
              ],
              "properties": {
                "role": {
                  "$ref": "#/components/schemas/schema_quote/$defs/Role"
                },
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 60,
                  "description": "A provider-neutral noun phrase, e.g. `managed PostgreSQL database`, `Linux virtual machine`. Taken from the role classifier's table, never the sister SKU name."
                },
                "quantity": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 10000
                },
                "vcpu": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1024
                },
                "memoryGb": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 32768
                },
                "storageGb": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 10000000
                },
                "os": {
                  "type": "string",
                  "enum": [
                    "linux",
                    "windows",
                    "other"
                  ]
                },
                "engine": {
                  "type": "string",
                  "maxLength": 40,
                  "description": "Database or cache engine, e.g. `postgresql`, `mysql`, `sqlserver`, `redis`."
                },
                "tier": {
                  "type": "string",
                  "maxLength": 40,
                  "description": "A neutral tier word: `standard`, `premium`, `hot`, `cool`, `archive`, `general-purpose`, `memory-optimized`."
                },
                "hoursPerMonth": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 744
                },
                "commitment": {
                  "type": "string",
                  "enum": [
                    "payg",
                    "1yr",
                    "3yr"
                  ]
                },
                "notes": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Anything the classifier could not map, verbatim from configuration, at most 200 characters."
                },
                "sourceLineItemIds": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "maxItems": 20
                }
              }
            }
          }
        }
      },
      "schema_canvasDocument": {
        "title": "CanvasDocument",
        "description": "A parent canvas (spine §5, ADR 0006). Cosmos container `canvases`, partition key `/ownerId`. Unlike the sisters, edges and the viewport are stored (the sisters derive edges from parentId and do not persist the viewport, discovery D13). Optimistic concurrency uses the Cosmos `_etag`, exposed as the HTTP `ETag` header and required as `If-Match` on PUT (412 on mismatch). Size guard 1.5 MB: `details.raw` is trimmed first.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "ownerId",
          "name",
          "createdAt",
          "updatedAt",
          "schemaVersion",
          "viewport",
          "nodes",
          "edges",
          "groups"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "ownerId": {
            "type": "string",
            "maxLength": 200,
            "description": "userDocument.id (`<provider>:<userId>`). Set by the server, never by the client."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "schemaVersion": {
            "const": 1
          },
          "viewport": {
            "$ref": "#/components/schemas/schema_canvasDocument/$defs/Viewport"
          },
          "nodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/schema_nodeDocument"
            },
            "maxItems": 200
          },
          "edges": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/schema_canvasDocument/$defs/Edge"
            },
            "maxItems": 400
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/schema_canvasDocument/$defs/Group"
            },
            "maxItems": 100
          }
        },
        "$defs": {
          "Viewport": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "x",
              "y",
              "zoom"
            ],
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              },
              "zoom": {
                "type": "number",
                "minimum": 0.05,
                "maximum": 4
              }
            }
          },
          "Edge": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "source",
              "target",
              "kind"
            ],
            "properties": {
              "id": {
                "type": "string",
                "maxLength": 140
              },
              "source": {
                "type": "string",
                "maxLength": 64
              },
              "target": {
                "type": "string",
                "maxLength": 64
              },
              "kind": {
                "type": "string",
                "enum": [
                  "child",
                  "comparison"
                ],
                "description": "`child` is the solid lineage edge from the + handle; `comparison` is the dashed mirror edge."
              }
            }
          },
          "Group": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "nodeIds"
            ],
            "properties": {
              "id": {
                "type": "string",
                "maxLength": 64
              },
              "name": {
                "type": "string",
                "maxLength": 80,
                "description": "The workload name shown on the group outline."
              },
              "nodeIds": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "minItems": 1,
                "maxItems": 3
              },
              "canonicalHash": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^[a-f0-9]{64}$",
                "description": "sha256 of the shared canonical workload, when the group came from a mirror or fan-out."
              }
            }
          },
          "CanvasSnapshot": {
            "description": "The canvas content without identity or ownership: what a share stores, and the body of POST /api/canvases.",
            "type": "object",
            "additionalProperties": false,
            "required": [
              "name",
              "nodes",
              "edges",
              "groups"
            ],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 80
              },
              "viewport": {
                "$ref": "#/components/schemas/schema_canvasDocument/$defs/Viewport"
              },
              "nodes": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/schema_nodeDocument"
                },
                "maxItems": 200
              },
              "edges": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/schema_canvasDocument/$defs/Edge"
                },
                "maxItems": 400
              },
              "groups": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/schema_canvasDocument/$defs/Group"
                },
                "maxItems": 100
              }
            }
          }
        }
      },
      "schema_nodeDocument": {
        "title": "NodeDocument",
        "description": "One quote node on a parent canvas (spine §5). Stored inside canvasDocument.nodes, inside a share snapshot, and in browser localStorage `cpc:canvas:v1`. `provider` is a property of the node. Edges are stored separately in canvasDocument.edges; `parentId` records child lineage as the sister quote object does (quoteSchema.js:79-93).",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "provider",
          "kind",
          "position",
          "name",
          "createdAt",
          "updatedAt",
          "status",
          "request",
          "quote",
          "details",
          "canonical",
          "origin"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "provider": {
            "$ref": "#/components/schemas/schema_quote/$defs/Provider"
          },
          "kind": {
            "const": "quote"
          },
          "position": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "x",
              "y"
            ],
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              }
            }
          },
          "width": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 200,
            "maximum": 1200
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "queued",
              "running",
              "complete",
              "needs_more_detail",
              "rate_limited",
              "failed"
            ],
            "description": "The Job.status values plus `draft` (created in the UI, not submitted)."
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000,
            "description": "A user-facing status message, e.g. the sister's needs_more_detail hint or a 429 message."
          },
          "request": {
            "type": "string",
            "maxLength": 1000,
            "description": "The request text sent to the engine (composer text, or the canonical rendering for mirrors)."
          },
          "quote": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/schema_quote/$defs/NormalizedQuote"
              },
              {
                "type": "null"
              }
            ]
          },
          "details": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/schema_quote/$defs/QuoteDetails"
              },
              {
                "type": "null"
              }
            ]
          },
          "canonical": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/schema_canonicalWorkload"
              },
              {
                "type": "null"
              }
            ]
          },
          "origin": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "type"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "composer",
                      "mirror",
                      "fanout",
                      "refresh",
                      "sister-share",
                      "sister-ailink",
                      "parent-ailink",
                      "parent-share",
                      "example"
                    ]
                  },
                  "site": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Host of the originating site for imports, e.g. `aws-pricing-calculator.com`."
                  },
                  "shareId": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2000
                  },
                  "sourceNodeId": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "For mirror and refresh: the node this one was derived from."
                  },
                  "exampleDate": {
                    "type": "string",
                    "format": "date",
                    "description": "For bundled examples: the date the fixture was priced."
                  }
                }
              }
            ]
          },
          "sister": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/schema_quote/$defs/SisterRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "jobId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64
          },
          "groupId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64
          },
          "revision": {
            "type": "integer",
            "minimum": 0,
            "description": "Increments on each Refresh price."
          },
          "previousTotal": {
            "type": [
              "number",
              "null"
            ],
            "description": "totalMonthlyCost before the last refresh, for the delta display."
          }
        }
      },
      "schema_quote": {
        "title": "Quote shapes shared by the federation API and the stored documents",
        "description": "NormalizedQuote is the agent-quote `quote` object (sister shared_code/agent_quote_core.py `_line_items`, :273-287, and the 201 body, :441-459), with three parent additions: `lineItem.id`, `lineItem.role`, and configuration keys with the model's trailing colon stripped. QuoteDetails carries what the details modal needs beyond NormalizedQuote. It comes from the sister share payload (`sharedData.quotes[].messages[]` and `selectedSKUs[]`, agent_quote_core.py :139-244) when share-fetch mode allows, otherwise from the agent-quote JSON alone, with `available` saying which parts are missing.",
        "$defs": {
          "Provider": {
            "type": "string",
            "enum": [
              "azure",
              "aws",
              "gcp"
            ]
          },
          "PriceSource": {
            "description": "Server-computed by the sister (price_provenance.py). null only on legacy recommendations from before the field existed.",
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "live",
              "fallback",
              "estimate",
              null
            ]
          },
          "Role": {
            "description": "Service role assigned by the parent's role classifier (spine §4). Compare-lane rows collapse container, serverless, cache and analytics into compute, compute, database and other respectively (test-plan T-P3-05).",
            "type": "string",
            "enum": [
              "compute",
              "database",
              "storage",
              "network",
              "container",
              "serverless",
              "cache",
              "analytics",
              "other"
            ]
          },
          "Confidence": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "High",
              "Medium",
              "Low",
              null
            ]
          },
          "LineItem": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "service",
              "role",
              "description",
              "shortDescription",
              "monthlyCost",
              "quantity",
              "configuration",
              "justification",
              "confidence",
              "priceSource"
            ],
            "properties": {
              "id": {
                "type": "string",
                "minLength": 1,
                "maxLength": 80,
                "description": "Stable within the quote: `li-` + the first 12 hex characters of sha256(service name + '|' + index) (architecture §4)."
              },
              "service": {
                "type": "string",
                "maxLength": 200
              },
              "role": {
                "$ref": "#/components/schemas/schema_quote/$defs/Role"
              },
              "description": {
                "type": "string",
                "maxLength": 400,
                "description": "The model's sentence, capped at about 300 characters at a sentence boundary by the sister."
              },
              "shortDescription": {
                "type": "string",
                "maxLength": 120,
                "description": "At most 6 words (the sister's aiDescription)."
              },
              "monthlyCost": {
                "type": "number",
                "minimum": 0,
                "description": "USD per month per unit, 2 dp."
              },
              "quantity": {
                "type": "integer",
                "minimum": 1,
                "description": "Always 1 from the sister engines today."
              },
              "configuration": {
                "type": "object",
                "description": "Setting name to chosen option. Keys have the model's trailing colon stripped (`Region:` becomes `Region`); values are strings.",
                "additionalProperties": {
                  "type": "string",
                  "maxLength": 300
                },
                "maxProperties": 40
              },
              "justification": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 4000
              },
              "confidence": {
                "$ref": "#/components/schemas/schema_quote/$defs/Confidence"
              },
              "priceSource": {
                "$ref": "#/components/schemas/schema_quote/$defs/PriceSource"
              }
            }
          },
          "UnpricedService": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "service"
            ],
            "properties": {
              "service": {
                "type": "string",
                "maxLength": 200
              },
              "reason": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 1000
              }
            }
          },
          "NormalizedQuote": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "name",
              "currency",
              "totalMonthlyCost",
              "summary",
              "lineItems",
              "unpricedServices"
            ],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 80
              },
              "currency": {
                "const": "USD"
              },
              "totalMonthlyCost": {
                "type": "number",
                "minimum": 0,
                "description": "Sum of monthlyCost × quantity, 2 dp."
              },
              "summary": {
                "type": "string",
                "maxLength": 20000,
                "description": "The engine's prose (markdown). Render as untrusted text."
              },
              "lineItems": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/schema_quote/$defs/LineItem"
                },
                "maxItems": 50
              },
              "unpricedServices": {
                "description": "Always present (empty when none). The sister omits the key when empty; it sends strings or objects, and the normaliser maps both to this shape.",
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/schema_quote/$defs/UnpricedService"
                },
                "maxItems": 20
              }
            }
          },
          "CheaperAlternative": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "monthlyCost": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "savings": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "savingsPercentage": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "whyNotRecommended": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "PremiumAlternative": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "monthlyCost": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "additionalCost": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "costIncreasePercentage": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "whenToConsider": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "CostAnalysis": {
            "description": "The sister's `cost_analysis` (azure-chat prompt schema :2412-2452), camelCased. Numbers that arrive as strings like `$1,234` are parsed; anything unparseable becomes null.",
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "recommendedCost": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "cheaper": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/schema_quote/$defs/CheaperAlternative"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "premium": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/schema_quote/$defs/PremiumAlternative"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "reserved1yrMonthly": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "AWS and GCP engines only."
              },
              "reserved3yrMonthly": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "AWS and GCP engines only."
              }
            }
          },
          "LineDetail": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "lineItemId",
              "service"
            ],
            "properties": {
              "lineItemId": {
                "type": "string"
              },
              "service": {
                "type": "string"
              },
              "configuration": {
                "type": "object",
                "additionalProperties": {
                  "type": "string",
                  "maxLength": 300
                },
                "maxProperties": 40,
                "description": "Same as the line item's configuration (colons stripped); repeated so the details modal renders from `lines` alone."
              },
              "requestJustification": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The sister's service_request_justification. Absent from the agent-quote JSON; present in the share payload."
              },
              "justification": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "matchQuality": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "substitutions": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "maxItems": 20
              },
              "costAnalysis": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/schema_quote/$defs/CostAnalysis"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "ConversationTurn": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "role",
              "text"
            ],
            "properties": {
              "role": {
                "type": "string",
                "enum": [
                  "user",
                  "assistant"
                ]
              },
              "text": {
                "type": "string",
                "maxLength": 20000
              }
            }
          },
          "QuoteDetails": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "available",
              "conversation",
              "lines"
            ],
            "properties": {
              "available": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "conversation",
                  "costAnalysis",
                  "requestJustification"
                ],
                "properties": {
                  "conversation": {
                    "type": "boolean"
                  },
                  "costAnalysis": {
                    "type": "boolean"
                  },
                  "requestJustification": {
                    "type": "boolean"
                  }
                },
                "description": "Which parts of the sister's browser-path detail are present. A false value is rendered as 'not available from this calculator yet', never invented (ADR 0002)."
              },
              "conversation": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/schema_quote/$defs/ConversationTurn"
                },
                "maxItems": 100
              },
              "lines": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/schema_quote/$defs/LineDetail"
                },
                "maxItems": 50
              },
              "raw": {
                "description": "Kept so later features need no new upstream call (architecture §4). Never rendered as HTML. `sharedData` is the sister share payload when it was fetched; `agentQuote` is the agent-quote 201 body minus `rateLimit`. Trimmed first when a canvas document or share snapshot exceeds its size guard.",
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "properties": {
                  "sharedData": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "agentQuote": {
                    "type": [
                      "object",
                      "null"
                    ]
                  }
                }
              },
              "source": {
                "type": "string",
                "enum": [
                  "share-payload",
                  "share-claim",
                  "agent-json",
                  "parent-share",
                  "fixture"
                ],
                "description": "Where the details came from. `agent-json` means share-fetch mode `none` (spine §3): conversation and cost tiers are unavailable. Optional; informational."
              }
            }
          },
          "SisterRef": {
            "description": "The public sister share that agent-quote created for this build (30-day sliding TTL on the sister).",
            "type": "object",
            "additionalProperties": false,
            "required": [
              "shareId",
              "shareUrl"
            ],
            "properties": {
              "shareId": {
                "type": "string",
                "format": "uuid"
              },
              "shareUrl": {
                "type": "string",
                "format": "uri"
              },
              "expiresInDays": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          }
        }
      },
      "schema_shareDocument": {
        "title": "ShareDocument",
        "description": "A parent share (spine §5, OD-7). Cosmos container `shares`, partition key `/id`. It is an immutable snapshot: reads never write. Unsigned shares get `expiresAt` = createdAt + 7 days, **fixed**, and a per-item `ttl` of 604800 as a backstop; `expiresAt` is also checked on every read (410 after it). Signed shares have `expiresAt: null` and no `ttl`.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "name",
          "ownerId",
          "signed",
          "recipientEmail",
          "createdAt",
          "expiresAt",
          "snapshot"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "ownerId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "userDocument.id, or null for an unsigned visitor's share. Never returned by the API."
          },
          "signed": {
            "type": "boolean"
          },
          "recipientEmail": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "maxLength": 254,
            "description": "Lower-cased. When set, only a signed-in user with this email (from the SWA principal) can open the share. Only signed-in users can create such shares. Never returned by the API."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ttl": {
            "type": "integer",
            "const": 604800,
            "description": "Present only on unsigned shares. A Cosmos backstop; the read-time check of `expiresAt` is authoritative."
          },
          "snapshot": {
            "$ref": "#/components/schemas/schema_canvasDocument/$defs/CanvasSnapshot"
          },
          "sourceCanvasId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "ui",
              "group-share",
              "mcp"
            ],
            "description": "`group-share` is POST /api/groups/{id}/share; `mcp` means it was called with an agent key."
          }
        }
      },
      "schema_userDocument": {
        "title": "UserDocument",
        "description": "A parent user (spine §6, OD-6, ADR 0012). Cosmos container `users`, partition key `/id`. An SWA user (`aad`, `google`) is created on the first GET /api/me with a valid SWA principal; an email user (`email`) is created when a single-use set-password token mailed to the address is consumed. Identity comes only from the platform-injected `x-ms-client-principal` or a server-side email session; there is no parent-issued JWT. `passwordHash`, `sessionEpoch` and the other credential fields are never returned by any endpoint (the response projection is the OpenAPI `PublicUser`). Never read from or written to the sisters' `ChattAppDB.Users`.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "provider",
          "userId",
          "email",
          "displayName",
          "createdAt",
          "lastSeenAt",
          "linkedAccounts"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^((aad|google):.{1,180}|email:[a-f0-9]{64})$",
            "description": "`<identityProvider>:<clientPrincipal.userId>` for SWA users; `email:<sha256 of the normalised (trimmed, lower-cased) address>` for email users."
          },
          "provider": {
            "type": "string",
            "enum": [
              "aad",
              "google",
              "email"
            ]
          },
          "userId": {
            "type": "string",
            "maxLength": 180,
            "description": "clientPrincipal.userId (SWA's stable per-app user id); for email users, the 64-hex sha256 of the address."
          },
          "email": {
            "type": "string",
            "maxLength": 254,
            "description": "SWA: clientPrincipal.userDetails, lower-cased. Email users: the verified address (proven by consuming a token mailed to it). Used for direct-share matching and display."
          },
          "displayName": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastSeenAt": {
            "type": "string",
            "format": "date-time"
          },
          "linkedAccounts": {
            "description": "Reserved for a later 'link my calculator account' import (brief: not built in v1). Always empty in v1. There is one shared sister user store (discovery F16), so a link names the sister site whose saved quotes to import.",
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "site",
                "email",
                "linkedAt"
              ],
              "properties": {
                "site": {
                  "type": "string",
                  "enum": [
                    "azure",
                    "aws",
                    "gcp"
                  ]
                },
                "email": {
                  "type": "string",
                  "maxLength": 254
                },
                "linkedAt": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "passwordHash": {
            "type": "string",
            "maxLength": 200,
            "pattern": "^scrypt\\$[0-9]{1,2}\\$[0-9]{1,2}\\$[0-9]{1,2}\\$[A-Za-z0-9_-]{16,64}\\$[A-Za-z0-9_-]{32,256}$",
            "description": "Email users only. `scrypt$<log2 N>$<r>$<p>$<salt>$<key>` (base64url): N = 2^15, r = 8, p = 1, 16-byte random salt, 64-byte key. Never in a response or a log."
          },
          "emailVerifiedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Email users only: when a token mailed to the address was first consumed."
          },
          "passwordChangedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Email users only: the last password set, reset or change. Emailed links minted before it no longer work."
          },
          "sessionEpoch": {
            "type": "integer",
            "minimum": 0,
            "description": "Email users only. Bumped on every password set, reset or change; sessions minted under an older epoch no longer resolve. Never in a response."
          }
        }
      }
    }
  },
  "x-generated": "Generated at build time from docs/api/federation.openapi.yaml and docs/schema/*.json by apps/web/scripts/openapi-bundle.mjs. Do not edit."
}
