@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toolstore manifest type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Describes an approved secure command bundle that CES can publish into
|
|
5
|
+
* its private immutable toolstore. Each manifest records:
|
|
6
|
+
*
|
|
7
|
+
* - **sourceUrl** — The canonical URL the bundle was fetched from.
|
|
8
|
+
* - **expectedDigest** — SHA-256 hex digest that the downloaded bytes
|
|
9
|
+
* must match before publication.
|
|
10
|
+
* - **bundleId** — Unique identifier for the command bundle
|
|
11
|
+
* (e.g. "gh-cli", "aws-cli").
|
|
12
|
+
* - **version** — Semantic version of the bundle.
|
|
13
|
+
* - **commandProfiles** — Profile names from the secure command manifest
|
|
14
|
+
* that this bundle declares.
|
|
15
|
+
*
|
|
16
|
+
* ## Publishing rules
|
|
17
|
+
*
|
|
18
|
+
* 1. **Only CES can write** — bundles are published into the CES-private
|
|
19
|
+
* data root, which the assistant process cannot reach.
|
|
20
|
+
* 2. **Immutable once published** — a bundle directory keyed by its digest
|
|
21
|
+
* is never overwritten. Re-publishing the same digest is a no-op.
|
|
22
|
+
* 3. **Workspace-origin binaries are never publishable** — bundles must
|
|
23
|
+
* come from a known source URL; arbitrary assistant-provided bytes are
|
|
24
|
+
* rejected.
|
|
25
|
+
* 4. **Publication does not grant credentials** — writing a bundle into
|
|
26
|
+
* the toolstore is purely a content operation. Credential-use grants
|
|
27
|
+
* are managed by a separate subsystem.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { SecureCommandManifest } from "../commands/profiles.js";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Bundle origin
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Describes the provenance of a bundle.
|
|
38
|
+
*
|
|
39
|
+
* `sourceUrl` must be an HTTPS URL. Workspace paths, file:// URLs, and
|
|
40
|
+
* data: URLs are structurally rejected.
|
|
41
|
+
*/
|
|
42
|
+
export interface BundleOrigin {
|
|
43
|
+
/** HTTPS URL from which the bundle was fetched. */
|
|
44
|
+
sourceUrl: string;
|
|
45
|
+
/** ISO-8601 timestamp of when the bundle was fetched. */
|
|
46
|
+
fetchedAt: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Toolstore manifest
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A toolstore manifest describes a single approved secure command bundle.
|
|
55
|
+
*
|
|
56
|
+
* This is the metadata stored alongside the bundle contents in the
|
|
57
|
+
* content-addressed toolstore directory.
|
|
58
|
+
*/
|
|
59
|
+
export interface ToolstoreManifest {
|
|
60
|
+
/** SHA-256 hex digest of the bundle contents. Content-address key. */
|
|
61
|
+
digest: string;
|
|
62
|
+
|
|
63
|
+
/** Unique identifier for the command bundle. */
|
|
64
|
+
bundleId: string;
|
|
65
|
+
|
|
66
|
+
/** Semantic version of the bundle. */
|
|
67
|
+
version: string;
|
|
68
|
+
|
|
69
|
+
/** Provenance information — where the bundle came from. */
|
|
70
|
+
origin: BundleOrigin;
|
|
71
|
+
|
|
72
|
+
/** Profile names declared in the secure command manifest. */
|
|
73
|
+
declaredProfiles: string[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The full secure command manifest embedded in the toolstore manifest.
|
|
77
|
+
* Used for runtime validation without needing to re-parse the bundle.
|
|
78
|
+
*/
|
|
79
|
+
secureCommandManifest: SecureCommandManifest;
|
|
80
|
+
|
|
81
|
+
/** ISO-8601 timestamp of when the bundle was published to the toolstore. */
|
|
82
|
+
publishedAt: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Validation helpers
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/** Regex for a valid SHA-256 hex digest. */
|
|
90
|
+
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns true if the given string is a valid SHA-256 hex digest.
|
|
94
|
+
*/
|
|
95
|
+
export function isValidSha256Hex(digest: string): boolean {
|
|
96
|
+
return SHA256_HEX_PATTERN.test(digest);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Schemes that are structurally rejected as bundle source URLs.
|
|
101
|
+
* Only HTTPS sources are accepted.
|
|
102
|
+
*/
|
|
103
|
+
const REJECTED_URL_SCHEMES = ["file:", "data:", "blob:", "javascript:"];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate a source URL for bundle origin.
|
|
107
|
+
*
|
|
108
|
+
* Accepted: HTTPS URLs only.
|
|
109
|
+
* Rejected: file://, data://, workspace paths, non-URL strings.
|
|
110
|
+
*
|
|
111
|
+
* Returns an error message if invalid, or null if valid.
|
|
112
|
+
*/
|
|
113
|
+
export function validateSourceUrl(sourceUrl: string): string | null {
|
|
114
|
+
if (!sourceUrl || sourceUrl.trim().length === 0) {
|
|
115
|
+
return "sourceUrl is required and must be non-empty.";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Must be a valid URL
|
|
119
|
+
let parsed: URL;
|
|
120
|
+
try {
|
|
121
|
+
parsed = new URL(sourceUrl);
|
|
122
|
+
} catch {
|
|
123
|
+
return `sourceUrl "${sourceUrl}" is not a valid URL. Only HTTPS URLs are accepted.`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Must be HTTPS
|
|
127
|
+
if (parsed.protocol !== "https:") {
|
|
128
|
+
if (REJECTED_URL_SCHEMES.includes(parsed.protocol)) {
|
|
129
|
+
return `sourceUrl scheme "${parsed.protocol}" is not allowed. Only HTTPS URLs are accepted as bundle sources.`;
|
|
130
|
+
}
|
|
131
|
+
return `sourceUrl scheme "${parsed.protocol}" is not allowed. Only HTTPS URLs are accepted.`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Workspace-origin path patterns that are never publishable as bundle
|
|
139
|
+
* sources. These catch attempts to publish assistant-provided bytes
|
|
140
|
+
* directly from the workspace directory.
|
|
141
|
+
*/
|
|
142
|
+
const WORKSPACE_PATH_PATTERNS = [
|
|
143
|
+
/^~?\/?\.vellum\//,
|
|
144
|
+
/\/\.vellum\//,
|
|
145
|
+
/\/workspace\//i,
|
|
146
|
+
] as const;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns true if the given path looks like a workspace-origin path
|
|
150
|
+
* that should never be accepted as a bundle source.
|
|
151
|
+
*/
|
|
152
|
+
export function isWorkspaceOriginPath(path: string): boolean {
|
|
153
|
+
return WORKSPACE_PATH_PATTERNS.some((pattern) => pattern.test(path));
|
|
154
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable toolstore publisher.
|
|
3
|
+
*
|
|
4
|
+
* Downloads (or re-fetches) secure command bundles inside CES, verifies
|
|
5
|
+
* their digest against the declared expected value, and writes them into
|
|
6
|
+
* content-addressed directories within the CES-private data root.
|
|
7
|
+
*
|
|
8
|
+
* ## Invariants
|
|
9
|
+
*
|
|
10
|
+
* 1. **CES-only writes** — All bundle content is written to the CES
|
|
11
|
+
* toolstore directory, which lives under the CES-private data root.
|
|
12
|
+
* The assistant process cannot read or write this path.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Immutable publications** — Once a digest directory exists, it is
|
|
15
|
+
* never overwritten. Re-publishing the same digest is a deduplicated
|
|
16
|
+
* no-op that returns success.
|
|
17
|
+
*
|
|
18
|
+
* 3. **Digest verification before write** — Downloaded bytes are verified
|
|
19
|
+
* against the expected digest before any file is created. A mismatch
|
|
20
|
+
* is a hard error; no partial writes occur.
|
|
21
|
+
*
|
|
22
|
+
* 4. **No credential grants** — Publishing a bundle into the toolstore
|
|
23
|
+
* is a pure content operation. It does not create, modify, or imply
|
|
24
|
+
* any credential-use grant.
|
|
25
|
+
*
|
|
26
|
+
* 5. **No workspace-origin bundles** — Source URLs that point to the
|
|
27
|
+
* workspace directory or use non-HTTPS schemes are rejected.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
|
|
33
|
+
import { getCesToolStoreDir, type CesMode } from "../paths.js";
|
|
34
|
+
import type { SecureCommandManifest } from "../commands/profiles.js";
|
|
35
|
+
import { validateManifest } from "../commands/validator.js";
|
|
36
|
+
import { verifyDigest } from "./integrity.js";
|
|
37
|
+
import {
|
|
38
|
+
isValidSha256Hex,
|
|
39
|
+
isWorkspaceOriginPath,
|
|
40
|
+
validateSourceUrl,
|
|
41
|
+
type ToolstoreManifest,
|
|
42
|
+
} from "./manifest.js";
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Publication result
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export interface PublishResult {
|
|
49
|
+
/** Whether the publication succeeded. */
|
|
50
|
+
success: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether this was a deduplicated no-op (the digest directory already
|
|
54
|
+
* existed from a previous publication).
|
|
55
|
+
*/
|
|
56
|
+
deduplicated: boolean;
|
|
57
|
+
|
|
58
|
+
/** The content-addressed directory path where the bundle is stored. */
|
|
59
|
+
bundlePath: string;
|
|
60
|
+
|
|
61
|
+
/** Error message if publication failed (undefined on success). */
|
|
62
|
+
error?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Publish request
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export interface PublishRequest {
|
|
70
|
+
/** Raw bundle bytes to publish. */
|
|
71
|
+
bundleBytes: Buffer | Uint8Array;
|
|
72
|
+
|
|
73
|
+
/** Expected SHA-256 hex digest of the bundle bytes. */
|
|
74
|
+
expectedDigest: string;
|
|
75
|
+
|
|
76
|
+
/** Unique identifier for the command bundle. */
|
|
77
|
+
bundleId: string;
|
|
78
|
+
|
|
79
|
+
/** Semantic version of the bundle. */
|
|
80
|
+
version: string;
|
|
81
|
+
|
|
82
|
+
/** HTTPS URL from which the bundle was fetched. */
|
|
83
|
+
sourceUrl: string;
|
|
84
|
+
|
|
85
|
+
/** The secure command manifest for this bundle. */
|
|
86
|
+
secureCommandManifest: SecureCommandManifest;
|
|
87
|
+
|
|
88
|
+
/** CES mode override (defaults to auto-detection). */
|
|
89
|
+
cesMode?: CesMode;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Content-addressed path helpers
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/** Manifest filename within a content-addressed bundle directory. */
|
|
97
|
+
const MANIFEST_FILENAME = "toolstore-manifest.json";
|
|
98
|
+
|
|
99
|
+
/** Bundle content filename within a content-addressed bundle directory. */
|
|
100
|
+
const BUNDLE_FILENAME = "bundle.bin";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Return the content-addressed directory path for a given digest.
|
|
104
|
+
*
|
|
105
|
+
* Layout: `<toolstoreDir>/<digest>/`
|
|
106
|
+
*/
|
|
107
|
+
export function getBundleDir(
|
|
108
|
+
toolstoreDir: string,
|
|
109
|
+
digest: string,
|
|
110
|
+
): string {
|
|
111
|
+
return join(toolstoreDir, digest);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Return the manifest file path for a given digest.
|
|
116
|
+
*/
|
|
117
|
+
export function getBundleManifestPath(
|
|
118
|
+
toolstoreDir: string,
|
|
119
|
+
digest: string,
|
|
120
|
+
): string {
|
|
121
|
+
return join(getBundleDir(toolstoreDir, digest), MANIFEST_FILENAME);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Return the bundle content file path for a given digest.
|
|
126
|
+
*/
|
|
127
|
+
export function getBundleContentPath(
|
|
128
|
+
toolstoreDir: string,
|
|
129
|
+
digest: string,
|
|
130
|
+
): string {
|
|
131
|
+
return join(getBundleDir(toolstoreDir, digest), BUNDLE_FILENAME);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Publisher
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Publish a secure command bundle into the CES-private immutable
|
|
140
|
+
* toolstore.
|
|
141
|
+
*
|
|
142
|
+
* This function:
|
|
143
|
+
* 1. Validates the source URL (HTTPS only, no workspace origins).
|
|
144
|
+
* 2. Validates the expected digest format.
|
|
145
|
+
* 3. Validates the secure command manifest.
|
|
146
|
+
* 4. Verifies the bundle bytes match the expected digest.
|
|
147
|
+
* 5. Checks for deduplication (returns early if already published).
|
|
148
|
+
* 6. Writes bundle contents and manifest atomically.
|
|
149
|
+
*
|
|
150
|
+
* Returns a {@link PublishResult} describing the outcome.
|
|
151
|
+
*/
|
|
152
|
+
export function publishBundle(request: PublishRequest): PublishResult {
|
|
153
|
+
const {
|
|
154
|
+
bundleBytes,
|
|
155
|
+
expectedDigest,
|
|
156
|
+
bundleId,
|
|
157
|
+
version,
|
|
158
|
+
sourceUrl,
|
|
159
|
+
secureCommandManifest,
|
|
160
|
+
cesMode,
|
|
161
|
+
} = request;
|
|
162
|
+
|
|
163
|
+
const toolstoreDir = getCesToolStoreDir(cesMode);
|
|
164
|
+
|
|
165
|
+
// -- Validate source URL ------------------------------------------------
|
|
166
|
+
const urlError = validateSourceUrl(sourceUrl);
|
|
167
|
+
if (urlError) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
deduplicated: false,
|
|
171
|
+
bundlePath: "",
|
|
172
|
+
error: `Invalid source URL: ${urlError}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// -- Reject workspace-origin paths in the URL ---------------------------
|
|
177
|
+
try {
|
|
178
|
+
const parsedUrl = new URL(sourceUrl);
|
|
179
|
+
if (isWorkspaceOriginPath(parsedUrl.pathname)) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
deduplicated: false,
|
|
183
|
+
bundlePath: "",
|
|
184
|
+
error: `Source URL path "${parsedUrl.pathname}" appears to be a workspace-origin path. ` +
|
|
185
|
+
`Workspace-origin binaries are never publishable.`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// URL parsing already validated above
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// -- Validate digest format ---------------------------------------------
|
|
193
|
+
if (!isValidSha256Hex(expectedDigest)) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
deduplicated: false,
|
|
197
|
+
bundlePath: "",
|
|
198
|
+
error: `Invalid expectedDigest "${expectedDigest}". ` +
|
|
199
|
+
`Must be a 64-character lowercase hex SHA-256 digest.`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// -- Validate secure command manifest -----------------------------------
|
|
204
|
+
const manifestValidation = validateManifest(secureCommandManifest);
|
|
205
|
+
if (!manifestValidation.valid) {
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
deduplicated: false,
|
|
209
|
+
bundlePath: "",
|
|
210
|
+
error: `Invalid secure command manifest: ${manifestValidation.errors.join("; ")}`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// -- Verify bundle digest -----------------------------------------------
|
|
215
|
+
const digestResult = verifyDigest(bundleBytes, expectedDigest);
|
|
216
|
+
if (!digestResult.valid) {
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
deduplicated: false,
|
|
220
|
+
bundlePath: "",
|
|
221
|
+
error: digestResult.error!,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// -- Deduplication check ------------------------------------------------
|
|
226
|
+
const bundleDir = getBundleDir(toolstoreDir, expectedDigest);
|
|
227
|
+
const manifestPath = getBundleManifestPath(toolstoreDir, expectedDigest);
|
|
228
|
+
|
|
229
|
+
if (existsSync(bundleDir) && existsSync(manifestPath)) {
|
|
230
|
+
// Already published — deduplicated no-op
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
deduplicated: true,
|
|
234
|
+
bundlePath: bundleDir,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// -- Ensure toolstore directory exists -----------------------------------
|
|
239
|
+
mkdirSync(toolstoreDir, { recursive: true });
|
|
240
|
+
|
|
241
|
+
// -- Write bundle atomically --------------------------------------------
|
|
242
|
+
//
|
|
243
|
+
// Write to a staging directory first, then rename to the final
|
|
244
|
+
// content-addressed path. This prevents partial writes from being
|
|
245
|
+
// visible to readers.
|
|
246
|
+
const stagingDir = join(toolstoreDir, `.staging-${expectedDigest}-${Date.now()}`);
|
|
247
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// Write bundle content
|
|
251
|
+
const stagingBundlePath = join(stagingDir, BUNDLE_FILENAME);
|
|
252
|
+
writeFileSync(stagingBundlePath, bundleBytes, { mode: 0o444 });
|
|
253
|
+
|
|
254
|
+
// Build and write toolstore manifest
|
|
255
|
+
const toolstoreManifest: ToolstoreManifest = {
|
|
256
|
+
digest: expectedDigest,
|
|
257
|
+
bundleId,
|
|
258
|
+
version,
|
|
259
|
+
origin: {
|
|
260
|
+
sourceUrl,
|
|
261
|
+
fetchedAt: new Date().toISOString(),
|
|
262
|
+
},
|
|
263
|
+
declaredProfiles: Object.keys(secureCommandManifest.commandProfiles),
|
|
264
|
+
secureCommandManifest,
|
|
265
|
+
publishedAt: new Date().toISOString(),
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const stagingManifestPath = join(stagingDir, MANIFEST_FILENAME);
|
|
269
|
+
writeFileSync(
|
|
270
|
+
stagingManifestPath,
|
|
271
|
+
JSON.stringify(toolstoreManifest, null, 2) + "\n",
|
|
272
|
+
{ mode: 0o444 },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Rename staging directory to final content-addressed path
|
|
276
|
+
//
|
|
277
|
+
// On POSIX, rename() is atomic within a filesystem. Since the
|
|
278
|
+
// staging dir is in the same parent as the final dir, this is
|
|
279
|
+
// a same-filesystem rename.
|
|
280
|
+
renameSync(stagingDir, bundleDir);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
// Clean up staging directory on failure
|
|
283
|
+
try {
|
|
284
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
285
|
+
} catch {
|
|
286
|
+
// Best effort cleanup
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
success: false,
|
|
290
|
+
deduplicated: false,
|
|
291
|
+
bundlePath: "",
|
|
292
|
+
error: `Failed to write bundle to toolstore: ${err instanceof Error ? err.message : String(err)}`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
success: true,
|
|
298
|
+
deduplicated: false,
|
|
299
|
+
bundlePath: bundleDir,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Reader helpers
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Read a published toolstore manifest by digest.
|
|
309
|
+
*
|
|
310
|
+
* Returns null if no bundle with the given digest is published.
|
|
311
|
+
*/
|
|
312
|
+
export function readPublishedManifest(
|
|
313
|
+
digest: string,
|
|
314
|
+
cesMode?: CesMode,
|
|
315
|
+
): ToolstoreManifest | null {
|
|
316
|
+
const toolstoreDir = getCesToolStoreDir(cesMode);
|
|
317
|
+
const manifestPath = getBundleManifestPath(toolstoreDir, digest);
|
|
318
|
+
|
|
319
|
+
if (!existsSync(manifestPath)) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
325
|
+
return JSON.parse(raw) as ToolstoreManifest;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check if a bundle with the given digest is published in the toolstore.
|
|
333
|
+
*/
|
|
334
|
+
export function isBundlePublished(
|
|
335
|
+
digest: string,
|
|
336
|
+
cesMode?: CesMode,
|
|
337
|
+
): boolean {
|
|
338
|
+
const toolstoreDir = getCesToolStoreDir(cesMode);
|
|
339
|
+
const bundleDir = getBundleDir(toolstoreDir, digest);
|
|
340
|
+
const manifestPath = getBundleManifestPath(toolstoreDir, digest);
|
|
341
|
+
return existsSync(bundleDir) && existsSync(manifestPath);
|
|
342
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src",
|
|
16
|
+
"types": ["bun-types"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|