@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/LICENSE +190 -0
- package/README.md +210 -0
- package/dist/client.d.ts +74 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.integration.test.d.ts +11 -0
- package/dist/client.integration.test.d.ts.map +1 -0
- package/dist/client.integration.test.js +121 -0
- package/dist/client.integration.test.js.map +1 -0
- package/dist/client.js +283 -0
- package/dist/client.js.map +1 -0
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/client.test.js +344 -0
- package/dist/client.test.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.test.d.ts +2 -0
- package/dist/errors.test.d.ts.map +1 -0
- package/dist/errors.test.js +136 -0
- package/dist/errors.test.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +65 -0
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 @@
|
|
|
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
|