{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openspatials.com/developers/spec/0.5/game-profile.schema.json",
  "title": "OpenSpatials game profile, version 0.2-draft",
  "description": "Spec: src/spec/core/APP-PROFILE-AND-DIRECTORY.md section 1 (signingKeys: OWNERSHIP-AND-UM.md). A draft profile (\"openspatials\": \"0.2-draft\"), served next to the start page as ./openspatials-world.json, the place looked at first, so worlds in sibling folders of one shared site each have their own; the site file /.well-known/openspatials-site.json at the origin root is the last fallback. Limit 64 KiB, checked on the bytes before parsing. Unknown fields are allowed and ignored.",
  "type": "object",
  "allOf": [
    {
      "$ref": "#/$defs/noCode"
    }
  ],
  "required": [
    "openspatials",
    "name",
    "url",
    "accepts"
  ],
  "properties": {
    "openspatials": {
      "description": "Spec version this game reads. The directory and connectors decide support from this value (04 sections 0 and 4).",
      "x-openspatials-reason": "unknown-version",
      "enum": [
        "0.1-draft",
        "0.2-draft"
      ]
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "pattern": "\\S"
    },
    "url": {
      "description": "The game's home page.",
      "$ref": "#/$defs/gameUrl"
    },
    "id": {
      "description": "0.5 addition (kit support next): the world's identity, separate from where it runs (WORLDS-WITHOUT-AN-ADDRESS.md section 1): an absolute https address with no query and no fragment that names the world and does not change when it moves; it need not serve the world. A world with no address of its own uses its registry page, https://openspatials.com/worlds/<owner>/<slug>/. A signed profile carried by value names it as its subject. Without it, the world's identity is the address its profile was read from.",
      "$ref": "#/$defs/worldIdentity"
    },
    "play": {
      "description": "The crossing entry point that takes ?openspatials=. Defaults to url.",
      "$ref": "#/$defs/gameUrl"
    },
    "exit": {
      "description": "Default destination for the exit portal.",
      "$ref": "#/$defs/gameUrl"
    },
    "accepts": {
      "type": "object",
      "required": [
        "rungs",
        "itemKinds",
        "maxBytes"
      ],
      "properties": {
        "rungs": {
          "description": "1 full model, 2 simple model, 3 sprite or portrait, 4 name tag. 2D games may list [3, 4].",
          "type": "array",
          "minItems": 1,
          "maxItems": 4,
          "uniqueItems": true,
          "items": {
            "enum": [
              1,
              2,
              3,
              4
            ]
          }
        },
        "itemKinds": {
          "description": "Item kinds this game shows. Template default [\"cosmetic\"]. An empty list accepts no items.",
          "type": "array",
          "maxItems": 3,
          "uniqueItems": true,
          "items": {
            "enum": [
              "cosmetic",
              "tool",
              "collectible"
            ]
          }
        },
        "useKinds": {
          "description": "Item kinds this game gives behavior to. For these the connector also checks the item owner's 'use' right (04 section 5, use per kind). Default [].",
          "type": "array",
          "maxItems": 3,
          "uniqueItems": true,
          "items": {
            "enum": [
              "cosmetic",
              "tool",
              "collectible"
            ]
          }
        },
        "keepsItems": {
          "description": "True when this game keeps anything of a visitor's items after the player leaves. Then the connector also checks the item owner's 'copy' right. Default false.",
          "type": "boolean"
        },
        "issuers": {
          "description": "Optional issuer policy: omitted accepts any verified issuer; empty grants no issuer use/copy. Entries match keyRef or resolved profile URLs by equal origin and complete path segment prefix. Signatures are checked before applying this policy.",
          "type": "array",
          "maxItems": 1000,
          "items": {
            "$ref": "#/$defs/gameUrl"
          }
        },
        "maxBytes": {
          "description": "Character model limit in bytes. The spec limit (20 MiB) applies when this is higher.",
          "type": "integer",
          "minimum": 1,
          "maximum": 20971520
        },
        "itemMaxBytes": {
          "description": "Item model limit in bytes. Default and maximum 2 MiB.",
          "type": "integer",
          "minimum": 1,
          "maximum": 2097152
        },
        "vibejam": {
          "description": "Vibe Jam compatibility switch. true (the default) accepts Vibe Jam portal arrivals; false refuses them with vibejam-not-accepted.",
          "type": "boolean"
        }
      }
    },
    "blockedSources": {
      "description": "URL prefixes refused as 'from'.",
      "type": "array",
      "maxItems": 1000,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 2048
      }
    },
    "requireIntegrity": {
      "description": "Refuse links that carry no hash (missing-integrity).",
      "type": "boolean"
    },
    "signingKeys": {
      "description": "Public keys this game signs ownership records with (04 section 5). A record names one with signature.keyRef = <address of this profile>#<id>. Entries use the Universal Manifest v0.4 signature member names algorithm and publicKeySpkiB64. Readers use Ed25519 entries only; ids must be unique (checked by code).",
      "type": "array",
      "maxItems": 8,
      "items": {
        "type": "object",
        "required": [
          "id",
          "algorithm",
          "publicKeySpkiB64"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9._-]{1,64}$"
          },
          "algorithm": {
            "type": "string",
            "minLength": 1,
            "maxLength": 32
          },
          "publicKeySpkiB64": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "pattern": "^[A-Za-z0-9+/_-]+={0,2}$"
          }
        }
      }
    },
    "signedProfile": {
      "description": "The address of this profile's signed form, a Universal Manifest v0.4 document beside it (the skill writes openspatials-world.signed.json), relative to this file or absolute (APP-PROFILE-AND-DIRECTORY.md section 1.5). A reader whose own profile reads \"0.5\" checks it; without it, or when it is unsigned or out of date, this profile still loads, unverified. The signed form carries every field of this profile except signedProfile.",
      "$ref": "#/$defs/docUrl"
    },
    "places": {
      "description": "0.5 addition (kit support next): the places the world runs, besides play (WORLDS-WITHOUT-AN-ADDRESS.md section 2): web pages on hosts the creator does not control, app links and platform links. A crossing link goes only to play or to a place with arrives true; other places are offered to the player, never opened by a reader on its own. A profile that names places names its play itself: play then does not default to url, which may be a page that takes no crossing links (its registry page).",
      "type": "array",
      "maxItems": 16,
      "items": {
        "$ref": "#/$defs/place"
      }
    },
    "state": {
      "description": "0.5 addition (kit support next): the address of the world's state document, relative to this profile or absolute (WORLD-STATE.md section 1; world-state.schema.json), usually openspatials-state.json beside it.",
      "$ref": "#/$defs/docUrl"
    }
  },
  "$defs": {
    "gameUrl": {
      "description": "An absolute https address, or http on a loopback host for local testing. A relative address is invalid-file; an absolute address with another scheme, a private network host or user info is unsafe-file.",
      "type": "string",
      "maxLength": 2048,
      "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:\\S*$",
      "if": {
        "type": "string",
        "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:"
      },
      "then": {
        "x-openspatials-reason": "unsafe-file",
        "anyOf": [
          {
            "$ref": "#/$defs/httpsUrl"
          },
          {
            "$ref": "#/$defs/loopbackHttpUrl"
          }
        ]
      }
    },
    "docUrl": {
      "description": "An address relative to this profile, or an absolute https address (http on a loopback host for local testing). White space makes it invalid-file. Any other scheme, a private network host, user info or a protocol-relative address is unsafe-file.",
      "type": "string",
      "minLength": 1,
      "maxLength": 2048,
      "pattern": "^\\S+$",
      "if": {
        "type": "string",
        "pattern": "^\\S+$"
      },
      "then": {
        "x-openspatials-reason": "unsafe-file",
        "anyOf": [
          {
            "$ref": "#/$defs/httpsUrl"
          },
          {
            "$ref": "#/$defs/loopbackHttpUrl"
          },
          {
            "type": "string",
            "pattern": "^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\\\/][\\\\/])\\S+$"
          }
        ]
      }
    },
    "httpsUrl": {
      "type": "string",
      "pattern": "^https://(?!(?:10|0)\\.\\d)(?!192\\.168\\.)(?!169\\.254\\.)(?!172\\.(?:1[6-9]|2\\d|3[01])\\.)(?!\\[[fF][cdeCDE])[^\\s/?#@\\\\]+(?:[/?#]\\S*)?$"
    },
    "loopbackHttpUrl": {
      "type": "string",
      "pattern": "^http://(?:127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|localhost|\\[::1\\])(?::\\d{1,5})?(?:[/?#]\\S*)?$"
    },
    "noCode": {
      "description": "04 section 1, closed list, at any depth: a key named script, scripts, eval or javascript; a key that is 'on' followed by a capital letter; a string value starting with javascript:, vbscript: or data:text/html (any case, after leading white space).",
      "x-openspatials-reason": "unsafe-file",
      "propertyNames": {
        "not": {
          "pattern": "^(?:script|scripts|eval|javascript|on[A-Z].*)$"
        }
      },
      "additionalProperties": {
        "$ref": "#/$defs/noCode"
      },
      "items": {
        "$ref": "#/$defs/noCode"
      },
      "not": {
        "type": "string",
        "pattern": "^\\s*(?:[Jj][Aa][Vv][Aa][Ss][Cc][Rr][Ii][Pp][Tt]:|[Vv][Bb][Ss][Cc][Rr][Ii][Pp][Tt]:|[Dd][Aa][Tt][Aa]:[Tt][Ee][Xx][Tt]/[Hh][Tt][Mm][Ll])"
      }
    },
    "worldIdentity": {
      "description": "An absolute https address with no query and no fragment, or http on a loopback host for local testing (WORLDS-WITHOUT-AN-ADDRESS.md section 1). A relative address, a query or a fragment is invalid-file; another scheme, a private network host or user info is unsafe-file.",
      "type": "string",
      "maxLength": 2048,
      "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s?#]+$",
      "if": {
        "type": "string",
        "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:"
      },
      "then": {
        "x-openspatials-reason": "unsafe-file",
        "anyOf": [
          {
            "$ref": "#/$defs/httpsUrl"
          },
          {
            "$ref": "#/$defs/loopbackHttpUrl"
          }
        ]
      }
    },
    "place": {
      "description": "One place the world runs (WORLDS-WITHOUT-AN-ADDRESS.md section 2). kind web: a web page that runs the world, an https address (http on loopback). kind app: an app link an installed app opens; kind platform: a link into a platform or store that starts the world. For app and platform, any scheme but javascript, vbscript, data, file, blob, about, filesystem and http (unsafe-file). A reader ignores a place of a kind it does not know. The same definition is copied in listing.schema.json; a test keeps the copies equal.",
      "type": "object",
      "required": [
        "url",
        "kind"
      ],
      "properties": {
        "url": {
          "description": "Where the place starts.",
          "type": "string",
          "maxLength": 2048,
          "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:\\S+$"
        },
        "kind": {
          "description": "web, app or platform.",
          "type": "string",
          "pattern": "^[a-z][a-z0-9-]{0,31}$"
        },
        "name": {
          "description": "The label people see.",
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "pattern": "\\S"
        },
        "host": {
          "description": "The host's name for people, such as itch.io.",
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "pattern": "\\S"
        },
        "arrives": {
          "description": "True when the place takes a crossing link. Default false.",
          "type": "boolean"
        },
        "network": {
          "description": "What the world can reach there: full (default), own-files or none.",
          "enum": [
            "full",
            "own-files",
            "none"
          ]
        }
      },
      "if": {
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "const": "web"
          }
        }
      },
      "then": {
        "properties": {
          "url": {
            "$ref": "#/$defs/gameUrl"
          }
        }
      },
      "else": {
        "properties": {
          "url": {
            "x-openspatials-reason": "unsafe-file",
            "not": {
              "pattern": "^(?:[Jj][Aa][Vv][Aa][Ss][Cc][Rr][Ii][Pp][Tt]|[Vv][Bb][Ss][Cc][Rr][Ii][Pp][Tt]|[Dd][Aa][Tt][Aa]|[Ff][Ii][Ll][Ee]|[Bb][Ll][Oo][Bb]|[Aa][Bb][Oo][Uu][Tt]|[Ff][Ii][Ll][Ee][Ss][Yy][Ss][Tt][Ee][Mm]|[Hh][Tt][Tt][Pp]):"
            }
          }
        }
      }
    }
  }
}
