trustdex 0.4.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,76 @@
1
+ import { sha256Json } from './canonical.mjs';
2
+
3
+ export function createSnapshot(results) {
4
+ const normalized = results
5
+ .map(({ name, kind = 'mcp', source, provenance = { status: 'unknown' }, fingerprint = null, signals = [], action }) => ({
6
+ name,
7
+ kind,
8
+ source,
9
+ provenance,
10
+ fingerprint,
11
+ signals: [...signals].sort(),
12
+ action
13
+ }))
14
+ .sort((a, b) => a.name.localeCompare(b.name));
15
+
16
+ return {
17
+ schemaVersion: 3,
18
+ createdAt: new Date().toISOString(),
19
+ digest: sha256Json(normalized),
20
+ servers: normalized
21
+ };
22
+ }
23
+
24
+ export function diffSnapshots(before, after) {
25
+ const left = new Map((before?.servers || []).map((server) => [server.name, server]));
26
+ const right = new Map((after?.servers || []).map((server) => [server.name, server]));
27
+ const changes = [];
28
+
29
+ for (const [name, current] of right) {
30
+ const previous = left.get(name);
31
+ if (!previous) {
32
+ changes.push({ type: 'server-added', name, current });
33
+ continue;
34
+ }
35
+
36
+ const previousSignals = previous.signals || [];
37
+ const currentSignals = current.signals || [];
38
+ const addedSignals = currentSignals.filter((signal) => !previousSignals.includes(signal));
39
+ const removedSignals = previousSignals.filter((signal) => !currentSignals.includes(signal));
40
+
41
+ if (addedSignals.length) changes.push({ type: 'signals-added', name, signals: addedSignals });
42
+ if (removedSignals.length) changes.push({ type: 'signals-removed', name, signals: removedSignals });
43
+
44
+ if (previous.action !== current.action) {
45
+ changes.push({ type: 'decision-changed', name, from: previous.action, to: current.action });
46
+ }
47
+
48
+ if (JSON.stringify(previous.source) !== JSON.stringify(current.source)) {
49
+ changes.push({ type: 'source-changed', name, from: previous.source, to: current.source });
50
+ }
51
+
52
+ if (JSON.stringify(previous.provenance || {}) !== JSON.stringify(current.provenance || {})) {
53
+ changes.push({
54
+ type: 'provenance-changed',
55
+ name,
56
+ from: previous.provenance || { status: 'unknown' },
57
+ to: current.provenance || { status: 'unknown' }
58
+ });
59
+ }
60
+
61
+ if ((previous.fingerprint || null) !== (current.fingerprint || null)) {
62
+ changes.push({
63
+ type: 'fingerprint-changed',
64
+ name,
65
+ from: previous.fingerprint || null,
66
+ to: current.fingerprint || null
67
+ });
68
+ }
69
+ }
70
+
71
+ for (const name of left.keys()) {
72
+ if (!right.has(name)) changes.push({ type: 'server-removed', name });
73
+ }
74
+
75
+ return changes;
76
+ }
@@ -0,0 +1,59 @@
1
+ import crypto from 'node:crypto';
2
+ import { canonicalJson, sha256Json } from './canonical.mjs';
3
+
4
+ function payloadFromRecord(record) {
5
+ return {
6
+ schemaVersion: record.schemaVersion,
7
+ algorithm: record.algorithm,
8
+ snapshotDigest: record.snapshotDigest,
9
+ policyDigest: record.policyDigest || null,
10
+ sourceRef: record.sourceRef || null,
11
+ createdAt: record.createdAt
12
+ };
13
+ }
14
+
15
+ export function generateTrustKeyPair() {
16
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
17
+ return {
18
+ privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' }),
19
+ publicKey: publicKey.export({ type: 'spki', format: 'pem' })
20
+ };
21
+ }
22
+
23
+ export function createTrustRecord(snapshot, privateKeyPem, options = {}) {
24
+ if (!snapshot?.digest) throw new Error('Snapshot is missing a digest.');
25
+ const record = {
26
+ schemaVersion: 1,
27
+ algorithm: 'ed25519',
28
+ snapshotDigest: snapshot.digest,
29
+ policyDigest: options.policy ? sha256Json(options.policy) : null,
30
+ sourceRef: options.sourceRef || null,
31
+ createdAt: new Date().toISOString()
32
+ };
33
+ const signature = crypto.sign(
34
+ null,
35
+ Buffer.from(canonicalJson(payloadFromRecord(record))),
36
+ privateKeyPem
37
+ );
38
+ return { ...record, signature: signature.toString('base64') };
39
+ }
40
+
41
+ export function verifyTrustRecord(record, publicKeyPem, options = {}) {
42
+ if (!record || record.algorithm !== 'ed25519' || !record.signature) {
43
+ return { valid: false, reason: 'unsupported or incomplete trust record' };
44
+ }
45
+ if (options.snapshot?.digest && options.snapshot.digest !== record.snapshotDigest) {
46
+ return { valid: false, reason: 'snapshot digest does not match trust record' };
47
+ }
48
+ if (options.policy && sha256Json(options.policy) !== record.policyDigest) {
49
+ return { valid: false, reason: 'policy digest does not match trust record' };
50
+ }
51
+
52
+ const valid = crypto.verify(
53
+ null,
54
+ Buffer.from(canonicalJson(payloadFromRecord(record))),
55
+ publicKeyPem,
56
+ Buffer.from(record.signature, 'base64')
57
+ );
58
+ return { valid, reason: valid ? null : 'signature verification failed' };
59
+ }