{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://caspian.md/docs/canvas.schema.json",
  "title": "Caspian canvas manifest",
  "description": "Schema for a canvas's canvas.json manifest file. See https://caspian.md/docs/ for the full authoring contract.",
  "type": "object",
  "required": ["id", "name", "version", "author", "apiVersion", "renderer", "schema", "capabilities"],
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
      "description": "Unique, stable identifier. Becomes part of the canvas's storage folder name and its canvas:// URL - don't change it after people have created instances of it."
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "description": "Display name shown in \"Create a new canvas\"."
    },
    "version": {
      "type": "string",
      "description": "Free-form version metadata, shown to the user before they approve the canvas."
    },
    "author": {
      "type": "string",
      "description": "Free-form author metadata, shown to the user before they approve the canvas."
    },
    "apiVersion": {
      "type": "string",
      "const": "1",
      "description": "Must match the host's bridge contract version, currently \"1\". A mismatch is rejected outright with an explicit \"needs an update\" error rather than failing silently later."
    },
    "renderer": {
      "type": "string",
      "minLength": 1,
      "description": "Entry file inside dist/ that gets loaded into the iframe, e.g. renderer.html."
    },
    "description": {
      "type": "string",
      "description": "Optional, shown alongside name."
    },
    "schema": {
      "type": "object",
      "description": "Reserved for future host-driven configuration UI. Leave it {} for now."
    },
    "capabilities": {
      "type": "array",
      "description": "Which bridge methods this canvas is allowed to call. The user approves the full list once, the first time they open an instance of it.",
      "items": {
        "type": "string",
        "enum": [
          "read-frontmatter",
          "write-frontmatter",
          "create-notes",
          "delete-notes",
          "network",
          "bundled-assets",
          "canvas-sidecar"
        ]
      },
      "uniqueItems": true
    },
    "features": {
      "type": "array",
      "description": "Optional. Visitor-facing controls, panels, or modes a publisher can switch off for a published copy. The publish dialog turns this into a checklist; unchecked ids are passed to the published renderer via window.__CASPIAN_DISABLED_FEATURES__. See sdk/features.ts.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "label"],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
            "description": "Stable kebab-case id. The publish on/off choice and the renderer's featureEnabled() check are both keyed by this."
          },
          "label": {
            "type": "string",
            "minLength": 1,
            "description": "Short, control-style name, e.g. \"Reset view button\"."
          },
          "description": {
            "type": "string",
            "description": "Optional. One line: what a visitor does with it."
          }
        }
      }
    }
  }
}
