@enactprotocol/trust 2.1.14 → 2.1.17
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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +181 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +402 -0
- package/dist/manifest.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +18 -0
- package/src/manifest.ts +587 -0
- package/tests/manifest.test.ts +371 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
type ChecksumManifest,
|
|
6
|
+
MANIFEST_VERSION,
|
|
7
|
+
computeManifestHash,
|
|
8
|
+
createChecksumManifest,
|
|
9
|
+
parseChecksumManifest,
|
|
10
|
+
serializeChecksumManifest,
|
|
11
|
+
verifyChecksumManifest,
|
|
12
|
+
} from "../src/manifest";
|
|
13
|
+
|
|
14
|
+
const TEST_DIR = join(import.meta.dir, "fixtures", "manifest-test");
|
|
15
|
+
const TOOL_DIR = join(TEST_DIR, "sample-tool");
|
|
16
|
+
|
|
17
|
+
describe("checksum manifest", () => {
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
// Create test directory structure
|
|
20
|
+
mkdirSync(TOOL_DIR, { recursive: true });
|
|
21
|
+
mkdirSync(join(TOOL_DIR, "src"), { recursive: true });
|
|
22
|
+
|
|
23
|
+
// Create sample tool files
|
|
24
|
+
writeFileSync(
|
|
25
|
+
join(TOOL_DIR, "SKILL.md"),
|
|
26
|
+
`---
|
|
27
|
+
name: test/sample-tool
|
|
28
|
+
version: 1.0.0
|
|
29
|
+
description: A sample tool for testing
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Sample Tool
|
|
33
|
+
|
|
34
|
+
This is a test tool.
|
|
35
|
+
`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
writeFileSync(
|
|
39
|
+
join(TOOL_DIR, "src", "main.py"),
|
|
40
|
+
`#!/usr/bin/env python3
|
|
41
|
+
def main():
|
|
42
|
+
print("Hello, World!")
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
|
46
|
+
`
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
writeFileSync(join(TOOL_DIR, "requirements.txt"), "requests>=2.28.0\n");
|
|
50
|
+
|
|
51
|
+
// Create files that should be ignored
|
|
52
|
+
writeFileSync(join(TOOL_DIR, ".gitignore"), "*.pyc\n__pycache__/\n");
|
|
53
|
+
writeFileSync(join(TOOL_DIR, ".DS_Store"), "");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
// Clean up test files
|
|
58
|
+
if (existsSync(TEST_DIR)) {
|
|
59
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("createChecksumManifest", () => {
|
|
64
|
+
test("creates manifest with correct structure", async () => {
|
|
65
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
66
|
+
|
|
67
|
+
expect(manifest.version).toBe(MANIFEST_VERSION);
|
|
68
|
+
expect(manifest.tool.name).toBe("test/sample-tool");
|
|
69
|
+
expect(manifest.tool.version).toBe("1.0.0");
|
|
70
|
+
expect(manifest.files).toBeArray();
|
|
71
|
+
expect(manifest.manifestHash).toBeDefined();
|
|
72
|
+
expect(manifest.manifestHash.algorithm).toBe("sha256");
|
|
73
|
+
expect(manifest.manifestHash.digest).toHaveLength(64); // SHA-256 hex
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("includes expected files", async () => {
|
|
77
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
78
|
+
|
|
79
|
+
const filePaths = manifest.files.map((f) => f.path);
|
|
80
|
+
|
|
81
|
+
expect(filePaths).toContain("SKILL.md");
|
|
82
|
+
expect(filePaths).toContain("src/main.py");
|
|
83
|
+
expect(filePaths).toContain("requirements.txt");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("excludes ignored files", async () => {
|
|
87
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
88
|
+
|
|
89
|
+
const filePaths = manifest.files.map((f) => f.path);
|
|
90
|
+
|
|
91
|
+
// Should not include .gitignore or .DS_Store
|
|
92
|
+
expect(filePaths).not.toContain(".gitignore");
|
|
93
|
+
expect(filePaths).not.toContain(".DS_Store");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("files are sorted by path", async () => {
|
|
97
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
98
|
+
|
|
99
|
+
const paths = manifest.files.map((f) => f.path);
|
|
100
|
+
const sortedPaths = [...paths].sort();
|
|
101
|
+
|
|
102
|
+
expect(paths).toEqual(sortedPaths);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("each file has correct hash format", async () => {
|
|
106
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
107
|
+
|
|
108
|
+
for (const file of manifest.files) {
|
|
109
|
+
expect(file.path).toBeString();
|
|
110
|
+
expect(file.sha256).toHaveLength(64); // SHA-256 hex
|
|
111
|
+
expect(file.sha256).toMatch(/^[a-f0-9]{64}$/);
|
|
112
|
+
expect(file.size).toBeNumber();
|
|
113
|
+
expect(file.size).toBeGreaterThan(0);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("calls progress callback for each file", async () => {
|
|
118
|
+
const processedFiles: string[] = [];
|
|
119
|
+
|
|
120
|
+
await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0", {
|
|
121
|
+
onProgress: (file) => processedFiles.push(file),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(processedFiles.length).toBeGreaterThan(0);
|
|
125
|
+
expect(processedFiles).toContain("SKILL.md");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("respects custom ignore patterns", async () => {
|
|
129
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0", {
|
|
130
|
+
ignorePatterns: ["requirements.txt"],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const filePaths = manifest.files.map((f) => f.path);
|
|
134
|
+
|
|
135
|
+
expect(filePaths).not.toContain("requirements.txt");
|
|
136
|
+
expect(filePaths).toContain("SKILL.md"); // Should still include other files
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("produces deterministic output", async () => {
|
|
140
|
+
const manifest1 = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
141
|
+
|
|
142
|
+
const manifest2 = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
143
|
+
|
|
144
|
+
expect(manifest1.manifestHash.digest).toBe(manifest2.manifestHash.digest);
|
|
145
|
+
expect(manifest1.files).toEqual(manifest2.files);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("computeManifestHash", () => {
|
|
150
|
+
test("computes hash excluding manifestHash field", async () => {
|
|
151
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
152
|
+
|
|
153
|
+
const hash = computeManifestHash(manifest);
|
|
154
|
+
|
|
155
|
+
expect(hash.algorithm).toBe("sha256");
|
|
156
|
+
expect(hash.digest).toHaveLength(64);
|
|
157
|
+
expect(hash.digest).toBe(manifest.manifestHash.digest);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("produces different hashes for different content", async () => {
|
|
161
|
+
const manifest1 = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
162
|
+
|
|
163
|
+
const manifest2 = await createChecksumManifest(
|
|
164
|
+
TOOL_DIR,
|
|
165
|
+
"test/sample-tool",
|
|
166
|
+
"2.0.0" // Different version
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(manifest1.manifestHash.digest).not.toBe(manifest2.manifestHash.digest);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("is deterministic", async () => {
|
|
173
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
174
|
+
|
|
175
|
+
const hash1 = computeManifestHash(manifest);
|
|
176
|
+
const hash2 = computeManifestHash(manifest);
|
|
177
|
+
|
|
178
|
+
expect(hash1.digest).toBe(hash2.digest);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("verifyChecksumManifest", () => {
|
|
183
|
+
test("returns valid for matching directory", async () => {
|
|
184
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
185
|
+
|
|
186
|
+
const result = await verifyChecksumManifest(TOOL_DIR, manifest);
|
|
187
|
+
|
|
188
|
+
expect(result.valid).toBe(true);
|
|
189
|
+
expect(result.errors).toBeUndefined();
|
|
190
|
+
expect(result.missingFiles).toBeUndefined();
|
|
191
|
+
expect(result.modifiedFiles).toBeUndefined();
|
|
192
|
+
expect(result.extraFiles).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("detects modified files", async () => {
|
|
196
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
197
|
+
|
|
198
|
+
// Modify a file
|
|
199
|
+
const originalContent = "requests>=2.28.0\n";
|
|
200
|
+
writeFileSync(join(TOOL_DIR, "requirements.txt"), "modified content\n");
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const result = await verifyChecksumManifest(TOOL_DIR, manifest);
|
|
204
|
+
|
|
205
|
+
expect(result.valid).toBe(false);
|
|
206
|
+
expect(result.modifiedFiles).toContain("requirements.txt");
|
|
207
|
+
expect(result.errors).toBeDefined();
|
|
208
|
+
expect(result.errors?.some((e) => e.includes("Modified file"))).toBe(true);
|
|
209
|
+
} finally {
|
|
210
|
+
// Restore original content
|
|
211
|
+
writeFileSync(join(TOOL_DIR, "requirements.txt"), originalContent);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("detects missing files", async () => {
|
|
216
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
217
|
+
|
|
218
|
+
// Remove a file
|
|
219
|
+
const filePath = join(TOOL_DIR, "requirements.txt");
|
|
220
|
+
const originalContent = "requests>=2.28.0\n";
|
|
221
|
+
rmSync(filePath);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const result = await verifyChecksumManifest(TOOL_DIR, manifest);
|
|
225
|
+
|
|
226
|
+
expect(result.valid).toBe(false);
|
|
227
|
+
expect(result.missingFiles).toContain("requirements.txt");
|
|
228
|
+
expect(result.errors).toBeDefined();
|
|
229
|
+
} finally {
|
|
230
|
+
// Restore file
|
|
231
|
+
writeFileSync(filePath, originalContent);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("detects extra files", async () => {
|
|
236
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
237
|
+
|
|
238
|
+
// Add a new file
|
|
239
|
+
const newFilePath = join(TOOL_DIR, "extra-file.txt");
|
|
240
|
+
writeFileSync(newFilePath, "extra content");
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const result = await verifyChecksumManifest(TOOL_DIR, manifest);
|
|
244
|
+
|
|
245
|
+
expect(result.valid).toBe(false);
|
|
246
|
+
expect(result.extraFiles).toContain("extra-file.txt");
|
|
247
|
+
expect(result.errors).toBeDefined();
|
|
248
|
+
} finally {
|
|
249
|
+
// Remove extra file
|
|
250
|
+
rmSync(newFilePath);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("detects corrupted manifest hash", async () => {
|
|
255
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
256
|
+
|
|
257
|
+
// Corrupt the manifest hash
|
|
258
|
+
const corruptedManifest: ChecksumManifest = {
|
|
259
|
+
...manifest,
|
|
260
|
+
manifestHash: {
|
|
261
|
+
algorithm: "sha256",
|
|
262
|
+
digest: "0".repeat(64), // Invalid hash
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = await verifyChecksumManifest(TOOL_DIR, corruptedManifest);
|
|
267
|
+
|
|
268
|
+
expect(result.valid).toBe(false);
|
|
269
|
+
expect(result.errors?.some((e) => e.includes("Manifest hash mismatch"))).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("parseChecksumManifest", () => {
|
|
274
|
+
test("parses valid manifest JSON", async () => {
|
|
275
|
+
const original = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
276
|
+
|
|
277
|
+
const json = serializeChecksumManifest(original);
|
|
278
|
+
const parsed = parseChecksumManifest(json);
|
|
279
|
+
|
|
280
|
+
expect(parsed.version).toBe(original.version);
|
|
281
|
+
expect(parsed.tool).toEqual(original.tool);
|
|
282
|
+
expect(parsed.files).toEqual(original.files);
|
|
283
|
+
expect(parsed.manifestHash).toEqual(original.manifestHash);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("throws on invalid version", () => {
|
|
287
|
+
const invalidJson = JSON.stringify({
|
|
288
|
+
version: "99.0",
|
|
289
|
+
tool: { name: "test", version: "1.0.0" },
|
|
290
|
+
files: [],
|
|
291
|
+
manifestHash: { algorithm: "sha256", digest: "abc" },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(() => parseChecksumManifest(invalidJson)).toThrow("Invalid manifest version");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("throws on missing tool info", () => {
|
|
298
|
+
const invalidJson = JSON.stringify({
|
|
299
|
+
version: "1.0",
|
|
300
|
+
files: [],
|
|
301
|
+
manifestHash: { algorithm: "sha256", digest: "abc" },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(() => parseChecksumManifest(invalidJson)).toThrow("missing tool name");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("throws on invalid files array", () => {
|
|
308
|
+
const invalidJson = JSON.stringify({
|
|
309
|
+
version: "1.0",
|
|
310
|
+
tool: { name: "test", version: "1.0.0" },
|
|
311
|
+
files: "not an array",
|
|
312
|
+
manifestHash: { algorithm: "sha256", digest: "abc" },
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(() => parseChecksumManifest(invalidJson)).toThrow("files must be an array");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("throws on missing manifestHash", () => {
|
|
319
|
+
const invalidJson = JSON.stringify({
|
|
320
|
+
version: "1.0",
|
|
321
|
+
tool: { name: "test", version: "1.0.0" },
|
|
322
|
+
files: [],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(() => parseChecksumManifest(invalidJson)).toThrow("missing manifestHash");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("serializeChecksumManifest", () => {
|
|
330
|
+
test("produces valid JSON", async () => {
|
|
331
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
332
|
+
|
|
333
|
+
const json = serializeChecksumManifest(manifest);
|
|
334
|
+
|
|
335
|
+
expect(() => JSON.parse(json)).not.toThrow();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("produces pretty-printed output", async () => {
|
|
339
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
340
|
+
|
|
341
|
+
const json = serializeChecksumManifest(manifest);
|
|
342
|
+
|
|
343
|
+
// Should contain newlines (pretty-printed)
|
|
344
|
+
expect(json).toContain("\n");
|
|
345
|
+
expect(json).toContain(" "); // 2-space indent
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("round-trips correctly", async () => {
|
|
349
|
+
const original = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
350
|
+
|
|
351
|
+
const json = serializeChecksumManifest(original);
|
|
352
|
+
const parsed = parseChecksumManifest(json);
|
|
353
|
+
|
|
354
|
+
expect(parsed).toEqual(original);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("cross-platform path handling", () => {
|
|
359
|
+
test("uses forward slashes in file paths", async () => {
|
|
360
|
+
const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
|
|
361
|
+
|
|
362
|
+
for (const file of manifest.files) {
|
|
363
|
+
expect(file.path).not.toContain("\\");
|
|
364
|
+
// Should use forward slashes even on Windows
|
|
365
|
+
if (file.path.includes("/")) {
|
|
366
|
+
expect(file.path).toMatch(/^[^\\]+$/);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|