prae-cli 0.2.1 → 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/README.md +16 -0
- package/lib/bin.js +4 -2
- package/lib/{cli-6hDu8-nF.js → cli-BvAYSK5w.js} +219 -595
- package/lib/evaluate-BilZez15.js +419 -0
- package/lib/index.js +4 -2
- package/lib/{instructions-ijuj4IP1.js → instructions-D4VOn2lk.js} +1 -1
- package/lib/mcp-BH5I4Eyu.js +208 -0
- package/lib/playground.js +38 -1
- package/lib/secret-catcher.prae.json +49 -0
- package/lib/src-Br1tGPtS.js +182 -0
- package/lib/src-CAUNqZJ1.js +225 -0
- package/lib/src-CBK_quVO.js +3 -0
- package/lib/src-Vnr0fcpK.js +3 -0
- package/package.json +5 -3
- /package/lib/{src-BrZXeEez.js → src-CCeafjdm.js} +0 -0
- /package/lib/{src-Dd2_8m2g.js → src-aU5WW02C.js} +0 -0
- /package/lib/{views-BU0sOciR.js → views-DalPkBZZ.js} +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"prae": "0.1",
|
|
3
|
+
"pack": "secret-catcher",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"id": "SEC-001",
|
|
8
|
+
"title": "Reads carrying credentials are refused",
|
|
9
|
+
"when": {
|
|
10
|
+
"capability": "fs:read",
|
|
11
|
+
"path": {
|
|
12
|
+
"outside": [
|
|
13
|
+
"./**/test/**",
|
|
14
|
+
"./**/tests/**",
|
|
15
|
+
"./**/fixtures/**",
|
|
16
|
+
"./**/*.example",
|
|
17
|
+
"./**/*.sample"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"secrets": { "minConfidence": 0.8 }
|
|
21
|
+
},
|
|
22
|
+
"effect": {
|
|
23
|
+
"kind": "deny",
|
|
24
|
+
"because": "the requested file contains a protected credential; the contents were not returned"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "SEC-002",
|
|
29
|
+
"title": "Private keys are refused everywhere",
|
|
30
|
+
"when": {
|
|
31
|
+
"capability": "fs:read",
|
|
32
|
+
"secrets": { "types": ["private-key"] }
|
|
33
|
+
},
|
|
34
|
+
"effect": {
|
|
35
|
+
"kind": "deny",
|
|
36
|
+
"because": "the requested file contains a private key; the contents were not returned"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "SEC-003",
|
|
41
|
+
"title": "Anything credential-shaped is at least recorded",
|
|
42
|
+
"when": { "capability": "fs:read", "secrets": { "minConfidence": 0.5 } },
|
|
43
|
+
"effect": {
|
|
44
|
+
"kind": "warn",
|
|
45
|
+
"because": "a possible credential was noticed in this file; where no stronger rule fired, the read proceeded and this row is the record"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//#region ../inspect/src/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* The built-in detectors.
|
|
4
|
+
*
|
|
5
|
+
* Anchored provider formats first — those prefixes exist precisely so that
|
|
6
|
+
* scanners can find them, and they carry the confidence to prove it. The
|
|
7
|
+
* loose assignment patterns at the bottom earn their keep only with an
|
|
8
|
+
* entropy check, because `password = "password"` is not a leak, it is a test.
|
|
9
|
+
*/
|
|
10
|
+
const DETECTORS = [
|
|
11
|
+
{
|
|
12
|
+
rule: "private-key",
|
|
13
|
+
type: "private-key",
|
|
14
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g,
|
|
15
|
+
confidence: .99
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
rule: "aws-access-key-id",
|
|
19
|
+
type: "cloud-credential",
|
|
20
|
+
pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g,
|
|
21
|
+
confidence: .9
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
rule: "github-token",
|
|
25
|
+
type: "token",
|
|
26
|
+
pattern: /\b(?:gh[poasur]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,255})\b/g,
|
|
27
|
+
confidence: .95
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
rule: "gitlab-token",
|
|
31
|
+
type: "token",
|
|
32
|
+
pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g,
|
|
33
|
+
confidence: .95
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
rule: "slack-token",
|
|
37
|
+
type: "token",
|
|
38
|
+
pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
39
|
+
confidence: .9
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
rule: "stripe-live-key",
|
|
43
|
+
type: "api-key",
|
|
44
|
+
pattern: /\b[sr]k_live_[A-Za-z0-9]{16,}\b/g,
|
|
45
|
+
confidence: .95
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
rule: "google-api-key",
|
|
49
|
+
type: "api-key",
|
|
50
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
51
|
+
confidence: .9
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
rule: "anthropic-key",
|
|
55
|
+
type: "api-key",
|
|
56
|
+
pattern: /\bsk-ant-[A-Za-z0-9-]{32,}\b/g,
|
|
57
|
+
confidence: .95
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
rule: "openai-key",
|
|
61
|
+
type: "api-key",
|
|
62
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/g,
|
|
63
|
+
confidence: .8
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
rule: "npm-token",
|
|
67
|
+
type: "token",
|
|
68
|
+
pattern: /\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
69
|
+
confidence: .95
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
rule: "credential-in-url",
|
|
73
|
+
type: "credential-in-url",
|
|
74
|
+
pattern: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqps?|ftp):\/\/[^:\s/@'"]+:[^@\s/'"]+@/g,
|
|
75
|
+
confidence: .85
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
rule: "jwt",
|
|
79
|
+
type: "token",
|
|
80
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/g,
|
|
81
|
+
confidence: .6
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
rule: "password-assignment",
|
|
85
|
+
type: "password",
|
|
86
|
+
pattern: /\b(?:password|passwd|pwd)\b\s*[:=]\s*['"]([^'"\s]{6,})['"]/gi,
|
|
87
|
+
confidence: .5,
|
|
88
|
+
minEntropy: 3
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
rule: "secret-assignment",
|
|
92
|
+
type: "generic-secret",
|
|
93
|
+
pattern: /\b(?:secret|token|api[_-]?key|auth[_-]?token|access[_-]?key)\b\w*\s*[:=]\s*['"]([A-Za-z0-9/+_=-]{20,})['"]/gi,
|
|
94
|
+
confidence: .6,
|
|
95
|
+
minEntropy: 3.5
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
/**
|
|
99
|
+
* Markers of a value that exists to be seen.
|
|
100
|
+
*
|
|
101
|
+
* A finding whose match carries one of these is dropped, not down-scored:
|
|
102
|
+
* `AKIAIOSFODNN7EXAMPLE` is in more documentation than any real key is in
|
|
103
|
+
* code, and a gate that denies the README teaches operators to turn it off.
|
|
104
|
+
* The AWS docs key is listed literally because it does not say "example"
|
|
105
|
+
* anywhere a case-insensitive substring would find. Real secrets do not
|
|
106
|
+
* contain these words; a placeholder that does is doing its job.
|
|
107
|
+
*/
|
|
108
|
+
const PLACEHOLDER = /example|changeme|change-me|placeholder|your[_-]|xxxx|dummy|sample|redacted|<[a-z_-]+>|AKIAIOSFODNN7/i;
|
|
109
|
+
/** Shannon entropy in bits per character. */
|
|
110
|
+
function entropy(text) {
|
|
111
|
+
if (text.length === 0) return 0;
|
|
112
|
+
const counts = /* @__PURE__ */ new Map();
|
|
113
|
+
for (const ch of text) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
114
|
+
let sum = 0;
|
|
115
|
+
for (const n of counts.values()) {
|
|
116
|
+
const p = n / text.length;
|
|
117
|
+
sum -= p * Math.log2(p);
|
|
118
|
+
}
|
|
119
|
+
return sum;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Content larger than this is not scanned — the caller is told, not silently
|
|
123
|
+
* covered. Regex over an unbounded file in the hot path of every read is how
|
|
124
|
+
* a gate becomes an outage; 2 MiB covers any file a secret plausibly lives in.
|
|
125
|
+
*/
|
|
126
|
+
const MAX_SCAN_BYTES = 2 * 1024 * 1024;
|
|
127
|
+
/** NUL in the first 8 KiB: binary, not text. Scanned as nothing. */
|
|
128
|
+
function looksBinary(content) {
|
|
129
|
+
return content.slice(0, 8192).includes("\0");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Scan text for secrets. Findings in document order, deduplicated by span —
|
|
133
|
+
* when two rules claim the same region, the higher confidence keeps it.
|
|
134
|
+
*/
|
|
135
|
+
function scan(content, options = {}) {
|
|
136
|
+
if (looksBinary(content)) return [];
|
|
137
|
+
const detectors = options.detectors ?? DETECTORS;
|
|
138
|
+
const floor = options.minConfidence ?? 0;
|
|
139
|
+
const starts = [0];
|
|
140
|
+
for (let i = 0; i < content.length; i++) if (content[i] === "\n") starts.push(i + 1);
|
|
141
|
+
const position = (offset) => {
|
|
142
|
+
let low = 0;
|
|
143
|
+
let high = starts.length - 1;
|
|
144
|
+
while (low < high) {
|
|
145
|
+
const mid = low + high + 1 >> 1;
|
|
146
|
+
if (starts[mid] <= offset) low = mid;
|
|
147
|
+
else high = mid - 1;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
line: low + 1,
|
|
151
|
+
column: offset - starts[low] + 1
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const bySpan = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const detector of detectors) {
|
|
156
|
+
const pattern = new RegExp(detector.pattern.source, detector.pattern.flags);
|
|
157
|
+
for (const match of content.matchAll(pattern)) {
|
|
158
|
+
const text = match[0];
|
|
159
|
+
if (PLACEHOLDER.test(text)) continue;
|
|
160
|
+
if (detector.minEntropy !== void 0) {
|
|
161
|
+
if (entropy(match[1] ?? text) < detector.minEntropy) continue;
|
|
162
|
+
}
|
|
163
|
+
if (detector.confidence < floor) continue;
|
|
164
|
+
const key = `${match.index}:${text.length}`;
|
|
165
|
+
const standing = bySpan.get(key);
|
|
166
|
+
if (standing !== void 0 && standing.confidence >= detector.confidence) continue;
|
|
167
|
+
const { line, column } = position(match.index);
|
|
168
|
+
bySpan.set(key, {
|
|
169
|
+
rule: detector.rule,
|
|
170
|
+
type: detector.type,
|
|
171
|
+
line,
|
|
172
|
+
column,
|
|
173
|
+
length: text.length,
|
|
174
|
+
confidence: detector.confidence
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return [...bySpan.values()].sort((a, b) => a.line - b.line || a.column - b.column);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
export { scan as a, looksBinary as i, MAX_SCAN_BYTES as n, entropy as r, DETECTORS as t };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
//#region ../ledger/src/entry.ts
|
|
6
|
+
/** The hash a chain starts from. */
|
|
7
|
+
const GENESIS = "0".repeat(64);
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic serialization: object keys sorted at every depth.
|
|
10
|
+
*
|
|
11
|
+
* A hash over `JSON.stringify(entry)` would depend on key insertion order,
|
|
12
|
+
* which means a re-serialized entry could hash differently while saying
|
|
13
|
+
* exactly the same thing — and a verifier that reports tampering on a
|
|
14
|
+
* faithful copy trains its operators to ignore it.
|
|
15
|
+
*/
|
|
16
|
+
function canonical(value) {
|
|
17
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
18
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
19
|
+
return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
20
|
+
}
|
|
21
|
+
/** Hash one entry's content together with its position and predecessor. */
|
|
22
|
+
function hashEntry(record, seq, prev) {
|
|
23
|
+
return createHash("sha256").update(canonical({
|
|
24
|
+
...record,
|
|
25
|
+
seq,
|
|
26
|
+
prev
|
|
27
|
+
})).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
/** Chain a record onto a predecessor. */
|
|
30
|
+
function chain(record, seq, prev) {
|
|
31
|
+
return {
|
|
32
|
+
...record,
|
|
33
|
+
seq,
|
|
34
|
+
prev,
|
|
35
|
+
hash: hashEntry(record, seq, prev)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** The short form an auditor quotes from a screenshot. */
|
|
39
|
+
function seal(entry) {
|
|
40
|
+
return `${entry.hash.slice(0, 4)}…${entry.hash.slice(-4)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region ../ledger/src/receipt.ts
|
|
45
|
+
function generateKeypair() {
|
|
46
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
47
|
+
return {
|
|
48
|
+
publicKey: publicKey.export({
|
|
49
|
+
type: "spki",
|
|
50
|
+
format: "pem"
|
|
51
|
+
}).toString(),
|
|
52
|
+
privateKey: privateKey.export({
|
|
53
|
+
type: "pkcs8",
|
|
54
|
+
format: "pem"
|
|
55
|
+
}).toString()
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Sign a row's seal. The seal already covers the whole canonical record. */
|
|
59
|
+
function signEntry(entry, privateKeyPem) {
|
|
60
|
+
const key = createPrivateKey(privateKeyPem);
|
|
61
|
+
return sign(null, Buffer.from(entry.hash, "hex"), key).toString("base64");
|
|
62
|
+
}
|
|
63
|
+
function verifySignature(entry, signature, publicKeyPem) {
|
|
64
|
+
try {
|
|
65
|
+
const key = createPublicKey(publicKeyPem);
|
|
66
|
+
return verify(null, Buffer.from(entry.hash, "hex"), key, Buffer.from(signature, "base64"));
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function makeReceipt(entry, privateKeyPem, publicKeyPem) {
|
|
72
|
+
return {
|
|
73
|
+
receipt: "prae/1",
|
|
74
|
+
entry,
|
|
75
|
+
signature: signEntry(entry, privateKeyPem),
|
|
76
|
+
publicKey: publicKeyPem
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A short identifier for a public key, for pinning by eye.
|
|
81
|
+
*
|
|
82
|
+
* A counterparty checks a receipt against a key they already trust; comparing
|
|
83
|
+
* the whole PEM by hand invites skimming, and eight grouped hex bytes do not.
|
|
84
|
+
*/
|
|
85
|
+
function fingerprint(publicKeyPem) {
|
|
86
|
+
const der = createPublicKey(publicKeyPem).export({
|
|
87
|
+
type: "spki",
|
|
88
|
+
format: "der"
|
|
89
|
+
});
|
|
90
|
+
return createHash("sha256").update(der).digest("hex").slice(0, 16).replace(/(.{4})/g, "$1 ").trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region ../ledger/src/ledger.ts
|
|
95
|
+
/**
|
|
96
|
+
* Read a ledger held as text.
|
|
97
|
+
*
|
|
98
|
+
* Split out from the class because a ledger is not always a file being
|
|
99
|
+
* appended to: a build step renders one it imported, and a reader pastes one
|
|
100
|
+
* in. Those callers need the same parsing and the same verification the CLI
|
|
101
|
+
* runs, and a second implementation of either is how two answers to "does this
|
|
102
|
+
* chain hold" come to exist.
|
|
103
|
+
*/
|
|
104
|
+
function readEntries(text) {
|
|
105
|
+
return text.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
|
|
106
|
+
}
|
|
107
|
+
/** Re-derive every hash and confirm each entry names its predecessor. */
|
|
108
|
+
function verifyEntries(text) {
|
|
109
|
+
const problems = [];
|
|
110
|
+
let expectedPrev = GENESIS;
|
|
111
|
+
let expectedSeq = 0;
|
|
112
|
+
let count = 0;
|
|
113
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
114
|
+
for (const [index, line] of lines.entries()) {
|
|
115
|
+
const at = index + 1;
|
|
116
|
+
count += 1;
|
|
117
|
+
let entry;
|
|
118
|
+
try {
|
|
119
|
+
entry = JSON.parse(line);
|
|
120
|
+
} catch {
|
|
121
|
+
problems.push({
|
|
122
|
+
line: at,
|
|
123
|
+
message: "not valid JSON"
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
entries: count,
|
|
128
|
+
problems
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (entry.seq !== expectedSeq) problems.push({
|
|
132
|
+
line: at,
|
|
133
|
+
message: `expected seq ${expectedSeq}, found ${entry.seq}`
|
|
134
|
+
});
|
|
135
|
+
if (entry.prev !== expectedPrev) problems.push({
|
|
136
|
+
line: at,
|
|
137
|
+
message: "does not name the previous entry; a row was removed or reordered"
|
|
138
|
+
});
|
|
139
|
+
const { seq, prev, hash, sig: _sig,...record } = entry;
|
|
140
|
+
if (hashEntry(record, seq, prev) !== hash) problems.push({
|
|
141
|
+
line: at,
|
|
142
|
+
message: "contents do not match the sealed hash"
|
|
143
|
+
});
|
|
144
|
+
expectedPrev = entry.hash;
|
|
145
|
+
expectedSeq = entry.seq + 1;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
ok: problems.length === 0,
|
|
149
|
+
entries: count,
|
|
150
|
+
problems
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
var Ledger = class Ledger {
|
|
154
|
+
path;
|
|
155
|
+
seq;
|
|
156
|
+
last;
|
|
157
|
+
constructor(path, seq, last) {
|
|
158
|
+
this.path = path;
|
|
159
|
+
this.seq = seq;
|
|
160
|
+
this.last = last;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Open a ledger, reading the tail to continue an existing chain.
|
|
164
|
+
*
|
|
165
|
+
* A corrupt or truncated final line is a hard error rather than something to
|
|
166
|
+
* skip past: appending onto a chain whose end we could not read would bury
|
|
167
|
+
* the damage under valid-looking entries.
|
|
168
|
+
*/
|
|
169
|
+
/** Sign every appended row with this Ed25519 private key (PEM). */
|
|
170
|
+
signWith;
|
|
171
|
+
static open(path, options = {}) {
|
|
172
|
+
const ledger = Ledger.openUnsigned(path);
|
|
173
|
+
ledger.signWith = options.signWith;
|
|
174
|
+
return ledger;
|
|
175
|
+
}
|
|
176
|
+
static openUnsigned(path) {
|
|
177
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
178
|
+
let text = "";
|
|
179
|
+
try {
|
|
180
|
+
text = readFileSync(path, "utf8");
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error.code !== "ENOENT") throw error;
|
|
183
|
+
writeFileSync(path, "", { flag: "wx" });
|
|
184
|
+
return new Ledger(path, 0, GENESIS);
|
|
185
|
+
}
|
|
186
|
+
const tail = text.split("\n").filter((line) => line.trim() !== "").at(-1);
|
|
187
|
+
if (tail === void 0) return new Ledger(path, 0, GENESIS);
|
|
188
|
+
let entry;
|
|
189
|
+
try {
|
|
190
|
+
entry = JSON.parse(tail);
|
|
191
|
+
} catch {
|
|
192
|
+
throw new Error(`prae ledger: last line of ${path} is not readable; refusing to append onto a chain that cannot be continued`);
|
|
193
|
+
}
|
|
194
|
+
return new Ledger(path, entry.seq + 1, entry.hash);
|
|
195
|
+
}
|
|
196
|
+
/** Append one decision and return the entry as written. */
|
|
197
|
+
append(record) {
|
|
198
|
+
const sealed = chain(record, this.seq, this.last);
|
|
199
|
+
const entry = this.signWith === void 0 ? sealed : {
|
|
200
|
+
...sealed,
|
|
201
|
+
sig: signEntry(sealed, this.signWith)
|
|
202
|
+
};
|
|
203
|
+
appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
|
|
204
|
+
this.seq += 1;
|
|
205
|
+
this.last = entry.hash;
|
|
206
|
+
return entry;
|
|
207
|
+
}
|
|
208
|
+
/** Every entry, in order. */
|
|
209
|
+
read() {
|
|
210
|
+
return readEntries(readFileSync(this.path, "utf8"));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Re-derive every hash and confirm each entry names its predecessor.
|
|
214
|
+
*
|
|
215
|
+
* Catches a deleted row, an edited field, and a reordering. Does not catch a
|
|
216
|
+
* wholesale rewrite of the file by someone holding the same code — see the
|
|
217
|
+
* note in `entry.ts`.
|
|
218
|
+
*/
|
|
219
|
+
verify() {
|
|
220
|
+
return verifyEntries(readFileSync(this.path, "utf8"));
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
export { generateKeypair as a, verifySignature as c, chain as d, hashEntry as f, fingerprint as i, GENESIS as l, readEntries as n, makeReceipt as o, seal as p, verifyEntries as r, signEntry as s, Ledger as t, canonical as u };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as generateKeypair, c as verifySignature, d as chain, f as hashEntry, i as fingerprint, l as GENESIS, n as readEntries, o as makeReceipt, p as seal, r as verifyEntries, s as signEntry, t as Ledger, u as canonical } from "./src-CAUNqZJ1.js";
|
|
2
|
+
|
|
3
|
+
export { GENESIS, Ledger, canonical, chain, fingerprint, generateKeypair, hashEntry, makeReceipt, readEntries, seal, signEntry, verifyEntries, verifySignature };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prae-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Operator policy for agent harnesses: validate packs, explain a decision, verify a decision log, and provision a governed DSH profile.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"node": ">=20"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
34
35
|
"capmark": "^0.3.1",
|
|
35
36
|
"dsh-overlay-check": "^0.4.0"
|
|
36
37
|
},
|
|
@@ -41,10 +42,11 @@
|
|
|
41
42
|
"vitest": "^3.2.4",
|
|
42
43
|
"@prae/controls": "0.1.0",
|
|
43
44
|
"@prae/extract": "0.1.0",
|
|
45
|
+
"@prae/inspect": "0.1.0",
|
|
44
46
|
"@prae/instructions": "0.1.0",
|
|
47
|
+
"@prae/kernel": "0.1.0",
|
|
45
48
|
"@prae/ledger": "0.1.0",
|
|
46
|
-
"@prae/tokens": "0.1.0"
|
|
47
|
-
"@prae/kernel": "0.1.0"
|
|
49
|
+
"@prae/tokens": "0.1.0"
|
|
48
50
|
},
|
|
49
51
|
"scripts": {
|
|
50
52
|
"test": "vitest run",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|