@moonglade/provenance 0.0.0-stage → 2.6.1
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/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +92 -2
- package/SPEC.md +176 -0
- package/TIMESTAMP.md +21 -0
- package/dist/artifact-cli.d.ts +2 -0
- package/dist/artifact-cli.js +113 -0
- package/dist/artifactEnvelope.d.ts +38 -0
- package/dist/artifactEnvelope.js +61 -0
- package/dist/artifactHook.d.ts +30 -0
- package/dist/artifactHook.js +114 -0
- package/dist/badge.d.ts +83 -0
- package/dist/badge.js +403 -0
- package/dist/hookInstaller.d.ts +75 -0
- package/dist/hookInstaller.js +410 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.js +782 -0
- package/dist/mcp.d.ts +87 -0
- package/dist/mcp.js +352 -0
- package/dist/merkle.d.ts +71 -0
- package/dist/merkle.js +176 -0
- package/dist/self-test.d.ts +21 -0
- package/dist/self-test.js +280 -0
- package/dist/timestamp.d.ts +274 -0
- package/dist/timestamp.js +1103 -0
- package/package.json +80 -4
- package/trust/README.md +19 -0
- package/trust/registry.json +12 -0
- package/trust/root-public.asc +10 -0
- package/wasm/mgc_wasm.d.ts +53 -0
- package/wasm/mgc_wasm.js +388 -0
- package/wasm/mgc_wasm_bg.wasm +0 -0
- package/wasm/mgc_wasm_bg.wasm.d.ts +20 -0
|
@@ -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
|
+
}
|
package/dist/badge.d.ts
ADDED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface HookSelection {
|
|
2
|
+
preCommit: boolean;
|
|
3
|
+
mcpVerifyRead: boolean;
|
|
4
|
+
mcpAutoSeal: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface InstallOptions extends HookSelection {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/** Replace a hook that was not installed by this tool, after backing it up. */
|
|
9
|
+
force?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface HookResult {
|
|
12
|
+
path: string;
|
|
13
|
+
action: 'created' | 'unchanged' | 'updated' | 'replaced';
|
|
14
|
+
backup?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface InstallResult {
|
|
17
|
+
/** What the manifest now records (selected options merged with earlier recorded state). */
|
|
18
|
+
selection: HookSelection;
|
|
19
|
+
hooksDir: string | null;
|
|
20
|
+
hook: HookResult | null;
|
|
21
|
+
manifestPath: string;
|
|
22
|
+
manifestBackup?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Marker line (line 2 of the generated hook). A hook is ours only if its whole content equals a known generated version. */
|
|
25
|
+
export declare const HOOK_MARKER = "# moonglade-provenance-hook v1";
|
|
26
|
+
export declare class HookConflictError extends Error {
|
|
27
|
+
readonly hookPath: string;
|
|
28
|
+
constructor(hookPath: string);
|
|
29
|
+
}
|
|
30
|
+
export declare function findGitRoot(startDir?: string): Promise<string | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Hooks directory as git itself resolves it (`git rev-parse --git-path hooks`), so linked worktrees,
|
|
33
|
+
* submodules and core.hooksPath work. Falls back to a `.git` directory only when git is not installed.
|
|
34
|
+
* Returns null when cwd is not inside a repository.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveHooksDir(cwd?: string): Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* The generated pre-commit hook. Written for bash (not POSIX sh): it reads NUL-delimited names (`read -d ''`)
|
|
39
|
+
* so file names with spaces, non-ASCII characters (git would quote them otherwise) or newlines are all checked.
|
|
40
|
+
* Fail closed: if the staged list cannot be read, the commit is blocked.
|
|
41
|
+
*/
|
|
42
|
+
export declare const PRE_COMMIT_SCRIPT = "#!/usr/bin/env bash\n# moonglade-provenance-hook v1\n# Moonglade Provenance - Git Pre-Commit Verification Gate (requires bash)\n# Blocks the commit when a staged artifact that has a provenance sidecar no longer matches it.\n\necho \"Moonglade Provenance: Verifying staged artifacts...\"\n\nTMP_LIST=$(mktemp) || exit 1\ntrap 'rm -f \"$TMP_LIST\"' EXIT\nif ! git diff --cached --name-only -z --diff-filter=ACMR > \"$TMP_LIST\"; then\n echo \"Moonglade Provenance: could not list staged files; commit aborted.\"\n exit 1\nfi\n\nFAILED=0\nwhile IFS= read -r -d '' FILE; do\n case \"$FILE\" in\n *.provenance.json|*.provenance-registry.json) continue ;;\n *.md|*.csv|*.json|*.sql|*.yml|*.yaml) ;;\n *) continue ;;\n esac\n [ -f \"$FILE\" ] || continue\n [ -f \"$FILE.provenance.json\" ] || continue\n TARGET=\"$FILE\"\n case \"$TARGET\" in -*) TARGET=\"./$TARGET\" ;; esac\n if ! npx --no-install @moonglade/provenance badge \"$TARGET\" --short < /dev/null > /dev/null 2>&1; then\n echo \"[PROVENANCE DRIFT] $FILE has mutated or failed verification!\"\n FAILED=1\n fi\ndone < \"$TMP_LIST\"\n\nif [ \"$FAILED\" -ne 0 ]; then\n echo \"\"\n echo \"Commit aborted by Moonglade Provenance Gate.\"\n echo \"Please re-seal mutated artifacts before committing:\"\n echo \" npx @moonglade/provenance seal-provisional <file>\"\n echo \"\"\n exit 1\nfi\n\necho \"All staged artifacts verified.\"\nexit 0\n";
|
|
43
|
+
/**
|
|
44
|
+
* Install one hook script. An existing hook is replaced only if its content equals a known generated version
|
|
45
|
+
* (`known`, default: the shipped hook). Anything else, including our own hook with an edit or a foreign hook
|
|
46
|
+
* that merely mentions the marker, is refused unless `force`, and a forced replace is backed up first.
|
|
47
|
+
*/
|
|
48
|
+
export declare function installGitHook(hooksDir: string, hookName: string, scriptContent: string, opts?: {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
known?: string[];
|
|
51
|
+
}): Promise<HookResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Write the manifest. An existing manifest that is not exactly in the shape this tool writes is backed up first.
|
|
54
|
+
*/
|
|
55
|
+
export declare function writeProvenanceConfig(dir: string, selection: HookSelection): Promise<{
|
|
56
|
+
path: string;
|
|
57
|
+
backup?: string;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Install exactly the selected gates. Nothing is enabled that was not selected. The manifest records the
|
|
61
|
+
* selected options merged with earlier recorded MCP choices (never switched off silently) and with reality:
|
|
62
|
+
* preCommit is true when an unmodified generated pre-commit hook is installed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function installHooks(opts: InstallOptions): Promise<InstallResult>;
|
|
65
|
+
export declare const HOOK_FLAGS: readonly ["--pre-commit", "--mcp-verify-read", "--mcp-auto-seal", "--force"];
|
|
66
|
+
export declare const HOOK_USAGE: string;
|
|
67
|
+
/** CLI entry. Returns the process exit code. */
|
|
68
|
+
export declare function runInstallHooksCli(args: string[], cwd?: string, io?: PromptIo): Promise<number>;
|
|
69
|
+
interface PromptIo {
|
|
70
|
+
input?: NodeJS.ReadableStream;
|
|
71
|
+
output?: NodeJS.WritableStream;
|
|
72
|
+
/** Force interactive (true) or non-interactive (false) mode; default is whether stdin and stdout are terminals. */
|
|
73
|
+
interactive?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export {};
|