@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.
Files changed (151) hide show
  1. package/README.md +249 -10
  2. package/bin/check-node.cjs +36 -0
  3. package/bin/moeicons.js +12 -1
  4. package/dist/api/client.d.ts +13 -2
  5. package/dist/api/client.js +145 -21
  6. package/dist/auth/device-login.d.ts +10 -7
  7. package/dist/auth/device-login.js +60 -37
  8. package/dist/auth/login-schemas.d.ts +24 -0
  9. package/dist/auth/login-schemas.js +67 -0
  10. package/dist/catalog/catalog.d.ts +11 -0
  11. package/dist/catalog/catalog.js +43 -0
  12. package/dist/catalog/catalog.json +12768 -3965
  13. package/dist/cli.d.ts +11 -2
  14. package/dist/cli.js +773 -147
  15. package/dist/commands/parser.d.ts +13 -2
  16. package/dist/commands/parser.js +138 -50
  17. package/dist/config-package/generated/SOURCE.json +25 -0
  18. package/dist/config-package/generated/moeicons-config.schema.json +191 -0
  19. package/dist/config-package/generated/moeicons.config.jsonc +42 -0
  20. package/dist/config-package/generated/render-config.cjs +145 -0
  21. package/dist/config-package/generated/validate-config.cjs +293 -0
  22. package/dist/config-package/generated-config.d.ts +32 -0
  23. package/dist/config-package/generated-config.js +19 -0
  24. package/dist/core/auth.d.ts +24 -0
  25. package/dist/core/auth.js +127 -8
  26. package/dist/core/bitmap-assets.d.ts +19 -5
  27. package/dist/core/bitmap-assets.js +40 -31
  28. package/dist/core/bitmap-shard-budget.d.ts +43 -0
  29. package/dist/core/bitmap-shard-budget.js +16 -0
  30. package/dist/core/bitmap-shard-cache.d.ts +68 -0
  31. package/dist/core/bitmap-shard-cache.js +115 -0
  32. package/dist/core/bitmap-shard-download.d.ts +130 -0
  33. package/dist/core/bitmap-shard-download.js +359 -0
  34. package/dist/core/bitmap-shard-resolver.d.ts +129 -0
  35. package/dist/core/bitmap-shard-resolver.js +262 -0
  36. package/dist/core/bitmap-shards.d.ts +70 -0
  37. package/dist/core/bitmap-shards.js +249 -0
  38. package/dist/core/bootstrap.d.ts +37 -0
  39. package/dist/core/bootstrap.js +54 -0
  40. package/dist/core/cache.d.ts +34 -0
  41. package/dist/core/cache.js +96 -0
  42. package/dist/core/context.d.ts +26 -0
  43. package/dist/core/doctor.d.ts +48 -0
  44. package/dist/core/doctor.js +190 -0
  45. package/dist/core/free-download.d.ts +64 -6
  46. package/dist/core/free-download.js +402 -30
  47. package/dist/core/generate.d.ts +29 -0
  48. package/dist/core/generate.js +341 -65
  49. package/dist/core/icon-selection.d.ts +8 -0
  50. package/dist/core/icon-selection.js +19 -0
  51. package/dist/core/install.d.ts +14 -0
  52. package/dist/core/install.js +132 -13
  53. package/dist/core/library-update.d.ts +5 -1
  54. package/dist/core/library-update.js +176 -17
  55. package/dist/core/local-test-env.d.ts +24 -0
  56. package/dist/core/local-test-env.js +43 -0
  57. package/dist/core/metadata-sync.d.ts +30 -0
  58. package/dist/core/metadata-sync.js +97 -0
  59. package/dist/core/pro-download.d.ts +63 -0
  60. package/dist/core/pro-download.js +247 -27
  61. package/dist/core/pro-install.d.ts +11 -3
  62. package/dist/core/pro-install.js +181 -19
  63. package/dist/core/pro-resources.d.ts +79 -0
  64. package/dist/core/pro-resources.js +204 -0
  65. package/dist/core/pro-selected-download.d.ts +18 -0
  66. package/dist/core/pro-selected-download.js +269 -0
  67. package/dist/core/release-descriptor.d.ts +42 -1
  68. package/dist/core/release-descriptor.js +117 -9
  69. package/dist/core/resource-variant.d.ts +27 -10
  70. package/dist/core/resource-variant.js +75 -34
  71. package/dist/core/selected-resources.d.ts +59 -0
  72. package/dist/core/selected-resources.js +350 -0
  73. package/dist/core/signed-artifact.d.ts +8 -1
  74. package/dist/core/signed-artifact.js +35 -11
  75. package/dist/core/target-subtree.d.ts +8 -0
  76. package/dist/core/target-subtree.js +54 -3
  77. package/dist/core/version-service.d.ts +11 -0
  78. package/dist/core/version-service.js +129 -24
  79. package/dist/core/wizard.d.ts +20 -17
  80. package/dist/core/wizard.js +66 -28
  81. package/dist/errors/errors.d.ts +2 -1
  82. package/dist/errors/errors.js +1 -0
  83. package/dist/generator/generate.d.ts +1 -1
  84. package/dist/generator/generate.js +420 -213
  85. package/dist/generator/shared/SOURCE.json +7 -0
  86. package/dist/generator/shared/svg-model.cjs +156 -0
  87. package/dist/generator/svg-model.d.ts +16 -0
  88. package/dist/generator/svg-model.js +4 -0
  89. package/dist/generator/theme-resolve.d.ts +2 -0
  90. package/dist/generator/theme-resolve.js +57 -26
  91. package/dist/mcp/server.d.ts +23 -36
  92. package/dist/mcp/server.js +74 -152
  93. package/dist/mcp/services.d.ts +4 -0
  94. package/dist/mcp/services.js +230 -0
  95. package/dist/metadata/archive.d.ts +38 -0
  96. package/dist/metadata/archive.js +94 -0
  97. package/dist/metadata/manifest.d.ts +33 -0
  98. package/dist/metadata/manifest.js +136 -0
  99. package/dist/metadata/version.d.ts +22 -0
  100. package/dist/metadata/version.js +53 -0
  101. package/dist/project/anchors/application.d.ts +13 -0
  102. package/dist/project/anchors/application.js +377 -0
  103. package/dist/project/anchors/apply.d.ts +28 -0
  104. package/dist/project/anchors/apply.js +65 -0
  105. package/dist/project/anchors/diagnose.d.ts +26 -0
  106. package/dist/project/anchors/diagnose.js +90 -0
  107. package/dist/project/anchors/helpers.d.ts +20 -0
  108. package/dist/project/anchors/helpers.js +55 -0
  109. package/dist/project/anchors/moeicons-config.d.ts +21 -0
  110. package/dist/project/anchors/moeicons-config.js +83 -0
  111. package/dist/project/anchors/parse.d.ts +14 -0
  112. package/dist/project/anchors/parse.js +36 -0
  113. package/dist/project/anchors/project-manifest.d.ts +19 -0
  114. package/dist/project/anchors/project-manifest.js +109 -0
  115. package/dist/project/anchors/style.d.ts +12 -0
  116. package/dist/project/anchors/style.js +87 -0
  117. package/dist/project/anchors/types.d.ts +32 -0
  118. package/dist/project/anchors/types.js +9 -0
  119. package/dist/project/config.d.ts +59 -5
  120. package/dist/project/config.js +234 -102
  121. package/dist/project/install-metadata.d.ts +23 -2
  122. package/dist/project/install-metadata.js +58 -7
  123. package/dist/project/install.d.ts +24 -4
  124. package/dist/project/install.js +402 -190
  125. package/dist/project/project-lock.d.ts +2 -2
  126. package/dist/project/project-lock.js +4 -4
  127. package/dist/project/tailwind.d.ts +2 -2
  128. package/dist/project/tailwind.js +3 -4
  129. package/dist/project/tar-gz.d.ts +18 -1
  130. package/dist/project/tar-gz.js +40 -3
  131. package/dist/tui/components.d.ts +62 -0
  132. package/dist/tui/components.js +100 -0
  133. package/dist/tui/primitives.d.ts +1 -0
  134. package/dist/tui/primitives.js +8 -1
  135. package/dist/ui/banner.d.ts +31 -3
  136. package/dist/ui/banner.js +120 -25
  137. package/dist/ui/branded-prompts.d.ts +41 -0
  138. package/dist/ui/branded-prompts.js +80 -0
  139. package/dist/ui/clack.d.ts +6 -3
  140. package/dist/ui/clack.js +56 -5
  141. package/dist/ui/create-ui.d.ts +1 -0
  142. package/dist/ui/create-ui.js +5 -1
  143. package/dist/ui/generated/wordmark-large.d.ts +17 -0
  144. package/dist/ui/generated/wordmark-large.js +28 -0
  145. package/dist/ui/generated/wordmark.d.ts +2 -0
  146. package/dist/ui/generated/wordmark.js +2 -0
  147. package/dist/ui/non-interactive.js +8 -0
  148. package/dist/ui/stream.js +30 -0
  149. package/dist/ui/theme.d.ts +42 -0
  150. package/dist/ui/theme.js +59 -0
  151. package/package.json +14 -6
@@ -0,0 +1,359 @@
1
+ /**
2
+ * BITMAP-SHARD-V1 (DEC-97..DEC-108): CLI shard descriptor fetch, direct-from-R2
3
+ * download, strict verification and atomic cached persistence.
4
+ *
5
+ * The bearer is only ever sent to the trusted API origin. The R2 shard download
6
+ * never forwards authorization/cookies (`downloadSignedArtifact`).
7
+ *
8
+ * Budget constants here are interim; `DEV-G08`/`OPS-05-05` freeze the final
9
+ * per-shard download/expand budgets after real measurement.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { join } from "node:path";
13
+ import { CliError } from "../errors/index.js";
14
+ import { cacheArtifact } from "./cache.js";
15
+ import { extractTarGz } from "../project/tar-gz.js";
16
+ import { PRO_DOWNLOAD_HOSTS } from "./pro-download.js";
17
+ import { downloadSignedArtifact } from "./signed-artifact.js";
18
+ import { BITMAP_SHARD_SCHEMA_VERSION, buildBitmapShardObjectKey, } from "./bitmap-shards.js";
19
+ import { BITMAP_SHARD_BUDGET, BITMAP_SHARD_MAX_ENTRIES, BITMAP_SHARD_MAX_EXPANDED_BYTES } from "./bitmap-shard-budget.js";
20
+ export { BITMAP_SHARD_MAX_ENTRIES, BITMAP_SHARD_MAX_EXPANDED_BYTES };
21
+ const API_ORIGIN = "https://api.moeicons.com";
22
+ const SHARD_ENDPOINT_PATH = "/v1/icon-library/pro/bitmap-shard-descriptor";
23
+ export const BITMAP_SHARD_DESCRIPTOR_URL = `${API_ORIGIN}${SHARD_ENDPOINT_PATH}`;
24
+ const SHA256 = /^[a-f0-9]{64}$/;
25
+ const ICON_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
26
+ const VERSION = /^\d+\.\d+\.\d+(?:-(?:alpha|beta))?$/;
27
+ const BITMAP_SIZES = new Set([64, 128, 256, 512]);
28
+ const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1"]);
29
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
30
+ /**
31
+ * Test seam: `MOEICONS_BITMAP_SHARD_DESCRIPTOR_URL` overrides the authenticated
32
+ * per-shard descriptor endpoint. Only https or loopback http is accepted, so a
33
+ * production endpoint can never be redirected by a stray environment variable
34
+ * to a plaintext or untrusted origin (mirrors the Pro descriptor seam).
35
+ */
36
+ export function resolveBitmapShardDescriptorEndpoint(env) {
37
+ const override = env.MOEICONS_BITMAP_SHARD_DESCRIPTOR_URL;
38
+ if (!override)
39
+ return { url: BITMAP_SHARD_DESCRIPTOR_URL, allowLoopback: false };
40
+ let parsed;
41
+ try {
42
+ parsed = new URL(override);
43
+ }
44
+ catch {
45
+ throw new CliError("VALIDATION_ERROR", "MOEICONS_BITMAP_SHARD_DESCRIPTOR_URL is not a valid URL");
46
+ }
47
+ const loopback = parsed.protocol === "http:" && LOOPBACK.has(parsed.hostname);
48
+ if (parsed.protocol !== "https:" && !loopback) {
49
+ throw new CliError("VALIDATION_ERROR", "MOEICONS_BITMAP_SHARD_DESCRIPTOR_URL must be https or loopback http");
50
+ }
51
+ return { url: override, allowLoopback: loopback };
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+ export function parseBitmapShardDescriptor(value, now, options = {}) {
57
+ if (!isRecord(value))
58
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
59
+ const allowed = ["ok", "tier", "version", "descriptorSha256", "styleGroupId", "imageSize", "format", "filename", "url", "expiresAt", "size", "sha256", "manifestSha256"];
60
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
61
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
62
+ if (value.ok !== undefined && value.ok !== true)
63
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
64
+ if (value.tier !== "pro" || typeof value.version !== "string" || !VERSION.test(value.version)) {
65
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
66
+ }
67
+ if (typeof value.descriptorSha256 !== "string" || !SHA256.test(value.descriptorSha256))
68
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
69
+ if (typeof value.styleGroupId !== "string" || typeof value.format !== "string" || (value.format !== "png" && value.format !== "webp")) {
70
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
71
+ }
72
+ const imageSize = value.imageSize;
73
+ if (!isRecord(imageSize) || typeof imageSize.width !== "number" || typeof imageSize.height !== "number"
74
+ || imageSize.width !== imageSize.height || !BITMAP_SIZES.has(imageSize.width)) {
75
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
76
+ }
77
+ if (typeof value.manifestSha256 !== "string" || !SHA256.test(value.manifestSha256))
78
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
79
+ const expectedKey = buildBitmapShardObjectKey({
80
+ tier: "pro",
81
+ styleGroupId: value.styleGroupId,
82
+ imageSize: { width: imageSize.width, height: imageSize.height },
83
+ format: value.format,
84
+ resourceVersion: value.version,
85
+ });
86
+ const expectedFilename = expectedKey.split("/").pop();
87
+ if (value.filename !== expectedFilename)
88
+ throw new CliError("VALIDATION_ERROR", "bitmap shard filename does not match its tuple");
89
+ const url = value.url;
90
+ const expiresAt = value.expiresAt;
91
+ if (typeof url !== "string" || typeof expiresAt !== "string" || typeof value.size !== "number" || !Number.isSafeInteger(value.size) || value.size < 1
92
+ || typeof value.sha256 !== "string" || !SHA256.test(value.sha256)) {
93
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard descriptor");
94
+ }
95
+ let parsed;
96
+ try {
97
+ parsed = new URL(url);
98
+ }
99
+ catch {
100
+ throw new CliError("VALIDATION_ERROR", "invalid bitmap shard URL");
101
+ }
102
+ const expires = Date.parse(expiresAt);
103
+ const loopbackHttp = options.allowLoopback === true && parsed.protocol === "http:" && (parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost" || parsed.hostname === "::1");
104
+ if ((parsed.protocol !== "https:" && !loopbackHttp) || !Number.isFinite(expires) || expires <= now) {
105
+ throw new CliError("VALIDATION_ERROR", "bitmap shard descriptor is expired or insecure");
106
+ }
107
+ return value;
108
+ }
109
+ /**
110
+ * POST one tuple to the trusted API. Bearer is never sent to R2. The request is
111
+ * bounded by `BITMAP_SHARD_BUDGET.descriptorTimeoutMs` unless overridden, and
112
+ * honors an external cancellation signal (DEV-G08).
113
+ */
114
+ export async function fetchBitmapShardDescriptor(tuple, accessToken, deps = {}) {
115
+ const url = deps.endpoint ?? BITMAP_SHARD_DESCRIPTOR_URL;
116
+ const controller = new AbortController();
117
+ const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? BITMAP_SHARD_BUDGET.descriptorTimeoutMs);
118
+ const onExternalAbort = () => controller.abort();
119
+ if (deps.signal?.aborted)
120
+ controller.abort();
121
+ deps.signal?.addEventListener("abort", onExternalAbort, { once: true });
122
+ try {
123
+ const response = await (deps.fetch ?? fetch)(url, {
124
+ method: "POST",
125
+ redirect: "error",
126
+ signal: controller.signal,
127
+ headers: { authorization: `Bearer ${accessToken}`, accept: "application/json", "content-type": "application/json" },
128
+ body: JSON.stringify({ version: tuple.version, descriptorSha256: tuple.descriptorSha256, styleGroupId: tuple.styleGroupId, imageSize: tuple.imageSize, format: tuple.format }),
129
+ });
130
+ if (!response.ok) {
131
+ const code = response.status === 401 ? "AUTH_ERROR" : response.status === 403 ? "FORBIDDEN" : response.status === 404 ? "NOT_FOUND" : "NETWORK_ERROR";
132
+ throw new CliError(code, `bitmap shard descriptor request failed with ${response.status}`);
133
+ }
134
+ return parseBitmapShardDescriptor(await response.json(), deps.now ?? Date.now(), deps.allowLoopback ? { allowLoopback: true } : {});
135
+ }
136
+ catch (error) {
137
+ if (error instanceof CliError)
138
+ throw error;
139
+ if (deps.signal?.aborted)
140
+ throw new CliError("CANCELLED", "bitmap shard descriptor request cancelled");
141
+ if (controller.signal.aborted)
142
+ throw new CliError("NETWORK_ERROR", "bitmap shard descriptor request timed out");
143
+ throw new CliError("NETWORK_ERROR", "bitmap shard descriptor request failed");
144
+ }
145
+ finally {
146
+ clearTimeout(timer);
147
+ deps.signal?.removeEventListener("abort", onExternalAbort);
148
+ }
149
+ }
150
+ function magicMatches(bytes, format) {
151
+ if (format === "png")
152
+ return bytes.byteLength >= 8 && Buffer.from(bytes.subarray(0, 8)).equals(PNG_SIGNATURE);
153
+ return bytes.byteLength >= 12 && Buffer.from(bytes).toString("ascii", 0, 4) === "RIFF" && Buffer.from(bytes).toString("ascii", 8, 12) === "WEBP";
154
+ }
155
+ function readImageSize(bytes, format) {
156
+ const buffer = Buffer.from(bytes);
157
+ if (format === "png") {
158
+ if (buffer.byteLength < 24 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE))
159
+ return undefined;
160
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
161
+ }
162
+ if (buffer.byteLength < 30 || buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WEBP")
163
+ return undefined;
164
+ const chunk = buffer.toString("ascii", 12, 16);
165
+ if (chunk === "VP8X")
166
+ return { width: 1 + (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)), height: 1 + (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) };
167
+ if (chunk === "VP8L") {
168
+ const bits = buffer.readUInt32LE(21);
169
+ return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 };
170
+ }
171
+ if (chunk === "VP8 ")
172
+ return { width: buffer.readUInt16LE(26) & 0x3fff, height: buffer.readUInt16LE(28) & 0x3fff };
173
+ return undefined;
174
+ }
175
+ /**
176
+ * DEC-101: verify response size/SHA, then the internal manifest digest,
177
+ * icon set, magic bytes and real dimensions before accepting any bytes.
178
+ */
179
+ function verifyShardContents(bytes, descriptor) {
180
+ if (bytes.byteLength !== descriptor.size) {
181
+ throw new CliError("VALIDATION_ERROR", `bitmap shard size mismatch: expected ${descriptor.size}, got ${bytes.byteLength}`);
182
+ }
183
+ if (bytes.byteLength > BITMAP_SHARD_BUDGET.compressedBytes) {
184
+ throw new CliError("VALIDATION_ERROR", `bitmap shard exceeds the compressed byte budget (${BITMAP_SHARD_BUDGET.compressedBytes})`);
185
+ }
186
+ const archiveSha = createHash("sha256").update(bytes).digest("hex");
187
+ if (archiveSha !== descriptor.sha256) {
188
+ throw new CliError("VALIDATION_ERROR", `bitmap shard SHA-256 mismatch: expected ${descriptor.sha256}, got ${archiveSha}`);
189
+ }
190
+ const extracted = extractTarGz(bytes, { maxEntries: BITMAP_SHARD_MAX_ENTRIES, maxExpandedBytes: BITMAP_SHARD_MAX_EXPANDED_BYTES });
191
+ if (extracted.errors.length > 0)
192
+ throw new CliError("VALIDATION_ERROR", `bitmap shard archive rejected: ${extracted.errors[0]}`);
193
+ const manifestBytes = extracted.files["manifest.json"];
194
+ if (!manifestBytes)
195
+ throw new CliError("VALIDATION_ERROR", "bitmap shard is missing manifest.json");
196
+ if (createHash("sha256").update(manifestBytes).digest("hex") !== descriptor.manifestSha256) {
197
+ throw new CliError("VALIDATION_ERROR", "bitmap shard manifest digest mismatch");
198
+ }
199
+ let manifest;
200
+ try {
201
+ manifest = JSON.parse(Buffer.from(manifestBytes).toString("utf8"));
202
+ }
203
+ catch {
204
+ throw new CliError("VALIDATION_ERROR", "bitmap shard manifest is not valid JSON");
205
+ }
206
+ if (manifest.schemaVersion !== BITMAP_SHARD_SCHEMA_VERSION || manifest.resourceVersion !== descriptor.version
207
+ || manifest.tier !== "pro" || manifest.styleGroupId !== descriptor.styleGroupId || manifest.format !== descriptor.format) {
208
+ throw new CliError("VALIDATION_ERROR", "bitmap shard manifest identity mismatch");
209
+ }
210
+ const declaredSize = manifest.imageSize;
211
+ if (!declaredSize || declaredSize.width !== descriptor.imageSize.width || declaredSize.height !== descriptor.imageSize.height) {
212
+ throw new CliError("VALIDATION_ERROR", "bitmap shard manifest imageSize mismatch");
213
+ }
214
+ const files = manifest.files;
215
+ if (!Array.isArray(files) || files.length === 0 || manifest.iconCount !== files.length) {
216
+ throw new CliError("VALIDATION_ERROR", "bitmap shard manifest files are invalid");
217
+ }
218
+ const expected = {};
219
+ const iconIds = [];
220
+ const seen = new Set();
221
+ for (const entry of files) {
222
+ if (!isRecord(entry) || typeof entry.iconId !== "string" || !ICON_ID.test(entry.iconId) || seen.has(entry.iconId)) {
223
+ throw new CliError("VALIDATION_ERROR", "bitmap shard manifest has an invalid icon id");
224
+ }
225
+ seen.add(entry.iconId);
226
+ const path = `icons/${entry.iconId}.${descriptor.format}`;
227
+ if (entry.path !== path)
228
+ throw new CliError("VALIDATION_ERROR", `bitmap shard manifest path ${String(entry.path)} is not canonical`);
229
+ const content = extracted.files[path];
230
+ if (!content)
231
+ throw new CliError("VALIDATION_ERROR", `bitmap shard is missing ${path}`);
232
+ if (content.byteLength > BITMAP_SHARD_BUDGET.singleFileBytes) {
233
+ throw new CliError("VALIDATION_ERROR", `bitmap shard payload ${path} exceeds the single-file byte budget`);
234
+ }
235
+ if (typeof entry.byteSize !== "number" || entry.byteSize !== content.byteLength)
236
+ throw new CliError("VALIDATION_ERROR", `bitmap shard size drift for ${path}`);
237
+ if (typeof entry.sha256 !== "string" || createHash("sha256").update(content).digest("hex") !== entry.sha256) {
238
+ throw new CliError("VALIDATION_ERROR", `bitmap shard digest drift for ${path}`);
239
+ }
240
+ if (!magicMatches(content, descriptor.format))
241
+ throw new CliError("VALIDATION_ERROR", `bitmap shard magic does not match ${descriptor.format} for ${path}`);
242
+ const size = readImageSize(content, descriptor.format);
243
+ if (!size || size.width !== descriptor.imageSize.width || size.height !== descriptor.imageSize.height) {
244
+ throw new CliError("VALIDATION_ERROR", `bitmap shard dimensions do not match its tuple for ${path}`);
245
+ }
246
+ expected[path] = content;
247
+ iconIds.push(entry.iconId);
248
+ }
249
+ const declaredFiles = Object.keys(extracted.files).filter((name) => name.startsWith("icons/"));
250
+ if (declaredFiles.length !== files.length)
251
+ throw new CliError("VALIDATION_ERROR", "bitmap shard archive has undeclared icons");
252
+ iconIds.sort((a, b) => a.localeCompare(b, "en"));
253
+ return { files: expected, iconIds };
254
+ }
255
+ /** DEC-101 verification of downloaded shard bytes against the full descriptor. */
256
+ export function verifyBitmapShardArchive(bytes, descriptor) {
257
+ const { files, iconIds } = verifyShardContents(bytes, descriptor);
258
+ return { descriptor: descriptor, files, iconIds };
259
+ }
260
+ /** Unambiguous cache identity (DEC-101). */
261
+ export function bitmapShardCacheKey(identity) {
262
+ return [identity.version, "pro", identity.styleGroupId, identity.imageSize.width, identity.imageSize.height, identity.format, identity.sha256].join("/");
263
+ }
264
+ export function bitmapShardCachePath(cacheDir, identity) {
265
+ return join(cacheDir, "bitmap-shards", ...bitmapShardCacheKey(identity).split("/").slice(0, -1), `${identity.sha256}.tgz`);
266
+ }
267
+ /**
268
+ * Offline/pin path: load one shard from the local cache and re-verify its
269
+ * identity, size, SHA, manifest digest, icon set and dimensions. A missing or
270
+ * partial cache fails closed; a poisoned cache fails digest verification.
271
+ */
272
+ export function loadCachedBitmapShard(target, cacheDir, io) {
273
+ const path = bitmapShardCachePath(cacheDir, target);
274
+ if (!io.existsSync(path) || !io.readFileSync) {
275
+ throw new CliError("VALIDATION_ERROR", `bitmap shard is not cached for ${target.styleGroupId}/${target.imageSize.width}/${target.format}; run install while online`);
276
+ }
277
+ const bytes = io.readFileSync(path);
278
+ const { files, iconIds } = verifyShardContents(bytes, target);
279
+ return { target, files, iconIds };
280
+ }
281
+ /** Reconstruct the offline verification identity pinned in install metadata. */
282
+ export function bitmapShardVerificationTargetFromPin(pin) {
283
+ return {
284
+ version: pin.resourceVersion,
285
+ styleGroupId: pin.styleGroupId,
286
+ imageSize: pin.imageSize,
287
+ format: pin.format,
288
+ size: pin.compressedSize,
289
+ sha256: pin.sha256,
290
+ manifestSha256: pin.manifestSha256,
291
+ };
292
+ }
293
+ /** Build the canonical pin identity from a freshly verified download. */
294
+ export function bitmapShardPinFromVerified(verified) {
295
+ const descriptor = verified.descriptor;
296
+ const expandedSize = Object.values(verified.files).reduce((total, bytes) => total + bytes.byteLength, 0);
297
+ const filename = descriptor.filename;
298
+ return {
299
+ schemaVersion: BITMAP_SHARD_SCHEMA_VERSION,
300
+ resourceVersion: descriptor.version,
301
+ tier: "pro",
302
+ styleGroupId: descriptor.styleGroupId,
303
+ imageSize: descriptor.imageSize,
304
+ format: descriptor.format,
305
+ filename,
306
+ objectKey: buildBitmapShardObjectKey({
307
+ tier: "pro",
308
+ styleGroupId: descriptor.styleGroupId,
309
+ imageSize: descriptor.imageSize,
310
+ format: descriptor.format,
311
+ resourceVersion: descriptor.version,
312
+ filename,
313
+ }),
314
+ compressedSize: descriptor.size,
315
+ expandedSize,
316
+ sha256: descriptor.sha256,
317
+ fileCount: verified.iconIds.length,
318
+ manifestSha256: descriptor.manifestSha256,
319
+ };
320
+ }
321
+ /**
322
+ * Download directly from the private R2 signed host and atomically cache the
323
+ * verified archive. Any size/SHA/manifest/dimension drift fails closed.
324
+ */
325
+ export async function downloadAndCacheBitmapShard(descriptor, deps) {
326
+ // DEV-G08-R2: reject an over-budget shard BEFORE issuing any network request,
327
+ // so a hostile/oversized descriptor costs zero bytes and never touches cache.
328
+ if (descriptor.size > BITMAP_SHARD_BUDGET.compressedBytes) {
329
+ throw new CliError("VALIDATION_ERROR", `bitmap shard ${descriptor.filename} exceeds the compressed byte budget (${BITMAP_SHARD_BUDGET.compressedBytes})`);
330
+ }
331
+ // DEV-G08: reserve room for the compressed shard PLUS the unpacked staging
332
+ // headroom before downloading. A missing statfs is treated as "enough"
333
+ // (non-Linux hosts); a statfs that throws fails closed, never silently.
334
+ if (deps.statfs) {
335
+ let available;
336
+ try {
337
+ available = deps.statfs(deps.cacheDir);
338
+ }
339
+ catch {
340
+ throw new CliError("DISK_FULL", `cannot determine cache space for bitmap shard ${descriptor.filename}`);
341
+ }
342
+ const required = descriptor.size + BITMAP_SHARD_BUDGET.tempDiskBytes;
343
+ if (available && available.availableBytes < required) {
344
+ throw new CliError("DISK_FULL", `insufficient cache space for bitmap shard ${descriptor.filename} (needs ${required} bytes, has ${available.availableBytes})`);
345
+ }
346
+ }
347
+ const bytes = await downloadSignedArtifact({ url: descriptor.url, expiresAt: descriptor.expiresAt, size: descriptor.size, sha256: descriptor.sha256 }, {
348
+ allowedHosts: deps.allowedHosts ?? PRO_DOWNLOAD_HOSTS,
349
+ ...(deps.fetch ? { fetch: deps.fetch } : {}),
350
+ ...(deps.signal ? { signal: deps.signal } : {}),
351
+ ...(deps.now !== undefined ? { now: deps.now } : {}),
352
+ ...(deps.allowLoopback ? { allowLoopback: true } : {}),
353
+ ...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
354
+ timeoutMs: deps.timeoutMs ?? BITMAP_SHARD_BUDGET.downloadTimeoutMs,
355
+ });
356
+ const verified = verifyBitmapShardArchive(bytes, descriptor);
357
+ cacheArtifact(deps.io, bitmapShardCachePath(deps.cacheDir, descriptor), bytes, descriptor.sha256);
358
+ return verified;
359
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * DEV-G07 (DEC-100/101/107): normalize configured bitmap references to canonical
3
+ * tuples, fetch exactly those shards (one descriptor each, bearer only to the
4
+ * API), and map verified shard bytes back to the existing local `assets/`
5
+ * layout so generated code/imports never change.
6
+ */
7
+ import type { MoeiconsConfigFile } from "../project/config.js";
8
+ import type { IconCatalog } from "../catalog/catalog.js";
9
+ import { type BitmapShard, type BitmapShardFormat, type BitmapShardImageSize } from "./bitmap-shards.js";
10
+ import { type VerifiedBitmapShard } from "./bitmap-shard-download.js";
11
+ import type { CacheIo } from "./cache.js";
12
+ export interface BitmapShardTuple {
13
+ readonly styleGroupId: string;
14
+ readonly imageSize: BitmapShardImageSize;
15
+ readonly format: BitmapShardFormat;
16
+ }
17
+ /**
18
+ * Resolve every bitmap variant the config actually references into unique,
19
+ * canonically sorted tuples. Same tuple used by many icons/themes appears once.
20
+ */
21
+ export declare function resolveBitmapTuples(config: MoeiconsConfigFile, sourceCatalog?: IconCatalog): {
22
+ readonly ok: true;
23
+ readonly tuples: readonly BitmapShardTuple[];
24
+ } | {
25
+ readonly ok: false;
26
+ readonly errors: readonly string[];
27
+ };
28
+ export interface BitmapShardRequest extends BitmapShardTuple {
29
+ readonly filename: string;
30
+ readonly objectKey: string;
31
+ }
32
+ /** Pure canonical request plan (one immutable key per selected tuple). */
33
+ export declare function planBitmapShardRequests(tuples: readonly BitmapShardTuple[], version: string): BitmapShardRequest[];
34
+ export interface FetchBitmapShardsDeps {
35
+ readonly version: string;
36
+ readonly descriptorSha256: string;
37
+ readonly accessToken: string;
38
+ readonly cacheDir: string;
39
+ readonly io: CacheIo;
40
+ readonly fetch?: typeof fetch;
41
+ readonly allowedHosts?: readonly string[];
42
+ readonly now?: number;
43
+ readonly allowLoopback?: boolean;
44
+ readonly endpoint?: string;
45
+ readonly signal?: AbortSignal;
46
+ readonly statfs?: (dir: string) => {
47
+ readonly availableBytes: number;
48
+ } | undefined;
49
+ /**
50
+ * DEV-G10: already-pinned shards. A tuple whose pinned shard still verifies in
51
+ * the cache is reused without any API/R2 request, so switching size only
52
+ * fetches the newly selected tuple.
53
+ */
54
+ readonly existingPins?: readonly BitmapShard[];
55
+ /** W2-B6: coarse per-tuple + byte-level progress for the UI. */
56
+ readonly onProgress?: (event: BitmapShardFetchProgress) => void;
57
+ }
58
+ /** W2-B6: progress events emitted while obtaining selected shards. */
59
+ export interface BitmapShardFetchProgress {
60
+ readonly tuple: BitmapShardTuple;
61
+ /** 1-based index of the tuple being processed. */
62
+ readonly index: number;
63
+ readonly total: number;
64
+ readonly phase: "reuse" | "descriptor" | "download" | "done";
65
+ readonly downloadedBytes?: number;
66
+ readonly totalBytes?: number;
67
+ }
68
+ export interface ResolvedBitmapShards {
69
+ readonly requests: readonly BitmapShardRequest[];
70
+ readonly shards: readonly VerifiedBitmapShard[];
71
+ /** Canonical pin identities recorded in install metadata. */
72
+ readonly pins: readonly BitmapShard[];
73
+ /** Local-layout files: `assets/<resourceVariantId>/<iconId>.<format>` -> bytes. */
74
+ readonly files: Readonly<Record<string, Uint8Array>>;
75
+ }
76
+ /** Map verified shard bytes to the canonical local `assets/` layout. */
77
+ export declare function shardAssetsToLocalLayout(shard: VerifiedBitmapShard): Record<string, Uint8Array>;
78
+ /**
79
+ * Fetch exactly the selected shards, sequentially (concurrency 1 per DEC-102/
80
+ * 107), verify each, and return the merged local-layout file map. Never
81
+ * prefetches unselected tuples/formats/sizes.
82
+ */
83
+ export declare function fetchSelectedBitmapShards(tuples: readonly BitmapShardTuple[], deps: FetchBitmapShardsDeps): Promise<ResolvedBitmapShards>;
84
+ export interface EnsureBitmapShardsDeps {
85
+ readonly config: MoeiconsConfigFile;
86
+ readonly catalog: IconCatalog;
87
+ readonly version: string;
88
+ readonly descriptorSha256: string;
89
+ readonly cacheDir: string;
90
+ readonly io: CacheIo;
91
+ readonly accessToken: string;
92
+ readonly fetch?: typeof fetch;
93
+ readonly allowedHosts?: readonly string[];
94
+ readonly env?: Readonly<Record<string, string | undefined>>;
95
+ readonly now?: number;
96
+ readonly signal?: AbortSignal;
97
+ readonly statfs?: (dir: string) => {
98
+ readonly availableBytes: number;
99
+ } | undefined;
100
+ readonly existingPins?: readonly BitmapShard[];
101
+ /** W2-B6: per-tuple progress for the configured-shard fetch. */
102
+ readonly onProgress?: (event: BitmapShardFetchProgress) => void;
103
+ }
104
+ export interface ConfiguredBitmapShards {
105
+ readonly tuples: readonly BitmapShardTuple[];
106
+ readonly requests: readonly BitmapShardRequest[];
107
+ readonly pins: readonly BitmapShard[];
108
+ /** Local-layout files: `assets/<resourceVariantId>/<iconId>.<format>` -> bytes. */
109
+ readonly files: Readonly<Record<string, Uint8Array>>;
110
+ readonly bitmapShardSetSha256: string;
111
+ }
112
+ /**
113
+ * DEV-G07: resolve the config's bitmap tuples, then obtain each selected shard.
114
+ * Online (an access token is available) fetches one descriptor per tuple from
115
+ * the API and downloads it from the signed R2 host. Offline (no token) loads
116
+ * the already-pinned cache and fails closed when any shard is missing.
117
+ */
118
+ export declare function resolveConfiguredBitmapShards(deps: EnsureBitmapShardsDeps): Promise<ConfiguredBitmapShards>;
119
+ /**
120
+ * Offline path: re-verify every configured tuple's shard against its pinned
121
+ * identity. The pin supplies the archive SHA, manifest SHA and size, so a
122
+ * truncated/poisoned cache can never be accepted, and every unselected tuple
123
+ * is never read.
124
+ */
125
+ export declare function loadPinnedBitmapShardAssets(pins: readonly BitmapShard[], selectedTuples: readonly BitmapShardTuple[], cacheDir: string, io: CacheIo): {
126
+ readonly files: Record<string, Uint8Array>;
127
+ readonly pins: readonly BitmapShard[];
128
+ readonly bitmapShardSetSha256: string;
129
+ };