import { Buffer } from 'node:buffer'; import { createHash, randomUUID } from 'node:crypto'; import { lstat, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises'; import path from 'node:path'; import AdmZip from 'adm-zip'; import { getDataDir } from '../utils/paths'; import { subscribeWorksSquareSession } from '../services/works-square-session'; import { loadCodingPluginDefinition, } from './manifest'; import type { CodingPluginDefinition } from '../../shared/coding-plugins'; import { buildPluginReleaseDescriptor, compareSemVer, isMakeLoreVersionCompatible, isValidSemVer, type PluginReleaseDescriptor, } from './release-descriptor'; import { createPluginSignatureVerifier, type PluginSignatureFailureCode, type PluginSignatureVerifier, } from './signature-verifier'; import { AccountPluginCache, type AccountBinding, type MarketplaceLibrarySnapshot, type MarketplaceResolveItem, type MarketplaceResolveSnapshot, } from './account-plugin-cache'; import type { DownloadGrant, DownloadRequest, InstalledReleaseInput, MarketplaceClient, ResolveRequest, } from './marketplace-client'; import type { PluginSigningKeyStore } from './trusted-keys'; const INDEX_SCHEMA_VERSION = 1; const CURRENT_SELECTION_SCHEMA_VERSION = 1; const DEFAULT_MAX_ARCHIVE_BYTES = 16 * 1024 * 1024; const DEFAULT_MAX_EXTRACTED_BYTES = 32 * 1024 * 1024; const DEFAULT_MAX_FILES = 256; const DEFAULT_MAX_FILE_BYTES = 4 * 1024 * 1024; const PLUGIN_MANIFEST_MAX_BYTES = 256 * 1024; const PLUGIN_SKILL_MAX_BYTES = 256 * 1024; const ORPHAN_ARCHIVE_FILE = '.makelore-release.zip'; const MAX_PLUGIN_ID = 128; const MAX_RELEASE_ID = 128; const MAX_VERSION = 128; const MAX_CONTRACT_VERSION = 2 ** 31 - 1; const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u; const RELEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u; const SHA256_PATTERN = /^[a-f0-9]{64}$/u; const INDEX_ROOT_KEYS = new Set(['schema_version', 'releases']); const INDEX_RELEASE_KEYS = new Set([ 'plugin_id', 'release_id', 'version', 'package_schema_version', 'contract_version', 'runtime_kind', 'sha256', 'size_bytes', 'installed_at', 'channel', 'min_makelore_version', 'max_makelore_version', ]); const CURRENT_SELECTION_ROOT_KEYS = new Set(['schema_version', 'current']); const PACKAGE_MANIFEST_PATHS = new Set(['plugin.json', 'com.makelore/capability.json']); const PACKAGE_TEXT_EXTENSIONS = new Set([ '.css', '.html', '.json', '.md', '.markdown', '.mustache', '.hbs', '.ini', '.jinja', '.jinja2', '.template', '.txt', '.toml', '.tmpl', '.xml', '.yaml', '.yml', ]); const PACKAGE_IMAGE_EXTENSIONS = new Set(['.jpeg', '.jpg', '.png', '.svg', '.webp']); type UnknownRecord = Record; export type { DownloadGrant, MarketplaceResolveSnapshot } from './marketplace-client'; export type ResolveSnapshot = MarketplaceResolveSnapshot; export interface InstalledReleaseRecord { readonly pluginId: string; readonly releaseId: string; readonly version: string; readonly packageSchemaVersion: number; readonly contractVersion: number; readonly runtimeKind: 'skill_only' | 'platform_hosted'; readonly sha256: string; readonly sizeBytes: number; readonly installedAt: string; /** Channel is the latest explicit selection intent; verified client range is immutable. */ readonly channel?: 'stable' | 'beta'; readonly minMakeloreVersion?: string; readonly maxMakeloreVersion?: string | null; } export interface InstalledRelease extends InstalledReleaseRecord { readonly packageRoot: string; readonly definition: CodingPluginDefinition; readonly unavailableReason?: 'plugin_incompatible_client'; } export type InstallationStatus = 'installed' | 'kept' | 'removed' | 'unavailable'; export interface InstallationSnapshot { readonly status: InstallationStatus; readonly pluginId: string; readonly releaseId?: string; readonly version?: string; /** The immutable channel selected for this device package. */ readonly channel?: 'stable' | 'beta'; readonly packageRoot?: string; readonly definition?: CodingPluginDefinition; readonly reason?: string; } export interface ResolveInstallInput { readonly pluginId: string; readonly makeloreVersion: string; readonly channel?: 'stable' | 'beta'; /** Beta packages are never selected by background/stable sync. */ readonly explicitBeta?: boolean; readonly resolveRequestId?: string; readonly resolveRequestDigest?: string; readonly installed?: InstalledReleaseInput | null; } export interface MarketplacePackageClientPort { resolve(input: ResolveRequest): Promise; issueDownload(input: DownloadRequest): Promise; downloadContent(grant: DownloadGrant): Promise; readLibrary?(): Promise; getCurrentAccountBinding?(): AccountBinding | null; } export interface PluginPackageStoreOptions { /** A test-owned root may be supplied; production derives it from appData. */ readonly rootDir?: string; readonly marketplace: MarketplacePackageClientPort | MarketplaceClient; readonly accountCache?: AccountPluginCache; readonly getAccountBinding?: () => AccountBinding | null; readonly subscribeSession?: (listener: () => void) => () => void; readonly clientVersion?: string; readonly keyStore?: PluginSigningKeyStore | ReadonlyMap; readonly signatureVerifier?: PluginSignatureVerifier; readonly now?: () => number; readonly maxArchiveBytes?: number; readonly maxExtractedBytes?: number; readonly maxFiles?: number; readonly maxFileBytes?: number; /** Replaced only by focused interruption tests; production is atomic. */ readonly writeIndex?: (filePath: string, bytes: Uint8Array) => Promise; readonly activeWorkerReleaseIds?: () => readonly string[]; } export type PluginPackageStoreErrorCode = | 'plugin_store_index_invalid' | 'plugin_beta_selection_required' | 'plugin_release_unavailable' | 'plugin_account_changed' | 'plugin_artifact_invalid' | 'plugin_signature_invalid' | 'plugin_incompatible_client' | 'plugin_manifest_invalid' | 'plugin_runtime_not_supported' | 'plugin_release_conflict' | 'plugin_release_not_ready' | 'plugin_release_yanked' | 'plugin_runtime_suspended' | 'plugin_library_required' | 'plugin_backend_unavailable' | 'plugin_install_failed'; export class PluginPackageStoreError extends Error { constructor( readonly code: PluginPackageStoreErrorCode, message: string = code, ) { super(message); this.name = 'PluginPackageStoreError'; } } interface IndexDocument { readonly schema_version: typeof INDEX_SCHEMA_VERSION; readonly releases: readonly InstalledReleaseRecord[]; } interface CurrentSelectionDocument { readonly schema_version: typeof CURRENT_SELECTION_SCHEMA_VERSION; readonly current: Readonly>; } function isRecord(value: unknown): value is UnknownRecord { return value !== null && typeof value === 'object' && !Array.isArray(value); } function fail(code: PluginPackageStoreErrorCode, message: string = code): never { throw new PluginPackageStoreError(code, message); } function assertExactKeys(value: UnknownRecord, allowed: ReadonlySet, context: string): void { for (const key of Object.keys(value)) { if (!allowed.has(key)) fail('plugin_store_index_invalid', `unknown ${context} field: ${key.slice(0, 128)}`); } } function boundedText( value: unknown, field: string, maximum: number, code: PluginPackageStoreErrorCode = 'plugin_store_index_invalid', ): string { if (typeof value !== 'string' || value.length === 0 || value.length > maximum) { fail(code, `invalid ${field}`); } return value; } function positiveInteger(value: unknown, field: string, maximum = Number.MAX_SAFE_INTEGER): number { if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > maximum) { fail('plugin_store_index_invalid', `invalid ${field}`); } return value as number; } function validPluginId( value: unknown, field = 'pluginId', code: PluginPackageStoreErrorCode = 'plugin_release_unavailable', ): string { const result = boundedText(value, field, MAX_PLUGIN_ID, code); if (!PLUGIN_ID_PATTERN.test(result)) fail(code, `invalid ${field}`); return result; } function validReleaseId( value: unknown, field = 'releaseId', code: PluginPackageStoreErrorCode = 'plugin_release_unavailable', ): string { const result = boundedText(value, field, MAX_RELEASE_ID, code); if (!RELEASE_ID_PATTERN.test(result)) fail(code, `invalid ${field}`); return result; } function validSha( value: unknown, field: string, code: PluginPackageStoreErrorCode = 'plugin_artifact_invalid', ): string { const result = boundedText(value, field, 64, code); if (!SHA256_PATTERN.test(result)) fail(code, `invalid ${field}`); return result; } function validVersion(value: unknown, field: string): string { const result = boundedText(value, field, MAX_VERSION); if (!isValidSemVer(result)) fail('plugin_store_index_invalid', `invalid ${field}`); return result; } function assertPositiveOption(value: number | undefined, fallback: number, field: string): number { const result = value ?? fallback; if (!Number.isSafeInteger(result) || result < 1) throw new RangeError(`${field} must be a positive safe integer`); return result; } function clone(value: T): T { return structuredClone(value); } function digest(bytes: Uint8Array): string { return createHash('sha256').update(bytes).digest('hex'); } function sameBinding(left: AccountBinding | null, right: AccountBinding): boolean { return left !== null && left.accountKey === right.accountKey && left.epoch === right.epoch; } function toBuffer(bytes: Uint8Array): Buffer { return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength); } function isNotFound(error: unknown): boolean { return isRecord(error) && error.code === 'ENOENT'; } function isDirectoryPath(value: string): Promise { return stat(value).then((entry) => entry.isDirectory()).catch(() => false); } function parseIndexDocument(value: unknown): IndexDocument { const root = isRecord(value) ? value : fail('plugin_store_index_invalid', 'index root must be an object'); assertExactKeys(root, INDEX_ROOT_KEYS, 'index'); if (root.schema_version !== INDEX_SCHEMA_VERSION) fail('plugin_store_index_invalid', 'unsupported index schema'); if (!Array.isArray(root.releases) || root.releases.length > DEFAULT_MAX_FILES) { fail('plugin_store_index_invalid', 'invalid index releases'); } const seen = new Set(); const releases = root.releases.map((value, index) => { const record = isRecord(value) ? value : fail('plugin_store_index_invalid', `invalid release ${index}`); assertExactKeys(record, INDEX_RELEASE_KEYS, `release ${index}`); if (record.package_schema_version !== 2) fail('plugin_store_index_invalid', `invalid release ${index} schema`); const pluginId = validPluginId(record.plugin_id, `release ${index}.plugin_id`, 'plugin_store_index_invalid'); const releaseId = validReleaseId(record.release_id, `release ${index}.release_id`, 'plugin_store_index_invalid'); const key = `${pluginId}\u0000${releaseId}`; if (seen.has(key)) fail('plugin_store_index_invalid', `duplicate release ${releaseId}`); seen.add(key); const runtimeKind = record.runtime_kind; if (runtimeKind !== 'skill_only' && runtimeKind !== 'platform_hosted') { fail('plugin_store_index_invalid', `invalid release ${index}.runtime_kind`); } const installedAt = boundedText(record.installed_at, `release ${index}.installed_at`, 80); if (!Number.isFinite(Date.parse(installedAt))) fail('plugin_store_index_invalid', `invalid release ${index}.installed_at`); const minMakeloreVersion = record.min_makelore_version === undefined ? undefined : validVersion(record.min_makelore_version, `release ${index}.min_makelore_version`); const maxMakeloreVersion = record.max_makelore_version === undefined || record.max_makelore_version === null ? record.max_makelore_version as string | null | undefined : validVersion(record.max_makelore_version, `release ${index}.max_makelore_version`); if (minMakeloreVersion && maxMakeloreVersion && compareSemVer(maxMakeloreVersion, minMakeloreVersion) < 0) { fail('plugin_store_index_invalid', `invalid release ${index}.max_makelore_version`); } const channel = record.channel === undefined ? undefined : record.channel; if (channel !== undefined && channel !== 'stable' && channel !== 'beta') { fail('plugin_store_index_invalid', `invalid release ${index}.channel`); } return Object.freeze({ pluginId, releaseId, version: validVersion(record.version, `release ${index}.version`), packageSchemaVersion: 2 as const, contractVersion: positiveInteger(record.contract_version, `release ${index}.contract_version`, MAX_CONTRACT_VERSION), runtimeKind, sha256: validSha(record.sha256, `release ${index}.sha256`, 'plugin_store_index_invalid'), sizeBytes: positiveInteger(record.size_bytes, `release ${index}.size_bytes`, DEFAULT_MAX_ARCHIVE_BYTES), installedAt, ...(channel === undefined ? {} : { channel }), ...(minMakeloreVersion === undefined ? {} : { minMakeloreVersion }), ...(maxMakeloreVersion === undefined ? {} : { maxMakeloreVersion }), }); }); return Object.freeze({ schema_version: INDEX_SCHEMA_VERSION, releases: Object.freeze(releases) }); } function serializeIndex(document: IndexDocument): Uint8Array { return Buffer.from(`${JSON.stringify({ schema_version: document.schema_version, releases: document.releases.map((record) => ({ plugin_id: record.pluginId, release_id: record.releaseId, version: record.version, package_schema_version: record.packageSchemaVersion, contract_version: record.contractVersion, runtime_kind: record.runtimeKind, sha256: record.sha256, size_bytes: record.sizeBytes, installed_at: record.installedAt, ...(record.channel === undefined ? {} : { channel: record.channel }), ...(record.minMakeloreVersion === undefined ? {} : { min_makelore_version: record.minMakeloreVersion }), ...(record.maxMakeloreVersion === undefined ? {} : { max_makelore_version: record.maxMakeloreVersion }), })), })}\n`, 'utf8'); } function parseCurrentSelectionDocument(value: unknown): CurrentSelectionDocument { const root = isRecord(value) ? value : fail('plugin_store_index_invalid', 'current selection root must be an object'); assertExactKeys(root, CURRENT_SELECTION_ROOT_KEYS, 'current selection'); if (root.schema_version !== CURRENT_SELECTION_SCHEMA_VERSION) { fail('plugin_store_index_invalid', 'unsupported current selection schema'); } if (!isRecord(root.current) || Object.keys(root.current).length > DEFAULT_MAX_FILES) { fail('plugin_store_index_invalid', 'invalid current selection'); } const current: Record = {}; for (const [pluginId, releaseId] of Object.entries(root.current)) { const validId = validPluginId(pluginId, 'current plugin_id', 'plugin_store_index_invalid'); current[validId] = validReleaseId(releaseId, `current ${validId}.release_id`, 'plugin_store_index_invalid'); } return Object.freeze({ schema_version: CURRENT_SELECTION_SCHEMA_VERSION, current: Object.freeze(current), }); } function serializeCurrentSelection(document: CurrentSelectionDocument): Uint8Array { return Buffer.from(`${JSON.stringify({ schema_version: document.schema_version, current: document.current, })}\n`, 'utf8'); } async function parseJsonFile(filePath: string): Promise { const bytes = await readFile(filePath); let source: string; try { source = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { fail('plugin_store_index_invalid', 'index is not valid UTF-8'); } try { return JSON.parse(source) as unknown; } catch { fail('plugin_store_index_invalid', 'index is not valid JSON'); } } async function atomicWriteIndex(filePath: string, bytes: Uint8Array): Promise { await mkdir(path.dirname(filePath), { recursive: true }); const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.tmp`); try { await writeFile(temporaryPath, toBuffer(bytes), { flag: 'wx' }); await rename(temporaryPath, filePath); } finally { await rm(temporaryPath, { force: true }).catch(() => undefined); } } function safeArchiveEntryName(entryName: string, isDirectory: boolean): string[] { if (entryName.length === 0 || entryName.includes('\\') || entryName.startsWith('/') || /^[A-Za-z]:/u.test(entryName) || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(entryName)) { fail('plugin_artifact_invalid', 'archive contains a non-canonical path'); } const segments = entryName.split('/'); if (segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) { fail('plugin_artifact_invalid', 'archive contains a path traversal'); } if (isDirectory && !entryName.endsWith('/')) { fail('plugin_artifact_invalid', 'archive directory path is not canonical'); } if (!isDirectory && entryName.endsWith('/')) { fail('plugin_artifact_invalid', 'archive file path is not canonical'); } return isDirectory ? segments.slice(0, -1) : segments; } function entryIsSymlink(entry: AdmZip.IZipEntry): boolean { const mode = (entry.attr >>> 16) & 0xffff; return (mode & 0xf000) === 0xa000; } function assertUtf8(content: Uint8Array, filePath: string): void { try { new TextDecoder('utf-8', { fatal: true }).decode(content); } catch { fail('plugin_artifact_invalid', `${filePath} must be UTF-8`); } } function validateArchiveFile(filePath: string, content: Uint8Array): void { if (PACKAGE_MANIFEST_PATHS.has(filePath)) { if (content.byteLength > PLUGIN_MANIFEST_MAX_BYTES) { fail('plugin_artifact_invalid', `${filePath} exceeds its byte limit`); } assertUtf8(content, filePath); return; } if (!filePath.startsWith('skills/')) { fail('plugin_artifact_invalid', 'package files must be under a declared Skill'); } const extension = path.posix.extname(filePath).toLowerCase(); if (!PACKAGE_TEXT_EXTENSIONS.has(extension) && !PACKAGE_IMAGE_EXTENSIONS.has(extension)) { fail('plugin_artifact_invalid', 'archive asset extension is not supported'); } if (filePath.endsWith('/SKILL.md') && content.byteLength > PLUGIN_SKILL_MAX_BYTES) { fail('plugin_artifact_invalid', 'Skill exceeds its byte limit'); } if (PACKAGE_TEXT_EXTENSIONS.has(extension)) assertUtf8(content, filePath); } function validateArchiveDefinition( files: readonly string[], definition: CodingPluginDefinition, ): void { for (const manifestPath of PACKAGE_MANIFEST_PATHS) { if (!files.includes(manifestPath)) fail('plugin_artifact_invalid', `archive is missing ${manifestPath}`); } const skillIds = new Set(definition.skills.map(({ id }) => id)); for (const filePath of files) { if (PACKAGE_MANIFEST_PATHS.has(filePath)) continue; const [, skillId] = filePath.split('/'); if (!skillId || !skillIds.has(skillId)) { fail('plugin_artifact_invalid', 'archive file is outside a declared Skill'); } } for (const { entryPath } of definition.skills) { if (!files.includes(entryPath)) fail('plugin_artifact_invalid', 'archive is missing a declared Skill entry'); } } async function extractArchive( bytes: Uint8Array, destination: string, options: { readonly maxExtractedBytes: number; readonly maxFiles: number; readonly maxFileBytes: number; }, ): Promise { let archive: AdmZip; try { archive = new AdmZip(toBuffer(bytes)); } catch { fail('plugin_artifact_invalid', 'artifact is not a readable ZIP archive'); } const entries = archive.getEntries(); if (entries.length === 0 || entries.length > options.maxFiles) { fail('plugin_artifact_invalid', 'artifact file count is outside its bound'); } const seen = new Set(); const files: string[] = []; let extractedBytes = 0; for (const entry of entries) { if (entryIsSymlink(entry)) fail('plugin_artifact_invalid', 'archive symlinks are not supported'); const segments = safeArchiveEntryName(entry.entryName, entry.isDirectory); const relative = segments.join('/'); if (seen.has(relative)) fail('plugin_artifact_invalid', 'archive contains duplicate paths'); seen.add(relative); const target = path.resolve(destination, ...segments); const relativeTarget = path.relative(destination, target); if (!relativeTarget || relativeTarget === '..' || relativeTarget.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTarget)) { fail('plugin_artifact_invalid', 'archive path escapes its staging directory'); } if (entry.isDirectory) { await mkdir(target, { recursive: true }); continue; } const declaredSize = entry.header.size; if (!Number.isSafeInteger(declaredSize) || declaredSize < 0 || declaredSize > options.maxFileBytes) { fail('plugin_artifact_invalid', 'archive file exceeds its bound'); } extractedBytes += declaredSize; if (extractedBytes > options.maxExtractedBytes) fail('plugin_artifact_invalid', 'archive extraction exceeds its bound'); let content: Buffer; try { content = entry.getData(); } catch { fail('plugin_artifact_invalid', 'archive entry could not be decompressed'); } if (content.byteLength !== declaredSize || content.byteLength > options.maxFileBytes) { fail('plugin_artifact_invalid', 'archive entry size is invalid'); } validateArchiveFile(relative, content); files.push(relative); await mkdir(path.dirname(target), { recursive: true }); try { await writeFile(target, content, { flag: 'wx' }); } catch { fail('plugin_artifact_invalid', 'archive extraction could not create a file'); } } return files; } function mapVerificationFailure(code: PluginSignatureFailureCode): PluginPackageStoreErrorCode { return code; } function validateGrantAgainstResolve( item: MarketplaceResolveItem, grant: DownloadGrant, input: ResolveInstallInput, now: number, maxArchiveBytes: number, ): void { if (!item.releaseId || !item.version || !item.sha256 || !item.sizeBytes || !item.releaseAdmissionId) { fail('plugin_release_unavailable', 'resolve result is missing download admission metadata'); } validReleaseId(item.releaseId); validPluginId(grant.pluginId); validReleaseId(grant.releaseId); validReleaseId(grant.releaseAdmissionId); validSha(item.sha256, 'resolve sha256'); if (!Number.isSafeInteger(item.sizeBytes) || item.sizeBytes < 1 || item.sizeBytes > maxArchiveBytes) { fail('plugin_artifact_invalid', 'resolve artifact size is outside its bound'); } if (!Number.isSafeInteger(grant.sizeBytes) || grant.sizeBytes < 1 || grant.sizeBytes > maxArchiveBytes) { fail('plugin_artifact_invalid', 'download grant size is outside its bound'); } if (item.pluginId !== input.pluginId || grant.pluginId !== input.pluginId || grant.releaseId !== item.releaseId || grant.version !== item.version || grant.sha256 !== item.sha256 || grant.sizeBytes !== item.sizeBytes || grant.releaseAdmissionId !== item.releaseAdmissionId) { fail('plugin_release_unavailable', 'download grant does not match the resolve result'); } if (grant.packageSchemaVersion !== 2) fail('plugin_artifact_invalid', 'downloaded Release must use package schema 2'); const expiresAt = typeof grant.expiresAt === 'string' ? Date.parse(grant.expiresAt) : Number.NaN; if (!Number.isFinite(expiresAt) || expiresAt <= now) fail('plugin_release_unavailable', 'download admission has expired'); } function toInstalledInput(record: InstalledReleaseRecord): InstalledReleaseInput { return { pluginId: record.pluginId, releaseId: record.releaseId, sha256: record.sha256 }; } /** * Main-owned device package store. The on-disk index is intentionally * account-free; account Library/admission state stays in AccountPluginCache. */ export class PluginPackageStore { private readonly rootDir: string; private readonly packagesDir: string; private readonly indexPath: string; private readonly currentSelectionPath: string; private readonly marketplace: MarketplacePackageClientPort; private readonly accountCache: AccountPluginCache; private readonly getAccountBindingImpl: () => AccountBinding | null; private readonly clientVersion: string; private readonly signatureVerifier: PluginSignatureVerifier; private readonly now: () => number; private readonly maxArchiveBytes: number; private readonly maxExtractedBytes: number; private readonly maxFiles: number; private readonly maxFileBytes: number; private readonly writeIndex: (filePath: string, bytes: Uint8Array) => Promise; private readonly activeWorkerReleaseIdsImpl: (() => readonly string[]) | null; private readonly activeWorkers = new Set(); private readonly pendingExplicitCleanupPluginIds = new Set(); private readonly unsubscribeSession: (() => void) | null; private operation: Promise = Promise.resolve(); constructor(options: PluginPackageStoreOptions) { this.rootDir = path.resolve(options.rootDir ?? path.join(getDataDir(), 'coding-plugins')); this.packagesDir = path.join(this.rootDir, 'packages'); this.indexPath = path.join(this.rootDir, 'index.json'); this.currentSelectionPath = path.join(this.rootDir, 'current.json'); this.marketplace = options.marketplace; this.accountCache = options.accountCache ?? new AccountPluginCache(); this.getAccountBindingImpl = options.getAccountBinding ?? (() => this.marketplace.getCurrentAccountBinding?.() ?? null); this.clientVersion = options.clientVersion ?? '1.0.0'; this.signatureVerifier = options.signatureVerifier ?? createPluginSignatureVerifier({ clientVersion: this.clientVersion, keyStore: options.keyStore, }); this.now = options.now ?? (() => Date.now()); this.maxArchiveBytes = assertPositiveOption(options.maxArchiveBytes, DEFAULT_MAX_ARCHIVE_BYTES, 'maxArchiveBytes'); this.maxExtractedBytes = assertPositiveOption(options.maxExtractedBytes, DEFAULT_MAX_EXTRACTED_BYTES, 'maxExtractedBytes'); this.maxFiles = assertPositiveOption(options.maxFiles, DEFAULT_MAX_FILES, 'maxFiles'); this.maxFileBytes = assertPositiveOption(options.maxFileBytes, DEFAULT_MAX_FILE_BYTES, 'maxFileBytes'); this.writeIndex = options.writeIndex ?? atomicWriteIndex; this.activeWorkerReleaseIdsImpl = options.activeWorkerReleaseIds ?? null; const subscribeSession = options.subscribeSession ?? ((listener: () => void) => subscribeWorksSquareSession(() => listener())); this.unsubscribeSession = subscribeSession(() => this.accountCache.invalidateAll()); } get indexFilePath(): string { return this.indexPath; } get packageRoot(): string { return this.rootDir; } dispose(): void { this.unsubscribeSession?.(); this.accountCache.invalidateAll(); } async syncLibrary(): Promise { const binding = this.requireBinding(); return this.withOperation(async () => { this.assertBinding(binding); if (!this.marketplace.readLibrary) fail('plugin_install_failed', 'Marketplace Library is unavailable'); const snapshot = await this.marketplace.readLibrary(); this.assertBinding(binding); this.accountCache.setLibrary(binding, snapshot); return clone(snapshot); }); } async resolveAndInstall(input: ResolveInstallInput): Promise { validPluginId(input.pluginId); if (input.channel !== undefined && input.channel !== 'stable' && input.channel !== 'beta') { fail('plugin_release_unavailable', 'invalid Marketplace channel'); } if ((input.channel ?? 'stable') === 'beta' && input.explicitBeta !== true) { fail('plugin_beta_selection_required'); } const binding = this.requireBinding(); return this.withOperation(async () => { const result = await this.resolveAndInstallLocked(input, binding); this.pendingExplicitCleanupPluginIds.delete(input.pluginId); return result; }); } async getInstalled(pluginId: string): Promise { const validated = validPluginId(pluginId); const index = await this.readIndex(); const current = await this.readCurrentSelection(); return this.getInstalledFromIndex(index, validated, undefined, current); } async getInstalledRelease(pluginId: string, releaseId: string): Promise { const validatedPluginId = validPluginId(pluginId); const validatedReleaseId = validReleaseId(releaseId); return this.getInstalledFromIndex( await this.readIndex(), validatedPluginId, validatedReleaseId, ); } async removeUnused(pluginId: string): Promise { const validated = validPluginId(pluginId); return this.withOperation(() => this.removeUnusedLocked(validated, undefined, 'background')); } /** Remove a device package while leaving the account Library projection intact. */ async uninstall(pluginId: string): Promise { const validated = validPluginId(pluginId); const binding = this.requireBinding(); return this.withOperation(async () => { this.assertBinding(binding); this.accountCache.invalidatePlugin(binding, validated); this.assertBinding(binding); const result = await this.removeUnusedLocked(validated, binding, 'explicit'); if (result.status === 'kept' && result.reason === 'active_worker_reference') { this.pendingExplicitCleanupPluginIds.add(validated); } else { this.pendingExplicitCleanupPluginIds.delete(validated); } return result; }); } registerActiveWorker(releaseId: string): void { this.activeWorkers.add(validReleaseId(releaseId)); } async releaseActiveWorker(releaseId: string): Promise { this.activeWorkers.delete(validReleaseId(releaseId)); if (this.pendingExplicitCleanupPluginIds.size === 0) return; await this.withOperation(async () => { for (const pluginId of [...this.pendingExplicitCleanupPluginIds]) { const result = await this.removeUnusedLocked(pluginId, undefined, 'explicit'); if (result.status === 'removed') this.pendingExplicitCleanupPluginIds.delete(pluginId); } }); } async readInstalledIndex(): Promise { const index = await this.readIndex(); return clone(index.releases); } private async resolveAndInstallLocked( input: ResolveInstallInput, binding: AccountBinding, ): Promise { const pluginId = validPluginId(input.pluginId); if (input.channel !== undefined && input.channel !== 'stable' && input.channel !== 'beta') { fail('plugin_release_unavailable', 'invalid Marketplace channel'); } const channel = input.channel ?? 'stable'; if (channel === 'beta' && input.explicitBeta !== true) fail('plugin_beta_selection_required'); this.assertBinding(binding); const index = await this.readIndex(); const current = await this.getInstalledFromIndex( index, pluginId, undefined, await this.readCurrentSelection(), ); const installed = input.installed === undefined ? (current ? [toInstalledInput(current)] : []) : input.installed === null ? [] : [input.installed]; const request: ResolveRequest = { makeloreVersion: input.makeloreVersion, channel, installed, resolveRequestId: input.resolveRequestId ?? `makelore-resolve-${randomUUID()}`, resolveRequestDigest: input.resolveRequestDigest, }; let resolved: MarketplaceResolveSnapshot; try { resolved = await this.marketplace.resolve(request); } catch (error) { throw this.mapMarketplaceError(error); } this.assertBinding(binding); const item = resolved.items.find((candidate) => candidate.pluginId === pluginId); if (!item) fail('plugin_release_unavailable', `Plugin ${pluginId} was not included in the resolve result`); if (item.channel !== undefined && item.channel !== null && item.channel !== channel) { fail('plugin_release_unavailable', 'resolve channel does not match the requested channel'); } if (item.action === 'unavailable') { const reason = item.reason; if (reason === 'plugin_release_not_ready' || reason === 'plugin_release_yanked' || reason === 'plugin_runtime_suspended' || reason === 'plugin_library_required' || reason === 'plugin_incompatible_client' || reason === 'plugin_signature_invalid' || reason === 'plugin_artifact_invalid' || reason === 'plugin_backend_unavailable') { fail(reason, reason); } fail('plugin_release_unavailable', reason ?? 'Plugin Release is unavailable'); } if (item.action === 'keep') { if (!current) fail('plugin_release_unavailable', 'resolve requested keep without an installed Release'); this.assertBinding(binding); await this.persistSelectedChannel(index, pluginId, current.releaseId, channel, binding); return { status: 'kept', pluginId, releaseId: current.releaseId, version: current.version, channel, packageRoot: current.packageRoot, definition: current.definition, }; } if (resolved.stale) fail('plugin_release_unavailable', 'stale resolve data cannot install a Release'); if (item.action !== 'install' && item.action !== 'update') fail('plugin_release_unavailable', 'unsupported resolve action'); if (!item.releaseId || !item.releaseAdmissionId || !item.version || !item.sha256 || !item.sizeBytes) { fail('plugin_release_unavailable', 'resolve result is missing Release metadata'); } const releaseId = validReleaseId(item.releaseId); const releaseAdmissionId = validReleaseId(item.releaseAdmissionId); validSha(item.sha256, 'resolve sha256'); if (!Number.isSafeInteger(item.sizeBytes) || item.sizeBytes < 1 || item.sizeBytes > this.maxArchiveBytes) { fail('plugin_artifact_invalid', 'resolve artifact size is outside its bound'); } const existingRecord = index.releases.find((record) => record.pluginId === pluginId && record.releaseId === item.releaseId); if (existingRecord && existingRecord.sha256 === item.sha256 && await isDirectoryPath(this.releaseDirectory(existingRecord))) { const existing = await this.getInstalledFromIndex(index, pluginId, item.releaseId); if (existing && !existing.unavailableReason) { this.assertBinding(binding); await this.persistSelectedChannel(index, pluginId, existing.releaseId, channel, binding); await this.setCurrentSelection( await this.readCurrentSelection(), pluginId, existing.releaseId, ); return { status: 'kept', pluginId, releaseId: existing.releaseId, version: existing.version, channel, packageRoot: existing.packageRoot, definition: existing.definition, }; } } let grant: DownloadGrant; try { grant = await this.marketplace.issueDownload({ releaseId, releaseAdmissionId }); this.assertBinding(binding); validateGrantAgainstResolve(item, grant, { ...input, pluginId }, this.now(), this.maxArchiveBytes); } catch (error) { if (error instanceof PluginPackageStoreError) throw error; throw this.mapMarketplaceError(error); } const packageRoot = this.releaseDirectory({ pluginId, releaseId: grant.releaseId } as InstalledReleaseRecord); if (await pathExists(packageRoot)) { return this.recoverOrphanedRelease({ index, currentSelection: await this.readCurrentSelection(), packageRoot, pluginId, grant, channel, binding, }); } let artifact: Uint8Array; try { artifact = await this.marketplace.downloadContent(grant); this.assertBinding(binding); } catch (error) { if (error instanceof PluginPackageStoreError) throw error; throw this.mapMarketplaceError(error); } const descriptor = this.verifyArtifact(artifact, grant); try { await mkdir(this.rootDir, { recursive: true }); } catch { fail('plugin_install_failed', 'package store is unavailable'); } let staging: string; try { staging = await mkdtemp(path.join(this.rootDir, `.download-${randomUUID()}-`)); } catch { fail('plugin_install_failed', 'package store is unavailable'); } let moved = false; try { const archivePath = path.join(staging, 'package.zip'); const extractedPath = path.join(staging, 'package'); await writeFile(archivePath, toBuffer(artifact), { flag: 'wx' }); await mkdir(extractedPath, { recursive: true }); const archiveFiles = await extractArchive(artifact, extractedPath, { maxExtractedBytes: this.maxExtractedBytes, maxFiles: this.maxFiles, maxFileBytes: this.maxFileBytes, }); const definition = await this.loadDefinition(extractedPath, grant, descriptor); validateArchiveDefinition(archiveFiles, definition); this.assertBinding(binding); await mkdir(path.dirname(packageRoot), { recursive: true }); if (await isDirectoryPath(packageRoot) || await pathExists(packageRoot)) { fail('plugin_release_conflict', 'immutable Release directory already exists'); } await writeFile(path.join(extractedPath, ORPHAN_ARCHIVE_FILE), toBuffer(artifact), { flag: 'wx' }); await rename(extractedPath, packageRoot); moved = true; const record = this.installedRecord(grant, channel, definition.runtimeKind); const records = index.releases.filter((candidate) => !(candidate.pluginId === pluginId && candidate.releaseId === grant.releaseId)); this.assertBinding(binding); try { await this.writeIndex(this.indexPath, serializeIndex({ schema_version: INDEX_SCHEMA_VERSION, releases: [...records, record] })); } catch { throw new PluginPackageStoreError('plugin_install_failed', 'package index replacement failed'); } this.assertBinding(binding); await this.setCurrentSelection(await this.readCurrentSelection(), pluginId, record.releaseId); this.assertBinding(binding); return { status: 'installed', pluginId, releaseId: record.releaseId, version: record.version, channel: record.channel, packageRoot, definition, }; } catch (error) { if (error instanceof PluginPackageStoreError) throw error; if (error instanceof Error && error.name === 'CodingPluginManifestError') { throw new PluginPackageStoreError('plugin_manifest_invalid', 'package manifest is invalid'); } throw new PluginPackageStoreError('plugin_install_failed', 'package installation failed'); } finally { if (moved) { await rm(path.join(staging, 'package.zip'), { force: true }).catch(() => undefined); } await rm(staging, { recursive: true, force: true }).catch(() => undefined); } } private async recoverOrphanedRelease(input: { readonly index: IndexDocument; readonly currentSelection: CurrentSelectionDocument; readonly packageRoot: string; readonly pluginId: string; readonly grant: DownloadGrant; readonly channel: 'stable' | 'beta'; readonly binding: AccountBinding; }): Promise { const existingRecord = input.index.releases.find((record) => ( record.pluginId === input.pluginId && record.releaseId === input.grant.releaseId )); if (existingRecord) fail('plugin_release_conflict', 'immutable Release directory already exists'); let artifact: Uint8Array; try { artifact = await readFile(path.join(input.packageRoot, ORPHAN_ARCHIVE_FILE)); } catch { fail('plugin_release_conflict', 'immutable Release directory cannot be recovered'); } const descriptor = this.verifyArtifact(artifact, input.grant); const definition = await this.loadDefinition(input.packageRoot, input.grant, descriptor); this.assertBinding(input.binding); const record = this.installedRecord(input.grant, input.channel, definition.runtimeKind); this.assertBinding(input.binding); try { await this.writeIndex(this.indexPath, serializeIndex({ schema_version: INDEX_SCHEMA_VERSION, releases: [...input.index.releases, record], })); } catch { throw new PluginPackageStoreError('plugin_install_failed', 'package index replacement failed'); } this.assertBinding(input.binding); await this.setCurrentSelection(input.currentSelection, input.pluginId, record.releaseId); this.assertBinding(input.binding); return { status: 'installed', pluginId: input.pluginId, releaseId: record.releaseId, version: record.version, channel: record.channel, packageRoot: input.packageRoot, definition, }; } private verifyArtifact(artifact: Uint8Array, grant: DownloadGrant): PluginReleaseDescriptor { const expiresAt = typeof grant.expiresAt === 'string' ? Date.parse(grant.expiresAt) : Number.NaN; if (!Number.isFinite(expiresAt) || expiresAt <= this.now()) { fail('plugin_release_unavailable', 'download admission has expired'); } if (!(artifact instanceof Uint8Array)) fail('plugin_artifact_invalid', 'downloaded artifact is not binary data'); if (artifact.byteLength !== grant.sizeBytes || artifact.byteLength > this.maxArchiveBytes || digest(artifact) !== grant.sha256) { fail('plugin_artifact_invalid', 'downloaded artifact does not match its grant'); } const descriptor = this.buildDescriptor(grant); if (!isMakeLoreVersionCompatible( this.clientVersion, descriptor.minMakeloreVersion, descriptor.maxMakeloreVersion, )) { fail('plugin_incompatible_client', 'Release is incompatible with this MakeLore client'); } const verification = this.signatureVerifier.verify({ keyId: grant.signingKeyId, signature: grant.descriptorSignature, descriptor, artifact, }); if (!verification.ok) fail(mapVerificationFailure(verification.code), verification.message); return descriptor; } private installedRecord( grant: DownloadGrant, channel: 'stable' | 'beta', runtimeKind: 'skill_only' | 'platform_hosted', ): InstalledReleaseRecord { return Object.freeze({ pluginId: grant.pluginId, releaseId: grant.releaseId, version: grant.version, packageSchemaVersion: grant.packageSchemaVersion, contractVersion: grant.contractVersion, runtimeKind, sha256: grant.sha256, sizeBytes: grant.sizeBytes, installedAt: new Date(this.now()).toISOString(), channel, minMakeloreVersion: grant.minMakeloreVersion, maxMakeloreVersion: grant.maxMakeloreVersion, }); } private async persistSelectedChannel( index: IndexDocument, pluginId: string, releaseId: string, channel: 'stable' | 'beta', binding: AccountBinding, ): Promise { const record = index.releases.find((candidate) => ( candidate.pluginId === pluginId && candidate.releaseId === releaseId )); if (!record) fail('plugin_store_index_invalid', 'selected Release is missing from the package index'); if (record.channel === channel) return; this.assertBinding(binding); const releases = index.releases.map((candidate) => ( candidate === record ? Object.freeze({ ...candidate, channel }) : candidate )); try { await this.writeIndex(this.indexPath, serializeIndex({ schema_version: INDEX_SCHEMA_VERSION, releases, })); } catch { throw new PluginPackageStoreError('plugin_install_failed', 'package channel update failed'); } this.assertBinding(binding); } private async removeUnusedLocked( validated: string, binding?: AccountBinding, mode: 'background' | 'explicit' = 'background', ): Promise { if (binding) this.assertBinding(binding); const index = await this.readIndex(); const currentSelection = await this.readCurrentSelection(); const records = index.releases.filter((record) => record.pluginId === validated); if (records.length === 0) { if (currentSelection.current[validated] !== undefined) { if (binding) this.assertBinding(binding); const current = { ...currentSelection.current }; delete current[validated]; await this.writeCurrentSelection({ schema_version: CURRENT_SELECTION_SCHEMA_VERSION, current: Object.freeze(current), }); } return { status: 'removed', pluginId: validated, reason: 'none' }; } const selected = currentSelection.current[validated] ? records.find((record) => record.releaseId === currentSelection.current[validated]) : undefined; const protectedIds = new Set([ ...this.accountCache.referencedReleaseIds(), ...(this.activeWorkerReleaseIds() ?? []), ...this.activeWorkers, ]); let removable: InstalledReleaseRecord[]; if (mode === 'explicit') { removable = records.filter((record) => !protectedIds.has(record.releaseId)); } else { if (!selected) { return { status: 'kept', pluginId: validated, reason: 'current_selection_missing', }; } removable = records.length === 1 && !protectedIds.has(selected.releaseId) ? records : records.filter((record) => ( record.releaseId !== selected.releaseId && !protectedIds.has(record.releaseId) )); if (removable.length === 0) { return { status: 'kept', pluginId: validated, releaseId: selected.releaseId, version: selected.version, ...(selected.channel === undefined ? {} : { channel: selected.channel }), }; } } const remaining = index.releases.filter((record) => !removable.includes(record)); if (binding) this.assertBinding(binding); if (removable.length > 0) { try { await this.writeIndex(this.indexPath, serializeIndex({ schema_version: INDEX_SCHEMA_VERSION, releases: remaining })); } catch { throw new PluginPackageStoreError('plugin_install_failed', 'package index cleanup failed'); } } const nextCurrent = { ...currentSelection.current }; if (mode === 'explicit') { delete nextCurrent[validated]; } else if (nextCurrent[validated] && !remaining.some((record) => ( record.pluginId === validated && record.releaseId === nextCurrent[validated] ))) delete nextCurrent[validated]; await this.writeCurrentSelection({ schema_version: CURRENT_SELECTION_SCHEMA_VERSION, current: Object.freeze(nextCurrent), }); if (binding) this.assertBinding(binding); await Promise.all(removable.map(async (record) => { await rm(this.releaseDirectory(record), { recursive: true, force: true }); })); if (binding) this.assertBinding(binding); const currentAfter = nextCurrent[validated] ? remaining.find((record) => record.pluginId === validated && record.releaseId === nextCurrent[validated]) : undefined; const selectedAfter = selected && remaining.some((record) => ( record.pluginId === validated && record.releaseId === selected.releaseId )) ? selected : undefined; if (mode === 'explicit' && !currentAfter && remaining.some((record) => record.pluginId === validated && protectedIds.has(record.releaseId))) { return { status: 'kept', pluginId: validated, reason: 'active_worker_reference' }; } const reportedAfter = currentAfter ?? selectedAfter ?? remaining.find((record) => record.pluginId === validated); if (reportedAfter) { return { status: 'kept', pluginId: validated, releaseId: reportedAfter.releaseId, version: reportedAfter.version, ...(reportedAfter.channel === undefined ? {} : { channel: reportedAfter.channel }), }; } return { status: 'removed', pluginId: validated, ...(selected ? { releaseId: selected.releaseId, version: selected.version } : { reason: 'none' }), }; } private buildDescriptor(grant: DownloadGrant): PluginReleaseDescriptor { try { return buildPluginReleaseDescriptor({ pluginId: grant.pluginId, version: grant.version, packageSchemaVersion: grant.packageSchemaVersion, contractVersion: grant.contractVersion, minMakeloreVersion: grant.minMakeloreVersion, maxMakeloreVersion: grant.maxMakeloreVersion, artifact: { sha256: grant.sha256, sizeBytes: grant.sizeBytes }, }); } catch (error) { fail('plugin_artifact_invalid', error instanceof Error ? error.message : 'invalid Release descriptor'); } } private async loadDefinition( packageRoot: string, grant: DownloadGrant, descriptor: PluginReleaseDescriptor, ): Promise { let definition: CodingPluginDefinition; try { definition = await loadCodingPluginDefinition(packageRoot, { acquisitionMode: 'user_acquired', releaseId: grant.releaseId, provenance: { source: 'marketplace', packageRoot }, }); } catch (error) { if (error instanceof PluginPackageStoreError) throw error; throw new PluginPackageStoreError('plugin_manifest_invalid', 'package manifest is invalid'); } if (definition.id !== descriptor.pluginId || definition.version !== descriptor.version || definition.contractVersion !== descriptor.contractVersion || definition.releaseId !== grant.releaseId || (definition.runtimeKind !== 'skill_only' && definition.runtimeKind !== 'platform_hosted') || definition.acquisitionMode !== 'user_acquired') { fail('plugin_manifest_invalid', 'package definition does not match the signed Release'); } return definition; } private requireBinding(): AccountBinding { const binding = this.getAccountBindingImpl(); if (!binding || typeof binding.accountKey !== 'string' || binding.accountKey.length === 0 || binding.accountKey.length > 512 || !Number.isSafeInteger(binding.epoch) || binding.epoch < 0) { fail('plugin_account_changed', 'Marketplace account session is unavailable'); } return { accountKey: binding.accountKey, epoch: binding.epoch }; } private assertBinding(binding: AccountBinding): void { if (!sameBinding(this.getAccountBindingImpl(), binding)) fail('plugin_account_changed', 'Marketplace account changed during package operation'); } private activeWorkerReleaseIds(): readonly string[] { return this.activeWorkerReleaseIdsImpl?.() ?? []; } private async readIndex(): Promise { try { return parseIndexDocument(await parseJsonFile(this.indexPath)); } catch (error) { if (isNotFound(error)) return { schema_version: INDEX_SCHEMA_VERSION, releases: [] }; if (error instanceof PluginPackageStoreError) throw error; throw new PluginPackageStoreError('plugin_store_index_invalid', 'package index is unavailable'); } } private async readCurrentSelection(): Promise { try { return parseCurrentSelectionDocument(await parseJsonFile(this.currentSelectionPath)); } catch (error) { if (isNotFound(error)) { return { schema_version: CURRENT_SELECTION_SCHEMA_VERSION, current: Object.freeze({}) }; } if (error instanceof PluginPackageStoreError) throw error; throw new PluginPackageStoreError('plugin_store_index_invalid', 'current package selection is unavailable'); } } private async writeCurrentSelection(document: CurrentSelectionDocument): Promise { try { await atomicWriteIndex(this.currentSelectionPath, serializeCurrentSelection(document)); } catch { throw new PluginPackageStoreError('plugin_install_failed', 'package current selection replacement failed'); } } private async setCurrentSelection( currentSelection: CurrentSelectionDocument, pluginId: string, releaseId: string, ): Promise { if (currentSelection.current[pluginId] === releaseId) return; const current = { ...currentSelection.current, [pluginId]: releaseId }; await this.writeCurrentSelection({ schema_version: CURRENT_SELECTION_SCHEMA_VERSION, current: Object.freeze(current), }); } private releaseDirectory(record: Pick): string { return path.join(this.packagesDir, record.pluginId, record.releaseId); } private async getInstalledFromIndex( index: IndexDocument, pluginId: string, releaseId?: string, currentSelection?: CurrentSelectionDocument, ): Promise { const pointedReleaseId = releaseId === undefined ? currentSelection?.current[pluginId] : undefined; const records = index.releases .map((record, index) => ({ record, index })) .filter(({ record }) => record.pluginId === pluginId && (releaseId === undefined || record.releaseId === releaseId)) .sort((left, right) => right.record.installedAt.localeCompare(left.record.installedAt) || right.index - left.index) .map(({ record }) => record); const selected = releaseId !== undefined ? records : pointedReleaseId === undefined ? [] : records.filter((record) => record.releaseId === pointedReleaseId); for (const record of selected) { const packageRoot = this.releaseDirectory(record); if (!await isDirectoryPath(packageRoot)) continue; try { const definition = await loadCodingPluginDefinition(packageRoot, { runtimeKind: record.runtimeKind, acquisitionMode: 'user_acquired', releaseId: record.releaseId, provenance: { source: 'marketplace', packageRoot }, }); if (definition.id !== record.pluginId || definition.version !== record.version || definition.contractVersion !== record.contractVersion || definition.runtimeKind !== record.runtimeKind) { throw new PluginPackageStoreError('plugin_manifest_invalid', 'installed package does not match its immutable index record'); } const compatible = record.minMakeloreVersion !== undefined && isMakeLoreVersionCompatible( this.clientVersion, record.minMakeloreVersion, record.maxMakeloreVersion ?? null, ); return { ...record, packageRoot, definition, ...(compatible ? {} : { unavailableReason: 'plugin_incompatible_client' as const }), }; } catch (error) { if (error instanceof PluginPackageStoreError) throw error; throw new PluginPackageStoreError('plugin_manifest_invalid', 'installed package manifest is invalid'); } } return null; } private mapMarketplaceError(error: unknown): PluginPackageStoreError { if (error instanceof PluginPackageStoreError) return error; if (isRecord(error) && error.code === 'marketplace_account_changed') { return new PluginPackageStoreError('plugin_account_changed', 'Marketplace account changed during package operation'); } if (isRecord(error) && error.code === 'marketplace_beta_selection_required') { return new PluginPackageStoreError('plugin_beta_selection_required'); } if (isRecord(error) && ( error.code === 'plugin_release_yanked' || error.code === 'plugin_release_not_ready' || error.code === 'plugin_runtime_suspended' || error.code === 'plugin_library_required' || error.code === 'plugin_backend_unavailable' || error.code === 'plugin_incompatible_client' || error.code === 'plugin_signature_invalid' || error.code === 'plugin_artifact_invalid' )) { return new PluginPackageStoreError( error.code, typeof error.message === 'string' ? error.message : error.code, ); } if (isRecord(error) && error.code === 'plugin_auth_required') { return new PluginPackageStoreError('plugin_account_changed', 'Marketplace authentication is required'); } return new PluginPackageStoreError('plugin_install_failed', 'Marketplace package operation failed'); } private async withOperation(operation: () => Promise): Promise { let release!: () => void; const previous = this.operation; this.operation = new Promise((resolve) => { release = resolve; }); await previous; try { return await operation(); } finally { release(); } } } async function pathExists(value: string): Promise { try { await lstat(value); return true; } catch { return false; } } export async function readPluginPackageIndex(filePath: string): Promise { try { const value = parseIndexDocument(await parseJsonFile(filePath)); return clone(value.releases); } catch (error) { if (error instanceof PluginPackageStoreError) throw error; throw new PluginPackageStoreError('plugin_store_index_invalid', 'package index is unavailable'); } }