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.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/SECURITY.md +47 -0
- package/bin/trustdex.mjs +498 -0
- package/docs/ARCHITECTURE.md +43 -0
- package/docs/RELEASING.md +53 -0
- package/docs/ROADMAP.md +33 -0
- package/docs/THREAT_MODEL.md +47 -0
- package/docs/TRUST_STORE.md +89 -0
- package/package.json +47 -0
- package/src/canonical.mjs +17 -0
- package/src/codex-config.mjs +308 -0
- package/src/extensions.mjs +112 -0
- package/src/gate.mjs +39 -0
- package/src/inspect.mjs +121 -0
- package/src/policy-packs.mjs +87 -0
- package/src/policy.mjs +102 -0
- package/src/provenance-online.mjs +394 -0
- package/src/provenance.mjs +194 -0
- package/src/review.mjs +39 -0
- package/src/snapshot.mjs +76 -0
- package/src/trust-record.mjs +59 -0
package/src/snapshot.mjs
ADDED
|
@@ -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
|
+
}
|