cli: waybetter store build|sign|verify, the store catalog JSON Schema
Moved from wayBetter for X (scripts/build-store.mjs, scripts/sign-store.mjs) into the `waybetter` npm package, generic over the app: - bin/waybetter.mjs + cli/store.mjs. `build --config <store.config.mjs>`: the config names its app and returns its items (metadata, file path + bytes, optional preview); build replaces only that app's items in the shared catalog (other apps' items and files stay), removes files only its old items used, fills url/sha256/size/apps, and validates the whole catalog with the rules apps apply natively before anything is written. `sign` (and `sign --init`: create the key once, 0600, never overwritten) signs the exact catalog bytes; `verify` checks the signature with any listed public key (rotation) and the catalog rules. The private key is never printed, copied or moved; --key defaults to $WAYBETTER_STORE_KEY. pluginHeader() for configs. - store-catalog.schema.json (format 2, `apps` per item), exported with the CLI module. - Tests: multi-app builds in one folder, refusals (bad items, duplicates, paths outside the folder, hosts), schema vs CLI rules, sign/verify round trip incl. rotation and tampering, no key material printed. jsonSchema.mjs learns minItems, uniqueItems and allOf.
This commit is contained in:
parent
dc449de9c2
commit
877dd8fa58
7 changed files with 518 additions and 4 deletions
62
packages/waybetter/store-catalog.schema.json
Normal file
62
packages/waybetter/store-catalog.schema.json
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"$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 (format 2). Every wayBetter app reads the same file (catalog.json, signed by catalog.json.sig) 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,
|
||||
"properties": {
|
||||
"format": { "const": 2 },
|
||||
"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", "url", "sha256", "size"],
|
||||
"additionalProperties": false,
|
||||
"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"] },
|
||||
"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. 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"] } }
|
||||
]
|
||||
},
|
||||
"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" } }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue