@letterblack/lbe-core 1.3.4 → 1.3.6
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/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +2 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +1 -1
- package/README.md +158 -170
- package/RELEASE_WORKSPACE_RULES.md +179 -0
- package/Release-README.md +67 -0
- package/WORKSPACE.md +422 -0
- package/_proof.mjs +246 -0
- package/assets/runtime-boundary.svg +36 -36
- package/bin/lbe.js +12 -0
- package/config/identity.config.json +3 -0
- package/config/policy.default.json +24 -0
- package/dist/cli/lbe.js +4431 -0
- package/dist/hooks/register.cjs +505 -0
- package/dist/state/appendCentral.cjs +87 -0
- package/dist/state/index.cjs +101 -0
- package/exec/cli.js +472 -0
- package/exec/index.js +2 -0
- package/index.js +24 -0
- package/npm-pack.json +0 -0
- package/package.json +77 -45
- package/release/README.md +216 -0
- package/release/TRUST.md +90 -0
- package/release/exec-README.md +215 -0
- package/release/exec-types.d.ts +50 -0
- package/release-exec/LICENSE +1 -0
- package/release-exec/README.md +215 -0
- package/release-exec/assets/lbe-gates.jpg +0 -0
- package/release-exec/assets/lbe-gates.png +0 -0
- package/release-exec/assets/runtime-boundary.svg +36 -0
- package/release-exec/assets/story-allow.jpg +0 -0
- package/release-exec/assets/story-allow.png +0 -0
- package/release-exec/assets/story-deny.jpg +0 -0
- package/release-exec/assets/story-deny.png +0 -0
- package/release-exec/dist/cli.js +2841 -0
- package/release-exec/dist/index.js +1835 -0
- package/release-exec/dist/lbe_engine.wasm +0 -0
- package/{dist → release-exec/dist}/wasm.lock.json +4 -5
- package/release-exec/hooks/register.cjs +473 -0
- package/release-exec/package.json +35 -0
- package/release-exec/types.d.ts +50 -0
- package/runtime/engine.js +322 -0
- package/runtime/lbe_engine.wasm +0 -0
- package/src/cli/commands/assertConsumer.js +198 -0
- package/src/cli/commands/auditVerify.js +36 -0
- package/src/cli/commands/dryrun.js +175 -0
- package/src/cli/commands/health.js +153 -0
- package/src/cli/commands/init.js +306 -0
- package/src/cli/commands/integrityCheck.js +57 -0
- package/src/cli/commands/logs.js +53 -0
- package/src/cli/commands/openState.js +44 -0
- package/src/cli/commands/policyAdd.js +8 -0
- package/src/cli/commands/policyMode.js +7 -0
- package/src/cli/commands/policySign.js +72 -0
- package/src/cli/commands/proof.js +102 -0
- package/src/cli/commands/run.js +342 -0
- package/src/cli/commands/status.js +73 -0
- package/src/cli/commands/verify.js +144 -0
- package/src/cli/main.js +181 -0
- package/src/cli/parseArgs.js +115 -0
- package/src/exec/localExecutor.js +289 -0
- package/src/hooks/register.cjs +505 -0
- package/src/state/appendCentral.cjs +87 -0
- package/src/state/fileIndex.js +140 -0
- package/src/state/index.cjs +101 -0
- package/src/state/index.js +65 -0
- package/src/state/intentRegistry.js +84 -0
- package/src/state/migration.js +112 -0
- package/src/state/proofRunner.js +246 -0
- package/src/state/stateRoot.js +40 -0
- package/src/state/targetRegistry.js +109 -0
- package/src/state/workspaceId.js +40 -0
- package/src/state/workspaceRegistry.js +65 -0
- package/types.d.ts +175 -2
- package/dist/cli.js +0 -141
- package/dist/index.js +0 -52
- /package/dist/{lbe_engine.wasm → cli/lbe_engine.wasm} +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// Runtime boundary for the compiled governance engine.
|
|
2
|
+
// All governance decisions route through here — the WASM module owns the logic.
|
|
3
|
+
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const wasmPath = path.join(runtimeDir, 'lbe_engine.wasm');
|
|
10
|
+
|
|
11
|
+
// ── Result tables (error code → message) ─────────────────────────────────────
|
|
12
|
+
// These map WASM integer codes to human-readable reasons for the JS surface.
|
|
13
|
+
|
|
14
|
+
const POLICY_MESSAGES = {
|
|
15
|
+
0: { allowed: true, reason: null, message: 'Policy check passed' },
|
|
16
|
+
1: { allowed: false, reason: 'POLICY_NOT_CONFIGURED', message: 'No policy configured' },
|
|
17
|
+
2: { allowed: false, reason: 'REQUESTER_NOT_ALLOWED', message: 'Requester not in policy' },
|
|
18
|
+
3: { allowed: false, reason: 'COMMAND_NOT_ALLOWED', message: 'Command not allowed for requester' },
|
|
19
|
+
4: { allowed: false, reason: 'ADAPTER_NOT_ALLOWED', message: 'Adapter not allowed' },
|
|
20
|
+
5: { allowed: false, reason: 'NO_FILESYSTEM_ROOTS_DEFINED', message: 'No filesystem roots defined for requester' },
|
|
21
|
+
6: { allowed: false, reason: 'CWD_OUTSIDE_ALLOWED_ROOT', message: 'Path not under allowed roots' },
|
|
22
|
+
7: { allowed: false, reason: 'PATH_DENIED_BY_PATTERN', message: 'Path matches deny pattern' },
|
|
23
|
+
8: { allowed: false, reason: 'SHELL_CMD_DENIED', message: 'Shell command not allowed' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const SCHEMA_MESSAGES = {
|
|
27
|
+
0: { valid: true, error: null },
|
|
28
|
+
1: { valid: false, error: 'Missing required field: id' },
|
|
29
|
+
2: { valid: false, error: 'Missing required field: commandId' },
|
|
30
|
+
3: { valid: false, error: 'Missing required field: requesterId' },
|
|
31
|
+
4: { valid: false, error: 'Missing required field: sessionId' },
|
|
32
|
+
5: { valid: false, error: 'Missing required field: timestamp' },
|
|
33
|
+
6: { valid: false, error: 'Missing required field: nonce' },
|
|
34
|
+
7: { valid: false, error: 'Missing required field: requires' },
|
|
35
|
+
8: { valid: false, error: 'Missing required field: payload' },
|
|
36
|
+
9: { valid: false, error: 'Missing required field: signature' },
|
|
37
|
+
10: { valid: false, error: "Field 'id' is invalid" },
|
|
38
|
+
11: { valid: false, error: "Field 'commandId' is invalid" },
|
|
39
|
+
12: { valid: false, error: "Field 'requesterId' is invalid" },
|
|
40
|
+
13: { valid: false, error: "Field 'sessionId' is invalid" },
|
|
41
|
+
14: { valid: false, error: "Field 'timestamp' is invalid" },
|
|
42
|
+
15: { valid: false, error: "Field 'nonce' is invalid" },
|
|
43
|
+
16: { valid: false, error: "Field 'requires' is invalid" },
|
|
44
|
+
17: { valid: false, error: 'payload: missing required field: adapter' },
|
|
45
|
+
18: { valid: false, error: "payload: field 'adapter' is invalid" },
|
|
46
|
+
19: { valid: false, error: 'signature: missing required field: alg' },
|
|
47
|
+
20: { valid: false, error: 'signature: missing required field: keyId' },
|
|
48
|
+
21: { valid: false, error: 'signature: missing required field: sig' },
|
|
49
|
+
22: { valid: false, error: "signature: field 'alg' must be ed25519" },
|
|
50
|
+
23: { valid: false, error: "signature: field 'sig' is invalid" },
|
|
51
|
+
24: { valid: false, error: "Field 'risk' is invalid" },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const KEY_REASONS = {
|
|
55
|
+
1: 'KEY_ID_INVALID',
|
|
56
|
+
2: 'KEY_NOT_TRUSTED',
|
|
57
|
+
3: 'KEY_DEPRECATED',
|
|
58
|
+
4: 'KEY_REQUESTER_MISMATCH',
|
|
59
|
+
5: 'KEY_LIFECYCLE_INVALID',
|
|
60
|
+
6: 'KEY_NOT_YET_VALID',
|
|
61
|
+
7: 'KEY_EXPIRED',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const PIPELINE_STAGES = {
|
|
65
|
+
0: 'schema',
|
|
66
|
+
1: 'timestamp',
|
|
67
|
+
2: 'key',
|
|
68
|
+
3: 'signature',
|
|
69
|
+
4: 'rate_limit',
|
|
70
|
+
5: 'nonce',
|
|
71
|
+
6: 'policy',
|
|
72
|
+
255: 'ok',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const RISK_LABELS = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
|
|
76
|
+
const COMMAND_TYPE = { ECHO: 0, READ_FILE: 1, WRITE_FILE: 2, PATCH_FILE: 3, DELETE_FILE: 4, RUN_SHELL: 5 };
|
|
77
|
+
|
|
78
|
+
// ── WASM instance (lazy singleton) ────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
let _instance = null;
|
|
81
|
+
|
|
82
|
+
function wasm() {
|
|
83
|
+
if (_instance) return _instance;
|
|
84
|
+
if (!fs.existsSync(wasmPath)) throw new Error(`LBE engine missing: ${wasmPath}`);
|
|
85
|
+
const bytes = fs.readFileSync(wasmPath);
|
|
86
|
+
_instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), {});
|
|
87
|
+
return _instance;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function memory() {
|
|
91
|
+
return new Uint8Array(wasm().exports.memory.buffer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function inPtr() { return wasm().exports.lbe_in_ptr(); }
|
|
95
|
+
function outPtr() { return wasm().exports.lbe_out_ptr(); }
|
|
96
|
+
function bufSize(){ return wasm().exports.lbe_buf_size(); }
|
|
97
|
+
|
|
98
|
+
// Write a UTF-8 string + null terminator into the WASM input buffer.
|
|
99
|
+
function writeIn(str) {
|
|
100
|
+
const enc = new TextEncoder().encode(str);
|
|
101
|
+
const mem = memory();
|
|
102
|
+
const ptr = inPtr();
|
|
103
|
+
mem.set(enc, ptr);
|
|
104
|
+
mem[ptr + enc.length] = 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Read a null-terminated UTF-8 string from the WASM output buffer.
|
|
108
|
+
function readOut() {
|
|
109
|
+
const mem = memory();
|
|
110
|
+
const ptr = outPtr();
|
|
111
|
+
let end = ptr;
|
|
112
|
+
while (mem[end] !== 0 && end - ptr < bufSize()) end++;
|
|
113
|
+
return new TextDecoder().decode(mem.slice(ptr, end));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Write raw bytes + null to input buffer (for audit hash — two segments).
|
|
117
|
+
function writeInBinary(a, b) {
|
|
118
|
+
const mem = memory();
|
|
119
|
+
const ptr = inPtr();
|
|
120
|
+
let pos = ptr;
|
|
121
|
+
for (let i = 0; i < a.length; i++) mem[pos++] = a[i];
|
|
122
|
+
mem[pos++] = 0;
|
|
123
|
+
for (let i = 0; i < b.length; i++) mem[pos++] = b[i];
|
|
124
|
+
mem[pos] = 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Write 48 u32 values (pipeline input struct) to input buffer.
|
|
128
|
+
function writePipelineInput(fields) {
|
|
129
|
+
const mem = memory();
|
|
130
|
+
const ptr = inPtr();
|
|
131
|
+
const view = new DataView(mem.buffer, ptr);
|
|
132
|
+
fields.forEach((v, i) => view.setUint32(i * 4, v >>> 0, true));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Read 2 u32 values (pipeline output struct) from output buffer.
|
|
136
|
+
function readPipelineOutput() {
|
|
137
|
+
const mem = memory();
|
|
138
|
+
const ptr = outPtr();
|
|
139
|
+
const view = new DataView(mem.buffer, ptr);
|
|
140
|
+
return { stage: view.getUint32(0, true), code: view.getUint32(4, true) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Public engine API ─────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export function getRuntimeInfo() {
|
|
146
|
+
return { mode: 'wasm', available: fs.existsSync(wasmPath), wasmPath, localFirst: true };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function loadWasmEngine() {
|
|
150
|
+
return { ok: true, mode: 'wasm', version: wasm().exports.lbe_engine_version() };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* runValidationPipeline — full 4-gate validation in WASM.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} flags All pre-extracted flags for each gate.
|
|
157
|
+
* @returns {{ stage, stageLabel, code, ok, policyResult, keyReason, schemaError }}
|
|
158
|
+
*/
|
|
159
|
+
export function runValidationPipeline(flags) {
|
|
160
|
+
// Pack the 49-field input struct into IN_BUF as little-endian u32 values.
|
|
161
|
+
// Order must match lib.rs lbe_validate_pipeline documentation.
|
|
162
|
+
writePipelineInput([
|
|
163
|
+
// Schema flags [0..24]
|
|
164
|
+
flags.hasId ? 1 : 0, flags.idValid ? 1 : 0,
|
|
165
|
+
flags.hasCommandId ? 1 : 0, flags.commandIdValid ? 1 : 0,
|
|
166
|
+
flags.hasRequesterId ? 1 : 0, flags.requesterIdValid ? 1 : 0,
|
|
167
|
+
flags.hasSessionId ? 1 : 0, flags.sessionIdValid ? 1 : 0,
|
|
168
|
+
flags.hasTimestamp ? 1 : 0, flags.timestampValid ? 1 : 0,
|
|
169
|
+
flags.hasNonce ? 1 : 0, flags.nonceValid ? 1 : 0,
|
|
170
|
+
flags.hasRequires ? 1 : 0, flags.requiresValid ? 1 : 0,
|
|
171
|
+
flags.hasPayload ? 1 : 0,
|
|
172
|
+
flags.hasPayloadAdapter ? 1 : 0, flags.payloadAdapterValid ? 1 : 0,
|
|
173
|
+
flags.hasSignature ? 1 : 0,
|
|
174
|
+
flags.hasSignatureAlg ? 1 : 0, flags.signatureAlgValid ? 1 : 0,
|
|
175
|
+
flags.hasSignatureKeyId ? 1 : 0,
|
|
176
|
+
flags.hasSignatureSig ? 1 : 0, flags.signatureSigValid ? 1 : 0,
|
|
177
|
+
flags.hasRisk ? 1 : 0, flags.riskValid ? 1 : 0,
|
|
178
|
+
// Timestamp [25..27]
|
|
179
|
+
flags.cmdTimestamp >>> 0, flags.nowSec >>> 0, flags.maxClockSkewSec >>> 0,
|
|
180
|
+
// Key lifecycle [28..34]
|
|
181
|
+
flags.keyIdFormatValid ? 1 : 0,
|
|
182
|
+
flags.keyFound ? 1 : 0,
|
|
183
|
+
flags.keyNotDeprecated ? 1 : 0,
|
|
184
|
+
flags.keyRequesterMatches ? 1 : 0,
|
|
185
|
+
flags.keyNotBeforeOk ? 1 : 0,
|
|
186
|
+
flags.keyNotExpired ? 1 : 0,
|
|
187
|
+
flags.keyLifecycleFieldsPresent ? 1 : 0,
|
|
188
|
+
// Signature [35]
|
|
189
|
+
flags.signatureValid ? 1 : 0,
|
|
190
|
+
// Rate limit [36..37]
|
|
191
|
+
flags.rateLimitOk ? 1 : 0, flags.rateLimitRetryAfterSec >>> 0,
|
|
192
|
+
// Nonce [38]
|
|
193
|
+
flags.nonceOk ? 1 : 0,
|
|
194
|
+
// Policy [39..48]
|
|
195
|
+
flags.policyConfigured ? 1 : 0,
|
|
196
|
+
flags.requesterConfigured ? 1 : 0,
|
|
197
|
+
flags.commandAllowed ? 1 : 0,
|
|
198
|
+
flags.adapterAllowed ? 1 : 0,
|
|
199
|
+
flags.filesystemRequired ? 1 : 0,
|
|
200
|
+
flags.filesystemRootsDefined ? 1 : 0,
|
|
201
|
+
flags.filesystemOk ? 1 : 0,
|
|
202
|
+
flags.pathDenied ? 1 : 0,
|
|
203
|
+
flags.shellRequired ? 1 : 0,
|
|
204
|
+
flags.shellCommandOk ? 1 : 0,
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
wasm().exports.lbe_validate_pipeline();
|
|
208
|
+
const { stage, code } = readPipelineOutput();
|
|
209
|
+
const ok = stage === 255;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
ok,
|
|
213
|
+
stage,
|
|
214
|
+
stageLabel: PIPELINE_STAGES[stage] || 'unknown',
|
|
215
|
+
code,
|
|
216
|
+
schemaError: stage === 0 ? (SCHEMA_MESSAGES[code]?.error || 'Schema invalid') : null,
|
|
217
|
+
keyReason: stage === 2 ? (KEY_REASONS[code] || 'KEY_ERROR') : null,
|
|
218
|
+
policyResult:stage === 6 ? { ...(POLICY_MESSAGES[code] || POLICY_MESSAGES[1]), code } : null,
|
|
219
|
+
retryAfterSec: stage === 4 ? code : 0,
|
|
220
|
+
skewSec: stage === 1 ? code : 0,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* checkNonce — delegates nonce deduplication to WASM.
|
|
226
|
+
* Returns { ok, updatedEntriesText } where updatedEntriesText is the new serialised
|
|
227
|
+
* nonce DB to persist (null when replay detected).
|
|
228
|
+
*/
|
|
229
|
+
export function checkNonce({ ttlSec, nowSec, newKey, existingEntries }) {
|
|
230
|
+
const lines = [`${ttlSec}:${nowSec}`, newKey, ...existingEntries].join('\n') + '\n';
|
|
231
|
+
writeIn(lines);
|
|
232
|
+
const isReplay = wasm().exports.lbe_nonce_check() !== 0;
|
|
233
|
+
if (isReplay) return { ok: false, updatedEntriesText: null };
|
|
234
|
+
const out = readOut();
|
|
235
|
+
// Strip leading "OK\n"
|
|
236
|
+
return { ok: true, updatedEntriesText: out.startsWith('OK\n') ? out.slice(3) : out };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* checkRateLimit — delegates rate-limit sliding-window logic to WASM.
|
|
241
|
+
* Returns { ok, retryAfterSec, updatedEntriesText }.
|
|
242
|
+
*/
|
|
243
|
+
export function checkRateLimit({ windowSec, maxRequests, nowSec, requesterId, existingEntries }) {
|
|
244
|
+
const lines = [
|
|
245
|
+
`${windowSec}:${maxRequests}:${nowSec}`,
|
|
246
|
+
requesterId,
|
|
247
|
+
...existingEntries,
|
|
248
|
+
].join('\n') + '\n';
|
|
249
|
+
writeIn(lines);
|
|
250
|
+
const exceeded = wasm().exports.lbe_rate_check() !== 0;
|
|
251
|
+
const out = readOut();
|
|
252
|
+
if (exceeded) {
|
|
253
|
+
const retryAfterSec = parseInt(out.match(/^EXCEEDED:(\d+)/)?.[1] ?? '1', 10);
|
|
254
|
+
const entriesText = out.replace(/^EXCEEDED:\d+\n/, '');
|
|
255
|
+
return { ok: false, retryAfterSec, updatedEntriesText: entriesText };
|
|
256
|
+
}
|
|
257
|
+
return { ok: true, retryAfterSec: 0, updatedEntriesText: out.startsWith('OK\n') ? out.slice(3) : out };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* computeAuditHash — SHA-256 of (prevHash || entryJson) computed in WASM.
|
|
262
|
+
*/
|
|
263
|
+
export function computeAuditHash(prevHash, entryJson) {
|
|
264
|
+
const enc = new TextEncoder();
|
|
265
|
+
writeInBinary(enc.encode(prevHash), enc.encode(entryJson));
|
|
266
|
+
wasm().exports.lbe_audit_hash();
|
|
267
|
+
return readOut().slice(0, 64); // 64-char hex string
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* classifyRisk — risk level via WASM.
|
|
272
|
+
* commandId: one of ECHO, READ_FILE, WRITE_FILE, PATCH_FILE, DELETE_FILE, RUN_SHELL
|
|
273
|
+
*/
|
|
274
|
+
export function classifyRisk(commandId, shellCmdIsRm = false) {
|
|
275
|
+
const typeCode = COMMAND_TYPE[commandId] ?? 0;
|
|
276
|
+
const code = wasm().exports.lbe_classify_risk(typeCode, shellCmdIsRm ? 1 : 0);
|
|
277
|
+
return RISK_LABELS[code] ?? 'LOW';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* shouldRollback — rollback decision via WASM.
|
|
282
|
+
*/
|
|
283
|
+
export function shouldRollback({ execFailed, postCheckFailed, backupExists, rollbackEnabled }) {
|
|
284
|
+
return wasm().exports.lbe_rollback_decision(
|
|
285
|
+
execFailed ? 1 : 0,
|
|
286
|
+
postCheckFailed ? 1 : 0,
|
|
287
|
+
backupExists ? 1 : 0,
|
|
288
|
+
rollbackEnabled ? 1 : 0
|
|
289
|
+
) === 1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Legacy scalar exports (back-compat) ───────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
export function evaluatePolicyDecision(input) {
|
|
295
|
+
const code = wasm().exports.lbe_policy_decision(
|
|
296
|
+
input.policyConfigured ? 1 : 0, input.requesterConfigured ? 1 : 0,
|
|
297
|
+
input.commandAllowed ? 1 : 0, input.adapterAllowed ? 1 : 0,
|
|
298
|
+
input.filesystemRequired ? 1 : 0, input.filesystemRootsDefined ? 1 : 0,
|
|
299
|
+
input.filesystemOk ? 1 : 0, input.pathDenied ? 1 : 0,
|
|
300
|
+
input.shellRequired ? 1 : 0, input.shellCommandOk ? 1 : 0
|
|
301
|
+
);
|
|
302
|
+
return { ...(POLICY_MESSAGES[code] || POLICY_MESSAGES[1]), code };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function evaluateSchemaDecision(input) {
|
|
306
|
+
const code = wasm().exports.lbe_schema_decision(
|
|
307
|
+
input.hasId ? 1 : 0, input.idValid ? 1 : 0,
|
|
308
|
+
input.hasCommandId ? 1 : 0, input.commandIdValid ? 1 : 0,
|
|
309
|
+
input.hasRequesterId ? 1 : 0, input.requesterIdValid ? 1 : 0,
|
|
310
|
+
input.hasSessionId ? 1 : 0, input.sessionIdValid ? 1 : 0,
|
|
311
|
+
input.hasTimestamp ? 1 : 0, input.timestampValid ? 1 : 0,
|
|
312
|
+
input.hasNonce ? 1 : 0, input.nonceValid ? 1 : 0,
|
|
313
|
+
input.hasRequires ? 1 : 0, input.requiresValid ? 1 : 0,
|
|
314
|
+
input.hasPayload ? 1 : 0,
|
|
315
|
+
input.hasPayloadAdapter ? 1 : 0, input.payloadAdapterValid ? 1 : 0,
|
|
316
|
+
input.hasSignature ? 1 : 0, input.hasSignatureAlg ? 1 : 0,
|
|
317
|
+
input.signatureAlgValid ? 1 : 0, input.hasSignatureKeyId ? 1 : 0,
|
|
318
|
+
input.hasSignatureSig ? 1 : 0, input.signatureSigValid ? 1 : 0,
|
|
319
|
+
input.hasRisk ? 1 : 0, input.riskValid ? 1 : 0
|
|
320
|
+
);
|
|
321
|
+
return { ...(SCHEMA_MESSAGES[code] || SCHEMA_MESSAGES[10]), code };
|
|
322
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/cli/commands/assertConsumer.js
|
|
2
|
+
// Guard that proves a consuming project is using LBE as an installed package,
|
|
3
|
+
// not treating a copied/source repository as release authority.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
|
|
9
|
+
const PACKAGE_NAME = '@letterblack/lbe-core';
|
|
10
|
+
const DEP_SECTIONS = [
|
|
11
|
+
'dependencies',
|
|
12
|
+
'devDependencies',
|
|
13
|
+
'optionalDependencies',
|
|
14
|
+
'peerDependencies'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function readJson(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addCheck(checks, name, ok, details = {}) {
|
|
26
|
+
checks.push({ name, ok, ...details });
|
|
27
|
+
return ok;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findDependencySpec(packageJson) {
|
|
31
|
+
if (!packageJson || typeof packageJson !== 'object') return null;
|
|
32
|
+
|
|
33
|
+
for (const section of DEP_SECTIONS) {
|
|
34
|
+
const value = packageJson[section]?.[PACKAGE_NAME];
|
|
35
|
+
if (value !== undefined) {
|
|
36
|
+
return { section, spec: String(value) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function classifyDependencySpec(spec) {
|
|
44
|
+
const raw = String(spec || '').trim();
|
|
45
|
+
const lower = raw.toLowerCase();
|
|
46
|
+
|
|
47
|
+
if (!raw) {
|
|
48
|
+
return { ok: false, reason: 'EMPTY_DEPENDENCY_SPEC' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const blockedPrefixes = [
|
|
52
|
+
'file:',
|
|
53
|
+
'link:',
|
|
54
|
+
'workspace:',
|
|
55
|
+
'git+',
|
|
56
|
+
'github:',
|
|
57
|
+
'git://',
|
|
58
|
+
'ssh://'
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const prefix of blockedPrefixes) {
|
|
62
|
+
if (lower.startsWith(prefix)) {
|
|
63
|
+
return { ok: false, reason: 'LOCAL_OR_GIT_DEPENDENCY_SPEC', prefix };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (/^[a-z]:[\\/]/i.test(raw) || raw.startsWith('./') || raw.startsWith('../')) {
|
|
68
|
+
return { ok: false, reason: 'PATH_DEPENDENCY_SPEC' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function findPackageRoot(startFile) {
|
|
75
|
+
let dir = fs.statSync(startFile).isDirectory() ? startFile : path.dirname(startFile);
|
|
76
|
+
|
|
77
|
+
while (dir) {
|
|
78
|
+
const candidate = path.join(dir, 'package.json');
|
|
79
|
+
const pkg = readJson(candidate);
|
|
80
|
+
if (pkg?.name === PACKAGE_NAME) return dir;
|
|
81
|
+
|
|
82
|
+
const parent = path.dirname(dir);
|
|
83
|
+
if (parent === dir) return null;
|
|
84
|
+
dir = parent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inspectPackageLock(root) {
|
|
89
|
+
const lockPath = path.join(root, 'package-lock.json');
|
|
90
|
+
const lock = readJson(lockPath);
|
|
91
|
+
if (!lock) return { present: false };
|
|
92
|
+
|
|
93
|
+
const packageEntry = lock.packages?.[`node_modules/${PACKAGE_NAME}`];
|
|
94
|
+
if (!packageEntry) return { present: true, packageEntryPresent: false };
|
|
95
|
+
|
|
96
|
+
const resolved = String(packageEntry.resolved || '');
|
|
97
|
+
const blockedResolved =
|
|
98
|
+
packageEntry.link === true ||
|
|
99
|
+
resolved.startsWith('file:') ||
|
|
100
|
+
resolved.startsWith('git+') ||
|
|
101
|
+
resolved.startsWith('github:') ||
|
|
102
|
+
resolved.startsWith('ssh://') ||
|
|
103
|
+
/^[a-z]:[\\/]/i.test(resolved);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
present: true,
|
|
107
|
+
packageEntryPresent: true,
|
|
108
|
+
link: packageEntry.link === true,
|
|
109
|
+
resolved: resolved || null,
|
|
110
|
+
blockedResolved
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getInstalledPackagePath(root) {
|
|
115
|
+
return path.join(root, 'node_modules', ...PACKAGE_NAME.split('/'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function assertConsumerCommand(opts = {}) {
|
|
119
|
+
const root = path.resolve(opts.root || process.cwd());
|
|
120
|
+
const packageJsonPath = path.join(root, 'package.json');
|
|
121
|
+
const packageJson = readJson(packageJsonPath);
|
|
122
|
+
const checks = [];
|
|
123
|
+
|
|
124
|
+
addCheck(checks, 'project-package-json-present', Boolean(packageJson), { path: packageJsonPath });
|
|
125
|
+
|
|
126
|
+
const dependency = findDependencySpec(packageJson);
|
|
127
|
+
addCheck(checks, 'declares-lbe-package-dependency', Boolean(dependency), dependency || {});
|
|
128
|
+
|
|
129
|
+
const specCheck = dependency ? classifyDependencySpec(dependency.spec) : { ok: false, reason: 'DEPENDENCY_NOT_DECLARED' };
|
|
130
|
+
addCheck(checks, 'dependency-spec-is-registry-package', specCheck.ok, {
|
|
131
|
+
spec: dependency?.spec || null,
|
|
132
|
+
reason: specCheck.reason || null,
|
|
133
|
+
blockedPrefix: specCheck.prefix || null
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
let resolvedEntry = null;
|
|
137
|
+
let resolvedRoot = null;
|
|
138
|
+
try {
|
|
139
|
+
const req = createRequire(path.join(root, 'package.json'));
|
|
140
|
+
resolvedEntry = req.resolve(PACKAGE_NAME);
|
|
141
|
+
resolvedRoot = findPackageRoot(resolvedEntry);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
addCheck(checks, 'lbe-package-resolves-from-project', false, { message: error.message });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (resolvedEntry) {
|
|
147
|
+
addCheck(checks, 'lbe-package-resolves-from-project', true, { resolvedEntry });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (resolvedRoot) {
|
|
151
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
152
|
+
addCheck(checks, 'lbe-package-is-not-symlink', !stat.isSymbolicLink(), { resolvedRoot });
|
|
153
|
+
} else {
|
|
154
|
+
addCheck(checks, 'lbe-package-is-not-symlink', false, { reason: 'PACKAGE_ROOT_NOT_FOUND' });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const installedPackagePath = getInstalledPackagePath(root);
|
|
158
|
+
if (fs.existsSync(installedPackagePath)) {
|
|
159
|
+
const installedStat = fs.lstatSync(installedPackagePath);
|
|
160
|
+
addCheck(checks, 'installed-node_modules-package-is-not-symlink', !installedStat.isSymbolicLink(), {
|
|
161
|
+
installedPackagePath
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
addCheck(checks, 'installed-node_modules-package-is-not-symlink', false, {
|
|
165
|
+
installedPackagePath,
|
|
166
|
+
reason: 'PACKAGE_NOT_INSTALLED_LOCALLY'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const lockInfo = inspectPackageLock(root);
|
|
171
|
+
if (lockInfo.present && lockInfo.packageEntryPresent) {
|
|
172
|
+
addCheck(checks, 'package-lock-does-not-link-local-lbe', !lockInfo.blockedResolved, lockInfo);
|
|
173
|
+
} else {
|
|
174
|
+
addCheck(checks, 'package-lock-does-not-link-local-lbe', true, lockInfo);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ok = checks.every((check) => check.ok);
|
|
178
|
+
const result = {
|
|
179
|
+
ok,
|
|
180
|
+
command: 'assert-consumer',
|
|
181
|
+
package: PACKAGE_NAME,
|
|
182
|
+
root,
|
|
183
|
+
classification: ok
|
|
184
|
+
? 'consumer-project-using-installed-registry-dependency'
|
|
185
|
+
: 'not-proven-consumer-installed-dependency',
|
|
186
|
+
releaseClaimsAllowed: false,
|
|
187
|
+
message: ok
|
|
188
|
+
? 'This project consumes LBE as an installed package dependency. This does not certify LBE release safety.'
|
|
189
|
+
: 'This project is not proven to consume LBE only as an installed registry dependency.',
|
|
190
|
+
checks
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
console.log(JSON.stringify(result, null, 2));
|
|
194
|
+
|
|
195
|
+
if (!ok) {
|
|
196
|
+
process.exitCode = 7;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/cli/commands/auditVerify.js
|
|
2
|
+
// Verify audit log hash-chain integrity
|
|
3
|
+
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { verifyAuditLogIntegrity } from '../../core/auditLog.js';
|
|
6
|
+
|
|
7
|
+
function toBoolean(value, defaultValue) {
|
|
8
|
+
if (value === undefined) return defaultValue;
|
|
9
|
+
if (value === true || value === false) return value;
|
|
10
|
+
const normalized = String(value).trim().toLowerCase();
|
|
11
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes') return true;
|
|
12
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no') return false;
|
|
13
|
+
return defaultValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function auditVerifyCommand(opts) {
|
|
17
|
+
const auditPath = opts.audit ? path.resolve(opts.audit) : path.resolve('.lbe/data/audit.log.jsonl');
|
|
18
|
+
const failFast = toBoolean(opts['fail-fast'], true);
|
|
19
|
+
const jsonOutput = toBoolean(opts.json, true);
|
|
20
|
+
const maxEntries = Number.isFinite(Number(opts.max)) ? Number(opts.max) : undefined;
|
|
21
|
+
|
|
22
|
+
const result = verifyAuditLogIntegrity(auditPath, { failFast, maxEntries });
|
|
23
|
+
|
|
24
|
+
if (jsonOutput) {
|
|
25
|
+
console.log(JSON.stringify(result, null, 2));
|
|
26
|
+
} else if (result.valid) {
|
|
27
|
+
console.log(`OK: ${result.file}`);
|
|
28
|
+
console.log(`Entries: ${result.entries}`);
|
|
29
|
+
} else {
|
|
30
|
+
console.log(`FAIL: ${result.file}`);
|
|
31
|
+
console.log(`First invalid index: ${result.firstInvalidIndex}`);
|
|
32
|
+
console.log(`Reason: ${result.reason}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.exit(result.valid ? 0 : 8);
|
|
36
|
+
}
|