@aiwg/cli 2026.8.0 → 2026.8.2
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/README.md +33 -0
- package/agentic/code/providers/capability-matrix.yaml +511 -0
- package/agentic/code/providers/model-capabilities.v1.json +120 -0
- package/agentic/code/providers/model-catalog.v1.json +96 -0
- package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
- package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
- package/bin/aiwg.mjs +14 -10
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/cli.js +2 -0
- package/dist/src/artifacts/types.js +4 -0
- package/dist/src/auth/client.js +209 -0
- package/dist/src/auth/config.js +38 -0
- package/dist/src/auth/credential-store.js +141 -0
- package/dist/src/auth/resource-credentials.js +25 -0
- package/dist/src/auth/types.js +2 -0
- package/dist/src/channel/manager.mjs +5 -5
- package/dist/src/cli/handlers/auth.js +125 -0
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/install.js +42 -4
- package/dist/src/cli/handlers/marketplace.js +375 -122
- package/dist/src/cli/handlers/resource-versions.js +2 -0
- package/dist/src/cli/handlers/sessions.js +23 -5
- package/dist/src/cli/handlers/subcommands.js +10 -1
- package/dist/src/cli/handlers/use.js +342 -43
- package/dist/src/config/gitignore.js +1 -0
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/marketplace/exchange.js +602 -0
- package/dist/src/marketplace/provenance-types.js +19 -0
- package/dist/src/marketplace/provenance.js +834 -0
- package/dist/src/memory/canonical-context.js +342 -0
- package/dist/src/memory/context-pack.js +282 -0
- package/dist/src/memory/index.js +4 -0
- package/dist/src/memory/intake.js +118 -0
- package/dist/src/packages/adapters/git.js +79 -29
- package/dist/src/packages/package-discovery.js +81 -0
- package/dist/src/packages/package-registry.js +2 -0
- package/dist/src/packages/registry.js +119 -20
- package/dist/src/resources/resolver.js +1 -0
- package/dist/src/resources/web-release.d.ts +3 -1
- package/dist/src/resources/web-release.js +14 -6
- package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
- package/dist/src/serve/fleet-mission-conductor.js +293 -0
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/output-registration.js +338 -0
- package/dist/src/sessions/promotion.js +73 -2
- package/dist/src/sessions/repository.js +2 -1
- package/dist/src/update/notifier.mjs +13 -2
- package/package.json +8 -1
- package/tools/_resolve-impl.mjs +74 -0
- package/tools/agents/deploy-agents.mjs +962 -0
- package/tools/agents/providers/base.mjs +2954 -0
- package/tools/agents/providers/claude.mjs +711 -0
- package/tools/agents/providers/codex.mjs +699 -0
- package/tools/agents/providers/copilot.mjs +659 -0
- package/tools/agents/providers/cursor.mjs +714 -0
- package/tools/agents/providers/factory.mjs +1130 -0
- package/tools/agents/providers/hermes.mjs +663 -0
- package/tools/agents/providers/hook-capabilities.mjs +85 -0
- package/tools/agents/providers/model-role.mjs +56 -0
- package/tools/agents/providers/openclaw-translator.mjs +348 -0
- package/tools/agents/providers/openclaw.mjs +680 -0
- package/tools/agents/providers/opencode.mjs +675 -0
- package/tools/agents/providers/openhuman.mjs +292 -0
- package/tools/agents/providers/warp.mjs +413 -0
- package/tools/agents/providers/windsurf.mjs +748 -0
- package/tools/commands/deploy-prompts-codex.mjs +336 -0
- package/tools/plugin/package-plugins.mjs +1013 -0
- package/tools/skills/deploy-skills-codex.mjs +571 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-native provenance, signing, locking, and Fortemi conversion primitives.
|
|
3
|
+
*
|
|
4
|
+
* No function in this module executes package content. Git is inspected as
|
|
5
|
+
* data, file traversal rejects links, and verification completes before callers
|
|
6
|
+
* may deploy or persist imported bytes.
|
|
7
|
+
*
|
|
8
|
+
* @implements #2009
|
|
9
|
+
*/
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
11
|
+
import { createHash, createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify, } from 'node:crypto';
|
|
12
|
+
import { lstat, readFile, readdir } from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { promisify } from 'node:util';
|
|
15
|
+
import { MARKETPLACE_ENVELOPE_SCHEMA, MARKETPLACE_LOCK_SCHEMA, MARKETPLACE_RECEIPT_SCHEMA, } from './provenance-types.js';
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
18
|
+
const COMMIT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
19
|
+
const SAFE_ID = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
|
|
20
|
+
const SUPPORTED_CAPABILITIES = new Set([
|
|
21
|
+
'immutable-git-v1',
|
|
22
|
+
'w3c-prov-v1',
|
|
23
|
+
'fortemi-full-v1',
|
|
24
|
+
'offline-verification-v1',
|
|
25
|
+
]);
|
|
26
|
+
const CONTROL_PATHS = [
|
|
27
|
+
/^\.git(?:\/|$)/,
|
|
28
|
+
/^\.aiwg\/marketplace(?:\/|$)/,
|
|
29
|
+
/^aiwg-marketplace-(?:envelope|receipt|lock|catalog)\.json$/,
|
|
30
|
+
];
|
|
31
|
+
export const INTEGRITY_ONLY_POLICY = Object.freeze({
|
|
32
|
+
requireSignature: false,
|
|
33
|
+
allowIntegrityOnly: true,
|
|
34
|
+
allowYanked: false,
|
|
35
|
+
allowDeprecated: true,
|
|
36
|
+
allowRefMove: false,
|
|
37
|
+
allowRollback: false,
|
|
38
|
+
});
|
|
39
|
+
export const SIGNED_POLICY = Object.freeze({
|
|
40
|
+
...INTEGRITY_ONLY_POLICY,
|
|
41
|
+
requireSignature: true,
|
|
42
|
+
allowIntegrityOnly: false,
|
|
43
|
+
});
|
|
44
|
+
function isRecord(value) {
|
|
45
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
function stable(value) {
|
|
48
|
+
if (Array.isArray(value))
|
|
49
|
+
return value.map(stable);
|
|
50
|
+
if (isRecord(value)) {
|
|
51
|
+
return Object.fromEntries(Object.keys(value)
|
|
52
|
+
.filter((key) => value[key] !== undefined)
|
|
53
|
+
.sort()
|
|
54
|
+
.map((key) => [key, stable(value[key])]));
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
/** RFC-8785-style deterministic JSON for the JSON-native protocol values. */
|
|
59
|
+
export function canonicalJson(value) {
|
|
60
|
+
return JSON.stringify(stable(value));
|
|
61
|
+
}
|
|
62
|
+
export function sha256(value) {
|
|
63
|
+
return createHash('sha256').update(value).digest('hex');
|
|
64
|
+
}
|
|
65
|
+
function normalizedRelative(value) {
|
|
66
|
+
const normalized = value.replaceAll(path.sep, '/').replace(/^\.\//, '');
|
|
67
|
+
if (!normalized || normalized.startsWith('/') || normalized.includes('\0')) {
|
|
68
|
+
throw new Error(`Unsafe marketplace path '${value}'`);
|
|
69
|
+
}
|
|
70
|
+
const parts = normalized.split('/');
|
|
71
|
+
if (parts.some((part) => part === '' || part === '.' || part === '..')) {
|
|
72
|
+
throw new Error(`Unsafe marketplace path '${value}'`);
|
|
73
|
+
}
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
export function isMarketplaceControlPath(relativePath) {
|
|
77
|
+
const normalized = relativePath.replaceAll(path.sep, '/');
|
|
78
|
+
return CONTROL_PATHS.some((pattern) => pattern.test(normalized));
|
|
79
|
+
}
|
|
80
|
+
/** Walk regular files only, deterministically, without following links. */
|
|
81
|
+
export async function inventoryDirectory(root) {
|
|
82
|
+
const resolvedRoot = path.resolve(root);
|
|
83
|
+
const entries = [];
|
|
84
|
+
const walk = async (current) => {
|
|
85
|
+
const children = (await readdir(current, { withFileTypes: true }))
|
|
86
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
87
|
+
for (const child of children) {
|
|
88
|
+
const absolute = path.join(current, child.name);
|
|
89
|
+
const relative = normalizedRelative(path.relative(resolvedRoot, absolute));
|
|
90
|
+
if (isMarketplaceControlPath(relative))
|
|
91
|
+
continue;
|
|
92
|
+
const stat = await lstat(absolute);
|
|
93
|
+
if (stat.isSymbolicLink()) {
|
|
94
|
+
throw new Error(`Marketplace packages cannot contain symbolic links: ${relative}`);
|
|
95
|
+
}
|
|
96
|
+
if (stat.isDirectory()) {
|
|
97
|
+
await walk(absolute);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!stat.isFile()) {
|
|
101
|
+
throw new Error(`Marketplace packages may contain regular files only: ${relative}`);
|
|
102
|
+
}
|
|
103
|
+
const bytes = await readFile(absolute);
|
|
104
|
+
entries.push({
|
|
105
|
+
path: relative,
|
|
106
|
+
bytes: bytes.byteLength,
|
|
107
|
+
mode: stat.mode & 0o777,
|
|
108
|
+
sha256: sha256(bytes),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
await walk(resolvedRoot);
|
|
113
|
+
return entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
114
|
+
}
|
|
115
|
+
export function inventorySha256(inventory) {
|
|
116
|
+
return sha256(canonicalJson(inventory));
|
|
117
|
+
}
|
|
118
|
+
function sanitizeRemoteUrl(raw) {
|
|
119
|
+
const trimmed = raw.trim();
|
|
120
|
+
if (!trimmed)
|
|
121
|
+
throw new Error('Git remote cannot be empty');
|
|
122
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
123
|
+
const url = new URL(trimmed);
|
|
124
|
+
if (url.password)
|
|
125
|
+
throw new Error('Git remotes containing embedded credentials are not permitted');
|
|
126
|
+
url.username = '';
|
|
127
|
+
url.hash = '';
|
|
128
|
+
url.search = '';
|
|
129
|
+
const pathname = url.pathname.replace(/\/$/, '').replace(/\.git$/, '');
|
|
130
|
+
url.pathname = `${pathname}.git`;
|
|
131
|
+
return url.toString().replace(/\/$/, '');
|
|
132
|
+
}
|
|
133
|
+
if (/^(?:git@|ssh:\/\/)/.test(trimmed)) {
|
|
134
|
+
return trimmed.replace(/\/$/, '').replace(/\.git$/, '') + '.git';
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Unsupported canonical Git remote '${raw}'`);
|
|
137
|
+
}
|
|
138
|
+
export function canonicalizeGitRemote(raw) {
|
|
139
|
+
return sanitizeRemoteUrl(raw);
|
|
140
|
+
}
|
|
141
|
+
async function git(cwd, args) {
|
|
142
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
143
|
+
cwd,
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
timeout: 120_000,
|
|
146
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
147
|
+
});
|
|
148
|
+
return stdout.trim();
|
|
149
|
+
}
|
|
150
|
+
async function gitBytes(cwd, args) {
|
|
151
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
152
|
+
cwd,
|
|
153
|
+
encoding: 'buffer',
|
|
154
|
+
timeout: 120_000,
|
|
155
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
156
|
+
});
|
|
157
|
+
return stdout;
|
|
158
|
+
}
|
|
159
|
+
export async function inspectGitCheckout(checkoutPath, remote) {
|
|
160
|
+
const resolvedCommit = await git(checkoutPath, ['rev-parse', 'HEAD^{commit}']);
|
|
161
|
+
if (!COMMIT.test(resolvedCommit))
|
|
162
|
+
throw new Error(`Git returned invalid commit '${resolvedCommit}'`);
|
|
163
|
+
const gitTreeObject = await git(checkoutPath, ['rev-parse', 'HEAD^{tree}']);
|
|
164
|
+
if (!COMMIT.test(gitTreeObject))
|
|
165
|
+
throw new Error(`Git returned invalid tree object '${gitTreeObject}'`);
|
|
166
|
+
const treeBytes = await gitBytes(checkoutPath, ['ls-tree', '-r', '--full-tree', '-z', 'HEAD']);
|
|
167
|
+
const commitTime = await git(checkoutPath, ['show', '-s', '--format=%cI', 'HEAD']);
|
|
168
|
+
const configuredRemote = remote ?? await git(checkoutPath, ['config', '--get', 'remote.origin.url']);
|
|
169
|
+
return {
|
|
170
|
+
canonicalRemote: canonicalizeGitRemote(configuredRemote),
|
|
171
|
+
resolvedCommit,
|
|
172
|
+
gitTreeObject,
|
|
173
|
+
treeSha256: sha256(treeBytes),
|
|
174
|
+
commitTime,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function namespaceValue(value) {
|
|
178
|
+
const candidate = String(value ?? 'third-party').toLowerCase()
|
|
179
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
180
|
+
.replace(/^-+|-+$/g, '');
|
|
181
|
+
return candidate && SAFE_ID.test(candidate) ? candidate : 'third-party';
|
|
182
|
+
}
|
|
183
|
+
function packageKind(value) {
|
|
184
|
+
return ['framework', 'addon', 'extension', 'plugin'].includes(String(value))
|
|
185
|
+
? value
|
|
186
|
+
: 'unknown';
|
|
187
|
+
}
|
|
188
|
+
function providersFromManifest(manifest) {
|
|
189
|
+
const platforms = isRecord(manifest.platforms) ? manifest.platforms : {};
|
|
190
|
+
return Object.entries(platforms)
|
|
191
|
+
.filter(([, support]) => support !== false && support !== 'none' && support !== undefined)
|
|
192
|
+
.map(([provider, support]) => ({ provider, support: String(support === true ? 'supported' : support) }))
|
|
193
|
+
.sort((a, b) => a.provider.localeCompare(b.provider));
|
|
194
|
+
}
|
|
195
|
+
function dependenciesFromManifest(manifest) {
|
|
196
|
+
if (!isRecord(manifest.dependencies))
|
|
197
|
+
return [];
|
|
198
|
+
const result = [];
|
|
199
|
+
for (const [group, optional] of [['required', false], ['optional', true]]) {
|
|
200
|
+
const dependencies = manifest.dependencies[group];
|
|
201
|
+
if (!Array.isArray(dependencies))
|
|
202
|
+
continue;
|
|
203
|
+
for (const raw of dependencies) {
|
|
204
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
205
|
+
continue;
|
|
206
|
+
const at = raw.lastIndexOf('@');
|
|
207
|
+
result.push({
|
|
208
|
+
identity: at > 0 ? raw.slice(0, at) : raw,
|
|
209
|
+
version: at > 0 ? raw.slice(at + 1) : '*',
|
|
210
|
+
optional,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return result.sort((a, b) => a.identity.localeCompare(b.identity));
|
|
215
|
+
}
|
|
216
|
+
export async function createProvenanceEnvelope(options) {
|
|
217
|
+
const checkout = path.resolve(options.checkoutPath);
|
|
218
|
+
const artifact = path.resolve(options.artifactPath);
|
|
219
|
+
const relativeArtifact = path.relative(checkout, artifact).replaceAll(path.sep, '/') || '.';
|
|
220
|
+
if (relativeArtifact === '..' || relativeArtifact.startsWith('../') || path.isAbsolute(relativeArtifact)) {
|
|
221
|
+
throw new Error('Marketplace artifact path must stay inside the Git checkout');
|
|
222
|
+
}
|
|
223
|
+
const gitIdentity = await inspectGitCheckout(checkout, options.remote);
|
|
224
|
+
const inventory = await inventoryDirectory(artifact);
|
|
225
|
+
if (inventory.length === 0)
|
|
226
|
+
throw new Error('Marketplace package contains no regular artifact files');
|
|
227
|
+
const now = (options.now ?? new Date()).toISOString();
|
|
228
|
+
const manifest = options.manifest;
|
|
229
|
+
const name = namespaceValue(manifest.id ?? manifest.name ?? path.basename(checkout));
|
|
230
|
+
const namespace = namespaceValue(manifest.namespace ?? manifest.author);
|
|
231
|
+
const version = String(manifest.version ?? gitIdentity.resolvedCommit);
|
|
232
|
+
const publisher = options.publisher ?? String(manifest.author ?? namespace);
|
|
233
|
+
const packageIdentity = `${namespace}/${name}@${version}`;
|
|
234
|
+
const activityId = `urn:aiwg:marketplace:activity:${sha256(`${packageIdentity}:${gitIdentity.resolvedCommit}`).slice(0, 24)}`;
|
|
235
|
+
const sourceId = `urn:aiwg:marketplace:git:${gitIdentity.resolvedCommit}`;
|
|
236
|
+
const artifactDigest = inventorySha256(inventory);
|
|
237
|
+
const packageId = `urn:aiwg:marketplace:package:${sha256(`${packageIdentity}:${artifactDigest}`).slice(0, 32)}`;
|
|
238
|
+
let sbom;
|
|
239
|
+
if (options.sbomPath) {
|
|
240
|
+
const sbomRelative = normalizedRelative(options.sbomPath);
|
|
241
|
+
const entry = inventory.find((item) => item.path === sbomRelative);
|
|
242
|
+
if (!entry)
|
|
243
|
+
throw new Error(`SBOM '${sbomRelative}' is not part of the artifact inventory`);
|
|
244
|
+
sbom = { format: path.extname(sbomRelative).slice(1) || 'unknown', sha256: entry.sha256, path: sbomRelative };
|
|
245
|
+
}
|
|
246
|
+
const envelope = {
|
|
247
|
+
schemaVersion: MARKETPLACE_ENVELOPE_SCHEMA,
|
|
248
|
+
requiredCapabilities: [...SUPPORTED_CAPABILITIES].sort(),
|
|
249
|
+
package: {
|
|
250
|
+
namespace,
|
|
251
|
+
name,
|
|
252
|
+
version,
|
|
253
|
+
type: packageKind(manifest.type),
|
|
254
|
+
description: String(manifest.description ?? manifest.name ?? name),
|
|
255
|
+
license: String(manifest.license ?? 'NOASSERTION'),
|
|
256
|
+
wrapperSchemaVersion: String(manifest.manifestVersion ?? 'legacy'),
|
|
257
|
+
wrapperVersion: version,
|
|
258
|
+
providers: providersFromManifest(manifest),
|
|
259
|
+
dependencies: dependenciesFromManifest(manifest),
|
|
260
|
+
inventory,
|
|
261
|
+
...(sbom ? { sbom } : {}),
|
|
262
|
+
},
|
|
263
|
+
source: {
|
|
264
|
+
...gitIdentity,
|
|
265
|
+
requestedRef: options.requestedRef,
|
|
266
|
+
artifactSha256: artifactDigest,
|
|
267
|
+
wrapperPath: options.wrapperPath?.replaceAll(path.sep, '/') || '.',
|
|
268
|
+
payloadPath: relativeArtifact,
|
|
269
|
+
resolvedAt: now,
|
|
270
|
+
...(!COMMIT.test(options.requestedRef) && options.requestedRef !== 'HEAD'
|
|
271
|
+
? { tag: { name: options.requestedRef } }
|
|
272
|
+
: {}),
|
|
273
|
+
},
|
|
274
|
+
publisher: {
|
|
275
|
+
id: publisher,
|
|
276
|
+
...(options.publisherDisplayName ? { displayName: options.publisherDisplayName } : {}),
|
|
277
|
+
},
|
|
278
|
+
publication: {
|
|
279
|
+
sequence: options.sequence ?? 1,
|
|
280
|
+
publishedAt: now,
|
|
281
|
+
},
|
|
282
|
+
provenance: {
|
|
283
|
+
standard: 'W3C-PROV',
|
|
284
|
+
entities: [
|
|
285
|
+
{ id: sourceId, type: 'prov:Entity/git-tree', digest: gitIdentity.treeSha256 },
|
|
286
|
+
{ id: packageId, type: 'prov:Entity/package', digest: artifactDigest },
|
|
287
|
+
],
|
|
288
|
+
activities: [{
|
|
289
|
+
id: activityId,
|
|
290
|
+
type: 'prov:Activity/package-publication',
|
|
291
|
+
startedAt: now,
|
|
292
|
+
endedAt: now,
|
|
293
|
+
attributes: { requestedRef: options.requestedRef, resolvedCommit: gitIdentity.resolvedCommit },
|
|
294
|
+
}],
|
|
295
|
+
agents: [
|
|
296
|
+
{ id: `urn:aiwg:publisher:${publisher}`, type: 'organization' },
|
|
297
|
+
{ id: 'urn:aiwg:software:aiwg', type: 'software', attributes: { operation: 'package-provenance' } },
|
|
298
|
+
],
|
|
299
|
+
relations: [
|
|
300
|
+
{ type: 'wasDerivedFrom', subject: packageId, object: sourceId },
|
|
301
|
+
{ type: 'wasGeneratedBy', subject: packageId, object: activityId },
|
|
302
|
+
{ type: 'used', subject: activityId, object: sourceId },
|
|
303
|
+
{ type: 'wasAssociatedWith', subject: activityId, object: `urn:aiwg:publisher:${publisher}` },
|
|
304
|
+
{ type: 'wasAttributedTo', subject: packageId, object: `urn:aiwg:publisher:${publisher}` },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
fortemi: {
|
|
308
|
+
schemaVersion: '2.0.0',
|
|
309
|
+
profile: 'full-v1',
|
|
310
|
+
sourceSchemaVersion: 'aiwg.fortemi.index.export.v2',
|
|
311
|
+
},
|
|
312
|
+
signatures: [],
|
|
313
|
+
};
|
|
314
|
+
validateProvenanceEnvelope(envelope);
|
|
315
|
+
return envelope;
|
|
316
|
+
}
|
|
317
|
+
function exactKeys(value, allowed, where) {
|
|
318
|
+
const extras = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
319
|
+
if (extras.length)
|
|
320
|
+
throw new Error(`${where} contains unknown required field(s): ${extras.join(', ')}`);
|
|
321
|
+
}
|
|
322
|
+
function requiredString(value, where) {
|
|
323
|
+
if (typeof value !== 'string' || !value.trim())
|
|
324
|
+
throw new Error(`${where} must be a non-empty string`);
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
/** Strict fail-closed envelope validation; unknown fields are never discarded. */
|
|
328
|
+
export function validateProvenanceEnvelope(value) {
|
|
329
|
+
if (!isRecord(value))
|
|
330
|
+
throw new Error('Marketplace provenance envelope must be an object');
|
|
331
|
+
exactKeys(value, ['schemaVersion', 'requiredCapabilities', 'package', 'source', 'publisher', 'publication', 'provenance', 'fortemi', 'signatures'], 'envelope');
|
|
332
|
+
if (value.schemaVersion !== MARKETPLACE_ENVELOPE_SCHEMA)
|
|
333
|
+
throw new Error(`Unsupported envelope schema '${String(value.schemaVersion)}'`);
|
|
334
|
+
if (!Array.isArray(value.requiredCapabilities) || value.requiredCapabilities.some((item) => typeof item !== 'string')) {
|
|
335
|
+
throw new Error('envelope.requiredCapabilities must be an array of strings');
|
|
336
|
+
}
|
|
337
|
+
const unsupported = value.requiredCapabilities.filter((item) => !SUPPORTED_CAPABILITIES.has(item));
|
|
338
|
+
if (unsupported.length)
|
|
339
|
+
throw new Error(`Unsupported required marketplace capabilities: ${unsupported.join(', ')}`);
|
|
340
|
+
if (!isRecord(value.package))
|
|
341
|
+
throw new Error('envelope.package must be an object');
|
|
342
|
+
exactKeys(value.package, ['namespace', 'name', 'version', 'type', 'description', 'license', 'wrapperSchemaVersion', 'wrapperVersion', 'providers', 'dependencies', 'inventory', 'sbom'], 'envelope.package');
|
|
343
|
+
for (const field of ['namespace', 'name', 'version', 'type', 'description', 'license', 'wrapperSchemaVersion', 'wrapperVersion']) {
|
|
344
|
+
requiredString(value.package[field], `envelope.package.${field}`);
|
|
345
|
+
}
|
|
346
|
+
if (!Array.isArray(value.package.inventory) || value.package.inventory.length === 0)
|
|
347
|
+
throw new Error('envelope.package.inventory must be non-empty');
|
|
348
|
+
let lastPath = '';
|
|
349
|
+
const seen = new Set();
|
|
350
|
+
for (const raw of value.package.inventory) {
|
|
351
|
+
if (!isRecord(raw))
|
|
352
|
+
throw new Error('envelope.package.inventory entries must be objects');
|
|
353
|
+
exactKeys(raw, ['path', 'bytes', 'mode', 'sha256'], 'inventory entry');
|
|
354
|
+
const itemPath = normalizedRelative(requiredString(raw.path, 'inventory.path'));
|
|
355
|
+
if (seen.has(itemPath))
|
|
356
|
+
throw new Error(`Duplicate inventory path '${itemPath}'`);
|
|
357
|
+
if (lastPath && itemPath.localeCompare(lastPath) < 0)
|
|
358
|
+
throw new Error('Inventory must be sorted by path');
|
|
359
|
+
seen.add(itemPath);
|
|
360
|
+
lastPath = itemPath;
|
|
361
|
+
if (!Number.isSafeInteger(raw.bytes) || Number(raw.bytes) < 0)
|
|
362
|
+
throw new Error(`Invalid byte count for '${itemPath}'`);
|
|
363
|
+
if (!Number.isSafeInteger(raw.mode) || Number(raw.mode) < 0 || Number(raw.mode) > 0o777)
|
|
364
|
+
throw new Error(`Invalid mode for '${itemPath}'`);
|
|
365
|
+
if (typeof raw.sha256 !== 'string' || !SHA256.test(raw.sha256))
|
|
366
|
+
throw new Error(`Invalid SHA-256 for '${itemPath}'`);
|
|
367
|
+
}
|
|
368
|
+
if (!Array.isArray(value.package.providers) || !Array.isArray(value.package.dependencies))
|
|
369
|
+
throw new Error('Envelope provider/dependency inventories must be arrays');
|
|
370
|
+
if (!isRecord(value.source))
|
|
371
|
+
throw new Error('envelope.source must be an object');
|
|
372
|
+
exactKeys(value.source, ['canonicalRemote', 'requestedRef', 'resolvedCommit', 'gitTreeObject', 'treeSha256', 'artifactSha256', 'wrapperPath', 'payloadPath', 'resolvedAt', 'commitTime', 'tag'], 'envelope.source');
|
|
373
|
+
for (const field of ['canonicalRemote', 'requestedRef', 'resolvedCommit', 'gitTreeObject', 'treeSha256', 'artifactSha256', 'wrapperPath', 'payloadPath', 'resolvedAt']) {
|
|
374
|
+
requiredString(value.source[field], `envelope.source.${field}`);
|
|
375
|
+
}
|
|
376
|
+
if (!COMMIT.test(String(value.source.resolvedCommit)) || !COMMIT.test(String(value.source.gitTreeObject)))
|
|
377
|
+
throw new Error('Envelope Git identities must be immutable object IDs');
|
|
378
|
+
for (const field of ['treeSha256', 'artifactSha256'])
|
|
379
|
+
if (!SHA256.test(String(value.source[field])))
|
|
380
|
+
throw new Error(`Invalid source ${field}`);
|
|
381
|
+
canonicalizeGitRemote(String(value.source.canonicalRemote));
|
|
382
|
+
if (!isRecord(value.publisher) || typeof value.publisher.id !== 'string')
|
|
383
|
+
throw new Error('envelope.publisher.id is required');
|
|
384
|
+
if (!isRecord(value.publication) || !Number.isSafeInteger(value.publication.sequence) || Number(value.publication.sequence) < 1)
|
|
385
|
+
throw new Error('envelope.publication.sequence must be a positive integer');
|
|
386
|
+
requiredString(value.publication.publishedAt, 'envelope.publication.publishedAt');
|
|
387
|
+
if (!isRecord(value.provenance) || value.provenance.standard !== 'W3C-PROV')
|
|
388
|
+
throw new Error('Envelope must carry a W3C-PROV graph');
|
|
389
|
+
for (const field of ['entities', 'activities', 'agents', 'relations'])
|
|
390
|
+
if (!Array.isArray(value.provenance[field]))
|
|
391
|
+
throw new Error(`envelope.provenance.${field} must be an array`);
|
|
392
|
+
if (!isRecord(value.fortemi) || value.fortemi.schemaVersion !== '2.0.0' || value.fortemi.profile !== 'full-v1' || value.fortemi.sourceSchemaVersion !== 'aiwg.fortemi.index.export.v2') {
|
|
393
|
+
throw new Error('Envelope requires the exact Fortemi 2.0.0/full-v1 contract');
|
|
394
|
+
}
|
|
395
|
+
if (!Array.isArray(value.signatures))
|
|
396
|
+
throw new Error('envelope.signatures must be an array');
|
|
397
|
+
for (const signature of value.signatures) {
|
|
398
|
+
if (!isRecord(signature))
|
|
399
|
+
throw new Error('Envelope signatures must be objects');
|
|
400
|
+
exactKeys(signature, ['keyId', 'algorithm', 'publicKey', 'signedAt', 'payloadSha256', 'signature'], 'envelope signature');
|
|
401
|
+
if (signature.algorithm !== 'ed25519')
|
|
402
|
+
throw new Error(`Unsupported signature algorithm '${String(signature.algorithm)}'`);
|
|
403
|
+
requiredString(signature.keyId, 'signature.keyId');
|
|
404
|
+
requiredString(signature.publicKey, 'signature.publicKey');
|
|
405
|
+
requiredString(signature.signature, 'signature.signature');
|
|
406
|
+
if (!SHA256.test(String(signature.payloadSha256)))
|
|
407
|
+
throw new Error('Invalid signature payload SHA-256');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
export function envelopeSigningPayload(envelope) {
|
|
411
|
+
const { signatures: _signatures, ...payload } = envelope;
|
|
412
|
+
return payload;
|
|
413
|
+
}
|
|
414
|
+
function publicKeyDer(key) {
|
|
415
|
+
const object = typeof key === 'string'
|
|
416
|
+
? createPublicKey(key)
|
|
417
|
+
: key.type === 'public' ? key : createPublicKey(key);
|
|
418
|
+
if (object.asymmetricKeyType !== 'ed25519')
|
|
419
|
+
throw new Error('Marketplace signing keys must use Ed25519');
|
|
420
|
+
return object.export({ format: 'der', type: 'spki' });
|
|
421
|
+
}
|
|
422
|
+
function publicKeyFromBase64(value) {
|
|
423
|
+
return createPublicKey({ key: Buffer.from(value, 'base64'), format: 'der', type: 'spki' });
|
|
424
|
+
}
|
|
425
|
+
export function signCanonicalDocument(document, privateKeyPem, options = {}) {
|
|
426
|
+
const privateKey = createPrivateKey(privateKeyPem);
|
|
427
|
+
if (privateKey.asymmetricKeyType !== 'ed25519')
|
|
428
|
+
throw new Error('Marketplace signing keys must use Ed25519');
|
|
429
|
+
const publicKey = options.publicKeyPem ? createPublicKey(options.publicKeyPem) : createPublicKey(privateKey);
|
|
430
|
+
const payload = canonicalJson(document);
|
|
431
|
+
return {
|
|
432
|
+
keyId: options.keyId ?? signingKeyId(publicKey),
|
|
433
|
+
algorithm: 'ed25519',
|
|
434
|
+
publicKey: publicKeyDer(publicKey).toString('base64'),
|
|
435
|
+
signedAt: options.signedAt ?? new Date().toISOString(),
|
|
436
|
+
payloadSha256: sha256(payload),
|
|
437
|
+
signature: cryptoSign(null, Buffer.from(payload), privateKey).toString('base64'),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
export function verifyCanonicalSignature(document, signature) {
|
|
441
|
+
const payload = canonicalJson(document);
|
|
442
|
+
return signature.algorithm === 'ed25519'
|
|
443
|
+
&& signature.payloadSha256 === sha256(payload)
|
|
444
|
+
&& cryptoVerify(null, Buffer.from(payload), publicKeyFromBase64(signature.publicKey), Buffer.from(signature.signature, 'base64'));
|
|
445
|
+
}
|
|
446
|
+
export function signingKeyId(publicKey) {
|
|
447
|
+
return `ed25519:${sha256(publicKeyDer(publicKey)).slice(0, 32)}`;
|
|
448
|
+
}
|
|
449
|
+
export function signProvenanceEnvelope(envelope, privateKeyPem, options = {}) {
|
|
450
|
+
validateProvenanceEnvelope(envelope);
|
|
451
|
+
const privateKey = createPrivateKey(privateKeyPem);
|
|
452
|
+
if (privateKey.asymmetricKeyType !== 'ed25519')
|
|
453
|
+
throw new Error('Marketplace signing keys must use Ed25519');
|
|
454
|
+
const publicKey = options.publicKeyPem ? createPublicKey(options.publicKeyPem) : createPublicKey(privateKey);
|
|
455
|
+
const keyId = options.keyId ?? signingKeyId(publicKey);
|
|
456
|
+
const prepared = {
|
|
457
|
+
...envelope,
|
|
458
|
+
publisher: { ...envelope.publisher, keyId },
|
|
459
|
+
};
|
|
460
|
+
const signature = signCanonicalDocument(envelopeSigningPayload(prepared), privateKeyPem, {
|
|
461
|
+
keyId,
|
|
462
|
+
signedAt: options.signedAt,
|
|
463
|
+
publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }).toString(),
|
|
464
|
+
});
|
|
465
|
+
return {
|
|
466
|
+
...prepared,
|
|
467
|
+
signatures: [...prepared.signatures.filter((item) => item.keyId !== keyId), signature]
|
|
468
|
+
.sort((a, b) => a.keyId.localeCompare(b.keyId)),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
export function keyDelegationStatement(key) {
|
|
472
|
+
return {
|
|
473
|
+
schemaVersion: 'aiwg.marketplace.key-delegation.v1',
|
|
474
|
+
keyId: key.keyId,
|
|
475
|
+
publicKey: key.publicKey,
|
|
476
|
+
publisher: key.publisher,
|
|
477
|
+
validFrom: key.validFrom,
|
|
478
|
+
...(key.validUntil ? { validUntil: key.validUntil } : {}),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
export function signKeyDelegation(key, parentPrivateKeyPem) {
|
|
482
|
+
return cryptoSign(null, Buffer.from(canonicalJson(keyDelegationStatement(key))), createPrivateKey(parentPrivateKeyPem)).toString('base64');
|
|
483
|
+
}
|
|
484
|
+
function verifyTrustedKey(key, store, at, seen = new Set()) {
|
|
485
|
+
if (seen.has(key.keyId))
|
|
486
|
+
return { ok: false, detail: `Delegation cycle at '${key.keyId}'` };
|
|
487
|
+
seen.add(key.keyId);
|
|
488
|
+
if (key.revokedAt && new Date(key.revokedAt) <= at)
|
|
489
|
+
return { ok: false, detail: `Key '${key.keyId}' is revoked` };
|
|
490
|
+
if (new Date(key.validFrom) > at)
|
|
491
|
+
return { ok: false, detail: `Key '${key.keyId}' is not yet valid` };
|
|
492
|
+
if (key.validUntil && new Date(key.validUntil) < at)
|
|
493
|
+
return { ok: false, detail: `Key '${key.keyId}' is expired` };
|
|
494
|
+
if (key.trustRoot)
|
|
495
|
+
return { ok: true, detail: `Trusted root '${key.keyId}'` };
|
|
496
|
+
if (!key.delegatedBy || !key.delegationSignature)
|
|
497
|
+
return { ok: false, detail: `Key '${key.keyId}' has no trusted delegation` };
|
|
498
|
+
const parent = store.keys.find((candidate) => candidate.keyId === key.delegatedBy);
|
|
499
|
+
if (!parent)
|
|
500
|
+
return { ok: false, detail: `Delegating key '${key.delegatedBy}' is unavailable` };
|
|
501
|
+
const parentResult = verifyTrustedKey(parent, store, at, seen);
|
|
502
|
+
if (!parentResult.ok)
|
|
503
|
+
return parentResult;
|
|
504
|
+
const valid = cryptoVerify(null, Buffer.from(canonicalJson(keyDelegationStatement(key))), publicKeyFromBase64(parent.publicKey), Buffer.from(key.delegationSignature, 'base64'));
|
|
505
|
+
return valid
|
|
506
|
+
? { ok: true, detail: `Key '${key.keyId}' delegated by '${parent.keyId}'` }
|
|
507
|
+
: { ok: false, detail: `Invalid delegation signature for '${key.keyId}'` };
|
|
508
|
+
}
|
|
509
|
+
export function verifyDocumentTrust(options) {
|
|
510
|
+
const errors = [];
|
|
511
|
+
for (const signature of options.signatures) {
|
|
512
|
+
if (!verifyCanonicalSignature(options.document, signature)) {
|
|
513
|
+
errors.push(`Invalid signature from '${signature.keyId}'`);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const key = options.trustStore.keys.find((candidate) => candidate.keyId === signature.keyId);
|
|
517
|
+
if (!key) {
|
|
518
|
+
errors.push(`Signature key '${signature.keyId}' is not trusted`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (key.publicKey !== signature.publicKey) {
|
|
522
|
+
errors.push(`Trusted key material mismatch for '${signature.keyId}'`);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (options.publisher && key.publisher !== options.publisher) {
|
|
526
|
+
errors.push(`Trusted key publisher mismatch for '${signature.keyId}'`);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const trust = verifyTrustedKey(key, options.trustStore, options.at ?? new Date(signature.signedAt));
|
|
530
|
+
if (trust.ok)
|
|
531
|
+
return { ok: true, signer: signature.keyId, errors: [] };
|
|
532
|
+
errors.push(trust.detail);
|
|
533
|
+
}
|
|
534
|
+
return { ok: false, errors };
|
|
535
|
+
}
|
|
536
|
+
export function createPackageLock(envelope, createdAt = new Date().toISOString()) {
|
|
537
|
+
validateProvenanceEnvelope(envelope);
|
|
538
|
+
const identity = `${envelope.package.namespace}/${envelope.package.name}`;
|
|
539
|
+
const immutableIdentity = {
|
|
540
|
+
identity,
|
|
541
|
+
version: envelope.package.version,
|
|
542
|
+
canonicalRemote: envelope.source.canonicalRemote,
|
|
543
|
+
resolvedCommit: envelope.source.resolvedCommit,
|
|
544
|
+
gitTreeObject: envelope.source.gitTreeObject,
|
|
545
|
+
treeSha256: envelope.source.treeSha256,
|
|
546
|
+
artifactSha256: envelope.source.artifactSha256,
|
|
547
|
+
wrapperSchemaVersion: envelope.package.wrapperSchemaVersion,
|
|
548
|
+
fortemiProfile: '2.0.0/full-v1',
|
|
549
|
+
dependencyLocks: Object.fromEntries(envelope.package.dependencies
|
|
550
|
+
.filter((dependency) => !dependency.optional && dependency.lockId)
|
|
551
|
+
.sort((a, b) => a.identity.localeCompare(b.identity))
|
|
552
|
+
.map((dependency) => [dependency.identity, dependency.lockId])),
|
|
553
|
+
};
|
|
554
|
+
return {
|
|
555
|
+
schemaVersion: MARKETPLACE_LOCK_SCHEMA,
|
|
556
|
+
lockId: `sha256:${sha256(canonicalJson(immutableIdentity))}`,
|
|
557
|
+
...immutableIdentity,
|
|
558
|
+
requestedRef: envelope.source.requestedRef,
|
|
559
|
+
envelopeSha256: sha256(canonicalJson(envelope)),
|
|
560
|
+
createdAt,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
export function envelopeToFortemiIndex(envelope) {
|
|
564
|
+
validateProvenanceEnvelope(envelope);
|
|
565
|
+
const identity = `${envelope.package.namespace}/${envelope.package.name}@${envelope.package.version}`;
|
|
566
|
+
const text = canonicalJson(envelope);
|
|
567
|
+
return {
|
|
568
|
+
schema_version: 'aiwg.fortemi.index.export.v2',
|
|
569
|
+
generated_at: envelope.publication.publishedAt,
|
|
570
|
+
source: {
|
|
571
|
+
repo: envelope.source.canonicalRemote,
|
|
572
|
+
privacy: 'public',
|
|
573
|
+
graph: 'marketplace-provenance',
|
|
574
|
+
},
|
|
575
|
+
compatibility: {
|
|
576
|
+
previous_schema_version: 'aiwg.fortemi.index.export.v1',
|
|
577
|
+
strategy: 'supported',
|
|
578
|
+
},
|
|
579
|
+
items: [{
|
|
580
|
+
schema_version: 'aiwg.fortemi.index.record.v2',
|
|
581
|
+
id: `marketplace:${sha256(identity).slice(0, 32)}`,
|
|
582
|
+
type: 'aiwg.artifact',
|
|
583
|
+
source: {
|
|
584
|
+
path: 'aiwg-marketplace-envelope.json',
|
|
585
|
+
repo_relative_path: 'aiwg-marketplace-envelope.json',
|
|
586
|
+
locator: identity,
|
|
587
|
+
updated_at: envelope.publication.publishedAt,
|
|
588
|
+
},
|
|
589
|
+
title: identity,
|
|
590
|
+
text,
|
|
591
|
+
facets: {},
|
|
592
|
+
tags: ['marketplace', 'provenance', envelope.package.type],
|
|
593
|
+
concepts: ['aiwg:marketplace-package', 'w3c:prov'],
|
|
594
|
+
skos_concepts: [
|
|
595
|
+
{
|
|
596
|
+
id: 'aiwg:marketplace-package',
|
|
597
|
+
prefLabel: 'AIWG marketplace package',
|
|
598
|
+
scheme: 'aiwg-marketplace',
|
|
599
|
+
definition: 'A Git-native AIWG package with a verifiable provenance envelope.',
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
id: 'w3c:prov',
|
|
603
|
+
prefLabel: 'W3C PROV',
|
|
604
|
+
scheme: 'provenance-standards',
|
|
605
|
+
definition: 'Entity, activity, agent, and relationship provenance graph.',
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
relationships: [],
|
|
609
|
+
provenance: [{
|
|
610
|
+
field: 'text',
|
|
611
|
+
source: envelope.source.canonicalRemote,
|
|
612
|
+
path: '$',
|
|
613
|
+
confidence: 'source',
|
|
614
|
+
privacy: 'public',
|
|
615
|
+
}],
|
|
616
|
+
provenance_events: envelope.provenance.activities.map((activity) => ({
|
|
617
|
+
id: activity.id,
|
|
618
|
+
activity: activity.type,
|
|
619
|
+
agent: envelope.publisher.id,
|
|
620
|
+
started_at: activity.startedAt,
|
|
621
|
+
ended_at: activity.endedAt,
|
|
622
|
+
attributes: activity.attributes ?? {},
|
|
623
|
+
})),
|
|
624
|
+
privacy: { classification: 'public', pii: false },
|
|
625
|
+
updated_at: envelope.publication.publishedAt,
|
|
626
|
+
}],
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
export async function buildFortemiEnvelopeShard(envelope) {
|
|
630
|
+
const core = await import('@fortemi/core/aiwg-index-shard');
|
|
631
|
+
const index = envelopeToFortemiIndex(envelope);
|
|
632
|
+
const result = await core.aiwgFortemiIndexToKnowledgeShardWithReport(index, {
|
|
633
|
+
createdAt: envelope.publication.publishedAt,
|
|
634
|
+
matricVersion: 'aiwg-marketplace-v1',
|
|
635
|
+
});
|
|
636
|
+
if (!result.success || !result.lossless || !result.archive) {
|
|
637
|
+
const detail = result.losses.map((loss) => `${loss.code}: ${loss.message}${loss.field_path ? ` (${loss.field_path})` : ''}${loss.reason ? ` [${loss.reason}]` : ''}`).join('; ');
|
|
638
|
+
throw new Error(`Fortemi full-v1 conversion would be lossy${detail ? `: ${detail}` : ''}`);
|
|
639
|
+
}
|
|
640
|
+
await verifyFortemiEnvelopeShard(envelope, result.archive);
|
|
641
|
+
return {
|
|
642
|
+
archive: result.archive,
|
|
643
|
+
conformance: {
|
|
644
|
+
profile: '2.0.0/full-v1',
|
|
645
|
+
lossless: true,
|
|
646
|
+
contractValid: result.receipt.contract_valid,
|
|
647
|
+
shardSha256: sha256(result.archive),
|
|
648
|
+
conversionReceipt: result.receipt,
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/** Validate the authority contract and prove the exact canonical envelope is embedded. */
|
|
653
|
+
export async function verifyFortemiEnvelopeShard(envelope, archive) {
|
|
654
|
+
const core = await import('@fortemi/core');
|
|
655
|
+
const validation = await core.validateFullV1ShardArchive(archive);
|
|
656
|
+
if (!validation.valid)
|
|
657
|
+
throw new Error(`Invalid Fortemi 2.0.0/full-v1 archive: ${validation.errors.join('; ')}`);
|
|
658
|
+
const files = core.unpackTarGz(archive);
|
|
659
|
+
const canonical = canonicalJson(envelope);
|
|
660
|
+
const escaped = JSON.stringify(canonical).slice(1, -1);
|
|
661
|
+
const embedded = [...files.values()].some((bytes) => {
|
|
662
|
+
const text = Buffer.from(bytes).toString('utf8');
|
|
663
|
+
return text.includes(canonical) || text.includes(escaped);
|
|
664
|
+
});
|
|
665
|
+
if (!embedded)
|
|
666
|
+
throw new Error('Fortemi full-v1 archive does not contain the exact canonical provenance envelope');
|
|
667
|
+
}
|
|
668
|
+
export function createOperationReceipt(options) {
|
|
669
|
+
const occurredAt = options.occurredAt ?? new Date().toISOString();
|
|
670
|
+
const body = {
|
|
671
|
+
operation: options.operation,
|
|
672
|
+
occurredAt,
|
|
673
|
+
actor: options.actor,
|
|
674
|
+
lockId: options.lock.lockId,
|
|
675
|
+
envelopeSha256: options.lock.envelopeSha256,
|
|
676
|
+
result: options.result ?? 'success',
|
|
677
|
+
verificationStatus: options.verificationStatus,
|
|
678
|
+
evidence: options.evidence ?? {},
|
|
679
|
+
conformance: options.conformance,
|
|
680
|
+
};
|
|
681
|
+
return {
|
|
682
|
+
schemaVersion: MARKETPLACE_RECEIPT_SCHEMA,
|
|
683
|
+
receiptId: `sha256:${sha256(canonicalJson(body))}`,
|
|
684
|
+
...body,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
async function verifyInventory(envelope, contentRoot) {
|
|
688
|
+
const actual = await inventoryDirectory(contentRoot);
|
|
689
|
+
const expected = envelope.package.inventory;
|
|
690
|
+
const checks = [];
|
|
691
|
+
const actualDigest = inventorySha256(actual);
|
|
692
|
+
checks.push({
|
|
693
|
+
check: 'artifact-digest',
|
|
694
|
+
ok: actualDigest === envelope.source.artifactSha256,
|
|
695
|
+
detail: actualDigest === envelope.source.artifactSha256
|
|
696
|
+
? actualDigest
|
|
697
|
+
: `expected ${envelope.source.artifactSha256}, got ${actualDigest}`,
|
|
698
|
+
});
|
|
699
|
+
const actualByPath = new Map(actual.map((entry) => [entry.path, entry]));
|
|
700
|
+
for (const expectedEntry of expected) {
|
|
701
|
+
const found = actualByPath.get(expectedEntry.path);
|
|
702
|
+
checks.push({
|
|
703
|
+
check: `file:${expectedEntry.path}`,
|
|
704
|
+
ok: Boolean(found && canonicalJson(found) === canonicalJson(expectedEntry)),
|
|
705
|
+
detail: found ? found.sha256 : 'missing',
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
const expectedPaths = new Set(expected.map((entry) => entry.path));
|
|
709
|
+
const extras = actual.filter((entry) => !expectedPaths.has(entry.path));
|
|
710
|
+
checks.push({
|
|
711
|
+
check: 'file-inventory-complete',
|
|
712
|
+
ok: extras.length === 0 && actual.length === expected.length,
|
|
713
|
+
detail: extras.length ? `unexpected: ${extras.map((entry) => entry.path).join(', ')}` : `${actual.length} files`,
|
|
714
|
+
});
|
|
715
|
+
return checks;
|
|
716
|
+
}
|
|
717
|
+
export async function verifyProvenanceEnvelope(options) {
|
|
718
|
+
const policy = { ...INTEGRITY_ONLY_POLICY, ...options.policy };
|
|
719
|
+
const checks = [];
|
|
720
|
+
const errors = [];
|
|
721
|
+
const warnings = [];
|
|
722
|
+
let envelope;
|
|
723
|
+
try {
|
|
724
|
+
validateProvenanceEnvelope(options.envelope);
|
|
725
|
+
envelope = options.envelope;
|
|
726
|
+
checks.push({ check: 'envelope-schema', ok: true, detail: MARKETPLACE_ENVELOPE_SCHEMA });
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
throw new Error(`Invalid marketplace provenance envelope: ${error instanceof Error ? error.message : String(error)}`);
|
|
730
|
+
}
|
|
731
|
+
const lock = createPackageLock(envelope, envelope.publication.publishedAt);
|
|
732
|
+
const envelopeSha256 = sha256(canonicalJson(envelope));
|
|
733
|
+
if (options.contentRoot) {
|
|
734
|
+
const inventoryChecks = await verifyInventory(envelope, options.contentRoot);
|
|
735
|
+
checks.push(...inventoryChecks);
|
|
736
|
+
errors.push(...inventoryChecks.filter((check) => !check.ok).map((check) => `${check.check}: ${check.detail}`));
|
|
737
|
+
}
|
|
738
|
+
if (options.checkoutPath) {
|
|
739
|
+
try {
|
|
740
|
+
const gitIdentity = await inspectGitCheckout(options.checkoutPath);
|
|
741
|
+
for (const [check, actual, expected] of [
|
|
742
|
+
['git-commit', gitIdentity.resolvedCommit, envelope.source.resolvedCommit],
|
|
743
|
+
['git-tree-object', gitIdentity.gitTreeObject, envelope.source.gitTreeObject],
|
|
744
|
+
['git-tree-digest', gitIdentity.treeSha256, envelope.source.treeSha256],
|
|
745
|
+
['git-remote', gitIdentity.canonicalRemote, envelope.source.canonicalRemote],
|
|
746
|
+
]) {
|
|
747
|
+
const ok = actual === expected;
|
|
748
|
+
checks.push({ check, ok, detail: ok ? actual : `expected ${expected}, got ${actual}` });
|
|
749
|
+
if (!ok)
|
|
750
|
+
errors.push(`${check} mismatch`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
errors.push(`git-evidence: ${error instanceof Error ? error.message : String(error)}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (envelope.publication.yanked && !policy.allowYanked)
|
|
758
|
+
errors.push('Package version is yanked by its publisher');
|
|
759
|
+
if (envelope.publication.deprecated && !policy.allowDeprecated)
|
|
760
|
+
errors.push('Package version is deprecated by policy');
|
|
761
|
+
const identity = `${envelope.package.namespace}/${envelope.package.name}`;
|
|
762
|
+
const minimum = policy.minimumSequence?.[identity];
|
|
763
|
+
if (minimum !== undefined && envelope.publication.sequence < minimum && !policy.allowRollback) {
|
|
764
|
+
errors.push(`Publication sequence ${envelope.publication.sequence} is below trusted minimum ${minimum}`);
|
|
765
|
+
}
|
|
766
|
+
if (policy.requiredPublisher && envelope.publisher.id !== policy.requiredPublisher) {
|
|
767
|
+
errors.push(`Publisher '${envelope.publisher.id}' does not match required publisher '${policy.requiredPublisher}'`);
|
|
768
|
+
}
|
|
769
|
+
if (options.previousLock && !policy.allowRefMove
|
|
770
|
+
&& options.previousLock.canonicalRemote === lock.canonicalRemote
|
|
771
|
+
&& options.previousLock.requestedRef === lock.requestedRef
|
|
772
|
+
&& options.previousLock.resolvedCommit !== lock.resolvedCommit) {
|
|
773
|
+
errors.push(`Mutable ref '${lock.requestedRef}' moved from ${options.previousLock.resolvedCommit} to ${lock.resolvedCommit}`);
|
|
774
|
+
}
|
|
775
|
+
for (const dependency of envelope.package.dependencies.filter((item) => !item.optional && item.lockId)) {
|
|
776
|
+
const actual = options.installedLocks?.[dependency.identity];
|
|
777
|
+
const ok = actual === dependency.lockId;
|
|
778
|
+
checks.push({ check: `dependency:${dependency.identity}`, ok, detail: actual ?? 'not installed' });
|
|
779
|
+
if (!ok)
|
|
780
|
+
errors.push(`Dependency substitution detected for '${dependency.identity}'`);
|
|
781
|
+
}
|
|
782
|
+
const payload = canonicalJson(envelopeSigningPayload(envelope));
|
|
783
|
+
const payloadDigest = sha256(payload);
|
|
784
|
+
let trustedSigner;
|
|
785
|
+
for (const signature of envelope.signatures) {
|
|
786
|
+
const digestOk = signature.payloadSha256 === payloadDigest;
|
|
787
|
+
const signatureOk = digestOk && cryptoVerify(null, Buffer.from(payload), publicKeyFromBase64(signature.publicKey), Buffer.from(signature.signature, 'base64'));
|
|
788
|
+
checks.push({
|
|
789
|
+
check: `signature:${signature.keyId}`,
|
|
790
|
+
ok: signatureOk,
|
|
791
|
+
detail: signatureOk ? 'cryptographically valid' : 'invalid signature or signed-payload digest',
|
|
792
|
+
});
|
|
793
|
+
if (!signatureOk) {
|
|
794
|
+
errors.push(`Invalid signature from '${signature.keyId}'`);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
const trustedKey = options.trustStore?.keys.find((key) => key.keyId === signature.keyId);
|
|
798
|
+
if (!trustedKey) {
|
|
799
|
+
warnings.push(`Signature key '${signature.keyId}' is not in the local trust store`);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (trustedKey.publicKey !== signature.publicKey) {
|
|
803
|
+
errors.push(`Trusted key material mismatch for '${signature.keyId}'`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const trust = verifyTrustedKey(trustedKey, options.trustStore, options.at ?? new Date());
|
|
807
|
+
checks.push({ check: `trust:${signature.keyId}`, ok: trust.ok, detail: trust.detail });
|
|
808
|
+
if (!trust.ok)
|
|
809
|
+
errors.push(trust.detail);
|
|
810
|
+
else if (trustedKey.publisher !== envelope.publisher.id)
|
|
811
|
+
errors.push(`Trusted key publisher mismatch for '${signature.keyId}'`);
|
|
812
|
+
else
|
|
813
|
+
trustedSigner = signature.keyId;
|
|
814
|
+
}
|
|
815
|
+
if (envelope.signatures.length === 0)
|
|
816
|
+
warnings.push('Envelope is unsigned');
|
|
817
|
+
if (policy.requireSignature && !trustedSigner)
|
|
818
|
+
errors.push('Policy requires a valid signature chained to a local trust root');
|
|
819
|
+
if (!trustedSigner && !policy.allowIntegrityOnly)
|
|
820
|
+
errors.push('Policy does not permit integrity-only verification');
|
|
821
|
+
const ok = errors.length === 0;
|
|
822
|
+
const status = !ok ? 'failed' : trustedSigner ? 'verified' : envelope.signatures.length ? 'untrusted' : 'integrity-only';
|
|
823
|
+
return {
|
|
824
|
+
ok,
|
|
825
|
+
status,
|
|
826
|
+
lock,
|
|
827
|
+
envelopeSha256,
|
|
828
|
+
...(trustedSigner ? { signer: trustedSigner } : {}),
|
|
829
|
+
checks,
|
|
830
|
+
errors,
|
|
831
|
+
warnings,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
//# sourceMappingURL=provenance.js.map
|