@moonglade/provenance 0.0.0-stage → 2.6.0

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.
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.processArtifact = processArtifact;
37
+ exports.moveArtifactWithAttestation = moveArtifactWithAttestation;
38
+ const fs = __importStar(require("fs/promises"));
39
+ const path = __importStar(require("path"));
40
+ const index_1 = require("./index");
41
+ const artifactEnvelope_1 = require("./artifactEnvelope");
42
+ const UNSAFE_NAMES = new Set(['.env', '.env.local', 'id_rsa', 'id_ed25519', 'id_dsa', 'id_ecdsa', '.npmrc', 'credentials', 'secrets.json']);
43
+ const UNSAFE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__']);
44
+ function isUnsafe(filePath) {
45
+ const parts = path.resolve(filePath).split(path.sep);
46
+ if (parts.some(part => UNSAFE_DIRS.has(part)))
47
+ return 'path is inside an excluded directory';
48
+ if (UNSAFE_NAMES.has(path.basename(filePath).toLowerCase()))
49
+ return 'filename is excluded';
50
+ if (/\.(pem|key|p12|pfx|asc|kdbx)$/i.test(filePath))
51
+ return 'private-key or certificate material is excluded';
52
+ return undefined;
53
+ }
54
+ async function processArtifact(filePath, opts = {}) {
55
+ const resolved = path.resolve(filePath);
56
+ const unsafe = isUnsafe(resolved);
57
+ if (unsafe)
58
+ return { path: resolved, action: opts.action ?? 'observe', skipped: true, reason: unsafe };
59
+ const action = opts.action ?? 'observe';
60
+ if (action === 'observe') {
61
+ const bytes = await (0, index_1.readRegularFile)(resolved, index_1.MAX_ARTIFACT_BYTES);
62
+ const hash = (await Promise.resolve().then(() => __importStar(require('crypto')))).createHash('sha256').update(bytes).digest('hex');
63
+ const envelope = (0, artifactEnvelope_1.createArtifactEnvelope)({ artifact_type: 'other', canonical_bytes: `sha256:${hash}`, created_at: new Date().toISOString(), parents: [], attestations: [], status: 'observed', evidence: { integrity: 'valid', signature: 'none', timestamp: 'none', scope: 'unknown' }, bytes });
64
+ return { path: resolved, action, skipped: false, sidecar: {
65
+ standard: 'Moonglade Provenance Observation v1', artifact: path.basename(resolved), attestations: [],
66
+ sha256_at_last_write: hash, updated: new Date().toISOString(), envelope
67
+ } };
68
+ }
69
+ if (!opts.privateKeyPem || !opts.signerIdentity)
70
+ throw new Error('seal-provisional requires privateKeyPem and signerIdentity');
71
+ const sidecar = await index_1.ProvenanceEngine.sealDocument(resolved, opts.privateKeyPem, opts.signerIdentity, opts.gitCommit ?? 'unknown', { vaultRoot: opts.vaultRoot, timestamp: false });
72
+ sidecar.status = 'provisional';
73
+ sidecar.generator = opts.generator ?? { agent: 'unknown' };
74
+ sidecar.parents = opts.parents ?? [];
75
+ sidecar.envelope = (0, artifactEnvelope_1.createArtifactEnvelope)({ artifact_type: 'other', canonical_bytes: `sha256:${sidecar.sha256_at_last_write}`, created_at: sidecar.updated, parents: opts.parents ?? [], attestations: sidecar.attestations.map(a => ({ method: a.method, signer: a.signer, status: a.method === 'moonglade-ed25519-v2' ? 'provisional' : 'indeterminate', signature: a.signature })), status: 'signed', evidence: { integrity: 'valid', signature: 'provisional', timestamp: 'none', scope: 'valid' }, generator: opts.generator, bytes: await (0, index_1.readRegularFile)(resolved, index_1.MAX_ARTIFACT_BYTES) });
76
+ await (0, index_1.writeSidecar)(`${resolved}.provenance.json`, sidecar);
77
+ return { path: resolved, action, skipped: false, sidecar };
78
+ }
79
+ /** Move an artifact and its adjacent attestation as one atomic logical unit. */
80
+ async function moveArtifactWithAttestation(sourcePath, destinationPath) {
81
+ const source = path.resolve(sourcePath);
82
+ const destination = path.resolve(destinationPath);
83
+ const sourceAttestation = `${source}.provenance.json`;
84
+ const destinationAttestation = `${destination}.provenance.json`;
85
+ for (const file of [destination, destinationAttestation]) {
86
+ try {
87
+ await fs.lstat(file);
88
+ throw new Error('Move destination already exists');
89
+ }
90
+ catch (error) {
91
+ if (error.code !== 'ENOENT')
92
+ throw error;
93
+ }
94
+ }
95
+ try {
96
+ await fs.lstat(sourceAttestation);
97
+ throw new Error('Attested move refused: copy and explicitly reseal at the destination before retiring the source');
98
+ }
99
+ catch (error) {
100
+ if (error.code !== 'ENOENT')
101
+ throw error;
102
+ }
103
+ await (0, index_1.readRegularFile)(source, index_1.MAX_ARTIFACT_BYTES);
104
+ // Exclusive link creation avoids rename-overwrite races. Cross-device moves fail safely.
105
+ await fs.link(source, destination);
106
+ try {
107
+ await fs.unlink(source);
108
+ }
109
+ catch (error) {
110
+ await fs.unlink(destination);
111
+ throw error;
112
+ }
113
+ return { artifactPath: destination, attestationPath: destinationAttestation, attestationMoved: false };
114
+ }
@@ -0,0 +1,83 @@
1
+ export type ProvenanceBadgeStatus = 'HASH_ONLY' | 'SEALED' | 'LIVING' | 'SCRATCH' | 'MUTATED' | 'INVALID_SIGNATURE' | 'NOT_FOUND';
2
+ export interface BadgeColorDefinition {
3
+ hex: string;
4
+ ansi: string;
5
+ bgHex: string;
6
+ bgAnsi: string;
7
+ resetAnsi: string;
8
+ }
9
+ export declare const BADGE_COLORS: Record<ProvenanceBadgeStatus, BadgeColorDefinition>;
10
+ export declare function getStatusColor(status: ProvenanceBadgeStatus): BadgeColorDefinition;
11
+ export interface BadgeOptions {
12
+ format?: 'text' | 'markdown' | 'markdown-link' | 'shields-url' | 'html' | 'json';
13
+ color?: boolean | 'ansi' | 'html';
14
+ shortHashLen?: number;
15
+ style?: 'short' | 'detailed';
16
+ targetUrl?: string;
17
+ publicKeyPem?: string;
18
+ requiredScope?: string;
19
+ }
20
+ export interface BadgeInfo {
21
+ status: ProvenanceBadgeStatus;
22
+ badgeText: string;
23
+ ansiText?: string;
24
+ html?: string;
25
+ colorHex: string;
26
+ markdown: string;
27
+ shieldsUrl: string;
28
+ filePath: string;
29
+ artifactName: string;
30
+ signer?: string;
31
+ shortHash?: string;
32
+ fullHash?: string;
33
+ tier?: string;
34
+ timestamp?: string;
35
+ error?: string;
36
+ }
37
+ /**
38
+ * Clean signer identifier for badge display (e.g. 'Synthetic (Fixture)...' -> 'Synthetic').
39
+ */
40
+ export declare function extractShortSigner(signer?: string): string | undefined;
41
+ /**
42
+ * Generate a Shields.io badge URL for a given status.
43
+ */
44
+ export declare function getShieldsBadgeUrl(status: ProvenanceBadgeStatus, details?: {
45
+ signer?: string;
46
+ shortHash?: string;
47
+ }): string;
48
+ /**
49
+ * Format an ANSI color-coded badge string for terminal display.
50
+ */
51
+ export declare function formatAnsiBadge(status: ProvenanceBadgeStatus, details?: {
52
+ signer?: string;
53
+ hash?: string;
54
+ tier?: string;
55
+ }, options?: BadgeOptions): string;
56
+ /**
57
+ * Format an inline HTML badge element for web apps, Astro, or rich markdown docs.
58
+ */
59
+ export declare function formatHtmlBadge(status: ProvenanceBadgeStatus, details?: {
60
+ signer?: string;
61
+ hash?: string;
62
+ tier?: string;
63
+ }, options?: BadgeOptions): string;
64
+ /**
65
+ * Format a badge string given status and metadata.
66
+ */
67
+ export declare function formatBadge(status: ProvenanceBadgeStatus, details?: {
68
+ signer?: string;
69
+ hash?: string;
70
+ tier?: string;
71
+ }, options?: BadgeOptions): string;
72
+ /**
73
+ * Format a complete markdown link with the badge adjacent.
74
+ */
75
+ export declare function formatMarkdownLink(filePath: string, badgeInfo: BadgeInfo, options?: {
76
+ label?: string;
77
+ urlPrefix?: string;
78
+ targetUrl?: string;
79
+ }): string;
80
+ /**
81
+ * Inspect an artifact and its sidecar to produce a complete Provenance BadgeInfo.
82
+ */
83
+ export declare function getArtifactBadge(filePath: string, options?: BadgeOptions): Promise<BadgeInfo>;
package/dist/badge.js ADDED
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BADGE_COLORS = void 0;
37
+ exports.getStatusColor = getStatusColor;
38
+ exports.extractShortSigner = extractShortSigner;
39
+ exports.getShieldsBadgeUrl = getShieldsBadgeUrl;
40
+ exports.formatAnsiBadge = formatAnsiBadge;
41
+ exports.formatHtmlBadge = formatHtmlBadge;
42
+ exports.formatBadge = formatBadge;
43
+ exports.formatMarkdownLink = formatMarkdownLink;
44
+ exports.getArtifactBadge = getArtifactBadge;
45
+ const fsSync = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const crypto = __importStar(require("crypto"));
48
+ const index_1 = require("./index");
49
+ exports.BADGE_COLORS = {
50
+ HASH_ONLY: { hex: '#d97706', ansi: '\x1b[1;33m', bgHex: 'rgba(217,119,6,0.15)', bgAnsi: '\x1b[43;30m', resetAnsi: '\x1b[0m' },
51
+ SEALED: {
52
+ hex: '#2ea44f',
53
+ ansi: '\x1b[1;32m',
54
+ bgHex: 'rgba(46,164,79,0.15)',
55
+ bgAnsi: '\x1b[42;30m',
56
+ resetAnsi: '\x1b[0m'
57
+ },
58
+ LIVING: {
59
+ hex: '#0969da',
60
+ ansi: '\x1b[1;34m',
61
+ bgHex: 'rgba(9,105,218,0.15)',
62
+ bgAnsi: '\x1b[44;37m',
63
+ resetAnsi: '\x1b[0m'
64
+ },
65
+ SCRATCH: {
66
+ hex: '#d97706',
67
+ ansi: '\x1b[1;33m',
68
+ bgHex: 'rgba(217,119,6,0.15)',
69
+ bgAnsi: '\x1b[43;30m',
70
+ resetAnsi: '\x1b[0m'
71
+ },
72
+ MUTATED: {
73
+ hex: '#cf222e',
74
+ ansi: '\x1b[1;31m',
75
+ bgHex: 'rgba(207,34,46,0.15)',
76
+ bgAnsi: '\x1b[41;37m',
77
+ resetAnsi: '\x1b[0m'
78
+ },
79
+ INVALID_SIGNATURE: {
80
+ hex: '#cf222e',
81
+ ansi: '\x1b[1;31m',
82
+ bgHex: 'rgba(207,34,46,0.15)',
83
+ bgAnsi: '\x1b[41;37m',
84
+ resetAnsi: '\x1b[0m'
85
+ },
86
+ NOT_FOUND: {
87
+ hex: '#6e7781',
88
+ ansi: '\x1b[90m',
89
+ bgHex: 'rgba(110,119,129,0.15)',
90
+ bgAnsi: '\x1b[47;30m',
91
+ resetAnsi: '\x1b[0m'
92
+ }
93
+ };
94
+ function getStatusColor(status) {
95
+ return exports.BADGE_COLORS[status] || exports.BADGE_COLORS.NOT_FOUND;
96
+ }
97
+ /**
98
+ * Clean signer identifier for badge display (e.g. 'Synthetic (Fixture)...' -> 'Synthetic').
99
+ */
100
+ function extractShortSigner(signer) {
101
+ if (!signer)
102
+ return undefined;
103
+ const match = signer.match(/^([^<(\s]+)/);
104
+ return match ? match[1] : signer;
105
+ }
106
+ /**
107
+ * Generate a Shields.io badge URL for a given status.
108
+ */
109
+ function getShieldsBadgeUrl(status, details) {
110
+ let label = 'provenance';
111
+ let message = 'unknown';
112
+ let color = 'lightgrey';
113
+ switch (status) {
114
+ case 'HASH_ONLY': return 'https://img.shields.io/badge/provenance-hash%20only-d97706?style=flat-square';
115
+ case 'SEALED': {
116
+ const parts = ['sealed'];
117
+ if (details?.signer)
118
+ parts.push(encodeURIComponent(details.signer));
119
+ if (details?.shortHash)
120
+ parts.push(details.shortHash);
121
+ message = parts.join('%20%C2%B7%20'); // URL-encoded middle dot
122
+ color = '2ea44f'; // Green
123
+ break;
124
+ }
125
+ case 'LIVING':
126
+ message = 'unsealed%20%C2%B7%20living';
127
+ color = '0969da'; // Blue
128
+ break;
129
+ case 'SCRATCH':
130
+ message = 'ephemeral%20%C2%B7%20scratch';
131
+ color = 'd97706'; // Amber
132
+ break;
133
+ case 'MUTATED':
134
+ message = 'mutated%20%C2%B7%20mismatch';
135
+ color = 'cf222e'; // Red
136
+ break;
137
+ case 'INVALID_SIGNATURE':
138
+ message = 'invalid%20signature';
139
+ color = 'cf222e'; // Red
140
+ break;
141
+ case 'NOT_FOUND':
142
+ message = 'not%20found';
143
+ color = 'grey';
144
+ break;
145
+ }
146
+ return `https://img.shields.io/badge/${label}-${message}-${color}?style=flat-square`;
147
+ }
148
+ /**
149
+ * Format an ANSI color-coded badge string for terminal display.
150
+ */
151
+ function formatAnsiBadge(status, details, options = {}) {
152
+ const colorDef = getStatusColor(status);
153
+ const plain = formatBadge(status, details, { ...options, color: false });
154
+ // Match the inner bracketed text: e.g. "🛡️ [SEALED · Synthetic · 9df0af4d]"
155
+ const match = plain.match(/^(\S+)\s*\[(.*)\]$/);
156
+ if (!match) {
157
+ return `${colorDef.ansi}${plain}${colorDef.resetAnsi}`;
158
+ }
159
+ const emoji = match[1];
160
+ const inner = match[2];
161
+ return `${emoji} [${colorDef.ansi}${inner}${colorDef.resetAnsi}]`;
162
+ }
163
+ /**
164
+ * Format an inline HTML badge element for web apps, Astro, or rich markdown docs.
165
+ */
166
+ function formatHtmlBadge(status, details, options = {}) {
167
+ const colorDef = getStatusColor(status);
168
+ const plain = formatBadge(status, details, { ...options, format: 'text', color: false });
169
+ const match = plain.match(/^(\S+)\s*\[(.*)\]$/);
170
+ if (!match) {
171
+ return `<span class="provenance-badge provenance-badge-${status.toLowerCase()}" style="color:${colorDef.hex};font-weight:600;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:0.85em;">${plain}</span>`;
172
+ }
173
+ const emoji = match[1];
174
+ const innerText = match[2].replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
175
+ return `${emoji} <span class="provenance-badge-status" style="color:${colorDef.hex};font-weight:600;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:0.85em;">[${innerText}]</span>`;
176
+ }
177
+ /**
178
+ * Format a badge string given status and metadata.
179
+ */
180
+ function formatBadge(status, details, options = {}) {
181
+ if (options.color === true || options.color === 'ansi' || options.format === 'text' && options.color) {
182
+ return formatAnsiBadge(status, details, { ...options, color: false });
183
+ }
184
+ if (options.color === 'html' || options.format === 'html') {
185
+ return formatHtmlBadge(status, details, options);
186
+ }
187
+ const shortHashLen = options.shortHashLen ?? 8;
188
+ const shortHash = details?.hash ? details.hash.slice(0, shortHashLen) : undefined;
189
+ const shortSigner = options.style === 'short'
190
+ ? undefined
191
+ : extractShortSigner(details?.signer);
192
+ switch (status) {
193
+ case 'HASH_ONLY': return '⚠️ [HASH-ONLY · UNVERIFIED]';
194
+ case 'SEALED': {
195
+ const parts = ['SEALED'];
196
+ if (shortSigner)
197
+ parts.push(shortSigner);
198
+ if (shortHash)
199
+ parts.push(shortHash);
200
+ return `🛡️ [${parts.join(' · ')}]`;
201
+ }
202
+ case 'LIVING':
203
+ return '📝 [UNSEALED · LIVING]';
204
+ case 'SCRATCH':
205
+ return '⚡ [EPHEMERAL · SCRATCH]';
206
+ case 'MUTATED':
207
+ return '🚨 [MUTATED · HASH MISMATCH]';
208
+ case 'INVALID_SIGNATURE':
209
+ return '🚨 [INVALID SIGNATURE]';
210
+ case 'NOT_FOUND':
211
+ return '❌ [NOT FOUND]';
212
+ }
213
+ }
214
+ /**
215
+ * Format a complete markdown link with the badge adjacent.
216
+ */
217
+ function formatMarkdownLink(filePath, badgeInfo, options = {}) {
218
+ const label = options.label || path.basename(filePath);
219
+ let fileUrl = options.targetUrl;
220
+ if (!fileUrl) {
221
+ const normalized = path.resolve(filePath).replace(/\\/g, '/');
222
+ fileUrl = options.urlPrefix
223
+ ? `${options.urlPrefix}/${path.basename(filePath)}`
224
+ : `file:///${normalized.startsWith('/') ? normalized.slice(1) : normalized}`;
225
+ }
226
+ return `[${label}](${fileUrl}) ${badgeInfo.badgeText}`;
227
+ }
228
+ /**
229
+ * Inspect an artifact and its sidecar to produce a complete Provenance BadgeInfo.
230
+ */
231
+ async function getArtifactBadge(filePath, options = {}) {
232
+ const resolved = path.resolve(filePath);
233
+ const artifactName = path.basename(resolved);
234
+ const isScratch = resolved.includes(`${path.sep}scratch${path.sep}`) ||
235
+ resolved.includes('/scratch/') ||
236
+ resolved.includes(`${path.sep}tmp${path.sep}`) ||
237
+ resolved.includes('/tmp/');
238
+ if (!fsSync.existsSync(resolved)) {
239
+ const badgeText = formatBadge('NOT_FOUND', {}, options);
240
+ const colorDef = getStatusColor('NOT_FOUND');
241
+ const ansiText = formatAnsiBadge('NOT_FOUND', {}, options);
242
+ const html = formatHtmlBadge('NOT_FOUND', {}, options);
243
+ return {
244
+ status: 'NOT_FOUND',
245
+ badgeText,
246
+ ansiText,
247
+ html,
248
+ colorHex: colorDef.hex,
249
+ markdown: `[${artifactName}] ${badgeText}`,
250
+ shieldsUrl: getShieldsBadgeUrl('NOT_FOUND'),
251
+ filePath: resolved,
252
+ artifactName,
253
+ error: `File not found: ${resolved}`
254
+ };
255
+ }
256
+ // Canonicalize and compute SHA-256 hash
257
+ const canonicalBytes = await index_1.ProvenanceEngine.canonicalizeFile(resolved);
258
+ const currentHash = crypto.createHash('sha256').update(canonicalBytes).digest('hex');
259
+ const shortHash = currentHash.slice(0, options.shortHashLen ?? 8);
260
+ // Locate companion sidecar
261
+ let sidecarPath = `${resolved}.provenance.json`;
262
+ if (!fsSync.existsSync(sidecarPath)) {
263
+ // Special check for package.json -> package.provenance.json in same directory
264
+ if (artifactName === 'package.json') {
265
+ const alt = path.join(path.dirname(resolved), 'package.provenance.json');
266
+ if (fsSync.existsSync(alt)) {
267
+ sidecarPath = alt;
268
+ }
269
+ }
270
+ }
271
+ // Case 1: No sidecar present
272
+ if (!fsSync.existsSync(sidecarPath)) {
273
+ const status = isScratch ? 'SCRATCH' : 'LIVING';
274
+ const badgeText = formatBadge(status, { hash: currentHash }, options);
275
+ const colorDef = getStatusColor(status);
276
+ const ansiText = formatAnsiBadge(status, { hash: currentHash }, options);
277
+ const html = formatHtmlBadge(status, { hash: currentHash }, options);
278
+ const badgeInfo = {
279
+ status,
280
+ badgeText,
281
+ ansiText,
282
+ html,
283
+ colorHex: colorDef.hex,
284
+ markdown: '',
285
+ shieldsUrl: getShieldsBadgeUrl(status, { shortHash }),
286
+ filePath: resolved,
287
+ artifactName,
288
+ shortHash,
289
+ fullHash: currentHash
290
+ };
291
+ badgeInfo.markdown = formatMarkdownLink(resolved, badgeInfo, { targetUrl: options.targetUrl });
292
+ return badgeInfo;
293
+ }
294
+ // Case 2: Sidecar present - parse and verify
295
+ try {
296
+ const sidecarRaw = (await (0, index_1.readRegularFile)(sidecarPath, index_1.MAX_SIDECAR_BYTES)).toString('utf8');
297
+ const sidecar = JSON.parse(sidecarRaw);
298
+ (0, index_1.validateSidecar)(sidecar);
299
+ // Hash check
300
+ if (sidecar.sha256_at_last_write && sidecar.sha256_at_last_write !== currentHash) {
301
+ const badgeText = formatBadge('MUTATED', { hash: currentHash }, options);
302
+ const colorDef = getStatusColor('MUTATED');
303
+ const ansiText = formatAnsiBadge('MUTATED', { hash: currentHash }, options);
304
+ const html = formatHtmlBadge('MUTATED', { hash: currentHash }, options);
305
+ const badgeInfo = {
306
+ status: 'MUTATED',
307
+ badgeText,
308
+ ansiText,
309
+ html,
310
+ colorHex: colorDef.hex,
311
+ markdown: '',
312
+ shieldsUrl: getShieldsBadgeUrl('MUTATED', { shortHash }),
313
+ filePath: resolved,
314
+ artifactName,
315
+ shortHash,
316
+ fullHash: currentHash,
317
+ error: `Sidecar recorded hash ${sidecar.sha256_at_last_write.slice(0, 12)}... does not match computed hash ${shortHash}...`
318
+ };
319
+ badgeInfo.markdown = formatMarkdownLink(resolved, badgeInfo, { targetUrl: options.targetUrl });
320
+ return badgeInfo;
321
+ }
322
+ // Extract signer information
323
+ const firstAtt = sidecar.attestations?.[0] || sidecar.envelope?.attestations?.[0];
324
+ const rawSigner = firstAtt?.signer || sidecar.generator?.agent;
325
+ const cleanSigner = extractShortSigner(rawSigner);
326
+ let verifiedTimestamp;
327
+ let verifiedTier;
328
+ // Cryptographic signature check if public key provided
329
+ if (options.publicKeyPem) {
330
+ const verifyRes = await index_1.ProvenanceEngine.verifyDocument(resolved, options.publicKeyPem, { expectedScope: options.requiredScope });
331
+ verifiedTimestamp = verifyRes.timestamp;
332
+ verifiedTier = verifyRes.timestampTier;
333
+ if (!verifyRes.verified) {
334
+ const badgeText = formatBadge('INVALID_SIGNATURE', { hash: currentHash, signer: cleanSigner }, options);
335
+ const colorDef = getStatusColor('INVALID_SIGNATURE');
336
+ const ansiText = formatAnsiBadge('INVALID_SIGNATURE', { hash: currentHash, signer: cleanSigner }, options);
337
+ const html = formatHtmlBadge('INVALID_SIGNATURE', { hash: currentHash, signer: cleanSigner }, options);
338
+ const badgeInfo = {
339
+ status: 'INVALID_SIGNATURE',
340
+ badgeText,
341
+ ansiText,
342
+ html,
343
+ colorHex: colorDef.hex,
344
+ markdown: '',
345
+ shieldsUrl: getShieldsBadgeUrl('INVALID_SIGNATURE', { signer: cleanSigner, shortHash }),
346
+ filePath: resolved,
347
+ artifactName,
348
+ signer: rawSigner,
349
+ shortHash,
350
+ fullHash: currentHash,
351
+ error: verifyRes.error
352
+ };
353
+ badgeInfo.markdown = formatMarkdownLink(resolved, badgeInfo, { targetUrl: options.targetUrl });
354
+ return badgeInfo;
355
+ }
356
+ }
357
+ const status = options.publicKeyPem ? 'SEALED' : 'HASH_ONLY';
358
+ const badgeText = formatBadge(status, { hash: currentHash, signer: cleanSigner }, options);
359
+ const colorDef = getStatusColor(status);
360
+ const ansiText = formatAnsiBadge(status, { hash: currentHash, signer: cleanSigner }, options);
361
+ const html = formatHtmlBadge(status, { hash: currentHash, signer: cleanSigner }, options);
362
+ const badgeInfo = {
363
+ status: status,
364
+ badgeText,
365
+ ansiText,
366
+ html,
367
+ colorHex: colorDef.hex,
368
+ markdown: '',
369
+ shieldsUrl: getShieldsBadgeUrl(status, { signer: cleanSigner, shortHash }),
370
+ filePath: resolved,
371
+ artifactName,
372
+ signer: rawSigner,
373
+ shortHash,
374
+ fullHash: currentHash,
375
+ tier: verifiedTier,
376
+ timestamp: verifiedTimestamp
377
+ };
378
+ badgeInfo.markdown = formatMarkdownLink(resolved, badgeInfo, { targetUrl: options.targetUrl });
379
+ return badgeInfo;
380
+ }
381
+ catch (err) {
382
+ const badgeText = formatBadge('MUTATED', { hash: currentHash }, options);
383
+ const colorDef = getStatusColor('MUTATED');
384
+ const ansiText = formatAnsiBadge('MUTATED', { hash: currentHash }, options);
385
+ const html = formatHtmlBadge('MUTATED', { hash: currentHash }, options);
386
+ const badgeInfo = {
387
+ status: 'MUTATED',
388
+ badgeText,
389
+ ansiText,
390
+ html,
391
+ colorHex: colorDef.hex,
392
+ markdown: '',
393
+ shieldsUrl: getShieldsBadgeUrl('MUTATED', { shortHash }),
394
+ filePath: resolved,
395
+ artifactName,
396
+ shortHash,
397
+ fullHash: currentHash,
398
+ error: `Failed to parse sidecar: ${err.message}`
399
+ };
400
+ badgeInfo.markdown = formatMarkdownLink(resolved, badgeInfo, { targetUrl: options.targetUrl });
401
+ return badgeInfo;
402
+ }
403
+ }