{
  "$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",
          "backend",
          "users",
          "spaces",
          "camera"
        ]
      },
      "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."
          }
        }
      }
    },
    "pages": {
      "type": "array",
      "description": "Optional. The canvas's pages when it has more than one. Each is an HTML file in dist/. The page whose file is \"renderer\" opens first (added if left out). See https://caspian.md/docs/#pages.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "title",
          "file"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
            "description": "Stable kebab-case id, used by page.open."
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "description": "Shown to people, e.g. on the sign-in page."
          },
          "file": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,80}\\.html$",
            "description": "The page's HTML file inside dist/."
          },
          "requiresAuth": {
            "type": "boolean",
            "description": "Only a signed-in user may open this page. Needs the project's \"User registration\" setting on and the \"users\" capability."
          },
          "signIn": {
            "type": "boolean",
            "description": "This is the canvas's own sign-in page (at most one; never also requiresAuth). Receives ?next=<page id>."
          }
        }
      },
      "maxItems": 30
    },
    "publishedSidecar": {
      "enum": [
        "whole",
        "split"
      ],
      "description": "Optional. How a published copy gets the sidecar. \"whole\" (default): as-is, readable by any visitor. \"split\": only its published section { public, members, byEmail } is uploaded; the live site serves public to everyone, members to signed-in visitors and byEmail[email] only to the visitor signed in with that confirmed email. See https://caspian.md/docs/#split-sidecar."
    }
  }
}
