@moewolf/moe-icons-cli 0.0.11 → 0.0.15
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.
- package/README.md +27 -19
- package/dist/cli.js +124 -146
- package/dist/config-package/generated/SOURCE.json +3 -3
- package/dist/config-package/generated/moeicons-config.schema.json +1 -1
- package/dist/core/bootstrap.js +5 -3
- package/dist/core/candidate-version-pointer.d.ts +13 -0
- package/dist/core/candidate-version-pointer.js +37 -0
- package/dist/core/config-package-plan.d.ts +25 -0
- package/dist/core/config-package-plan.js +156 -0
- package/dist/core/doctor.js +4 -0
- package/dist/core/free-download.js +34 -4
- package/dist/core/free-small-package-download.d.ts +67 -0
- package/dist/core/free-small-package-download.js +344 -0
- package/dist/core/generate.js +49 -33
- package/dist/core/install.d.ts +8 -2
- package/dist/core/install.js +70 -0
- package/dist/core/library-update.d.ts +6 -1
- package/dist/core/library-update.js +54 -0
- package/dist/core/manage.d.ts +4 -0
- package/dist/core/manage.js +4 -0
- package/dist/core/metadata-sync.js +119 -1
- package/dist/core/pro-download.js +19 -2
- package/dist/core/pro-install.d.ts +10 -4
- package/dist/core/pro-install.js +47 -0
- package/dist/core/pro-resources.d.ts +23 -0
- package/dist/core/pro-resources.js +51 -0
- package/dist/core/pro-selected-download.js +12 -2
- package/dist/core/pro-small-package-download.d.ts +61 -0
- package/dist/core/pro-small-package-download.js +301 -0
- package/dist/core/release-descriptor.js +6 -2
- package/dist/core/small-pack/PIN.json +11 -0
- package/dist/core/small-pack/candidate-set-v1.schema.json +187 -0
- package/dist/core/small-pack/candidate-set.cjs +286 -0
- package/dist/core/small-pack/candidate-set.d.cts +12 -0
- package/dist/core/small-pack/tar-read.cjs +220 -0
- package/dist/core/small-pack/tar-read.d.cts +2 -0
- package/dist/core/small-package-install.d.ts +40 -0
- package/dist/core/small-package-install.js +252 -0
- package/dist/core/small-package-selection.d.ts +27 -0
- package/dist/core/small-package-selection.js +114 -0
- package/dist/core/update-policy.d.ts +1 -0
- package/dist/core/update-policy.js +10 -3
- package/dist/mcp/services.js +1 -1
- package/dist/project/config.js +1 -1
- package/dist/project/install-metadata.d.ts +2 -1
- package/dist/project/install-metadata.js +6 -3
- package/dist/project/install.d.ts +11 -2
- package/dist/project/install.js +104 -12
- package/dist/project/package-install-metadata.d.ts +31 -0
- package/dist/project/package-install-metadata.js +99 -0
- package/dist/project/tar-gz.d.ts +4 -8
- package/dist/project/tar-gz.js +6 -10
- package/package.json +11 -11
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/moewolf-dev/moe-icons-code-library/main/schemas/small-pack-candidate-set-v1.schema.json",
|
|
4
|
+
"title": "Moe Icons small-package candidate-set v1",
|
|
5
|
+
"description": "Wire shape and per-object budgets for a tier-isolated package-set root or package page. Canonical bytes, digest equality, sorted order, cross-page uniqueness, dependency closure, and package contents are enforced by the executable candidate-set validator.",
|
|
6
|
+
"oneOf": [
|
|
7
|
+
{ "$ref": "#/$defs/candidateRoot" },
|
|
8
|
+
{ "$ref": "#/$defs/packagePage" }
|
|
9
|
+
],
|
|
10
|
+
"$defs": {
|
|
11
|
+
"sha256": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
14
|
+
},
|
|
15
|
+
"commit": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[a-f0-9]{40}$"
|
|
18
|
+
},
|
|
19
|
+
"version": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-(?:alpha|beta)(?:\\.(0|[1-9][0-9]*))?)?$"
|
|
22
|
+
},
|
|
23
|
+
"tier": {
|
|
24
|
+
"enum": ["free", "pro"]
|
|
25
|
+
},
|
|
26
|
+
"styleGroupId": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"pattern": "^moe-[a-z0-9]+(?:-[a-z0-9]+)*$"
|
|
29
|
+
},
|
|
30
|
+
"target": {
|
|
31
|
+
"enum": ["react", "vue", "vanilla"]
|
|
32
|
+
},
|
|
33
|
+
"candidateRoot": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": [
|
|
36
|
+
"schemaVersion",
|
|
37
|
+
"fullVersion",
|
|
38
|
+
"tier",
|
|
39
|
+
"sourceCommit",
|
|
40
|
+
"generatorCommit",
|
|
41
|
+
"packagePageCount",
|
|
42
|
+
"packagePages",
|
|
43
|
+
"packageSetSha256"
|
|
44
|
+
],
|
|
45
|
+
"properties": {
|
|
46
|
+
"schemaVersion": { "const": 1 },
|
|
47
|
+
"fullVersion": { "$ref": "#/$defs/version" },
|
|
48
|
+
"tier": { "$ref": "#/$defs/tier" },
|
|
49
|
+
"sourceCommit": { "$ref": "#/$defs/commit" },
|
|
50
|
+
"generatorCommit": { "$ref": "#/$defs/commit" },
|
|
51
|
+
"packagePageCount": { "type": "integer", "minimum": 1, "maximum": 128 },
|
|
52
|
+
"packagePages": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"minItems": 1,
|
|
55
|
+
"maxItems": 128,
|
|
56
|
+
"items": { "$ref": "#/$defs/pageReference" }
|
|
57
|
+
},
|
|
58
|
+
"packageSetSha256": { "$ref": "#/$defs/sha256" }
|
|
59
|
+
},
|
|
60
|
+
"additionalProperties": false
|
|
61
|
+
},
|
|
62
|
+
"pageReference": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"required": ["filename", "sha256", "sizeBytes", "packageCount"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"filename": { "type": "string", "pattern": "^package-page-(?:free|pro)-[0-9]{4}\\.json$" },
|
|
67
|
+
"sha256": { "$ref": "#/$defs/sha256" },
|
|
68
|
+
"sizeBytes": { "type": "integer", "minimum": 1, "maximum": 8388608 },
|
|
69
|
+
"packageCount": { "type": "integer", "minimum": 1, "maximum": 20000 }
|
|
70
|
+
},
|
|
71
|
+
"additionalProperties": false
|
|
72
|
+
},
|
|
73
|
+
"packagePage": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"required": ["schemaVersion", "fullVersion", "tier", "packages"],
|
|
76
|
+
"properties": {
|
|
77
|
+
"schemaVersion": { "const": 1 },
|
|
78
|
+
"fullVersion": { "$ref": "#/$defs/version" },
|
|
79
|
+
"tier": { "$ref": "#/$defs/tier" },
|
|
80
|
+
"packages": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"minItems": 1,
|
|
83
|
+
"maxItems": 20000,
|
|
84
|
+
"items": { "$ref": "#/$defs/packageRow" }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"additionalProperties": false
|
|
88
|
+
},
|
|
89
|
+
"packageRow": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"required": [
|
|
92
|
+
"packageSha256",
|
|
93
|
+
"filename",
|
|
94
|
+
"kind",
|
|
95
|
+
"tier",
|
|
96
|
+
"resourceKey",
|
|
97
|
+
"fullVersion",
|
|
98
|
+
"compressedBytes",
|
|
99
|
+
"expandedBytes",
|
|
100
|
+
"fileCount",
|
|
101
|
+
"dependencies"
|
|
102
|
+
],
|
|
103
|
+
"properties": {
|
|
104
|
+
"packageSha256": { "$ref": "#/$defs/sha256" },
|
|
105
|
+
"filename": { "type": "string", "pattern": "^packages/[a-f0-9]{64}\\.tgz$" },
|
|
106
|
+
"kind": { "enum": ["bitmap", "svg", "code", "shared", "metadata", "website"] },
|
|
107
|
+
"tier": { "$ref": "#/$defs/tier" },
|
|
108
|
+
"resourceKey": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
109
|
+
"fullVersion": { "$ref": "#/$defs/version" },
|
|
110
|
+
"compressedBytes": { "type": "integer", "minimum": 1, "maximum": 134217728 },
|
|
111
|
+
"expandedBytes": { "type": "integer", "minimum": 1, "maximum": 134217728 },
|
|
112
|
+
"fileCount": { "type": "integer", "minimum": 1, "maximum": 100000 },
|
|
113
|
+
"dependencies": {
|
|
114
|
+
"type": "array",
|
|
115
|
+
"uniqueItems": true,
|
|
116
|
+
"items": { "$ref": "#/$defs/sha256" }
|
|
117
|
+
},
|
|
118
|
+
"styleGroupId": { "$ref": "#/$defs/styleGroupId" },
|
|
119
|
+
"target": { "$ref": "#/$defs/target" },
|
|
120
|
+
"variant": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"required": ["width", "height", "format"],
|
|
123
|
+
"properties": {
|
|
124
|
+
"width": { "type": "integer", "minimum": 1, "maximum": 4096 },
|
|
125
|
+
"height": { "type": "integer", "minimum": 1, "maximum": 4096 },
|
|
126
|
+
"format": { "enum": ["png", "webp"] }
|
|
127
|
+
},
|
|
128
|
+
"additionalProperties": false
|
|
129
|
+
},
|
|
130
|
+
"inputSha256": { "$ref": "#/$defs/sha256" }
|
|
131
|
+
},
|
|
132
|
+
"allOf": [
|
|
133
|
+
{
|
|
134
|
+
"if": { "properties": { "kind": { "const": "bitmap" } }, "required": ["kind"] },
|
|
135
|
+
"then": {
|
|
136
|
+
"required": ["styleGroupId", "variant"],
|
|
137
|
+
"not": { "required": ["target"] },
|
|
138
|
+
"properties": {
|
|
139
|
+
"resourceKey": { "type": "string", "pattern": "^bitmap/(?:free|pro)/moe-[a-z0-9]+(?:-[a-z0-9]+)*/[1-9][0-9]*x[1-9][0-9]*/(?:png|webp)$" }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"if": { "properties": { "kind": { "const": "svg" } }, "required": ["kind"] },
|
|
145
|
+
"then": {
|
|
146
|
+
"required": ["styleGroupId"],
|
|
147
|
+
"not": { "anyOf": [{ "required": ["target"] }, { "required": ["variant"] }] },
|
|
148
|
+
"properties": { "compressedBytes": { "type": "integer", "maximum": 33554432 }, "expandedBytes": { "type": "integer", "maximum": 33554432 }, "resourceKey": { "type": "string", "pattern": "^svg/(?:free|pro)/moe-[a-z0-9]+(?:-[a-z0-9]+)*$" } }
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"if": { "properties": { "kind": { "const": "code" } }, "required": ["kind"] },
|
|
153
|
+
"then": {
|
|
154
|
+
"required": ["styleGroupId", "target"],
|
|
155
|
+
"not": { "required": ["variant"] },
|
|
156
|
+
"properties": { "compressedBytes": { "type": "integer", "maximum": 33554432 }, "expandedBytes": { "type": "integer", "maximum": 33554432 }, "resourceKey": { "type": "string", "pattern": "^code/(?:free|pro)/(?:react|vue|vanilla)/moe-[a-z0-9]+(?:-[a-z0-9]+)*$" } }
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"if": { "properties": { "kind": { "const": "shared" } }, "required": ["kind"] },
|
|
161
|
+
"then": {
|
|
162
|
+
"required": ["target"],
|
|
163
|
+
"not": { "anyOf": [{ "required": ["styleGroupId"] }, { "required": ["variant"] }] },
|
|
164
|
+
"properties": { "compressedBytes": { "type": "integer", "maximum": 33554432 }, "expandedBytes": { "type": "integer", "maximum": 33554432 }, "resourceKey": { "type": "string", "pattern": "^shared/(?:free|pro)/(?:react|vue|vanilla)$" } }
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"if": { "properties": { "kind": { "const": "metadata" } }, "required": ["kind"] },
|
|
169
|
+
"then": {
|
|
170
|
+
"required": ["styleGroupId"],
|
|
171
|
+
"not": { "anyOf": [{ "required": ["target"] }, { "required": ["variant"] }] },
|
|
172
|
+
"properties": { "compressedBytes": { "type": "integer", "maximum": 8388608 }, "expandedBytes": { "type": "integer", "maximum": 8388608 }, "resourceKey": { "type": "string", "pattern": "^metadata/(?:free|pro)/moe-[a-z0-9]+(?:-[a-z0-9]+)*$" } }
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"if": { "properties": { "kind": { "const": "website" } }, "required": ["kind"] },
|
|
177
|
+
"then": {
|
|
178
|
+
"required": ["styleGroupId"],
|
|
179
|
+
"not": { "anyOf": [{ "required": ["target"] }, { "required": ["variant"] }] },
|
|
180
|
+
"properties": { "compressedBytes": { "type": "integer", "maximum": 33554432 }, "expandedBytes": { "type": "integer", "maximum": 33554432 }, "resourceKey": { "type": "string", "pattern": "^website/(?:free|pro)/moe-[a-z0-9]+(?:-[a-z0-9]+)*$" } }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"additionalProperties": false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// S01 contract: the root is a small reference snapshot; resource bytes live in
|
|
4
|
+
// independently addressed packages and are fetched only by selected consumers.
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
|
|
7
|
+
const MAX_ROOT_BYTES = 1 * 1024 ** 2;
|
|
8
|
+
const MAX_PAGE_BYTES = 8 * 1024 ** 2;
|
|
9
|
+
const MAX_PACKAGE_BYTES = 128 * 1024 ** 2;
|
|
10
|
+
const MAX_CODE_PACKAGE_BYTES = 32 * 1024 ** 2;
|
|
11
|
+
const MAX_PACKAGES = 20_000;
|
|
12
|
+
// Keep a tier candidate within the downstream website projection's existing
|
|
13
|
+
// selected-package byte ceiling. This is a protocol fail-closed bound, not an
|
|
14
|
+
// approved Hosted capacity budget.
|
|
15
|
+
const MAX_CANDIDATE_BYTES = 4 * 1024 ** 3;
|
|
16
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
17
|
+
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:alpha|beta)(?:\.(0|[1-9]\d*))?)?$/;
|
|
18
|
+
const COMMIT = /^[a-f0-9]{40}$/;
|
|
19
|
+
const TARGETS = new Set(['react', 'vue', 'vanilla']);
|
|
20
|
+
const KINDS = new Set(['bitmap', 'svg', 'code', 'shared', 'metadata', 'website']);
|
|
21
|
+
|
|
22
|
+
function fail(message) {
|
|
23
|
+
throw new Error(`candidate-set: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonical(value) {
|
|
27
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
28
|
+
if (value && typeof value === 'object') {
|
|
29
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function canonicalBytes(value) {
|
|
35
|
+
return Buffer.from(`${JSON.stringify(canonical(value))}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256(bytes) {
|
|
39
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function record(value, field) {
|
|
43
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${field} must be an object`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function exactKeys(value, required, optional, field) {
|
|
48
|
+
const allowed = new Set([...required, ...optional]);
|
|
49
|
+
for (const key of required) if (!Object.hasOwn(value, key)) fail(`${field}.${key} is required`);
|
|
50
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) fail(`${field}.${key} is not allowed`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function positiveInteger(value, field, maximum) {
|
|
54
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) fail(`${field} is outside its byte/count budget`);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validatePageRef(value, index, tier) {
|
|
59
|
+
const field = `packagePages[${index}]`;
|
|
60
|
+
record(value, field);
|
|
61
|
+
exactKeys(value, ['filename', 'sha256', 'sizeBytes', 'packageCount'], [], field);
|
|
62
|
+
const expectedName = `package-page-${tier}-${String(index + 1).padStart(4, '0')}.json`;
|
|
63
|
+
if (value.filename !== expectedName) fail(`${field}.filename must be ${expectedName}`);
|
|
64
|
+
if (!SHA256.test(value.sha256 || '')) fail(`${field}.sha256 must be lowercase SHA-256`);
|
|
65
|
+
positiveInteger(value.sizeBytes, `${field}.sizeBytes`, MAX_PAGE_BYTES);
|
|
66
|
+
positiveInteger(value.packageCount, `${field}.packageCount`, MAX_PACKAGES);
|
|
67
|
+
return { filename: value.filename, sha256: value.sha256, sizeBytes: value.sizeBytes, packageCount: value.packageCount };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseCandidateSet(input) {
|
|
71
|
+
const bytes = Buffer.from(input);
|
|
72
|
+
if (bytes.length > MAX_ROOT_BYTES) fail('root descriptor exceeds 1 MiB');
|
|
73
|
+
let raw;
|
|
74
|
+
try { raw = JSON.parse(bytes.toString('utf8')); } catch { fail('root descriptor is not valid JSON'); }
|
|
75
|
+
record(raw, 'root');
|
|
76
|
+
if (!bytes.equals(canonicalBytes(raw))) fail('root descriptor is not canonical JSON');
|
|
77
|
+
exactKeys(raw, ['schemaVersion', 'fullVersion', 'tier', 'sourceCommit', 'generatorCommit', 'packagePageCount', 'packagePages', 'packageSetSha256'], [], 'root');
|
|
78
|
+
if (raw.schemaVersion !== 1) fail('schemaVersion must be 1');
|
|
79
|
+
if (!VERSION.test(raw.fullVersion || '')) fail('fullVersion is invalid');
|
|
80
|
+
if (raw.tier !== 'free' && raw.tier !== 'pro') fail('tier must be free or pro');
|
|
81
|
+
if (!COMMIT.test(raw.sourceCommit || '') || !COMMIT.test(raw.generatorCommit || '')) fail('source/generator commit must be 40-char lowercase SHA-1');
|
|
82
|
+
if (!Array.isArray(raw.packagePages) || raw.packagePages.length < 1 || raw.packagePages.length > 128) fail('packagePages count is outside 1..128');
|
|
83
|
+
if (raw.packagePageCount !== raw.packagePages.length) fail('packagePageCount mismatch');
|
|
84
|
+
const pages = raw.packagePages.map((page, index) => validatePageRef(page, index, raw.tier));
|
|
85
|
+
if (!SHA256.test(raw.packageSetSha256 || '')) fail('packageSetSha256 must be lowercase SHA-256');
|
|
86
|
+
const projection = {
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
fullVersion: raw.fullVersion,
|
|
89
|
+
tier: raw.tier,
|
|
90
|
+
sourceCommit: raw.sourceCommit,
|
|
91
|
+
generatorCommit: raw.generatorCommit,
|
|
92
|
+
packagePageCount: pages.length,
|
|
93
|
+
packagePages: pages,
|
|
94
|
+
};
|
|
95
|
+
if (sha256(canonicalBytes(projection)) !== raw.packageSetSha256) fail('packageSetSha256 mismatch');
|
|
96
|
+
return { ...projection, packageSetSha256: raw.packageSetSha256 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createCandidateSet({ fullVersion, tier, sourceCommit, generatorCommit, packagePages }) {
|
|
100
|
+
if (!Array.isArray(packagePages) || packagePages.length < 1 || packagePages.length > 128) fail('packagePages count is outside 1..128');
|
|
101
|
+
if (tier !== 'free' && tier !== 'pro') fail('tier must be free or pro');
|
|
102
|
+
const projection = {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
fullVersion,
|
|
105
|
+
tier,
|
|
106
|
+
sourceCommit,
|
|
107
|
+
generatorCommit,
|
|
108
|
+
packagePageCount: packagePages.length,
|
|
109
|
+
packagePages,
|
|
110
|
+
};
|
|
111
|
+
const bytes = canonicalBytes(projection);
|
|
112
|
+
if (bytes.length > MAX_ROOT_BYTES) fail('root descriptor exceeds 1 MiB');
|
|
113
|
+
return canonicalBytes({ ...projection, packageSetSha256: sha256(bytes) });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function expectedResourceKey(item) {
|
|
117
|
+
const { kind, tier, styleGroupId, target, variant } = item;
|
|
118
|
+
if (kind === 'bitmap') return `bitmap/${tier}/${styleGroupId}/${variant.width}x${variant.height}/${variant.format}`;
|
|
119
|
+
if (kind === 'svg') return `svg/${tier}/${styleGroupId}`;
|
|
120
|
+
if (kind === 'code') return `code/${tier}/${target}/${styleGroupId}`;
|
|
121
|
+
if (kind === 'shared') return `shared/${tier}/${target}`;
|
|
122
|
+
if (kind === 'metadata') return `metadata/${tier}/${styleGroupId || 'common'}`;
|
|
123
|
+
return `website/${tier}/${styleGroupId}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validatePackage(item, index, fullVersion) {
|
|
127
|
+
const field = `packages[${index}]`;
|
|
128
|
+
record(item, field);
|
|
129
|
+
exactKeys(item,
|
|
130
|
+
['packageSha256', 'filename', 'kind', 'tier', 'resourceKey', 'fullVersion', 'compressedBytes', 'expandedBytes', 'fileCount', 'dependencies'],
|
|
131
|
+
['styleGroupId', 'target', 'variant', 'inputSha256'], field);
|
|
132
|
+
if (item.inputSha256 !== undefined && !SHA256.test(item.inputSha256)) fail(`${field}.inputSha256 must be lowercase SHA-256`);
|
|
133
|
+
if (!KINDS.has(item.kind)) fail(`${field}.kind is not supported`);
|
|
134
|
+
if (item.tier !== 'free' && item.tier !== 'pro') fail(`${field}.tier must be free or pro`);
|
|
135
|
+
if (!SHA256.test(item.packageSha256 || '')) fail(`${field}.packageSha256 must be lowercase SHA-256`);
|
|
136
|
+
if (item.filename !== `packages/${item.packageSha256}.tgz`) fail(`${field}.filename must be content-addressed`);
|
|
137
|
+
positiveInteger(item.compressedBytes, `${field}.compressedBytes`, MAX_PACKAGE_BYTES);
|
|
138
|
+
positiveInteger(item.expandedBytes, `${field}.expandedBytes`, MAX_PACKAGE_BYTES);
|
|
139
|
+
if (!Number.isSafeInteger(item.fileCount) || item.fileCount < 1 || item.fileCount > 100_000) fail(`${field}.fileCount is invalid`);
|
|
140
|
+
if (!Array.isArray(item.dependencies) || item.dependencies.some((sha) => !SHA256.test(sha)) || new Set(item.dependencies).size !== item.dependencies.length || item.dependencies.includes(item.packageSha256) || item.dependencies.some((sha, position) => position > 0 && item.dependencies[position - 1].localeCompare(sha, 'en') >= 0)) fail(`${field}.dependencies is invalid`);
|
|
141
|
+
if (item.kind === 'bitmap') {
|
|
142
|
+
if (!/^moe-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(item.styleGroupId || '')) fail(`${field}.styleGroupId is invalid`);
|
|
143
|
+
record(item.variant, `${field}.variant`);
|
|
144
|
+
exactKeys(item.variant, ['width', 'height', 'format'], [], `${field}.variant`);
|
|
145
|
+
if (item.target !== undefined) fail(`${field}.target is not valid for bitmap`);
|
|
146
|
+
if (!Number.isSafeInteger(item.variant.width) || item.variant.width < 1 || item.variant.width > 4096 || item.variant.height !== item.variant.width) fail(`${field}.variant must be a square bitmap size`);
|
|
147
|
+
if (item.variant.format !== 'png' && item.variant.format !== 'webp') fail(`${field}.variant.format is unsupported`);
|
|
148
|
+
} else if (item.kind === 'shared') {
|
|
149
|
+
if (!TARGETS.has(item.target)) fail(`${field}.target is invalid`);
|
|
150
|
+
if (item.styleGroupId !== undefined || item.variant !== undefined) fail(`${field} shared package cannot be group/variant scoped`);
|
|
151
|
+
} else {
|
|
152
|
+
if (!/^moe-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(item.styleGroupId || '')) fail(`${field}.styleGroupId is required`);
|
|
153
|
+
if (item.kind === 'code' && !TARGETS.has(item.target)) fail(`${field}.target is invalid`);
|
|
154
|
+
if (item.kind !== 'code' && item.target !== undefined) fail(`${field}.target is not valid for ${item.kind}`);
|
|
155
|
+
if (item.variant !== undefined) fail(`${field}.variant is only valid for bitmap`);
|
|
156
|
+
}
|
|
157
|
+
const byteBudget = item.kind === 'metadata' ? MAX_PAGE_BYTES : ['svg', 'code', 'shared', 'website'].includes(item.kind) ? MAX_CODE_PACKAGE_BYTES : MAX_PACKAGE_BYTES;
|
|
158
|
+
if (item.compressedBytes > byteBudget || item.expandedBytes > byteBudget) fail(`${field} exceeds the ${item.kind} package budget`);
|
|
159
|
+
if (item.resourceKey !== expectedResourceKey(item)) fail(`${field}.resourceKey does not match its scope`);
|
|
160
|
+
if (item.fullVersion !== fullVersion) fail(`${field}.fullVersion mismatch`);
|
|
161
|
+
return item;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parsePackagePage(input, expectedRef, expectedVersion, expectedTier) {
|
|
165
|
+
const bytes = Buffer.from(input);
|
|
166
|
+
if (bytes.length > MAX_PAGE_BYTES) fail('package page exceeds 8 MiB');
|
|
167
|
+
if (expectedRef) {
|
|
168
|
+
if (bytes.length !== expectedRef.sizeBytes || sha256(bytes) !== expectedRef.sha256) fail('package page size/digest mismatch');
|
|
169
|
+
}
|
|
170
|
+
let raw;
|
|
171
|
+
try { raw = JSON.parse(bytes.toString('utf8')); } catch { fail('package page is not valid JSON'); }
|
|
172
|
+
record(raw, 'page');
|
|
173
|
+
if (!bytes.equals(canonicalBytes(raw))) fail('package page is not canonical JSON');
|
|
174
|
+
exactKeys(raw, ['schemaVersion', 'fullVersion', 'tier', 'packages'], [], 'page');
|
|
175
|
+
if (raw.schemaVersion !== 1 || raw.fullVersion !== expectedVersion || (raw.tier !== 'free' && raw.tier !== 'pro') || (expectedTier && raw.tier !== expectedTier) || !Array.isArray(raw.packages)) fail('package page identity/schema is invalid');
|
|
176
|
+
if (raw.packages.length < 1 || raw.packages.length > MAX_PACKAGES) fail('package count is outside budget');
|
|
177
|
+
const packages = raw.packages.map((item, index) => validatePackage(item, index, expectedVersion));
|
|
178
|
+
if (packages.some((item) => item.tier !== raw.tier)) fail('package page contains a package from another tier');
|
|
179
|
+
const keys = packages.map((item) => item.resourceKey);
|
|
180
|
+
if (new Set(keys).size !== keys.length) fail('duplicate resourceKey in package page');
|
|
181
|
+
if (new Set(packages.map((item) => item.packageSha256)).size !== packages.length) fail('duplicate package digest in package page');
|
|
182
|
+
if (keys.some((key, index) => index > 0 && keys[index - 1].localeCompare(key, 'en') >= 0)) fail('packages must be sorted by unique resourceKey');
|
|
183
|
+
if (expectedRef && expectedRef.packageCount !== packages.length) fail('packageCount mismatch');
|
|
184
|
+
return { schemaVersion: 1, fullVersion: raw.fullVersion, tier: raw.tier, packages };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function verifyPackagePayload(row, input) {
|
|
188
|
+
const bytes = Buffer.from(input);
|
|
189
|
+
if (bytes.length !== row.compressedBytes) fail(`package ${row.resourceKey} byte count mismatch`);
|
|
190
|
+
if (sha256(bytes) !== row.packageSha256) fail(`package ${row.resourceKey} SHA-256 mismatch`);
|
|
191
|
+
return bytes;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validatePackagePages(candidateSet, pageBytesByName) {
|
|
195
|
+
const set = typeof candidateSet === 'object' && !Buffer.isBuffer(candidateSet)
|
|
196
|
+
? parseCandidateSet(canonicalBytes(candidateSet))
|
|
197
|
+
: parseCandidateSet(candidateSet);
|
|
198
|
+
const all = [];
|
|
199
|
+
let compressedBytes = 0;
|
|
200
|
+
let expandedBytes = 0;
|
|
201
|
+
for (const ref of set.packagePages) {
|
|
202
|
+
const bytes = pageBytesByName instanceof Map ? pageBytesByName.get(ref.filename) : pageBytesByName?.[ref.filename];
|
|
203
|
+
if (!bytes) fail(`missing package page ${ref.filename}`);
|
|
204
|
+
const page = parsePackagePage(bytes, ref, set.fullVersion, set.tier).packages;
|
|
205
|
+
if (all.length + page.length > MAX_PACKAGES) fail(`candidate package count exceeds ${MAX_PACKAGES}`);
|
|
206
|
+
for (const item of page) {
|
|
207
|
+
compressedBytes += item.compressedBytes;
|
|
208
|
+
expandedBytes += item.expandedBytes;
|
|
209
|
+
if (compressedBytes > MAX_CANDIDATE_BYTES) fail(`candidate compressed package bytes exceed ${MAX_CANDIDATE_BYTES}`);
|
|
210
|
+
if (expandedBytes > MAX_CANDIDATE_BYTES) fail(`candidate expanded package bytes exceed ${MAX_CANDIDATE_BYTES}`);
|
|
211
|
+
}
|
|
212
|
+
all.push(...page);
|
|
213
|
+
}
|
|
214
|
+
if (new Set(all.map((item) => item.packageSha256)).size !== all.length) {
|
|
215
|
+
fail('duplicate package digest across candidate set');
|
|
216
|
+
}
|
|
217
|
+
const packageIds = new Set(all.map((item) => item.packageSha256));
|
|
218
|
+
const keys = new Set();
|
|
219
|
+
for (const item of all) {
|
|
220
|
+
if (keys.has(item.resourceKey)) fail(`duplicate resourceKey across pages: ${item.resourceKey}`);
|
|
221
|
+
keys.add(item.resourceKey);
|
|
222
|
+
for (const dependency of item.dependencies) if (!packageIds.has(dependency)) fail(`missing dependency package ${dependency}`);
|
|
223
|
+
}
|
|
224
|
+
return all;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildCandidateSet({ fullVersion, tier, sourceCommit, generatorCommit, packages }) {
|
|
228
|
+
if (!Array.isArray(packages) || packages.length < 1 || packages.length > MAX_PACKAGES) fail(`package count is outside 1..${MAX_PACKAGES}`);
|
|
229
|
+
if (tier !== 'free' && tier !== 'pro') fail('tier must be free or pro');
|
|
230
|
+
if (packages.some((item) => item.tier !== tier)) fail('candidate-set packages must belong to exactly one tier');
|
|
231
|
+
const sorted = packages.map((item, index) => validatePackage(item, index, fullVersion))
|
|
232
|
+
.sort((a, b) => a.resourceKey.localeCompare(b.resourceKey, 'en'));
|
|
233
|
+
let compressedBytes = 0;
|
|
234
|
+
let expandedBytes = 0;
|
|
235
|
+
for (const item of sorted) {
|
|
236
|
+
compressedBytes += item.compressedBytes;
|
|
237
|
+
expandedBytes += item.expandedBytes;
|
|
238
|
+
if (compressedBytes > MAX_CANDIDATE_BYTES) fail(`candidate compressed package bytes exceed ${MAX_CANDIDATE_BYTES}`);
|
|
239
|
+
if (expandedBytes > MAX_CANDIDATE_BYTES) fail(`candidate expanded package bytes exceed ${MAX_CANDIDATE_BYTES}`);
|
|
240
|
+
}
|
|
241
|
+
const keys = sorted.map((item) => item.resourceKey);
|
|
242
|
+
if (new Set(keys).size !== keys.length) fail('duplicate resourceKey across candidate set');
|
|
243
|
+
const pageBytesByName = new Map();
|
|
244
|
+
const refs = [];
|
|
245
|
+
let page = [];
|
|
246
|
+
function flush() {
|
|
247
|
+
if (!page.length) return;
|
|
248
|
+
const index = refs.length + 1;
|
|
249
|
+
const filename = `package-page-${tier}-${String(index).padStart(4, '0')}.json`;
|
|
250
|
+
const bytes = canonicalBytes({ schemaVersion: 1, fullVersion, tier, packages: page });
|
|
251
|
+
if (bytes.length > MAX_PAGE_BYTES) fail(`package page ${index} exceeds 8 MiB`);
|
|
252
|
+
refs.push({ filename, sha256: sha256(bytes), sizeBytes: bytes.length, packageCount: page.length });
|
|
253
|
+
pageBytesByName.set(filename, bytes);
|
|
254
|
+
page = [];
|
|
255
|
+
}
|
|
256
|
+
for (const item of sorted) {
|
|
257
|
+
const tentative = canonicalBytes({ schemaVersion: 1, fullVersion, tier, packages: [...page, item] });
|
|
258
|
+
if (tentative.length > MAX_PAGE_BYTES && page.length) flush();
|
|
259
|
+
page.push(item);
|
|
260
|
+
if (canonicalBytes({ schemaVersion: 1, fullVersion, tier, packages: page }).length > MAX_PAGE_BYTES) fail(`package ${item.resourceKey} cannot fit within one metadata page`);
|
|
261
|
+
}
|
|
262
|
+
flush();
|
|
263
|
+
if (refs.length > 128) fail('package page count exceeds 128');
|
|
264
|
+
const rootBytes = createCandidateSet({ fullVersion, tier, sourceCommit, generatorCommit, packagePages: refs });
|
|
265
|
+
const candidateSet = parseCandidateSet(rootBytes);
|
|
266
|
+
validatePackagePages(candidateSet, pageBytesByName);
|
|
267
|
+
return { candidateSet, rootBytes, pageBytesByName };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
MAX_ROOT_BYTES,
|
|
272
|
+
MAX_PAGE_BYTES,
|
|
273
|
+
MAX_PACKAGE_BYTES,
|
|
274
|
+
MAX_CODE_PACKAGE_BYTES,
|
|
275
|
+
MAX_PACKAGES,
|
|
276
|
+
MAX_CANDIDATE_BYTES,
|
|
277
|
+
canonicalBytes,
|
|
278
|
+
sha256,
|
|
279
|
+
parseCandidateSet,
|
|
280
|
+
parsePackagePage,
|
|
281
|
+
verifyPackagePayload,
|
|
282
|
+
validatePackagePages,
|
|
283
|
+
createCandidateSet,
|
|
284
|
+
buildCandidateSet,
|
|
285
|
+
expectedResourceKey,
|
|
286
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PackageRow {
|
|
2
|
+
packageSha256: string; filename: string; kind: 'bitmap' | 'svg' | 'code' | 'shared' | 'metadata' | 'website'; tier: 'free' | 'pro'; resourceKey: string; fullVersion: string;
|
|
3
|
+
compressedBytes: number; expandedBytes: number; fileCount: number; dependencies: string[]; styleGroupId?: string; target?: string; variant?: { width: number; height: number; format: string }; inputSha256?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface PageRef { filename: string; sha256: string; sizeBytes: number; packageCount: number }
|
|
6
|
+
export interface CandidateRoot { schemaVersion: number; fullVersion: string; tier: 'free' | 'pro'; sourceCommit: string; generatorCommit: string; packagePageCount: number; packagePages: PageRef[]; packageSetSha256: string }
|
|
7
|
+
export function canonicalBytes(value: unknown): Buffer;
|
|
8
|
+
export function sha256(bytes: Uint8Array): string;
|
|
9
|
+
export function parseCandidateSet(bytes: Uint8Array): CandidateRoot;
|
|
10
|
+
export function validatePackagePages(root: CandidateRoot, pages: Map<string, Uint8Array>): PackageRow[];
|
|
11
|
+
export function verifyPackagePayload(row: PackageRow, bytes: Uint8Array): Buffer;
|
|
12
|
+
export function buildCandidateSet(input: { fullVersion: string; tier: 'free' | 'pro'; sourceCommit: string; generatorCommit: string; packages: PackageRow[] }): { candidateSet: CandidateRoot; rootBytes: Buffer; pageBytesByName: Map<string, Buffer> };
|