{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://remora.anovelproject.com/schema/messages.v1.schema.json",
  "title": "Remora wire messages (protocol 1.0)",
  "description": "Every JSON message of the Remora wire protocol, as defined in SPEC.md section 6 and section 9.2. The root schema accepts any known message in any direction. To validate one direction of one connection, use `#/$defs/appToServer`, `#/$defs/serverToApp`, `#/$defs/viewerToServer` or `#/$defs/serverToViewer`. Message objects are open: receivers ignore fields they do not recognise, and a message whose `type` is not listed here is ignored rather than invalid.",
  "anyOf": [
    { "$ref": "#/$defs/appToServer" },
    { "$ref": "#/$defs/serverToApp" },
    { "$ref": "#/$defs/viewerToServer" },
    { "$ref": "#/$defs/serverToViewer" }
  ],

  "$defs": {
    "appToServer": {
      "type": "object",
      "required": ["type"],
      "properties": {
        "type": { "enum": ["hello", "register", "state", "asset", "error"] }
      },
      "allOf": [
        { "if": { "properties": { "type": { "const": "hello" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/hello" } },
        { "if": { "properties": { "type": { "const": "register" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/register" } },
        { "if": { "properties": { "type": { "const": "state" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/appState" } },
        { "if": { "properties": { "type": { "const": "asset" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/asset" } },
        { "if": { "properties": { "type": { "const": "error" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/error" } }
      ]
    },
    "serverToApp": {
      "type": "object",
      "required": ["type"],
      "properties": {
        "type": { "enum": ["welcome", "packageRequired", "ready", "watchers", "action", "error"] }
      },
      "allOf": [
        { "if": { "properties": { "type": { "const": "welcome" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/welcome" } },
        { "if": { "properties": { "type": { "const": "ready" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/ready" } },
        { "if": { "properties": { "type": { "const": "watchers" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/watchers" } },
        { "if": { "properties": { "type": { "const": "action" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/appAction" } },
        { "if": { "properties": { "type": { "const": "error" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/error" } }
      ]
    },
    "viewerToServer": {
      "type": "object",
      "required": ["type"],
      "properties": {
        "type": { "enum": ["hello", "pair", "auth", "watch", "action", "error"] }
      },
      "allOf": [
        { "if": { "properties": { "type": { "const": "hello" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/hello" } },
        { "if": { "properties": { "type": { "const": "pair" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/pair" } },
        { "if": { "properties": { "type": { "const": "auth" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/auth" } },
        { "if": { "properties": { "type": { "const": "watch" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/watch" } },
        { "if": { "properties": { "type": { "const": "action" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/viewerAction" } },
        { "if": { "properties": { "type": { "const": "error" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/error" } }
      ]
    },
    "serverToViewer": {
      "type": "object",
      "required": ["type"],
      "properties": {
        "type": { "enum": ["welcome", "paired", "apps", "snapshot", "state", "error"] }
      },
      "allOf": [
        { "if": { "properties": { "type": { "const": "welcome" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/welcome" } },
        { "if": { "properties": { "type": { "const": "paired" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/paired" } },
        { "if": { "properties": { "type": { "const": "apps" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/apps" } },
        { "if": { "properties": { "type": { "const": "snapshot" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/snapshot" } },
        { "if": { "properties": { "type": { "const": "state" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/viewerState" } },
        { "if": { "properties": { "type": { "const": "error" } }, "required": ["type"] }, "then": { "$ref": "#/$defs/error" } }
      ]
    },

    "name": {
      "type": "string",
      "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
    },
    "appId": {
      "type": "string",
      "maxLength": 128,
      "pattern": "^[a-z0-9]+([.-][a-z0-9]+)+$"
    },
    "opaqueId": {
      "description": "A server-assigned instance, device or server ID.",
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{1,64}$"
    },
    "packageHash": {
      "description": "SHA-256 of a package, as 64 lowercase hexadecimal characters.",
      "type": "string",
      "pattern": "^[0-9a-f]{64}$"
    },
    "protocolVersion": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+$"
    },
    "assetPath": {
      "type": "string",
      "pattern": "^[^/\\\\]+(/[^/\\\\]+)*$",
      "not": { "pattern": "(^|/)\\.\\.(/|$)" }
    },
    "byteCount": {
      "type": "integer",
      "minimum": 1
    },

    "hello": {
      "description": "First message on any connection.",
      "type": "object",
      "required": ["type", "role", "versions"],
      "properties": {
        "type": { "const": "hello" },
        "role": { "enum": ["app", "viewer"] },
        "versions": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/protocolVersion" }
        },
        "client": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "version": { "type": "string" }
          }
        }
      }
    },
    "welcome": {
      "type": "object",
      "required": ["type", "version", "server", "limits"],
      "properties": {
        "type": { "const": "welcome" },
        "version": { "$ref": "#/$defs/protocolVersion" },
        "server": {
          "type": "object",
          "required": ["id", "name"],
          "properties": {
            "id": { "$ref": "#/$defs/opaqueId" },
            "name": { "type": "string" }
          }
        },
        "limits": {
          "type": "object",
          "required": ["messageBytes", "packageBytes", "stateBytes", "updatesPerSecond"],
          "properties": {
            "messageBytes": { "$ref": "#/$defs/byteCount" },
            "packageBytes": { "$ref": "#/$defs/byteCount" },
            "stateBytes": { "$ref": "#/$defs/byteCount" },
            "updatesPerSecond": { "type": "number", "exclusiveMinimum": 0 }
          }
        }
      }
    },
    "error": {
      "type": "object",
      "required": ["type", "code", "fatal", "message"],
      "properties": {
        "type": { "const": "error" },
        "code": {
          "description": "Known codes: unsupportedVersion, invalidMessage, unauthorized, pairingFailed, invalidPackage, packageTooLarge, unknownApp, stateTooLarge, assetRejected, unknownInstance, actionRejected, internal. An unknown code is treated as `internal`.",
          "type": "string"
        },
        "fatal": { "type": "boolean" },
        "message": { "type": "string" },
        "details": { "type": "object" }
      }
    },

    "register": {
      "description": "Names the template in exactly one of three ways.",
      "type": "object",
      "required": ["type"],
      "properties": {
        "type": { "const": "register" },
        "manifest": { "$ref": "manifest.v1.schema.json" },
        "package": { "$ref": "#/$defs/packageHash" },
        "app": { "$ref": "#/$defs/appId" },
        "label": { "type": "string" }
      },
      "oneOf": [
        { "required": ["manifest"] },
        { "required": ["package"] },
        { "required": ["app"] }
      ]
    },
    "ready": {
      "type": "object",
      "required": ["type", "instance", "package"],
      "properties": {
        "type": { "const": "ready" },
        "instance": { "$ref": "#/$defs/opaqueId" },
        "package": { "$ref": "#/$defs/packageHash" }
      }
    },
    "appState": {
      "description": "A JSON Merge Patch (RFC 7386) for the application's state document.",
      "type": "object",
      "required": ["type", "patch"],
      "properties": {
        "type": { "const": "state" },
        "patch": { "type": "object" }
      }
    },
    "asset": {
      "description": "Announces a runtime asset. The next frame is a binary frame with the file.",
      "type": "object",
      "required": ["type", "path", "contentType"],
      "properties": {
        "type": { "const": "asset" },
        "path": { "$ref": "#/$defs/assetPath" },
        "contentType": { "enum": ["image/png", "image/jpeg", "image/svg+xml", "image/webp"] }
      }
    },
    "watchers": {
      "type": "object",
      "required": ["type", "count"],
      "properties": {
        "type": { "const": "watchers" },
        "count": { "type": "integer", "minimum": 0 }
      }
    },
    "appAction": {
      "description": "An action forwarded to the application.",
      "type": "object",
      "required": ["type", "action"],
      "properties": {
        "type": { "const": "action" },
        "action": { "$ref": "#/$defs/name" },
        "value": true,
        "device": { "$ref": "#/$defs/opaqueId" }
      }
    },

    "pair": {
      "type": "object",
      "required": ["type", "code", "device"],
      "properties": {
        "type": { "const": "pair" },
        "code": { "type": "string", "minLength": 1 },
        "device": {
          "type": "object",
          "required": ["name"],
          "properties": {
            "name": { "type": "string", "minLength": 1 },
            "kind": {
              "description": "A hint for the operator's device list. Known values: phone, tablet, watch, desktop, other.",
              "type": "string"
            }
          }
        }
      }
    },
    "auth": {
      "type": "object",
      "required": ["type", "device", "token"],
      "properties": {
        "type": { "const": "auth" },
        "device": { "$ref": "#/$defs/opaqueId" },
        "token": { "type": "string", "minLength": 1 }
      }
    },
    "paired": {
      "type": "object",
      "required": ["type", "device", "token"],
      "properties": {
        "type": { "const": "paired" },
        "device": { "$ref": "#/$defs/opaqueId" },
        "token": { "type": "string", "minLength": 1 }
      }
    },
    "apps": {
      "description": "The full list of registered instances.",
      "type": "object",
      "required": ["type", "apps"],
      "properties": {
        "type": { "const": "apps" },
        "apps": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["instance", "app", "name", "version", "package"],
            "properties": {
              "instance": { "$ref": "#/$defs/opaqueId" },
              "app": { "$ref": "#/$defs/appId" },
              "name": { "type": "string" },
              "label": { "type": "string" },
              "description": { "type": "string" },
              "version": { "type": "string" },
              "package": { "$ref": "#/$defs/packageHash" },
              "icon": { "$ref": "#/$defs/assetPath" }
            }
          }
        }
      }
    },
    "watch": {
      "description": "Start watching an instance, or stop with null.",
      "type": "object",
      "required": ["type", "instance"],
      "properties": {
        "type": { "const": "watch" },
        "instance": { "anyOf": [{ "$ref": "#/$defs/opaqueId" }, { "type": "null" }] }
      }
    },
    "snapshot": {
      "type": "object",
      "required": ["type", "instance", "state"],
      "properties": {
        "type": { "const": "snapshot" },
        "instance": { "$ref": "#/$defs/opaqueId" },
        "state": { "type": "object" }
      }
    },
    "viewerState": {
      "description": "A JSON Merge Patch (RFC 7386) for the viewer's copy of an instance's state.",
      "type": "object",
      "required": ["type", "instance", "patch"],
      "properties": {
        "type": { "const": "state" },
        "instance": { "$ref": "#/$defs/opaqueId" },
        "patch": { "type": "object" }
      }
    },
    "viewerAction": {
      "description": "An action emitted by the viewer.",
      "type": "object",
      "required": ["type", "instance", "action"],
      "properties": {
        "type": { "const": "action" },
        "instance": { "$ref": "#/$defs/opaqueId" },
        "action": { "$ref": "#/$defs/name" },
        "value": true
      }
    }
  }
}
