@aiwg/cli 2026.8.11 → 2026.8.13

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.
Files changed (42) hide show
  1. package/bin/aiwg.mjs +2 -0
  2. package/dist/src/api/index.d.ts +6 -0
  3. package/dist/src/api/index.js +6 -0
  4. package/dist/src/cli/handlers/artifact-verify.js +171 -0
  5. package/dist/src/cli/handlers/index.js +3 -1
  6. package/dist/src/cli/handlers/setup-manifest.js +52 -3
  7. package/dist/src/cli/handlers/setup.js +15 -2
  8. package/dist/src/cli/handlers/use.js +71 -6
  9. package/dist/src/cli/scope-resolver.js +6 -1
  10. package/dist/src/cli/services/deployment-verification.js +65 -7
  11. package/dist/src/config/aiwg-config.js +4 -3
  12. package/dist/src/config/cli.js +3 -1
  13. package/dist/src/config/gitignore.js +67 -21
  14. package/dist/src/config/workspace.js +8 -1
  15. package/dist/src/extensions/commands/definitions.js +19 -0
  16. package/dist/src/extensions/project-quickref.js +9 -0
  17. package/dist/src/marketplace/artifact-attestation.js +195 -0
  18. package/dist/src/marketplace/exchange.js +437 -79
  19. package/dist/src/marketplace/provenance-types.js +1 -0
  20. package/dist/src/marketplace/provenance.js +7 -1
  21. package/dist/src/providers/hermes-home.js +20 -0
  22. package/dist/src/providers/provider-definitions.js +5 -4
  23. package/dist/src/providers/transformation-receipt-integration.js +448 -0
  24. package/dist/src/providers/transformation-receipt.js +215 -0
  25. package/dist/src/resources/web-release.d.ts +11 -0
  26. package/dist/src/resources/web-release.js +61 -6
  27. package/dist/src/security/artifact-attestation.js +117 -0
  28. package/dist/src/security/artifact-trust.js +557 -0
  29. package/dist/src/security/artifact-verifier.js +478 -0
  30. package/dist/src/skills/deployer.js +5 -1
  31. package/dist/src/tracker/capability-protocol.js +7 -2
  32. package/package.json +5 -1
  33. package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
  34. package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
  35. package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
  36. package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
  37. package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
  38. package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
  39. package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
  40. package/tools/agents/deploy-agents.mjs +8 -2
  41. package/tools/agents/providers/base.mjs +29 -2
  42. package/tools/agents/providers/hermes.mjs +163 -19
@@ -0,0 +1,557 @@
1
+ import { constants as cryptoConstants, createHash, createPublicKey, verify as verifySignature, } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ export const ARTIFACT_TRUST_ROOT_MEDIA_TYPE = 'application/vnd.aiwg.artifact-trust-root.v1+json';
5
+ export const ARTIFACT_TRUST_ROOT_SCHEMA_VERSION = 'aiwg.artifact-trust-root.v1';
6
+ export const ARTIFACT_TRUST_STATE_SCHEMA_VERSION = 'aiwg.artifact-trust-state.v1';
7
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
8
+ const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
9
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+={0,2}$/;
10
+ function isRecord(value) {
11
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
12
+ }
13
+ function assertKeys(value, allowed, label) {
14
+ const unknown = Object.keys(value).filter(key => !allowed.includes(key));
15
+ if (unknown.length > 0)
16
+ throw new Error(`${label} contains unknown field '${unknown[0]}'`);
17
+ }
18
+ function assertIsoDate(value, label) {
19
+ const parsed = Date.parse(value);
20
+ if (!Number.isFinite(parsed))
21
+ throw new Error(`${label} must be an RFC 3339 date-time`);
22
+ return parsed;
23
+ }
24
+ function assertUniqueStrings(values, label) {
25
+ if (!Array.isArray(values) || values.length === 0 || values.some(value => typeof value !== 'string' || value.length === 0)) {
26
+ throw new Error(`${label} must contain at least one non-empty string`);
27
+ }
28
+ if (new Set(values).size !== values.length)
29
+ throw new Error(`${label} must not contain duplicates`);
30
+ }
31
+ function assertScope(scope, label) {
32
+ if (!isRecord(scope))
33
+ throw new Error(`${label} must be an object`);
34
+ assertKeys(scope, ['assetTypes', 'namespaces', 'channels'], label);
35
+ assertUniqueStrings(scope.assetTypes, `${label}.assetTypes`);
36
+ assertUniqueStrings(scope.namespaces, `${label}.namespaces`);
37
+ assertUniqueStrings(scope.channels, `${label}.channels`);
38
+ }
39
+ function patternMatches(pattern, value) {
40
+ if (pattern === '*')
41
+ return true;
42
+ if (pattern.endsWith('*'))
43
+ return value.startsWith(pattern.slice(0, -1));
44
+ return pattern === value;
45
+ }
46
+ function patternContains(parent, child) {
47
+ if (parent === '*')
48
+ return true;
49
+ if (!parent.endsWith('*'))
50
+ return parent === child;
51
+ const prefix = parent.slice(0, -1);
52
+ return child.startsWith(prefix);
53
+ }
54
+ function dimensionContains(parent, child) {
55
+ return child.every(childPattern => parent.some(parentPattern => patternContains(parentPattern, childPattern)));
56
+ }
57
+ export function scopeContains(parent, child) {
58
+ return dimensionContains(parent.assetTypes, child.assetTypes)
59
+ && dimensionContains(parent.namespaces, child.namespaces)
60
+ && dimensionContains(parent.channels, child.channels);
61
+ }
62
+ export function scopeMatches(scope, input) {
63
+ return scope.assetTypes.some(pattern => patternMatches(pattern, input.assetType))
64
+ && scope.namespaces.some(pattern => patternMatches(pattern, input.namespace))
65
+ && scope.channels.some(pattern => patternMatches(pattern, input.channel));
66
+ }
67
+ /** RFC 8785-compatible for JSON values accepted by AIWG metadata schemas. */
68
+ export function canonicalJson(value) {
69
+ if (value === null || typeof value === 'boolean' || typeof value === 'string')
70
+ return JSON.stringify(value);
71
+ if (typeof value === 'number') {
72
+ if (!Number.isFinite(value))
73
+ throw new Error('Canonical JSON does not permit non-finite numbers');
74
+ return JSON.stringify(value);
75
+ }
76
+ if (Array.isArray(value))
77
+ return `[${value.map(entry => canonicalJson(entry)).join(',')}]`;
78
+ if (isRecord(value)) {
79
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
80
+ }
81
+ throw new Error(`Canonical JSON cannot encode ${typeof value}`);
82
+ }
83
+ export function sha256(bytes) {
84
+ return createHash('sha256').update(bytes).digest('hex');
85
+ }
86
+ export function dssePae(payloadType, payload) {
87
+ const type = Buffer.from(payloadType, 'utf8');
88
+ const body = Buffer.from(payload);
89
+ return Buffer.concat([
90
+ Buffer.from(`DSSEv1 ${type.length} `, 'utf8'),
91
+ type,
92
+ Buffer.from(` ${body.length} `, 'utf8'),
93
+ body,
94
+ ]);
95
+ }
96
+ export function decodeBase64(value, label) {
97
+ if (BASE64_PATTERN.test(value)) {
98
+ const decoded = Buffer.from(value, 'base64');
99
+ if (decoded.toString('base64') !== value)
100
+ throw new Error(`${label} is not canonical standard base64`);
101
+ return decoded;
102
+ }
103
+ if (!BASE64URL_PATTERN.test(value))
104
+ throw new Error(`${label} must use valid standard or URL-safe base64`);
105
+ const unpadded = value.replace(/=+$/, '');
106
+ if (unpadded.length % 4 === 1)
107
+ throw new Error(`${label} has an invalid base64 length`);
108
+ const requiredPadding = (4 - (unpadded.length % 4)) % 4;
109
+ const suppliedPadding = value.length - unpadded.length;
110
+ if (suppliedPadding !== 0 && suppliedPadding !== requiredPadding)
111
+ throw new Error(`${label} has invalid base64 padding`);
112
+ const decoded = Buffer.from(unpadded, 'base64url');
113
+ if (decoded.toString('base64url') !== unpadded)
114
+ throw new Error(`${label} is not canonical URL-safe base64`);
115
+ return decoded;
116
+ }
117
+ export function publicKeyObject(publicKey) {
118
+ if (publicKey.includes('-----BEGIN'))
119
+ return createPublicKey(publicKey);
120
+ return createPublicKey({ key: decodeBase64(publicKey, 'publicKey'), format: 'der', type: 'spki' });
121
+ }
122
+ export function publicKeyFingerprint(publicKey) {
123
+ const key = publicKeyObject(publicKey);
124
+ const der = key.export({ format: 'der', type: 'spki' });
125
+ return sha256(der);
126
+ }
127
+ export function verifyBytes(algorithm, publicKey, payload, signature) {
128
+ const key = publicKeyObject(publicKey);
129
+ if (algorithm === 'ed25519')
130
+ return verifySignature(null, payload, key, signature);
131
+ if (algorithm === 'ecdsa-p256-sha256')
132
+ return verifySignature('sha256', payload, key, signature);
133
+ return verifySignature('sha256', payload, {
134
+ key,
135
+ padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
136
+ saltLength: cryptoConstants.RSA_PSS_SALTLEN_DIGEST,
137
+ }, signature);
138
+ }
139
+ function validateRole(role, label, identities, requirePublicKeys) {
140
+ if (!isRecord(role))
141
+ throw new Error(`${label} must be an object`);
142
+ assertKeys(role, label.startsWith('delegation')
143
+ ? ['id', 'parent', 'keyIds', 'threshold', 'scope', 'notBefore', 'expiresAt']
144
+ : ['keyIds', 'threshold', 'scope'], label);
145
+ assertUniqueStrings(role.keyIds, `${label}.keyIds`);
146
+ if (!Number.isInteger(role.threshold) || role.threshold < 1)
147
+ throw new Error(`${label}.threshold must be a positive integer`);
148
+ assertScope(role.scope, `${label}.scope`);
149
+ const groups = new Set();
150
+ for (const id of role.keyIds) {
151
+ const identity = identities.get(id);
152
+ if (!identity)
153
+ throw new Error(`${label} references unknown identity '${id}'`);
154
+ if (requirePublicKeys && identity.kind !== 'public-key')
155
+ throw new Error(`${label} root identities must use public keys`);
156
+ groups.add(identity.independenceGroup);
157
+ }
158
+ if (role.threshold > groups.size) {
159
+ throw new Error(`${label}.threshold exceeds the number of independent identities`);
160
+ }
161
+ }
162
+ export function validateTrustRoot(root) {
163
+ if (!isRecord(root))
164
+ throw new Error('trust root must be an object');
165
+ assertKeys(root, ['mediaType', 'signed', 'signatures'], 'trust root');
166
+ if (root.mediaType !== ARTIFACT_TRUST_ROOT_MEDIA_TYPE)
167
+ throw new Error('unsupported trust-root media type');
168
+ if (!isRecord(root.signed))
169
+ throw new Error('trust root signed payload must be an object');
170
+ assertKeys(root.signed, ['schemaVersion', 'version', 'issuedAt', 'expiresAt', 'identities', 'sigstoreProfiles', 'root', 'delegations', 'revocations', 'policy'], 'trust root signed payload');
171
+ if (root.signed.schemaVersion !== ARTIFACT_TRUST_ROOT_SCHEMA_VERSION)
172
+ throw new Error('unsupported trust-root schema version');
173
+ if (!Number.isInteger(root.signed.version) || root.signed.version < 1)
174
+ throw new Error('root version must be a positive integer');
175
+ const issuedAt = assertIsoDate(root.signed.issuedAt, 'root.issuedAt');
176
+ const expiresAt = assertIsoDate(root.signed.expiresAt, 'root.expiresAt');
177
+ if (expiresAt <= issuedAt)
178
+ throw new Error('root expiry must be after issuance');
179
+ if (!Array.isArray(root.signed.identities) || root.signed.identities.length === 0)
180
+ throw new Error('root identities must not be empty');
181
+ const identities = new Map();
182
+ const authorities = new Map();
183
+ for (const identity of root.signed.identities) {
184
+ if (!isRecord(identity) || typeof identity.id !== 'string' || !identity.id)
185
+ throw new Error('identity.id is required');
186
+ if (identities.has(identity.id))
187
+ throw new Error(`duplicate identity '${identity.id}'`);
188
+ if (typeof identity.independenceGroup !== 'string' || !identity.independenceGroup) {
189
+ throw new Error(`identity '${identity.id}' requires independenceGroup`);
190
+ }
191
+ if (identity.kind === 'public-key') {
192
+ assertKeys(identity, ['id', 'independenceGroup', 'kind', 'algorithm', 'publicKey'], `identity '${identity.id}'`);
193
+ if (!['ed25519', 'ecdsa-p256-sha256', 'rsa-pss-sha256'].includes(identity.algorithm)) {
194
+ throw new Error(`identity '${identity.id}' has unsupported algorithm`);
195
+ }
196
+ publicKeyObject(identity.publicKey);
197
+ const authority = `public-key:${publicKeyFingerprint(identity.publicKey)}`;
198
+ if (authorities.has(authority))
199
+ throw new Error(`identity '${identity.id}' duplicates cryptographic authority '${authorities.get(authority)}'`);
200
+ authorities.set(authority, identity.id);
201
+ }
202
+ else if (identity.kind === 'sigstore') {
203
+ assertKeys(identity, ['id', 'independenceGroup', 'kind', 'profile', 'subjectAlternativeName', 'issuer'], `identity '${identity.id}'`);
204
+ if (!identity.profile || !identity.subjectAlternativeName)
205
+ throw new Error(`Sigstore identity '${identity.id}' is incomplete`);
206
+ try {
207
+ new RegExp(identity.subjectAlternativeName, 'u');
208
+ }
209
+ catch (error) {
210
+ throw new Error(`Sigstore identity '${identity.id}' has invalid subjectAlternativeName: ${String(error)}`);
211
+ }
212
+ const authority = `sigstore:${identity.profile}:${identity.issuer ?? ''}:${identity.subjectAlternativeName}`;
213
+ if (authorities.has(authority))
214
+ throw new Error(`identity '${identity.id}' duplicates cryptographic authority '${authorities.get(authority)}'`);
215
+ authorities.set(authority, identity.id);
216
+ }
217
+ else {
218
+ throw new Error(`identity '${identity.id}' has unsupported kind`);
219
+ }
220
+ identities.set(identity.id, identity);
221
+ }
222
+ if (!Array.isArray(root.signed.sigstoreProfiles))
223
+ throw new Error('sigstoreProfiles must be an array');
224
+ const profiles = new Set();
225
+ for (const profile of root.signed.sigstoreProfiles) {
226
+ if (!isRecord(profile) || typeof profile.id !== 'string' || !profile.id)
227
+ throw new Error('Sigstore profile id is required');
228
+ assertKeys(profile, ['id', 'trustedRoot', 'tlogThreshold', 'ctlogThreshold', 'timestampThreshold'], `Sigstore profile '${profile.id}'`);
229
+ if (profiles.has(profile.id))
230
+ throw new Error(`duplicate Sigstore profile '${profile.id}'`);
231
+ if (!isRecord(profile.trustedRoot))
232
+ throw new Error(`Sigstore profile '${profile.id}' requires trustedRoot`);
233
+ for (const field of ['tlogThreshold', 'ctlogThreshold', 'timestampThreshold']) {
234
+ if (!Number.isInteger(profile[field]) || profile[field] < 0)
235
+ throw new Error(`Sigstore profile '${profile.id}' ${field} must be non-negative`);
236
+ }
237
+ profiles.add(profile.id);
238
+ }
239
+ for (const identity of identities.values()) {
240
+ if (identity.kind === 'sigstore' && !profiles.has(identity.profile)) {
241
+ throw new Error(`Sigstore identity '${identity.id}' references unknown profile '${identity.profile}'`);
242
+ }
243
+ }
244
+ validateRole(root.signed.root, 'root role', identities, true);
245
+ if (!Array.isArray(root.signed.delegations))
246
+ throw new Error('delegations must be an array');
247
+ const delegations = new Map();
248
+ for (const delegation of root.signed.delegations) {
249
+ if (!isRecord(delegation) || typeof delegation.id !== 'string' || !delegation.id)
250
+ throw new Error('delegation.id is required');
251
+ if (delegations.has(delegation.id) || delegation.id === 'root')
252
+ throw new Error(`duplicate or reserved delegation '${delegation.id}'`);
253
+ validateRole(delegation, `delegation '${delegation.id}'`, identities, false);
254
+ if (delegation.notBefore)
255
+ assertIsoDate(delegation.notBefore, `delegation '${delegation.id}'.notBefore`);
256
+ if (delegation.expiresAt)
257
+ assertIsoDate(delegation.expiresAt, `delegation '${delegation.id}'.expiresAt`);
258
+ if (delegation.notBefore && delegation.expiresAt && Date.parse(delegation.expiresAt) <= Date.parse(delegation.notBefore)) {
259
+ throw new Error(`delegation '${delegation.id}' validity window must be ordered`);
260
+ }
261
+ delegations.set(delegation.id, delegation);
262
+ }
263
+ const visiting = new Set();
264
+ const visited = new Set();
265
+ const verifyParent = (delegation) => {
266
+ if (visited.has(delegation.id))
267
+ return;
268
+ if (visiting.has(delegation.id))
269
+ throw new Error(`delegation cycle at '${delegation.id}'`);
270
+ visiting.add(delegation.id);
271
+ const parent = delegation.parent === 'root' ? root.signed.root : delegations.get(delegation.parent);
272
+ if (!parent)
273
+ throw new Error(`delegation '${delegation.id}' references unknown parent '${delegation.parent}'`);
274
+ if (delegation.parent !== 'root')
275
+ verifyParent(parent);
276
+ if (!scopeContains(parent.scope, delegation.scope))
277
+ throw new Error(`delegation '${delegation.id}' expands parent scope`);
278
+ if (delegation.parent !== 'root') {
279
+ const parentDelegation = parent;
280
+ if (parentDelegation.notBefore && (!delegation.notBefore || Date.parse(delegation.notBefore) < Date.parse(parentDelegation.notBefore))) {
281
+ throw new Error(`delegation '${delegation.id}' expands parent validity window`);
282
+ }
283
+ if (parentDelegation.expiresAt && (!delegation.expiresAt || Date.parse(delegation.expiresAt) > Date.parse(parentDelegation.expiresAt))) {
284
+ throw new Error(`delegation '${delegation.id}' expands parent validity window`);
285
+ }
286
+ }
287
+ visiting.delete(delegation.id);
288
+ visited.add(delegation.id);
289
+ };
290
+ for (const delegation of delegations.values())
291
+ verifyParent(delegation);
292
+ if (!Array.isArray(root.signed.revocations))
293
+ throw new Error('revocations must be an array');
294
+ for (const revocation of root.signed.revocations) {
295
+ if (!isRecord(revocation) || !identities.has(revocation.identityId))
296
+ throw new Error('revocation references an unknown identity');
297
+ assertKeys(revocation, ['identityId', 'effectiveAt', 'compromisedFrom', 'compromisedUntil', 'scope', 'reason'], 'revocation');
298
+ const effectiveAt = assertIsoDate(revocation.effectiveAt, 'revocation.effectiveAt');
299
+ if (revocation.compromisedFrom)
300
+ assertIsoDate(revocation.compromisedFrom, 'revocation.compromisedFrom');
301
+ if (revocation.compromisedUntil) {
302
+ const until = assertIsoDate(revocation.compromisedUntil, 'revocation.compromisedUntil');
303
+ const from = assertIsoDate(revocation.compromisedFrom ?? revocation.effectiveAt, 'revocation compromise start');
304
+ if (until <= from)
305
+ throw new Error('revocation compromise interval must be ordered');
306
+ }
307
+ if (!revocation.reason)
308
+ throw new Error('revocation reason is required');
309
+ assertScope(revocation.scope, 'revocation.scope');
310
+ if (!Number.isFinite(effectiveAt))
311
+ throw new Error('revocation effectiveAt is invalid');
312
+ }
313
+ if (!isRecord(root.signed.policy) || !root.signed.policy.name)
314
+ throw new Error('policy name is required');
315
+ assertKeys(root.signed.policy, ['name', 'requireMaterialDigests', 'maxFreezeSeconds', 'allowPolicyExempt', 'marketplace'], 'policy');
316
+ if (typeof root.signed.policy.requireMaterialDigests !== 'boolean')
317
+ throw new Error('policy requireMaterialDigests must be boolean');
318
+ if (!Number.isInteger(root.signed.policy.maxFreezeSeconds) || root.signed.policy.maxFreezeSeconds < 0) {
319
+ throw new Error('policy maxFreezeSeconds must be a non-negative integer');
320
+ }
321
+ if (!Array.isArray(root.signed.policy.allowPolicyExempt))
322
+ throw new Error('policy allowPolicyExempt must be an array');
323
+ root.signed.policy.allowPolicyExempt.forEach((scope, index) => assertScope(scope, `policy.allowPolicyExempt[${index}]`));
324
+ if (root.signed.policy.marketplace !== undefined) {
325
+ const marketplace = root.signed.policy.marketplace;
326
+ if (!isRecord(marketplace))
327
+ throw new Error('policy marketplace must be an object');
328
+ assertKeys(marketplace, ['evidenceMode', 'legacySignatureMigrationGate', 'recursiveDependencies'], 'policy marketplace');
329
+ if (!['marketplace-only', 'dual-required', 'cross-asset-required'].includes(marketplace.evidenceMode))
330
+ throw new Error('policy marketplace evidenceMode is invalid');
331
+ if (typeof marketplace.legacySignatureMigrationGate !== 'boolean')
332
+ throw new Error('policy marketplace legacySignatureMigrationGate must be boolean');
333
+ if (!['if-present', 'required'].includes(marketplace.recursiveDependencies))
334
+ throw new Error('policy marketplace recursiveDependencies is invalid');
335
+ if (marketplace.legacySignatureMigrationGate && marketplace.evidenceMode !== 'cross-asset-required') {
336
+ throw new Error('policy marketplace migration gate requires cross-asset-required mode');
337
+ }
338
+ }
339
+ if (!Array.isArray(root.signatures) || root.signatures.length === 0)
340
+ throw new Error('trust root signatures must not be empty');
341
+ root.signatures.forEach((signature, index) => {
342
+ if (!isRecord(signature))
343
+ throw new Error(`trust root signature ${index} must be an object`);
344
+ assertKeys(signature, ['identityId', 'sig'], `trust root signature ${index}`);
345
+ if (typeof signature.sig !== 'string')
346
+ throw new Error(`trust root signature ${index} requires sig`);
347
+ });
348
+ }
349
+ export function parseTrustRoot(bytes) {
350
+ let value;
351
+ try {
352
+ value = JSON.parse(Buffer.from(bytes).toString('utf8'));
353
+ }
354
+ catch (error) {
355
+ throw new Error(`trust root is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
356
+ }
357
+ validateTrustRoot(value);
358
+ return value;
359
+ }
360
+ export function trustRootSigningBytes(root) {
361
+ return Buffer.from(canonicalJson(root.signed), 'utf8');
362
+ }
363
+ export function verifyThresholdSignatures(payload, signatures, allowedIdentityIds, threshold, identities) {
364
+ const allowed = identities.filter((identity) => (allowedIdentityIds.includes(identity.id) && identity.kind === 'public-key'));
365
+ const acceptedIds = new Set();
366
+ const acceptedGroups = new Set();
367
+ for (const signatureRecord of signatures) {
368
+ let signature;
369
+ try {
370
+ signature = decodeBase64(signatureRecord.sig, 'signature');
371
+ }
372
+ catch {
373
+ continue;
374
+ }
375
+ const hinted = signatureRecord.identityId
376
+ ? allowed.filter(identity => identity.id === signatureRecord.identityId)
377
+ : [];
378
+ const candidates = [...hinted, ...allowed.filter(identity => !hinted.includes(identity))];
379
+ for (const identity of candidates) {
380
+ if (acceptedIds.has(identity.id))
381
+ continue;
382
+ try {
383
+ if (!verifyBytes(identity.algorithm, identity.publicKey, payload, signature))
384
+ continue;
385
+ }
386
+ catch {
387
+ continue;
388
+ }
389
+ acceptedIds.add(identity.id);
390
+ acceptedGroups.add(identity.independenceGroup);
391
+ break;
392
+ }
393
+ }
394
+ return {
395
+ identityIds: [...acceptedIds].sort(),
396
+ independenceGroups: [...acceptedGroups].sort(),
397
+ threshold,
398
+ satisfied: acceptedGroups.size >= threshold,
399
+ };
400
+ }
401
+ export function createInitialTrustState(root, rootSha256, now) {
402
+ return {
403
+ schemaVersion: ARTIFACT_TRUST_STATE_SCHEMA_VERSION,
404
+ rootVersion: root.signed.version,
405
+ rootSha256,
406
+ trustedTime: now,
407
+ channels: {},
408
+ };
409
+ }
410
+ export function bootstrapTrustRoot(rootBytes, expectedSha256, now = new Date().toISOString()) {
411
+ if (!SHA256_PATTERN.test(expectedSha256))
412
+ throw new Error('bootstrap fingerprint must be lowercase SHA-256');
413
+ const rootSha256 = sha256(rootBytes);
414
+ if (rootSha256 !== expectedSha256)
415
+ throw new Error('bootstrap fingerprint does not match the exact trust-root bytes');
416
+ const root = parseTrustRoot(rootBytes);
417
+ const nowMs = assertIsoDate(now, 'bootstrap time');
418
+ if (nowMs < assertIsoDate(root.signed.issuedAt, 'root.issuedAt') || nowMs > assertIsoDate(root.signed.expiresAt, 'root.expiresAt')) {
419
+ throw new Error('initial trust root is outside its validity window');
420
+ }
421
+ const threshold = verifyThresholdSignatures(trustRootSigningBytes(root), root.signatures, root.signed.root.keyIds, root.signed.root.threshold, root.signed.identities);
422
+ if (!threshold.satisfied)
423
+ throw new Error('initial trust root does not satisfy its independent signature threshold');
424
+ return {
425
+ root,
426
+ rootSha256,
427
+ authorizedIdentities: threshold.identityIds,
428
+ state: createInitialTrustState(root, rootSha256, now),
429
+ };
430
+ }
431
+ export function validateTrustState(state) {
432
+ if (!isRecord(state) || state.schemaVersion !== ARTIFACT_TRUST_STATE_SCHEMA_VERSION) {
433
+ throw new Error('unsupported trust-state schema version');
434
+ }
435
+ assertKeys(state, ['schemaVersion', 'rootVersion', 'rootSha256', 'trustedTime', 'channels'], 'trust state');
436
+ if (!Number.isInteger(state.rootVersion) || state.rootVersion < 1)
437
+ throw new Error('trust-state rootVersion must be positive');
438
+ if (!SHA256_PATTERN.test(state.rootSha256))
439
+ throw new Error('trust-state rootSha256 is invalid');
440
+ assertIsoDate(state.trustedTime, 'trust-state trustedTime');
441
+ if (!isRecord(state.channels))
442
+ throw new Error('trust-state channels must be an object');
443
+ for (const [key, channel] of Object.entries(state.channels)) {
444
+ if (!isRecord(channel) || !channel.namespace || !channel.channel || !channel.version)
445
+ throw new Error(`trust-state channel '${key}' is incomplete`);
446
+ assertKeys(channel, ['namespace', 'channel', 'subject', 'assetType', 'sequence', 'artifactSha256', 'version', 'verifiedAt'], `trust-state channel '${key}'`);
447
+ if ((channel.subject === undefined) !== (channel.assetType === undefined))
448
+ throw new Error(`trust-state channel '${key}' subject and assetType must appear together`);
449
+ if (channel.subject !== undefined && (typeof channel.subject !== 'string' || !channel.subject || typeof channel.assetType !== 'string' || !channel.assetType)) {
450
+ throw new Error(`trust-state channel '${key}' subject scope is invalid`);
451
+ }
452
+ if (key !== channelStateKey(String(channel.namespace), String(channel.channel), channel.subject === undefined ? undefined : {
453
+ assetType: String(channel.assetType),
454
+ subject: String(channel.subject),
455
+ }))
456
+ throw new Error(`trust-state channel '${key}' key does not match its scope`);
457
+ if (!Number.isInteger(channel.sequence) || channel.sequence < 1)
458
+ throw new Error(`trust-state channel '${key}' sequence is invalid`);
459
+ if (!SHA256_PATTERN.test(channel.artifactSha256))
460
+ throw new Error(`trust-state channel '${key}' digest is invalid`);
461
+ assertIsoDate(channel.verifiedAt, `trust-state channel '${key}' verifiedAt`);
462
+ }
463
+ }
464
+ export function parseTrustState(bytes) {
465
+ let value;
466
+ try {
467
+ value = JSON.parse(Buffer.from(bytes).toString('utf8'));
468
+ }
469
+ catch (error) {
470
+ throw new Error(`trust state is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
471
+ }
472
+ validateTrustState(value);
473
+ return value;
474
+ }
475
+ export function readTrustState(file) {
476
+ return parseTrustState(readFileSync(file));
477
+ }
478
+ export function writeTrustState(file, state) {
479
+ validateTrustState(state);
480
+ const directory = path.dirname(file);
481
+ mkdirSync(directory, { recursive: true });
482
+ const temporary = path.join(directory, `.${path.basename(file)}.${process.pid}.tmp`);
483
+ writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
484
+ renameSync(temporary, file);
485
+ }
486
+ export function verifyRootTransition(currentBytes, nextBytes, state, now = new Date().toISOString()) {
487
+ const current = parseTrustRoot(currentBytes);
488
+ validateTrustState(state);
489
+ const currentSha256 = sha256(currentBytes);
490
+ if (state.rootVersion !== current.signed.version)
491
+ throw new Error('persisted root version does not match the current root');
492
+ if (state.rootSha256 !== currentSha256)
493
+ throw new Error('persisted root digest does not match the exact current root bytes');
494
+ const next = parseTrustRoot(nextBytes);
495
+ if (next.signed.version !== current.signed.version + 1) {
496
+ throw new Error('root update must be exactly the next version; rollback and fast-forward are rejected');
497
+ }
498
+ const nowMs = assertIsoDate(now, 'root update time');
499
+ if (nowMs < Date.parse(state.trustedTime))
500
+ throw new Error('root update time predates persisted trusted time');
501
+ if (nowMs > assertIsoDate(current.signed.expiresAt, 'current root expiry'))
502
+ throw new Error('current root is expired');
503
+ if (nowMs < assertIsoDate(next.signed.issuedAt, 'next root issuance') || nowMs > assertIsoDate(next.signed.expiresAt, 'next root expiry')) {
504
+ throw new Error('next root is outside its validity window');
505
+ }
506
+ if (Date.parse(next.signed.issuedAt) < Date.parse(current.signed.issuedAt))
507
+ throw new Error('next root issuance cannot predate the current root');
508
+ const payload = trustRootSigningBytes(next);
509
+ const oldThreshold = verifyThresholdSignatures(payload, next.signatures, current.signed.root.keyIds, current.signed.root.threshold, current.signed.identities);
510
+ const newThreshold = verifyThresholdSignatures(payload, next.signatures, next.signed.root.keyIds, next.signed.root.threshold, next.signed.identities);
511
+ if (!oldThreshold.satisfied || !newThreshold.satisfied) {
512
+ throw new Error('root update must satisfy both old and new independent signature thresholds');
513
+ }
514
+ const rootSha256 = sha256(nextBytes);
515
+ return {
516
+ rootSha256,
517
+ oldAuthorizedIdentities: oldThreshold.identityIds,
518
+ newAuthorizedIdentities: newThreshold.identityIds,
519
+ state: {
520
+ ...state,
521
+ rootVersion: next.signed.version,
522
+ rootSha256,
523
+ trustedTime: new Date(Math.max(Date.parse(state.trustedTime), nowMs)).toISOString(),
524
+ },
525
+ };
526
+ }
527
+ export function channelStateKey(namespace, channel, member) {
528
+ const base = `${encodeURIComponent(namespace)}::${encodeURIComponent(channel)}`;
529
+ return member
530
+ ? `${base}::${encodeURIComponent(member.assetType)}::${encodeURIComponent(member.subject)}`
531
+ : base;
532
+ }
533
+ export function selectDelegations(root, input) {
534
+ const nowMs = assertIsoDate(input.now, 'verification time');
535
+ return root.signed.delegations.filter(delegation => {
536
+ if (!scopeMatches(delegation.scope, input))
537
+ return false;
538
+ if (delegation.notBefore && nowMs < Date.parse(delegation.notBefore))
539
+ return false;
540
+ if (delegation.expiresAt && nowMs > Date.parse(delegation.expiresAt))
541
+ return false;
542
+ return true;
543
+ });
544
+ }
545
+ export function isIdentityRevoked(root, identityId, input) {
546
+ const issuedAt = assertIsoDate(input.issuedAt, 'artifact issuedAt');
547
+ const now = assertIsoDate(input.now, 'verification time');
548
+ return root.signed.revocations.find(revocation => {
549
+ if (revocation.identityId !== identityId || !scopeMatches(revocation.scope, input))
550
+ return false;
551
+ const effectiveAt = Date.parse(revocation.effectiveAt);
552
+ const from = Date.parse(revocation.compromisedFrom ?? revocation.effectiveAt);
553
+ const until = revocation.compromisedUntil ? Date.parse(revocation.compromisedUntil) : Number.POSITIVE_INFINITY;
554
+ return now >= effectiveAt || (issuedAt >= from && issuedAt <= until);
555
+ });
556
+ }
557
+ //# sourceMappingURL=artifact-trust.js.map