@nimblebrain/mpak-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js ADDED
@@ -0,0 +1,283 @@
1
+ import { createHash } from 'crypto';
2
+ import { MpakNotFoundError, MpakIntegrityError, MpakNetworkError, } from './errors.js';
3
+ const DEFAULT_REGISTRY_URL = 'https://api.mpak.dev';
4
+ const DEFAULT_TIMEOUT = 30000;
5
+ /**
6
+ * Client for interacting with the mpak registry
7
+ *
8
+ * Zero runtime dependencies - uses native fetch and crypto only.
9
+ * Requires Node.js 18+ for native fetch support.
10
+ */
11
+ export class MpakClient {
12
+ registryUrl;
13
+ timeout;
14
+ constructor(config = {}) {
15
+ this.registryUrl = config.registryUrl ?? DEFAULT_REGISTRY_URL;
16
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
17
+ }
18
+ // ===========================================================================
19
+ // Bundle API
20
+ // ===========================================================================
21
+ /**
22
+ * Search for bundles
23
+ */
24
+ async searchBundles(params = {}) {
25
+ const searchParams = new URLSearchParams();
26
+ if (params.q)
27
+ searchParams.set('q', params.q);
28
+ if (params.type)
29
+ searchParams.set('type', params.type);
30
+ if (params.sort)
31
+ searchParams.set('sort', params.sort);
32
+ if (params.limit)
33
+ searchParams.set('limit', String(params.limit));
34
+ if (params.offset)
35
+ searchParams.set('offset', String(params.offset));
36
+ const queryString = searchParams.toString();
37
+ const url = `${this.registryUrl}/v1/bundles/search${queryString ? `?${queryString}` : ''}`;
38
+ const response = await this.fetchWithTimeout(url);
39
+ if (!response.ok) {
40
+ throw new MpakNetworkError(`Failed to search bundles: HTTP ${response.status}`);
41
+ }
42
+ return response.json();
43
+ }
44
+ /**
45
+ * Get bundle details
46
+ */
47
+ async getBundle(name) {
48
+ this.validateScopedName(name);
49
+ const url = `${this.registryUrl}/v1/bundles/${name}`;
50
+ const response = await this.fetchWithTimeout(url);
51
+ if (response.status === 404) {
52
+ throw new MpakNotFoundError(name);
53
+ }
54
+ if (!response.ok) {
55
+ throw new MpakNetworkError(`Failed to get bundle: HTTP ${response.status}`);
56
+ }
57
+ return response.json();
58
+ }
59
+ /**
60
+ * Get all versions of a bundle
61
+ */
62
+ async getBundleVersions(name) {
63
+ this.validateScopedName(name);
64
+ const url = `${this.registryUrl}/v1/bundles/${name}/versions`;
65
+ const response = await this.fetchWithTimeout(url);
66
+ if (response.status === 404) {
67
+ throw new MpakNotFoundError(name);
68
+ }
69
+ if (!response.ok) {
70
+ throw new MpakNetworkError(`Failed to get bundle versions: HTTP ${response.status}`);
71
+ }
72
+ return response.json();
73
+ }
74
+ /**
75
+ * Get a specific version of a bundle
76
+ */
77
+ async getBundleVersion(name, version) {
78
+ this.validateScopedName(name);
79
+ const url = `${this.registryUrl}/v1/bundles/${name}/versions/${version}`;
80
+ const response = await this.fetchWithTimeout(url);
81
+ if (response.status === 404) {
82
+ throw new MpakNotFoundError(`${name}@${version}`);
83
+ }
84
+ if (!response.ok) {
85
+ throw new MpakNetworkError(`Failed to get bundle version: HTTP ${response.status}`);
86
+ }
87
+ return response.json();
88
+ }
89
+ /**
90
+ * Get download info for a bundle
91
+ */
92
+ async getBundleDownload(name, version, platform) {
93
+ this.validateScopedName(name);
94
+ const params = new URLSearchParams();
95
+ if (platform) {
96
+ params.set('os', platform.os);
97
+ params.set('arch', platform.arch);
98
+ }
99
+ const queryString = params.toString();
100
+ const url = `${this.registryUrl}/v1/bundles/${name}/versions/${version}/download${queryString ? `?${queryString}` : ''}`;
101
+ const response = await this.fetchWithTimeout(url, {
102
+ headers: { Accept: 'application/json' },
103
+ });
104
+ if (response.status === 404) {
105
+ throw new MpakNotFoundError(`${name}@${version}`);
106
+ }
107
+ if (!response.ok) {
108
+ throw new MpakNetworkError(`Failed to get bundle download: HTTP ${response.status}`);
109
+ }
110
+ return response.json();
111
+ }
112
+ // ===========================================================================
113
+ // Skill API
114
+ // ===========================================================================
115
+ /**
116
+ * Search for skills
117
+ */
118
+ async searchSkills(params = {}) {
119
+ const searchParams = new URLSearchParams();
120
+ if (params.q)
121
+ searchParams.set('q', params.q);
122
+ if (params.tags)
123
+ searchParams.set('tags', params.tags);
124
+ if (params.category)
125
+ searchParams.set('category', params.category);
126
+ if (params.surface)
127
+ searchParams.set('surface', params.surface);
128
+ if (params.sort)
129
+ searchParams.set('sort', params.sort);
130
+ if (params.limit)
131
+ searchParams.set('limit', String(params.limit));
132
+ if (params.offset)
133
+ searchParams.set('offset', String(params.offset));
134
+ const queryString = searchParams.toString();
135
+ const url = `${this.registryUrl}/v1/skills/search${queryString ? `?${queryString}` : ''}`;
136
+ const response = await this.fetchWithTimeout(url);
137
+ if (!response.ok) {
138
+ throw new MpakNetworkError(`Failed to search skills: HTTP ${response.status}`);
139
+ }
140
+ return response.json();
141
+ }
142
+ /**
143
+ * Get skill details
144
+ */
145
+ async getSkill(name) {
146
+ this.validateScopedName(name);
147
+ const url = `${this.registryUrl}/v1/skills/${name}`;
148
+ const response = await this.fetchWithTimeout(url);
149
+ if (response.status === 404) {
150
+ throw new MpakNotFoundError(name);
151
+ }
152
+ if (!response.ok) {
153
+ throw new MpakNetworkError(`Failed to get skill: HTTP ${response.status}`);
154
+ }
155
+ return response.json();
156
+ }
157
+ /**
158
+ * Get download info for a skill (latest version)
159
+ */
160
+ async getSkillDownload(name) {
161
+ this.validateScopedName(name);
162
+ const url = `${this.registryUrl}/v1/skills/${name}/download`;
163
+ const response = await this.fetchWithTimeout(url, {
164
+ headers: { Accept: 'application/json' },
165
+ });
166
+ if (response.status === 404) {
167
+ throw new MpakNotFoundError(name);
168
+ }
169
+ if (!response.ok) {
170
+ throw new MpakNetworkError(`Failed to get skill download: HTTP ${response.status}`);
171
+ }
172
+ return response.json();
173
+ }
174
+ /**
175
+ * Get download info for a specific skill version
176
+ */
177
+ async getSkillVersionDownload(name, version) {
178
+ this.validateScopedName(name);
179
+ const url = `${this.registryUrl}/v1/skills/${name}/versions/${version}/download`;
180
+ const response = await this.fetchWithTimeout(url, {
181
+ headers: { Accept: 'application/json' },
182
+ });
183
+ if (response.status === 404) {
184
+ throw new MpakNotFoundError(`${name}@${version}`);
185
+ }
186
+ if (!response.ok) {
187
+ throw new MpakNetworkError(`Failed to get skill download: HTTP ${response.status}`);
188
+ }
189
+ return response.json();
190
+ }
191
+ /**
192
+ * Download skill content and verify integrity
193
+ *
194
+ * @throws {MpakIntegrityError} If expectedSha256 is provided and doesn't match (fail-closed)
195
+ */
196
+ async downloadSkillContent(downloadUrl, expectedSha256) {
197
+ const response = await this.fetchWithTimeout(downloadUrl);
198
+ if (!response.ok) {
199
+ throw new MpakNetworkError(`Failed to download skill: HTTP ${response.status}`);
200
+ }
201
+ const content = await response.text();
202
+ if (expectedSha256) {
203
+ const actualHash = this.computeSha256(content);
204
+ if (actualHash !== expectedSha256) {
205
+ throw new MpakIntegrityError(expectedSha256, actualHash);
206
+ }
207
+ return { content, verified: true };
208
+ }
209
+ return { content, verified: false };
210
+ }
211
+ // ===========================================================================
212
+ // Utility Methods
213
+ // ===========================================================================
214
+ /**
215
+ * Detect the current platform
216
+ */
217
+ static detectPlatform() {
218
+ const nodePlatform = process.platform;
219
+ const nodeArch = process.arch;
220
+ let os;
221
+ switch (nodePlatform) {
222
+ case 'darwin':
223
+ os = 'darwin';
224
+ break;
225
+ case 'win32':
226
+ os = 'win32';
227
+ break;
228
+ case 'linux':
229
+ os = 'linux';
230
+ break;
231
+ default:
232
+ os = 'any';
233
+ }
234
+ let arch;
235
+ switch (nodeArch) {
236
+ case 'x64':
237
+ arch = 'x64';
238
+ break;
239
+ case 'arm64':
240
+ arch = 'arm64';
241
+ break;
242
+ default:
243
+ arch = 'any';
244
+ }
245
+ return { os, arch };
246
+ }
247
+ /**
248
+ * Compute SHA256 hash of content
249
+ */
250
+ computeSha256(content) {
251
+ return createHash('sha256').update(content, 'utf8').digest('hex');
252
+ }
253
+ /**
254
+ * Validate that a name is scoped (@scope/name)
255
+ */
256
+ validateScopedName(name) {
257
+ if (!name.startsWith('@')) {
258
+ throw new Error('Package name must be scoped (e.g., @scope/package-name)');
259
+ }
260
+ }
261
+ /**
262
+ * Fetch with timeout support
263
+ */
264
+ async fetchWithTimeout(url, init) {
265
+ const controller = new AbortController();
266
+ const timeoutId = setTimeout(() => {
267
+ controller.abort();
268
+ }, this.timeout);
269
+ try {
270
+ return await fetch(url, { ...init, signal: controller.signal });
271
+ }
272
+ catch (error) {
273
+ if (error instanceof Error && error.name === 'AbortError') {
274
+ throw new MpakNetworkError(`Request timeout after ${this.timeout}ms`);
275
+ }
276
+ throw new MpakNetworkError(error instanceof Error ? error.message : 'Network error');
277
+ }
278
+ finally {
279
+ clearTimeout(timeoutId);
280
+ }
281
+ }
282
+ }
283
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../src/client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,344 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createHash } from 'crypto';
3
+ import { MpakClient } from './client.js';
4
+ import { MpakNotFoundError, MpakIntegrityError, MpakNetworkError, } from './errors.js';
5
+ // Helper to compute SHA256 hash (same as client implementation)
6
+ function sha256(content) {
7
+ return createHash('sha256').update(content, 'utf8').digest('hex');
8
+ }
9
+ // Helper to create a mock Response
10
+ function mockResponse(body, init = {}) {
11
+ const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
12
+ return {
13
+ text: () => Promise.resolve(bodyStr),
14
+ json: () => Promise.resolve(typeof body === 'string' ? JSON.parse(body) : body),
15
+ status: init.status ?? 200,
16
+ ok: init.ok ?? (init.status === undefined || init.status < 400),
17
+ };
18
+ }
19
+ describe('MpakClient', () => {
20
+ let fetchMock;
21
+ beforeEach(() => {
22
+ fetchMock = vi.fn();
23
+ vi.stubGlobal('fetch', fetchMock);
24
+ });
25
+ afterEach(() => {
26
+ vi.unstubAllGlobals();
27
+ });
28
+ describe('constructor', () => {
29
+ it('uses default registry URL when not specified', async () => {
30
+ const client = new MpakClient();
31
+ fetchMock.mockResolvedValueOnce(mockResponse({ bundles: [], total: 0, pagination: {} }));
32
+ await client.searchBundles();
33
+ expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('https://api.mpak.dev'), expect.any(Object));
34
+ });
35
+ it('uses custom registry URL when specified', async () => {
36
+ const client = new MpakClient({
37
+ registryUrl: 'https://custom.registry.com',
38
+ });
39
+ fetchMock.mockResolvedValueOnce(mockResponse({ bundles: [], total: 0, pagination: {} }));
40
+ await client.searchBundles();
41
+ expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('https://custom.registry.com'), expect.any(Object));
42
+ });
43
+ });
44
+ describe('searchBundles', () => {
45
+ it('returns search results', async () => {
46
+ const client = new MpakClient();
47
+ const searchResponse = {
48
+ bundles: [
49
+ { name: '@test/bundle-1', latest_version: '1.0.0', downloads: 100, published_at: '2024-01-01', verified: true },
50
+ ],
51
+ total: 1,
52
+ pagination: { limit: 20, offset: 0, has_more: false },
53
+ };
54
+ fetchMock.mockResolvedValueOnce(mockResponse(searchResponse));
55
+ const result = await client.searchBundles({ q: 'test' });
56
+ expect(result.bundles).toHaveLength(1);
57
+ expect(result.total).toBe(1);
58
+ });
59
+ it('passes query parameters correctly', async () => {
60
+ const client = new MpakClient();
61
+ fetchMock.mockResolvedValueOnce(mockResponse({ bundles: [], total: 0, pagination: {} }));
62
+ await client.searchBundles({
63
+ q: 'mcp',
64
+ type: 'python',
65
+ sort: 'downloads',
66
+ limit: 10,
67
+ offset: 5,
68
+ });
69
+ const calledUrl = fetchMock.mock.calls[0]?.[0];
70
+ expect(calledUrl).toContain('q=mcp');
71
+ expect(calledUrl).toContain('type=python');
72
+ expect(calledUrl).toContain('sort=downloads');
73
+ expect(calledUrl).toContain('limit=10');
74
+ expect(calledUrl).toContain('offset=5');
75
+ });
76
+ it('calls /v1/bundles/search endpoint', async () => {
77
+ const client = new MpakClient();
78
+ fetchMock.mockResolvedValueOnce(mockResponse({ bundles: [], total: 0, pagination: {} }));
79
+ await client.searchBundles();
80
+ const calledUrl = fetchMock.mock.calls[0]?.[0];
81
+ expect(calledUrl).toContain('/v1/bundles/search');
82
+ });
83
+ });
84
+ describe('getBundle', () => {
85
+ it('returns bundle details', async () => {
86
+ const client = new MpakClient();
87
+ const bundleResponse = {
88
+ name: '@test/bundle',
89
+ latest_version: '1.0.0',
90
+ downloads: 100,
91
+ published_at: '2024-01-01',
92
+ verified: true,
93
+ versions: [],
94
+ };
95
+ fetchMock.mockResolvedValueOnce(mockResponse(bundleResponse));
96
+ const result = await client.getBundle('@test/bundle');
97
+ expect(result.name).toBe('@test/bundle');
98
+ });
99
+ it('throws error for unscoped name', async () => {
100
+ const client = new MpakClient();
101
+ await expect(client.getBundle('invalid-name')).rejects.toThrow('Package name must be scoped');
102
+ });
103
+ it('throws MpakNotFoundError on 404', async () => {
104
+ const client = new MpakClient();
105
+ fetchMock.mockResolvedValueOnce(mockResponse('', { status: 404 }));
106
+ await expect(client.getBundle('@test/nonexistent')).rejects.toThrow(MpakNotFoundError);
107
+ });
108
+ });
109
+ describe('getBundleVersions', () => {
110
+ it('returns versions list', async () => {
111
+ const client = new MpakClient();
112
+ const versionsResponse = {
113
+ name: '@test/bundle',
114
+ latest: '1.0.0',
115
+ versions: [
116
+ { version: '1.0.0', artifacts_count: 1, platforms: [], published_at: '2024-01-01', downloads: 50, publish_method: null },
117
+ ],
118
+ };
119
+ fetchMock.mockResolvedValueOnce(mockResponse(versionsResponse));
120
+ const result = await client.getBundleVersions('@test/bundle');
121
+ expect(result.versions).toHaveLength(1);
122
+ expect(result.latest).toBe('1.0.0');
123
+ });
124
+ });
125
+ describe('getBundleDownload', () => {
126
+ it('returns download info with URL', async () => {
127
+ const client = new MpakClient();
128
+ const downloadResponse = {
129
+ url: 'https://storage.example.com/bundle.mcpb',
130
+ bundle: {
131
+ name: '@test/bundle',
132
+ version: '1.0.0',
133
+ platform: { os: 'darwin', arch: 'arm64' },
134
+ sha256: 'abc123',
135
+ size: 12345,
136
+ },
137
+ expires_at: '2024-01-02T00:00:00Z',
138
+ };
139
+ fetchMock.mockResolvedValueOnce(mockResponse(downloadResponse));
140
+ const result = await client.getBundleDownload('@test/bundle', '1.0.0');
141
+ expect(result.url).toBe('https://storage.example.com/bundle.mcpb');
142
+ expect(result.bundle.sha256).toBe('abc123');
143
+ });
144
+ it('passes platform parameters', async () => {
145
+ const client = new MpakClient();
146
+ fetchMock.mockResolvedValueOnce(mockResponse({
147
+ url: 'https://example.com',
148
+ bundle: { name: '@test/bundle', version: '1.0.0', platform: {}, sha256: '', size: 0 },
149
+ }));
150
+ await client.getBundleDownload('@test/bundle', '1.0.0', { os: 'linux', arch: 'x64' });
151
+ const calledUrl = fetchMock.mock.calls[0]?.[0];
152
+ expect(calledUrl).toContain('os=linux');
153
+ expect(calledUrl).toContain('arch=x64');
154
+ });
155
+ });
156
+ describe('searchSkills', () => {
157
+ it('returns search results', async () => {
158
+ const client = new MpakClient();
159
+ const searchResponse = {
160
+ skills: [
161
+ { name: '@test/skill-1', description: 'Test skill', latest_version: '1.0.0', downloads: 50, published_at: '2024-01-01' },
162
+ ],
163
+ total: 1,
164
+ pagination: { limit: 20, offset: 0, has_more: false },
165
+ };
166
+ fetchMock.mockResolvedValueOnce(mockResponse(searchResponse));
167
+ const result = await client.searchSkills({ q: 'test' });
168
+ expect(result.skills).toHaveLength(1);
169
+ expect(result.total).toBe(1);
170
+ });
171
+ it('passes all query parameters', async () => {
172
+ const client = new MpakClient();
173
+ fetchMock.mockResolvedValueOnce(mockResponse({ skills: [], total: 0, pagination: {} }));
174
+ await client.searchSkills({
175
+ q: 'crm',
176
+ tags: 'sales,contacts',
177
+ category: 'development',
178
+ surface: 'claude-code',
179
+ sort: 'recent',
180
+ limit: 10,
181
+ offset: 5,
182
+ });
183
+ const calledUrl = fetchMock.mock.calls[0]?.[0];
184
+ expect(calledUrl).toContain('q=crm');
185
+ expect(calledUrl).toContain('tags=sales%2Ccontacts');
186
+ expect(calledUrl).toContain('category=development');
187
+ expect(calledUrl).toContain('surface=claude-code');
188
+ expect(calledUrl).toContain('sort=recent');
189
+ });
190
+ it('calls /v1/skills/search endpoint', async () => {
191
+ const client = new MpakClient();
192
+ fetchMock.mockResolvedValueOnce(mockResponse({ skills: [], total: 0, pagination: {} }));
193
+ await client.searchSkills();
194
+ const calledUrl = fetchMock.mock.calls[0]?.[0];
195
+ expect(calledUrl).toContain('/v1/skills/search');
196
+ });
197
+ });
198
+ describe('getSkill', () => {
199
+ it('returns skill details', async () => {
200
+ const client = new MpakClient();
201
+ const skillResponse = {
202
+ name: '@test/skill',
203
+ description: 'A test skill',
204
+ latest_version: '1.0.0',
205
+ downloads: 100,
206
+ published_at: '2024-01-01',
207
+ versions: [],
208
+ };
209
+ fetchMock.mockResolvedValueOnce(mockResponse(skillResponse));
210
+ const result = await client.getSkill('@test/skill');
211
+ expect(result.name).toBe('@test/skill');
212
+ expect(result.description).toBe('A test skill');
213
+ });
214
+ it('throws MpakNotFoundError on 404', async () => {
215
+ const client = new MpakClient();
216
+ fetchMock.mockResolvedValueOnce(mockResponse('', { status: 404 }));
217
+ await expect(client.getSkill('@test/nonexistent')).rejects.toThrow(MpakNotFoundError);
218
+ });
219
+ });
220
+ describe('getSkillDownload', () => {
221
+ it('returns download info', async () => {
222
+ const client = new MpakClient();
223
+ const downloadResponse = {
224
+ url: 'https://storage.example.com/skill.skill',
225
+ skill: {
226
+ name: '@test/skill',
227
+ version: '1.0.0',
228
+ sha256: 'abc123def456',
229
+ size: 1024,
230
+ },
231
+ expires_at: '2024-01-02T00:00:00Z',
232
+ };
233
+ fetchMock.mockResolvedValueOnce(mockResponse(downloadResponse));
234
+ const result = await client.getSkillDownload('@test/skill');
235
+ expect(result.url).toBe('https://storage.example.com/skill.skill');
236
+ expect(result.skill.sha256).toBe('abc123def456');
237
+ });
238
+ });
239
+ describe('getSkillVersionDownload', () => {
240
+ it('returns download info for specific version', async () => {
241
+ const client = new MpakClient();
242
+ const downloadResponse = {
243
+ url: 'https://storage.example.com/skill-v1.skill',
244
+ skill: {
245
+ name: '@test/skill',
246
+ version: '1.0.0',
247
+ sha256: 'version1hash',
248
+ size: 1024,
249
+ },
250
+ expires_at: '2024-01-02T00:00:00Z',
251
+ };
252
+ fetchMock.mockResolvedValueOnce(mockResponse(downloadResponse));
253
+ const result = await client.getSkillVersionDownload('@test/skill', '1.0.0');
254
+ expect(result.skill.version).toBe('1.0.0');
255
+ });
256
+ it('calls correct versioned endpoint', async () => {
257
+ const client = new MpakClient();
258
+ fetchMock.mockResolvedValueOnce(mockResponse({
259
+ url: 'https://example.com',
260
+ skill: { name: '@test/skill', version: '2.0.0', sha256: '', size: 0 },
261
+ expires_at: '',
262
+ }));
263
+ await client.getSkillVersionDownload('@test/skill', '2.0.0');
264
+ const calledUrl = fetchMock.mock.calls[0]?.[0];
265
+ expect(calledUrl).toContain('/versions/2.0.0/download');
266
+ });
267
+ });
268
+ describe('downloadSkillContent', () => {
269
+ it('downloads content without verification', async () => {
270
+ const client = new MpakClient();
271
+ const content = '# My Skill\n\nSkill content here';
272
+ fetchMock.mockResolvedValueOnce(mockResponse(content));
273
+ const result = await client.downloadSkillContent('https://example.com/skill.skill');
274
+ expect(result.content).toBe(content);
275
+ expect(result.verified).toBe(false);
276
+ });
277
+ it('verifies integrity when hash provided', async () => {
278
+ const client = new MpakClient();
279
+ const content = 'skill content';
280
+ const hash = sha256(content);
281
+ fetchMock.mockResolvedValueOnce(mockResponse(content));
282
+ const result = await client.downloadSkillContent('https://example.com/skill.skill', hash);
283
+ expect(result.content).toBe(content);
284
+ expect(result.verified).toBe(true);
285
+ });
286
+ it('throws MpakIntegrityError on hash mismatch (fail-closed)', async () => {
287
+ const client = new MpakClient();
288
+ const content = 'actual content';
289
+ fetchMock.mockResolvedValueOnce(mockResponse(content));
290
+ await expect(client.downloadSkillContent('https://example.com/skill.skill', 'wrong_hash')).rejects.toThrow(MpakIntegrityError);
291
+ });
292
+ it('does not return content when integrity fails', async () => {
293
+ const client = new MpakClient();
294
+ const secretContent = 'sensitive skill content';
295
+ fetchMock.mockResolvedValueOnce(mockResponse(secretContent));
296
+ let leakedContent;
297
+ try {
298
+ const result = await client.downloadSkillContent('https://example.com/skill.skill', 'wrong_hash');
299
+ leakedContent = result.content;
300
+ }
301
+ catch {
302
+ // Expected
303
+ }
304
+ expect(leakedContent).toBeUndefined();
305
+ });
306
+ });
307
+ describe('detectPlatform', () => {
308
+ it('returns current platform', () => {
309
+ const platform = MpakClient.detectPlatform();
310
+ expect(platform).toHaveProperty('os');
311
+ expect(platform).toHaveProperty('arch');
312
+ expect(['darwin', 'linux', 'win32', 'any']).toContain(platform.os);
313
+ expect(['x64', 'arm64', 'any']).toContain(platform.arch);
314
+ });
315
+ });
316
+ describe('timeout handling', () => {
317
+ it('throws MpakNetworkError on timeout', async () => {
318
+ const client = new MpakClient({ timeout: 100 });
319
+ fetchMock.mockImplementationOnce(() => new Promise((_, reject) => {
320
+ setTimeout(() => {
321
+ const error = new Error('AbortError');
322
+ error.name = 'AbortError';
323
+ reject(error);
324
+ }, 50);
325
+ }));
326
+ await expect(client.searchBundles()).rejects.toThrow(MpakNetworkError);
327
+ });
328
+ it('includes timeout duration in error message', async () => {
329
+ const client = new MpakClient({ timeout: 5000 });
330
+ fetchMock.mockImplementationOnce(() => {
331
+ const error = new Error('AbortError');
332
+ error.name = 'AbortError';
333
+ return Promise.reject(error);
334
+ });
335
+ await expect(client.searchBundles()).rejects.toThrow('5000ms');
336
+ });
337
+ it('wraps generic fetch errors as MpakNetworkError', async () => {
338
+ const client = new MpakClient();
339
+ fetchMock.mockRejectedValueOnce(new Error('ECONNREFUSED'));
340
+ await expect(client.searchBundles()).rejects.toThrow(MpakNetworkError);
341
+ });
342
+ });
343
+ });
344
+ //# sourceMappingURL=client.test.js.map