@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.
package/dist/merkle.js ADDED
@@ -0,0 +1,176 @@
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.nodeHash = exports.leafHash = exports.MERKLE_ALGORITHM = void 0;
37
+ exports.leavesFromRegistry = leavesFromRegistry;
38
+ exports.buildLevels = buildLevels;
39
+ exports.merkleRoot = merkleRoot;
40
+ exports.inclusionProof = inclusionProof;
41
+ exports.verifyProof = verifyProof;
42
+ const crypto = __importStar(require("crypto"));
43
+ /**
44
+ * RFC 6962 Merkle tree over vault artifacts, with inclusion proofs.
45
+ *
46
+ * WHY THIS REPLACES scanAndAudit's ROOT
47
+ * -------------------------------------
48
+ * `ProvenanceVault.scanAndAudit` computed its "merkleRoot" as a single SHA-256
49
+ * over every hash concatenated together. That is a manifest checksum. It detects
50
+ * that something changed and nothing else - in particular it cannot produce an
51
+ * inclusion proof, which is the only reason to use a tree in the first place.
52
+ *
53
+ * With a tree, a third party can verify ONE document against a published root
54
+ * without downloading the whole registry: log2(n) hashes, about 9 for a few
55
+ * hundred artifacts. With a linear digest they would need every artifact hash in
56
+ * the vault to check a single file.
57
+ *
58
+ * DOMAIN SEPARATION
59
+ * -----------------
60
+ * Leaves are hashed with a 0x00 prefix and internal nodes with 0x01. Without it
61
+ * an internal node can be presented as a leaf - a second-preimage attack on the
62
+ * tree shape.
63
+ *
64
+ * Odd nodes are PROMOTED, never duplicated. Duplicating the last leaf lets two
65
+ * distinct trees share a root, which is the Bitcoin CVE-2012-2459 class of bug.
66
+ *
67
+ * PARITY WITH THE PYTHON IMPLEMENTATION
68
+ * -------------------------------------
69
+ * This is a deliberate port of the vault's `_merkle.py` and must agree with it
70
+ * byte for byte - two disagreeing provenance implementations would be worse than
71
+ * one honest flat digest. Leaf ordering, leaf encoding (note the TWO spaces
72
+ * between hash and path) and the empty-tree value are all fixed by that
73
+ * requirement, and `merkle.test.ts` checks against roots generated by the Python.
74
+ */
75
+ exports.MERKLE_ALGORITHM = 'merkle-sha256-rfc6962';
76
+ const LEAF_PREFIX = Buffer.from([0x00]);
77
+ const NODE_PREFIX = Buffer.from([0x01]);
78
+ const sha256 = (b) => crypto.createHash('sha256').update(b).digest();
79
+ const leafHash = (data) => sha256(Buffer.concat([LEAF_PREFIX, data]));
80
+ exports.leafHash = leafHash;
81
+ const nodeHash = (left, right) => sha256(Buffer.concat([NODE_PREFIX, left, right]));
82
+ exports.nodeHash = nodeHash;
83
+ /**
84
+ * Deterministic leaf order: sorted by path. Walk order is not stable across
85
+ * filesystems; sorting is.
86
+ */
87
+ function leavesFromRegistry(artifacts) {
88
+ return Object.keys(artifacts).sort().map(p => [p, (0, exports.leafHash)(Buffer.from(`${artifacts[p].sha256} ${p}`, 'utf8'))]);
89
+ }
90
+ /** Every level, leaves first, root last. */
91
+ function buildLevels(leaves) {
92
+ if (leaves.length === 0)
93
+ return [[sha256(Buffer.alloc(0))]];
94
+ const levels = [leaves];
95
+ while (levels[levels.length - 1].length > 1) {
96
+ const cur = levels[levels.length - 1];
97
+ const next = [];
98
+ for (let i = 0; i + 1 < cur.length; i += 2)
99
+ next.push((0, exports.nodeHash)(cur[i], cur[i + 1]));
100
+ if (cur.length % 2)
101
+ next.push(cur[cur.length - 1]); // promote, never duplicate
102
+ levels.push(next);
103
+ }
104
+ return levels;
105
+ }
106
+ function merkleRoot(artifacts) {
107
+ const leaves = leavesFromRegistry(artifacts).map(([, h]) => h);
108
+ const levels = buildLevels(leaves);
109
+ return levels[levels.length - 1][0].toString('hex');
110
+ }
111
+ /** The sibling hashes needed to rebuild the root from one leaf. */
112
+ function inclusionProof(artifacts, target) {
113
+ const entries = leavesFromRegistry(artifacts);
114
+ const idx = entries.findIndex(([p]) => p === target);
115
+ if (idx === -1)
116
+ return null;
117
+ const levels = buildLevels(entries.map(([, h]) => h));
118
+ const proof = [];
119
+ let i = idx;
120
+ for (const level of levels.slice(0, -1)) {
121
+ const sibling = i ^ 1;
122
+ if (sibling < level.length) {
123
+ proof.push({
124
+ side: sibling > i ? 'right' : 'left',
125
+ hash: level[sibling].toString('hex')
126
+ });
127
+ }
128
+ // A promoted node has no sibling at this level; the index still halves.
129
+ i = Math.floor(i / 2);
130
+ }
131
+ return {
132
+ algorithm: exports.MERKLE_ALGORITHM,
133
+ artifact: target,
134
+ artifact_sha256: artifacts[target].sha256,
135
+ leaf_sha256: entries[idx][1].toString('hex'),
136
+ leaf_index: idx,
137
+ tree_size: entries.length,
138
+ proof,
139
+ merkle_root: levels[levels.length - 1][0].toString('hex')
140
+ };
141
+ }
142
+ /**
143
+ * Recompute the root from a leaf plus its audit path.
144
+ * If expectedRoot is provided, validates that the proof resolves to it.
145
+ */
146
+ function verifyProof(proof, expectedRoot, expectedArtifact) {
147
+ try {
148
+ const hex = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
149
+ if (!proof || proof.algorithm !== exports.MERKLE_ALGORITHM || typeof proof.artifact !== 'string' || !proof.artifact ||
150
+ !hex(proof.artifact_sha256) || !hex(proof.leaf_sha256) || !hex(proof.merkle_root) ||
151
+ !Number.isSafeInteger(proof.tree_size) || proof.tree_size < 1 || !Number.isSafeInteger(proof.leaf_index) || proof.leaf_index < 0 || proof.leaf_index >= proof.tree_size ||
152
+ !Array.isArray(proof.proof) || proof.proof.length > 53 || (expectedRoot !== undefined && (!hex(expectedRoot) || proof.merkle_root !== expectedRoot)))
153
+ return false;
154
+ if (expectedArtifact && (expectedArtifact.path !== proof.artifact || expectedArtifact.sha256 !== proof.artifact_sha256))
155
+ return false;
156
+ let h = (0, exports.leafHash)(Buffer.from(`${proof.artifact_sha256} ${proof.artifact}`, 'utf8'));
157
+ if (h.toString('hex') !== proof.leaf_sha256)
158
+ return false;
159
+ let index = proof.leaf_index, width = proof.tree_size, stepIndex = 0;
160
+ while (width > 1) {
161
+ if (index % 2 === 1 || index + 1 < width) {
162
+ const step = proof.proof[stepIndex++];
163
+ if (!step || !hex(step.hash) || step.side !== (index % 2 === 1 ? 'left' : 'right'))
164
+ return false;
165
+ const sibling = Buffer.from(step.hash, 'hex');
166
+ h = step.side === 'left' ? (0, exports.nodeHash)(sibling, h) : (0, exports.nodeHash)(h, sibling);
167
+ }
168
+ index = Math.floor(index / 2);
169
+ width = Math.ceil(width / 2);
170
+ }
171
+ return stepIndex === proof.proof.length && h.toString('hex') === proof.merkle_root;
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ }
@@ -0,0 +1,21 @@
1
+ export interface ConditionResult {
2
+ name: string;
3
+ passed: boolean;
4
+ durationMs: number;
5
+ error?: string;
6
+ details?: string;
7
+ }
8
+ export interface SelfTestReport {
9
+ passed: boolean;
10
+ version: string;
11
+ nodeVersion: string;
12
+ platform: string;
13
+ conditions: ConditionResult[];
14
+ totalDurationMs: number;
15
+ verifiedPackageArtifacts: number;
16
+ }
17
+ export declare function runSelfTests(options?: {
18
+ packageRoot?: string;
19
+ verbose?: boolean;
20
+ }): Promise<SelfTestReport>;
21
+ export declare function printSelfTestReport(report: SelfTestReport): void;
@@ -0,0 +1,280 @@
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.runSelfTests = runSelfTests;
37
+ exports.printSelfTestReport = printSelfTestReport;
38
+ const crypto = __importStar(require("crypto"));
39
+ const fs = __importStar(require("fs/promises"));
40
+ const path = __importStar(require("path"));
41
+ const index_1 = require("./index");
42
+ const merkle_1 = require("./merkle");
43
+ const artifactEnvelope_1 = require("./artifactEnvelope");
44
+ const timestamp_1 = require("./timestamp");
45
+ async function runSelfTests(options = {}) {
46
+ const startTime = Date.now();
47
+ const root = options.packageRoot ?? path.resolve(__dirname, '..');
48
+ const conditions = [];
49
+ // Helper to record condition results
50
+ async function runCondition(name, fn) {
51
+ const t0 = Date.now();
52
+ try {
53
+ const details = await fn();
54
+ conditions.push({
55
+ name,
56
+ passed: true,
57
+ durationMs: Date.now() - t0,
58
+ details: details || undefined
59
+ });
60
+ }
61
+ catch (err) {
62
+ conditions.push({
63
+ name,
64
+ passed: false,
65
+ durationMs: Date.now() - t0,
66
+ error: err?.message || String(err)
67
+ });
68
+ }
69
+ }
70
+ // Condition 1: Ed25519 Micro-Signatures & Structured V2 Framing
71
+ await runCondition('Ed25519 Micro-Signatures & V2 Framing', async () => {
72
+ const keyPair = index_1.ProvenanceEngine.generateEd25519KeyPair();
73
+ const digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
74
+ const payload = index_1.ProvenanceEngine.buildSigningPayload({
75
+ scope: 'moonglade:self-test:doc',
76
+ sha256: digest,
77
+ signer: 'test@moonglade.ai'
78
+ });
79
+ const signature = crypto.sign(null, payload, keyPair.privateKey);
80
+ const valid = crypto.verify(null, payload, keyPair.publicKey, signature);
81
+ if (!valid)
82
+ throw new Error('Valid signature failed verification');
83
+ // Reject tampered payload
84
+ const tamperedPayload = index_1.ProvenanceEngine.buildSigningPayload({
85
+ scope: 'moonglade:self-test:doc',
86
+ sha256: '0000000000000000000000000000000000000000000000000000000000000000',
87
+ signer: 'test@moonglade.ai'
88
+ });
89
+ const tamperedPayloadValid = crypto.verify(null, tamperedPayload, keyPair.publicKey, signature);
90
+ if (tamperedPayloadValid)
91
+ throw new Error('Tampered payload was accepted as valid');
92
+ // Reject tampered signature
93
+ const corruptedSig = Buffer.from(signature);
94
+ corruptedSig[0] ^= 0xff;
95
+ const tamperedSigValid = crypto.verify(null, payload, keyPair.publicKey, corruptedSig);
96
+ if (tamperedSigValid)
97
+ throw new Error('Corrupted signature was accepted as valid');
98
+ return 'Sign, verify, and tamper rejection passed';
99
+ });
100
+ // Condition 2: RFC 6962 Merkle Trees & Leaf Inclusion Proofs
101
+ await runCondition('RFC 6962 Merkle Trees & Inclusion Proofs', async () => {
102
+ const mockRegistry = {
103
+ 'docs/a.md': { sha256: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' },
104
+ 'docs/b.md': { sha256: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' },
105
+ 'docs/c.md': { sha256: 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' },
106
+ 'docs/d.md': { sha256: 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd' }
107
+ };
108
+ const root = (0, merkle_1.merkleRoot)(mockRegistry);
109
+ if (!/^[0-9a-f]{64}$/.test(root))
110
+ throw new Error('Invalid Merkle root derived');
111
+ const proof = (0, merkle_1.inclusionProof)(mockRegistry, 'docs/c.md');
112
+ if (!proof)
113
+ throw new Error('Inclusion proof derivation failed for existing leaf');
114
+ const validProof = (0, merkle_1.verifyProof)(proof, root);
115
+ if (!validProof)
116
+ throw new Error('Valid inclusion proof failed mathematical verification');
117
+ // Corrupted proof must fail
118
+ const corruptedProof = JSON.parse(JSON.stringify(proof));
119
+ corruptedProof.proof[0].hash = '0000000000000000000000000000000000000000000000000000000000000000';
120
+ if ((0, merkle_1.verifyProof)(corruptedProof, root))
121
+ throw new Error('Corrupted inclusion proof was accepted');
122
+ // Missing artifact returns null
123
+ if ((0, merkle_1.inclusionProof)(mockRegistry, 'docs/missing.md') !== null) {
124
+ throw new Error('Inclusion proof returned non-null for missing leaf');
125
+ }
126
+ return `Tree of size 4 verified; root ${root.slice(0, 12)}...`;
127
+ });
128
+ // Condition 3: Artifact Envelope v1 Contract & Canonical Byte Binding
129
+ await runCondition('Artifact Envelope v1 Schema & Hash Binding', async () => {
130
+ const rawBytes = Buffer.from('# Moonglade Self Test\nInvariant proof substrate.', 'utf8');
131
+ const expectedId = (0, artifactEnvelope_1.artifactId)(rawBytes);
132
+ const envelope = (0, artifactEnvelope_1.createArtifactEnvelope)({
133
+ artifact_type: 'release',
134
+ canonical_bytes: expectedId,
135
+ created_at: new Date().toISOString(),
136
+ parents: [],
137
+ attestations: [{ method: 'self-test', status: 'provisional' }],
138
+ status: 'signed',
139
+ evidence: { integrity: 'valid', signature: 'provisional', timestamp: 'none', scope: 'valid' },
140
+ generator: { agent: 'synthetic-test', model: 'self-test-harness' },
141
+ bytes: rawBytes
142
+ });
143
+ if (!(0, artifactEnvelope_1.validateArtifactEnvelope)(envelope))
144
+ throw new Error('Constructed envelope failed schema validation');
145
+ if (envelope.artifact_id !== expectedId)
146
+ throw new Error('Envelope artifact_id does not match canonical bytes hash');
147
+ // Tampered schema must fail
148
+ const tampered = { ...envelope, artifact_id: 'invalid-id' };
149
+ if ((0, artifactEnvelope_1.validateArtifactEnvelope)(tampered))
150
+ throw new Error('Invalid envelope format was accepted as valid');
151
+ return `Envelope ${envelope.artifact_id.slice(0, 19)}... verified`;
152
+ });
153
+ // Condition 4: Fail-Closed Gate Barriers & Anti-Tampering Rejection
154
+ await runCondition('Fail-Closed Gate Barriers & Mutation Rejection', async () => {
155
+ const tempDir = path.join(root, '.tmp-selftest-' + Date.now());
156
+ await fs.mkdir(tempDir, { recursive: true });
157
+ const testDoc = path.join(tempDir, 'gate-test.md');
158
+ const keyPair = index_1.ProvenanceEngine.generateEd25519KeyPair();
159
+ try {
160
+ await fs.writeFile(testDoc, '# Gate Assertion Test\nOriginal untouched bytes.\n', 'utf8');
161
+ await index_1.ProvenanceEngine.sealDocument(testDoc, keyPair.privateKey, 'test-signer', 'head', {
162
+ vaultRoot: tempDir,
163
+ timestamp: false
164
+ });
165
+ // Valid check must pass
166
+ const initialVerify = await index_1.ProvenanceEngine.verifyDocument(testDoc, keyPair.publicKey);
167
+ if (!initialVerify.verified) {
168
+ throw new Error(`Expected verification to succeed: ${initialVerify.error}`);
169
+ }
170
+ // Mutate the document
171
+ await fs.appendFile(testDoc, '\nMalicious unsealed line inserted.\n', 'utf8');
172
+ const tamperedVerify = await index_1.ProvenanceEngine.verifyDocument(testDoc, keyPair.publicKey);
173
+ if (tamperedVerify.verified) {
174
+ throw new Error('Mutated document passed verification; fail-closed gate violated');
175
+ }
176
+ if (!tamperedVerify.error?.includes('Hash mismatch')) {
177
+ throw new Error(`Expected hash mismatch error, got: ${tamperedVerify.error}`);
178
+ }
179
+ return 'Sealed artifact verified; post-seal modification rejected fail-closed';
180
+ }
181
+ finally {
182
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
183
+ }
184
+ });
185
+ // Condition 5: Concurrent Degrading Timestamping Architecture (DTA)
186
+ await runCondition('Concurrent Degrading Timestamp Architecture (DTA)', async () => {
187
+ const dummyDigest = crypto.createHash('sha256').update('dta-test').digest();
188
+ const dta = await (0, timestamp_1.concurrentDegradingTimestamp)(dummyDigest, { specifiedTier: 'L4_HOST_UNAUTHENTICATED' });
189
+ if (!dta.tier || dta.tier !== 'L4_HOST_UNAUTHENTICATED') {
190
+ throw new Error(`Expected L4 offline degradation tier, got ${dta.tier}`);
191
+ }
192
+ if (!dta.tsa_time || isNaN(Date.parse(dta.tsa_time))) {
193
+ throw new Error('Invalid ISO timestamp returned by DTA degradation fallback');
194
+ }
195
+ if (!Array.isArray(dta.probes) || dta.probes.length === 0) {
196
+ throw new Error('Expected probe audit telemetry in DTA result');
197
+ }
198
+ return `Degraded to ${dta.tier} at ${dta.tsa_time}`;
199
+ });
200
+ // Condition 6: Package Self-Provenance (All Shipped Items Carry Provenance)
201
+ let verifiedCount = 0;
202
+ await runCondition('Package Self-Provenance Verification', async () => {
203
+ // Look for all .provenance.json files directly in package root and docs
204
+ const entries = await fs.readdir(root, { withFileTypes: true });
205
+ const sidecarFiles = [];
206
+ for (const ent of entries) {
207
+ if (ent.isFile() && ent.name.endsWith('.provenance.json')) {
208
+ sidecarFiles.push(path.join(root, ent.name));
209
+ }
210
+ }
211
+ // Only package-root sidecars belong to this distribution inventory.
212
+ if (sidecarFiles.length === 0) {
213
+ return 'No shipped sidecars; distribution files are intentionally unsealed pending release approval';
214
+ }
215
+ for (const sidecarPath of sidecarFiles) {
216
+ const sidecarRaw = await fs.readFile(sidecarPath, 'utf8');
217
+ const sidecar = JSON.parse(sidecarRaw);
218
+ const targetPath = sidecarPath.replace(/\.provenance\.json$/, '');
219
+ let fileBytes;
220
+ try {
221
+ fileBytes = await fs.readFile(targetPath);
222
+ }
223
+ catch {
224
+ // Special case: package.provenance.json may target package.json
225
+ if (path.basename(sidecarPath) === 'package.provenance.json') {
226
+ fileBytes = await fs.readFile(path.join(root, 'package.json'));
227
+ }
228
+ else {
229
+ throw new Error(`Target artifact missing for sidecar: ${path.basename(sidecarPath)}`);
230
+ }
231
+ }
232
+ const compareBytes = index_1.ProvenanceEngine.canonicalizeBytes(fileBytes, path.basename(sidecarPath) === 'package.provenance.json' ? 'package.json' : targetPath);
233
+ const computedHash = crypto.createHash('sha256').update(compareBytes).digest('hex');
234
+ if (!/^[a-f0-9]{64}$/.test(sidecar.sha256_at_last_write) || sidecar.sha256_at_last_write !== computedHash) {
235
+ throw new Error(`Hash mismatch on shipped artifact ${path.basename(targetPath)}: sidecar has ${sidecar.sha256_at_last_write}, actual is ${computedHash}`);
236
+ }
237
+ if (sidecar.envelope) {
238
+ if (!(0, artifactEnvelope_1.validateArtifactEnvelope)(sidecar.envelope)) {
239
+ throw new Error(`Invalid envelope on shipped artifact ${path.basename(targetPath)}`);
240
+ }
241
+ }
242
+ verifiedCount++;
243
+ }
244
+ return `${verifiedCount} shipped sidecar hashes compared; no package signature verification performed`;
245
+ });
246
+ const totalDurationMs = Date.now() - startTime;
247
+ const passed = conditions.every(c => c.passed);
248
+ return {
249
+ passed,
250
+ version: require('../package.json').version,
251
+ nodeVersion: process.version,
252
+ platform: `${process.platform}-${process.arch}`,
253
+ conditions,
254
+ totalDurationMs,
255
+ verifiedPackageArtifacts: verifiedCount
256
+ };
257
+ }
258
+ function printSelfTestReport(report) {
259
+ console.log(`\nMoonglade Provenance Self-Test Suite v${report.version} (${report.platform}, Node ${report.nodeVersion})`);
260
+ console.log('='.repeat(75));
261
+ for (let i = 0; i < report.conditions.length; i++) {
262
+ const c = report.conditions[i];
263
+ const statusTag = c.passed ? '[PASS]' : '[FAIL]';
264
+ console.log(`${statusTag} ${i + 1}. ${c.name} (${c.durationMs}ms)`);
265
+ if (c.details) {
266
+ console.log(` Evidence: ${c.details}`);
267
+ }
268
+ if (c.error) {
269
+ console.log(` Error: ${c.error}`);
270
+ }
271
+ }
272
+ console.log('-'.repeat(75));
273
+ if (report.passed) {
274
+ console.log(`All ${report.conditions.length} condition suites PASSED (${report.totalDurationMs}ms). Environment verified.`);
275
+ }
276
+ else {
277
+ const failedCount = report.conditions.filter(c => !c.passed).length;
278
+ console.error(`${failedCount} condition suite(s) FAILED (${report.totalDurationMs}ms). Verification failed.`);
279
+ }
280
+ }