@vellumai/credential-executor 0.4.55
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/Dockerfile +55 -0
- package/bun.lock +37 -0
- package/package.json +32 -0
- package/src/__tests__/command-executor.test.ts +1333 -0
- package/src/__tests__/command-validator.test.ts +708 -0
- package/src/__tests__/command-workspace.test.ts +997 -0
- package/src/__tests__/grant-store.test.ts +467 -0
- package/src/__tests__/http-executor.test.ts +1251 -0
- package/src/__tests__/http-policy.test.ts +970 -0
- package/src/__tests__/local-materializers.test.ts +826 -0
- package/src/__tests__/managed-materializers.test.ts +961 -0
- package/src/__tests__/toolstore.test.ts +539 -0
- package/src/__tests__/transport.test.ts +388 -0
- package/src/audit/store.ts +188 -0
- package/src/commands/auth-adapters.ts +169 -0
- package/src/commands/executor.ts +840 -0
- package/src/commands/output-scan.ts +157 -0
- package/src/commands/profiles.ts +282 -0
- package/src/commands/validator.ts +438 -0
- package/src/commands/workspace.ts +512 -0
- package/src/grants/index.ts +17 -0
- package/src/grants/persistent-store.ts +247 -0
- package/src/grants/rpc-handlers.ts +269 -0
- package/src/grants/temporary-store.ts +219 -0
- package/src/http/audit.ts +84 -0
- package/src/http/executor.ts +540 -0
- package/src/http/path-template.ts +179 -0
- package/src/http/policy.ts +256 -0
- package/src/http/response-filter.ts +233 -0
- package/src/index.ts +106 -0
- package/src/main.ts +263 -0
- package/src/managed-main.ts +420 -0
- package/src/materializers/local.ts +300 -0
- package/src/materializers/managed-platform.ts +270 -0
- package/src/paths.ts +137 -0
- package/src/server.ts +636 -0
- package/src/subjects/local.ts +177 -0
- package/src/subjects/managed.ts +290 -0
- package/src/toolstore/integrity.ts +94 -0
- package/src/toolstore/manifest.ts +154 -0
- package/src/toolstore/publish.ts +342 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type SecureCommandManifest,
|
|
8
|
+
MANIFEST_SCHEMA_VERSION,
|
|
9
|
+
EgressMode,
|
|
10
|
+
} from "../commands/profiles.js";
|
|
11
|
+
import { AuthAdapterType } from "../commands/auth-adapters.js";
|
|
12
|
+
import { computeDigest, verifyDigest } from "../toolstore/integrity.js";
|
|
13
|
+
import {
|
|
14
|
+
isValidSha256Hex,
|
|
15
|
+
validateSourceUrl,
|
|
16
|
+
isWorkspaceOriginPath,
|
|
17
|
+
} from "../toolstore/manifest.js";
|
|
18
|
+
import {
|
|
19
|
+
publishBundle,
|
|
20
|
+
readPublishedManifest,
|
|
21
|
+
isBundlePublished,
|
|
22
|
+
type PublishRequest,
|
|
23
|
+
} from "../toolstore/publish.js";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Test fixtures
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** Sample bundle bytes (just some arbitrary content for testing). */
|
|
30
|
+
const SAMPLE_BUNDLE_BYTES = Buffer.from(
|
|
31
|
+
"#!/usr/bin/env bash\necho hello\n",
|
|
32
|
+
"utf-8",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** The correct SHA-256 digest of SAMPLE_BUNDLE_BYTES. */
|
|
36
|
+
const SAMPLE_BUNDLE_DIGEST = computeDigest(SAMPLE_BUNDLE_BYTES);
|
|
37
|
+
|
|
38
|
+
/** A different set of bytes to test digest mismatches. */
|
|
39
|
+
const TAMPERED_BUNDLE_BYTES = Buffer.from(
|
|
40
|
+
"#!/usr/bin/env bash\nrm -rf /\n",
|
|
41
|
+
"utf-8",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a minimal valid SecureCommandManifest for testing.
|
|
46
|
+
*/
|
|
47
|
+
function buildSecureManifest(
|
|
48
|
+
overrides: Partial<SecureCommandManifest> = {},
|
|
49
|
+
): SecureCommandManifest {
|
|
50
|
+
return {
|
|
51
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
52
|
+
bundleDigest: SAMPLE_BUNDLE_DIGEST,
|
|
53
|
+
bundleId: "test-cli",
|
|
54
|
+
version: "1.0.0",
|
|
55
|
+
entrypoint: "bin/test-cli",
|
|
56
|
+
commandProfiles: {
|
|
57
|
+
"read-data": {
|
|
58
|
+
description: "Read-only data access",
|
|
59
|
+
allowedArgvPatterns: [
|
|
60
|
+
{ name: "list", tokens: ["list", "<resource>"] },
|
|
61
|
+
],
|
|
62
|
+
deniedSubcommands: ["admin"],
|
|
63
|
+
allowedNetworkTargets: [
|
|
64
|
+
{ hostPattern: "api.example.com", protocols: ["https"] },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
authAdapter: {
|
|
69
|
+
type: AuthAdapterType.EnvVar,
|
|
70
|
+
envVarName: "TEST_TOKEN",
|
|
71
|
+
},
|
|
72
|
+
egressMode: EgressMode.ProxyRequired,
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a valid PublishRequest for testing.
|
|
79
|
+
*/
|
|
80
|
+
function buildPublishRequest(
|
|
81
|
+
overrides: Partial<PublishRequest> = {},
|
|
82
|
+
): PublishRequest {
|
|
83
|
+
return {
|
|
84
|
+
bundleBytes: SAMPLE_BUNDLE_BYTES,
|
|
85
|
+
expectedDigest: SAMPLE_BUNDLE_DIGEST,
|
|
86
|
+
bundleId: "test-cli",
|
|
87
|
+
version: "1.0.0",
|
|
88
|
+
sourceUrl: "https://releases.example.com/test-cli/v1.0.0/bundle.tar.gz",
|
|
89
|
+
secureCommandManifest: buildSecureManifest(),
|
|
90
|
+
...overrides,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Temp directory management for publisher tests
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
let testTmpDir: string;
|
|
99
|
+
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
testTmpDir = join(
|
|
102
|
+
tmpdir(),
|
|
103
|
+
`ces-toolstore-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
104
|
+
);
|
|
105
|
+
mkdirSync(testTmpDir, { recursive: true });
|
|
106
|
+
|
|
107
|
+
// Point CES data root to the temp directory so tests are isolated
|
|
108
|
+
process.env["BASE_DATA_DIR"] = testTmpDir;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
try {
|
|
113
|
+
rmSync(testTmpDir, { recursive: true, force: true });
|
|
114
|
+
} catch {
|
|
115
|
+
// Best effort cleanup
|
|
116
|
+
}
|
|
117
|
+
delete process.env["BASE_DATA_DIR"];
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Integrity: computeDigest / verifyDigest
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("integrity", () => {
|
|
125
|
+
test("computeDigest returns a 64-character hex string", () => {
|
|
126
|
+
const digest = computeDigest(SAMPLE_BUNDLE_BYTES);
|
|
127
|
+
expect(digest).toHaveLength(64);
|
|
128
|
+
expect(/^[a-f0-9]{64}$/.test(digest)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("computeDigest is deterministic", () => {
|
|
132
|
+
const d1 = computeDigest(SAMPLE_BUNDLE_BYTES);
|
|
133
|
+
const d2 = computeDigest(SAMPLE_BUNDLE_BYTES);
|
|
134
|
+
expect(d1).toBe(d2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("computeDigest differs for different inputs", () => {
|
|
138
|
+
const d1 = computeDigest(SAMPLE_BUNDLE_BYTES);
|
|
139
|
+
const d2 = computeDigest(TAMPERED_BUNDLE_BYTES);
|
|
140
|
+
expect(d1).not.toBe(d2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("verifyDigest succeeds when digest matches", () => {
|
|
144
|
+
const result = verifyDigest(SAMPLE_BUNDLE_BYTES, SAMPLE_BUNDLE_DIGEST);
|
|
145
|
+
expect(result.valid).toBe(true);
|
|
146
|
+
expect(result.computedDigest).toBe(SAMPLE_BUNDLE_DIGEST);
|
|
147
|
+
expect(result.expectedDigest).toBe(SAMPLE_BUNDLE_DIGEST);
|
|
148
|
+
expect(result.error).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("verifyDigest fails when digest does not match", () => {
|
|
152
|
+
const wrongDigest = computeDigest(TAMPERED_BUNDLE_BYTES);
|
|
153
|
+
const result = verifyDigest(SAMPLE_BUNDLE_BYTES, wrongDigest);
|
|
154
|
+
expect(result.valid).toBe(false);
|
|
155
|
+
expect(result.computedDigest).toBe(SAMPLE_BUNDLE_DIGEST);
|
|
156
|
+
expect(result.expectedDigest).toBe(wrongDigest);
|
|
157
|
+
expect(result.error).toContain("Digest mismatch");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("verifyDigest fails for invalid hex digest", () => {
|
|
161
|
+
const result = verifyDigest(SAMPLE_BUNDLE_BYTES, "not-a-valid-hex-digest");
|
|
162
|
+
expect(result.valid).toBe(false);
|
|
163
|
+
expect(result.error).toContain("Digest mismatch");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("verifyDigest fails for truncated digest", () => {
|
|
167
|
+
const truncated = SAMPLE_BUNDLE_DIGEST.slice(0, 32);
|
|
168
|
+
const result = verifyDigest(SAMPLE_BUNDLE_BYTES, truncated);
|
|
169
|
+
expect(result.valid).toBe(false);
|
|
170
|
+
expect(result.error).toContain("Digest mismatch");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Manifest validation helpers
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
describe("manifest validation helpers", () => {
|
|
179
|
+
describe("isValidSha256Hex", () => {
|
|
180
|
+
test("accepts valid 64-char hex digest", () => {
|
|
181
|
+
expect(isValidSha256Hex(SAMPLE_BUNDLE_DIGEST)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("accepts all-zero digest", () => {
|
|
185
|
+
expect(isValidSha256Hex("0".repeat(64))).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("rejects too-short digest", () => {
|
|
189
|
+
expect(isValidSha256Hex("abc123")).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("rejects too-long digest", () => {
|
|
193
|
+
expect(isValidSha256Hex("a".repeat(65))).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("rejects uppercase hex", () => {
|
|
197
|
+
expect(isValidSha256Hex("A".repeat(64))).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("rejects non-hex characters", () => {
|
|
201
|
+
expect(isValidSha256Hex("g".repeat(64))).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("rejects empty string", () => {
|
|
205
|
+
expect(isValidSha256Hex("")).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("validateSourceUrl", () => {
|
|
210
|
+
test("accepts valid HTTPS URL", () => {
|
|
211
|
+
expect(
|
|
212
|
+
validateSourceUrl("https://releases.example.com/v1/bundle.tar.gz"),
|
|
213
|
+
).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("rejects empty string", () => {
|
|
217
|
+
const err = validateSourceUrl("");
|
|
218
|
+
expect(err).not.toBeNull();
|
|
219
|
+
expect(err).toContain("required");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("rejects file:// URL", () => {
|
|
223
|
+
const err = validateSourceUrl("file:///tmp/bundle.tar.gz");
|
|
224
|
+
expect(err).not.toBeNull();
|
|
225
|
+
expect(err).toContain("file:");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("rejects data: URL", () => {
|
|
229
|
+
const err = validateSourceUrl("data:application/octet-stream;base64,AA==");
|
|
230
|
+
expect(err).not.toBeNull();
|
|
231
|
+
expect(err).toContain("data:");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("rejects HTTP URL (not HTTPS)", () => {
|
|
235
|
+
const err = validateSourceUrl("http://example.com/bundle.tar.gz");
|
|
236
|
+
expect(err).not.toBeNull();
|
|
237
|
+
expect(err).toContain("HTTPS");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("rejects non-URL string", () => {
|
|
241
|
+
const err = validateSourceUrl("/usr/local/bin/my-tool");
|
|
242
|
+
expect(err).not.toBeNull();
|
|
243
|
+
expect(err).toContain("not a valid URL");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("isWorkspaceOriginPath", () => {
|
|
248
|
+
test("detects .vellum/ paths", () => {
|
|
249
|
+
expect(isWorkspaceOriginPath("~/.vellum/workspace/tools/my-tool")).toBe(
|
|
250
|
+
true,
|
|
251
|
+
);
|
|
252
|
+
expect(isWorkspaceOriginPath(".vellum/workspace/tools/my-tool")).toBe(
|
|
253
|
+
true,
|
|
254
|
+
);
|
|
255
|
+
expect(
|
|
256
|
+
isWorkspaceOriginPath("/home/user/.vellum/workspace/tools/my-tool"),
|
|
257
|
+
).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("detects workspace paths", () => {
|
|
261
|
+
expect(isWorkspaceOriginPath("/some/path/workspace/tools/my-tool")).toBe(
|
|
262
|
+
true,
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("allows non-workspace paths", () => {
|
|
267
|
+
expect(isWorkspaceOriginPath("/usr/local/bin/gh")).toBe(false);
|
|
268
|
+
expect(isWorkspaceOriginPath("/opt/tools/my-cli")).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Publisher: digest mismatch rejection
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
describe("publishBundle — digest mismatch rejection", () => {
|
|
278
|
+
test("rejects bundle whose bytes do not match expectedDigest", () => {
|
|
279
|
+
const result = publishBundle(
|
|
280
|
+
buildPublishRequest({
|
|
281
|
+
bundleBytes: TAMPERED_BUNDLE_BYTES,
|
|
282
|
+
// expectedDigest is for SAMPLE_BUNDLE_BYTES, not TAMPERED_BUNDLE_BYTES
|
|
283
|
+
expectedDigest: SAMPLE_BUNDLE_DIGEST,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
expect(result.success).toBe(false);
|
|
287
|
+
expect(result.deduplicated).toBe(false);
|
|
288
|
+
expect(result.error).toContain("Digest mismatch");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("rejects bundle with invalid digest format", () => {
|
|
292
|
+
const result = publishBundle(
|
|
293
|
+
buildPublishRequest({
|
|
294
|
+
expectedDigest: "not-a-valid-digest",
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
expect(result.success).toBe(false);
|
|
298
|
+
expect(result.error).toContain("Invalid expectedDigest");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("no files are written when digest mismatches", () => {
|
|
302
|
+
const wrongDigest = computeDigest(TAMPERED_BUNDLE_BYTES);
|
|
303
|
+
publishBundle(
|
|
304
|
+
buildPublishRequest({
|
|
305
|
+
bundleBytes: TAMPERED_BUNDLE_BYTES,
|
|
306
|
+
expectedDigest: SAMPLE_BUNDLE_DIGEST,
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// The toolstore directory should not contain a directory for the expected digest
|
|
311
|
+
const toolstoreDir = join(
|
|
312
|
+
testTmpDir,
|
|
313
|
+
".vellum",
|
|
314
|
+
"protected",
|
|
315
|
+
"credential-executor",
|
|
316
|
+
"toolstore",
|
|
317
|
+
);
|
|
318
|
+
const bundleDir = join(toolstoreDir, SAMPLE_BUNDLE_DIGEST);
|
|
319
|
+
expect(existsSync(bundleDir)).toBe(false);
|
|
320
|
+
|
|
321
|
+
// Also check that no staging directories were left behind
|
|
322
|
+
if (existsSync(toolstoreDir)) {
|
|
323
|
+
const { readdirSync } = require("node:fs");
|
|
324
|
+
const entries = readdirSync(toolstoreDir) as string[];
|
|
325
|
+
const stagingDirs = entries.filter((e: string) => e.startsWith(".staging-"));
|
|
326
|
+
expect(stagingDirs).toHaveLength(0);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Publisher: immutable and deduplicated
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
describe("publishBundle — immutable and deduplicated by digest", () => {
|
|
336
|
+
test("first publish succeeds with deduplicated=false", () => {
|
|
337
|
+
const result = publishBundle(buildPublishRequest());
|
|
338
|
+
expect(result.success).toBe(true);
|
|
339
|
+
expect(result.deduplicated).toBe(false);
|
|
340
|
+
expect(result.bundlePath).toContain(SAMPLE_BUNDLE_DIGEST);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("second publish of same digest returns deduplicated=true", () => {
|
|
344
|
+
const first = publishBundle(buildPublishRequest());
|
|
345
|
+
expect(first.success).toBe(true);
|
|
346
|
+
expect(first.deduplicated).toBe(false);
|
|
347
|
+
|
|
348
|
+
const second = publishBundle(buildPublishRequest());
|
|
349
|
+
expect(second.success).toBe(true);
|
|
350
|
+
expect(second.deduplicated).toBe(true);
|
|
351
|
+
expect(second.bundlePath).toBe(first.bundlePath);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("published bundle content is readable and matches original bytes", () => {
|
|
355
|
+
const result = publishBundle(buildPublishRequest());
|
|
356
|
+
expect(result.success).toBe(true);
|
|
357
|
+
|
|
358
|
+
const bundleContentPath = join(result.bundlePath, "bundle.bin");
|
|
359
|
+
expect(existsSync(bundleContentPath)).toBe(true);
|
|
360
|
+
|
|
361
|
+
const readBack = readFileSync(bundleContentPath);
|
|
362
|
+
expect(Buffer.compare(readBack, SAMPLE_BUNDLE_BYTES)).toBe(0);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("published manifest is readable and has correct fields", () => {
|
|
366
|
+
publishBundle(buildPublishRequest());
|
|
367
|
+
|
|
368
|
+
const manifest = readPublishedManifest(SAMPLE_BUNDLE_DIGEST);
|
|
369
|
+
expect(manifest).not.toBeNull();
|
|
370
|
+
expect(manifest!.digest).toBe(SAMPLE_BUNDLE_DIGEST);
|
|
371
|
+
expect(manifest!.bundleId).toBe("test-cli");
|
|
372
|
+
expect(manifest!.version).toBe("1.0.0");
|
|
373
|
+
expect(manifest!.origin.sourceUrl).toBe(
|
|
374
|
+
"https://releases.example.com/test-cli/v1.0.0/bundle.tar.gz",
|
|
375
|
+
);
|
|
376
|
+
expect(manifest!.declaredProfiles).toEqual(["read-data"]);
|
|
377
|
+
expect(manifest!.publishedAt).toBeTruthy();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("isBundlePublished returns false before publish", () => {
|
|
381
|
+
expect(isBundlePublished(SAMPLE_BUNDLE_DIGEST)).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("isBundlePublished returns true after publish", () => {
|
|
385
|
+
publishBundle(buildPublishRequest());
|
|
386
|
+
expect(isBundlePublished(SAMPLE_BUNDLE_DIGEST)).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("different bundles with different digests are stored independently", () => {
|
|
390
|
+
// Publish first bundle
|
|
391
|
+
const firstResult = publishBundle(buildPublishRequest());
|
|
392
|
+
expect(firstResult.success).toBe(true);
|
|
393
|
+
|
|
394
|
+
// Publish a second, different bundle
|
|
395
|
+
const otherBytes = Buffer.from("#!/usr/bin/env bash\necho other\n", "utf-8");
|
|
396
|
+
const otherDigest = computeDigest(otherBytes);
|
|
397
|
+
const otherManifest = buildSecureManifest({
|
|
398
|
+
bundleDigest: otherDigest,
|
|
399
|
+
bundleId: "other-cli",
|
|
400
|
+
version: "2.0.0",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const secondResult = publishBundle(
|
|
404
|
+
buildPublishRequest({
|
|
405
|
+
bundleBytes: otherBytes,
|
|
406
|
+
expectedDigest: otherDigest,
|
|
407
|
+
bundleId: "other-cli",
|
|
408
|
+
version: "2.0.0",
|
|
409
|
+
sourceUrl: "https://releases.example.com/other-cli/v2.0.0/bundle.tar.gz",
|
|
410
|
+
secureCommandManifest: otherManifest,
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
expect(secondResult.success).toBe(true);
|
|
414
|
+
expect(secondResult.deduplicated).toBe(false);
|
|
415
|
+
expect(secondResult.bundlePath).not.toBe(firstResult.bundlePath);
|
|
416
|
+
|
|
417
|
+
// Both are independently published
|
|
418
|
+
expect(isBundlePublished(SAMPLE_BUNDLE_DIGEST)).toBe(true);
|
|
419
|
+
expect(isBundlePublished(otherDigest)).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Publisher: publication does not imply credential grant
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
describe("publishBundle — publication does not imply credential grant", () => {
|
|
428
|
+
test("publish result does not contain any grant or credential fields", () => {
|
|
429
|
+
const result = publishBundle(buildPublishRequest());
|
|
430
|
+
expect(result.success).toBe(true);
|
|
431
|
+
|
|
432
|
+
// The PublishResult type only has success, deduplicated, bundlePath, error.
|
|
433
|
+
// There are no grant-related fields.
|
|
434
|
+
const keys = Object.keys(result);
|
|
435
|
+
expect(keys).not.toContain("grant");
|
|
436
|
+
expect(keys).not.toContain("credential");
|
|
437
|
+
expect(keys).not.toContain("token");
|
|
438
|
+
expect(keys).not.toContain("secret");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("toolstore manifest does not contain grant or credential data", () => {
|
|
442
|
+
publishBundle(buildPublishRequest());
|
|
443
|
+
|
|
444
|
+
const manifest = readPublishedManifest(SAMPLE_BUNDLE_DIGEST);
|
|
445
|
+
expect(manifest).not.toBeNull();
|
|
446
|
+
|
|
447
|
+
// Verify the manifest only contains content metadata, not grants
|
|
448
|
+
const keys = Object.keys(manifest!);
|
|
449
|
+
expect(keys).not.toContain("grant");
|
|
450
|
+
expect(keys).not.toContain("credential");
|
|
451
|
+
expect(keys).not.toContain("token");
|
|
452
|
+
expect(keys).not.toContain("secret");
|
|
453
|
+
expect(keys).toContain("digest");
|
|
454
|
+
expect(keys).toContain("bundleId");
|
|
455
|
+
expect(keys).toContain("origin");
|
|
456
|
+
expect(keys).toContain("declaredProfiles");
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Publisher: source URL validation
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
describe("publishBundle — source URL validation", () => {
|
|
465
|
+
test("rejects file:// source URL", () => {
|
|
466
|
+
const result = publishBundle(
|
|
467
|
+
buildPublishRequest({
|
|
468
|
+
sourceUrl: "file:///tmp/bundle.tar.gz",
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
expect(result.success).toBe(false);
|
|
472
|
+
expect(result.error).toContain("file:");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("rejects HTTP source URL", () => {
|
|
476
|
+
const result = publishBundle(
|
|
477
|
+
buildPublishRequest({
|
|
478
|
+
sourceUrl: "http://insecure.example.com/bundle.tar.gz",
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
expect(result.success).toBe(false);
|
|
482
|
+
expect(result.error).toContain("HTTPS");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("rejects workspace-origin source URL", () => {
|
|
486
|
+
const result = publishBundle(
|
|
487
|
+
buildPublishRequest({
|
|
488
|
+
sourceUrl: "https://example.com/.vellum/workspace/tools/bundle.tar.gz",
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
expect(result.success).toBe(false);
|
|
492
|
+
expect(result.error).toContain("workspace-origin");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("rejects empty source URL", () => {
|
|
496
|
+
const result = publishBundle(
|
|
497
|
+
buildPublishRequest({
|
|
498
|
+
sourceUrl: "",
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
expect(result.success).toBe(false);
|
|
502
|
+
expect(result.error).toContain("required");
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
// Publisher: manifest validation pass-through
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
describe("publishBundle — manifest validation", () => {
|
|
511
|
+
test("rejects bundle with invalid secure command manifest", () => {
|
|
512
|
+
const invalidManifest = buildSecureManifest({
|
|
513
|
+
entrypoint: "/bin/bash", // denied binary
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const result = publishBundle(
|
|
517
|
+
buildPublishRequest({
|
|
518
|
+
secureCommandManifest: invalidManifest,
|
|
519
|
+
}),
|
|
520
|
+
);
|
|
521
|
+
expect(result.success).toBe(false);
|
|
522
|
+
expect(result.error).toContain("Invalid secure command manifest");
|
|
523
|
+
expect(result.error).toContain("bash");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("rejects bundle with empty command profiles", () => {
|
|
527
|
+
const invalidManifest = buildSecureManifest({
|
|
528
|
+
commandProfiles: {},
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const result = publishBundle(
|
|
532
|
+
buildPublishRequest({
|
|
533
|
+
secureCommandManifest: invalidManifest,
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
expect(result.success).toBe(false);
|
|
537
|
+
expect(result.error).toContain("Invalid secure command manifest");
|
|
538
|
+
});
|
|
539
|
+
});
|