{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.fablepool.org/v1/widget-manifest.schema.json", "title": "FablePool Widget Manifest (widget.json)", "description": "Manifest every sandboxed widget package must include at its root. Validated at registry publish time; the runtime refuses to load widgets whose files do not match the declared integrity hashes. See docs/architecture/07-widget-sandboxing.md for the security model.", "type": "object", "required": ["schemaVersion", "name", "version", "displayName", "description", "entry", "files", "protocolVersion", "accessibility", "license"], "additionalProperties": false, "properties": { "schemaVersion": { "const": "1" }, "name": { "description": "Registry-unique widget name, kebab-case, e.g. 'number-line' or 'graph-explorer'.", "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "minLength": 3, "maxLength": 80 }, "version": { "description": "Semantic version. Published versions are immutable; content pins exact versions.", "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" }, "displayName": { "type": "string", "minLength": 1, "maxLength": 120 }, "description": { "type": "string", "minLength": 10, "maxLength": 1000 }, "license": { "description": "Widget code license; must be OSI-approved and AGPLv3-compatible.", "type": "string", "enum": ["AGPL-3.0-only", "AGPL-3.0-or-later", "GPL-3.0-or-later", "MIT", "Apache-2.0", "BSD-3-Clause", "MPL-2.0"] }, "authors": { "type": "array", "minItems": 1, "maxItems": 10, "items": { "type": "object", "required": ["name"], "additionalProperties": false, "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "userId": { "type": "string", "format": "uuid" }, "url": { "type": "string", "format": "uri", "pattern": "^https://" } } } }, "repository": { "type": "string", "format": "uri", "pattern": "^https://" }, "entry": { "description": "Path (within the package) of the HTML document loaded into the sandboxed iframe. Must be self-contained: all scripts/styles loaded relatively from package files; no external network references (enforced by static scan and CSP).", "type": "string", "pattern": "^(?!\\.\\.)[a-zA-Z0-9_./-]+\\.html$", "maxLength": 200 }, "files": { "description": "Complete integrity manifest. Every file in the package must be listed; the registry rejects packages with unlisted files, and the serving layer verifies hashes.", "type": "array", "minItems": 1, "maxItems": 200, "items": { "type": "object", "required": ["path", "sha256", "byteSize"], "additionalProperties": false, "properties": { "path": { "type": "string", "pattern": "^(?!\\.\\.)(?!/)[a-zA-Z0-9_./-]+$", "maxLength": 200 }, "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "byteSize": { "type": "integer", "minimum": 1, "maximum": 2097152 } } } }, "maxTotalBytes": { "description": "Declared total package size; hard registry cap is 5 MiB to keep widgets viable on low-bandwidth connections.", "type": "integer", "minimum": 1, "maximum": 5242880 }, "protocolVersion": { "description": "Version of the host↔widget postMessage protocol the widget implements.", "type": "string", "enum": ["1"] }, "communication": { "type": "object", "additionalProperties": false, "properties": { "emits": { "description": "Messages the widget may send to the host. Anything outside this list is dropped and logged by the host bridge.", "type": "array", "uniqueItems": true, "maxItems": 6, "items": { "type": "string", "enum": ["ready", "state-changed", "resize", "telemetry", "error"] } }, "handles": { "description": "Messages the widget accepts from the host.", "type": "array", "uniqueItems": true, "maxItems": 6, "items": { "type": "string", "enum": ["init", "set-params", "get-state", "set-state", "theme-changed", "locale-changed"] } }, "maxStateBytes": { "description": "Maximum serialized size of the state object the widget reports; host truncates and flags violations.", "type": "integer", "minimum": 64, "maximum": 32768, "default": 8192 } } }, "sandbox": { "description": "Requested iframe capabilities. The host grants at most these; defaults are the most restrictive. 'allow-same-origin' is never grantable — widgets are always served from the isolated widget origin.", "type": "object", "additionalProperties": false, "properties": { "allowPointerLock": { "type": "boolean", "default": false }, "allowFullscreen": { "type": "boolean", "default": false }, "needsKeyboard": { "description": "Declares that the widget captures keyboard input, so the host renders focus-trap entry/exit affordances.", "type": "boolean", "default": false } } }, "paramsSchema": { "description": "Embedded JSON Schema (draft 2020-12) describing the 'params' object accepted from WidgetBlock instances. Authoring UI uses it to render a parameter form; the backend validates instance params against it.", "type": "object" }, "stateSchema": { "description": "Embedded JSON Schema describing the state object the widget reports. Used by the grading service for structural validation before any validator runs.", "type": "object" }, "gradable": { "description": "Whether this widget can back a 'widget'-type AnswerSpec. Gradable widgets must declare a stateSchema.", "type": "boolean", "default": false }, "accessibility": { "type": "object", "required": ["keyboardOperable", "screenReaderStrategy", "respectsReducedMotion"], "additionalProperties": false, "properties": { "keyboardOperable": { "description": "Must be true for registry acceptance: every interaction reachable by keyboard alone.", "const": true }, "screenReaderStrategy": { "description": "native: standard ARIA-labelled DOM controls; live-region: state narrated via aria-live announcements; alternative-task: the widget is inherently visual and the WidgetBlock textFallback serves as the accessible equivalent task.", "type": "string", "enum": ["native", "live-region", "alternative-task"] }, "respectsReducedMotion": { "const": true }, "minContrastChecked": { "type": "boolean", "default": false }, "notes": { "type": "string", "maxLength": 1000 } } }, "i18n": { "type": "object", "additionalProperties": false, "properties": { "supportedLocales": { "type": "array", "minItems": 1, "maxItems": 50, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$" } }, "fallbackLocale": { "type": "string", "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$" } } }, "changelog": { "type": "string", "maxLength": 2000 } }, "allOf": [ { "if": { "properties": { "gradable": { "const": true } }, "required": ["gradable"] }, "then": { "required": ["stateSchema"] } } ] }