@hyprune/schema 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +31 -1
  3. package/bin/validate.mjs +11 -5
  4. package/examples/registry/entry.json +106 -0
  5. package/examples/registry/index.json +137 -0
  6. package/examples/registry/invalid/registry-entry--bad-spdx.json +106 -0
  7. package/examples/registry/invalid/registry-entry--build-metadata.json +106 -0
  8. package/examples/registry/invalid/registry-entry--control-characters.json +106 -0
  9. package/examples/registry/invalid/registry-entry--duplicate-version.json +136 -0
  10. package/examples/registry/invalid/registry-entry--five-thumbnails.json +130 -0
  11. package/examples/registry/invalid/registry-entry--library-over-limit.json +22 -0
  12. package/examples/registry/invalid/registry-entry--name-too-long.json +106 -0
  13. package/examples/registry/invalid/registry-entry--tag-mismatch.json +106 -0
  14. package/examples/registry/invalid/registry-entry--uppercase-hash.json +106 -0
  15. package/examples/registry/invalid/registry-entry--versions-oldest-first.json +106 -0
  16. package/examples/registry/invalid/registry-index--duplicate-id.json +159 -0
  17. package/examples/registry/invalid/registry-index--entry-schema-pointer.json +138 -0
  18. package/examples/registry/invalid/registry-index--http-url.json +137 -0
  19. package/examples/registry/invalid/registry-index--newer-index-version.json +137 -0
  20. package/examples/registry/invalid/registry-index--zero-serial.json +137 -0
  21. package/examples/registry/invalid/release-record--archive-name.json +42 -0
  22. package/examples/registry/invalid/release-record--library-requires.json +42 -0
  23. package/examples/registry/invalid/release-record--short-commit.json +42 -0
  24. package/examples/registry/invalid/release-record--tag-mismatch.json +42 -0
  25. package/examples/registry/invalid/release-record--thumbnail-name.json +42 -0
  26. package/examples/registry/invalid/sources--archive-without-hash.json +50 -0
  27. package/examples/registry/invalid/sources--duplicate-id.json +59 -0
  28. package/examples/registry/invalid/sources--github-without-repo.json +50 -0
  29. package/examples/registry/invalid/sources--index-with-repo.json +52 -0
  30. package/examples/registry/invalid/sources--label-too-long.json +51 -0
  31. package/examples/registry/invalid/sources--reserved-id.json +51 -0
  32. package/examples/registry/invalid/sources--too-many.json +277 -0
  33. package/examples/registry/release-record.json +42 -0
  34. package/examples/registry/sources.json +51 -0
  35. package/lib/validate.d.mts +24 -0
  36. package/lib/validate.mjs +147 -0
  37. package/package.json +2 -1
  38. package/registry/v0.1/index.schema.json +309 -0
  39. package/registry/v0.1/release-record.schema.json +181 -0
  40. package/registry/v0.1/sources.schema.json +129 -0
  41. package/v0.10/object.schema.json +39 -0
  42. package/v0.25/ipc.schema.json +20085 -0
package/lib/validate.mjs CHANGED
@@ -423,6 +423,8 @@ function validateMachine(value, fail) {
423
423
  function traitCombinations(names, fail) {
424
424
  for (const a of names) for (const c of names) if (a < c && !(traitPairs[a] || []).includes(c)) fail(`traits: ${a} and ${c} can't be combined`);
425
425
  }
426
+ // RFC-0010 canonical character clip names (talk: the companions amendment).
427
+ const canonicalClips = ['idle', 'idle_fidget', 'walk', 'run', 'turn_left', 'turn_right', 'look_around', 'wave', 'nod', 'sit', 'sit_idle', 'stand', 'talk'];
426
428
  function objectTraitRules(traits, slots, fail, inside = () => {}) {
427
429
  const quat = (q, where) => { if (Math.abs(Math.hypot(...q) - 1) > 0.002) fail(`${where}: quaternion must be normalized`); };
428
430
  const unique = (items, field, label) => { const ids = items.map(v => v[field]); if (new Set(ids).size !== ids.length) fail(`${label}: duplicate ${field}`); };
@@ -473,6 +475,14 @@ function validateObjectManifest(value, version, options, fail, warn) {
473
475
  const traits = value.traits, slots = value.slots || {}, names = Object.keys(traits), b = value.bounds;
474
476
  if (options.directory !== undefined && options.directory !== value.id) fail(`id: "${value.id}" differs from its directory "${options.directory}"`);
475
477
  traitCombinations(names, fail);
478
+ // Companion block (object format 0.10 draft, RFC-0010 companions amendment).
479
+ if (value.companion) {
480
+ if (!traits.character) fail('companion: only characters can be companions');
481
+ else for (const clip of value.companion.idles || []) {
482
+ if (!(clip in traits.character.clips)) fail(`companion: idle variant "${clip}" is not a clip of traits.character`);
483
+ if (canonicalClips.includes(clip)) fail(`companion: "${clip}" is a canonical clip, not an idle variant`);
484
+ }
485
+ }
476
486
  for (let j = 0; j < 3; j++) if (b.min[j] >= b.max[j]) fail(`bounds: min ${'xyz'[j]} must be below max`);
477
487
  const t = objectLimits.boundsTolerance, surfaces = new Set(traits.movable ? traits.movable.surfaces || ['floor'] : []);
478
488
  const one = surfaces.size === 1 ? [...surfaces][0] : undefined;
@@ -893,3 +903,140 @@ export function validate(kind, value, method, version, options = {}) {
893
903
  }
894
904
  return { valid: errors.length === 0, errors };
895
905
  }
906
+
907
+ // Registry documents (RFC-0015), versioned on their own under registry/v0.N/: the index
908
+ // (and one entry of it, the shape of a registry entry file), the release record and
909
+ // sources.json. Discovered from the directories like every other contract.
910
+ const registryRoot = new URL('../registry/', import.meta.url);
911
+ export const registryVersions = existsSync(registryRoot)
912
+ ? readdirSync(registryRoot).filter(d => /^v0\.\d+$/.test(d)).map(d => d.slice(1)).sort(byVersion) : [];
913
+ const registrySchemas = { 'registry-index': 'index', 'registry-entry': 'index', 'release-record': 'release-record', sources: 'sources' };
914
+ for (const v of registryVersions) for (const name of new Set(Object.values(registrySchemas)))
915
+ if (existsSync(new URL(`v${v}/${name}.schema.json`, registryRoot))) ajv.addSchema(JSON.parse(readFileSync(new URL(`v${v}/${name}.schema.json`, registryRoot))));
916
+ /** RFC-0015 archive limits per package type (compressed bytes, expanded bytes, files, single file). */
917
+ export const registryLimits = Object.freeze({
918
+ world: Object.freeze({ bytes: 512 * 1048576, expandedBytes: 1024 * 1048576, files: 10000, fileBytes: 128 * 1048576 }),
919
+ library: Object.freeze({ bytes: 256 * 1048576, expandedBytes: 512 * 1048576, files: 4000, fileBytes: 32 * 1048576 }),
920
+ index: Object.freeze({ bytes: 16 * 1048576, entries: 20000, versions: 64, thumbnails: 4, custom: 32 }),
921
+ });
922
+ const SEMVER_PARTS = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
923
+ /** SemVer 2.0 parse; undefined when invalid (including numeric pre-release identifiers with leading zeros). */
924
+ export function parseSemver(text) {
925
+ const m = typeof text === 'string' ? SEMVER_PARTS.exec(text) : null;
926
+ if (!m) return undefined;
927
+ const pre = m[4] === undefined ? [] : m[4].split('.');
928
+ if (pre.some(p => /^0\d+$/.test(p))) return undefined;
929
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), prerelease: pre, build: m[5] ?? null };
930
+ }
931
+ /** SemVer precedence: negative when a < b, 0 when equal (build metadata ignored), positive when a > b. Throws on invalid versions. */
932
+ export function compareSemver(a, b) {
933
+ const x = parseSemver(a), y = parseSemver(b);
934
+ if (!x || !y) throw new Error(`Invalid SemVer: ${!x ? a : b}`);
935
+ for (const k of ['major', 'minor', 'patch']) if (x[k] !== y[k]) return x[k] < y[k] ? -1 : 1;
936
+ if (!x.prerelease.length || !y.prerelease.length) return Math.sign(y.prerelease.length - x.prerelease.length);
937
+ for (let i = 0; i < Math.max(x.prerelease.length, y.prerelease.length); i++) {
938
+ const p = x.prerelease[i], q = y.prerelease[i];
939
+ if (p === undefined) return -1;
940
+ if (q === undefined) return 1;
941
+ const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
942
+ if (pn && qn) { const d = BigInt(p) - BigInt(q); if (d) return d < 0n ? -1 : 1; }
943
+ else if (pn !== qn) return pn ? -1 : 1;
944
+ else if (p !== q) return p < q ? -1 : 1;
945
+ }
946
+ return 0;
947
+ }
948
+ /**
949
+ * The version a tag names by the RFC-0015 tag convention: `v<version>` for a repository
950
+ * with one package, `<name>-v<version>` (name a lowercase slug) for one of several.
951
+ * Returns {name|null, version} or undefined when the tag follows neither form.
952
+ */
953
+ export function tagVersion(tag) {
954
+ if (typeof tag !== 'string') return undefined;
955
+ const m = /^(?:([a-z0-9]+(?:-[a-z0-9]+)*)-)?v(\d[0-9A-Za-z.+-]*)$/.exec(tag);
956
+ if (!m || !parseSemver(m[2])) return undefined;
957
+ return { name: m[1] ?? null, version: m[2] };
958
+ }
959
+ const spdxValid = text => { try { parseSpdx(text); return true; } catch { return false; } };
960
+ // Semantic rules of one index entry (shared by kinds registry-index and registry-entry).
961
+ function registryEntryRules(entry, label, fail) {
962
+ const limits = registryLimits[entry.type];
963
+ const tags = new Set(), versions = new Set();
964
+ entry.versions.forEach((v, i) => {
965
+ const at = `${label}versions/${i} (${v.tag})`;
966
+ if (tags.has(v.tag)) fail(`${at}: duplicate tag`); tags.add(v.tag);
967
+ if (versions.has(v.version)) fail(`${at}: duplicate version ${v.version}`); versions.add(v.version);
968
+ if (!parseSemver(v.version)) fail(`${at}: version ${v.version} is not SemVer (numeric pre-release identifiers can't have leading zeros)`);
969
+ const named = tagVersion(v.tag);
970
+ if (!named || named.version !== v.version) fail(`${at}: tag must name version ${v.version} (v${v.version} or <name>-v${v.version})`);
971
+ if (i > 0 && parseSemver(v.version) && parseSemver(entry.versions[i - 1].version) && compareSemver(entry.versions[i - 1].version, v.version) <= 0)
972
+ fail(`${at}: versions must be newest first (${entry.versions[i - 1].version} before ${v.version})`);
973
+ if (!spdxValid(v.license)) fail(`${at}: license: invalid SPDX expression`);
974
+ if (v.bytes > limits.bytes) fail(`${at}: bytes ${v.bytes} exceed the ${entry.type} archive limit ${limits.bytes}`);
975
+ if (v.expandedBytes > limits.expandedBytes) fail(`${at}: expandedBytes ${v.expandedBytes} exceed the ${entry.type} limit ${limits.expandedBytes}`);
976
+ if (v.files > limits.files) fail(`${at}: files ${v.files} exceed the ${entry.type} limit ${limits.files}`);
977
+ if (entry.type === 'library' && v.requires?.libraries?.length) fail(`${at}: a library requires no libraries`);
978
+ if ((v.requires?.libraries || []).some(r => r.startsWith(entry.id + '@'))) fail(`${at}: requires itself`);
979
+ });
980
+ }
981
+ function releaseRecordRules(r, fail) {
982
+ const limits = registryLimits[r.type], stem = `${r.id}-${r.version}`;
983
+ if (!parseSemver(r.version)) fail(`version: ${r.version} is not SemVer (numeric pre-release identifiers can't have leading zeros)`);
984
+ if (!spdxValid(r.license)) fail('license: invalid SPDX expression');
985
+ const named = tagVersion(r.source.tag);
986
+ if (!named || named.version !== r.version) fail(`source.tag: ${r.source.tag} must name version ${r.version} (v${r.version} or <name>-v${r.version})`);
987
+ if (r.archive.name !== `${stem}.tar.zst`) fail(`archive.name: must be ${stem}.tar.zst`);
988
+ if (r.archive.bytes > limits.bytes) fail(`archive.bytes: ${r.archive.bytes} exceed the ${r.type} archive limit ${limits.bytes}`);
989
+ if (r.archive.expandedBytes > limits.expandedBytes) fail(`archive.expandedBytes: ${r.archive.expandedBytes} exceed the ${r.type} limit ${limits.expandedBytes}`);
990
+ if (r.archive.files > limits.files) fail(`archive.files: ${r.archive.files} exceed the ${r.type} limit ${limits.files}`);
991
+ if (r.type === 'library' && r.requires?.libraries?.length) fail('requires.libraries: a library requires no libraries');
992
+ const names = (r.thumbnails || []).map(t => t.name);
993
+ names.forEach((name, i) => { if (!name.startsWith(`${stem}.thumb-${i + 1}.`)) fail(`thumbnails/${i}: name must be ${stem}.thumb-${i + 1}.png or .jpg`); });
994
+ }
995
+ function sourcesRules(doc, fail) {
996
+ const ids = new Set();
997
+ (doc.custom || []).forEach((s, i) => {
998
+ const at = `custom/${i} (${s.id})`;
999
+ if (ids.has(s.id)) fail(`${at}: duplicate id`); ids.add(s.id);
1000
+ if (['official', 'community'].includes(s.id)) fail(`${at}: id is reserved for a built-in source`);
1001
+ if (s.label.trim() !== s.label) fail(`${at}: label has leading or trailing spaces`);
1002
+ const extra = { index: ['repo', 'sha256'], github: ['url', 'sha256'], url: ['repo'] }[s.kind].filter(k => s[k] !== undefined);
1003
+ if (extra.length) fail(`${at}: kind ${s.kind} takes no ${extra.join(', ')}`);
1004
+ if (s.kind === 'url') {
1005
+ const path = new URL(s.url).pathname;
1006
+ if (path.endsWith('.tar.zst') && !s.sha256) fail(`${at}: an archive URL needs its sha256`);
1007
+ if (!path.endsWith('.tar.zst') && !path.endsWith('.json')) fail(`${at}: a package URL is a .tar.zst archive or a .release.json record`);
1008
+ if (path.endsWith('.json') && s.sha256) fail(`${at}: a release record URL supplies its own hash`);
1009
+ }
1010
+ });
1011
+ }
1012
+ /**
1013
+ * Validate a registry document (RFC-0015). Kinds: `registry-index` (an index document),
1014
+ * `registry-entry` (one entry, e.g. a registry entry file, which may carry `$schema`),
1015
+ * `release-record` and `sources`. Adds the rules a schema can't express: unique ids,
1016
+ * versions unique and newest first by SemVer, the tag naming the version, SPDX
1017
+ * expressions, per-type archive limits.
1018
+ * @param version registry format (default: the newest registry/v0.N)
1019
+ * @throws on an unknown kind or version
1020
+ */
1021
+ export function validateRegistry(kind, value, version = registryVersions.at(-1)) {
1022
+ if (!Object.hasOwn(registrySchemas, kind)) throw new Error(`Unknown registry kind: ${kind}`);
1023
+ if (!registryVersions.includes(version)) throw new Error(`Unknown registry version: ${version}`);
1024
+ const id = `https://hyprune.com/schema/registry/v${version}/${registrySchemas[kind]}.schema.json${kind === 'registry-entry' ? '#/$defs/entry' : ''}`;
1025
+ const key = `registry:${version}:${kind}`;
1026
+ if (!validators.has(key)) validators.set(key, ajv.getSchema(id));
1027
+ const check = validators.get(key);
1028
+ if (!check(value)) return { valid: false, errors: check.errors.map(e => `${e.instancePath || '/'} ${e.message}`) };
1029
+ const errors = [], fail = m => errors.push(m);
1030
+ if (kind === 'registry-entry') registryEntryRules(value, '', fail);
1031
+ if (kind === 'registry-index') {
1032
+ const ids = new Set();
1033
+ value.entries.forEach((entry, i) => {
1034
+ if (ids.has(entry.id)) fail(`entries/${i}: duplicate id ${entry.id}`); ids.add(entry.id);
1035
+ if (entry.$schema !== undefined) fail(`entries/${i}: $schema belongs to entry files, not index entries`);
1036
+ registryEntryRules(entry, `entries/${i} ${entry.id}: `, fail);
1037
+ });
1038
+ }
1039
+ if (kind === 'release-record') releaseRecordRules(value, fail);
1040
+ if (kind === 'sources') sourcesRules(value, fail);
1041
+ return { valid: errors.length === 0, errors };
1042
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyprune/schema",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Versioned JSON Schemas for Hyprune world packages and the IPC protocol, with a validator and CLI for world authors",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,6 +39,7 @@
39
39
  "lib/",
40
40
  "v0*/**",
41
41
  "extensions/",
42
+ "registry/",
42
43
  "examples/",
43
44
  "CHANGELOG.md",
44
45
  "README.md",
@@ -0,0 +1,309 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://hyprune.com/schema/registry/v0.1/index.schema.json",
4
+ "title": "Hyprune registry index 0.1 (RFC-0015): the document every index source serves; $defs.entry is one entry and the shape of a registry entry file",
5
+ "type": "object",
6
+ "properties": {
7
+ "indexVersion": {
8
+ "const": "0.1"
9
+ },
10
+ "name": {
11
+ "type": "string",
12
+ "$ref": "#/$defs/line",
13
+ "maxLength": 64
14
+ },
15
+ "homepage": {
16
+ "$ref": "#/$defs/https"
17
+ },
18
+ "serial": {
19
+ "type": "integer",
20
+ "minimum": 1,
21
+ "maximum": 9007199254740991
22
+ },
23
+ "generated": {
24
+ "type": "string",
25
+ "maxLength": 40,
26
+ "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?Z$"
27
+ },
28
+ "successor": {
29
+ "anyOf": [
30
+ {
31
+ "$ref": "#/$defs/https"
32
+ },
33
+ {
34
+ "type": "null"
35
+ }
36
+ ]
37
+ },
38
+ "entries": {
39
+ "type": "array",
40
+ "maxItems": 20000,
41
+ "items": {
42
+ "$ref": "#/$defs/entry"
43
+ }
44
+ }
45
+ },
46
+ "required": [
47
+ "indexVersion",
48
+ "serial",
49
+ "generated",
50
+ "entries"
51
+ ],
52
+ "additionalProperties": false,
53
+ "$defs": {
54
+ "id": {
55
+ "type": "string",
56
+ "minLength": 3,
57
+ "maxLength": 128,
58
+ "pattern": "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$"
59
+ },
60
+ "version": {
61
+ "description": "SemVer 2.0 without build metadata (published versions).",
62
+ "type": "string",
63
+ "minLength": 5,
64
+ "maxLength": 64,
65
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
66
+ },
67
+ "tag": {
68
+ "description": "The git tag the release was built from: v<version> or <name>-v<version>.",
69
+ "type": "string",
70
+ "minLength": 1,
71
+ "maxLength": 128,
72
+ "pattern": "^[A-Za-z0-9._-]+$"
73
+ },
74
+ "formatVersion": {
75
+ "type": "string",
76
+ "maxLength": 16,
77
+ "pattern": "^0\\.(0|[1-9][0-9]*)$"
78
+ },
79
+ "license": {
80
+ "description": "SPDX expression (parsed by the validator).",
81
+ "type": "string",
82
+ "minLength": 1,
83
+ "maxLength": 256
84
+ },
85
+ "sha256": {
86
+ "type": "string",
87
+ "pattern": "^[0-9a-f]{64}$"
88
+ },
89
+ "https": {
90
+ "type": "string",
91
+ "minLength": 9,
92
+ "maxLength": 2048,
93
+ "pattern": "^https://[^\\s/?#@]+(/[^\\s]*)?$"
94
+ },
95
+ "line": {
96
+ "description": "One line of plain text: no control characters.",
97
+ "type": "string",
98
+ "minLength": 1,
99
+ "pattern": "^[^\\u0000-\\u001f\\u007f-\\u009f]+$"
100
+ },
101
+ "slug": {
102
+ "type": "string",
103
+ "minLength": 1,
104
+ "maxLength": 32,
105
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
106
+ },
107
+ "libraryRef": {
108
+ "description": "An exact library id@version (RFC-0011).",
109
+ "type": "string",
110
+ "maxLength": 200,
111
+ "pattern": "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+@(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
112
+ },
113
+ "requires": {
114
+ "type": "object",
115
+ "properties": {
116
+ "libraries": {
117
+ "type": "array",
118
+ "maxItems": 64,
119
+ "uniqueItems": true,
120
+ "items": {
121
+ "$ref": "#/$defs/libraryRef"
122
+ }
123
+ }
124
+ },
125
+ "additionalProperties": false
126
+ },
127
+ "thumbnail": {
128
+ "type": "object",
129
+ "properties": {
130
+ "url": {
131
+ "$ref": "#/$defs/https"
132
+ },
133
+ "sha256": {
134
+ "$ref": "#/$defs/sha256"
135
+ },
136
+ "width": {
137
+ "type": "integer",
138
+ "minimum": 1,
139
+ "maximum": 1280
140
+ },
141
+ "height": {
142
+ "type": "integer",
143
+ "minimum": 1,
144
+ "maximum": 720
145
+ }
146
+ },
147
+ "required": [
148
+ "url",
149
+ "sha256",
150
+ "width",
151
+ "height"
152
+ ],
153
+ "additionalProperties": false
154
+ },
155
+ "version-entry": {
156
+ "description": "One published version of an entry (versions[]).",
157
+ "type": "object",
158
+ "properties": {
159
+ "tag": {
160
+ "$ref": "#/$defs/tag"
161
+ },
162
+ "version": {
163
+ "$ref": "#/$defs/version"
164
+ },
165
+ "formatVersion": {
166
+ "$ref": "#/$defs/formatVersion"
167
+ },
168
+ "published": {
169
+ "type": "string",
170
+ "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"
171
+ },
172
+ "license": {
173
+ "$ref": "#/$defs/license"
174
+ },
175
+ "url": {
176
+ "$ref": "#/$defs/https"
177
+ },
178
+ "sha256": {
179
+ "$ref": "#/$defs/sha256"
180
+ },
181
+ "bytes": {
182
+ "type": "integer",
183
+ "minimum": 1,
184
+ "maximum": 536870912
185
+ },
186
+ "expandedBytes": {
187
+ "type": "integer",
188
+ "minimum": 0,
189
+ "maximum": 1073741824
190
+ },
191
+ "files": {
192
+ "type": "integer",
193
+ "minimum": 1,
194
+ "maximum": 10000
195
+ },
196
+ "record": {
197
+ "type": "object",
198
+ "properties": {
199
+ "url": {
200
+ "$ref": "#/$defs/https"
201
+ },
202
+ "sha256": {
203
+ "$ref": "#/$defs/sha256"
204
+ }
205
+ },
206
+ "required": [
207
+ "url",
208
+ "sha256"
209
+ ],
210
+ "additionalProperties": false
211
+ },
212
+ "requires": {
213
+ "$ref": "#/$defs/requires"
214
+ },
215
+ "thumbnails": {
216
+ "type": "array",
217
+ "maxItems": 4,
218
+ "items": {
219
+ "$ref": "#/$defs/thumbnail"
220
+ }
221
+ },
222
+ "status": {
223
+ "enum": [
224
+ "available",
225
+ "unavailable"
226
+ ]
227
+ }
228
+ },
229
+ "required": [
230
+ "tag",
231
+ "version",
232
+ "formatVersion",
233
+ "published",
234
+ "license",
235
+ "url",
236
+ "sha256",
237
+ "bytes",
238
+ "expandedBytes",
239
+ "files"
240
+ ],
241
+ "additionalProperties": false
242
+ },
243
+ "entry": {
244
+ "description": "One package of an index. A registry entry file is exactly one entry and may add a $schema pointer; entries inside an index carry none.",
245
+ "type": "object",
246
+ "properties": {
247
+ "$schema": {
248
+ "$ref": "#/$defs/https"
249
+ },
250
+ "id": {
251
+ "$ref": "#/$defs/id"
252
+ },
253
+ "type": {
254
+ "enum": [
255
+ "world",
256
+ "library"
257
+ ]
258
+ },
259
+ "name": {
260
+ "type": "string",
261
+ "$ref": "#/$defs/line",
262
+ "maxLength": 64
263
+ },
264
+ "summary": {
265
+ "type": "string",
266
+ "maxLength": 140,
267
+ "pattern": "^[^\\u0000-\\u001f\\u007f-\\u009f]*$"
268
+ },
269
+ "description": {
270
+ "type": "string",
271
+ "maxLength": 2000,
272
+ "pattern": "^[^\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f]*$"
273
+ },
274
+ "creator": {
275
+ "type": "string",
276
+ "$ref": "#/$defs/line",
277
+ "maxLength": 64
278
+ },
279
+ "repo": {
280
+ "$ref": "#/$defs/https"
281
+ },
282
+ "tags": {
283
+ "type": "array",
284
+ "maxItems": 8,
285
+ "uniqueItems": true,
286
+ "items": {
287
+ "$ref": "#/$defs/slug"
288
+ }
289
+ },
290
+ "versions": {
291
+ "type": "array",
292
+ "minItems": 1,
293
+ "maxItems": 64,
294
+ "items": {
295
+ "$ref": "#/$defs/version-entry"
296
+ }
297
+ }
298
+ },
299
+ "required": [
300
+ "id",
301
+ "type",
302
+ "name",
303
+ "creator",
304
+ "versions"
305
+ ],
306
+ "additionalProperties": false
307
+ }
308
+ }
309
+ }
@@ -0,0 +1,181 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://hyprune.com/schema/registry/v0.1/release-record.schema.json",
4
+ "title": "Hyprune release record 0.1 (RFC-0015): <id>-<version>.release.json, published with the archive of one tag",
5
+ "type": "object",
6
+ "properties": {
7
+ "$schema": {
8
+ "$ref": "index.schema.json#/$defs/https"
9
+ },
10
+ "recordVersion": {
11
+ "const": "0.1"
12
+ },
13
+ "type": {
14
+ "enum": [
15
+ "world",
16
+ "library"
17
+ ]
18
+ },
19
+ "id": {
20
+ "$ref": "index.schema.json#/$defs/id"
21
+ },
22
+ "version": {
23
+ "$ref": "index.schema.json#/$defs/version"
24
+ },
25
+ "formatVersion": {
26
+ "$ref": "index.schema.json#/$defs/formatVersion"
27
+ },
28
+ "name": {
29
+ "type": "string",
30
+ "$ref": "index.schema.json#/$defs/line",
31
+ "maxLength": 64
32
+ },
33
+ "license": {
34
+ "$ref": "index.schema.json#/$defs/license"
35
+ },
36
+ "archive": {
37
+ "type": "object",
38
+ "properties": {
39
+ "name": {
40
+ "type": "string",
41
+ "maxLength": 220,
42
+ "pattern": "^[a-z][a-z0-9.-]*-[0-9A-Za-z.-]+\\.tar\\.zst$"
43
+ },
44
+ "sha256": {
45
+ "$ref": "index.schema.json#/$defs/sha256"
46
+ },
47
+ "bytes": {
48
+ "type": "integer",
49
+ "minimum": 1,
50
+ "maximum": 536870912
51
+ },
52
+ "expandedBytes": {
53
+ "type": "integer",
54
+ "minimum": 0,
55
+ "maximum": 1073741824
56
+ },
57
+ "files": {
58
+ "type": "integer",
59
+ "minimum": 1,
60
+ "maximum": 10000
61
+ }
62
+ },
63
+ "required": [
64
+ "name",
65
+ "sha256",
66
+ "bytes",
67
+ "expandedBytes",
68
+ "files"
69
+ ],
70
+ "additionalProperties": false
71
+ },
72
+ "requires": {
73
+ "$ref": "index.schema.json#/$defs/requires"
74
+ },
75
+ "thumbnails": {
76
+ "type": "array",
77
+ "maxItems": 4,
78
+ "items": {
79
+ "type": "object",
80
+ "properties": {
81
+ "name": {
82
+ "type": "string",
83
+ "maxLength": 220,
84
+ "pattern": "^[a-z][a-z0-9.-]*-[0-9A-Za-z.-]+\\.thumb-[1-4]\\.(png|jpg)$"
85
+ },
86
+ "sha256": {
87
+ "$ref": "index.schema.json#/$defs/sha256"
88
+ },
89
+ "width": {
90
+ "type": "integer",
91
+ "minimum": 1,
92
+ "maximum": 1280
93
+ },
94
+ "height": {
95
+ "type": "integer",
96
+ "minimum": 1,
97
+ "maximum": 720
98
+ }
99
+ },
100
+ "required": [
101
+ "name",
102
+ "sha256",
103
+ "width",
104
+ "height"
105
+ ],
106
+ "additionalProperties": false
107
+ }
108
+ },
109
+ "source": {
110
+ "type": "object",
111
+ "properties": {
112
+ "repo": {
113
+ "$ref": "index.schema.json#/$defs/https"
114
+ },
115
+ "tag": {
116
+ "$ref": "index.schema.json#/$defs/tag"
117
+ },
118
+ "commit": {
119
+ "type": "string",
120
+ "pattern": "^([0-9a-f]{40}|[0-9a-f]{64})$"
121
+ }
122
+ },
123
+ "required": [
124
+ "repo",
125
+ "tag",
126
+ "commit"
127
+ ],
128
+ "additionalProperties": false
129
+ },
130
+ "tool": {
131
+ "type": "object",
132
+ "properties": {
133
+ "name": {
134
+ "type": "string",
135
+ "$ref": "index.schema.json#/$defs/line",
136
+ "maxLength": 64
137
+ },
138
+ "version": {
139
+ "type": "string",
140
+ "minLength": 1,
141
+ "maxLength": 64,
142
+ "pattern": "^[0-9A-Za-z.+-]+$"
143
+ }
144
+ },
145
+ "required": [
146
+ "name",
147
+ "version"
148
+ ],
149
+ "additionalProperties": false
150
+ },
151
+ "checks": {
152
+ "description": "Informative: which local checks passed when the release was built. Clients never trust it.",
153
+ "type": "object",
154
+ "maxProperties": 16,
155
+ "propertyNames": {
156
+ "type": "string",
157
+ "maxLength": 32,
158
+ "pattern": "^[a-z][A-Za-z0-9]*$"
159
+ },
160
+ "additionalProperties": {
161
+ "enum": [
162
+ "pass",
163
+ "fail",
164
+ "skipped"
165
+ ]
166
+ }
167
+ }
168
+ },
169
+ "required": [
170
+ "recordVersion",
171
+ "type",
172
+ "id",
173
+ "version",
174
+ "formatVersion",
175
+ "name",
176
+ "license",
177
+ "archive",
178
+ "source"
179
+ ],
180
+ "additionalProperties": false
181
+ }