An app that does not know a catalog item's kind refuses the whole signed file, so the icons and builtin kinds can never go into catalog.json. `waybetter store build` now writes catalog.json (format 2: theme, plugin, strings only; what every app reads today) and catalog.v3.json (format 3: every kind) from the same items; `sign` and `verify` handle the pair; the rules refuse a format 2 file that lists a newer kind (CLI, JSON Schema, host). The host reads catalog.v3.json first and falls back to catalog.json only when the newer file is not there (a newer file that does not verify is an error). An item of a kind a version does not know is skipped instead of refusing the file, so the next new kind costs current apps nothing.
78 lines
6.3 KiB
JSON
78 lines
6.3 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$id": "https://unpkg.com/waybetter/store-catalog.schema.json",
|
|
"title": "wayBetter Store catalog",
|
|
"description": "The shared, signed catalog of the wayBetter Store, in two files built from one source: catalog.json (format 2: only theme, plugin and strings items, which every app knows) and catalog.v3.json (format 3: every kind, read first by apps that know the newer kinds; they fall back to catalog.json). Each is signed by its own .sig. Every wayBetter app reads the same files and keeps only the items whose `apps` list its id. Apps verify the Ed25519 signature over the exact bytes first, then these rules (waybetter-host store::catalog), then each download's sha256 and size. Build it with `waybetter store build`, sign it with `waybetter store sign`.",
|
|
"type": "object",
|
|
"required": ["format", "name", "generatedAt", "items"],
|
|
"additionalProperties": false,
|
|
"if": { "properties": { "format": { "const": 2 } } },
|
|
"then": { "properties": { "items": { "items": { "properties": { "kind": { "enum": ["theme", "plugin", "strings"] } } } } } },
|
|
"properties": {
|
|
"format": { "enum": [2, 3], "description": "2: only theme, plugin and strings items (an app from before a kind existed refuses a whole catalog that lists it). 3: every kind." },
|
|
"name": { "type": "string" },
|
|
"generatedAt": { "type": "string", "description": "When the catalog was built (RFC 3339, UTC)." },
|
|
"items": { "type": "array", "maxItems": 500, "items": { "$ref": "#/$defs/item" } }
|
|
},
|
|
"$defs": {
|
|
"text": { "type": "string", "pattern": "^[^\\u0000-\\u001f\\u007f-\\u009f]*$" },
|
|
"https": { "type": "string", "pattern": "^https://", "maxLength": 2048 },
|
|
"color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
|
|
"item": {
|
|
"type": "object",
|
|
"required": ["apps", "kind", "id", "name", "version"],
|
|
"additionalProperties": false,
|
|
"allOf": [
|
|
{
|
|
"if": { "properties": { "kind": { "const": "builtin" } } },
|
|
"then": { "not": { "anyOf": [{ "required": ["url"] }, { "required": ["sha256"] }, { "required": ["size"] }, { "required": ["file"] }, { "required": ["permissions"] }, { "required": ["colors"] }, { "required": ["language"] }] } },
|
|
"else": { "required": ["url", "sha256", "size"] }
|
|
},
|
|
{
|
|
"if": { "properties": { "kind": { "const": "icons" } } },
|
|
"then": { "not": { "anyOf": [{ "required": ["file"] }, { "required": ["permissions"] }, { "required": ["colors"] }, { "required": ["language"] }] }, "properties": { "size": { "maximum": 120000 } } }
|
|
}
|
|
],
|
|
"properties": {
|
|
"apps": {
|
|
"type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true,
|
|
"description": "The wayBetter apps this item is for (app ids such as \"x\"). An app never shows, stages or installs another app's items.",
|
|
"items": { "type": "string", "pattern": "^[a-z0-9-]{1,32}$" }
|
|
},
|
|
"kind": { "enum": ["theme", "plugin", "strings", "icons", "builtin"], "description": "strings: a string pack (data that replaces the site's own UI strings; waybetter docs/STRING-PACKS.md). icons: an icon pack (`*.icons.json`, data only, at most 120 KB). builtin: a feature that ships inside the app (an area plugin), listed in the Store as built in with its switch; it has no url, sha256 or size because there is nothing to download." },
|
|
"id": { "type": "string", "pattern": "^[a-z0-9-]{1,40}$", "description": "Unique per app and kind." },
|
|
"name": { "allOf": [{ "$ref": "#/$defs/text" }], "type": "string", "minLength": 1, "maxLength": 60 },
|
|
"author": { "allOf": [{ "$ref": "#/$defs/text" }], "type": "string", "maxLength": 60 },
|
|
"version": { "allOf": [{ "$ref": "#/$defs/text" }], "type": "string", "minLength": 1, "maxLength": 32 },
|
|
"description": { "allOf": [{ "$ref": "#/$defs/text" }], "type": "string", "maxLength": 600 },
|
|
"category": { "allOf": [{ "$ref": "#/$defs/text" }], "type": "string", "maxLength": 32, "description": "Shown as `wb.store.category.<id>` when the framework catalogs name it (reading, writing, filters, focus, tools, dark, light), else as is." },
|
|
"tags": { "type": "array", "maxItems": 12, "items": { "allOf": [{ "$ref": "#/$defs/text" }], "type": "string", "maxLength": 32 } },
|
|
"url": { "$ref": "#/$defs/https", "description": "The file (not on a `builtin` item). It must be on the reading app's download hosts (waybetter.fyi, cdn1.crt.fyi, git.crt.fyi raw files by default): https, default port, no credentials, no fragment." },
|
|
"file": { "type": "string", "pattern": "^[A-Za-z0-9_\\-. ]+\\.plugin\\.js$", "maxLength": 128, "description": "Plugins only: the file name it is installed under (unique per app, ignoring case)." },
|
|
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
"size": { "type": "integer", "minimum": 1, "maximum": 2097152 },
|
|
"preview": { "$ref": "#/$defs/https" },
|
|
"permissions": {
|
|
"description": "Plugins only: the declared @permissions; null = no @permissions line (legacy, full API).",
|
|
"anyOf": [
|
|
{ "type": "null" },
|
|
{ "type": "array", "uniqueItems": true, "items": { "enum": ["storage", "ui", "clipboard", "account", "users", "commands", "network", "network:none"] } }
|
|
]
|
|
},
|
|
"language": { "type": "string", "pattern": "^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$", "description": "String packs only (required there): the language tag of the pack." },
|
|
"license": { "allOf": [{ "$ref": "#/$defs/text" }], "type": "string", "minLength": 1, "maxLength": 60, "description": "The licence of the item (an SPDX id such as CC-BY-4.0, or a short name)." },
|
|
"credits": { "allOf": [{ "$ref": "#/$defs/text" }], "type": "string", "maxLength": 600, "description": "Who made, translated or reviewed it, plain text." },
|
|
"colors": {
|
|
"description": "Themes only: swatches for the card.",
|
|
"anyOf": [
|
|
{ "type": "null" },
|
|
{
|
|
"type": "object", "required": ["bg", "text", "accent"], "additionalProperties": false,
|
|
"properties": { "bg": { "$ref": "#/$defs/color" }, "surface": { "$ref": "#/$defs/color" }, "text": { "$ref": "#/$defs/color" }, "accent": { "$ref": "#/$defs/color" } }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|