@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.
Files changed (53) hide show
  1. package/README.md +27 -19
  2. package/dist/cli.js +124 -146
  3. package/dist/config-package/generated/SOURCE.json +3 -3
  4. package/dist/config-package/generated/moeicons-config.schema.json +1 -1
  5. package/dist/core/bootstrap.js +5 -3
  6. package/dist/core/candidate-version-pointer.d.ts +13 -0
  7. package/dist/core/candidate-version-pointer.js +37 -0
  8. package/dist/core/config-package-plan.d.ts +25 -0
  9. package/dist/core/config-package-plan.js +156 -0
  10. package/dist/core/doctor.js +4 -0
  11. package/dist/core/free-download.js +34 -4
  12. package/dist/core/free-small-package-download.d.ts +67 -0
  13. package/dist/core/free-small-package-download.js +344 -0
  14. package/dist/core/generate.js +49 -33
  15. package/dist/core/install.d.ts +8 -2
  16. package/dist/core/install.js +70 -0
  17. package/dist/core/library-update.d.ts +6 -1
  18. package/dist/core/library-update.js +54 -0
  19. package/dist/core/manage.d.ts +4 -0
  20. package/dist/core/manage.js +4 -0
  21. package/dist/core/metadata-sync.js +119 -1
  22. package/dist/core/pro-download.js +19 -2
  23. package/dist/core/pro-install.d.ts +10 -4
  24. package/dist/core/pro-install.js +47 -0
  25. package/dist/core/pro-resources.d.ts +23 -0
  26. package/dist/core/pro-resources.js +51 -0
  27. package/dist/core/pro-selected-download.js +12 -2
  28. package/dist/core/pro-small-package-download.d.ts +61 -0
  29. package/dist/core/pro-small-package-download.js +301 -0
  30. package/dist/core/release-descriptor.js +6 -2
  31. package/dist/core/small-pack/PIN.json +11 -0
  32. package/dist/core/small-pack/candidate-set-v1.schema.json +187 -0
  33. package/dist/core/small-pack/candidate-set.cjs +286 -0
  34. package/dist/core/small-pack/candidate-set.d.cts +12 -0
  35. package/dist/core/small-pack/tar-read.cjs +220 -0
  36. package/dist/core/small-pack/tar-read.d.cts +2 -0
  37. package/dist/core/small-package-install.d.ts +40 -0
  38. package/dist/core/small-package-install.js +252 -0
  39. package/dist/core/small-package-selection.d.ts +27 -0
  40. package/dist/core/small-package-selection.js +114 -0
  41. package/dist/core/update-policy.d.ts +1 -0
  42. package/dist/core/update-policy.js +10 -3
  43. package/dist/mcp/services.js +1 -1
  44. package/dist/project/config.js +1 -1
  45. package/dist/project/install-metadata.d.ts +2 -1
  46. package/dist/project/install-metadata.js +6 -3
  47. package/dist/project/install.d.ts +11 -2
  48. package/dist/project/install.js +104 -12
  49. package/dist/project/package-install-metadata.d.ts +31 -0
  50. package/dist/project/package-install-metadata.js +99 -0
  51. package/dist/project/tar-gz.d.ts +4 -8
  52. package/dist/project/tar-gz.js +6 -10
  53. package/package.json +11 -11
@@ -14,6 +14,8 @@ import { catalog as bundledCatalog, parseCatalog } from "../catalog/catalog.js";
14
14
  import { resolveBitmapTuples, resolveConfiguredBitmapShards } from "./bitmap-shard-resolver.js";
15
15
  import { mergeBitmapShardCacheManifest, readBitmapShardCacheManifest, writeBitmapShardCacheManifest, } from "./bitmap-shard-cache.js";
16
16
  import { allowLocalTestFromEnv } from "./local-test-env.js";
17
+ import { runSmallPackageInstall } from "./small-package-install.js";
18
+ import { fetchProStablePackageSet } from "./pro-small-package-download.js";
17
19
  function toCacheIo(fs_) {
18
20
  return {
19
21
  // Shard cache keys are nested several levels deep; the CacheIo contract
@@ -64,6 +66,51 @@ export async function runProInstallUseCase(context, deps, expected) {
64
66
  if (target !== bootstrap.config.target) {
65
67
  throw new CliError("VALIDATION_ERROR", `config target is "${bootstrap.config.target}" but install target is "${target}"; set target to "${target}" in moeicons.config before installing`);
66
68
  }
69
+ if (expected.allowLocalTest !== true) {
70
+ const accessToken = await runAccessTokenUseCase(context, deps.auth);
71
+ const identity = await fetchProStablePackageSet({
72
+ accessToken,
73
+ ...(context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
74
+ ...(deps.fetch ? { fetchFn: deps.fetch } : {}),
75
+ ...(context.signal ? { signal: context.signal } : {}),
76
+ });
77
+ if (expected.version && expected.version !== identity.fullVersion) {
78
+ throw new CliError("VALIDATION_ERROR", "published Pro package-set changed after version selection; retry the install");
79
+ }
80
+ const installed = await runSmallPackageInstall({
81
+ projectRoot: project.root,
82
+ fullVersion: identity.fullVersion,
83
+ expectedPackageSetSha256: identity.packageSetSha256,
84
+ ...(context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
85
+ config: bootstrap.config,
86
+ plannedConfigDocument: JSON.stringify(document),
87
+ fs: deps.fs,
88
+ cacheDir: resolveCacheDir(context.env),
89
+ now: context.now,
90
+ proAccessToken: accessToken,
91
+ ...(context.signal ? { signal: context.signal } : {}),
92
+ ...(deps.fetch ? { fetchFn: deps.fetch } : {}),
93
+ });
94
+ return {
95
+ projectRoot: project.root,
96
+ artifactVersion: installed.artifactVersion,
97
+ candidateRootSha256: installed.candidateRootSha256,
98
+ packageSetSha256: installed.packageSetSha256,
99
+ catalogSha256: installed.catalogSha256,
100
+ artifactBytes: installed.artifactBytes,
101
+ downloadMode: "packages",
102
+ downloadNotes: installed.fallbacks.map((fallback) => `Theme fallback: ${fallback}`),
103
+ networkBytes: installed.networkBytes + identity.networkBytes,
104
+ selectedFiles: installed.selectedFiles,
105
+ packageCount: installed.packageCount,
106
+ cacheHits: installed.cacheHits,
107
+ metadataCacheHits: installed.metadataCacheHits,
108
+ transferRetryCount: installed.transferRetryCount,
109
+ };
110
+ }
111
+ if (!expected.version || !expected.descriptorSha256) {
112
+ throw new CliError("VALIDATION_ERROR", "local Pro test install requires an exact legacy fixture pin");
113
+ }
67
114
  const downloaded = await downloadProArtifact(context, deps.auth, {
68
115
  version: expected.version,
69
116
  descriptorSha256: expected.descriptorSha256,
@@ -48,6 +48,8 @@ export declare function runProPredownloadUseCase(context: CommandContext, auth:
48
48
  readonly fetch?: typeof fetch;
49
49
  readonly allowedProHosts?: readonly string[];
50
50
  readonly force?: boolean;
51
+ /** Explicitly isolate the retired aggregate archive path to test fixtures. */
52
+ readonly allowLocalTest?: boolean;
51
53
  readonly onProgress?: (event: {
52
54
  readonly downloadedBytes: number;
53
55
  readonly totalBytes?: number;
@@ -65,11 +67,32 @@ export interface ProWarmupDeps {
65
67
  readonly descriptorSha256: string;
66
68
  readonly metadataSha256: string;
67
69
  readonly catalogSha256: string;
70
+ /** Explicit test-fixture opt-in for the retired legacy shard route. */
71
+ readonly allowLocalTest?: boolean;
68
72
  readonly fetch?: typeof fetch;
69
73
  readonly allowedProHosts?: readonly string[];
70
74
  readonly onPlan?: (plan: WarmupPlan) => void;
71
75
  readonly onProgress?: (event: BitmapShardFetchProgress) => void;
72
76
  }
77
+ export interface ProPackageWarmupPlan {
78
+ readonly version: string;
79
+ readonly packageSetSha256: string;
80
+ readonly groups: number;
81
+ readonly packages: number;
82
+ readonly compressedBytes: number;
83
+ }
84
+ export interface ProPackageWarmupResult extends ProPackageWarmupPlan {
85
+ readonly downloaded: number;
86
+ readonly reused: number;
87
+ readonly networkBytes: number;
88
+ readonly metadataNetworkBytes: number;
89
+ }
90
+ /** Warm the explicit default preview tuple (128-WebP) as separate package objects. */
91
+ export declare function runProPreviewPackageWarmupUseCase(context: CommandContext, auth: AuthUseCaseDependencies, deps?: {
92
+ readonly fetch?: typeof fetch;
93
+ readonly apiBaseUrl?: string;
94
+ readonly onPlan?: (plan: ProPackageWarmupPlan) => Promise<void> | void;
95
+ }): Promise<ProPackageWarmupResult>;
73
96
  /**
74
97
  * W2-A: after the Pro code + metadata are cached, warm the default bitmap shard
75
98
  * set (every bitmap style group's 128/webp shard) into the global cache. The
@@ -14,6 +14,8 @@ import { parseCatalog } from "../catalog/catalog.js";
14
14
  import { runAccessTokenUseCase } from "./auth.js";
15
15
  import { PRO_DOWNLOAD_HOSTS } from "./pro-download.js";
16
16
  import { resolveBitmapShardDescriptorEndpoint } from "./bitmap-shard-download.js";
17
+ import { downloadProStablePackages } from "./pro-small-package-download.js";
18
+ import { planSelectedPackages } from "./small-package-selection.js";
17
19
  import { warmDefaultBitmapShards, } from "./bitmap-shard-cache.js";
18
20
  function cacheDir(env) {
19
21
  return env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
@@ -91,6 +93,9 @@ export async function proResourceState(context, auth, deps = {}) {
91
93
  * login or from the home screen, without installing anything into a project.
92
94
  */
93
95
  export async function runProPredownloadUseCase(context, auth, deps = {}) {
96
+ if (deps.allowLocalTest !== true) {
97
+ throw new CliError('VALIDATION_ERROR', 'aggregate Pro archive pre-download is retired; use package-set project installs or preview package warm-up');
98
+ }
94
99
  const versions = await fetchLibraryVersions({ ...(deps.fetch ? { fetch: deps.fetch } : {}), signal: context.signal, env: context.env });
95
100
  if (!versions.pro)
96
101
  throw new CliError("NOT_FOUND", "no pro release is published");
@@ -136,6 +141,49 @@ export async function runProPredownloadUseCase(context, auth, deps = {}) {
136
141
  metadataBytes,
137
142
  };
138
143
  }
144
+ /** Warm the explicit default preview tuple (128-WebP) as separate package objects. */
145
+ export async function runProPreviewPackageWarmupUseCase(context, auth, deps = {}) {
146
+ const accessToken = await runAccessTokenUseCase(context, auth);
147
+ const cache = cacheDir(context.env);
148
+ let plan;
149
+ const report = await downloadProStablePackages({
150
+ accessToken,
151
+ cacheDir: cache,
152
+ ...(deps.apiBaseUrl ? { apiBaseUrl: deps.apiBaseUrl } : context.env.MOEICONS_API_BASE_URL ? { apiBaseUrl: context.env.MOEICONS_API_BASE_URL } : {}),
153
+ ...(deps.fetch ? { fetchFn: deps.fetch } : {}),
154
+ ...(context.signal ? { signal: context.signal } : {}),
155
+ resolveResourceKeys: async (candidate) => {
156
+ const selected = candidate.rows.filter((row) => row.kind === 'bitmap' && row.tier === 'pro'
157
+ && row.variant?.width === 128 && row.variant.height === 128 && row.variant.format === 'webp')
158
+ .sort((a, b) => a.resourceKey.localeCompare(b.resourceKey, 'en'));
159
+ if (selected.length === 0)
160
+ throw new CliError('NOT_FOUND', 'the current Pro package set has no 128-WebP preview packages');
161
+ const selectedRows = planSelectedPackages(candidate, selected.map((row) => row.resourceKey));
162
+ const compressedBytes = selectedRows.reduce((sum, row) => sum + row.compressedBytes, 0);
163
+ if (!Number.isSafeInteger(compressedBytes) || compressedBytes > 4 * 1024 * 1024 * 1024)
164
+ throw new CliError('VALIDATION_ERROR', 'Pro preview warm-up exceeds the 4 GiB package budget');
165
+ plan = {
166
+ version: candidate.root.fullVersion,
167
+ packageSetSha256: candidate.root.packageSetSha256,
168
+ groups: new Set(selected.map((row) => row.styleGroupId)).size,
169
+ packages: selectedRows.length,
170
+ compressedBytes,
171
+ };
172
+ await deps.onPlan?.(plan);
173
+ return selected.map((row) => row.resourceKey);
174
+ },
175
+ consume: () => Promise.resolve(),
176
+ });
177
+ if (!plan)
178
+ throw new CliError('VALIDATION_ERROR', 'Pro preview warm-up did not produce a candidate plan');
179
+ return {
180
+ ...plan,
181
+ downloaded: report.packageCount - report.cacheHits,
182
+ reused: report.cacheHits,
183
+ networkBytes: report.networkBytes,
184
+ metadataNetworkBytes: report.metadataNetworkBytes,
185
+ };
186
+ }
139
187
  function cacheIo() {
140
188
  return {
141
189
  mkdirSync: (path) => mkdirSync(path, { recursive: true }),
@@ -154,6 +202,9 @@ function cacheIo() {
154
202
  * second artifact download is needed.
155
203
  */
156
204
  export async function runProResourceWarmupUseCase(context, auth, deps) {
205
+ if (deps.allowLocalTest !== true) {
206
+ throw new CliError('VALIDATION_ERROR', 'legacy Pro bitmap shard warm-up is retired; use package-set preview package warm-up');
207
+ }
157
208
  const dir = cacheDir(context.env);
158
209
  const metadataPath = metadataCachePath(dir, deps.version, deps.metadataSha256);
159
210
  if (!existsSync(metadataPath)) {
@@ -14,6 +14,12 @@ import { homedir } from "node:os";
14
14
  export async function downloadProSelected(context, auth, descriptor, selection, deps) {
15
15
  const fetchFn = deps.fetch ?? fetch;
16
16
  const endpointInfo = resolveProDescriptorEndpoint(context.env);
17
+ // CI FLOW RULE (S05): this implementation reads the retired tier-wide
18
+ // resource-index / `.bin` bundle. Keep it reachable only from an explicitly
19
+ // declared local-test descriptor served by a loopback endpoint; a descriptor
20
+ // label alone must never reopen the production reader.
21
+ if (!endpointInfo.allowLoopback || descriptor.channel !== "local-test")
22
+ throw new CliError("VALIDATION_ERROR", "legacy Pro resource-bundle downloads require a local-test descriptor and loopback endpoint");
17
23
  const endpoint = new URL(endpointInfo.url.replace(/\/artifact-descriptor$/, "/resource-descriptor"));
18
24
  if (endpoint.origin !== "https://api.moeicons.com" && !endpointInfo.allowLoopback)
19
25
  throw new CliError("VALIDATION_ERROR", "resource descriptor endpoint must be the trusted API or a test loopback");
@@ -150,7 +156,11 @@ export async function downloadProSelected(context, auth, descriptor, selection,
150
156
  catch (error) {
151
157
  if (!expired(error) || attempt === 2)
152
158
  throw error;
153
- const renewed = await fetchProDescriptor(context, auth, { version: descriptor.version, descriptorSha256: descriptor.descriptorSha256 }, deps);
159
+ const renewed = await fetchProDescriptor(context, auth, {
160
+ version: descriptor.version,
161
+ descriptorSha256: descriptor.descriptorSha256,
162
+ ...(descriptor.channel === "local-test" ? { allowLocalTest: true } : {}),
163
+ }, deps);
154
164
  if (renewed.sha256 !== descriptor.sha256 ||
155
165
  !renewed.metadata ||
156
166
  renewed.metadata.sha256 !== originalMetadata.sha256 ||
@@ -161,7 +171,7 @@ export async function downloadProSelected(context, auth, descriptor, selection,
161
171
  }
162
172
  if (!metadataBytes)
163
173
  throw new CliError("NETWORK_ERROR", "metadata URL renewal failed");
164
- const metadata = extractProMetadata(metadataBytes, descriptor.catalogSha256, descriptor.version);
174
+ const metadata = extractProMetadata(metadataBytes, descriptor.catalogSha256, descriptor.version, descriptor.channel === "local-test");
165
175
  const catalog = parseCatalog(JSON.parse(metadata.catalogJson));
166
176
  const strict = validateConfigDocument(selection.document, catalog);
167
177
  if (strict.kind !== "ok")
@@ -0,0 +1,61 @@
1
+ import { type PackageRow } from './small-pack/candidate-set.cjs';
2
+ import { type CandidateIdentity, type SelectedCandidate } from './small-package-selection.js';
3
+ export interface ProPackageSetIdentity extends CandidateIdentity {
4
+ readonly schemaVersion: 1;
5
+ readonly tier: 'pro';
6
+ readonly candidateRootSha256: string;
7
+ readonly candidateRootBytes: number;
8
+ readonly updatedAt: string;
9
+ readonly networkBytes: number;
10
+ }
11
+ export interface ProSmallPackageOptions {
12
+ readonly expected: CandidateIdentity & {
13
+ readonly tier: 'pro';
14
+ };
15
+ readonly resourceKeys?: readonly string[];
16
+ readonly resolveResourceKeys?: (candidate: SelectedCandidate, loadVerifiedPackage: (row: PackageRow) => Promise<Readonly<Record<string, Uint8Array>>>) => Promise<readonly string[]>;
17
+ readonly cacheDir: string;
18
+ readonly consume: (row: PackageRow, files: Readonly<Record<string, Uint8Array>>) => Promise<void>;
19
+ readonly accessToken: string;
20
+ readonly signal?: AbortSignal;
21
+ readonly fetchFn?: typeof fetch;
22
+ readonly apiBaseUrl?: string;
23
+ readonly expectedRootSha256?: string;
24
+ readonly expectedRootBytes?: number;
25
+ }
26
+ export declare function fetchProPackageSetIdentity(options: {
27
+ readonly version: string;
28
+ readonly packageSetSha256?: string;
29
+ readonly accessToken: string;
30
+ readonly apiBaseUrl?: string;
31
+ readonly fetchFn?: typeof fetch;
32
+ readonly signal?: AbortSignal;
33
+ }): Promise<ProPackageSetIdentity>;
34
+ export declare function fetchProStablePackageSet(options: Omit<Parameters<typeof fetchProPackageSetIdentity>[0], 'version' | 'packageSetSha256'>): Promise<ProPackageSetIdentity>;
35
+ export declare function downloadProSelectedPackages(options: ProSmallPackageOptions): Promise<{
36
+ packageCount: number;
37
+ networkBytes: number;
38
+ cacheHits: number;
39
+ metadataNetworkBytes: number;
40
+ metadataCacheHits: number;
41
+ transferRetryCount: number;
42
+ }>;
43
+ export declare function downloadProStablePackages(options: Omit<ProSmallPackageOptions, 'expected' | 'expectedRootSha256' | 'expectedRootBytes'>): Promise<{
44
+ packageCount: number;
45
+ networkBytes: number;
46
+ cacheHits: number;
47
+ metadataNetworkBytes: number;
48
+ metadataCacheHits: number;
49
+ transferRetryCount: number;
50
+ }>;
51
+ export declare function downloadProVersionPackages(options: Omit<ProSmallPackageOptions, 'expected' | 'expectedRootSha256' | 'expectedRootBytes'> & {
52
+ readonly fullVersion: string;
53
+ readonly expectedPackageSetSha256: string;
54
+ }): Promise<{
55
+ packageCount: number;
56
+ networkBytes: number;
57
+ cacheHits: number;
58
+ metadataNetworkBytes: number;
59
+ metadataCacheHits: number;
60
+ transferRetryCount: number;
61
+ }>;
@@ -0,0 +1,301 @@
1
+ import * as fs from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { cacheArtifact } from './cache.js';
4
+ import { downloadArtifact } from '../project/download.js';
5
+ import { parseCandidateSet, sha256 } from './small-pack/candidate-set.cjs';
6
+ import { readSelectedCandidate, consumeSelectedPackages, planSelectedPackages, verifySelectedPackage } from './small-package-selection.js';
7
+ const API_DEFAULT = 'https://api.moeicons.com';
8
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:alpha|beta)(?:\.(?:0|[1-9]\d*))?)?$/;
9
+ const SHA = /^[a-f0-9]{64}$/;
10
+ const COMMIT = /^[a-f0-9]{40}$/;
11
+ function check(ok, message) { if (!ok)
12
+ throw new Error(`Pro package download: ${message}`); }
13
+ function readPrivateCache(file, maximum, exact) {
14
+ let handle;
15
+ try {
16
+ const entry = fs.lstatSync(file);
17
+ check(entry.isFile() && !entry.isSymbolicLink(), 'cached object must be a regular non-link file');
18
+ if (process.platform !== 'win32') {
19
+ check((entry.mode & 0o077) === 0 && (typeof process.getuid !== 'function' || entry.uid === process.getuid()), 'Pro cache permissions or owner are unsafe');
20
+ }
21
+ handle = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
22
+ }
23
+ catch (error) {
24
+ if (error.code === 'ENOENT')
25
+ return undefined;
26
+ throw error;
27
+ }
28
+ try {
29
+ const stat = fs.fstatSync(handle);
30
+ check(stat.isFile() && stat.size > 0 && stat.size <= maximum && (exact === undefined || stat.size === exact), 'cached object size/type invalid');
31
+ if (process.platform !== 'win32')
32
+ check((stat.mode & 0o077) === 0 && (typeof process.getuid !== 'function' || stat.uid === process.getuid()), 'Pro cache permissions or owner changed');
33
+ const bytes = Buffer.alloc(stat.size + 1);
34
+ let count = 0;
35
+ while (count < bytes.length) {
36
+ const read = fs.readSync(handle, bytes, count, bytes.length - count, count);
37
+ if (!read)
38
+ break;
39
+ count += read;
40
+ }
41
+ check(count === stat.size, 'cache changed during bounded read');
42
+ return bytes.subarray(0, count);
43
+ }
44
+ finally {
45
+ fs.closeSync(handle);
46
+ }
47
+ }
48
+ function privateCacheIo() {
49
+ return {
50
+ mkdirSync(path) {
51
+ fs.mkdirSync(path, { recursive: true, mode: 0o700 });
52
+ if (process.platform !== 'win32')
53
+ fs.chmodSync(path, 0o700);
54
+ },
55
+ writeFileSync(path, bytes) { fs.writeFileSync(path, bytes, { mode: 0o600 }); },
56
+ renameSync: fs.renameSync,
57
+ existsSync: fs.existsSync,
58
+ rmSync: fs.rmSync,
59
+ readFileSync: fs.readFileSync,
60
+ readdirSync: fs.readdirSync,
61
+ };
62
+ }
63
+ function resolveApiBase(value) {
64
+ const base = new URL(value ?? API_DEFAULT);
65
+ const loopback = base.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname);
66
+ check((base.protocol === 'https:' || loopback) && !base.username && !base.password && !base.search && !base.hash && base.pathname === '/', 'API base must be HTTPS or loopback HTTP');
67
+ return base;
68
+ }
69
+ function authorizedFetch(fetchFn, apiBase, accessToken) {
70
+ check(typeof accessToken === 'string' && accessToken.length > 0 && accessToken.length <= 8192 && !/[\r\n]/.test(accessToken), 'authenticated Pro access token required');
71
+ return async (input, init) => {
72
+ const address = input instanceof Request ? input.url : input instanceof URL ? input.href : String(input);
73
+ if (new URL(address).origin !== apiBase.origin)
74
+ throw new Error('Pro package download: refusing to send credentials outside the configured API origin');
75
+ const headers = new Headers(input instanceof Request ? input.headers : undefined);
76
+ new Headers(init?.headers).forEach((value, key) => headers.set(key, value));
77
+ headers.set('Authorization', `Bearer ${accessToken}`);
78
+ return await fetchFn(input, { ...init, headers });
79
+ };
80
+ }
81
+ function identityUrl(apiBase, version, packageSetSha256) {
82
+ const url = new URL('/v1/icon-library/pro/package-set', apiBase);
83
+ url.searchParams.set('version', version);
84
+ if (packageSetSha256)
85
+ url.searchParams.set('packageSetSha256', packageSetSha256);
86
+ return url.href;
87
+ }
88
+ function assetUrl(apiBase, identity, path) {
89
+ const url = new URL(`/v1/icon-library/pro/package-set/${path}`, apiBase);
90
+ url.searchParams.set('version', identity.fullVersion);
91
+ url.searchParams.set('packageSetSha256', identity.packageSetSha256);
92
+ return url.href;
93
+ }
94
+ export async function fetchProPackageSetIdentity(options) {
95
+ check(options.version === 'stable' || VERSION.test(options.version), 'requested Pro version is invalid');
96
+ if (options.version === 'stable')
97
+ check(options.packageSetSha256 === undefined, 'stable lookup cannot include a package-set digest');
98
+ else
99
+ check(SHA.test(options.packageSetSha256 ?? ''), 'exact Pro version lookup requires a package-set digest');
100
+ const apiBase = resolveApiBase(options.apiBaseUrl);
101
+ const fetchFn = authorizedFetch(options.fetchFn ?? globalThis.fetch, apiBase, options.accessToken);
102
+ const result = await downloadArtifact(identityUrl(apiBase, options.version, options.packageSetSha256), {
103
+ maxBytes: 16 * 1024,
104
+ timeoutMs: 120000,
105
+ maxRedirects: 0,
106
+ allowedHosts: [apiBase.host],
107
+ ...(apiBase.protocol === 'http:' ? { allowHttpLoopback: true } : {}),
108
+ userAgent: 'moeicons/small-package',
109
+ }, { fetchFn, ...(options.signal ? { signal: options.signal } : {}) });
110
+ if (!result.ok)
111
+ throw new Error(`Pro package-set identity: ${result.code}: ${result.message}`);
112
+ let raw;
113
+ try {
114
+ raw = JSON.parse(Buffer.from(result.bytes).toString('utf8'));
115
+ }
116
+ catch {
117
+ throw new Error('Pro package-set identity: invalid JSON');
118
+ }
119
+ check(raw !== null && typeof raw === 'object' && !Array.isArray(raw), 'identity object required');
120
+ const value = raw;
121
+ const fields = ['schemaVersion', 'tier', 'fullVersion', 'sourceCommit', 'generatorCommit', 'packageSetSha256', 'candidateRootSha256', 'candidateRootBytes', 'updatedAt'];
122
+ check(Object.keys(value).sort().join(',') === [...fields].sort().join(','), 'identity fields differ from the Pro package-set contract');
123
+ check(value.schemaVersion === 1 && value.tier === 'pro' && typeof value.fullVersion === 'string' && VERSION.test(value.fullVersion), 'Pro version identity invalid');
124
+ check(options.version === 'stable' || value.fullVersion === options.version, 'identity version differs from request');
125
+ check(typeof value.sourceCommit === 'string' && COMMIT.test(value.sourceCommit), 'Source commit invalid');
126
+ check(typeof value.generatorCommit === 'string' && COMMIT.test(value.generatorCommit), 'Generator commit invalid');
127
+ check(typeof value.packageSetSha256 === 'string' && SHA.test(value.packageSetSha256), 'package-set digest invalid');
128
+ check(options.packageSetSha256 === undefined || value.packageSetSha256 === options.packageSetSha256, 'identity package-set digest differs from request');
129
+ check(typeof value.candidateRootSha256 === 'string' && SHA.test(value.candidateRootSha256), 'candidate root digest invalid');
130
+ check(Number.isSafeInteger(value.candidateRootBytes) && Number(value.candidateRootBytes) > 0 && Number(value.candidateRootBytes) <= 1024 ** 2, 'candidate root size invalid');
131
+ check(typeof value.updatedAt === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value.updatedAt) && !Number.isNaN(Date.parse(value.updatedAt)), 'identity timestamp invalid');
132
+ return { ...value, networkBytes: result.bytes.length };
133
+ }
134
+ export async function fetchProStablePackageSet(options) {
135
+ return fetchProPackageSetIdentity({ ...options, version: 'stable' });
136
+ }
137
+ export async function downloadProSelectedPackages(options) {
138
+ options.signal?.throwIfAborted();
139
+ check((options.resourceKeys !== undefined) !== (options.resolveResourceKeys !== undefined), 'provide exactly one explicit selection or candidate-based planner');
140
+ if (options.resourceKeys)
141
+ check(options.resourceKeys.length > 0 && new Set(options.resourceKeys).size === options.resourceKeys.length, 'explicit unique resource selection required');
142
+ const expected = options.expected;
143
+ check(expected.tier === 'pro' && SHA.test(expected.packageSetSha256) && COMMIT.test(expected.sourceCommit) && COMMIT.test(expected.generatorCommit) && VERSION.test(expected.fullVersion), 'exact Pro candidate pin required');
144
+ if (options.expectedRootSha256 !== undefined)
145
+ check(SHA.test(options.expectedRootSha256), 'candidate root pin must be lowercase SHA-256');
146
+ if (options.expectedRootBytes !== undefined)
147
+ check(Number.isSafeInteger(options.expectedRootBytes) && options.expectedRootBytes > 0 && options.expectedRootBytes <= 1024 ** 2, 'candidate root byte pin is invalid');
148
+ const apiBase = resolveApiBase(options.apiBaseUrl);
149
+ const fetchFn = authorizedFetch(options.fetchFn ?? globalThis.fetch, apiBase, options.accessToken);
150
+ let transferRetryCount = 0;
151
+ const fetchBytes = async (address, maximum) => {
152
+ for (let attempt = 0; attempt < 3; attempt++) {
153
+ options.signal?.throwIfAborted();
154
+ const result = await downloadArtifact(address, {
155
+ maxBytes: maximum,
156
+ timeoutMs: 120000,
157
+ maxRedirects: 0,
158
+ allowedHosts: [apiBase.host],
159
+ ...(apiBase.protocol === 'http:' ? { allowHttpLoopback: true } : {}),
160
+ userAgent: 'moeicons/small-package',
161
+ }, { fetchFn, ...(options.signal ? { signal: options.signal } : {}) });
162
+ if (result.ok)
163
+ return result.bytes;
164
+ const transient = result.code === 'NETWORK_ERROR' || result.code === 'HTTP_ERROR' && /\b(?:429|500|502|503|504)\b/.test(result.message);
165
+ if (!transient || attempt === 2)
166
+ throw new Error(`Pro package download: ${result.code}: ${result.message}`);
167
+ transferRetryCount++;
168
+ await new Promise((resolve, reject) => {
169
+ const aborted = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', aborted); reject(new Error('Pro package download cancelled')); };
170
+ const timer = setTimeout(() => { options.signal?.removeEventListener('abort', aborted); resolve(); }, 250 * (attempt + 1));
171
+ options.signal?.addEventListener('abort', aborted, { once: true });
172
+ if (options.signal?.aborted)
173
+ aborted();
174
+ });
175
+ }
176
+ throw new Error('Pro package download retry budget exhausted');
177
+ };
178
+ const cache = join(options.cacheDir, 'small-pack-v1', 'pro');
179
+ const io = privateCacheIo();
180
+ const persist = (name, bytes, digest) => cacheArtifact(io, join(cache, name), bytes, digest);
181
+ const rootName = `candidate-set-${expected.packageSetSha256}.json`;
182
+ let metadataNetworkBytes = 0;
183
+ let metadataCacheHits = 0;
184
+ let rootBytes = readPrivateCache(join(cache, rootName), 1024 ** 2);
185
+ const rootCached = rootBytes !== undefined;
186
+ if (rootBytes)
187
+ metadataCacheHits++;
188
+ else {
189
+ rootBytes = await fetchBytes(assetUrl(apiBase, expected, 'root'), 1024 ** 2);
190
+ metadataNetworkBytes += rootBytes.length;
191
+ }
192
+ if (options.expectedRootSha256)
193
+ check(sha256(rootBytes) === options.expectedRootSha256, 'candidate root differs from authenticated identity');
194
+ if (options.expectedRootBytes !== undefined)
195
+ check(rootBytes.length === options.expectedRootBytes, 'candidate root size differs from authenticated identity');
196
+ const root = parseCandidateSet(rootBytes);
197
+ for (const key of ['fullVersion', 'tier', 'sourceCommit', 'generatorCommit', 'packageSetSha256'])
198
+ check(root[key] === expected[key], 'root differs from authenticated Pro identity');
199
+ const pending = [];
200
+ const candidate = await readSelectedCandidate(rootBytes, expected, async (ref) => {
201
+ const name = `page-${ref.sha256}.json`;
202
+ let bytes = readPrivateCache(join(cache, name), ref.sizeBytes, ref.sizeBytes);
203
+ if (bytes)
204
+ metadataCacheHits++;
205
+ else {
206
+ const index = root.packagePages.findIndex(item => item.filename === ref.filename && item.sha256 === ref.sha256) + 1;
207
+ check(index > 0, 'page reference is outside the verified root');
208
+ bytes = await fetchBytes(assetUrl(apiBase, expected, `pages/${index}`), ref.sizeBytes);
209
+ metadataNetworkBytes += bytes.length;
210
+ pending.push({ name, bytes, digest: ref.sha256 });
211
+ }
212
+ return bytes;
213
+ });
214
+ if (!rootCached)
215
+ persist(rootName, rootBytes, sha256(rootBytes));
216
+ for (const page of pending)
217
+ persist(page.name, page.bytes, page.digest);
218
+ let bootstrapNetworkBytes = 0;
219
+ const prefetched = new Map();
220
+ const loadVerifiedPackage = async (row) => {
221
+ const trusted = candidate.rows.find(item => item.resourceKey === row.resourceKey);
222
+ check(trusted && trusted.packageSha256 === row.packageSha256 && trusted.compressedBytes === row.compressedBytes, 'bootstrap package is outside the exact candidate rows');
223
+ const path = join(cache, `${row.packageSha256}.tgz`);
224
+ const cached = readPrivateCache(path, row.compressedBytes, row.compressedBytes);
225
+ const bytes = cached ?? await fetchBytes(assetUrl(apiBase, expected, `packages/${row.packageSha256}.tgz`), row.compressedBytes);
226
+ const files = verifySelectedPackage(trusted, bytes);
227
+ if (!cached) {
228
+ bootstrapNetworkBytes += bytes.length;
229
+ persist(`${row.packageSha256}.tgz`, bytes, row.packageSha256);
230
+ }
231
+ prefetched.set(row.packageSha256, { bytes, network: !cached });
232
+ return files;
233
+ };
234
+ const resourceKeys = options.resolveResourceKeys
235
+ ? await options.resolveResourceKeys(candidate, loadVerifiedPackage)
236
+ : options.resourceKeys;
237
+ check(resourceKeys && resourceKeys.length > 0 && new Set(resourceKeys).size === resourceKeys.length, 'candidate planner must return explicit unique resource keys');
238
+ const selectedRows = planSelectedPackages(candidate, resourceKeys);
239
+ const prefetchedNetworkDigests = new Set([...prefetched].filter(([, value]) => value.network).map(([digest]) => digest));
240
+ const report = await consumeSelectedPackages(candidate, resourceKeys, {
241
+ readCache: row => Promise.resolve(prefetched.get(row.packageSha256)?.bytes ?? readPrivateCache(join(cache, `${row.packageSha256}.tgz`), row.compressedBytes, row.compressedBytes)),
242
+ writeCache: (row, bytes) => { persist(`${row.packageSha256}.tgz`, bytes, row.packageSha256); return Promise.resolve(); },
243
+ download: row => fetchBytes(assetUrl(apiBase, expected, `packages/${row.packageSha256}.tgz`), row.compressedBytes),
244
+ consume: options.consume,
245
+ ...(options.signal ? { signal: options.signal } : {}),
246
+ });
247
+ const prefetchedSelectedNetworkCount = selectedRows.filter(row => prefetchedNetworkDigests.has(row.packageSha256)).length;
248
+ return {
249
+ ...report,
250
+ networkBytes: report.networkBytes + bootstrapNetworkBytes,
251
+ cacheHits: report.cacheHits - prefetchedSelectedNetworkCount,
252
+ metadataNetworkBytes,
253
+ metadataCacheHits,
254
+ transferRetryCount,
255
+ };
256
+ }
257
+ export async function downloadProStablePackages(options) {
258
+ const identity = await fetchProStablePackageSet({
259
+ accessToken: options.accessToken,
260
+ ...(options.apiBaseUrl ? { apiBaseUrl: options.apiBaseUrl } : {}),
261
+ ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}),
262
+ ...(options.signal ? { signal: options.signal } : {}),
263
+ });
264
+ const { accessToken, apiBaseUrl, fetchFn, signal, ...rest } = options;
265
+ const report = await downloadProSelectedPackages({
266
+ ...rest,
267
+ accessToken,
268
+ ...(apiBaseUrl ? { apiBaseUrl } : {}),
269
+ ...(fetchFn ? { fetchFn } : {}),
270
+ ...(signal ? { signal } : {}),
271
+ expected: identity,
272
+ expectedRootSha256: identity.candidateRootSha256,
273
+ expectedRootBytes: identity.candidateRootBytes,
274
+ });
275
+ return { ...report, metadataNetworkBytes: report.metadataNetworkBytes + identity.networkBytes };
276
+ }
277
+ export async function downloadProVersionPackages(options) {
278
+ check(VERSION.test(options.fullVersion) && SHA.test(options.expectedPackageSetSha256), 'exact Pro version and package-set pin required');
279
+ const identity = await fetchProPackageSetIdentity({
280
+ version: options.fullVersion,
281
+ packageSetSha256: options.expectedPackageSetSha256,
282
+ accessToken: options.accessToken,
283
+ ...(options.apiBaseUrl ? { apiBaseUrl: options.apiBaseUrl } : {}),
284
+ ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}),
285
+ ...(options.signal ? { signal: options.signal } : {}),
286
+ });
287
+ const report = await downloadProSelectedPackages({
288
+ cacheDir: options.cacheDir,
289
+ ...(options.resourceKeys ? { resourceKeys: options.resourceKeys } : {}),
290
+ ...(options.resolveResourceKeys ? { resolveResourceKeys: options.resolveResourceKeys } : {}),
291
+ consume: options.consume,
292
+ accessToken: options.accessToken,
293
+ ...(options.apiBaseUrl ? { apiBaseUrl: options.apiBaseUrl } : {}),
294
+ ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}),
295
+ ...(options.signal ? { signal: options.signal } : {}),
296
+ expected: identity,
297
+ expectedRootSha256: identity.candidateRootSha256,
298
+ expectedRootBytes: identity.candidateRootBytes,
299
+ });
300
+ return { ...report, metadataNetworkBytes: report.metadataNetworkBytes + identity.networkBytes };
301
+ }
@@ -213,8 +213,12 @@ export const FREE_DOWNLOAD_HOSTS = [
213
213
  "release-assets.githubusercontent.com",
214
214
  ];
215
215
  export function githubReleaseAssetUrl(tag, filename) {
216
- if (!tag.startsWith("v"))
217
- throw new Error("release tag must be v<fullVersion>");
216
+ // Legacy Free releases use v<fullVersion>; the bounded package publisher
217
+ // uses immutable content-addressed/version-pointer tags as well.
218
+ const version = String.raw `(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:alpha|beta)(?:\.(?:0|[1-9]\d*))?)?`;
219
+ const validTag = new RegExp(`^(?:v${version}|candidate-set-v1-free-[a-f0-9]{64}|free-package-v1-[a-f0-9]{64}|candidate-version-v1-free-${version})$`);
220
+ if (!validTag.test(tag))
221
+ throw new Error("release tag is not a supported Moeicons release tag");
218
222
  return `https://github.com/${PUBLIC_FREE_REPO.owner}/${PUBLIC_FREE_REPO.name}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(filename)}`;
219
223
  }
220
224
  export function cacheKey(fullVersion, sha256) {
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "sourceCommit": "24d7e9ea5d4ac7c338ee08995ebf09b485950092",
4
+ "files": {
5
+ "candidate-set.cjs": "eeb3882a77e2ad29f73a2d741f140750715e9f4a5700ab338b5716013b862732",
6
+ "tar-read.cjs": "053cd19f14d2a779b9e964e497129aac6a0226d3766797b1f281e230ab41ad49"
7
+ },
8
+ "contracts": {
9
+ "candidate-set-v1.schema.json": "8e0aac41672fd9929e89f669487aaa6c100ea9460eaeb1935542e8393c6139fc"
10
+ }
11
+ }