@aiwg/cli 2026.8.11 → 2026.8.12
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/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +6 -0
- package/dist/src/api/index.js +6 -0
- package/dist/src/cli/handlers/artifact-verify.js +171 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/setup-manifest.js +52 -3
- package/dist/src/cli/handlers/setup.js +15 -2
- package/dist/src/cli/handlers/use.js +64 -0
- package/dist/src/cli/services/deployment-verification.js +65 -7
- package/dist/src/config/aiwg-config.js +4 -3
- package/dist/src/config/cli.js +3 -1
- package/dist/src/config/gitignore.js +67 -21
- package/dist/src/config/workspace.js +8 -1
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/marketplace/artifact-attestation.js +195 -0
- package/dist/src/marketplace/exchange.js +437 -79
- package/dist/src/marketplace/provenance-types.js +1 -0
- package/dist/src/marketplace/provenance.js +7 -1
- package/dist/src/providers/transformation-receipt-integration.js +448 -0
- package/dist/src/providers/transformation-receipt.js +215 -0
- package/dist/src/resources/web-release.d.ts +11 -0
- package/dist/src/resources/web-release.js +61 -6
- package/dist/src/security/artifact-attestation.js +117 -0
- package/dist/src/security/artifact-trust.js +557 -0
- package/dist/src/security/artifact-verifier.js +478 -0
- package/dist/src/tracker/capability-protocol.js +7 -2
- package/package.json +5 -1
- package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
- package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
- package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
- package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
- package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
- package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
package/dist/src/config/cli.js
CHANGED
|
@@ -140,7 +140,8 @@ async function handleSet(config, args) {
|
|
|
140
140
|
// aiwg config get --project remotes.primary
|
|
141
141
|
//
|
|
142
142
|
// Set validates enum membership for known fields (delivery.mode,
|
|
143
|
-
// delivery.merge_style, delivery.force_push_policy
|
|
143
|
+
// delivery.merge_style, delivery.force_push_policy, remotes.issue_provider)
|
|
144
|
+
// before writing.
|
|
144
145
|
const ENUM_RULES = {
|
|
145
146
|
'delivery.mode': ['direct', 'feature-branch', 'pr-required'],
|
|
146
147
|
'delivery.merge_style': ['rebase-merge', 'squash', 'merge', 'fast-forward-only'],
|
|
@@ -149,6 +150,7 @@ const ENUM_RULES = {
|
|
|
149
150
|
'delivery.signing.enforce': ['commits', 'tags', 'all'],
|
|
150
151
|
'delivery.release_signing.format': ['openpgp', 'ssh', 'x509'],
|
|
151
152
|
'delivery.release_signing.enforce': ['commits', 'tags', 'all'],
|
|
153
|
+
'remotes.issue_provider': ['gitea', 'github', 'local'],
|
|
152
154
|
'remotes.tracker_actor.via': ['tea', 'gh', 'mcp', 'api'],
|
|
153
155
|
'remotes.transport.protocol': ['ssh', 'https'],
|
|
154
156
|
'repo_maintainer.tiers.local': ['collaborator', 'maintainer', 'admin'],
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from 'fs/promises';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
+
import { execFile } from 'child_process';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
const GITIGNORE_PROBE_BASENAME = '.aiwg-ignore-probe';
|
|
12
16
|
// ===========================
|
|
13
17
|
// Recommended Patterns
|
|
14
18
|
// ===========================
|
|
@@ -62,6 +66,63 @@ export const ALL_RECOMMENDED_PATTERNS = [
|
|
|
62
66
|
// ===========================
|
|
63
67
|
// Core Functions
|
|
64
68
|
// ===========================
|
|
69
|
+
function isTextuallyCovered(pattern, lines) {
|
|
70
|
+
if (lines.includes(pattern))
|
|
71
|
+
return true;
|
|
72
|
+
if (lines.includes(pattern.replace(/\/$/, '')))
|
|
73
|
+
return true;
|
|
74
|
+
const parts = pattern.split('/').filter(Boolean);
|
|
75
|
+
for (let i = 1; i < parts.length; i++) {
|
|
76
|
+
const parent = parts.slice(0, i).join('/') + '/';
|
|
77
|
+
if (lines.includes(parent) || lines.includes(parent.replace(/\/$/, ''))) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function probePath(pattern) {
|
|
84
|
+
return pattern.endsWith('/') ? `${pattern}${GITIGNORE_PROBE_BASENAME}` : pattern;
|
|
85
|
+
}
|
|
86
|
+
async function isGitWorktree(projectRoot) {
|
|
87
|
+
try {
|
|
88
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
89
|
+
cwd: projectRoot,
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
});
|
|
92
|
+
return stdout.trim() === 'true';
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve effective ignore coverage through Git when possible. This honors
|
|
100
|
+
* negations, .git/info/exclude, and core.excludesFile. A textual fallback keeps
|
|
101
|
+
* the helper deterministic outside Git worktrees or when Git is unavailable.
|
|
102
|
+
*
|
|
103
|
+
* @implements #2106
|
|
104
|
+
*/
|
|
105
|
+
async function resolveCoveredPatterns(projectRoot, patterns, lines) {
|
|
106
|
+
if (!(await isGitWorktree(projectRoot))) {
|
|
107
|
+
return new Set(patterns.filter(pattern => isTextuallyCovered(pattern, lines)));
|
|
108
|
+
}
|
|
109
|
+
const covered = await Promise.all(patterns.map(async (pattern) => {
|
|
110
|
+
try {
|
|
111
|
+
await execFileAsync('git', ['check-ignore', '--quiet', '--', probePath(pattern)], {
|
|
112
|
+
cwd: projectRoot,
|
|
113
|
+
windowsHide: true,
|
|
114
|
+
});
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const code = error.code;
|
|
119
|
+
if (code === 1)
|
|
120
|
+
return false;
|
|
121
|
+
return isTextuallyCovered(pattern, lines);
|
|
122
|
+
}
|
|
123
|
+
}));
|
|
124
|
+
return new Set(patterns.filter((_, index) => covered[index]));
|
|
125
|
+
}
|
|
65
126
|
/**
|
|
66
127
|
* Check which recommended patterns are missing from the project's .gitignore.
|
|
67
128
|
*/
|
|
@@ -77,26 +138,10 @@ export async function checkGitignore(projectRoot) {
|
|
|
77
138
|
// File doesn't exist — all patterns are missing
|
|
78
139
|
}
|
|
79
140
|
const lines = content.split('\n').map(l => l.trim());
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Pattern without trailing slash is also sufficient
|
|
85
|
-
if (lines.includes(pattern.replace(/\/$/, '')))
|
|
86
|
-
return true;
|
|
87
|
-
// Parent directory is ignored (covers all children)
|
|
88
|
-
const parts = pattern.split('/').filter(Boolean);
|
|
89
|
-
for (let i = 1; i < parts.length; i++) {
|
|
90
|
-
const parent = parts.slice(0, i).join('/') + '/';
|
|
91
|
-
if (lines.includes(parent) || lines.includes(parent.replace(/\/$/, ''))) {
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return false;
|
|
96
|
-
};
|
|
97
|
-
const missingRuntime = AIWG_RUNTIME_PATTERNS.filter(p => !isCovered(p));
|
|
98
|
-
const missingSession = CLAUDE_SESSION_PATTERNS.filter(p => !isCovered(p));
|
|
99
|
-
const missingProvider = PROVIDER_CONVENTIONAL_PATTERNS.filter(p => !isCovered(p));
|
|
141
|
+
const covered = await resolveCoveredPatterns(projectRoot, ALL_RECOMMENDED_PATTERNS, lines);
|
|
142
|
+
const missingRuntime = AIWG_RUNTIME_PATTERNS.filter(p => !covered.has(p));
|
|
143
|
+
const missingSession = CLAUDE_SESSION_PATTERNS.filter(p => !covered.has(p));
|
|
144
|
+
const missingProvider = PROVIDER_CONVENTIONAL_PATTERNS.filter(p => !covered.has(p));
|
|
100
145
|
const missing = [...missingRuntime, ...missingSession, ...missingProvider];
|
|
101
146
|
return { exists, missing, missingRuntime, missingSession, missingProvider };
|
|
102
147
|
}
|
|
@@ -116,10 +161,11 @@ export async function appendGitignore(projectRoot, patterns) {
|
|
|
116
161
|
// File doesn't exist, will create
|
|
117
162
|
}
|
|
118
163
|
const existingLines = existing.split('\n').map(l => l.trim());
|
|
164
|
+
const covered = await resolveCoveredPatterns(projectRoot, patterns, existingLines);
|
|
119
165
|
const toAdd = [];
|
|
120
166
|
const alreadyPresent = [];
|
|
121
167
|
for (const pattern of patterns) {
|
|
122
|
-
if (
|
|
168
|
+
if (covered.has(pattern)) {
|
|
123
169
|
alreadyPresent.push(pattern);
|
|
124
170
|
}
|
|
125
171
|
else {
|
|
@@ -119,14 +119,21 @@ async function resolveEndpoint(repoPath, remote, providerHint) {
|
|
|
119
119
|
: 'unknown',
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
|
+
function trackerProviderHint(remotes, fallback) {
|
|
123
|
+
const configured = remotes.issue_provider;
|
|
124
|
+
if (configured === 'gitea' || configured === 'github')
|
|
125
|
+
return configured;
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
122
128
|
async function resolveMember(entry, workspaceRoot) {
|
|
123
129
|
const memberPath = resolveFrom(workspaceRoot, entry.path);
|
|
124
130
|
const configPath = getConfigPath(memberPath);
|
|
125
131
|
const config = await readAiwgConfig(memberPath);
|
|
126
132
|
const remotes = resolveRemotes(config?.remotes);
|
|
133
|
+
const issueProviderHint = trackerProviderHint(remotes, entry.provider);
|
|
127
134
|
const [primary, issueTracker, ci] = await Promise.all([
|
|
128
135
|
resolveEndpoint(memberPath, remotes.primary, entry.provider),
|
|
129
|
-
resolveEndpoint(memberPath, remotes.issue_tracker,
|
|
136
|
+
resolveEndpoint(memberPath, remotes.issue_tracker, issueProviderHint),
|
|
130
137
|
resolveEndpoint(memberPath, remotes.ci, entry.provider),
|
|
131
138
|
]);
|
|
132
139
|
const drift = [];
|
|
@@ -2478,6 +2478,24 @@ export const evidenceCommand = {
|
|
|
2478
2478
|
},
|
|
2479
2479
|
},
|
|
2480
2480
|
};
|
|
2481
|
+
export const artifactVerifyCommand = {
|
|
2482
|
+
id: 'verify',
|
|
2483
|
+
type: 'command',
|
|
2484
|
+
name: 'Artifact Verification',
|
|
2485
|
+
description: 'Verify cross-asset DSSE provenance and manage versioned trust roots',
|
|
2486
|
+
version: '1.0.0',
|
|
2487
|
+
capabilities: ['cli', 'security', 'dsse', 'sigstore', 'provenance', 'trust-policy'],
|
|
2488
|
+
keywords: ['verify', 'artifact', 'attestation', 'dsse', 'sigstore', 'trust-root', 'revocation'],
|
|
2489
|
+
category: 'utility',
|
|
2490
|
+
platforms: { claude: 'full', generic: 'full' },
|
|
2491
|
+
deployment: { pathTemplate: '.{platform}/commands/{id}.md', core: true },
|
|
2492
|
+
metadata: {
|
|
2493
|
+
type: 'command',
|
|
2494
|
+
template: 'utility',
|
|
2495
|
+
argumentHint: '<artifact> --attestation <path> --policy <root.json> [--offline] [--json] | trust <bootstrap|update|status>',
|
|
2496
|
+
allowedTools: ['Read', 'Bash'],
|
|
2497
|
+
},
|
|
2498
|
+
};
|
|
2481
2499
|
export const costReportCommand = {
|
|
2482
2500
|
id: 'cost-report',
|
|
2483
2501
|
type: 'skill',
|
|
@@ -3614,6 +3632,7 @@ export const commandDefinitions = [
|
|
|
3614
3632
|
teamCommand,
|
|
3615
3633
|
// Metrics (4)
|
|
3616
3634
|
evidenceCommand,
|
|
3635
|
+
artifactVerifyCommand,
|
|
3617
3636
|
costReportCommand,
|
|
3618
3637
|
costHistoryCommand,
|
|
3619
3638
|
metricsTokensCommand,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createArtifactAttestation, serializeArtifactAttestation, } from '../security/artifact-attestation.js';
|
|
2
|
+
import { verifyArtifact, } from '../security/artifact-verifier.js';
|
|
3
|
+
import { canonicalJson as artifactCanonicalJson, parseTrustRoot, sha256 as artifactSha256, } from '../security/artifact-trust.js';
|
|
4
|
+
import { canonicalJson, createPackageLock, verifyProvenanceEnvelope, } from './provenance.js';
|
|
5
|
+
export const MARKETPLACE_ENVELOPE_SUBJECT = 'aiwg-marketplace-envelope.json';
|
|
6
|
+
export function serializeMarketplaceEnvelope(envelope) {
|
|
7
|
+
return Buffer.from(`${canonicalJson(envelope)}\n`, 'utf8');
|
|
8
|
+
}
|
|
9
|
+
export function marketplaceArtifactScope(envelope) {
|
|
10
|
+
return {
|
|
11
|
+
assetType: 'marketplace-envelope',
|
|
12
|
+
namespace: envelope.package.namespace,
|
|
13
|
+
channel: `marketplace:${envelope.package.name}`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function requiredMaterialNames(materials) {
|
|
17
|
+
const names = new Set(materials.map(material => material.name));
|
|
18
|
+
for (const required of ['lock', 'inventory', 'git-tree', 'fortemi-shard', 'receipt', 'sbom', 'license']) {
|
|
19
|
+
if (!names.has(required))
|
|
20
|
+
throw new Error(`Marketplace attestation requires '${required}' material`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function createMarketplaceArtifactAttestation(options) {
|
|
24
|
+
requiredMaterialNames(options.materials);
|
|
25
|
+
const scope = marketplaceArtifactScope(options.envelope);
|
|
26
|
+
const envelopeBytes = serializeMarketplaceEnvelope(options.envelope);
|
|
27
|
+
const provenanceBytes = Buffer.from(artifactCanonicalJson(options.envelope.provenance), 'utf8');
|
|
28
|
+
const dependencies = options.envelope.package.dependencies
|
|
29
|
+
.filter(dependency => !dependency.optional)
|
|
30
|
+
.map(dependency => {
|
|
31
|
+
if (!dependency.lockId)
|
|
32
|
+
throw new Error(`Required dependency '${dependency.identity}' has no immutable lockId`);
|
|
33
|
+
return {
|
|
34
|
+
name: dependency.identity,
|
|
35
|
+
uri: `aiwg:marketplace-lock:${dependency.lockId}`,
|
|
36
|
+
mediaType: 'application/vnd.aiwg.marketplace-lock.v1+json',
|
|
37
|
+
digest: { sha256: dependency.lockId.replace(/^sha256:/, '') },
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
const attestation = createArtifactAttestation({
|
|
41
|
+
artifact: {
|
|
42
|
+
name: MARKETPLACE_ENVELOPE_SUBJECT,
|
|
43
|
+
bytes: envelopeBytes,
|
|
44
|
+
mediaType: 'application/vnd.aiwg.marketplace-envelope.v1+json',
|
|
45
|
+
},
|
|
46
|
+
assetType: scope.assetType,
|
|
47
|
+
publisher: {
|
|
48
|
+
id: options.publisherIdentity ?? options.envelope.publisher.id,
|
|
49
|
+
namespace: scope.namespace,
|
|
50
|
+
role: 'marketplace-publisher',
|
|
51
|
+
},
|
|
52
|
+
publication: {
|
|
53
|
+
version: options.envelope.package.version,
|
|
54
|
+
channel: scope.channel,
|
|
55
|
+
sequence: options.envelope.publication.sequence,
|
|
56
|
+
sourceUri: options.envelope.source.canonicalRemote,
|
|
57
|
+
},
|
|
58
|
+
issuedAt: options.issuedAt ?? options.envelope.publication.publishedAt,
|
|
59
|
+
expiresAt: options.expiresAt,
|
|
60
|
+
derivation: {
|
|
61
|
+
builder: options.builder,
|
|
62
|
+
materials: options.materials.map(material => ({
|
|
63
|
+
name: material.name,
|
|
64
|
+
uri: material.uri,
|
|
65
|
+
...(material.mediaType ? { mediaType: material.mediaType } : {}),
|
|
66
|
+
digest: { sha256: artifactSha256(material.bytes) },
|
|
67
|
+
})),
|
|
68
|
+
reproducible: true,
|
|
69
|
+
},
|
|
70
|
+
provenanceGraph: {
|
|
71
|
+
standard: 'W3C-PROV',
|
|
72
|
+
uri: 'aiwg:marketplace-provenance:w3c-prov',
|
|
73
|
+
sha256: artifactSha256(provenanceBytes),
|
|
74
|
+
},
|
|
75
|
+
dependencies,
|
|
76
|
+
privateKey: options.privateKey,
|
|
77
|
+
});
|
|
78
|
+
return { attestation, materials: options.materials };
|
|
79
|
+
}
|
|
80
|
+
export function serializeMarketplaceAttestation(evidence) {
|
|
81
|
+
return serializeArtifactAttestation(evidence.attestation);
|
|
82
|
+
}
|
|
83
|
+
function signedMarketplacePolicy(root) {
|
|
84
|
+
return root.signed.policy.marketplace ?? {
|
|
85
|
+
evidenceMode: 'marketplace-only',
|
|
86
|
+
legacySignatureMigrationGate: false,
|
|
87
|
+
recursiveDependencies: 'if-present',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export async function verifyMarketplaceEvidence(options) {
|
|
91
|
+
const authenticatedRoot = options.artifact ? parseTrustRoot(options.artifact.rootBytes) : undefined;
|
|
92
|
+
const signedPolicy = authenticatedRoot ? signedMarketplacePolicy(authenticatedRoot) : {
|
|
93
|
+
evidenceMode: 'marketplace-only',
|
|
94
|
+
legacySignatureMigrationGate: false,
|
|
95
|
+
recursiveDependencies: 'if-present',
|
|
96
|
+
};
|
|
97
|
+
const legacyRequired = signedPolicy.evidenceMode !== 'cross-asset-required'
|
|
98
|
+
|| !signedPolicy.legacySignatureMigrationGate;
|
|
99
|
+
const marketplace = await verifyProvenanceEnvelope({
|
|
100
|
+
envelope: options.envelope,
|
|
101
|
+
contentRoot: options.contentRoot,
|
|
102
|
+
checkoutPath: options.checkoutPath,
|
|
103
|
+
trustStore: options.trustStore,
|
|
104
|
+
installedLocks: options.installedLocks,
|
|
105
|
+
at: options.artifact?.now ? new Date(options.artifact.now) : undefined,
|
|
106
|
+
policy: legacyRequired
|
|
107
|
+
? {
|
|
108
|
+
requireSignature: true,
|
|
109
|
+
allowIntegrityOnly: false,
|
|
110
|
+
requireDependencyLocks: signedPolicy.recursiveDependencies === 'required',
|
|
111
|
+
...options.marketplacePolicy,
|
|
112
|
+
}
|
|
113
|
+
: {
|
|
114
|
+
requireDependencyLocks: signedPolicy.recursiveDependencies === 'required',
|
|
115
|
+
...options.marketplacePolicy,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const errors = marketplace.ok ? [] : marketplace.errors.map(error => `marketplace: ${error}`);
|
|
119
|
+
let crossAsset;
|
|
120
|
+
const crossRequired = signedPolicy.evidenceMode !== 'marketplace-only';
|
|
121
|
+
if (options.artifact) {
|
|
122
|
+
const scope = marketplaceArtifactScope(options.envelope);
|
|
123
|
+
crossAsset = await verifyArtifact({
|
|
124
|
+
artifactBytes: serializeMarketplaceEnvelope(options.envelope),
|
|
125
|
+
artifactName: MARKETPLACE_ENVELOPE_SUBJECT,
|
|
126
|
+
attestation: options.artifact.evidence.attestation,
|
|
127
|
+
rootBytes: options.artifact.rootBytes,
|
|
128
|
+
state: options.artifact.state,
|
|
129
|
+
materials: new Map(options.artifact.evidence.materials.map(material => [material.uri, material.bytes])),
|
|
130
|
+
expectedScope: scope,
|
|
131
|
+
offline: options.artifact.offline,
|
|
132
|
+
now: options.artifact.now,
|
|
133
|
+
});
|
|
134
|
+
if (crossAsset.status !== 'verified')
|
|
135
|
+
errors.push(`cross-asset: ${crossAsset.status}`);
|
|
136
|
+
}
|
|
137
|
+
else if (crossRequired) {
|
|
138
|
+
errors.push('cross-asset: required evidence is unavailable');
|
|
139
|
+
}
|
|
140
|
+
if (legacyRequired && marketplace.status !== 'verified')
|
|
141
|
+
errors.push('marketplace: a trusted legacy publisher signature remains mandatory');
|
|
142
|
+
if (crossAsset?.status === 'verified' && marketplace.signer && options.trustStore) {
|
|
143
|
+
const legacyKey = options.trustStore.keys.find(key => key.keyId === marketplace.signer);
|
|
144
|
+
if (!legacyKey?.artifactIdentityId || !crossAsset.identities.includes(legacyKey.artifactIdentityId)) {
|
|
145
|
+
errors.push('trust-parity: marketplace and cross-asset verification resolved different publisher authorities');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
ok: errors.length === 0,
|
|
150
|
+
status: errors.length === 0 ? 'verified' : 'failed',
|
|
151
|
+
marketplace,
|
|
152
|
+
...(crossAsset ? { crossAsset } : {}),
|
|
153
|
+
errors: [...new Set(errors)],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export async function verifyMarketplaceDependencyClosure(options) {
|
|
157
|
+
const errors = [];
|
|
158
|
+
const verified = new Set();
|
|
159
|
+
const active = new Set();
|
|
160
|
+
const visit = async (envelope) => {
|
|
161
|
+
const lock = createPackageLock(envelope, envelope.publication.publishedAt);
|
|
162
|
+
if (verified.has(lock.lockId))
|
|
163
|
+
return;
|
|
164
|
+
if (active.has(lock.lockId)) {
|
|
165
|
+
errors.push(`dependency cycle detected at ${lock.lockId}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
active.add(lock.lockId);
|
|
169
|
+
if (!await options.verify(envelope))
|
|
170
|
+
errors.push(`dependency evidence failed for ${lock.lockId}`);
|
|
171
|
+
for (const dependency of envelope.package.dependencies.filter(item => !item.optional)) {
|
|
172
|
+
if (!dependency.lockId) {
|
|
173
|
+
if (options.required)
|
|
174
|
+
errors.push(`required dependency '${dependency.identity}' has no immutable lockId`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const child = options.packages.get(dependency.lockId);
|
|
178
|
+
if (!child) {
|
|
179
|
+
errors.push(`required dependency '${dependency.identity}' is unavailable at ${dependency.lockId}`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const actual = createPackageLock(child, child.publication.publishedAt).lockId;
|
|
183
|
+
if (actual !== dependency.lockId) {
|
|
184
|
+
errors.push(`dependency substitution for '${dependency.identity}': expected ${dependency.lockId}, got ${actual}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
await visit(child);
|
|
188
|
+
}
|
|
189
|
+
active.delete(lock.lockId);
|
|
190
|
+
verified.add(lock.lockId);
|
|
191
|
+
};
|
|
192
|
+
await visit(options.root);
|
|
193
|
+
return { ok: errors.length === 0, errors, verifiedLocks: [...verified].sort() };
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=artifact-attestation.js.map
|