diff --git a/packages/sdk-typescript/package.json b/packages/sdk-typescript/package.json index 97056ad..9e34a43 100644 --- a/packages/sdk-typescript/package.json +++ b/packages/sdk-typescript/package.json @@ -61,11 +61,11 @@ "node": ">=22" }, "dependencies": { - "@nimblebrain/mpak-schemas": "workspace:*", - "jszip": "^3.10.1" + "@nimblebrain/mpak-schemas": "workspace:*" }, "devDependencies": { "@types/node": "^22.0.0", + "jszip": "^3.10.1", "tsup": "^8.4.0", "typescript": "^5.7.0", "vitest": "^3.0.0" diff --git a/packages/sdk-typescript/src/client.ts b/packages/sdk-typescript/src/client.ts index 49af3ab..25748d7 100644 --- a/packages/sdk-typescript/src/client.ts +++ b/packages/sdk-typescript/src/client.ts @@ -1,22 +1,18 @@ +import type { BundleSearchResponse, SkillSearchResponse } from '@nimblebrain/mpak-schemas'; import { createHash } from 'crypto'; +import { MpakIntegrityError, MpakNetworkError, MpakNotFoundError } from './errors.js'; import type { - MpakClientConfig, - BundleSearchParams, BundleDetailResponse, - BundleVersionsResponse, - BundleVersionResponse, BundleDownloadResponse, + BundleSearchParams, + BundleVersionResponse, + BundleVersionsResponse, + MpakClientConfig, + Platform, SkillDetailResponse, SkillDownloadResponse, SkillSearchParams, - Platform, - SkillReference, - GithubSkillReference, - UrlSkillReference, - ResolvedSkill, } from './types.js'; -import type { BundleSearchResponse, SkillSearchResponse } from '@nimblebrain/mpak-schemas'; -import { MpakNotFoundError, MpakIntegrityError, MpakNetworkError } from './errors.js'; const DEFAULT_REGISTRY_URL = 'https://registry.mpak.dev'; const DEFAULT_TIMEOUT = 30000; @@ -25,7 +21,6 @@ const DEFAULT_TIMEOUT = 30000; * Client for interacting with the mpak registry * * Requires Node.js 18+ for native fetch support. - * Uses jszip for skill bundle extraction. */ export class MpakClient { private readonly registryUrl: string; @@ -262,214 +257,79 @@ export class MpakClient { return response.json() as Promise; } - /** - * Download skill content and verify integrity - * - * @throws {MpakIntegrityError} If expectedSha256 is provided and doesn't match (fail-closed) - */ - async downloadSkillContent( - downloadUrl: string, - expectedSha256?: string, - ): Promise<{ content: string; verified: boolean }> { - const response = await this.fetchWithTimeout(downloadUrl); - - if (!response.ok) { - throw new MpakNetworkError(`Failed to download skill: HTTP ${response.status}`); - } - - const content = await response.text(); - - if (expectedSha256) { - const actualHash = this.computeSha256(content); - if (actualHash !== expectedSha256) { - throw new MpakIntegrityError(expectedSha256, actualHash); - } - return { content, verified: true }; - } - - return { content, verified: false }; - } + // =========================================================================== + // Download Methods + // =========================================================================== /** - * Resolve a skill reference to actual content + * Download content from a URL and verify its SHA-256 integrity. * - * Supports mpak, github, and url sources. This is the main method for - * fetching skill content from any supported source. - * - * @throws {MpakNotFoundError} If skill not found - * @throws {MpakIntegrityError} If integrity check fails (fail-closed) + * @throws {MpakIntegrityError} If SHA-256 doesn't match * @throws {MpakNetworkError} For network failures - * - * @example - * ```typescript - * // Resolve from mpak registry - * const skill = await client.resolveSkillRef({ - * source: 'mpak', - * name: '@nimblebraininc/folk-crm', - * version: '1.3.0', - * }); - * - * // Resolve from GitHub - * const skill = await client.resolveSkillRef({ - * source: 'github', - * name: '@example/my-skill', - * version: 'v1.0.0', - * repo: 'owner/repo', - * path: 'skills/my-skill/SKILL.md', - * }); - * - * // Resolve from URL - * const skill = await client.resolveSkillRef({ - * source: 'url', - * name: '@example/custom', - * version: '1.0.0', - * url: 'https://example.com/skill.md', - * }); - * ``` */ - async resolveSkillRef(ref: SkillReference): Promise { - switch (ref.source) { - case 'mpak': - return this.resolveMpakSkill(ref); - case 'github': - return this.resolveGithubSkill(ref); - case 'url': - return this.resolveUrlSkill(ref); - default: { - const _exhaustive: never = ref; - throw new Error(`Unknown skill source: ${(_exhaustive as SkillReference).source}`); - } - } - } - - /** - * Resolve a skill from mpak registry - * - * The API returns a ZIP bundle containing SKILL.md and metadata. - */ - private async resolveMpakSkill(ref: SkillReference & { source: 'mpak' }): Promise { - const url = `${this.registryUrl}/v1/skills/${ref.name}/versions/${ref.version}/download`; - - const response = await this.fetchWithTimeout(url); - - if (response.status === 404) { - throw new MpakNotFoundError(`${ref.name}@${ref.version}`); - } - - if (!response.ok) { - throw new MpakNetworkError(`Failed to fetch skill: HTTP ${response.status}`); - } - - // Response is a ZIP file - extract SKILL.md - const zipBuffer = await response.arrayBuffer(); - const content = await this.extractSkillFromZip(zipBuffer, ref.name); - - if (ref.integrity) { - this.verifyIntegrityOrThrow(content, ref.integrity); - return { content, version: ref.version, source: 'mpak', verified: true }; - } - - return { content, version: ref.version, source: 'mpak', verified: false }; - } - - /** - * Resolve a skill from GitHub releases - */ - private async resolveGithubSkill(ref: GithubSkillReference): Promise { - const url = `https://github.com/${ref.repo}/releases/download/${ref.version}/${ref.path}`; + async downloadContent(url: string, sha256: string): Promise { const response = await this.fetchWithTimeout(url); if (!response.ok) { - throw new MpakNotFoundError(`github:${ref.repo}/${ref.path}@${ref.version}`); + throw new MpakNetworkError(`Failed to download: HTTP ${response.status}`); } - const content = await response.text(); + const downloadedRawData = new Uint8Array(await response.arrayBuffer()); - if (ref.integrity) { - this.verifyIntegrityOrThrow(content, ref.integrity); - return { - content, - version: ref.version, - source: 'github', - verified: true, - }; + const computedHash = this.computeSha256(downloadedRawData); + if (computedHash !== sha256) { + throw new MpakIntegrityError(sha256, computedHash); } - return { - content, - version: ref.version, - source: 'github', - verified: false, - }; + return downloadedRawData; } /** - * Resolve a skill from a direct URL + * Download a bundle by name, with optional version and platform. + * Defaults to latest version and auto-detected platform. + * + * @throws {MpakNotFoundError} If bundle not found + * @throws {MpakIntegrityError} If SHA-256 doesn't match + * @throws {MpakNetworkError} For network failures */ - private async resolveUrlSkill(ref: UrlSkillReference): Promise { - const response = await this.fetchWithTimeout(ref.url); - - if (!response.ok) { - throw new MpakNotFoundError(`url:${ref.url}`); - } - - const content = await response.text(); - - if (ref.integrity) { - this.verifyIntegrityOrThrow(content, ref.integrity); - return { content, version: ref.version, source: 'url', verified: true }; - } - - return { content, version: ref.version, source: 'url', verified: false }; - } + async downloadBundle( + name: string, + version?: string, + platform?: Platform, + ): Promise<{ + data: Uint8Array; + metadata: BundleDownloadResponse['bundle']; + }> { + const resolvedPlatform = platform ?? MpakClient.detectPlatform(); + const resolvedVersion = version ?? 'latest'; - /** - * Extract SKILL.md content from a skill bundle ZIP - */ - private async extractSkillFromZip(zipBuffer: ArrayBuffer, skillName: string): Promise { - const JSZip = (await import('jszip')).default; - const zip = await JSZip.loadAsync(zipBuffer); - - // Skill name format: @scope/name -> folder is just 'name' - const folderName = skillName.split('/').pop() ?? skillName; - const skillPath = `${folderName}/SKILL.md`; - - const skillFile = zip.file(skillPath); - if (!skillFile) { - // Try without folder prefix - const altFile = zip.file('SKILL.md'); - if (!altFile) { - throw new MpakNotFoundError(`SKILL.md not found in bundle for ${skillName}`); - } - return altFile.async('string'); - } + const downloadInfo = await this.getBundleDownload(name, resolvedVersion, resolvedPlatform); + const data = await this.downloadContent(downloadInfo.url, downloadInfo.bundle.sha256); - return skillFile.async('string'); + return { data, metadata: downloadInfo.bundle }; } /** - * Verify content integrity and throw if mismatch (fail-closed) + * Download a skill bundle by name, with optional version. + * Defaults to latest version. + * + * @throws {MpakNotFoundError} If skill not found + * @throws {MpakIntegrityError} If SHA-256 doesn't match + * @throws {MpakNetworkError} For network failures */ - private verifyIntegrityOrThrow(content: string, integrity: string): void { - const expectedHash = this.extractHash(integrity); - const actualHash = this.computeSha256(content); + async downloadSkillBundle( + name: string, + version?: string, + ): Promise<{ + data: Uint8Array; + metadata: SkillDownloadResponse['skill']; + }> { + const resolvedVersion = version ?? 'latest'; - if (actualHash !== expectedHash) { - throw new MpakIntegrityError(expectedHash, actualHash); - } - } + const downloadInfo = await this.getSkillVersionDownload(name, resolvedVersion); + const data = await this.downloadContent(downloadInfo.url, downloadInfo.skill.sha256); - /** - * Extract hash from integrity string (removes prefix) - */ - private extractHash(integrity: string): string { - if (integrity.startsWith('sha256:')) { - return integrity.slice(7); - } - if (integrity.startsWith('sha256-')) { - return integrity.slice(7); - } - return integrity; + return { data, metadata: downloadInfo.skill }; } // =========================================================================== @@ -516,8 +376,8 @@ export class MpakClient { /** * Compute SHA256 hash of content */ - private computeSha256(content: string): string { - return createHash('sha256').update(content, 'utf8').digest('hex'); + private computeSha256(content: string | Uint8Array): string { + return createHash('sha256').update(content).digest('hex'); } /** diff --git a/packages/sdk-typescript/src/index.ts b/packages/sdk-typescript/src/index.ts index d6c9a10..86d19b3 100644 --- a/packages/sdk-typescript/src/index.ts +++ b/packages/sdk-typescript/src/index.ts @@ -7,27 +7,18 @@ * * @example * ```typescript - * import { MpakClient, SkillReference } from '@nimblebrain/mpak-sdk'; + * import { MpakClient } from '@nimblebrain/mpak-sdk'; * * const client = new MpakClient(); * * // Search for bundles * const bundles = await client.searchBundles({ q: 'mcp' }); * - * // Get bundle details - * const bundle = await client.getBundle('@nimbletools/echo'); + * // Download a bundle (latest version, auto-detected platform) + * const { data, metadata } = await client.downloadBundle('@nimbletools/echo'); * - * // Search for skills - * const skills = await client.searchSkills({ q: 'crm' }); - * - * // Resolve a skill reference to content (recommended) - * const ref: SkillReference = { - * source: 'mpak', - * name: '@nimblebraininc/folk-crm', - * version: '1.3.0', - * }; - * const resolved = await client.resolveSkillRef(ref); - * console.log(resolved.content); // Skill markdown content + * // Download a skill bundle + * const { data, metadata } = await client.downloadSkillBundle('@nimblebraininc/folk-crm'); * ``` */ @@ -62,12 +53,6 @@ export type { SkillDetail, SkillDownloadInfo, SkillVersion, - // Skill reference types (for resolveSkillRef) - SkillReference, - MpakSkillReference, - GithubSkillReference, - UrlSkillReference, - ResolvedSkill, } from './types.js'; // Re-export SkillSearchResponse from schemas diff --git a/packages/sdk-typescript/src/types.ts b/packages/sdk-typescript/src/types.ts index 7f23368..fd13f74 100644 --- a/packages/sdk-typescript/src/types.ts +++ b/packages/sdk-typescript/src/types.ts @@ -146,65 +146,3 @@ export interface MpakClientConfig { */ userAgent?: string; } - -// ============================================================================= -// Skill Reference Types (for resolveSkillRef) -// ============================================================================= - -/** - * Base fields shared by all skill reference types - */ -interface SkillReferenceBase { - /** Skill artifact identifier (e.g., '@nimbletools/folk-crm') */ - name: string; - /** Semver version (e.g., '1.0.0') or 'latest' */ - version: string; - /** SHA256 integrity hash (format: 'sha256-hexdigest') */ - integrity?: string; -} - -/** - * Skill reference from mpak registry - */ -export interface MpakSkillReference extends SkillReferenceBase { - source: 'mpak'; -} - -/** - * Skill reference from GitHub repository - */ -export interface GithubSkillReference extends SkillReferenceBase { - source: 'github'; - /** GitHub repository (owner/repo) */ - repo: string; - /** Path to skill file in repo */ - path: string; -} - -/** - * Skill reference from direct URL - */ -export interface UrlSkillReference extends SkillReferenceBase { - source: 'url'; - /** Direct download URL */ - url: string; -} - -/** - * Discriminated union of skill reference types - */ -export type SkillReference = MpakSkillReference | GithubSkillReference | UrlSkillReference; - -/** - * Result of resolving a skill reference - */ -export interface ResolvedSkill { - /** The markdown content of the skill */ - content: string; - /** Version that was resolved */ - version: string; - /** Source the skill was fetched from */ - source: 'mpak' | 'github' | 'url'; - /** Whether integrity was verified */ - verified: boolean; -} diff --git a/packages/sdk-typescript/tests/client.integration.test.ts b/packages/sdk-typescript/tests/client.integration.test.ts index 3c5745b..d827ed4 100644 --- a/packages/sdk-typescript/tests/client.integration.test.ts +++ b/packages/sdk-typescript/tests/client.integration.test.ts @@ -14,8 +14,9 @@ import { describe, it, expect } from 'vitest'; import { MpakClient } from '../src/client.js'; import { MpakNotFoundError } from '../src/errors.js'; -// Known bundle that exists in the registry +// Known bundle and skill that exist in the registry const KNOWN_BUNDLE = '@nimblebraininc/echo'; +const KNOWN_SKILL = '@nimblebraininc/ipinfo'; describe('MpakClient Integration Tests', () => { const client = new MpakClient(); @@ -119,6 +120,17 @@ describe('MpakClient Integration Tests', () => { expect(manifest.server).toBeDefined(); }); + it('downloads bundle with verified integrity', async () => { + const { data, metadata } = await client.downloadBundle(KNOWN_BUNDLE); + + expect(data).toBeInstanceOf(Uint8Array); + expect(data.byteLength).toBeGreaterThan(0); + expect(metadata.name).toBe(KNOWN_BUNDLE); + expect(metadata.version).toBeDefined(); + expect(metadata.sha256).toBeDefined(); + expect(metadata.size).toBeGreaterThan(0); + }); + it('throws MpakNotFoundError for nonexistent bundle', async () => { await expect(client.getBundle('@nonexistent/bundle-that-does-not-exist')).rejects.toThrow( MpakNotFoundError, @@ -145,6 +157,17 @@ describe('MpakClient Integration Tests', () => { expect(result.pagination).toBeDefined(); }); + it('downloads skill bundle with verified integrity', async () => { + const { data, metadata } = await client.downloadSkillBundle(KNOWN_SKILL); + + expect(data).toBeInstanceOf(Uint8Array); + expect(data.byteLength).toBeGreaterThan(0); + expect(metadata.name).toBe(KNOWN_SKILL); + expect(metadata.version).toBeDefined(); + expect(metadata.sha256).toBeDefined(); + expect(metadata.size).toBeGreaterThan(0); + }); + it('throws MpakNotFoundError for nonexistent skill', async () => { await expect(client.getSkill('@nonexistent/skill-that-does-not-exist')).rejects.toThrow( MpakNotFoundError, diff --git a/packages/sdk-typescript/tests/client.test.ts b/packages/sdk-typescript/tests/client.test.ts index ea616a1..c81fed7 100644 --- a/packages/sdk-typescript/tests/client.test.ts +++ b/packages/sdk-typescript/tests/client.test.ts @@ -4,8 +4,8 @@ import { MpakClient } from '../src/client.js'; import { MpakNotFoundError, MpakIntegrityError, MpakNetworkError } from '../src/errors.js'; // Helper to compute SHA256 hash (same as client implementation) -function sha256(content: string): string { - return createHash('sha256').update(content, 'utf8').digest('hex'); +function sha256(content: string | Uint8Array): string { + return createHash('sha256').update(content).digest('hex'); } // Helper to create a mock Response @@ -17,6 +17,20 @@ function mockResponse( return { text: () => Promise.resolve(bodyStr), json: () => Promise.resolve(typeof body === 'string' ? JSON.parse(body) : body), + arrayBuffer: () => Promise.resolve(Buffer.from(bodyStr).buffer), + status: init.status ?? 200, + ok: init.ok ?? (init.status === undefined || init.status < 400), + } as Response; +} + +// Helper to create a mock binary Response +function mockBinaryResponse( + data: Uint8Array, + init: { status?: number; ok?: boolean } = {}, +): Response { + return { + arrayBuffer: () => + Promise.resolve(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)), status: init.status ?? 200, ok: init.ok ?? (init.status === undefined || init.status < 400), } as Response; @@ -371,57 +385,168 @@ describe('MpakClient', () => { }); }); - describe('downloadSkillContent', () => { - it('downloads content without verification', async () => { + describe('downloadContent', () => { + it('downloads and verifies SHA-256', async () => { const client = new MpakClient(); - const content = '# My Skill\n\nSkill content here'; - fetchMock.mockResolvedValueOnce(mockResponse(content)); + const content = new TextEncoder().encode('bundle binary data'); + const hash = sha256(content); + fetchMock.mockResolvedValueOnce(mockBinaryResponse(content)); - const result = await client.downloadSkillContent('https://example.com/skill.skill'); + const result = await client.downloadContent('https://example.com/file.mcpb', hash); - expect(result.content).toBe(content); - expect(result.verified).toBe(false); + expect(result).toBeInstanceOf(Uint8Array); + expect(new TextDecoder().decode(result)).toBe('bundle binary data'); }); - it('verifies integrity when hash provided', async () => { + it('throws MpakIntegrityError on SHA-256 mismatch', async () => { const client = new MpakClient(); - const content = 'skill content'; - const hash = sha256(content); - fetchMock.mockResolvedValueOnce(mockResponse(content)); + const content = new TextEncoder().encode('some data'); + fetchMock.mockResolvedValueOnce(mockBinaryResponse(content)); - const result = await client.downloadSkillContent('https://example.com/skill.skill', hash); - - expect(result.content).toBe(content); - expect(result.verified).toBe(true); + await expect( + client.downloadContent('https://example.com/file.mcpb', 'wrong_hash'), + ).rejects.toThrow(MpakIntegrityError); }); - it('throws MpakIntegrityError on hash mismatch (fail-closed)', async () => { + it('throws MpakNetworkError on fetch failure', async () => { const client = new MpakClient(); - const content = 'actual content'; - fetchMock.mockResolvedValueOnce(mockResponse(content)); + fetchMock.mockResolvedValueOnce( + mockBinaryResponse(new Uint8Array(0), { status: 500, ok: false }), + ); await expect( - client.downloadSkillContent('https://example.com/skill.skill', 'wrong_hash'), - ).rejects.toThrow(MpakIntegrityError); + client.downloadContent('https://example.com/file.mcpb', 'anyhash'), + ).rejects.toThrow(MpakNetworkError); + }); + }); + + describe('downloadBundle', () => { + const bundleContent = new TextEncoder().encode('fake mcpb bundle'); + const bundleHash = sha256(bundleContent); + const downloadInfoResponse = { + url: 'https://storage.example.com/bundle.mcpb', + bundle: { + name: '@test/bundle', + version: '1.0.0', + platform: { os: 'darwin', arch: 'arm64' }, + sha256: bundleHash, + size: bundleContent.length, + }, + expires_at: '2024-01-02T00:00:00Z', + }; + + it('resolves download info and returns verified buffer + metadata', async () => { + const client = new MpakClient(); + fetchMock + .mockResolvedValueOnce(mockResponse(downloadInfoResponse)) + .mockResolvedValueOnce(mockBinaryResponse(bundleContent)); + + const result = await client.downloadBundle('@test/bundle', '1.0.0', { + os: 'darwin', + arch: 'arm64', + }); + + expect(result.data).toBeInstanceOf(Uint8Array); + expect(new TextDecoder().decode(result.data)).toBe('fake mcpb bundle'); + expect(result.metadata.name).toBe('@test/bundle'); + expect(result.metadata.version).toBe('1.0.0'); + expect(result.metadata.sha256).toBe(bundleHash); }); - it('does not return content when integrity fails', async () => { + it('defaults version to latest and auto-detects platform', async () => { const client = new MpakClient(); - const secretContent = 'sensitive skill content'; - fetchMock.mockResolvedValueOnce(mockResponse(secretContent)); - - let leakedContent: string | undefined; - try { - const result = await client.downloadSkillContent( - 'https://example.com/skill.skill', - 'wrong_hash', - ); - leakedContent = result.content; - } catch { - // Expected - } - - expect(leakedContent).toBeUndefined(); + fetchMock + .mockResolvedValueOnce(mockResponse(downloadInfoResponse)) + .mockResolvedValueOnce(mockBinaryResponse(bundleContent)); + + await client.downloadBundle('@test/bundle'); + + const calledUrl = fetchMock.mock.calls[0]?.[0] as string; + expect(calledUrl).toContain('/versions/latest/download'); + expect(calledUrl).toContain('os='); + expect(calledUrl).toContain('arch='); + }); + + it('propagates MpakNotFoundError from getBundleDownload', async () => { + const client = new MpakClient(); + fetchMock.mockResolvedValueOnce(mockResponse('', { status: 404 })); + + await expect(client.downloadBundle('@test/nonexistent')).rejects.toThrow(MpakNotFoundError); + }); + + it('propagates MpakIntegrityError on SHA-256 mismatch', async () => { + const client = new MpakClient(); + const tampered = new TextEncoder().encode('tampered content'); + fetchMock + .mockResolvedValueOnce(mockResponse(downloadInfoResponse)) + .mockResolvedValueOnce(mockBinaryResponse(tampered)); + + await expect(client.downloadBundle('@test/bundle', '1.0.0')).rejects.toThrow( + MpakIntegrityError, + ); + }); + }); + + describe('downloadSkillBundle', () => { + const skillContent = new TextEncoder().encode('fake skill bundle'); + const skillHash = sha256(skillContent); + const skillDownloadInfoResponse = { + url: 'https://storage.example.com/skill.skill', + skill: { + name: '@test/skill', + version: '1.0.0', + sha256: skillHash, + size: skillContent.length, + }, + expires_at: '2024-01-02T00:00:00Z', + }; + + it('resolves download info and returns verified buffer + metadata', async () => { + const client = new MpakClient(); + fetchMock + .mockResolvedValueOnce(mockResponse(skillDownloadInfoResponse)) + .mockResolvedValueOnce(mockBinaryResponse(skillContent)); + + const result = await client.downloadSkillBundle('@test/skill', '1.0.0'); + + expect(result.data).toBeInstanceOf(Uint8Array); + expect(new TextDecoder().decode(result.data)).toBe('fake skill bundle'); + expect(result.metadata.name).toBe('@test/skill'); + expect(result.metadata.version).toBe('1.0.0'); + expect(result.metadata.sha256).toBe(skillHash); + }); + + it('defaults version to latest', async () => { + const client = new MpakClient(); + fetchMock + .mockResolvedValueOnce(mockResponse(skillDownloadInfoResponse)) + .mockResolvedValueOnce(mockBinaryResponse(skillContent)); + + await client.downloadSkillBundle('@test/skill'); + + const calledUrl = fetchMock.mock.calls[0]?.[0] as string; + expect(calledUrl).toContain('/versions/latest/download'); + }); + + it('propagates MpakNotFoundError from getSkillVersionDownload', async () => { + const client = new MpakClient(); + fetchMock.mockResolvedValueOnce(mockResponse('', { status: 404 })); + + await expect(client.downloadSkillBundle('@test/nonexistent')).rejects.toThrow( + MpakNotFoundError, + ); + }); + + it('propagates MpakIntegrityError on SHA-256 mismatch', async () => { + const client = new MpakClient(); + const tampered = new TextEncoder().encode('tampered content'); + fetchMock + .mockResolvedValueOnce(mockResponse(skillDownloadInfoResponse)) + .mockResolvedValueOnce(mockBinaryResponse(tampered)); + + await expect(client.downloadSkillBundle('@test/skill', '1.0.0')).rejects.toThrow( + MpakIntegrityError, + ); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40d5eb3..0d1d31f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -288,13 +288,13 @@ importers: '@nimblebrain/mpak-schemas': specifier: workspace:* version: link:../schemas - jszip: - specifier: ^3.10.1 - version: 3.10.1 devDependencies: '@types/node': specifier: ^22.0.0 version: 22.19.10 + jszip: + specifier: ^3.10.1 + version: 3.10.1 tsup: specifier: ^8.4.0 version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)