@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,215 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lstat, mkdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ export const PROVIDER_TRANSFORMATION_RECEIPT_SCHEMA = 'aiwg.provider-transformation-receipt.v1';
5
+ const SHA256 = /^[a-f0-9]{64}$/;
6
+ const IDENTIFIER = /^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$/;
7
+ const FILENAME_SEGMENT = /^[a-zA-Z0-9][a-zA-Z0-9._+-]*$/;
8
+ function digest(bytes) {
9
+ return createHash('sha256').update(bytes).digest('hex');
10
+ }
11
+ function portableRelative(value, label) {
12
+ const normalized = value.replaceAll('\\', '/').replace(/^\.\//, '');
13
+ if (!normalized || normalized.startsWith('/') || normalized.includes('\0')) {
14
+ throw new Error(`${label} must be a portable relative path`);
15
+ }
16
+ if (normalized.split('/').some(part => !part || part === '.' || part === '..')) {
17
+ throw new Error(`${label} must not escape its portable root`);
18
+ }
19
+ return normalized;
20
+ }
21
+ function identifier(value, label) {
22
+ if (!IDENTIFIER.test(value))
23
+ throw new Error(`${label} contains a non-portable identifier`);
24
+ return value;
25
+ }
26
+ function filenameSegment(value, label) {
27
+ if (!FILENAME_SEGMENT.test(value) || value === '.' || value === '..') {
28
+ throw new Error(`${label} must be a portable filename segment`);
29
+ }
30
+ return value;
31
+ }
32
+ function sha(value, label) {
33
+ if (!SHA256.test(value))
34
+ throw new Error(`${label} must be a lowercase SHA-256 digest`);
35
+ return value;
36
+ }
37
+ async function readRegularOutput(root, relative) {
38
+ const canonicalRoot = await realpath(root);
39
+ const absolute = path.resolve(canonicalRoot, relative);
40
+ const metadata = await lstat(absolute);
41
+ if (metadata.isSymbolicLink() || !metadata.isFile()) {
42
+ throw new Error(`receipt output '${relative}' must be a regular file and must not be a symbolic link`);
43
+ }
44
+ const canonicalOutput = await realpath(absolute);
45
+ const containment = path.relative(canonicalRoot, canonicalOutput);
46
+ if (containment === '..' || containment.startsWith(`..${path.sep}`) || path.isAbsolute(containment)) {
47
+ throw new Error(`receipt output '${relative}' resolves outside its configured root`);
48
+ }
49
+ return readFile(canonicalOutput);
50
+ }
51
+ export function providerTransformationReceiptPath(projectRoot, provider, scope) {
52
+ filenameSegment(provider, 'provider');
53
+ if (scope !== 'project' && scope !== 'user')
54
+ throw new Error('scope must be project or user');
55
+ return path.join(projectRoot, '.aiwg', 'receipts', 'providers', `${provider}.${scope}.json`);
56
+ }
57
+ export function validateProviderTransformationReceipt(value) {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value))
59
+ throw new Error('receipt must be an object');
60
+ const receipt = value;
61
+ if (receipt.schemaVersion !== PROVIDER_TRANSFORMATION_RECEIPT_SCHEMA)
62
+ throw new Error('unsupported receipt schema');
63
+ if (!Number.isFinite(Date.parse(receipt.generatedAt)))
64
+ throw new Error('generatedAt must be an RFC 3339 date-time');
65
+ if (receipt.scope !== 'project' && receipt.scope !== 'user')
66
+ throw new Error('scope must be project or user');
67
+ filenameSegment(receipt.provider, 'provider');
68
+ identifier(receipt.source?.subject, 'source.subject');
69
+ sha(receipt.source?.sha256, 'source.sha256');
70
+ if (receipt.source?.verification !== 'verified') {
71
+ throw new Error('source.verification must be verified');
72
+ }
73
+ identifier(receipt.transformer?.id, 'transformer.id');
74
+ identifier(receipt.transformer?.version, 'transformer.version');
75
+ identifier(receipt.transformer?.providerAdapter, 'transformer.providerAdapter');
76
+ identifier(receipt.transformer?.providerAdapterVersion, 'transformer.providerAdapterVersion');
77
+ if (!Array.isArray(receipt.outputs) || receipt.outputs.length === 0)
78
+ throw new Error('outputs must not be empty');
79
+ const paths = new Set();
80
+ for (const [index, output] of receipt.outputs.entries()) {
81
+ output.path = portableRelative(output.path, `outputs[${index}].path`);
82
+ if (paths.has(output.path))
83
+ throw new Error(`duplicate output path '${output.path}'`);
84
+ paths.add(output.path);
85
+ sha(output.sha256, `outputs[${index}].sha256`);
86
+ if (!Number.isSafeInteger(output.bytes) || output.bytes < 0)
87
+ throw new Error(`outputs[${index}].bytes is invalid`);
88
+ }
89
+ receipt.outputs.sort((a, b) => a.path.localeCompare(b.path));
90
+ return receipt;
91
+ }
92
+ export async function createProviderTransformationReceipt(options) {
93
+ const outputs = [];
94
+ for (const raw of [...new Set(options.outputPaths)].sort()) {
95
+ const relative = portableRelative(raw, 'output path');
96
+ const bytes = await readRegularOutput(options.outputRoot ?? options.projectRoot, relative);
97
+ outputs.push({ path: relative, sha256: digest(bytes), bytes: bytes.byteLength });
98
+ }
99
+ return validateProviderTransformationReceipt({
100
+ schemaVersion: PROVIDER_TRANSFORMATION_RECEIPT_SCHEMA,
101
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
102
+ scope: options.scope,
103
+ provider: options.provider,
104
+ source: options.source,
105
+ transformer: options.transformer,
106
+ outputs,
107
+ });
108
+ }
109
+ export async function writeProviderTransformationReceipt(projectRoot, receipt) {
110
+ validateProviderTransformationReceipt(receipt);
111
+ const destination = providerTransformationReceiptPath(projectRoot, receipt.provider, receipt.scope);
112
+ await mkdir(path.dirname(destination), { recursive: true });
113
+ try {
114
+ const existing = validateProviderTransformationReceipt(JSON.parse(await readFile(destination, 'utf8')));
115
+ const semantic = (value) => JSON.stringify({ ...value, generatedAt: undefined });
116
+ if (semantic(existing) === semantic(receipt))
117
+ return destination;
118
+ }
119
+ catch {
120
+ // A missing or invalid prior receipt is replaced atomically below.
121
+ }
122
+ const temporary = `${destination}.${randomUUID()}.tmp`;
123
+ try {
124
+ await writeFile(temporary, `${JSON.stringify(receipt, null, 2)}\n`, {
125
+ encoding: 'utf8', mode: 0o600, flag: 'wx',
126
+ });
127
+ await rename(temporary, destination);
128
+ }
129
+ catch (error) {
130
+ await rm(temporary, { force: true }).catch(() => undefined);
131
+ throw error;
132
+ }
133
+ return destination;
134
+ }
135
+ export async function diagnoseProviderTransformationReceipt(options) {
136
+ const receiptPath = providerTransformationReceiptPath(options.projectRoot, options.provider, options.scope);
137
+ let receipt;
138
+ try {
139
+ receipt = validateProviderTransformationReceipt(JSON.parse(await readFile(receiptPath, 'utf8')));
140
+ }
141
+ catch (error) {
142
+ const missing = error.code === 'ENOENT';
143
+ return {
144
+ status: missing ? 'missing-receipt' : 'drifted',
145
+ receiptPath,
146
+ checkedOutputs: 0,
147
+ findings: [{
148
+ kind: 'missing-receipt',
149
+ message: missing
150
+ ? 'No transformation receipt exists for this provider deployment.'
151
+ : `The transformation receipt is unreadable or invalid: ${error instanceof Error ? error.message : String(error)}`,
152
+ }],
153
+ };
154
+ }
155
+ const findings = [];
156
+ if (options.source?.verification === 'failed') {
157
+ findings.push({ kind: 'source-verification-failure', message: 'The canonical source subject did not verify.' });
158
+ }
159
+ else if (options.source && options.source.sha256 !== receipt.source.sha256) {
160
+ findings.push({
161
+ kind: 'stale-output',
162
+ message: 'The deployed output was generated from an older canonical source subject.',
163
+ expected: options.source.sha256,
164
+ actual: receipt.source.sha256,
165
+ });
166
+ }
167
+ if (options.transformer) {
168
+ for (const key of ['id', 'version', 'providerAdapter', 'providerAdapterVersion']) {
169
+ if (options.transformer[key] !== receipt.transformer[key]) {
170
+ findings.push({
171
+ kind: 'transformation-mismatch',
172
+ message: `The recorded ${key} does not match the active transformer.`,
173
+ expected: options.transformer[key],
174
+ actual: receipt.transformer[key],
175
+ });
176
+ }
177
+ }
178
+ }
179
+ for (const output of receipt.outputs) {
180
+ try {
181
+ const bytes = await readRegularOutput(options.outputRoot ?? options.projectRoot, output.path);
182
+ const actual = digest(bytes);
183
+ if (actual !== output.sha256 || bytes.byteLength !== output.bytes) {
184
+ findings.push({
185
+ kind: 'user-modification',
186
+ path: output.path,
187
+ message: 'A generated provider output differs from its recorded receipt.',
188
+ expected: output.sha256,
189
+ actual,
190
+ });
191
+ }
192
+ }
193
+ catch (error) {
194
+ const unsafeType = error instanceof Error
195
+ && (error.message.includes('must be a regular file') || error.message.includes('outside its configured root'));
196
+ findings.push({
197
+ kind: unsafeType ? 'user-modification' : 'stale-output',
198
+ path: output.path,
199
+ message: unsafeType
200
+ ? 'A receipt-bound output was replaced by a symbolic link, non-regular file, or path outside its configured root.'
201
+ : error.code === 'ENOENT'
202
+ ? 'A receipt-bound generated output is missing (partial deployment).'
203
+ : `A receipt-bound generated output could not be read: ${error instanceof Error ? error.message : String(error)}`,
204
+ expected: output.sha256,
205
+ });
206
+ }
207
+ }
208
+ return {
209
+ status: findings.length === 0 ? 'verified' : 'drifted',
210
+ receiptPath,
211
+ checkedOutputs: receipt.outputs.length,
212
+ findings,
213
+ };
214
+ }
215
+ //# sourceMappingURL=transformation-receipt.js.map
@@ -1,5 +1,6 @@
1
1
  export declare const DEFAULT_RESOURCE_BASE_URL = "https://releases.aiwg.io";
2
2
  export declare const AIWG_RELEASE_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA8BsJ2vjuHBReexz328sknfL7MKUtxynX6MGfqFVMD38=\n-----END PUBLIC KEY-----";
3
+ declare const ARTIFACT_ATTESTATION_MEDIA_TYPE = "application/vnd.aiwg.artifact-attestation.v1+json";
3
4
  export type ResourceSource = "local" | "web" | "auto";
4
5
  export type ResourceSelector = {
5
6
  kind: "exact";
@@ -61,6 +62,14 @@ export interface VerifiedReleaseDescriptor {
61
62
  path: string;
62
63
  size: number;
63
64
  sha256: string;
65
+ mediaType?: string;
66
+ attestation?: VerifiedReleaseAttestationDescriptor;
67
+ }
68
+ export interface VerifiedReleaseAttestationDescriptor {
69
+ path: string;
70
+ size: number;
71
+ sha256: string;
72
+ mediaType: typeof ARTIFACT_ATTESTATION_MEDIA_TYPE;
64
73
  }
65
74
  export interface VerifiedWebRelease {
66
75
  selector: string;
@@ -79,6 +88,7 @@ export interface VerifiedWebRelease {
79
88
  fortemiExportSha256: string;
80
89
  fortemiExportSize: number;
81
90
  channelSequence?: number;
91
+ channelExpiresAt?: string;
82
92
  descriptors: ReadonlyMap<string, VerifiedReleaseDescriptor>;
83
93
  }
84
94
  export interface VerifiedRawResourceOptions extends Pick<WebReleaseOptions, "baseUrl" | "fetcher" | "credentialProvider" | "allowInsecureLoopbackHttp"> {
@@ -103,4 +113,5 @@ export declare function loadResourceTrustRootFile(pathname: string): Buffer;
103
113
  export declare function resolveWebRelease(options?: WebReleaseOptions): Promise<VerifiedWebRelease>;
104
114
  export declare function fetchVerifiedRawResource(release: VerifiedWebRelease, resourcePath: string, options?: VerifiedRawResourceOptions): Promise<Buffer>;
105
115
  export declare function createWebReleaseTestOptions(baseUrl: string, overrides?: Omit<WebReleaseOptions, "baseUrl" | "allowInsecureLoopbackHttp">): WebReleaseOptions;
116
+ export {};
106
117
  //# sourceMappingURL=web-release.d.ts.map
@@ -12,6 +12,10 @@ const CHANNEL_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
12
12
  const SHA256_PATTERN = /^[0-9a-f]{64}$/;
13
13
  const DIGEST_SELECTOR_PATTERN = /^sha256:([0-9a-f]{64})$/;
14
14
  const SIGNATURE_PATTERN = /^(?:[A-Za-z0-9+/]{4}){21}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)$/;
15
+ const RFC3339_UTC_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
16
+ const MEDIA_TYPE_PATTERN = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,127}$/;
17
+ const ARTIFACT_ATTESTATION_MEDIA_TYPE = "application/vnd.aiwg.artifact-attestation.v1+json";
18
+ const ARTIFACT_ATTESTATION_SUFFIX = ".aiwg-attestation.json";
15
19
  const RELEASE_MANIFEST_SCHEMAS = new Set([
16
20
  "aiwg.resource-manifest/v1",
17
21
  "aiwg.resource-manifest/v2",
@@ -117,7 +121,7 @@ function assertSafeRelativePath(value, label) {
117
121
  throw new Error(`${label} is not a safe relative path`);
118
122
  }
119
123
  }
120
- function descriptorFrom(value, descriptorPath) {
124
+ function descriptorBaseFrom(value, descriptorPath) {
121
125
  if (!isRecord(value))
122
126
  throw new Error(`release descriptor for ${descriptorPath} must be an object`);
123
127
  assertSafeRelativePath(value.path, `release descriptor path for ${descriptorPath}`);
@@ -127,7 +131,34 @@ function descriptorFrom(value, descriptorPath) {
127
131
  if (typeof value.sha256 !== "string" || !SHA256_PATTERN.test(value.sha256)) {
128
132
  throw new Error(`release descriptor digest for ${value.path} is invalid`);
129
133
  }
130
- return { path: value.path, size: value.size, sha256: value.sha256 };
134
+ if (value.mediaType !== undefined && (typeof value.mediaType !== "string" || !MEDIA_TYPE_PATTERN.test(value.mediaType))) {
135
+ throw new Error(`release descriptor mediaType for ${value.path} is invalid`);
136
+ }
137
+ return {
138
+ path: value.path,
139
+ size: value.size,
140
+ sha256: value.sha256,
141
+ ...(typeof value.mediaType === "string" ? { mediaType: value.mediaType } : {}),
142
+ };
143
+ }
144
+ function descriptorFrom(value, descriptorPath) {
145
+ const descriptor = descriptorBaseFrom(value, descriptorPath);
146
+ if (!isRecord(value) || value.attestation === undefined)
147
+ return descriptor;
148
+ if (!descriptor.mediaType) {
149
+ throw new Error(`release descriptor ${descriptor.path} with an attestation must declare mediaType`);
150
+ }
151
+ const attestation = descriptorBaseFrom(value.attestation, `${descriptor.path} attestation`);
152
+ if (attestation.path !== `${descriptor.path}${ARTIFACT_ATTESTATION_SUFFIX}`) {
153
+ throw new Error(`release attestation descriptor for ${descriptor.path} is not adjacent to its artifact`);
154
+ }
155
+ if (attestation.mediaType !== ARTIFACT_ATTESTATION_MEDIA_TYPE) {
156
+ throw new Error(`release attestation descriptor for ${descriptor.path} has an invalid mediaType`);
157
+ }
158
+ if (isRecord(value.attestation) && value.attestation.attestation !== undefined) {
159
+ throw new Error(`release attestation descriptor for ${descriptor.path} must not contain another attestation`);
160
+ }
161
+ return { ...descriptor, attestation: attestation };
131
162
  }
132
163
  function validateReleaseManifest(value, version) {
133
164
  if (!isRecord(value) || typeof value.schemaVersion !== "string" || !RELEASE_MANIFEST_SCHEMAS.has(value.schemaVersion)) {
@@ -157,12 +188,24 @@ function validateReleaseManifest(value, version) {
157
188
  if (descriptors.has(descriptor.path))
158
189
  throw new Error(`duplicate release descriptor path: ${descriptor.path}`);
159
190
  descriptors.set(descriptor.path, descriptor);
191
+ if (descriptor.attestation) {
192
+ if (descriptors.has(descriptor.attestation.path)) {
193
+ throw new Error(`duplicate release descriptor path: ${descriptor.attestation.path}`);
194
+ }
195
+ descriptors.set(descriptor.attestation.path, descriptor.attestation);
196
+ }
160
197
  };
161
198
  for (const bundle of value.bundles) {
162
199
  if (!isRecord(bundle) || typeof bundle.filename !== "string" || !/^[a-z0-9-]+\.tar\.zst$/.test(bundle.filename)) {
163
200
  throw new Error("release manifest contains an unsafe bundle filename");
164
201
  }
165
- add(descriptorFrom({ path: `bundles/${bundle.filename}`, size: bundle.size, sha256: bundle.sha256 }, bundle.filename));
202
+ add(descriptorFrom({
203
+ path: `bundles/${bundle.filename}`,
204
+ size: bundle.size,
205
+ sha256: bundle.sha256,
206
+ mediaType: bundle.mediaType,
207
+ attestation: bundle.attestation,
208
+ }, bundle.filename));
166
209
  }
167
210
  for (const file of value.files)
168
211
  add(descriptorFrom(file, "file"));
@@ -187,6 +230,16 @@ function validateChannelManifest(value, channel) {
187
230
  if (typeof value.releaseManifestSha256 !== "string" || !SHA256_PATTERN.test(value.releaseManifestSha256)) {
188
231
  throw new Error(`channel ${channel} has an invalid release manifest digest`);
189
232
  }
233
+ if (value.expiresAt !== undefined) {
234
+ if (typeof value.expiresAt !== "string" ||
235
+ !RFC3339_UTC_PATTERN.test(value.expiresAt) ||
236
+ !Number.isFinite(Date.parse(value.expiresAt))) {
237
+ throw new Error(`channel ${channel} has an invalid expiry`);
238
+ }
239
+ if (Date.parse(value.expiresAt) <= Date.now()) {
240
+ throw new Error(`channel ${channel} signed metadata has expired`);
241
+ }
242
+ }
190
243
  return value;
191
244
  }
192
245
  function validateVersionIndex(value) {
@@ -1054,7 +1107,8 @@ export async function resolveWebRelease(options = {}) {
1054
1107
  const cached = readCachedChannel(cacheRoot, selector.value, publicKeyPem);
1055
1108
  if (!cached)
1056
1109
  throw new Error(`AIWG resource channel ${selector.value} is not cached; offline mode cannot fetch it`);
1057
- return resolveOfflineExact(cacheRoot, selector, cached.manifest.version, publicKeyPem, base, cached.manifest.releaseManifestSha256, cached.manifest.sequence);
1110
+ const release = resolveOfflineExact(cacheRoot, selector, cached.manifest.version, publicKeyPem, base, cached.manifest.releaseManifestSha256, cached.manifest.sequence);
1111
+ return cached.manifest.expiresAt ? { ...release, channelExpiresAt: cached.manifest.expiresAt } : release;
1058
1112
  }
1059
1113
  const fetcher = authorize;
1060
1114
  if (!fetcher)
@@ -1071,7 +1125,8 @@ export async function resolveWebRelease(options = {}) {
1071
1125
  if (fetched.notModified) {
1072
1126
  cacheChannel(cacheRoot, prior.manifest, prior.bytes, prior.signatureBytes, prior.digest, fetched.validator);
1073
1127
  options.onDiagnostic?.({ resource: "channel", outcome: "conditional-hit", validator: prior.validator.etag ? "etag" : "last-modified" });
1074
- return fetchAndCacheRelease(base, fetcher, cacheRoot, selector, prior.manifest.version, publicKeyPem, prior.manifest.releaseManifestSha256, prior.manifest.sequence);
1128
+ const release = await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, prior.manifest.version, publicKeyPem, prior.manifest.releaseManifestSha256, prior.manifest.sequence);
1129
+ return prior.manifest.expiresAt ? { ...release, channelExpiresAt: prior.manifest.expiresAt } : release;
1075
1130
  }
1076
1131
  const channelBytes = fetched.bytes;
1077
1132
  const channelSignatureBytes = await fetchBytes(fetcher, resourceUrl(base, `${channelPrefix}.sig`), `channel ${selector.value} signature`, MAX_SIGNATURE_BYTES);
@@ -1090,7 +1145,7 @@ export async function resolveWebRelease(options = {}) {
1090
1145
  const release = await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, channel.version, publicKeyPem, channel.releaseManifestSha256, channel.sequence);
1091
1146
  cacheChannel(cacheRoot, channel, channelBytes, channelSignatureBytes, channelDigest, fetched.validator);
1092
1147
  options.onDiagnostic?.({ resource: "channel", outcome: prior?.validator ? "revalidated" : "unconditional", validator: fetched.validator?.etag ? "etag" : fetched.validator?.lastModified ? "last-modified" : "none" });
1093
- return release;
1148
+ return channel.expiresAt ? { ...release, channelExpiresAt: channel.expiresAt } : release;
1094
1149
  }
1095
1150
  export async function fetchVerifiedRawResource(release, resourcePath, options = {}) {
1096
1151
  assertSafeRelativePath(resourcePath, "raw resource path");
@@ -0,0 +1,117 @@
1
+ import { createPrivateKey, createPublicKey, sign as signBytes, } from 'node:crypto';
2
+ import { canonicalJson, dssePae, publicKeyFingerprint, sha256 } from './artifact-trust.js';
3
+ export const ARTIFACT_ATTESTATION_MEDIA_TYPE = 'application/vnd.aiwg.artifact-attestation.v1+json';
4
+ export const ARTIFACT_PROVENANCE_PREDICATE_TYPE = 'https://aiwg.io/attestations/artifact-provenance/v1';
5
+ export const IN_TOTO_STATEMENT_V1 = 'https://in-toto.io/Statement/v1';
6
+ export const DSSE_IN_TOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json';
7
+ function assertNonEmpty(value, label) {
8
+ if (!value)
9
+ throw new Error(`${label} must not be empty`);
10
+ }
11
+ function assertTimestamp(value, label) {
12
+ const parsed = Date.parse(value);
13
+ if (!Number.isFinite(parsed))
14
+ throw new Error(`${label} must be an RFC 3339 date-time`);
15
+ return parsed;
16
+ }
17
+ function assertSha256(value, label) {
18
+ if (!/^[a-f0-9]{64}$/.test(value))
19
+ throw new Error(`${label} must be a lowercase SHA-256 digest`);
20
+ }
21
+ function validateDescriptor(descriptor, label) {
22
+ assertNonEmpty(descriptor.name, `${label}.name`);
23
+ assertSha256(descriptor.digest.sha256, `${label}.digest.sha256`);
24
+ }
25
+ /** Build the exact canonical in-toto bytes that DSSE signs. */
26
+ export function createArtifactProvenanceStatement(options) {
27
+ assertNonEmpty(options.artifact.name, 'artifact.name');
28
+ assertNonEmpty(options.assetType, 'assetType');
29
+ assertNonEmpty(options.publisher.id, 'publisher.id');
30
+ assertNonEmpty(options.publisher.namespace, 'publisher.namespace');
31
+ assertNonEmpty(options.publication.version, 'publication.version');
32
+ assertNonEmpty(options.publication.channel, 'publication.channel');
33
+ if (!Number.isSafeInteger(options.publication.sequence) || options.publication.sequence < 1) {
34
+ throw new Error('publication.sequence must be a positive safe integer');
35
+ }
36
+ const issuedAt = assertTimestamp(options.issuedAt, 'issuedAt');
37
+ const notBefore = options.notBefore ? assertTimestamp(options.notBefore, 'notBefore') : undefined;
38
+ const expiresAt = options.expiresAt ? assertTimestamp(options.expiresAt, 'expiresAt') : undefined;
39
+ if (notBefore !== undefined && expiresAt !== undefined && expiresAt <= notBefore) {
40
+ throw new Error('expiresAt must follow notBefore');
41
+ }
42
+ if (expiresAt !== undefined && expiresAt <= issuedAt)
43
+ throw new Error('expiresAt must follow issuedAt');
44
+ for (const [index, dependency] of (options.dependencies ?? []).entries()) {
45
+ validateDescriptor(dependency, `dependencies[${index}]`);
46
+ }
47
+ for (const [index, material] of (options.derivation?.materials ?? []).entries()) {
48
+ if (material.name !== undefined)
49
+ assertNonEmpty(material.name, `derivation.materials[${index}].name`);
50
+ assertNonEmpty(material.uri, `derivation.materials[${index}].uri`);
51
+ if (material.mediaType !== undefined)
52
+ assertNonEmpty(material.mediaType, `derivation.materials[${index}].mediaType`);
53
+ assertSha256(material.digest.sha256, `derivation.materials[${index}].digest.sha256`);
54
+ }
55
+ const subject = {
56
+ name: options.artifact.name,
57
+ ...(options.artifact.uri ? { uri: options.artifact.uri } : {}),
58
+ ...(options.artifact.mediaType ? { mediaType: options.artifact.mediaType } : {}),
59
+ digest: { sha256: sha256(options.artifact.bytes) },
60
+ };
61
+ return {
62
+ _type: IN_TOTO_STATEMENT_V1,
63
+ subject: [subject],
64
+ predicateType: ARTIFACT_PROVENANCE_PREDICATE_TYPE,
65
+ predicate: {
66
+ schemaVersion: 'aiwg.artifact-provenance.v1',
67
+ assetType: options.assetType,
68
+ publisher: options.publisher,
69
+ publication: options.publication,
70
+ issuedAt: options.issuedAt,
71
+ ...(options.notBefore ? { notBefore: options.notBefore } : {}),
72
+ ...(options.expiresAt ? { expiresAt: options.expiresAt } : {}),
73
+ ...(options.derivation ? { derivation: options.derivation } : {}),
74
+ ...(options.provenanceGraph ? { provenanceGraph: options.provenanceGraph } : {}),
75
+ ...(options.dependencies ? { dependencies: options.dependencies } : {}),
76
+ },
77
+ };
78
+ }
79
+ /** Create a portable public-key DSSE attestation over exact canonical payload bytes. */
80
+ export function createArtifactAttestation(options) {
81
+ const privateKey = options.privateKey instanceof Object && 'type' in options.privateKey
82
+ ? options.privateKey
83
+ : createPrivateKey(options.privateKey);
84
+ if (privateKey.asymmetricKeyType !== 'ed25519')
85
+ throw new Error('artifact attestation key must be Ed25519');
86
+ const publicKey = createPublicKey(privateKey);
87
+ const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
88
+ const statement = createArtifactProvenanceStatement(options);
89
+ const payload = Buffer.from(canonicalJson(statement), 'utf8');
90
+ const signature = signBytes(null, dssePae(DSSE_IN_TOTO_PAYLOAD_TYPE, payload), privateKey);
91
+ return {
92
+ mediaType: ARTIFACT_ATTESTATION_MEDIA_TYPE,
93
+ envelope: {
94
+ payloadType: DSSE_IN_TOTO_PAYLOAD_TYPE,
95
+ payload: payload.toString('base64'),
96
+ signatures: [{ keyid: publicKeyFingerprint(publicKeyPem), sig: signature.toString('base64') }],
97
+ },
98
+ verificationMaterial: {
99
+ kind: 'public-key',
100
+ algorithm: 'ed25519',
101
+ publicKey: publicKeyPem,
102
+ },
103
+ };
104
+ }
105
+ export function serializeArtifactAttestation(attestation) {
106
+ return Buffer.from(`${canonicalJson(attestation)}\n`, 'utf8');
107
+ }
108
+ export function describeAttestationSidecar(artifactPath, attestationBytes) {
109
+ assertNonEmpty(artifactPath, 'artifactPath');
110
+ return {
111
+ path: `${artifactPath}.aiwg-attestation.json`,
112
+ sha256: sha256(attestationBytes),
113
+ bytes: attestationBytes.byteLength,
114
+ mediaType: ARTIFACT_ATTESTATION_MEDIA_TYPE,
115
+ };
116
+ }
117
+ //# sourceMappingURL=artifact-attestation.js.map