// @vitest-environment node import { createHash, generateKeyPairSync, sign } from 'node:crypto'; import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; import path from 'node:path'; import AdmZip from 'adm-zip'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { AccountPluginCache, type AccountBinding, type MarketplaceLibrarySnapshot, } from '../../electron/coding-plugins/account-plugin-cache'; import { MarketplaceClientError, createMarketplaceClient, type MarketplaceClient, type ResolveRequest, } from '../../electron/coding-plugins/marketplace-client'; import { PluginPackageStore, type DownloadGrant, type ResolveSnapshot, } from '../../electron/coding-plugins/package-store'; import { buildPluginReleaseDescriptor, serializePluginReleaseDescriptor, } from '../../electron/coding-plugins/release-descriptor'; const ACCOUNT_A: AccountBinding = { accountKey: 'a'.repeat(64), epoch: 1 }; const ACCOUNT_B: AccountBinding = { accountKey: 'b'.repeat(64), epoch: 2 }; const PLUGIN_ID = 'makelore.example'; const RELEASE_ID = 'release-1'; const ADMISSION_ID = 'admission-1'; const SHA256 = 'a'.repeat(64); function deferred() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; } const catalogPage = { items: [{ plugin_id: PLUGIN_ID, title: 'Example', summary: 'Example Skill', category: 'tools', tags: ['example'], provider_display_name: 'MakeLore', runtime_kind: 'skill_only', runtime_status: 'enabled', acquisition: 'free', usage_billing: 'included', included_operation_count: 0, metered_operation_count: 0, stable_version: '1.0.0', beta_version: null, }], next_cursor: null, total: 1, catalog_generation: 7, }; function response(body: unknown, init: ResponseInit = {}, headers: Record = {}): Response { return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json', ...headers }, ...init, }); } function requestBody(fetcher: ReturnType, index: number): Record { return JSON.parse(fetcher.mock.calls[index]?.[1]?.body as string) as Record; } function makeResolveResult(input: ResolveRequest, itemOverrides: Record = {}): ResolveSnapshot { return { resolveRequestId: input.resolveRequestId ?? 'generated', resolveRequestDigest: input.resolveRequestDigest ?? SHA256, items: [{ pluginId: PLUGIN_ID, action: 'install', releaseId: RELEASE_ID, version: '1.0.0', sha256: SHA256, sizeBytes: 1, releaseAdmissionId: ADMISSION_ID, expiresAt: '2100-01-01T00:00:00Z', channel: input.channel, reason: null, ...itemOverrides, }], catalogGeneration: 7, etag: '"plugins-7-tp-none"', stale: false, }; } function buildSkillOnlyArchive( overrides: Readonly> = {}, ): Buffer { const zip = new AdmZip(); const files = new Map([ ['plugin.json', JSON.stringify({ $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', name: PLUGIN_ID, version: '1.0.0', description: 'Example Skill', author: { name: 'MakeLore' }, extensions: { 'com.makelore': { capabilityManifest: './com.makelore/capability.json' } }, })], ['com.makelore/capability.json', JSON.stringify({ schemaVersion: 2, pluginId: PLUGIN_ID, contractVersion: 1, scope: 'project', runtime: { kind: 'skill_only' }, skills: [{ id: 'example-skill', entry: '../skills/example-skill/SKILL.md', grants: [] }], tools: [], })], ['skills/example-skill/SKILL.md', '# Example\n'], ]); for (const [filePath, content] of Object.entries(overrides)) files.set(filePath, content); for (const [filePath, content] of files) { zip.addFile(filePath, Buffer.isBuffer(content) ? content : Buffer.from(content)); } return zip.toBuffer(); } function signedGrant( archive: Buffer, options: { readonly releaseId?: string; readonly signingKeyId?: string; readonly minMakeloreVersion?: string; readonly maxMakeloreVersion?: string | null; } = {}, ): { grant: DownloadGrant; publicKey: Buffer; signature: string } { const { publicKey, privateKey } = generateKeyPairSync('ed25519'); const sha256 = createHash('sha256').update(archive).digest('hex'); const releaseId = options.releaseId ?? RELEASE_ID; const descriptor = buildPluginReleaseDescriptor({ pluginId: PLUGIN_ID, version: '1.0.0', packageSchemaVersion: 2, contractVersion: 1, minMakeloreVersion: options.minMakeloreVersion ?? '1.0.0', maxMakeloreVersion: options.maxMakeloreVersion ?? null, artifact: { sha256, sizeBytes: archive.byteLength }, }); const signature = sign(null, serializePluginReleaseDescriptor(descriptor), privateKey).toString('base64url'); return { grant: { releaseAdmissionId: ADMISSION_ID, releaseId, pluginId: PLUGIN_ID, version: '1.0.0', packageSchemaVersion: 2, contractVersion: 1, minMakeloreVersion: options.minMakeloreVersion ?? '1.0.0', maxMakeloreVersion: options.maxMakeloreVersion ?? null, sizeBytes: archive.byteLength, sha256, signingKeyId: options.signingKeyId ?? 'test-key', descriptorSignature: signature, expiresAt: '2100-01-01T00:00:00Z', contentUrl: `/api/plugin-marketplace/v1/releases/${releaseId}/content?release_admission_id=${ADMISSION_ID}`, }, publicKey: publicKey.export({ type: 'spki', format: 'der' }) as Buffer, signature, }; } describe('Marketplace client and account cache', () => { afterEach(() => vi.restoreAllMocks()); function rawLibraryEntry(pluginId: string, title: string) { return { plugin_id: pluginId, title, summary: title, category: 'tools', acquisition: 'free', acquisition_mode: 'user_acquired', catalog_status: 'active', runtime_status: 'enabled', acquired_at: '2026-08-28T00:00:00Z', removed_at: null, stable_version: '1.0.0', beta_version: null, }; } function rawLibrary(entries: readonly Record[]) { return { items: entries, total: entries.length }; } it('keeps Library and admission snapshots isolated by account and invalidates on logout', () => { const cache = new AccountPluginCache(); const aLibrary: MarketplaceLibrarySnapshot = { items: [{ pluginId: PLUGIN_ID, title: 'A', summary: 'A', category: 'tools', acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled', acquiredAt: null, removedAt: null, stableVersion: '1.0.0', betaVersion: null, }], total: 1, stale: false, fetchedAt: 1, }; cache.setLibrary(ACCOUNT_A, aLibrary); cache.setResolve(ACCOUNT_A, 'resolve-a', { resolveRequestId: 'resolve-a', resolveRequestDigest: SHA256, items: [{ pluginId: PLUGIN_ID, action: 'install', releaseId: RELEASE_ID }], catalogGeneration: 1, etag: null, stale: false, }); expect(cache.getLibrary(ACCOUNT_A)).toEqual(aLibrary); expect(cache.getLibrary(ACCOUNT_B)).toBeNull(); expect(cache.getResolve(ACCOUNT_B, 'resolve-a')).toBeNull(); expect(cache.referencedReleaseIds()).toEqual(new Set([RELEASE_ID])); cache.invalidateAll(); expect(cache.getLibrary(ACCOUNT_A)).toBeNull(); expect(cache.referencedReleaseIds()).toEqual(new Set()); }); it('does not let an older Library read overwrite a newer mutation/read intent', async () => { const oldRead = deferred(); const mutation = deferred(); const newestRead = deferred(); const fetcher = vi.fn() .mockImplementationOnce(() => oldRead.promise) .mockImplementationOnce(() => mutation.promise) .mockImplementationOnce(() => newestRead.promise); const cache = new AccountPluginCache(); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async () => 'token', getAccountBinding: () => ACCOUNT_A, subscribeSession: () => () => undefined, accountCache: cache, }); const old = client.readLibrary(); const acquired = client.acquire(PLUGIN_ID); mutation.resolve(response(rawLibraryEntry(PLUGIN_ID, 'acquired'))); await Promise.resolve(); newestRead.resolve(response(rawLibrary([rawLibraryEntry(PLUGIN_ID, 'newest')]))); oldRead.resolve(response(rawLibrary([rawLibraryEntry(PLUGIN_ID, 'old')]))); await Promise.all([old, acquired]); expect(cache.getLibrary(ACCOUNT_A)?.items[0]?.title).toBe('newest'); }); it('keeps the latest same-account mutation intent when mutation responses complete out of order', async () => { const firstMutation = deferred(); const secondMutation = deferred(); const firstRead = deferred(); const secondRead = deferred(); const fetcher = vi.fn() .mockImplementationOnce(() => firstMutation.promise) .mockImplementationOnce(() => secondMutation.promise) .mockImplementationOnce(() => secondRead.promise) .mockImplementationOnce(() => firstRead.promise); const cache = new AccountPluginCache(); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async () => 'token', getAccountBinding: () => ACCOUNT_A, subscribeSession: () => () => undefined, accountCache: cache, }); const first = client.acquire('makelore.first'); const second = client.acquire('makelore.second'); secondMutation.resolve(response(rawLibraryEntry('makelore.second', 'second mutation'))); await Promise.resolve(); secondRead.resolve(response(rawLibrary([rawLibraryEntry('makelore.second', 'second newest')]))); firstMutation.resolve(response(rawLibraryEntry('makelore.first', 'first mutation'))); await Promise.resolve(); firstRead.resolve(response(rawLibrary([rawLibraryEntry('makelore.first', 'first stale')]))); await Promise.all([first, second]); expect(cache.getLibrary(ACCOUNT_A)?.items[0]?.title).toBe('second newest'); }); it('parses bounded catalog metadata, refreshes exactly once after a 401, and marks stale data', async () => { const fetcher = vi.fn(); fetcher .mockResolvedValueOnce(new Response('', { status: 401 })) .mockResolvedValueOnce(response(catalogPage, {}, { ETag: '"plugins-7-tp-none"', 'X-Plugin-Catalog-Generation': '7', 'X-Token-Point-Pricing-Version': 'none', })); const refresh = vi.fn(async () => 'refreshed-token'); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async (options) => options?.forceRefresh ? refresh() : 'initial-token', getAccountBinding: () => ACCOUNT_A, subscribeSession: () => () => undefined, }); await expect(client.readCatalog({ limit: 10 })).resolves.toMatchObject({ total: 1, etag: '"plugins-7-tp-none"', stale: false, }); expect(fetcher).toHaveBeenCalledTimes(2); expect(refresh).toHaveBeenCalledTimes(1); expect(fetcher.mock.calls[1]?.[1]?.headers).toMatchObject({ Authorization: 'Bearer refreshed-token' }); fetcher.mockRejectedValueOnce(new Error('offline')); await expect(client.readCatalog({ limit: 10 })).resolves.toMatchObject({ stale: true, total: 1 }); }); it('refreshes download authentication at most once before accepting the artifact', async () => { const archive = Buffer.from('signed-artifact'); const { grant } = signedGrant(archive); const fetcher = vi.fn() .mockResolvedValueOnce(new Response(null, { status: 401 })) .mockResolvedValueOnce(new Response(archive, { status: 200, headers: { 'content-length': String(archive.byteLength) }, })); const refresh = vi.fn(async () => 'refreshed-token'); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async (options) => options?.forceRefresh ? refresh() : 'initial-token', getAccountBinding: () => ACCOUNT_A, subscribeSession: () => () => undefined, }); const downloaded = await client.downloadContent(grant); expect(Buffer.from(downloaded)).toEqual(archive); expect(fetcher).toHaveBeenCalledTimes(2); expect(refresh).toHaveBeenCalledTimes(1); expect(fetcher.mock.calls[1]?.[1]?.headers).toMatchObject({ Authorization: 'Bearer refreshed-token' }); }); it('parses the detail DTO with its nested release projection', async () => { const fetcher = vi.fn().mockResolvedValue(response({ ...catalogPage.items[0], description_markdown: 'Example details', permissions: ['plugin.example.read'], operations: [], stable_release: null, beta_release: null, }, {}, { ETag: '"plugins-7-tp-none"', 'X-Plugin-Catalog-Generation': '7', 'X-Token-Point-Pricing-Version': 'none', })); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async () => null, subscribeSession: () => () => undefined, }); await expect(client.readDetail(PLUGIN_ID)).resolves.toMatchObject({ pluginId: PLUGIN_ID, descriptionMarkdown: 'Example details', permissions: ['plugin.example.read'], etag: '"plugins-7-tp-none"', }); }); it('derives stable resolve identity from one logical request and changes it when installed state changes', async () => { const fetcher = vi.fn().mockImplementation(async (_input, init) => { const request = JSON.parse(init?.body as string) as Record; return response({ resolve_request_id: request.resolve_request_id, resolve_request_digest: request.resolve_request_digest, items: [], catalog_generation: 7, }, {}, { ETag: '"plugins-7-tp-none"' }); }); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async () => 'token', getAccountBinding: () => ACCOUNT_A, subscribeSession: () => () => undefined, }); const base: ResolveRequest = { makeloreVersion: '1.0.0', channel: 'stable', installed: [], }; await client.resolve(base); await client.resolve(base); const firstId = requestBody(fetcher, 0).resolve_request_id; const secondId = requestBody(fetcher, 1).resolve_request_id; expect(firstId).toBe(secondId); await client.resolve({ ...base, installed: [{ pluginId: PLUGIN_ID, releaseId: RELEASE_ID, sha256: SHA256 }] }); expect(requestBody(fetcher, 2).resolve_request_id).not.toBe(firstId); }); it('rejects a response body above the bounded DTO limit', async () => { const fetcher = vi.fn().mockResolvedValue(new Response('x'.repeat(2_100_000), { status: 200 })); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async () => null, subscribeSession: () => () => undefined, }); await expect(client.readCatalog({ limit: 10 })).rejects.toMatchObject({ code: 'marketplace_response_too_large' }); }); it('applies one deadline to response headers and a body that never completes', async () => { const neverBody = new ReadableStream({ start() { /* intentionally never closes */ } }); const fetcher = vi.fn().mockResolvedValue(new Response(neverBody, { status: 200 })); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', requestTimeoutMs: 20, getAccessToken: async () => null, subscribeSession: () => () => undefined, }); await expect(client.readCatalog({ limit: 10 })).rejects.toMatchObject({ code: 'marketplace_request_failed' }); }); it('preserves a bounded server release status from the response body', async () => { const fetcher = vi.fn().mockResolvedValue(response({ success: false, code: 'plugin_release_yanked', error: 'Release is no longer available', }, { status: 409 })); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async () => null, subscribeSession: () => () => undefined, }); await expect(client.readCatalog({ limit: 10 })).rejects.toMatchObject({ code: 'plugin_release_yanked', status: 409, }); }); it('rejects a malformed authenticated response with a stable client error', async () => { const fetcher = vi.fn().mockResolvedValue(response({ items: [] })); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async () => 'token', getAccountBinding: () => ACCOUNT_A, subscribeSession: () => () => undefined, }); await expect(client.readLibrary()).rejects.toBeInstanceOf(MarketplaceClientError); }); }); describe('PluginPackageStore', () => { let temporaryRoot: string | null = null; afterEach(async () => { if (temporaryRoot) await rm(temporaryRoot, { recursive: true, force: true }); temporaryRoot = null; }); it('verifies and atomically installs a signed Skill-only package without account data in index', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const { grant, publicKey } = signedGrant(archive); const resolve = vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })); const issueDownload = vi.fn(async () => grant); const marketplace: MarketplaceClient = { resolve, issueDownload, downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', publicKey]]), accountCache: new AccountPluginCache(), getAccountBinding: () => ACCOUNT_A, }); await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' })) .resolves.toMatchObject({ pluginId: PLUGIN_ID, releaseId: RELEASE_ID, status: 'installed' }); await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ pluginId: PLUGIN_ID, releaseId: RELEASE_ID, version: '1.0.0', }); const index = JSON.parse(await readFile(path.join(temporaryRoot, 'index.json'), 'utf8')) as Record; expect(JSON.stringify(index)).not.toContain('account'); expect(JSON.stringify(index)).not.toContain('admission'); expect(JSON.stringify(index)).not.toContain('token'); expect(resolve).toHaveBeenCalledTimes(1); expect(issueDownload).toHaveBeenCalledWith({ releaseId: RELEASE_ID, releaseAdmissionId: ADMISSION_ID }); }); it('removes only releases with no account or active-worker reference', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const { grant, publicKey } = signedGrant(archive); const accountCache = new AccountPluginCache(); const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })), issueDownload: vi.fn(async () => grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', publicKey]]), accountCache, getAccountBinding: () => ACCOUNT_A, }); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); accountCache.setResolve(ACCOUNT_A, 'protected', makeResolveResult({ makeloreVersion: '1.0.0' }, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })); await expect(store.removeUnused(PLUGIN_ID)).resolves.toMatchObject({ status: 'kept', pluginId: PLUGIN_ID, releaseId: RELEASE_ID, }); accountCache.invalidateAll(); store.registerActiveWorker(RELEASE_ID); await expect(store.removeUnused(PLUGIN_ID)).resolves.toMatchObject({ status: 'kept', pluginId: PLUGIN_ID, releaseId: RELEASE_ID, }); store.releaseActiveWorker(RELEASE_ID); await expect(store.removeUnused(PLUGIN_ID)).resolves.toMatchObject({ status: 'removed', pluginId: PLUGIN_ID, releaseId: RELEASE_ID, }); await expect(store.readInstalledIndex()).resolves.toEqual([]); await expect(store.removeUnused(PLUGIN_ID)).resolves.toEqual({ status: 'removed', pluginId: PLUGIN_ID, reason: 'none', }); }); it('preserves the old immutable release when index replacement fails', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const { grant, publicKey } = signedGrant(archive); const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })), issueDownload: vi.fn(async () => grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', publicKey]]), getAccountBinding: () => ACCOUNT_A, }); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); const replacement = signedGrant(archive, { releaseId: 'release-2' }); const replacementMarketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { releaseId: replacement.grant.releaseId, sha256: replacement.grant.sha256, sizeBytes: replacement.grant.sizeBytes, })), issueDownload: vi.fn(async () => replacement.grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const failingStore = new PluginPackageStore({ rootDir: temporaryRoot, marketplace: replacementMarketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', replacement.publicKey]]), getAccountBinding: () => ACCOUNT_A, writeIndex: async () => { throw new Error('simulated index interruption'); }, }); await expect(failingStore.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' })) .rejects.toMatchObject({ code: 'plugin_install_failed' }); await expect(failingStore.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: RELEASE_ID }); await expect(stat(path.join(temporaryRoot, 'packages', PLUGIN_ID, 'release-2'))) .resolves.toMatchObject({ isDirectory: expect.any(Function) }); const recoveredStore = new PluginPackageStore({ rootDir: temporaryRoot, marketplace: replacementMarketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', replacement.publicKey]]), getAccountBinding: () => ACCOUNT_A, }); await expect(recoveredStore.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' })) .resolves.toMatchObject({ status: 'installed', releaseId: 'release-2' }); await expect(recoveredStore.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: 'release-2' }); }); it('uninstalls a device package without removing the account Library snapshot', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const { grant, publicKey } = signedGrant(archive); const accountCache = new AccountPluginCache(); const library: MarketplaceLibrarySnapshot = { items: [{ pluginId: PLUGIN_ID, title: 'Example', summary: 'Example', category: 'tools', acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled', acquiredAt: '2026-08-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: null, }], total: 1, stale: false, fetchedAt: 1, }; const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })), issueDownload: vi.fn(async () => grant), downloadContent: async () => archive, readLibrary: vi.fn(async () => library), getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, accountCache, getAccountBinding: () => ACCOUNT_A, keyStore: new Map([['test-key', publicKey]]), clientVersion: '1.0.0', }); await store.syncLibrary(); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); await expect(store.uninstall(PLUGIN_ID)).resolves.toMatchObject({ status: 'removed', pluginId: PLUGIN_ID }); expect(accountCache.getLibrary(ACCOUNT_A)).toEqual(library); expect(accountCache.referencedReleaseIds()).toEqual(new Set()); await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull(); }); it('makes a cached rollback the Package Store current selection while retaining both immutable releases', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const first = signedGrant(archive, { releaseId: 'release-1' }); const second = signedGrant(archive, { releaseId: 'release-2', signingKeyId: 'test-key-2' }); let current = second; const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { releaseId: current.grant.releaseId, sha256: current.grant.sha256, sizeBytes: current.grant.sizeBytes, })), issueDownload: vi.fn(async () => current.grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, getAccountBinding: () => ACCOUNT_A, keyStore: new Map([['test-key', first.publicKey], ['test-key-2', second.publicKey]]), clientVersion: '1.0.0', now: (() => { let value = 1; return () => value++ * 1_000; })(), }); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); current = first; await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); await expect(store.readInstalledIndex()).resolves.toHaveLength(2); await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: 'release-1' }); const index = JSON.parse(await readFile(path.join(temporaryRoot, 'index.json'), 'utf8')) as { releases: Array<{ release_id: string }>; }; expect(index.releases.map(({ release_id }) => release_id)).toEqual(['release-2', 'release-1']); store.registerActiveWorker('release-1'); await expect(store.uninstall(PLUGIN_ID)).resolves.toMatchObject({ status: 'kept', pluginId: PLUGIN_ID, releaseId: 'release-1', version: '1.0.0', }); await expect(store.readInstalledIndex()).resolves.toHaveLength(1); await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: 'release-1' }); }); it('persists the installed channel and client range, and fails closed after a client upgrade', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const stable = signedGrant(archive, { releaseId: 'release-stable', minMakeloreVersion: '1.0.0', maxMakeloreVersion: '1.5.0' }); let current = stable; const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { releaseId: current.grant.releaseId, sha256: current.grant.sha256, sizeBytes: current.grant.sizeBytes, })), issueDownload: vi.fn(async () => current.grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, getAccountBinding: () => ACCOUNT_A, clientVersion: '1.0.0', keyStore: new Map([['test-key', stable.publicKey]]), }); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0', channel: 'stable' }); const index = JSON.parse(await readFile(path.join(temporaryRoot, 'index.json'), 'utf8')) as { releases: Array>; }; expect(index.releases[0]).toMatchObject({ channel: 'stable', min_makelore_version: '1.0.0', max_makelore_version: '1.5.0', }); await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ channel: 'stable', minMakeloreVersion: '1.0.0', maxMakeloreVersion: '1.5.0', }); const upgradedStore = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, getAccountBinding: () => ACCOUNT_A, clientVersion: '2.0.0', keyStore: new Map([['test-key', stable.publicKey]]), }); await expect(upgradedStore.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ unavailableReason: 'plugin_incompatible_client', }); await expect(upgradedStore.readInstalledIndex()).resolves.toHaveLength(1); }); it('removes only old releases and never guesses a new current selection from installedAt', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const first = signedGrant(archive, { releaseId: 'release-old' }); const second = signedGrant(archive, { releaseId: 'release-current', signingKeyId: 'test-key-2' }); let current = first; const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { releaseId: current.grant.releaseId, sha256: current.grant.sha256, sizeBytes: current.grant.sizeBytes, })), issueDownload: vi.fn(async () => current.grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, getAccountBinding: () => ACCOUNT_A, keyStore: new Map([['test-key', first.publicKey], ['test-key-2', second.publicKey]]), clientVersion: '1.0.0', }); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); current = second; await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); await expect(store.removeUnused(PLUGIN_ID)).resolves.toMatchObject({ status: 'kept', releaseId: 'release-current' }); await expect(store.readInstalledIndex()).resolves.toHaveLength(1); await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: 'release-current' }); await expect(readFile(path.join(temporaryRoot, 'current.json'), 'utf8')).resolves.toContain('release-current'); }); it('explicitly uninstalls every unprotected release without selecting by install time', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const first = signedGrant(archive, { releaseId: 'release-old' }); const second = signedGrant(archive, { releaseId: 'release-current', signingKeyId: 'test-key-2' }); let current = first; const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { releaseId: current.grant.releaseId, sha256: current.grant.sha256, sizeBytes: current.grant.sizeBytes, })), issueDownload: vi.fn(async () => current.grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, getAccountBinding: () => ACCOUNT_A, keyStore: new Map([['test-key', first.publicKey], ['test-key-2', second.publicKey]]), clientVersion: '1.0.0', }); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); current = second; await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); await expect(store.uninstall(PLUGIN_ID)).resolves.toMatchObject({ status: 'removed', pluginId: PLUGIN_ID, releaseId: 'release-current', }); await expect(store.readInstalledIndex()).resolves.toEqual([]); await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull(); await expect(readFile(path.join(temporaryRoot, 'current.json'), 'utf8')).resolves.toContain('"current":{}'); }); it('removes an unprotected current Release while retaining only an active-worker Release', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const protectedRelease = signedGrant(archive, { releaseId: 'release-worker' }); const selectedRelease = signedGrant(archive, { releaseId: 'release-current', signingKeyId: 'test-key-current', }); let current = protectedRelease; const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { releaseId: current.grant.releaseId, sha256: current.grant.sha256, sizeBytes: current.grant.sizeBytes, })), issueDownload: vi.fn(async () => current.grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, getAccountBinding: () => ACCOUNT_A, keyStore: new Map([ ['test-key', protectedRelease.publicKey], ['test-key-current', selectedRelease.publicKey], ]), clientVersion: '1.0.0', }); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); current = selectedRelease; await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); store.registerActiveWorker('release-worker'); await expect(store.uninstall(PLUGIN_ID)).resolves.toEqual({ status: 'kept', pluginId: PLUGIN_ID, reason: 'active_worker_reference', }); await expect(store.readInstalledIndex()).resolves.toEqual([ expect.objectContaining({ releaseId: 'release-worker' }), ]); await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull(); }); it('preserves the old release across download, signature, and extraction failures', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const oldArchive = buildSkillOnlyArchive(); const old = signedGrant(oldArchive, { releaseId: RELEASE_ID }); const oldMarketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: old.grant.sha256, sizeBytes: old.grant.sizeBytes, })), issueDownload: vi.fn(async () => old.grant), downloadContent: async () => oldArchive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const oldStore = new PluginPackageStore({ rootDir: temporaryRoot, marketplace: oldMarketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', old.publicKey]]), getAccountBinding: () => ACCOUNT_A, }); await oldStore.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); const invalidArchive = Buffer.from('not a ZIP archive'); const cases = [ { releaseId: 'release-download-failure', expectedCode: 'plugin_install_failed', artifact: oldArchive, }, { releaseId: 'release-signature-failure', expectedCode: 'plugin_signature_invalid', artifact: oldArchive, }, { releaseId: 'release-extraction-failure', expectedCode: 'plugin_artifact_invalid', artifact: invalidArchive, }, ] as const; for (const [index, scenario] of cases.entries()) { const signed = signedGrant(scenario.artifact, { releaseId: scenario.releaseId }); const grant = scenario.expectedCode === 'plugin_signature_invalid' ? { ...signed.grant, descriptorSignature: signed.grant.descriptorSignature[0] === 'A' ? `B${signed.grant.descriptorSignature.slice(1)}` : `A${signed.grant.descriptorSignature.slice(1)}` } : signed.grant; const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { releaseId: grant.releaseId, sha256: grant.sha256, sizeBytes: grant.sizeBytes, })), issueDownload: vi.fn(async () => grant), downloadContent: async () => { if (index === 0) throw new Error('simulated download interruption'); return scenario.artifact; }, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', signed.publicKey]]), getAccountBinding: () => ACCOUNT_A, }); await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' })) .rejects.toMatchObject({ code: scenario.expectedCode }); await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: RELEASE_ID, version: '1.0.0', }); await expect(store.readInstalledIndex()).resolves.toHaveLength(1); } }); it('rejects a Release outside the MakeLore client range before installation', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const { grant, publicKey } = signedGrant(archive, { maxMakeloreVersion: '1.5.0' }); const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })), issueDownload: vi.fn(async () => grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, clientVersion: '2.0.0', keyStore: new Map([['test-key', publicKey]]), getAccountBinding: () => ACCOUNT_A, }); await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '2.0.0' })) .rejects.toMatchObject({ code: 'plugin_incompatible_client' }); await expect(store.readInstalledIndex()).resolves.toEqual([]); }); it('aborts an in-flight install when the Main account changes', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const { grant, publicKey } = signedGrant(archive); let binding: AccountBinding | null = ACCOUNT_A; const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })), issueDownload: vi.fn(async () => grant), downloadContent: vi.fn(async () => { binding = ACCOUNT_B; return archive; }), getCurrentAccountBinding: () => binding, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, keyStore: new Map([['test-key', publicKey]]), getAccountBinding: () => binding, }); await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' })) .rejects.toMatchObject({ code: 'plugin_account_changed' }); await expect(store.readInstalledIndex()).resolves.toEqual([]); }); it('captures the account before queued package mutations execute', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const { grant, publicKey } = signedGrant(archive); let binding: AccountBinding | null = ACCOUNT_A; const firstResolve = deferred(); const resolve = vi.fn(async (input: ResolveRequest) => ( resolve.mock.calls.length === 1 ? firstResolve.promise : makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes }) )); const marketplace: MarketplaceClient = { resolve, issueDownload: vi.fn(async () => grant), downloadContent: async () => archive, getCurrentAccountBinding: () => binding, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', publicKey]]), getAccountBinding: () => binding, }); const inFlight = store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); await vi.waitFor(() => expect(resolve).toHaveBeenCalledTimes(1)); const queued = store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); binding = ACCOUNT_B; firstResolve.resolve(makeResolveResult({ makeloreVersion: '1.0.0', channel: 'stable' })); await expect(inFlight).rejects.toMatchObject({ code: 'plugin_account_changed' }); await expect(queued).rejects.toMatchObject({ code: 'plugin_account_changed' }); expect(resolve).toHaveBeenCalledTimes(1); await expect(store.readInstalledIndex()).resolves.toEqual([]); }); it('does not let a queued uninstall switch accounts while waiting', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const archive = buildSkillOnlyArchive(); const { grant, publicKey } = signedGrant(archive); let binding: AccountBinding | null = ACCOUNT_A; const resolve = vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })); const marketplace: MarketplaceClient = { resolve, issueDownload: vi.fn(async () => grant), downloadContent: async () => archive, getCurrentAccountBinding: () => binding, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', publicKey]]), getAccountBinding: () => binding, }); await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); const firstResolve = deferred(); resolve.mockImplementationOnce(async () => firstResolve.promise); const inFlight = store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }); await vi.waitFor(() => expect(resolve).toHaveBeenCalledTimes(2)); const queued = store.uninstall(PLUGIN_ID); binding = ACCOUNT_B; firstResolve.resolve(makeResolveResult({ makeloreVersion: '1.0.0', channel: 'stable' }, { action: 'keep', })); await expect(inFlight).rejects.toMatchObject({ code: 'plugin_account_changed' }); await expect(queued).rejects.toMatchObject({ code: 'plugin_account_changed' }); await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: RELEASE_ID }); }); it('rejects archive traversal before materializing a package', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const zip = new AdmZip(); zip.addFile('C:/outside.txt', Buffer.from('outside')); const archive = zip.toBuffer(); const { grant, publicKey } = signedGrant(archive); const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes, })), issueDownload: vi.fn(async () => grant), downloadContent: async () => archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, keyStore: new Map([['test-key', publicKey]]), getAccountBinding: () => ACCOUNT_A, }); await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' })) .rejects.toMatchObject({ code: 'plugin_artifact_invalid' }); await expect(store.readInstalledIndex()).resolves.toEqual([]); }); it('enforces the canonical archive contract before installing a package', async () => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const oversizedManifest = JSON.stringify({ $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', name: PLUGIN_ID, version: '1.0.0', description: 'x'.repeat(256 * 1024), author: { name: 'MakeLore' }, extensions: { 'com.makelore': { capabilityManifest: './com.makelore/capability.json' } }, }); const cases = [ { name: 'unsupported asset extension', archive: buildSkillOnlyArchive({ 'skills/example-skill/assets/run.exe': Buffer.from('not executable') }), }, { name: 'non-UTF-8 Skill text', archive: buildSkillOnlyArchive({ 'skills/example-skill/SKILL.md': Buffer.from([0xff, 0xfe, 0xfd]) }), }, { name: 'non-UTF-8 capability manifest', archive: buildSkillOnlyArchive({ 'com.makelore/capability.json': Buffer.from([0xff, 0xfe, 0xfd]) }), }, { name: 'asset outside a declared Skill', archive: buildSkillOnlyArchive({ 'skills/other-skill/notes.md': '# Hidden\n' }), }, { name: 'oversized manifest', archive: buildSkillOnlyArchive({ 'plugin.json': oversizedManifest }), }, { name: 'oversized Skill', archive: buildSkillOnlyArchive({ 'skills/example-skill/SKILL.md': Buffer.alloc(256 * 1024 + 1, 0x61) }), }, ]; for (const [index, scenario] of cases.entries()) { const { grant, publicKey } = signedGrant(scenario.archive, { releaseId: `release-invalid-${index}` }); const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { releaseId: grant.releaseId, sha256: grant.sha256, sizeBytes: grant.sizeBytes, })), issueDownload: vi.fn(async () => grant), downloadContent: async () => scenario.archive, getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, clientVersion: '1.0.0', keyStore: new Map([['test-key', publicKey]]), getAccountBinding: () => ACCOUNT_A, }); await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }), scenario.name) .rejects.toMatchObject({ code: 'plugin_artifact_invalid' }); await expect(store.readInstalledIndex()).resolves.toEqual([]); } }); it('requires explicit beta selection and rejects content paths outside the server route', async () => { const fetcher = vi.fn().mockResolvedValue(response({ release_admission_id: ADMISSION_ID, release_id: RELEASE_ID, plugin_id: PLUGIN_ID, version: '1.0.0', package_schema_version: 2, contract_version: 1, min_makelore_version: '1.0.0', max_makelore_version: null, size_bytes: 1, sha256: SHA256, signing_key_id: 'test-key', descriptor_signature: 'a'.repeat(86), expires_at: '2026-08-29T00:00:00Z', content_url: 'https://evil.example/archive.zip', })); const client = createMarketplaceClient({ fetchImpl: fetcher, apiBaseUrl: 'https://square.example', getAccessToken: async () => 'token', getAccountBinding: () => ACCOUNT_A, subscribeSession: () => () => undefined, }); await expect(client.issueDownload({ releaseId: RELEASE_ID, releaseAdmissionId: ADMISSION_ID })) .rejects.toMatchObject({ code: 'marketplace_response_invalid' }); const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input)), issueDownload: vi.fn(), } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: await mkdtemp(path.join(process.cwd(), '.marketplace-test-')), marketplace }); await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, channel: 'beta', makeloreVersion: '1.0.0' })) .rejects.toMatchObject({ code: 'plugin_beta_selection_required' }); }); it.each([ 'plugin_release_yanked', 'plugin_incompatible_client', 'plugin_signature_invalid', ] as const)('preserves bounded resolve unavailable code %s for the UI projection', async (reason) => { temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-')); const marketplace: MarketplaceClient = { resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, { action: 'unavailable', releaseId: null, version: null, sha256: null, sizeBytes: null, releaseAdmissionId: null, reason, })), issueDownload: vi.fn(), downloadContent: vi.fn(), getCurrentAccountBinding: () => ACCOUNT_A, } as MarketplaceClient; const store = new PluginPackageStore({ rootDir: temporaryRoot, marketplace, getAccountBinding: () => ACCOUNT_A, clientVersion: '1.0.0', }); await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' })) .rejects.toMatchObject({ code: reason }); }); });