{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://schemas.revenexx.com/theme.schema.json",
  "title": "revenexx Theme Manifest",
  "description": "The contract between a revenexx Theme (a shareable, sellable Site) and the Platform. A Theme is registered in the same registry as an App, discriminated by `kind` + `engine` — the marker the Cockpit reads to know 'this is a Blokkli theme' (ADR-0061). Reuses the App identity / `type` / `permissions` building blocks (ADR-0058); adds `site` (how the FRONTEND is served — by domain, NOT the gateway, per ADR-0057 §8), `requires` (capabilities the theme needs to function — a hard install dependency), and `blokkli` (the editor payload the theme ships, opaque to the platform). Billing is declared separately in billing.json (pricing only).",
  "type": "object",
  "required": ["kind", "engine", "name", "vendor", "version", "title", "type"],
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "description": "Reference to this JSON Schema for editor validation and autocompletion."
    },
    "kind": {
      "const": "theme",
      "description": "Resource discriminator (ADR-0061). For a theme this is always 'theme'. Together with `engine` this is the single marker the Cockpit reads to recognise a Blokkli theme; it is the entirety of the platform's interface surface into the Cockpit."
    },
    "engine": {
      "enum": ["blokkli", "static", "custom"],
      "description": "The engine that drives this Site. 'blokkli' — a Nuxt page-builder whose adapter turns every edit into an SDK/gateway call; only blokkli themes are offered in the Cockpit page editor (and may ship the `blokkli` payload). 'static' — a fixed Nuxt site with no editor (e.g. a landing or placeholder page). 'custom' — a bespoke Site whose editing/data flow is owned by the theme itself. Together with `kind` this is the marker the Cockpit reads."
    },
    "name": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*$",
      "description": "Unique theme identifier within the vendor. Lowercase, alphanumeric with hyphens. Used as the registry id across Console, the gateway, and Cockpit."
    },
    "vendor": {
      "type": "string",
      "description": "Theme publisher. Use 'revenexx' for core themes, partner or organization name for third-party themes. A vendor namespace is claimed by the first tenant that registers under it (PE-170)."
    },
    "version": {
      "type": "string",
      "pattern": "^\\d+\\.\\d+\\.\\d+",
      "description": "Semantic version (e.g. '1.0.0'). Used for registry versioning; versions are immutable."
    },
    "title": {
      "type": "string",
      "description": "Human-readable display name shown in Cockpit, Marketplace, and Console."
    },
    "type": {
      "type": "string",
      "enum": ["public", "private"],
      "description": "Access type (ingress; ADR-0058, same semantics as an App). 'private' — only the owner tenant may install; never listed in the Marketplace. 'public' — once the operator publishes it, any tenant may install; listed in the Marketplace. Decoupled from billing.json (which is pricing only)."
    },
    "description": {
      "type": "string",
      "description": "Short description of the theme's purpose. Displayed in the Marketplace and Console."
    },
    "icon": {
      "type": "string",
      "description": "Path to the theme icon relative to the theme root. Used in Cockpit and Marketplace."
    },
    "site": {
      "type": "object",
      "description": "How the theme FRONTEND is served. The frontend (HTML / SSR / assets) is reached over the theme's OWN DOMAIN on the public Sites entrypoint, with the tenant resolved from the request Host (ADR-0057 §8) — it is NEVER served through the API gateway. Only the Blokkli adapter's data/capability calls (the SDK) traverse the gateway.",
      "required": ["framework", "adapter"],
      "additionalProperties": false,
      "properties": {
        "framework": {
          "type": "string",
          "enum": ["nuxt"],
          "description": "Frontend framework. Blokkli themes are Nuxt apps."
        },
        "adapter": {
          "type": "string",
          "enum": ["ssr", "static"],
          "description": "Rendering mode. 'ssr' — server-rendered (receives the per-invocation tenant context, ADR-0057, so server fetches are tenant-correct). 'static' — pre-rendered output."
        },
        "buildCommand": {
          "type": "string",
          "description": "Command that builds the site (e.g. 'npm run build'). Defaults to the framework's standard build when omitted."
        },
        "installCommand": {
          "type": "string",
          "description": "Optional dependency-install command (e.g. 'npm ci')."
        },
        "outputDirectory": {
          "type": "string",
          "description": "Build output directory served by the platform (e.g. '.output' for Nuxt SSR, 'dist' for static)."
        },
        "domains": {
          "type": "array",
          "items": { "type": "string" },
          "uniqueItems": true,
          "description": "Domains the theme frontend is served on (public entrypoint). Supports the '{{tenant_domain}}' template, resolved to the installing tenant's domain via the host→tenant resolver (ADR-0057 §8). The gateway is NOT among these — it carries adapter traffic only."
        }
      }
    },
    "requires": {
      "type": "array",
      "description": "Capabilities this theme needs to FUNCTION (ADR-0061). A hard install-time dependency: Console blocks the install on any installing tenant for which a required capability is not routed, and suggests the App(s) that implement it (marketplace cross-sell). Distinct from a `permissions` capability grant, which only asserts the right to CALL a capability — `requires` asserts the capability must EXIST for the tenant.",
      "items": {
        "type": "object",
        "required": ["capability"],
        "additionalProperties": false,
        "properties": {
          "capability": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$",
            "description": "The capability_key / operationId the theme depends on, e.g. 'products.list'."
          },
          "compatible": {
            "type": "string",
            "description": "Optional semver range the theme is built against, e.g. '^1.0'. The install gate resolves a compatible routed implementation."
          },
          "reason": {
            "type": "string",
            "description": "Optional human-readable reason shown in the Cockpit suggestion (e.g. 'renders the product grid')."
          }
        }
      }
    },
    "permissions": {
      "type": "array",
      "description": "The theme's access register — the single declarative source of truth for everything the theme's adapter is allowed to reach (ADR-0058 §2). Same discriminated shape as an App manifest. Themes get NO access by default; every grant must be declared. Console renders this to the Customer at install for consent and wires it into the per-tenant RBAC.",
      "items": {
        "$ref": "#/$defs/permission"
      }
    },
    "blokkli": {
      "type": "object",
      "description": "The Blokkli editor payload the theme ships — blocks, presets, and configuration. Opaque to the platform: the Cockpit hands this to the (separately-built) Blokkli integration. Validation here is intentionally loose so the Blokkli surface can evolve without a schema change.",
      "additionalProperties": true,
      "properties": {
        "blocks": {
          "type": "array",
          "description": "Block definitions the theme provides to the editor.",
          "items": { "type": "object", "additionalProperties": true }
        },
        "presets": {
          "type": "array",
          "description": "Preset layouts / starter content the theme ships.",
          "items": { "type": "object", "additionalProperties": true }
        }
      }
    }
  },
  "$defs": {
    "permission": {
      "description": "One grant in the theme's access register. Exactly one resource key (entity | capability | outbound | storage | ai | mail | secret | inbound) is present; that key both names the grant kind and carries its target. Mirrors manifest.schema.json so a theme and an app consent screen render identically.",
      "oneOf": [
        {
          "type": "object",
          "required": ["entity", "access"],
          "additionalProperties": false,
          "description": "Data access (Postgres RLS, ADR-0013): which entity (table) the theme adapter may touch and how.",
          "properties": {
            "entity": {
              "type": "string",
              "description": "The entity (table) name this grant applies to."
            },
            "access": {
              "type": "array",
              "items": { "type": "string", "enum": ["read", "create", "update", "delete"] },
              "minItems": 1,
              "uniqueItems": true,
              "description": "Allowed operations on the entity."
            }
          }
        },
        {
          "type": "object",
          "required": ["capability"],
          "additionalProperties": false,
          "description": "Permission to CALL a gateway capability (ADR-0055). The theme adapter may invoke this capability_key through the gateway. Pair with a `requires` entry when the theme cannot function without it.",
          "properties": {
            "capability": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$",
              "description": "The capability_key / operationId the theme is allowed to call, e.g. 'products.list'."
            },
            "compatible": {
              "type": "string",
              "description": "Optional semver range the caller is built against, e.g. '^1.0'. The gateway resolves a compatible implementation."
            }
          }
        },
        {
          "type": "object",
          "required": ["outbound"],
          "additionalProperties": false,
          "description": "External HTTP egress. The theme may make outbound calls to this host (default-deny otherwise).",
          "properties": {
            "outbound": {
              "type": "string",
              "description": "Allowed external hostname. Supports the '{{tenant_domain}}' template, resolved per tenant at enforcement time."
            },
            "paths": {
              "type": "array",
              "items": { "type": "string" },
              "uniqueItems": true,
              "description": "Optional allowed URL path globs (e.g. ['/v2/*']). Omit to allow any path on the host."
            }
          }
        },
        {
          "type": "object",
          "required": ["storage", "access"],
          "additionalProperties": false,
          "description": "File storage. The theme's brokered storage token is scoped to this bucket and these modes.",
          "properties": {
            "storage": {
              "type": "string",
              "description": "Bucket name, or '*' for all of the theme's buckets."
            },
            "access": {
              "type": "array",
              "items": { "type": "string", "enum": ["read", "write"] },
              "minItems": 1,
              "uniqueItems": true,
              "description": "Allowed storage modes."
            }
          }
        },
        {
          "type": "object",
          "required": ["ai"],
          "additionalProperties": false,
          "description": "AI inference via the LiteLLM gateway (ADR-0050).",
          "properties": {
            "ai": {
              "type": "string",
              "description": "Allowed model or model family, or '*' for any model the tenant has enabled."
            }
          }
        },
        {
          "type": "object",
          "required": ["mail"],
          "additionalProperties": false,
          "description": "Transactional mail sending via Postal (ADR-0009).",
          "properties": {
            "mail": {
              "type": "string",
              "description": "Allowed sending domain, or '*' for the tenant's configured mail domain."
            }
          }
        },
        {
          "type": "object",
          "required": ["secret"],
          "additionalProperties": false,
          "description": "Read access to secrets in the Infisical store (ADR-0022).",
          "properties": {
            "secret": {
              "type": "string",
              "description": "Secret path or path glob the theme may read (e.g. '/themes/storefront/*')."
            },
            "access": {
              "type": "array",
              "items": { "type": "string", "enum": ["read"] },
              "uniqueItems": true,
              "description": "Allowed secret operations. Read-only for now."
            }
          }
        },
        {
          "type": "object",
          "required": ["inbound"],
          "additionalProperties": false,
          "description": "Inbound webhook delivery via Hookdeck Outpost (ADR-0044). The theme may receive events from this source.",
          "properties": {
            "inbound": {
              "type": "string",
              "description": "Inbound source name the theme subscribes to."
            }
          }
        }
      ]
    }
  }
}
