@peac/cli 0.11.1 → 0.11.3
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/commands/reconcile.d.ts +17 -0
- package/dist/commands/reconcile.d.ts.map +1 -0
- package/dist/index.cjs +284 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +285 -4
- package/dist/index.mjs.map +1 -1
- package/dist/lib/conformance/digest.d.ts.map +1 -1
- package/dist/lib/conformance/profiles.d.ts.map +1 -1
- package/dist/lib/conformance/validators.d.ts.map +1 -1
- package/dist/lib/conformance-runner.d.ts.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PEAC Reconcile CLI Command (v0.11.3+)
|
|
3
|
+
*
|
|
4
|
+
* Merge two evidence bundles and detect conflicts.
|
|
5
|
+
* Conflict key: composite (iss, jti) with 3-step fallback:
|
|
6
|
+
* 1. (iss, jti) from JWT claims
|
|
7
|
+
* 2. (iss, rid) for Wire 0.1
|
|
8
|
+
* 3. (iss, receipt_ref) where receipt_ref = sha256(receipt_jws)
|
|
9
|
+
*
|
|
10
|
+
* NO auto-resolution: conflicts are surfaced for human decision.
|
|
11
|
+
* Deterministic output: receipts and conflicts sorted by (iss, jti) lexicographically.
|
|
12
|
+
*
|
|
13
|
+
* Security: 16 MB bundle size limit, path traversal prevention.
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from 'commander';
|
|
16
|
+
export declare function reconcileCommand(): Command;
|
|
17
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/commands/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqNpC,wBAAgB,gBAAgB,IAAI,OAAO,CAuH1C"}
|
package/dist/index.cjs
CHANGED
|
@@ -11,6 +11,7 @@ var child_process = require('child_process');
|
|
|
11
11
|
var crypto$1 = require('crypto');
|
|
12
12
|
var schema = require('@peac/schema');
|
|
13
13
|
var disc = require('@peac/disc');
|
|
14
|
+
var audit = require('@peac/audit');
|
|
14
15
|
|
|
15
16
|
function _interopNamespace(e) {
|
|
16
17
|
if (e && e.__esModule) return e;
|
|
@@ -597,7 +598,8 @@ function computeCanonicalDigest(input) {
|
|
|
597
598
|
const canonical = crypto.canonicalize(input);
|
|
598
599
|
return { alg: "sha-256+jcs", value: sha256(canonical) };
|
|
599
600
|
} catch {
|
|
600
|
-
|
|
601
|
+
const serialized = JSON.stringify(input) ?? "null";
|
|
602
|
+
return { alg: "sha-256+json", value: sha256(serialized) };
|
|
601
603
|
}
|
|
602
604
|
}
|
|
603
605
|
var VECTORS_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".txt", ".jwks", ".pem", ".zip"]);
|
|
@@ -748,6 +750,18 @@ var PROFILE_CAPABILITIES = {
|
|
|
748
750
|
level: "shape",
|
|
749
751
|
validator: "structural-check",
|
|
750
752
|
notes: "Structure-only; no offer/receipt verification or term matching"
|
|
753
|
+
},
|
|
754
|
+
errors: {
|
|
755
|
+
profile: "errors.hints",
|
|
756
|
+
level: "semantic",
|
|
757
|
+
validator: "validateErrorHintsInput",
|
|
758
|
+
notes: "Validates next_action hint table shape and vocabulary (DD-132, DD-133)"
|
|
759
|
+
},
|
|
760
|
+
"content-usage": {
|
|
761
|
+
profile: "content-usage.parse",
|
|
762
|
+
level: "shape",
|
|
763
|
+
validator: "validateContentUsageInput",
|
|
764
|
+
notes: "Validates Content-Usage fixture input type; full parsing tested in dedicated suite"
|
|
751
765
|
}
|
|
752
766
|
};
|
|
753
767
|
function getCategoryCapability(category) {
|
|
@@ -1034,6 +1048,68 @@ function validateIssueInput(input) {
|
|
|
1034
1048
|
}
|
|
1035
1049
|
return { valid: true };
|
|
1036
1050
|
}
|
|
1051
|
+
var VALID_NEXT_ACTIONS = /* @__PURE__ */ new Set([
|
|
1052
|
+
"retry_after_delay",
|
|
1053
|
+
"retry_with_different_key",
|
|
1054
|
+
"retry_with_different_input",
|
|
1055
|
+
"refresh_attestation",
|
|
1056
|
+
"contact_issuer",
|
|
1057
|
+
"abort",
|
|
1058
|
+
"none"
|
|
1059
|
+
]);
|
|
1060
|
+
function validateErrorHintsInput(input) {
|
|
1061
|
+
if (typeof input !== "object" || input === null) {
|
|
1062
|
+
return {
|
|
1063
|
+
valid: false,
|
|
1064
|
+
error_code: "E_INVALID_ERROR_HINTS",
|
|
1065
|
+
error_message: "Error hints input must be an object"
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const obj = input;
|
|
1069
|
+
if (!obj.hints || typeof obj.hints !== "object") {
|
|
1070
|
+
return {
|
|
1071
|
+
valid: false,
|
|
1072
|
+
error_code: "E_MISSING_HINTS",
|
|
1073
|
+
error_message: "Error hints missing required hints object"
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const hints = obj.hints;
|
|
1077
|
+
for (const [code, hint] of Object.entries(hints)) {
|
|
1078
|
+
if (typeof hint !== "object" || hint === null) {
|
|
1079
|
+
return {
|
|
1080
|
+
valid: false,
|
|
1081
|
+
error_code: "E_INVALID_HINT",
|
|
1082
|
+
error_message: `Hint for ${code} must be an object`
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
const h = hint;
|
|
1086
|
+
if (!VALID_NEXT_ACTIONS.has(h.next_action)) {
|
|
1087
|
+
return {
|
|
1088
|
+
valid: false,
|
|
1089
|
+
error_code: "E_INVALID_NEXT_ACTION",
|
|
1090
|
+
error_message: `Hint for ${code} has invalid next_action: ${String(h.next_action)}`
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof h.retryable !== "boolean") {
|
|
1094
|
+
return {
|
|
1095
|
+
valid: false,
|
|
1096
|
+
error_code: "E_INVALID_RETRYABLE",
|
|
1097
|
+
error_message: `Hint for ${code} has non-boolean retryable: ${String(h.retryable)}`
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return { valid: true };
|
|
1102
|
+
}
|
|
1103
|
+
function validateContentUsageInput(input) {
|
|
1104
|
+
if (typeof input === "string") {
|
|
1105
|
+
return { valid: true };
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
valid: false,
|
|
1109
|
+
error_code: "E_INVALID_CONTENT_USAGE",
|
|
1110
|
+
error_message: "Content-Usage input must be a string (HTTP header value)"
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1037
1113
|
var CATEGORY_VALIDATORS = {
|
|
1038
1114
|
// Receipt claim validation
|
|
1039
1115
|
valid: (input) => {
|
|
@@ -1070,7 +1146,9 @@ var CATEGORY_VALIDATORS = {
|
|
|
1070
1146
|
return validateReceiptPayload(obj.payload ?? obj.claims ?? input);
|
|
1071
1147
|
},
|
|
1072
1148
|
workflow: validateWorkflowInput,
|
|
1073
|
-
x402: validateX402Input
|
|
1149
|
+
x402: validateX402Input,
|
|
1150
|
+
errors: validateErrorHintsInput,
|
|
1151
|
+
"content-usage": validateContentUsageInput
|
|
1074
1152
|
};
|
|
1075
1153
|
function getValidator(category) {
|
|
1076
1154
|
return CATEGORY_VALIDATORS[category] ?? ((input) => {
|
|
@@ -1169,8 +1247,22 @@ function runConformance(options, callbacks) {
|
|
|
1169
1247
|
const f = pack.fixtures[i];
|
|
1170
1248
|
const fixtureName = f.name ?? `fixture_${i}`;
|
|
1171
1249
|
const testId = `${baseTestId}.${fixtureName.replace(/\s+/g, "_")}`;
|
|
1172
|
-
const inputDigest = computeCanonicalDigest(f.input);
|
|
1173
1250
|
callbacks?.onTestStart?.(testId);
|
|
1251
|
+
if (!f.expected || typeof f.expected.valid !== "boolean") {
|
|
1252
|
+
skipped++;
|
|
1253
|
+
const result2 = {
|
|
1254
|
+
id: testId,
|
|
1255
|
+
category: profile,
|
|
1256
|
+
status: "skip",
|
|
1257
|
+
diagnostics: {
|
|
1258
|
+
skip_reason: "Non-standard fixture format (domain-specific test suite)"
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
results.push(result2);
|
|
1262
|
+
callbacks?.onTestComplete?.(result2);
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const inputDigest = computeCanonicalDigest(f.input);
|
|
1174
1266
|
if (typeof f.input === "object" && f.input !== null) {
|
|
1175
1267
|
const inputObj = f.input;
|
|
1176
1268
|
if (inputObj.claims !== void 0 && inputObj.payload !== void 0) {
|
|
@@ -2109,6 +2201,194 @@ JWKS for verification: ${path6__namespace.relative(outputDir, jwksPath)}`);
|
|
|
2109
2201
|
process.exitCode = 1;
|
|
2110
2202
|
}
|
|
2111
2203
|
});
|
|
2204
|
+
var MAX_BUNDLE_SIZE = 16 * 1024 * 1024;
|
|
2205
|
+
function getGlobalOptions4(cmd) {
|
|
2206
|
+
const parent = cmd.parent;
|
|
2207
|
+
if (!parent) return {};
|
|
2208
|
+
return parent.opts();
|
|
2209
|
+
}
|
|
2210
|
+
function outputError4(error, details, opts) {
|
|
2211
|
+
if (opts.json) {
|
|
2212
|
+
console.log(JSON.stringify({ success: false, error, ...details }, null, 2));
|
|
2213
|
+
} else {
|
|
2214
|
+
console.error(`Error: ${error}`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
function decodeJwsPayload(jws) {
|
|
2218
|
+
try {
|
|
2219
|
+
const { payload } = crypto.decode(jws);
|
|
2220
|
+
return payload;
|
|
2221
|
+
} catch {
|
|
2222
|
+
return null;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
function extractConflictKey(jws, receiptRef) {
|
|
2226
|
+
const payload = decodeJwsPayload(jws);
|
|
2227
|
+
if (!payload) {
|
|
2228
|
+
return `unknown|${receiptRef}`;
|
|
2229
|
+
}
|
|
2230
|
+
const iss = typeof payload.iss === "string" ? payload.iss : "unknown";
|
|
2231
|
+
if (typeof payload.jti === "string" && payload.jti.length > 0) {
|
|
2232
|
+
return `${iss}|${payload.jti}`;
|
|
2233
|
+
}
|
|
2234
|
+
if (typeof payload.rid === "string" && payload.rid.length > 0) {
|
|
2235
|
+
return `${iss}|${payload.rid}`;
|
|
2236
|
+
}
|
|
2237
|
+
return `${iss}|${receiptRef}`;
|
|
2238
|
+
}
|
|
2239
|
+
async function extractReceipts(contents, sourceLabel) {
|
|
2240
|
+
const records = [];
|
|
2241
|
+
for (const [_id, jws] of contents.receipts) {
|
|
2242
|
+
const receiptRef = await schema.computeReceiptRef(jws);
|
|
2243
|
+
const key = extractConflictKey(jws, receiptRef);
|
|
2244
|
+
records.push({ key, jws, receipt_ref: receiptRef, source: sourceLabel });
|
|
2245
|
+
}
|
|
2246
|
+
return records;
|
|
2247
|
+
}
|
|
2248
|
+
function findDiffFields(jwsA, jwsB) {
|
|
2249
|
+
const payloadA = decodeJwsPayload(jwsA);
|
|
2250
|
+
const payloadB = decodeJwsPayload(jwsB);
|
|
2251
|
+
if (!payloadA || !payloadB) return ["payload_decode_failed"];
|
|
2252
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(payloadA), ...Object.keys(payloadB)]);
|
|
2253
|
+
const diffs = [];
|
|
2254
|
+
for (const k of allKeys) {
|
|
2255
|
+
if (JSON.stringify(payloadA[k]) !== JSON.stringify(payloadB[k])) {
|
|
2256
|
+
diffs.push(k);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
return diffs.sort();
|
|
2260
|
+
}
|
|
2261
|
+
async function readBundle(bundlePath, label) {
|
|
2262
|
+
if (!fs6__namespace.existsSync(bundlePath)) {
|
|
2263
|
+
return { ok: false, error: `Bundle file not found: ${bundlePath}` };
|
|
2264
|
+
}
|
|
2265
|
+
const stat = fs6__namespace.statSync(bundlePath);
|
|
2266
|
+
if (stat.size > MAX_BUNDLE_SIZE) {
|
|
2267
|
+
return { ok: false, error: `Bundle ${label} exceeds 16 MB size limit (${stat.size} bytes)` };
|
|
2268
|
+
}
|
|
2269
|
+
const resolved = fs6__namespace.realpathSync(bundlePath);
|
|
2270
|
+
if (resolved !== bundlePath && !resolved.startsWith(process.cwd())) {
|
|
2271
|
+
if (!fs6__namespace.existsSync(resolved)) {
|
|
2272
|
+
return { ok: false, error: `Path traversal detected for ${label}` };
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const zipBuffer = fs6__namespace.readFileSync(bundlePath);
|
|
2276
|
+
const result = await audit.readDisputeBundle(zipBuffer);
|
|
2277
|
+
if (!result.ok) {
|
|
2278
|
+
return { ok: false, error: `Failed to read ${label}: ${result.error.message}` };
|
|
2279
|
+
}
|
|
2280
|
+
return { ok: true, value: result.value };
|
|
2281
|
+
}
|
|
2282
|
+
function formatReportText2(report) {
|
|
2283
|
+
const lines = [
|
|
2284
|
+
"PEAC Reconciliation Report",
|
|
2285
|
+
"==========================",
|
|
2286
|
+
"",
|
|
2287
|
+
`Generated: ${report.generated_at}`,
|
|
2288
|
+
`Bundles: ${report.bundles.join(", ")}`,
|
|
2289
|
+
`Total receipts: ${report.total_receipts}`,
|
|
2290
|
+
`Merged receipts: ${report.merged_receipts}`,
|
|
2291
|
+
`Conflicts: ${report.conflicts.length}`,
|
|
2292
|
+
""
|
|
2293
|
+
];
|
|
2294
|
+
if (report.conflicts.length === 0) {
|
|
2295
|
+
lines.push("No conflicts detected. Bundles merge cleanly.");
|
|
2296
|
+
} else {
|
|
2297
|
+
lines.push("Conflicts:");
|
|
2298
|
+
lines.push("---------");
|
|
2299
|
+
for (const conflict of report.conflicts) {
|
|
2300
|
+
lines.push(` Key: ${conflict.key}`);
|
|
2301
|
+
lines.push(` Bundle A ref: ${conflict.bundle_a_receipt_ref}`);
|
|
2302
|
+
lines.push(` Bundle B ref: ${conflict.bundle_b_receipt_ref}`);
|
|
2303
|
+
lines.push(` Different fields: ${conflict.diff_fields.join(", ")}`);
|
|
2304
|
+
lines.push("");
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return lines.join("\n");
|
|
2308
|
+
}
|
|
2309
|
+
function reconcileCommand() {
|
|
2310
|
+
const reconcile = new commander.Command("reconcile").description(
|
|
2311
|
+
"Merge evidence bundles and detect conflicts (v0.11.3+)"
|
|
2312
|
+
);
|
|
2313
|
+
reconcile.argument("<bundle1>", "Path to first bundle ZIP file").argument("<bundle2>", "Path to second bundle ZIP file").option("-f, --format <format>", "Output format: json or text", "text").option("--fail-on-conflict", "Exit code 1 when any conflict detected", false).action(
|
|
2314
|
+
async (bundle1Path, bundle2Path, options, cmd) => {
|
|
2315
|
+
const globalOpts = getGlobalOptions4(cmd);
|
|
2316
|
+
const format = options.format || "text";
|
|
2317
|
+
const failOnConflict = !!options.failOnConflict;
|
|
2318
|
+
try {
|
|
2319
|
+
const [result1, result2] = await Promise.all([
|
|
2320
|
+
readBundle(bundle1Path, "bundle1"),
|
|
2321
|
+
readBundle(bundle2Path, "bundle2")
|
|
2322
|
+
]);
|
|
2323
|
+
if (!result1.ok) {
|
|
2324
|
+
outputError4(result1.error, {}, globalOpts);
|
|
2325
|
+
process.exitCode = 1;
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
if (!result2.ok) {
|
|
2329
|
+
outputError4(result2.error, {}, globalOpts);
|
|
2330
|
+
process.exitCode = 1;
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
const [receipts1, receipts2] = await Promise.all([
|
|
2334
|
+
extractReceipts(result1.value, bundle1Path),
|
|
2335
|
+
extractReceipts(result2.value, bundle2Path)
|
|
2336
|
+
]);
|
|
2337
|
+
const index = /* @__PURE__ */ new Map();
|
|
2338
|
+
for (const r of [...receipts1, ...receipts2]) {
|
|
2339
|
+
const existing = index.get(r.key) ?? [];
|
|
2340
|
+
existing.push(r);
|
|
2341
|
+
index.set(r.key, existing);
|
|
2342
|
+
}
|
|
2343
|
+
const conflicts = [];
|
|
2344
|
+
let mergedCount = 0;
|
|
2345
|
+
const sortedKeys = [...index.keys()].sort();
|
|
2346
|
+
for (const key of sortedKeys) {
|
|
2347
|
+
const records = index.get(key);
|
|
2348
|
+
const fromA = records.filter((r) => r.source === bundle1Path);
|
|
2349
|
+
const fromB = records.filter((r) => r.source === bundle2Path);
|
|
2350
|
+
if (fromA.length > 0 && fromB.length > 0) {
|
|
2351
|
+
const a = fromA[0];
|
|
2352
|
+
const b = fromB[0];
|
|
2353
|
+
if (a.receipt_ref !== b.receipt_ref) {
|
|
2354
|
+
conflicts.push({
|
|
2355
|
+
key,
|
|
2356
|
+
bundle_a_receipt_ref: a.receipt_ref,
|
|
2357
|
+
bundle_b_receipt_ref: b.receipt_ref,
|
|
2358
|
+
diff_fields: findDiffFields(a.jws, b.jws)
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
mergedCount++;
|
|
2362
|
+
} else {
|
|
2363
|
+
mergedCount++;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
const report = {
|
|
2367
|
+
version: "1.0",
|
|
2368
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2369
|
+
bundles: [bundle1Path, bundle2Path],
|
|
2370
|
+
total_receipts: receipts1.length + receipts2.length,
|
|
2371
|
+
merged_receipts: mergedCount,
|
|
2372
|
+
conflicts
|
|
2373
|
+
};
|
|
2374
|
+
if (format === "json" || globalOpts.json) {
|
|
2375
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2376
|
+
} else {
|
|
2377
|
+
console.log(formatReportText2(report));
|
|
2378
|
+
}
|
|
2379
|
+
if (failOnConflict && conflicts.length > 0) {
|
|
2380
|
+
process.exitCode = 1;
|
|
2381
|
+
} else {
|
|
2382
|
+
process.exitCode = 0;
|
|
2383
|
+
}
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
outputError4(err.message, {}, globalOpts);
|
|
2386
|
+
process.exitCode = 1;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
);
|
|
2390
|
+
return reconcile;
|
|
2391
|
+
}
|
|
2112
2392
|
|
|
2113
2393
|
// src/index.ts
|
|
2114
2394
|
var program = new commander.Command();
|
|
@@ -2244,6 +2524,7 @@ program.command("decode").description("Decode a PEAC receipt JWS (without verifi
|
|
|
2244
2524
|
program.addCommand(policy);
|
|
2245
2525
|
program.addCommand(conformance);
|
|
2246
2526
|
program.addCommand(samples);
|
|
2527
|
+
program.addCommand(reconcileCommand());
|
|
2247
2528
|
try {
|
|
2248
2529
|
program.parse();
|
|
2249
2530
|
} catch (err) {
|