@moewolf/moe-icons-cli 0.0.1 → 0.0.3
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 +249 -10
- package/bin/check-node.cjs +36 -0
- package/bin/moeicons.js +12 -1
- package/dist/api/client.d.ts +13 -2
- package/dist/api/client.js +145 -21
- package/dist/auth/device-login.d.ts +10 -7
- package/dist/auth/device-login.js +60 -37
- package/dist/auth/login-schemas.d.ts +24 -0
- package/dist/auth/login-schemas.js +67 -0
- package/dist/catalog/catalog.d.ts +11 -0
- package/dist/catalog/catalog.js +43 -0
- package/dist/catalog/catalog.json +12768 -3965
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +773 -147
- package/dist/commands/parser.d.ts +13 -2
- package/dist/commands/parser.js +138 -50
- package/dist/config-package/generated/SOURCE.json +25 -0
- package/dist/config-package/generated/moeicons-config.schema.json +191 -0
- package/dist/config-package/generated/moeicons.config.jsonc +42 -0
- package/dist/config-package/generated/render-config.cjs +145 -0
- package/dist/config-package/generated/validate-config.cjs +293 -0
- package/dist/config-package/generated-config.d.ts +32 -0
- package/dist/config-package/generated-config.js +19 -0
- package/dist/core/auth.d.ts +24 -0
- package/dist/core/auth.js +127 -8
- package/dist/core/bitmap-assets.d.ts +19 -5
- package/dist/core/bitmap-assets.js +40 -31
- package/dist/core/bitmap-shard-budget.d.ts +43 -0
- package/dist/core/bitmap-shard-budget.js +16 -0
- package/dist/core/bitmap-shard-cache.d.ts +68 -0
- package/dist/core/bitmap-shard-cache.js +115 -0
- package/dist/core/bitmap-shard-download.d.ts +130 -0
- package/dist/core/bitmap-shard-download.js +359 -0
- package/dist/core/bitmap-shard-resolver.d.ts +129 -0
- package/dist/core/bitmap-shard-resolver.js +262 -0
- package/dist/core/bitmap-shards.d.ts +70 -0
- package/dist/core/bitmap-shards.js +249 -0
- package/dist/core/bootstrap.d.ts +37 -0
- package/dist/core/bootstrap.js +54 -0
- package/dist/core/cache.d.ts +34 -0
- package/dist/core/cache.js +96 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/doctor.d.ts +48 -0
- package/dist/core/doctor.js +190 -0
- package/dist/core/free-download.d.ts +64 -6
- package/dist/core/free-download.js +402 -30
- package/dist/core/generate.d.ts +29 -0
- package/dist/core/generate.js +341 -65
- package/dist/core/icon-selection.d.ts +8 -0
- package/dist/core/icon-selection.js +19 -0
- package/dist/core/install.d.ts +14 -0
- package/dist/core/install.js +132 -13
- package/dist/core/library-update.d.ts +5 -1
- package/dist/core/library-update.js +176 -17
- package/dist/core/local-test-env.d.ts +24 -0
- package/dist/core/local-test-env.js +43 -0
- package/dist/core/metadata-sync.d.ts +30 -0
- package/dist/core/metadata-sync.js +97 -0
- package/dist/core/pro-download.d.ts +63 -0
- package/dist/core/pro-download.js +247 -27
- package/dist/core/pro-install.d.ts +11 -3
- package/dist/core/pro-install.js +181 -19
- package/dist/core/pro-resources.d.ts +79 -0
- package/dist/core/pro-resources.js +204 -0
- package/dist/core/pro-selected-download.d.ts +18 -0
- package/dist/core/pro-selected-download.js +269 -0
- package/dist/core/release-descriptor.d.ts +42 -1
- package/dist/core/release-descriptor.js +117 -9
- package/dist/core/resource-variant.d.ts +27 -10
- package/dist/core/resource-variant.js +75 -34
- package/dist/core/selected-resources.d.ts +59 -0
- package/dist/core/selected-resources.js +350 -0
- package/dist/core/signed-artifact.d.ts +8 -1
- package/dist/core/signed-artifact.js +35 -11
- package/dist/core/target-subtree.d.ts +8 -0
- package/dist/core/target-subtree.js +54 -3
- package/dist/core/version-service.d.ts +11 -0
- package/dist/core/version-service.js +129 -24
- package/dist/core/wizard.d.ts +20 -17
- package/dist/core/wizard.js +66 -28
- package/dist/errors/errors.d.ts +2 -1
- package/dist/errors/errors.js +1 -0
- package/dist/generator/generate.d.ts +1 -1
- package/dist/generator/generate.js +420 -213
- package/dist/generator/shared/SOURCE.json +7 -0
- package/dist/generator/shared/svg-model.cjs +156 -0
- package/dist/generator/svg-model.d.ts +16 -0
- package/dist/generator/svg-model.js +4 -0
- package/dist/generator/theme-resolve.d.ts +2 -0
- package/dist/generator/theme-resolve.js +57 -26
- package/dist/mcp/server.d.ts +23 -36
- package/dist/mcp/server.js +74 -152
- package/dist/mcp/services.d.ts +4 -0
- package/dist/mcp/services.js +230 -0
- package/dist/metadata/archive.d.ts +38 -0
- package/dist/metadata/archive.js +94 -0
- package/dist/metadata/manifest.d.ts +33 -0
- package/dist/metadata/manifest.js +136 -0
- package/dist/metadata/version.d.ts +22 -0
- package/dist/metadata/version.js +53 -0
- package/dist/project/anchors/application.d.ts +13 -0
- package/dist/project/anchors/application.js +377 -0
- package/dist/project/anchors/apply.d.ts +28 -0
- package/dist/project/anchors/apply.js +65 -0
- package/dist/project/anchors/diagnose.d.ts +26 -0
- package/dist/project/anchors/diagnose.js +90 -0
- package/dist/project/anchors/helpers.d.ts +20 -0
- package/dist/project/anchors/helpers.js +55 -0
- package/dist/project/anchors/moeicons-config.d.ts +21 -0
- package/dist/project/anchors/moeicons-config.js +83 -0
- package/dist/project/anchors/parse.d.ts +14 -0
- package/dist/project/anchors/parse.js +36 -0
- package/dist/project/anchors/project-manifest.d.ts +19 -0
- package/dist/project/anchors/project-manifest.js +109 -0
- package/dist/project/anchors/style.d.ts +12 -0
- package/dist/project/anchors/style.js +87 -0
- package/dist/project/anchors/types.d.ts +32 -0
- package/dist/project/anchors/types.js +9 -0
- package/dist/project/config.d.ts +59 -5
- package/dist/project/config.js +234 -102
- package/dist/project/install-metadata.d.ts +23 -2
- package/dist/project/install-metadata.js +58 -7
- package/dist/project/install.d.ts +24 -4
- package/dist/project/install.js +402 -190
- package/dist/project/project-lock.d.ts +2 -2
- package/dist/project/project-lock.js +4 -4
- package/dist/project/tailwind.d.ts +2 -2
- package/dist/project/tailwind.js +3 -4
- package/dist/project/tar-gz.d.ts +18 -1
- package/dist/project/tar-gz.js +40 -3
- package/dist/tui/components.d.ts +62 -0
- package/dist/tui/components.js +100 -0
- package/dist/tui/primitives.d.ts +1 -0
- package/dist/tui/primitives.js +8 -1
- package/dist/ui/banner.d.ts +31 -3
- package/dist/ui/banner.js +120 -25
- package/dist/ui/branded-prompts.d.ts +41 -0
- package/dist/ui/branded-prompts.js +80 -0
- package/dist/ui/clack.d.ts +6 -3
- package/dist/ui/clack.js +56 -5
- package/dist/ui/create-ui.d.ts +1 -0
- package/dist/ui/create-ui.js +5 -1
- package/dist/ui/generated/wordmark-large.d.ts +17 -0
- package/dist/ui/generated/wordmark-large.js +28 -0
- package/dist/ui/generated/wordmark.d.ts +2 -0
- package/dist/ui/generated/wordmark.js +2 -0
- package/dist/ui/non-interactive.js +8 -0
- package/dist/ui/stream.js +30 -0
- package/dist/ui/theme.d.ts +42 -0
- package/dist/ui/theme.js +59 -0
- package/package.json +14 -6
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { sha256Bytes } from "../project/install-metadata.js";
|
|
2
|
+
import { gunzipSync } from "node:zlib";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync, renameSync, rmSync, readdirSync, } from "node:fs";
|
|
5
|
+
import { CliError } from "../errors/index.js";
|
|
6
|
+
import { runAccessTokenUseCase } from "./auth.js";
|
|
7
|
+
import { downloadSignedArtifact, parseSignedDescriptor } from "./signed-artifact.js";
|
|
8
|
+
import { parseResourceRefs, parseResourceIndex, planSelectedResources, downloadSelectedResources, } from "./selected-resources.js";
|
|
9
|
+
import { resolveProDescriptorEndpoint, proSignedDownloadOptions, extractProMetadata, fetchProDescriptor, } from "./pro-download.js";
|
|
10
|
+
import { validateConfigDocument, } from "../project/config.js";
|
|
11
|
+
import { parseCatalog } from "../catalog/catalog.js";
|
|
12
|
+
import { cacheArtifact } from "./cache.js";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
export async function downloadProSelected(context, auth, descriptor, selection, deps) {
|
|
15
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
16
|
+
const endpointInfo = resolveProDescriptorEndpoint(context.env);
|
|
17
|
+
const endpoint = new URL(endpointInfo.url.replace(/\/artifact-descriptor$/, "/resource-descriptor"));
|
|
18
|
+
if (endpoint.origin !== "https://api.moeicons.com" && !endpointInfo.allowLoopback)
|
|
19
|
+
throw new CliError("VALIDATION_ERROR", "resource descriptor endpoint must be the trusted API or a test loopback");
|
|
20
|
+
const getDescriptor = async (kind) => {
|
|
21
|
+
let token = await runAccessTokenUseCase(context, auth);
|
|
22
|
+
let refreshed = false;
|
|
23
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetchFn(endpoint, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
version: descriptor.version,
|
|
31
|
+
descriptorSha256: descriptor.descriptorSha256,
|
|
32
|
+
kind,
|
|
33
|
+
}),
|
|
34
|
+
redirect: "error",
|
|
35
|
+
signal: AbortSignal.any([context.signal, AbortSignal.timeout(30000)]),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
if (context.signal.aborted)
|
|
40
|
+
throw new CliError("CANCELLED", "selected resource download cancelled");
|
|
41
|
+
if (attempt === 2)
|
|
42
|
+
throw new CliError("NETWORK_ERROR", "resource authorization request failed after 3 attempts; retry the same fixed release");
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 200 * (attempt + 1)));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (response.status === 401 && !refreshed) {
|
|
47
|
+
await response.body?.cancel();
|
|
48
|
+
token = await runAccessTokenUseCase(context, auth, true);
|
|
49
|
+
refreshed = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (response.status === 403) {
|
|
53
|
+
await response.body?.cancel();
|
|
54
|
+
throw new CliError("FORBIDDEN", "active pro entitlement required for selected resources");
|
|
55
|
+
}
|
|
56
|
+
// Bound JSON reads independently of Content-Length.
|
|
57
|
+
const reader = response.body?.getReader();
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let size = 0;
|
|
60
|
+
if (!reader)
|
|
61
|
+
throw new CliError("NETWORK_ERROR", "empty resource descriptor response");
|
|
62
|
+
try {
|
|
63
|
+
for (;;) {
|
|
64
|
+
const part = await reader.read();
|
|
65
|
+
if (part.done)
|
|
66
|
+
break;
|
|
67
|
+
size += part.value.length;
|
|
68
|
+
if (size > 8192) {
|
|
69
|
+
await reader.cancel();
|
|
70
|
+
throw new CliError("VALIDATION_ERROR", "resource descriptor exceeds 8 KiB");
|
|
71
|
+
}
|
|
72
|
+
chunks.push(part.value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
let value;
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString());
|
|
81
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
82
|
+
throw new Error("invalid JSON object");
|
|
83
|
+
value = parsed;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new CliError("VALIDATION_ERROR", "invalid resource descriptor JSON");
|
|
87
|
+
}
|
|
88
|
+
if (response.status === 404 && value?.code === "RESOURCE_NOT_AVAILABLE")
|
|
89
|
+
return undefined;
|
|
90
|
+
if (response.status === 401)
|
|
91
|
+
throw new CliError("AUTH_ERROR", "resource authentication failed after refresh; run moeicons login");
|
|
92
|
+
if ([429, 500, 502, 503, 504].includes(response.status) && attempt < 2) {
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 200 * (attempt + 1)));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (response.status !== 200)
|
|
97
|
+
throw new CliError("NETWORK_ERROR", `resource descriptor request failed (${response.status}); no full archive was downloaded`);
|
|
98
|
+
const filename = kind === "resource-index"
|
|
99
|
+
? `moe-icons-pro-resource-index-${descriptor.version}.json.gz`
|
|
100
|
+
: `moe-icons-pro-resources-${descriptor.version}.bin`;
|
|
101
|
+
if (!value ||
|
|
102
|
+
Array.isArray(value) ||
|
|
103
|
+
value.ok !== true ||
|
|
104
|
+
value.tier !== "pro" ||
|
|
105
|
+
value.kind !== kind ||
|
|
106
|
+
value.version !== descriptor.version ||
|
|
107
|
+
value.descriptorSha256 !== descriptor.descriptorSha256 ||
|
|
108
|
+
value.parentArtifactSha256 !== descriptor.sha256 ||
|
|
109
|
+
value.filename !== filename ||
|
|
110
|
+
(kind === "resource-index" &&
|
|
111
|
+
(value.sha256 !== descriptor.resources.index.sha256 ||
|
|
112
|
+
value.size !== descriptor.resources.index.size)) ||
|
|
113
|
+
Object.keys(value).some((k) => ![
|
|
114
|
+
"ok",
|
|
115
|
+
"tier",
|
|
116
|
+
"kind",
|
|
117
|
+
"version",
|
|
118
|
+
"descriptorSha256",
|
|
119
|
+
"filename",
|
|
120
|
+
"parentArtifactSha256",
|
|
121
|
+
"url",
|
|
122
|
+
"expiresAt",
|
|
123
|
+
"size",
|
|
124
|
+
"sha256",
|
|
125
|
+
].includes(k)))
|
|
126
|
+
throw new CliError("VALIDATION_ERROR", "resource descriptor identity changed; retry the original fixed release");
|
|
127
|
+
const signed = parseSignedDescriptor({ url: value.url, expiresAt: value.expiresAt, size: value.size, sha256: value.sha256 }, context.now().getTime(), { allowLoopback: endpointInfo.allowLoopback });
|
|
128
|
+
return { ...signed, filename, parentArtifactSha256: descriptor.sha256 };
|
|
129
|
+
}
|
|
130
|
+
throw new CliError("NETWORK_ERROR", "resource descriptor retry budget exhausted");
|
|
131
|
+
};
|
|
132
|
+
const indexDescriptor = await getDescriptor("resource-index");
|
|
133
|
+
if (!indexDescriptor)
|
|
134
|
+
throw new CliError("VALIDATION_ERROR", "release advertises selected resources but the resource endpoint is unavailable; retry the same release or explicitly set downloadMode=full. No full archive was downloaded");
|
|
135
|
+
if (indexDescriptor.size > 8 * 1024 * 1024)
|
|
136
|
+
throw new CliError("VALIDATION_ERROR", "compressed resource index exceeds 8 MiB");
|
|
137
|
+
if (!descriptor.metadata)
|
|
138
|
+
throw new CliError("VALIDATION_ERROR", "pro metadata is missing");
|
|
139
|
+
const downloadOptions = proSignedDownloadOptions(context, deps);
|
|
140
|
+
const expired = (error) => error instanceof CliError &&
|
|
141
|
+
/HTTP (?:401|403)\b|failed with (?:401|403)\b|expired/i.test(error.message);
|
|
142
|
+
const originalMetadata = descriptor.metadata;
|
|
143
|
+
let currentMetadata = originalMetadata;
|
|
144
|
+
let metadataBytes;
|
|
145
|
+
for (let attempt = 0; attempt < 3; attempt++)
|
|
146
|
+
try {
|
|
147
|
+
metadataBytes = await downloadSignedArtifact(currentMetadata, downloadOptions);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (!expired(error) || attempt === 2)
|
|
152
|
+
throw error;
|
|
153
|
+
const renewed = await fetchProDescriptor(context, auth, { version: descriptor.version, descriptorSha256: descriptor.descriptorSha256 }, deps);
|
|
154
|
+
if (renewed.sha256 !== descriptor.sha256 ||
|
|
155
|
+
!renewed.metadata ||
|
|
156
|
+
renewed.metadata.sha256 !== originalMetadata.sha256 ||
|
|
157
|
+
renewed.metadata.size !== originalMetadata.size ||
|
|
158
|
+
JSON.stringify(renewed.resources) !== JSON.stringify(descriptor.resources))
|
|
159
|
+
throw new CliError("VALIDATION_ERROR", "release identity changed during metadata URL renewal");
|
|
160
|
+
currentMetadata = renewed.metadata;
|
|
161
|
+
}
|
|
162
|
+
if (!metadataBytes)
|
|
163
|
+
throw new CliError("NETWORK_ERROR", "metadata URL renewal failed");
|
|
164
|
+
const metadata = extractProMetadata(metadataBytes, descriptor.catalogSha256, descriptor.version);
|
|
165
|
+
const catalog = parseCatalog(JSON.parse(metadata.catalogJson));
|
|
166
|
+
const strict = validateConfigDocument(selection.document, catalog);
|
|
167
|
+
if (strict.kind !== "ok")
|
|
168
|
+
throw new CliError("VALIDATION_ERROR", strict.kind === "invalid" ? strict.message : `config state: ${strict.kind}`);
|
|
169
|
+
const cacheDir = context.env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
|
|
170
|
+
const io = {
|
|
171
|
+
mkdirSync: (path) => {
|
|
172
|
+
mkdirSync(path, { recursive: true });
|
|
173
|
+
},
|
|
174
|
+
writeFileSync,
|
|
175
|
+
readFileSync,
|
|
176
|
+
existsSync,
|
|
177
|
+
renameSync,
|
|
178
|
+
rmSync,
|
|
179
|
+
readdirSync,
|
|
180
|
+
};
|
|
181
|
+
const cached = join(cacheDir, "resources", "pro", descriptor.version, indexDescriptor.sha256, "index.json.gz");
|
|
182
|
+
let indexBytes = existsSync(cached) ? readFileSync(cached) : undefined;
|
|
183
|
+
if (!indexBytes ||
|
|
184
|
+
indexBytes.length !== indexDescriptor.size ||
|
|
185
|
+
sha256Bytes(indexBytes) !== indexDescriptor.sha256) {
|
|
186
|
+
let signed = indexDescriptor;
|
|
187
|
+
for (let attempt = 0; attempt < 3; attempt++)
|
|
188
|
+
try {
|
|
189
|
+
indexBytes = Buffer.from(await downloadSignedArtifact({
|
|
190
|
+
url: signed.url,
|
|
191
|
+
expiresAt: signed.expiresAt,
|
|
192
|
+
size: signed.size,
|
|
193
|
+
sha256: signed.sha256,
|
|
194
|
+
}, downloadOptions));
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (!expired(error) || attempt === 2)
|
|
199
|
+
throw error;
|
|
200
|
+
const renewed = await getDescriptor("resource-index");
|
|
201
|
+
if (!renewed ||
|
|
202
|
+
renewed.sha256 !== indexDescriptor.sha256 ||
|
|
203
|
+
renewed.size !== indexDescriptor.size)
|
|
204
|
+
throw new CliError("VALIDATION_ERROR", "resource index changed during URL renewal");
|
|
205
|
+
signed = renewed;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!indexBytes)
|
|
209
|
+
throw new CliError("NETWORK_ERROR", "resource index URL renewal failed");
|
|
210
|
+
let bundle;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(gunzipSync(indexBytes, { maxOutputLength: 64 * 1024 * 1024 }).toString());
|
|
213
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
214
|
+
throw new Error("invalid index object");
|
|
215
|
+
bundle = parsed.bundle;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw new CliError("VALIDATION_ERROR", "invalid compressed resource index");
|
|
219
|
+
}
|
|
220
|
+
const refs = parseResourceRefs({
|
|
221
|
+
schemaVersion: 1,
|
|
222
|
+
index: {
|
|
223
|
+
filename: indexDescriptor.filename,
|
|
224
|
+
size: indexDescriptor.size,
|
|
225
|
+
sha256: indexDescriptor.sha256,
|
|
226
|
+
},
|
|
227
|
+
bundle,
|
|
228
|
+
});
|
|
229
|
+
if (refs.bundle.sha256 !== descriptor.resources.bundle.sha256 ||
|
|
230
|
+
refs.bundle.size !== descriptor.resources.bundle.size ||
|
|
231
|
+
refs.bundle.filename !== descriptor.resources.bundle.filename)
|
|
232
|
+
throw new CliError("VALIDATION_ERROR", "resource index bundle does not match advertised release");
|
|
233
|
+
const index = parseResourceIndex(indexBytes, refs, {
|
|
234
|
+
version: descriptor.version,
|
|
235
|
+
tier: "pro",
|
|
236
|
+
artifactSha256: descriptor.sha256,
|
|
237
|
+
});
|
|
238
|
+
const plan = planSelectedResources(strict.config, catalog, index);
|
|
239
|
+
context.ui.note(`Download plan: pro/${strict.config.target}, ${strict.config.icons.length} registered icons, ${Object.keys(strict.config.themes).length} themes, ${plan.paths.length} resource files, up to ${plan.payloadBytes} compressed bytes (verified cache reduces network traffic).${plan.fallbacks.length ? ` Fallbacks: ${plan.fallbacks.join("; ")}` : ""}`, context.signal);
|
|
240
|
+
cacheArtifact(io, cached, indexBytes, refs.index.sha256);
|
|
241
|
+
const downloaded = await downloadSelectedResources(index, refs.index.sha256, plan.paths, {
|
|
242
|
+
io,
|
|
243
|
+
cacheDir,
|
|
244
|
+
fetch: fetchFn,
|
|
245
|
+
signal: context.signal,
|
|
246
|
+
allowedHosts: downloadOptions.allowedHosts,
|
|
247
|
+
...(endpointInfo.allowLoopback ? { allowLoopback: true } : {}),
|
|
248
|
+
getBundleUrl: async () => {
|
|
249
|
+
const signed = await getDescriptor("resource-bundle");
|
|
250
|
+
if (!signed ||
|
|
251
|
+
signed.sha256 !== refs.bundle.sha256 ||
|
|
252
|
+
signed.size !== refs.bundle.size ||
|
|
253
|
+
signed.filename !== refs.bundle.filename)
|
|
254
|
+
throw new CliError("VALIDATION_ERROR", "resource bundle changed after index verification");
|
|
255
|
+
return signed.url;
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
...metadata,
|
|
260
|
+
metadataBytes,
|
|
261
|
+
selected: {
|
|
262
|
+
...downloaded,
|
|
263
|
+
refs,
|
|
264
|
+
indexBytes,
|
|
265
|
+
payloadBytes: plan.payloadBytes,
|
|
266
|
+
fallbacks: plan.fallbacks,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ResourceRefs } from "./selected-resources.js";
|
|
1
2
|
export type ReleaseTarget = "react" | "vue" | "vanilla" | "assets";
|
|
2
3
|
export declare const RELEASE_TARGETS: readonly ReleaseTarget[];
|
|
3
4
|
/** Per-target subtree metadata written by the code-library release packer. */
|
|
@@ -7,13 +8,40 @@ export interface ReleaseTargetMetadata {
|
|
|
7
8
|
readonly fileCount: number;
|
|
8
9
|
readonly byteCount: number;
|
|
9
10
|
}
|
|
11
|
+
/** Per-file digest inside a metadata archive. */
|
|
12
|
+
export interface ReleaseFileDigest {
|
|
13
|
+
readonly size: number;
|
|
14
|
+
readonly sha256: string;
|
|
15
|
+
}
|
|
16
|
+
/** Reference to a tier's metadata archive (MANUAL.md / catalog.json / manifest.json). */
|
|
17
|
+
export interface ReleaseMetadataRef {
|
|
18
|
+
readonly filename: string;
|
|
19
|
+
readonly sha256: string;
|
|
20
|
+
readonly size: number;
|
|
21
|
+
readonly files: {
|
|
22
|
+
readonly "MANUAL.md": ReleaseFileDigest;
|
|
23
|
+
readonly "catalog.json": ReleaseFileDigest;
|
|
24
|
+
readonly "manifest.json": ReleaseFileDigest;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Optional assets-only archive reference (ICON-E2E-0907 / E2E-C7). */
|
|
28
|
+
export interface ReleaseAssetsRef {
|
|
29
|
+
readonly filename: string;
|
|
30
|
+
readonly sha256: string;
|
|
31
|
+
readonly size: number;
|
|
32
|
+
}
|
|
10
33
|
export interface ReleaseTierArtifact {
|
|
34
|
+
readonly resources?: ResourceRefs;
|
|
11
35
|
readonly filename: string;
|
|
12
36
|
readonly sha256: string;
|
|
37
|
+
readonly size?: number;
|
|
13
38
|
readonly styleGroups?: readonly string[];
|
|
14
39
|
readonly styleGroupCount?: number;
|
|
15
40
|
readonly targets?: readonly ReleaseTarget[];
|
|
16
41
|
readonly targetMetadata?: Readonly<Partial<Record<ReleaseTarget, ReleaseTargetMetadata>>>;
|
|
42
|
+
readonly metadata?: ReleaseMetadataRef;
|
|
43
|
+
/** Present on new descriptors; absent on legacy four-archive descriptors. */
|
|
44
|
+
readonly assets?: ReleaseAssetsRef;
|
|
17
45
|
}
|
|
18
46
|
export interface ReleaseCatalogRef {
|
|
19
47
|
readonly filename: string;
|
|
@@ -26,15 +54,28 @@ export interface ReleaseDescriptor {
|
|
|
26
54
|
readonly fullVersion: string;
|
|
27
55
|
readonly free: ReleaseTierArtifact;
|
|
28
56
|
readonly catalog: ReleaseCatalogRef;
|
|
57
|
+
/** ICON-E2E-0907 / E2E-E2: present only on local-test candidates. */
|
|
58
|
+
readonly channel?: "local-test";
|
|
59
|
+
readonly publishable?: boolean;
|
|
60
|
+
readonly schemaVersion?: number;
|
|
61
|
+
readonly baseVersion?: string;
|
|
29
62
|
}
|
|
30
63
|
/** Parse release-descriptor.json. Never invent free.filename. */
|
|
31
64
|
export declare function parseReleaseDescriptor(bytes: Uint8Array): ReleaseDescriptor;
|
|
65
|
+
/** True when a fullVersion is a `X.Y.Z-test` local candidate version. */
|
|
66
|
+
export declare function isLocalTestVersion(fullVersion: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Reject local-test candidate descriptors unless they are explicitly local and
|
|
69
|
+
* non-publishable: an HTTP/remote source, a missing `local-test` channel, or
|
|
70
|
+
* `publishable: true` must never be accepted by the free install flow.
|
|
71
|
+
*/
|
|
72
|
+
export declare function assertLocalCandidateAllowed(descriptor: ReleaseDescriptor, viaLocalFixture: boolean): void;
|
|
32
73
|
/** Parse `hex filename` sidecar produced next to the descriptor. */
|
|
33
74
|
export declare function parseSha256Sidecar(text: string): string;
|
|
34
75
|
export declare const PUBLIC_FREE_REPO: {
|
|
35
76
|
readonly owner: "moewolf-dev";
|
|
36
77
|
readonly name: "moe-icons";
|
|
37
78
|
};
|
|
38
|
-
export declare const FREE_DOWNLOAD_HOSTS: readonly ["github.com", "objects.githubusercontent.com"];
|
|
79
|
+
export declare const FREE_DOWNLOAD_HOSTS: readonly ["github.com", "objects.githubusercontent.com", "release-assets.githubusercontent.com"];
|
|
39
80
|
export declare function githubReleaseAssetUrl(tag: string, filename: string): string;
|
|
40
81
|
export declare function cacheKey(fullVersion: string, sha256: string): string;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { parseResourceRefs } from "./selected-resources.js";
|
|
1
2
|
const SHA256_HEX = /^[0-9a-f]{64}$/i;
|
|
2
3
|
const SAFE_FILENAME = /^[A-Za-z0-9._-]+$/;
|
|
3
4
|
const SAFE_SUBTREE_PATH = /^[A-Za-z0-9._/-]+$/;
|
|
4
5
|
export const RELEASE_TARGETS = ["react", "vue", "vanilla", "assets"];
|
|
6
|
+
const LOCAL_TEST_VERSION = /^(\d+)\.(\d+)\.(\d+)-test$/;
|
|
5
7
|
function isRecord(value) {
|
|
6
8
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
9
|
}
|
|
@@ -34,10 +36,14 @@ function parseTargetMetadata(value, field) {
|
|
|
34
36
|
if (typeof entry.path !== "string" || !SAFE_SUBTREE_PATH.test(entry.path)) {
|
|
35
37
|
throw new Error(`${field}.${key}.path must be a safe subtree path`);
|
|
36
38
|
}
|
|
37
|
-
if (typeof entry.fileCount !== "number" ||
|
|
39
|
+
if (typeof entry.fileCount !== "number" ||
|
|
40
|
+
!Number.isSafeInteger(entry.fileCount) ||
|
|
41
|
+
entry.fileCount < 0) {
|
|
38
42
|
throw new Error(`${field}.${key}.fileCount must be a non-negative integer`);
|
|
39
43
|
}
|
|
40
|
-
if (typeof entry.byteCount !== "number" ||
|
|
44
|
+
if (typeof entry.byteCount !== "number" ||
|
|
45
|
+
!Number.isSafeInteger(entry.byteCount) ||
|
|
46
|
+
entry.byteCount < 0) {
|
|
41
47
|
throw new Error(`${field}.${key}.byteCount must be a non-negative integer`);
|
|
42
48
|
}
|
|
43
49
|
result[target] = {
|
|
@@ -49,22 +55,80 @@ function parseTargetMetadata(value, field) {
|
|
|
49
55
|
}
|
|
50
56
|
return result;
|
|
51
57
|
}
|
|
52
|
-
function
|
|
58
|
+
function parseFileDigest(value, field) {
|
|
53
59
|
if (!isRecord(value))
|
|
54
60
|
throw new Error(`${field} must be an object`);
|
|
61
|
+
if (typeof value.size !== "number" || !Number.isSafeInteger(value.size) || value.size < 1) {
|
|
62
|
+
throw new Error(`${field}.size must be a positive integer`);
|
|
63
|
+
}
|
|
64
|
+
return { size: value.size, sha256: requireSha(value.sha256, `${field}.sha256`) };
|
|
65
|
+
}
|
|
66
|
+
function parseMetadataRef(value, field) {
|
|
67
|
+
if (!isRecord(value))
|
|
68
|
+
throw new Error(`${field} must be an object`);
|
|
69
|
+
if (!isRecord(value.files))
|
|
70
|
+
throw new Error(`${field}.files must be an object`);
|
|
71
|
+
const files = {
|
|
72
|
+
"MANUAL.md": parseFileDigest(value.files["MANUAL.md"], `${field}.files.MANUAL.md`),
|
|
73
|
+
"catalog.json": parseFileDigest(value.files["catalog.json"], `${field}.files.catalog.json`),
|
|
74
|
+
"manifest.json": parseFileDigest(value.files["manifest.json"], `${field}.files.manifest.json`),
|
|
75
|
+
};
|
|
76
|
+
if (typeof value.size !== "number" || !Number.isSafeInteger(value.size) || value.size < 1) {
|
|
77
|
+
throw new Error(`${field}.size must be a positive integer`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
filename: requireFilename(value.filename, `${field}.filename`),
|
|
81
|
+
sha256: requireSha(value.sha256, `${field}.sha256`),
|
|
82
|
+
size: value.size,
|
|
83
|
+
files,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function parseAssetsRef(value, field) {
|
|
87
|
+
if (!isRecord(value))
|
|
88
|
+
throw new Error(`${field} must be an object`);
|
|
89
|
+
if (typeof value.size !== "number" || !Number.isSafeInteger(value.size) || value.size < 1) {
|
|
90
|
+
throw new Error(`${field}.size must be a positive integer`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
filename: requireFilename(value.filename, `${field}.filename`),
|
|
94
|
+
sha256: requireSha(value.sha256, `${field}.sha256`),
|
|
95
|
+
size: value.size,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function parseTier(value, field, version) {
|
|
99
|
+
if (!isRecord(value))
|
|
100
|
+
throw new Error(`${field} must be an object`);
|
|
101
|
+
const resources = value.resources !== undefined ? parseResourceRefs(value.resources) : undefined;
|
|
102
|
+
if (resources &&
|
|
103
|
+
(resources.index.filename !== `moe-icons-${field}-resource-index-${version}.json.gz` ||
|
|
104
|
+
resources.bundle.filename !== `moe-icons-${field}-resources-${version}.bin`))
|
|
105
|
+
throw new Error(`${field}.resources filenames do not match tier/version`);
|
|
55
106
|
return {
|
|
107
|
+
...(resources ? { resources } : {}),
|
|
56
108
|
filename: requireFilename(value.filename, `${field}.filename`),
|
|
57
109
|
sha256: requireSha(value.sha256, `${field}.sha256`),
|
|
58
|
-
...(
|
|
110
|
+
...(typeof value.size === "number" && Number.isSafeInteger(value.size) && value.size > 0
|
|
111
|
+
? { size: value.size }
|
|
112
|
+
: {}),
|
|
113
|
+
...(Array.isArray(value.styleGroups) &&
|
|
114
|
+
value.styleGroups.every((item) => typeof item === "string")
|
|
59
115
|
? { styleGroups: value.styleGroups }
|
|
60
116
|
: {}),
|
|
61
|
-
...(typeof value.styleGroupCount === "number"
|
|
117
|
+
...(typeof value.styleGroupCount === "number"
|
|
118
|
+
? { styleGroupCount: value.styleGroupCount }
|
|
119
|
+
: {}),
|
|
62
120
|
...(Array.isArray(value.targets)
|
|
63
121
|
? { targets: value.targets.map((item) => requireTarget(item, `${field}.targets[]`)) }
|
|
64
122
|
: {}),
|
|
65
123
|
...(value.targetMetadata !== undefined
|
|
66
124
|
? { targetMetadata: parseTargetMetadata(value.targetMetadata, `${field}.targetMetadata`) }
|
|
67
125
|
: {}),
|
|
126
|
+
...(value.metadata !== undefined
|
|
127
|
+
? { metadata: parseMetadataRef(value.metadata, `${field}.metadata`) }
|
|
128
|
+
: {}),
|
|
129
|
+
...(value.assets !== undefined
|
|
130
|
+
? { assets: parseAssetsRef(value.assets, `${field}.assets`) }
|
|
131
|
+
: {}),
|
|
68
132
|
};
|
|
69
133
|
}
|
|
70
134
|
/** Parse release-descriptor.json. Never invent free.filename. */
|
|
@@ -83,18 +147,56 @@ export function parseReleaseDescriptor(bytes) {
|
|
|
83
147
|
}
|
|
84
148
|
if (!isRecord(parsed.catalog))
|
|
85
149
|
throw new Error("release-descriptor.json missing catalog");
|
|
150
|
+
const channel = parsed.channel === "local-test" ? "local-test" : undefined;
|
|
151
|
+
const publishable = parsed.publishable === true || parsed.publishable === false ? parsed.publishable : undefined;
|
|
86
152
|
return {
|
|
87
153
|
fullVersion: parsed.fullVersion,
|
|
88
|
-
free: parseTier(parsed.free, "free"),
|
|
154
|
+
free: parseTier(parsed.free, "free", parsed.fullVersion),
|
|
89
155
|
catalog: {
|
|
90
156
|
filename: requireFilename(parsed.catalog.filename, "catalog.filename"),
|
|
91
157
|
sha256: requireSha(parsed.catalog.sha256, "catalog.sha256"),
|
|
92
158
|
schemaVersion: typeof parsed.catalog.schemaVersion === "number" ? parsed.catalog.schemaVersion : 1,
|
|
93
|
-
...(typeof parsed.catalog.iconCount === "number"
|
|
94
|
-
|
|
159
|
+
...(typeof parsed.catalog.iconCount === "number"
|
|
160
|
+
? { iconCount: parsed.catalog.iconCount }
|
|
161
|
+
: {}),
|
|
162
|
+
...(typeof parsed.catalog.styleGroupCount === "number"
|
|
163
|
+
? { styleGroupCount: parsed.catalog.styleGroupCount }
|
|
164
|
+
: {}),
|
|
95
165
|
},
|
|
166
|
+
...(channel ? { channel } : {}),
|
|
167
|
+
...(publishable !== undefined ? { publishable } : {}),
|
|
168
|
+
...(typeof parsed.schemaVersion === "number" ? { schemaVersion: parsed.schemaVersion } : {}),
|
|
169
|
+
...(typeof parsed.baseVersion === "string" ? { baseVersion: parsed.baseVersion } : {}),
|
|
96
170
|
};
|
|
97
171
|
}
|
|
172
|
+
/** True when a fullVersion is a `X.Y.Z-test` local candidate version. */
|
|
173
|
+
export function isLocalTestVersion(fullVersion) {
|
|
174
|
+
return LOCAL_TEST_VERSION.test(fullVersion);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Reject local-test candidate descriptors unless they are explicitly local and
|
|
178
|
+
* non-publishable: an HTTP/remote source, a missing `local-test` channel, or
|
|
179
|
+
* `publishable: true` must never be accepted by the free install flow.
|
|
180
|
+
*/
|
|
181
|
+
export function assertLocalCandidateAllowed(descriptor, viaLocalFixture) {
|
|
182
|
+
if (!isLocalTestVersion(descriptor.fullVersion))
|
|
183
|
+
return;
|
|
184
|
+
if (!viaLocalFixture) {
|
|
185
|
+
throw new Error("local-test candidate descriptors are only accepted from a local release directory");
|
|
186
|
+
}
|
|
187
|
+
if (descriptor.channel !== "local-test") {
|
|
188
|
+
throw new Error("local-test candidate descriptor must declare channel local-test");
|
|
189
|
+
}
|
|
190
|
+
if (descriptor.publishable !== false) {
|
|
191
|
+
throw new Error("local-test candidate descriptor must declare publishable: false");
|
|
192
|
+
}
|
|
193
|
+
if (descriptor.schemaVersion !== 2) {
|
|
194
|
+
throw new Error("local-test candidate descriptor must be schemaVersion 2");
|
|
195
|
+
}
|
|
196
|
+
if (descriptor.baseVersion !== descriptor.fullVersion.replace(/-test$/, "")) {
|
|
197
|
+
throw new Error("local-test candidate descriptor baseVersion must match fullVersion");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
98
200
|
/** Parse `hex filename` sidecar produced next to the descriptor. */
|
|
99
201
|
export function parseSha256Sidecar(text) {
|
|
100
202
|
const token = text.trim().split(/\s+/)[0] ?? "";
|
|
@@ -103,7 +205,13 @@ export function parseSha256Sidecar(text) {
|
|
|
103
205
|
return token.toLowerCase();
|
|
104
206
|
}
|
|
105
207
|
export const PUBLIC_FREE_REPO = { owner: "moewolf-dev", name: "moe-icons" };
|
|
106
|
-
|
|
208
|
+
// GitHub Release assets currently redirect through release-assets.githubusercontent.com;
|
|
209
|
+
// keep the legacy objects host for older releases and CDN routing variants.
|
|
210
|
+
export const FREE_DOWNLOAD_HOSTS = [
|
|
211
|
+
"github.com",
|
|
212
|
+
"objects.githubusercontent.com",
|
|
213
|
+
"release-assets.githubusercontent.com",
|
|
214
|
+
];
|
|
107
215
|
export function githubReleaseAssetUrl(tag, filename) {
|
|
108
216
|
if (!tag.startsWith("v"))
|
|
109
217
|
throw new Error("release tag must be v<fullVersion>");
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* MEDIA-FORMAT-V2: styleGroupId vs resourceVariantId.
|
|
3
|
+
*
|
|
4
|
+
* Canonical variant ids are the actual source directory names:
|
|
5
|
+
* `<styleGroupId>-<imageSize>-<format>` (e.g. `example-bitmap-style-256-webp`). Parsing
|
|
6
|
+
* is controlled keyword-token based, never substring based; `3d` is an ordinary
|
|
7
|
+
* token. The legacy `<group ending -3d>-<format>-<size>` form is only readable
|
|
8
|
+
* through the explicit `parseLegacyResourceVariantId` compatibility branch.
|
|
5
9
|
*/
|
|
6
10
|
export declare const BITMAP_STYLE_GROUP_RE: RegExp;
|
|
7
|
-
export declare const
|
|
11
|
+
export declare const LEGACY_BITMAP_STYLE_GROUP_RE: RegExp;
|
|
12
|
+
export declare const BITMAP_FORMATS: readonly ["png", "webp"];
|
|
8
13
|
export declare const BITMAP_SIZES: readonly [64, 128, 256, 512];
|
|
9
14
|
export type BitmapFormat = (typeof BITMAP_FORMATS)[number];
|
|
10
15
|
export type BitmapSize = (typeof BITMAP_SIZES)[number];
|
|
@@ -16,17 +21,29 @@ export interface ResourceVariant {
|
|
|
16
21
|
}
|
|
17
22
|
export declare const DEFAULT_BITMAP_FORMAT: BitmapFormat;
|
|
18
23
|
export declare const DEFAULT_BITMAP_SIZE: BitmapSize;
|
|
19
|
-
export declare function isBitmapStyleGroupId(id: string): boolean;
|
|
20
24
|
export declare function isBitmapFormat(value: string): value is BitmapFormat;
|
|
21
25
|
export declare function isBitmapSize(value: number): value is BitmapSize;
|
|
22
|
-
/** Build
|
|
23
|
-
export declare function buildResourceVariantId(styleGroupId: string, format?:
|
|
26
|
+
/** Build the canonical v2 resourceVariantId (`<group>-<size>-<format>`). */
|
|
27
|
+
export declare function buildResourceVariantId(styleGroupId: string, format?: string, imageSize?: number): string;
|
|
24
28
|
/**
|
|
25
|
-
* Parse
|
|
26
|
-
*
|
|
29
|
+
* Parse a v2 name using complete `-` tokens. Returns undefined when the name is
|
|
30
|
+
* not a bitmap candidate (no format token); throws on duplicate/conflicting
|
|
31
|
+
* keywords; returns undefined when the canonical rebuilt id differs (e.g. the
|
|
32
|
+
* legacy format-before-size order).
|
|
27
33
|
*/
|
|
34
|
+
export declare function parseBitmapName(name: string): ResourceVariant | undefined;
|
|
35
|
+
/** Parse a canonical v2 resourceVariantId or throw. */
|
|
28
36
|
export declare function parseResourceVariantId(resourceVariantId: string): ResourceVariant;
|
|
29
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Explicit v1 compatibility branch: legacy ids are
|
|
39
|
+
* `<styleGroupId ending -3d>-<format>-<size>`. Never guessed by the v2 parser.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseLegacyResourceVariantId(resourceVariantId: string): ResourceVariant;
|
|
42
|
+
/**
|
|
43
|
+
* Build a canonical variant from a group id and optional format/size, applying
|
|
44
|
+
* the schema defaults `webp`/`256`. Availability must still be checked against
|
|
45
|
+
* the catalog by the caller (MEDIA-FORMAT-V2 D-05/B6).
|
|
46
|
+
*/
|
|
30
47
|
export declare function resolveResourceVariant(styleGroupId: string, options?: {
|
|
31
48
|
readonly format?: string;
|
|
32
49
|
readonly imageSize?: number;
|