@enactprotocol/cli 2.1.15 → 2.1.20
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/commands/publish/index.d.ts +1 -0
- package/dist/commands/publish/index.d.ts.map +1 -1
- package/dist/commands/publish/index.js +95 -2
- package/dist/commands/publish/index.js.map +1 -1
- package/dist/commands/sign/index.d.ts +7 -2
- package/dist/commands/sign/index.d.ts.map +1 -1
- package/dist/commands/sign/index.js +102 -49
- package/dist/commands/sign/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/src/commands/publish/index.ts +111 -1
- package/src/commands/sign/index.ts +127 -52
- package/src/index.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enactprotocol/cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.20",
|
|
4
4
|
"description": "Command-line interface for Enact - the npm for AI tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@clack/prompts": "^0.11.0",
|
|
37
|
-
"@enactprotocol/api": "2.1.
|
|
38
|
-
"@enactprotocol/execution": "2.1.
|
|
39
|
-
"@enactprotocol/secrets": "2.1.
|
|
40
|
-
"@enactprotocol/shared": "2.1.
|
|
37
|
+
"@enactprotocol/api": "2.1.20",
|
|
38
|
+
"@enactprotocol/execution": "2.1.20",
|
|
39
|
+
"@enactprotocol/secrets": "2.1.20",
|
|
40
|
+
"@enactprotocol/shared": "2.1.20",
|
|
41
41
|
"commander": "^12.1.0",
|
|
42
42
|
"picocolors": "^1.1.1"
|
|
43
43
|
},
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* enact publish command
|
|
3
3
|
*
|
|
4
4
|
* Publish a tool to the Enact registry using v2 multipart upload.
|
|
5
|
+
* Supports pre-signed attestations via manifest-based signing.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
@@ -16,9 +17,16 @@ import {
|
|
|
16
17
|
loadManifestFromDir,
|
|
17
18
|
validateManifest,
|
|
18
19
|
} from "@enactprotocol/shared";
|
|
20
|
+
import {
|
|
21
|
+
type ChecksumManifest,
|
|
22
|
+
type SigstoreBundle,
|
|
23
|
+
parseChecksumManifest,
|
|
24
|
+
verifyChecksumManifest,
|
|
25
|
+
} from "@enactprotocol/trust";
|
|
19
26
|
import type { Command } from "commander";
|
|
20
27
|
import type { CommandContext, GlobalOptions } from "../../types";
|
|
21
28
|
import {
|
|
29
|
+
confirm,
|
|
22
30
|
dim,
|
|
23
31
|
error,
|
|
24
32
|
extractNamespace,
|
|
@@ -208,6 +216,88 @@ async function publishHandler(
|
|
|
208
216
|
header(`Publishing ${toolName}@${version}`);
|
|
209
217
|
newline();
|
|
210
218
|
|
|
219
|
+
// Check for pre-signed attestation (manifest-based signing)
|
|
220
|
+
const checksumManifestPath = join(toolDir, ".enact-manifest.json");
|
|
221
|
+
const sigstoreBundlePath = join(toolDir, ".sigstore-bundle.json");
|
|
222
|
+
|
|
223
|
+
let checksumManifest: ChecksumManifest | undefined;
|
|
224
|
+
let sigstoreBundle: SigstoreBundle | undefined;
|
|
225
|
+
let hasPreSignedAttestation = false;
|
|
226
|
+
|
|
227
|
+
if (existsSync(checksumManifestPath) && existsSync(sigstoreBundlePath)) {
|
|
228
|
+
info("Found pre-signed attestation files");
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// Load and parse the checksum manifest
|
|
232
|
+
const manifestContent = readFileSync(checksumManifestPath, "utf-8");
|
|
233
|
+
checksumManifest = parseChecksumManifest(manifestContent);
|
|
234
|
+
|
|
235
|
+
// Load the sigstore bundle
|
|
236
|
+
const bundleContent = readFileSync(sigstoreBundlePath, "utf-8");
|
|
237
|
+
sigstoreBundle = JSON.parse(bundleContent) as SigstoreBundle;
|
|
238
|
+
|
|
239
|
+
// Verify the checksum manifest matches current files
|
|
240
|
+
const ignorePatterns = loadGitignore(toolDir);
|
|
241
|
+
const verification = await verifyChecksumManifest(toolDir, checksumManifest, {
|
|
242
|
+
ignorePatterns,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!verification.valid) {
|
|
246
|
+
newline();
|
|
247
|
+
warning("Pre-signed attestation is outdated - files have changed since signing:");
|
|
248
|
+
if (verification.modifiedFiles?.length) {
|
|
249
|
+
for (const file of verification.modifiedFiles) {
|
|
250
|
+
dim(` • Modified: ${file}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (verification.missingFiles?.length) {
|
|
254
|
+
for (const file of verification.missingFiles) {
|
|
255
|
+
dim(` • Missing: ${file}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (verification.extraFiles?.length) {
|
|
259
|
+
for (const file of verification.extraFiles) {
|
|
260
|
+
dim(` • New file: ${file}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
newline();
|
|
264
|
+
|
|
265
|
+
if (ctx.isInteractive) {
|
|
266
|
+
const continueWithoutAttestation = await confirm(
|
|
267
|
+
"Continue publishing without the pre-signed attestation?",
|
|
268
|
+
false
|
|
269
|
+
);
|
|
270
|
+
if (!continueWithoutAttestation) {
|
|
271
|
+
info("Publishing cancelled. Please re-sign with 'enact sign .' after making changes.");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Clear the attestation since it's outdated
|
|
275
|
+
checksumManifest = undefined;
|
|
276
|
+
sigstoreBundle = undefined;
|
|
277
|
+
} else {
|
|
278
|
+
error("Pre-signed attestation does not match current files.");
|
|
279
|
+
dim(
|
|
280
|
+
"Please re-sign with 'enact sign .' or remove .enact-manifest.json and .sigstore-bundle.json"
|
|
281
|
+
);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
hasPreSignedAttestation = true;
|
|
286
|
+
keyValue("Attestation", "Pre-signed (valid)");
|
|
287
|
+
keyValue("Manifest hash", `${checksumManifest.manifestHash.digest.slice(0, 16)}...`);
|
|
288
|
+
keyValue("Files in attestation", String(checksumManifest.files.length));
|
|
289
|
+
}
|
|
290
|
+
} catch (err) {
|
|
291
|
+
warning("Failed to load pre-signed attestation:");
|
|
292
|
+
if (err instanceof Error) {
|
|
293
|
+
dim(` ${err.message}`);
|
|
294
|
+
}
|
|
295
|
+
dim("Continuing without attestation...");
|
|
296
|
+
checksumManifest = undefined;
|
|
297
|
+
sigstoreBundle = undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
211
301
|
// Determine visibility (private by default for security)
|
|
212
302
|
const visibility: ToolVisibility = options.public
|
|
213
303
|
? "public"
|
|
@@ -341,12 +431,22 @@ async function publishHandler(
|
|
|
341
431
|
bundle,
|
|
342
432
|
rawManifest: rawManifestContent,
|
|
343
433
|
visibility,
|
|
434
|
+
// Include pre-signed attestation if available (cast to Record for API compatibility)
|
|
435
|
+
checksumManifest: hasPreSignedAttestation
|
|
436
|
+
? (checksumManifest as unknown as Record<string, unknown>)
|
|
437
|
+
: undefined,
|
|
438
|
+
sigstoreBundle: hasPreSignedAttestation
|
|
439
|
+
? (sigstoreBundle as unknown as Record<string, unknown>)
|
|
440
|
+
: undefined,
|
|
344
441
|
});
|
|
345
442
|
});
|
|
346
443
|
|
|
347
444
|
// JSON output
|
|
348
445
|
if (options.json) {
|
|
349
|
-
json(
|
|
446
|
+
json({
|
|
447
|
+
...result,
|
|
448
|
+
hasAttestation: hasPreSignedAttestation,
|
|
449
|
+
});
|
|
350
450
|
return;
|
|
351
451
|
}
|
|
352
452
|
|
|
@@ -355,6 +455,9 @@ async function publishHandler(
|
|
|
355
455
|
success(`Published ${result.name}@${result.version} (${visibility})`);
|
|
356
456
|
keyValue("Bundle Hash", result.bundleHash);
|
|
357
457
|
keyValue("Published At", result.publishedAt.toISOString());
|
|
458
|
+
if (hasPreSignedAttestation) {
|
|
459
|
+
keyValue("Attestation", "Included (pre-signed)");
|
|
460
|
+
}
|
|
358
461
|
newline();
|
|
359
462
|
if (visibility === "private") {
|
|
360
463
|
dim("This tool is private - only you can access it.");
|
|
@@ -362,6 +465,13 @@ async function publishHandler(
|
|
|
362
465
|
dim("This tool is unlisted - accessible via direct link, not searchable.");
|
|
363
466
|
}
|
|
364
467
|
dim(`Install with: enact install ${toolName}`);
|
|
468
|
+
|
|
469
|
+
if (!hasPreSignedAttestation) {
|
|
470
|
+
newline();
|
|
471
|
+
info("Tip: Sign your tool before publishing for verified attestations:");
|
|
472
|
+
dim(` 1. enact sign ${pathArg} # Create pre-signed attestation`);
|
|
473
|
+
dim(` 2. enact publish ${pathArg} # Publish with attestation`);
|
|
474
|
+
}
|
|
365
475
|
}
|
|
366
476
|
|
|
367
477
|
/**
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
* enact sign command
|
|
3
3
|
*
|
|
4
4
|
* Cryptographically sign a tool using Sigstore keyless signing.
|
|
5
|
-
* Creates an in-toto attestation
|
|
6
|
-
* and submits
|
|
5
|
+
* Creates an in-toto attestation based on a checksum manifest,
|
|
6
|
+
* logs to Rekor transparency log, and optionally submits to the registry.
|
|
7
|
+
*
|
|
8
|
+
* Uses manifest-based signing (per Sigstore team recommendation):
|
|
9
|
+
* - Creates deterministic checksum manifest of all files
|
|
10
|
+
* - Signs the manifest hash (not tar.gz bundle hash)
|
|
11
|
+
* - Enables pre-publish signing workflow
|
|
7
12
|
*
|
|
8
13
|
* Supports both local paths and remote tool references:
|
|
9
14
|
* - Local: enact sign ./my-tool
|
|
@@ -11,7 +16,7 @@
|
|
|
11
16
|
* - Remote: enact sign author/tool@1.0.0 (specific version)
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
|
-
import {
|
|
19
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
15
20
|
import { dirname, join, resolve } from "node:path";
|
|
16
21
|
import {
|
|
17
22
|
createApiClient,
|
|
@@ -30,10 +35,13 @@ import {
|
|
|
30
35
|
validateManifest,
|
|
31
36
|
} from "@enactprotocol/shared";
|
|
32
37
|
import {
|
|
38
|
+
type ChecksumManifest,
|
|
33
39
|
type EnactToolAttestationOptions,
|
|
34
40
|
type SigstoreBundle,
|
|
41
|
+
createChecksumManifest,
|
|
35
42
|
createEnactToolStatement,
|
|
36
43
|
extractCertificateFromBundle,
|
|
44
|
+
serializeChecksumManifest,
|
|
37
45
|
signAttestation,
|
|
38
46
|
} from "@enactprotocol/trust";
|
|
39
47
|
import type { Command } from "commander";
|
|
@@ -54,6 +62,7 @@ import {
|
|
|
54
62
|
warning,
|
|
55
63
|
withSpinner,
|
|
56
64
|
} from "../../utils";
|
|
65
|
+
import { loadGitignore } from "../../utils/ignore";
|
|
57
66
|
|
|
58
67
|
/** Auth namespace for token storage */
|
|
59
68
|
const AUTH_NAMESPACE = "enact:auth";
|
|
@@ -66,8 +75,9 @@ interface SignOptions extends GlobalOptions {
|
|
|
66
75
|
local?: boolean;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
/** Default output
|
|
78
|
+
/** Default output filenames for signing artifacts */
|
|
70
79
|
const DEFAULT_BUNDLE_FILENAME = ".sigstore-bundle.json";
|
|
80
|
+
const DEFAULT_MANIFEST_FILENAME = ".enact-manifest.json";
|
|
71
81
|
|
|
72
82
|
/**
|
|
73
83
|
* Parse a remote tool reference like "author/tool@1.0.0" or "author/tool"
|
|
@@ -140,32 +150,44 @@ function findManifestPath(pathArg: string): { manifestPath: string; manifestDir:
|
|
|
140
150
|
function displayDryRun(
|
|
141
151
|
manifestPath: string,
|
|
142
152
|
manifest: { name: string; version?: string; description?: string },
|
|
143
|
-
|
|
153
|
+
manifestDir: string,
|
|
144
154
|
options: SignOptions
|
|
145
155
|
): void {
|
|
156
|
+
const bundlePath = join(manifestDir, DEFAULT_BUNDLE_FILENAME);
|
|
157
|
+
const checksumManifestPath = join(manifestDir, DEFAULT_MANIFEST_FILENAME);
|
|
158
|
+
|
|
146
159
|
newline();
|
|
147
|
-
info(colors.bold("Dry Run Preview - Signing"));
|
|
160
|
+
info(colors.bold("Dry Run Preview - Manifest-Based Signing"));
|
|
148
161
|
newline();
|
|
149
162
|
|
|
150
163
|
keyValue("Tool", manifest.name);
|
|
151
164
|
keyValue("Version", manifest.version ?? "unversioned");
|
|
152
165
|
keyValue("Manifest", manifestPath);
|
|
153
|
-
keyValue("
|
|
166
|
+
keyValue("Checksum manifest output", checksumManifestPath);
|
|
167
|
+
keyValue("Sigstore bundle output", bundlePath);
|
|
154
168
|
keyValue("Submit to registry", options.local ? "No (local only)" : "Yes");
|
|
155
169
|
newline();
|
|
156
170
|
|
|
157
171
|
info("Actions that would be performed:");
|
|
158
|
-
dim(" 1.
|
|
159
|
-
dim(" 2. Create
|
|
160
|
-
dim(" 3.
|
|
161
|
-
dim(" 4.
|
|
162
|
-
dim(" 5.
|
|
163
|
-
dim(
|
|
172
|
+
dim(" 1. Scan tool directory and compute file checksums");
|
|
173
|
+
dim(" 2. Create checksum manifest (.enact-manifest.json)");
|
|
174
|
+
dim(" 3. Authenticate via OIDC (browser-based OAuth flow)");
|
|
175
|
+
dim(" 4. Create in-toto attestation for manifest hash");
|
|
176
|
+
dim(" 5. Request signing certificate from Fulcio");
|
|
177
|
+
dim(" 6. Sign attestation with ephemeral keypair");
|
|
178
|
+
dim(" 7. Log signature to Rekor transparency log");
|
|
179
|
+
dim(` 8. Write Sigstore bundle to ${bundlePath}`);
|
|
164
180
|
if (!options.local) {
|
|
165
|
-
dim("
|
|
181
|
+
dim(" 9. Submit attestation to Enact registry");
|
|
166
182
|
}
|
|
167
183
|
newline();
|
|
168
184
|
|
|
185
|
+
info("This enables pre-publish signing:");
|
|
186
|
+
dim(" • File checksums are deterministic (unlike tar.gz bundles)");
|
|
187
|
+
dim(" • Sign locally, then publish with pre-signed attestation");
|
|
188
|
+
dim(" • Server verifies manifest matches uploaded bundle");
|
|
189
|
+
newline();
|
|
190
|
+
|
|
169
191
|
warning("Note: Actual signing requires OIDC authentication.");
|
|
170
192
|
dim("You will be prompted to authenticate in your browser.");
|
|
171
193
|
}
|
|
@@ -236,7 +258,9 @@ async function promptAddToTrustList(
|
|
|
236
258
|
*/
|
|
237
259
|
function displayResult(
|
|
238
260
|
bundle: SigstoreBundle,
|
|
239
|
-
|
|
261
|
+
bundlePath: string,
|
|
262
|
+
manifestPath: string,
|
|
263
|
+
checksumManifest: ChecksumManifest,
|
|
240
264
|
manifest: { name: string; version?: string },
|
|
241
265
|
options: SignOptions,
|
|
242
266
|
registryResult?: { auditor: string; rekorLogIndex: number | undefined }
|
|
@@ -246,7 +270,10 @@ function displayResult(
|
|
|
246
270
|
success: true,
|
|
247
271
|
tool: manifest.name,
|
|
248
272
|
version: manifest.version ?? "unversioned",
|
|
249
|
-
|
|
273
|
+
checksumManifestPath: manifestPath,
|
|
274
|
+
sigstoreBundlePath: bundlePath,
|
|
275
|
+
manifestHash: checksumManifest.manifestHash.digest,
|
|
276
|
+
fileCount: checksumManifest.files.length,
|
|
250
277
|
bundle,
|
|
251
278
|
registry: registryResult
|
|
252
279
|
? {
|
|
@@ -263,7 +290,10 @@ function displayResult(
|
|
|
263
290
|
success(`Successfully signed ${manifest.name}@${manifest.version ?? "unversioned"}`);
|
|
264
291
|
newline();
|
|
265
292
|
|
|
266
|
-
keyValue("
|
|
293
|
+
keyValue("Checksum manifest", manifestPath);
|
|
294
|
+
keyValue("Sigstore bundle", bundlePath);
|
|
295
|
+
keyValue("Manifest hash", `${checksumManifest.manifestHash.digest.slice(0, 16)}...`);
|
|
296
|
+
keyValue("Files signed", String(checksumManifest.files.length));
|
|
267
297
|
|
|
268
298
|
// Show some bundle details
|
|
269
299
|
if (bundle.verificationMaterial?.tlogEntries?.[0]) {
|
|
@@ -286,7 +316,10 @@ function displayResult(
|
|
|
286
316
|
newline();
|
|
287
317
|
if (options.local) {
|
|
288
318
|
info("Note: Attestation saved locally only (--local flag)");
|
|
289
|
-
dim(" • Run 'enact
|
|
319
|
+
dim(" • Run 'enact publish .' to publish with this pre-signed attestation");
|
|
320
|
+
} else {
|
|
321
|
+
info("Next step:");
|
|
322
|
+
dim(" • Run 'enact publish .' to publish with this pre-signed attestation");
|
|
290
323
|
}
|
|
291
324
|
}
|
|
292
325
|
|
|
@@ -571,7 +604,7 @@ async function signRemoteTool(
|
|
|
571
604
|
}
|
|
572
605
|
|
|
573
606
|
/**
|
|
574
|
-
* Sign command handler (local files)
|
|
607
|
+
* Sign command handler (local files) - uses manifest-based signing
|
|
575
608
|
*/
|
|
576
609
|
async function signLocalTool(
|
|
577
610
|
pathArg: string,
|
|
@@ -580,7 +613,6 @@ async function signLocalTool(
|
|
|
580
613
|
): Promise<void> {
|
|
581
614
|
// Find manifest
|
|
582
615
|
const { manifestPath, manifestDir } = findManifestPath(pathArg);
|
|
583
|
-
const manifestContent = readFileSync(manifestPath, "utf-8");
|
|
584
616
|
|
|
585
617
|
// Load and validate manifest
|
|
586
618
|
const loaded = tryLoadManifest(manifestPath);
|
|
@@ -591,28 +623,6 @@ async function signLocalTool(
|
|
|
591
623
|
|
|
592
624
|
const manifest = loaded.manifest;
|
|
593
625
|
|
|
594
|
-
// Warn about local signing workflow - attestation hash won't match published bundle
|
|
595
|
-
if (_ctx.isInteractive && !options.dryRun) {
|
|
596
|
-
newline();
|
|
597
|
-
warning("Local signing creates an attestation for the manifest content hash.");
|
|
598
|
-
dim("If you plan to publish this tool, the published bundle will have a different hash.");
|
|
599
|
-
dim("The attestation won't match and verification will fail.");
|
|
600
|
-
newline();
|
|
601
|
-
info("Recommended workflow:");
|
|
602
|
-
dim(` 1. ${colors.command(`enact publish ${pathArg}`)} # Publish first`);
|
|
603
|
-
dim(
|
|
604
|
-
` 2. ${colors.command(`enact sign ${manifest.name}@${manifest.version ?? "1.0.0"}`)} # Then sign the published version`
|
|
605
|
-
);
|
|
606
|
-
newline();
|
|
607
|
-
|
|
608
|
-
const shouldContinue = await confirm("Continue with local signing anyway?", false);
|
|
609
|
-
if (!shouldContinue) {
|
|
610
|
-
info("Signing cancelled. Use the recommended workflow above.");
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
newline();
|
|
614
|
-
}
|
|
615
|
-
|
|
616
626
|
// Validate manifest
|
|
617
627
|
const validation = validateManifest(manifest);
|
|
618
628
|
if (!validation.valid && validation.errors) {
|
|
@@ -623,17 +633,66 @@ async function signLocalTool(
|
|
|
623
633
|
process.exit(1);
|
|
624
634
|
}
|
|
625
635
|
|
|
626
|
-
//
|
|
627
|
-
const
|
|
636
|
+
// Output paths
|
|
637
|
+
const bundlePath = options.output
|
|
628
638
|
? resolve(options.output)
|
|
629
639
|
: join(manifestDir, DEFAULT_BUNDLE_FILENAME);
|
|
640
|
+
const checksumManifestPath = join(manifestDir, DEFAULT_MANIFEST_FILENAME);
|
|
630
641
|
|
|
631
642
|
// Dry run mode
|
|
632
643
|
if (options.dryRun) {
|
|
633
|
-
displayDryRun(manifestPath, manifest,
|
|
644
|
+
displayDryRun(manifestPath, manifest, manifestDir, options);
|
|
634
645
|
return;
|
|
635
646
|
}
|
|
636
647
|
|
|
648
|
+
// Check for existing pre-signed attestation
|
|
649
|
+
if (existsSync(checksumManifestPath) && existsSync(bundlePath)) {
|
|
650
|
+
newline();
|
|
651
|
+
warning("Existing signature files found:");
|
|
652
|
+
dim(` • ${checksumManifestPath}`);
|
|
653
|
+
dim(` • ${bundlePath}`);
|
|
654
|
+
newline();
|
|
655
|
+
|
|
656
|
+
if (_ctx.isInteractive) {
|
|
657
|
+
const shouldOverwrite = await confirm("Overwrite existing signature?", false);
|
|
658
|
+
if (!shouldOverwrite) {
|
|
659
|
+
info("Signing cancelled. Existing signature preserved.");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
info("Overwriting existing signature (non-interactive mode).");
|
|
664
|
+
}
|
|
665
|
+
newline();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Load gitignore patterns for manifest creation
|
|
669
|
+
const ignorePatterns = loadGitignore(manifestDir);
|
|
670
|
+
|
|
671
|
+
// Create checksum manifest
|
|
672
|
+
info("Creating checksum manifest...");
|
|
673
|
+
const checksumManifest = await withSpinner(
|
|
674
|
+
"Scanning files and computing checksums...",
|
|
675
|
+
async () => {
|
|
676
|
+
return await createChecksumManifest(manifestDir, manifest.name, manifest.version ?? "1.0.0", {
|
|
677
|
+
ignorePatterns,
|
|
678
|
+
onProgress: options.verbose ? (file) => dim(` Hashing: ${file}`) : undefined,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
if (options.verbose) {
|
|
684
|
+
newline();
|
|
685
|
+
info(`Checksum manifest created with ${checksumManifest.files.length} files:`);
|
|
686
|
+
for (const file of checksumManifest.files) {
|
|
687
|
+
dim(` ${file.path} (${file.sha256.slice(0, 12)}...)`);
|
|
688
|
+
}
|
|
689
|
+
newline();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
keyValue("Files to sign", String(checksumManifest.files.length));
|
|
693
|
+
keyValue("Manifest hash", `${checksumManifest.manifestHash.digest.slice(0, 16)}...`);
|
|
694
|
+
newline();
|
|
695
|
+
|
|
637
696
|
// Prepare attestation options
|
|
638
697
|
const attestationOptions: EnactToolAttestationOptions = {
|
|
639
698
|
name: manifest.name,
|
|
@@ -641,6 +700,8 @@ async function signLocalTool(
|
|
|
641
700
|
publisher: options.identity ?? "unknown",
|
|
642
701
|
description: manifest.description,
|
|
643
702
|
buildTimestamp: new Date(),
|
|
703
|
+
// Use manifest hash as the bundle hash for attestation
|
|
704
|
+
bundleHash: checksumManifest.manifestHash.digest,
|
|
644
705
|
};
|
|
645
706
|
|
|
646
707
|
// Check for git repository for source info
|
|
@@ -664,8 +725,11 @@ async function signLocalTool(
|
|
|
664
725
|
}
|
|
665
726
|
}
|
|
666
727
|
|
|
667
|
-
// Create in-toto attestation statement
|
|
668
|
-
const statement = createEnactToolStatement(
|
|
728
|
+
// Create in-toto attestation statement using manifest hash as the content identifier
|
|
729
|
+
const statement = createEnactToolStatement(
|
|
730
|
+
checksumManifest.manifestHash.digest,
|
|
731
|
+
attestationOptions
|
|
732
|
+
);
|
|
669
733
|
|
|
670
734
|
if (options.verbose) {
|
|
671
735
|
info("Created attestation statement:");
|
|
@@ -707,8 +771,11 @@ async function signLocalTool(
|
|
|
707
771
|
}
|
|
708
772
|
});
|
|
709
773
|
|
|
710
|
-
// Save the
|
|
711
|
-
writeFileSync(
|
|
774
|
+
// Save the checksum manifest
|
|
775
|
+
writeFileSync(checksumManifestPath, serializeChecksumManifest(checksumManifest));
|
|
776
|
+
|
|
777
|
+
// Save the Sigstore bundle
|
|
778
|
+
writeFileSync(bundlePath, JSON.stringify(result.bundle, null, 2));
|
|
712
779
|
|
|
713
780
|
// Submit attestation to registry (unless --local)
|
|
714
781
|
let registryResult: { auditor: string; rekorLogIndex: number | undefined } | undefined;
|
|
@@ -719,7 +786,7 @@ async function signLocalTool(
|
|
|
719
786
|
|
|
720
787
|
if (!authToken) {
|
|
721
788
|
warning("Not authenticated with registry - attestation saved locally only");
|
|
722
|
-
dim("Run 'enact auth login' to authenticate, then
|
|
789
|
+
dim("Run 'enact auth login' to authenticate, then publish with pre-signed attestation");
|
|
723
790
|
} else {
|
|
724
791
|
const client = createApiClient();
|
|
725
792
|
client.setAuthToken(authToken);
|
|
@@ -756,13 +823,21 @@ async function signLocalTool(
|
|
|
756
823
|
dim(` ${err.message}`);
|
|
757
824
|
}
|
|
758
825
|
dim("The attestation was saved locally and logged to Rekor.");
|
|
759
|
-
dim("You can
|
|
826
|
+
dim("You can publish with the pre-signed attestation using 'enact publish .'");
|
|
760
827
|
}
|
|
761
828
|
}
|
|
762
829
|
}
|
|
763
830
|
|
|
764
831
|
// Display result
|
|
765
|
-
displayResult(
|
|
832
|
+
displayResult(
|
|
833
|
+
result.bundle,
|
|
834
|
+
bundlePath,
|
|
835
|
+
checksumManifestPath,
|
|
836
|
+
checksumManifest,
|
|
837
|
+
manifest,
|
|
838
|
+
options,
|
|
839
|
+
registryResult
|
|
840
|
+
);
|
|
766
841
|
}
|
|
767
842
|
|
|
768
843
|
/**
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from "./commands";
|
|
35
35
|
import { error, formatError } from "./utils";
|
|
36
36
|
|
|
37
|
-
export const version = "2.1.
|
|
37
|
+
export const version = "2.1.20";
|
|
38
38
|
|
|
39
39
|
// Export types for external use
|
|
40
40
|
export type { GlobalOptions, CommandContext } from "./types";
|