@sanning/proof 0.16.0 → 0.17.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 +49 -4
- package/dist/agent-proof.js +50 -23
- package/dist/case-file.d.ts +24 -4
- package/dist/case-file.js +62 -5
- package/dist/case.d.ts +104 -8
- package/dist/case.js +195 -53
- package/dist/cli.d.ts +105 -2
- package/dist/cli.js +664 -123
- package/dist/compose.js +32 -0
- package/dist/evidence.js +108 -38
- package/dist/file-match.d.ts +64 -5
- package/dist/file-match.js +113 -22
- package/dist/grading.d.ts +7 -0
- package/dist/grading.js +14 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/merkle.js +13 -7
- package/dist/verifier.js +24 -19
- package/dist/zip-reader.d.ts +7 -0
- package/dist/zip-reader.js +7 -0
- package/package.json +1 -1
- package/src/agent-proof.ts +53 -27
- package/src/case-file.ts +61 -8
- package/src/case.ts +293 -64
- package/src/cli.ts +703 -132
- package/src/compose.ts +33 -0
- package/src/evidence.ts +109 -43
- package/src/file-match.ts +160 -26
- package/src/grading.ts +15 -2
- package/src/index.ts +6 -0
- package/src/merkle.ts +13 -7
- package/src/verifier.ts +26 -22
- package/src/zip-reader.ts +7 -0
- package/vectors/README.md +5 -1
- package/vectors/case-file-index.json +88 -2
- package/vectors/file-match.json +135 -1
package/dist/case.js
CHANGED
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
*/
|
|
244
244
|
import { DECLARATION_VERSION, DECLARATION_VERSION_V2, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
245
245
|
import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
|
|
246
|
-
import { coSignaturesPresent, verifyEvidenceBundle } from "./evidence.js";
|
|
246
|
+
import { coSignaturesPresent, resolveTraceEvents, verifyEvidenceBundle } from "./evidence.js";
|
|
247
247
|
import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
|
|
248
248
|
import { renderWitnessTime } from "./witness-cache.js";
|
|
249
249
|
// ---- The vocabulary ---------------------------------------------------------
|
|
@@ -717,6 +717,40 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
717
717
|
RECORD_PROFILE_QUARANTINED,
|
|
718
718
|
CASE_HOLDS_NO_PACKS,
|
|
719
719
|
];
|
|
720
|
+
/**
|
|
721
|
+
* The status every record in every pack carries (evidence-bundle 5.5.4
|
|
722
|
+
* v1.35, A43, proof#204), as a closed set. A record is listed for each element
|
|
723
|
+
* of its pack's `body.events` as `packRecords` reads them, or, for a
|
|
724
|
+
* `sanning.evidence.export/v2` pack, of the events of the inline source
|
|
725
|
+
* bundle it wraps (`resolveTraceEvents`), and gets exactly one:
|
|
726
|
+
*
|
|
727
|
+
* - `read_clean`: the record went into the pile, no exception carries its
|
|
728
|
+
* `packIndex` and `recordIndex`, and its pack's declaration check
|
|
729
|
+
* names it by nothing (`declarationFindings` is empty). It says nothing
|
|
730
|
+
* about its PACK: the pack's own grade and exceptions are on the pack, and a
|
|
731
|
+
* surface MUST show that grade beside the status.
|
|
732
|
+
* - `read_flagged`: the record went into the pile, and at least one
|
|
733
|
+
* exception carries its `packIndex` and `recordIndex`, or its pack's
|
|
734
|
+
* declaration check names its `eventId` by a deviation or a content
|
|
735
|
+
* finding (`declarationFindings` is not empty; under an `eventId` the
|
|
736
|
+
* pack carries twice, every record with that id). An exception about the
|
|
737
|
+
* pack alone flags no record.
|
|
738
|
+
* - `not_read`: the record never went into the pile. `reason` is the kind
|
|
739
|
+
* of the exception that says why, on the record
|
|
740
|
+
* (`record_profile_quarantined`, `record_envelope_does_not_verify`,
|
|
741
|
+
* `record_bytes_withheld`, `record_bytes_unreadable`) or on its pack
|
|
742
|
+
* (`pack_key_unreadable`, `pack_does_not_verify`,
|
|
743
|
+
* `pack_body_type_unknown`, `pack_grade_not_verified` where the body
|
|
744
|
+
* could not be read or the verifier graded a different number of records).
|
|
745
|
+
* An export's wrapped records carry the reason of the exception their pack
|
|
746
|
+
* actually got: `pack_body_type_unknown` where its wrapper holds, and
|
|
747
|
+
* otherwise `pack_key_unreadable`, `pack_does_not_verify` or
|
|
748
|
+
* `pack_grade_not_verified`.
|
|
749
|
+
*
|
|
750
|
+
* Before A43 the reading listed only the records it read, so a surface had to
|
|
751
|
+
* work out which records were missing, and why, by a rule of its own.
|
|
752
|
+
*/
|
|
753
|
+
export const CASE_RECORD_STATUSES = ["read_clean", "read_flagged", "not_read"];
|
|
720
754
|
/**
|
|
721
755
|
* What each exception kind contributes to the case verdict. It is a TABLE and
|
|
722
756
|
* not a chain of branches, so "which exceptions move the verdict" is one thing
|
|
@@ -970,6 +1004,20 @@ export async function readCase(packs) {
|
|
|
970
1004
|
// human to read; a join rebuilt by splitting one is a join on a value a
|
|
971
1005
|
// holder can put a separator in.
|
|
972
1006
|
const provenance = [];
|
|
1007
|
+
// Every record of every pack, in pack order and bundle order (A43): where it
|
|
1008
|
+
// went into the pile, its place there (`heldAt`), and where it did not, the
|
|
1009
|
+
// kind of the exception that says why. Nothing is listed only where the pack
|
|
1010
|
+
// body carries no `events` array, and that pack's own exception says why.
|
|
1011
|
+
const slots = [];
|
|
1012
|
+
const unread = (position, packRef, listed, reason) => {
|
|
1013
|
+
listed.forEach((record, recordIndex) => slots.push({ position, packRef, recordIndex, eventId: record.eventId, heldAt: null, reason }));
|
|
1014
|
+
};
|
|
1015
|
+
// The kinds each pack's declaration check names each record by, keyed by the
|
|
1016
|
+
// pack's POSITION and then by `eventId` (A43). The check names a record by
|
|
1017
|
+
// `event_id` alone, so under an id the pack carries twice every record with
|
|
1018
|
+
// that id is named: failing closed, rather than guessing which of the two the
|
|
1019
|
+
// check meant.
|
|
1020
|
+
const declarationFindings = new Map();
|
|
973
1021
|
// Keyed by the pack's POSITION and never by its `ref`. `ref` is the reader's
|
|
974
1022
|
// own name for a pack, and nothing stops two packs in one archive carrying
|
|
975
1023
|
// the same one: keyed by ref, the second pack's claims silently replaced the
|
|
@@ -1020,9 +1068,11 @@ export async function readCase(packs) {
|
|
|
1020
1068
|
// pack whose signer is UNKNOWN, and not a pack nobody signed. Checked
|
|
1021
1069
|
// BEFORE verification, because the two bundle verifiers disagree on some
|
|
1022
1070
|
// of these spellings (proof#135) and the reading must not.
|
|
1023
|
-
note(PACK_KEY_UNREADABLE, { packRef: pack.ref });
|
|
1071
|
+
note(PACK_KEY_UNREADABLE, { packRef: pack.ref, packIndex: position });
|
|
1024
1072
|
unattributed += 1;
|
|
1073
|
+
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_KEY_UNREADABLE);
|
|
1025
1074
|
packReadings.push({
|
|
1075
|
+
index: position,
|
|
1026
1076
|
ref: pack.ref,
|
|
1027
1077
|
signingKey: null,
|
|
1028
1078
|
grade: null,
|
|
@@ -1049,9 +1099,11 @@ export async function readCase(packs) {
|
|
|
1049
1099
|
: verified;
|
|
1050
1100
|
// WHOSE PACK IS IT. The wrapper, and only the wrapper.
|
|
1051
1101
|
if (!wrapperHolds(verified)) {
|
|
1052
|
-
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref });
|
|
1102
|
+
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref, packIndex: position });
|
|
1053
1103
|
unattributed += 1;
|
|
1104
|
+
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_DOES_NOT_VERIFY);
|
|
1054
1105
|
packReadings.push({
|
|
1106
|
+
index: position,
|
|
1055
1107
|
ref: pack.ref,
|
|
1056
1108
|
signingKey: holder,
|
|
1057
1109
|
grade: null,
|
|
@@ -1067,7 +1119,7 @@ export async function readCase(packs) {
|
|
|
1067
1119
|
// the case reading names it rather than moving anything on its account.
|
|
1068
1120
|
caseNotes.push({
|
|
1069
1121
|
position,
|
|
1070
|
-
note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref },
|
|
1122
|
+
note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref, packIndex: position },
|
|
1071
1123
|
});
|
|
1072
1124
|
}
|
|
1073
1125
|
const bodyType = bodyTypeOf(pack.bundle);
|
|
@@ -1131,6 +1183,7 @@ export async function readCase(packs) {
|
|
|
1131
1183
|
}
|
|
1132
1184
|
note(PACK_GRADE_NOT_VERIFIED, {
|
|
1133
1185
|
packRef: pack.ref,
|
|
1186
|
+
packIndex: position,
|
|
1134
1187
|
status: grade,
|
|
1135
1188
|
causes,
|
|
1136
1189
|
reasons,
|
|
@@ -1139,11 +1192,13 @@ export async function readCase(packs) {
|
|
|
1139
1192
|
if (verified.status === "failed" && signed.status !== "failed") {
|
|
1140
1193
|
note(PACK_BUNDLE_CONTENT_DEPENDENT, {
|
|
1141
1194
|
packRef: pack.ref,
|
|
1195
|
+
packIndex: position,
|
|
1142
1196
|
reasons: onlyIn(bundleReasons(verified), bundleReasons(signed)),
|
|
1143
1197
|
});
|
|
1144
1198
|
}
|
|
1145
1199
|
const recordsWithheld = full !== null ? full.recordsWithheld : 0;
|
|
1146
1200
|
packReadings.push({
|
|
1201
|
+
index: position,
|
|
1147
1202
|
ref: pack.ref,
|
|
1148
1203
|
signingKey: holder,
|
|
1149
1204
|
grade,
|
|
@@ -1156,13 +1211,20 @@ export async function readCase(packs) {
|
|
|
1156
1211
|
// them, so a clean verdict never hides them.
|
|
1157
1212
|
caseNotes.push({
|
|
1158
1213
|
position,
|
|
1159
|
-
note: {
|
|
1214
|
+
note: {
|
|
1215
|
+
kind: CONTENT_WITHHELD_BY_DECLARATION,
|
|
1216
|
+
packRef: pack.ref,
|
|
1217
|
+
packIndex: position,
|
|
1218
|
+
count: recordsWithheld,
|
|
1219
|
+
},
|
|
1160
1220
|
});
|
|
1161
1221
|
}
|
|
1162
1222
|
// The body is one the verifier could not read, so it graded no record, and
|
|
1163
1223
|
// nothing below has a grading to read. Named above.
|
|
1164
|
-
if (signed.status === "malformed")
|
|
1224
|
+
if (signed.status === "malformed") {
|
|
1225
|
+
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_GRADE_NOT_VERIFIED);
|
|
1165
1226
|
continue;
|
|
1227
|
+
}
|
|
1166
1228
|
if (!knownBody) {
|
|
1167
1229
|
// `sanning.evidence.export/v2` lands here, and it is NOT unwrapped: its
|
|
1168
1230
|
// records sit under `body.source_bundle`, and reading them is a change to
|
|
@@ -1170,20 +1232,35 @@ export async function readCase(packs) {
|
|
|
1170
1232
|
// a repair (evidence-bundle.md 5.5.1 step 1 says so too). What this exit
|
|
1171
1233
|
// costs is that pack's RECORDS, and it is named every time. It no longer
|
|
1172
1234
|
// costs the pack's KEY: that was read above.
|
|
1173
|
-
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref });
|
|
1235
|
+
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref, packIndex: position });
|
|
1236
|
+
// Listed, and not read: the records a `sanning.evidence.export/v2` wraps
|
|
1237
|
+
// are listed from its inline source bundle, so a reading never says "0
|
|
1238
|
+
// not read" of a pack that holds records (A43).
|
|
1239
|
+
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_BODY_TYPE_UNKNOWN);
|
|
1174
1240
|
continue;
|
|
1175
1241
|
}
|
|
1176
1242
|
// Graded above (`malformed` where the verifier called it `verified`), and
|
|
1177
1243
|
// none of its records are read.
|
|
1178
|
-
if (ungraded)
|
|
1244
|
+
if (ungraded) {
|
|
1245
|
+
unread(position, pack.ref, records, PACK_GRADE_NOT_VERIFIED);
|
|
1179
1246
|
continue;
|
|
1247
|
+
}
|
|
1180
1248
|
if (records.length === 0) {
|
|
1181
|
-
note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref });
|
|
1249
|
+
note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref, packIndex: position });
|
|
1182
1250
|
continue;
|
|
1183
1251
|
}
|
|
1184
1252
|
const checked = check;
|
|
1185
1253
|
claimsByPack.set(position, declarationClaims(pack.bundle));
|
|
1186
1254
|
rolesByPack.set(position, new Map((checked.roles ?? []).map((r) => [r.eventId, r.role])));
|
|
1255
|
+
const namedByCheck = new Map();
|
|
1256
|
+
for (const finding of [...checked.deviations, ...checked.contentFindings]) {
|
|
1257
|
+
if (finding.eventId === undefined)
|
|
1258
|
+
continue;
|
|
1259
|
+
const kinds = namedByCheck.get(finding.eventId) ?? [];
|
|
1260
|
+
kinds.push(finding.kind);
|
|
1261
|
+
namedByCheck.set(finding.eventId, kinds);
|
|
1262
|
+
}
|
|
1263
|
+
declarationFindings.set(position, namedByCheck);
|
|
1187
1264
|
if (checked.contentFindings.length > 0) {
|
|
1188
1265
|
// BESIDE the deviation exception and never instead of it: a pack can do
|
|
1189
1266
|
// both, over different records, and the two say different things. This
|
|
@@ -1191,12 +1268,14 @@ export async function readCase(packs) {
|
|
|
1191
1268
|
// it turns on (proof#147).
|
|
1192
1269
|
note(PACK_DECLARATION_CONTENT_DEPENDENT, {
|
|
1193
1270
|
packRef: pack.ref,
|
|
1271
|
+
packIndex: position,
|
|
1194
1272
|
count: checked.contentFindings.length,
|
|
1195
1273
|
});
|
|
1196
1274
|
}
|
|
1197
1275
|
if (checked.verdict === "failed") {
|
|
1198
1276
|
note(PACK_DECLARATION_DEVIATES, {
|
|
1199
1277
|
packRef: pack.ref,
|
|
1278
|
+
packIndex: position,
|
|
1200
1279
|
count: checked.deviations.length,
|
|
1201
1280
|
});
|
|
1202
1281
|
}
|
|
@@ -1219,7 +1298,7 @@ export async function readCase(packs) {
|
|
|
1219
1298
|
// cannot read", which `gradePack` already carries to `incomplete`. The
|
|
1220
1299
|
// same posture here, for the same reason: the field has no accept-set and
|
|
1221
1300
|
// the holder writes it.
|
|
1222
|
-
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref });
|
|
1301
|
+
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref, packIndex: position });
|
|
1223
1302
|
}
|
|
1224
1303
|
else {
|
|
1225
1304
|
// WHAT IT CONTRIBUTES TURNS ON THE WHOLE READING (5.5.1 step 2 v1.20,
|
|
@@ -1242,30 +1321,34 @@ export async function readCase(packs) {
|
|
|
1242
1321
|
const record = records[index];
|
|
1243
1322
|
const graded = verified.events[index];
|
|
1244
1323
|
const bare = signed.events[index];
|
|
1324
|
+
// Which pack and which record, by position: the two names that are
|
|
1325
|
+
// unique in the reading whatever the reader called the pack and whatever
|
|
1326
|
+
// the holder wrote as `event_id` (A43).
|
|
1327
|
+
const at = {
|
|
1328
|
+
packRef: pack.ref,
|
|
1329
|
+
packIndex: position,
|
|
1330
|
+
eventId: record.eventId,
|
|
1331
|
+
recordIndex: index,
|
|
1332
|
+
};
|
|
1333
|
+
// Not read: listed with the kind of the exception that says why.
|
|
1334
|
+
const skip = (kind, fields = {}) => {
|
|
1335
|
+
note(kind, { ...at, ...fields });
|
|
1336
|
+
slots.push({ position, packRef: pack.ref, recordIndex: index, eventId: record.eventId, heldAt: null, reason: kind });
|
|
1337
|
+
};
|
|
1245
1338
|
if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
|
|
1246
|
-
|
|
1339
|
+
skip(RECORD_PROFILE_QUARANTINED);
|
|
1247
1340
|
continue;
|
|
1248
1341
|
}
|
|
1249
1342
|
if (!graded.envelopeOk) {
|
|
1250
|
-
|
|
1251
|
-
packRef: pack.ref,
|
|
1252
|
-
eventId: record.eventId,
|
|
1253
|
-
reasons: [...graded.errors],
|
|
1254
|
-
});
|
|
1343
|
+
skip(RECORD_ENVELOPE_DOES_NOT_VERIFY, { reasons: [...graded.errors] });
|
|
1255
1344
|
continue;
|
|
1256
1345
|
}
|
|
1257
1346
|
if (!record.bytesSupplied) {
|
|
1258
|
-
|
|
1259
|
-
packRef: pack.ref,
|
|
1260
|
-
eventId: record.eventId,
|
|
1261
|
-
});
|
|
1347
|
+
skip(RECORD_BYTES_WITHHELD);
|
|
1262
1348
|
continue;
|
|
1263
1349
|
}
|
|
1264
1350
|
if (record.record === null) {
|
|
1265
|
-
|
|
1266
|
-
packRef: pack.ref,
|
|
1267
|
-
eventId: record.eventId,
|
|
1268
|
-
});
|
|
1351
|
+
skip(RECORD_BYTES_UNREADABLE);
|
|
1269
1352
|
continue;
|
|
1270
1353
|
}
|
|
1271
1354
|
// Named, and the record stays in the pile (see the kinds). A mismatch
|
|
@@ -1273,15 +1356,13 @@ export async function readCase(packs) {
|
|
|
1273
1356
|
// with the side map is `incomplete` (proof#159).
|
|
1274
1357
|
if (bare.contentOk === false) {
|
|
1275
1358
|
note(RECORD_CONTENT_MISMATCH, {
|
|
1276
|
-
|
|
1277
|
-
eventId: record.eventId,
|
|
1359
|
+
...at,
|
|
1278
1360
|
reasons: [...bare.errors],
|
|
1279
1361
|
});
|
|
1280
1362
|
}
|
|
1281
1363
|
else if (graded.contentOk === false) {
|
|
1282
1364
|
note(RECORD_CONTENT_DEPENDENT, {
|
|
1283
|
-
|
|
1284
|
-
eventId: record.eventId,
|
|
1365
|
+
...at,
|
|
1285
1366
|
reasons: onlyIn(graded.errors, bare.errors),
|
|
1286
1367
|
});
|
|
1287
1368
|
}
|
|
@@ -1290,17 +1371,24 @@ export async function readCase(packs) {
|
|
|
1290
1371
|
if (!(bare.inclusionOk && bare.checkpointBound) ||
|
|
1291
1372
|
(!bare.ok && bare.contentOk !== false)) {
|
|
1292
1373
|
note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
|
|
1293
|
-
|
|
1294
|
-
eventId: record.eventId,
|
|
1374
|
+
...at,
|
|
1295
1375
|
reasons: [...bare.errors],
|
|
1296
1376
|
});
|
|
1297
1377
|
}
|
|
1378
|
+
slots.push({
|
|
1379
|
+
position,
|
|
1380
|
+
packRef: pack.ref,
|
|
1381
|
+
recordIndex: index,
|
|
1382
|
+
eventId: record.eventId,
|
|
1383
|
+
heldAt: held.length,
|
|
1384
|
+
reason: null,
|
|
1385
|
+
});
|
|
1298
1386
|
held.push({
|
|
1299
1387
|
publicKey: record.publicKey,
|
|
1300
1388
|
record: record.record,
|
|
1301
1389
|
ref: `${pack.ref}/${record.eventId}`,
|
|
1302
1390
|
});
|
|
1303
|
-
provenance.push({ position, packRef: pack.ref, eventId: record.eventId });
|
|
1391
|
+
provenance.push({ position, packRef: pack.ref, eventId: record.eventId, recordIndex: index });
|
|
1304
1392
|
}
|
|
1305
1393
|
}
|
|
1306
1394
|
// Step 3. ONE pile, no partition. This is the line anchor#198 turns on and
|
|
@@ -1309,20 +1397,12 @@ export async function readCase(packs) {
|
|
|
1309
1397
|
// read exactly as a pair inside one.
|
|
1310
1398
|
const readings = readHandoffs(held);
|
|
1311
1399
|
// Step 4.
|
|
1312
|
-
const records = [];
|
|
1313
1400
|
let handoffRecords = 0;
|
|
1314
1401
|
for (let index = 0; index < readings.length; index += 1) {
|
|
1315
1402
|
const reading = readings[index];
|
|
1316
|
-
const { position, packRef, eventId } = provenance[index];
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
eventId,
|
|
1320
|
-
verdict: reading.verdict,
|
|
1321
|
-
findings: [...reading.findings],
|
|
1322
|
-
failedChecks: [...reading.failedChecks],
|
|
1323
|
-
counterparts: [...reading.candidates],
|
|
1324
|
-
role: rolesByPack.get(position)?.get(eventId) ?? null,
|
|
1325
|
-
});
|
|
1403
|
+
const { position, packRef, eventId, recordIndex } = provenance[index];
|
|
1404
|
+
// The record this reading is about, by position (A43).
|
|
1405
|
+
const at = { packRef, packIndex: position, eventId, recordIndex };
|
|
1326
1406
|
// proof#137's door, named. Read from the ONE reader of the declaration
|
|
1327
1407
|
// (`declarationClaims`), never from a second parse here. Only a READABLE
|
|
1328
1408
|
// declaration can make a record unclaimed: where the claim itself is absent
|
|
@@ -1343,7 +1423,7 @@ export async function readCase(packs) {
|
|
|
1343
1423
|
claims.readable &&
|
|
1344
1424
|
claims.version === DECLARATION_VERSION &&
|
|
1345
1425
|
!claims.claimedSides.includes(eventId)) {
|
|
1346
|
-
note(RECORD_UNCLAIMED,
|
|
1426
|
+
note(RECORD_UNCLAIMED, at);
|
|
1347
1427
|
}
|
|
1348
1428
|
if (reading.findings.includes(NOT_A_HANDOFF_FINDING)) {
|
|
1349
1429
|
// 4.1 (v1.9, A14): information, not absence. A record 4.1 does not govern
|
|
@@ -1360,10 +1440,10 @@ export async function readCase(packs) {
|
|
|
1360
1440
|
// counterpart, ambiguity and `failedChecks` reporting below all keep
|
|
1361
1441
|
// running over it.
|
|
1362
1442
|
if (recordIsMalformed(held[index])) {
|
|
1363
|
-
note(RECORD_MALFORMED,
|
|
1443
|
+
note(RECORD_MALFORMED, at);
|
|
1364
1444
|
continue;
|
|
1365
1445
|
}
|
|
1366
|
-
note(COUNTERPART_MALFORMED,
|
|
1446
|
+
note(COUNTERPART_MALFORMED, at);
|
|
1367
1447
|
}
|
|
1368
1448
|
if (reading.findings.includes(COUNTERPART_ABSENT)) {
|
|
1369
1449
|
// `held`, `provenance` and `readings` are three views of one list in one
|
|
@@ -1402,14 +1482,13 @@ export async function readCase(packs) {
|
|
|
1402
1482
|
? COUNTERPART_ABSENT_SIGNER_UNKNOWN
|
|
1403
1483
|
: COUNTERPART_ABSENT;
|
|
1404
1484
|
note(kind, setAside > 0
|
|
1405
|
-
? {
|
|
1406
|
-
: {
|
|
1485
|
+
? { ...at, counterpartyKey: named, count: setAside }
|
|
1486
|
+
: { ...at, counterpartyKey: named });
|
|
1407
1487
|
continue;
|
|
1408
1488
|
}
|
|
1409
1489
|
if (reading.findings.includes(COUNTERPART_AMBIGUOUS)) {
|
|
1410
1490
|
note(COUNTERPART_AMBIGUOUS, {
|
|
1411
|
-
|
|
1412
|
-
eventId,
|
|
1491
|
+
...at,
|
|
1413
1492
|
count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
|
|
1414
1493
|
});
|
|
1415
1494
|
}
|
|
@@ -1422,8 +1501,7 @@ export async function readCase(packs) {
|
|
|
1422
1501
|
// check whatever the aggregate word is.
|
|
1423
1502
|
if (reading.failedChecks.length > 0) {
|
|
1424
1503
|
note(PAIR_FAILED, {
|
|
1425
|
-
|
|
1426
|
-
eventId,
|
|
1504
|
+
...at,
|
|
1427
1505
|
failedChecks: [...reading.failedChecks],
|
|
1428
1506
|
});
|
|
1429
1507
|
}
|
|
@@ -1438,11 +1516,18 @@ export async function readCase(packs) {
|
|
|
1438
1516
|
// none.
|
|
1439
1517
|
for (const slot of undeclaredSlots) {
|
|
1440
1518
|
if (handoffRecords > 0) {
|
|
1441
|
-
exceptions[slot.exceptionAt] = {
|
|
1519
|
+
exceptions[slot.exceptionAt] = {
|
|
1520
|
+
kind: PACK_UNDECLARED,
|
|
1521
|
+
packRef: slot.ref,
|
|
1522
|
+
packIndex: slot.position,
|
|
1523
|
+
};
|
|
1442
1524
|
contributions[slot.contributionAt] = [slot.ref, EXCEPTION_VERDICT[PACK_UNDECLARED]];
|
|
1443
1525
|
}
|
|
1444
1526
|
else {
|
|
1445
|
-
caseNotes.push({
|
|
1527
|
+
caseNotes.push({
|
|
1528
|
+
position: slot.position,
|
|
1529
|
+
note: { kind: PACK_UNDECLARED, packRef: slot.ref, packIndex: slot.position },
|
|
1530
|
+
});
|
|
1446
1531
|
}
|
|
1447
1532
|
}
|
|
1448
1533
|
const named = exceptions.filter((e) => e !== null);
|
|
@@ -1453,6 +1538,50 @@ export async function readCase(packs) {
|
|
|
1453
1538
|
.map((n, i) => ({ ...n, i }))
|
|
1454
1539
|
.sort((a, b) => a.position - b.position || a.i - b.i)
|
|
1455
1540
|
.map((n) => n.note);
|
|
1541
|
+
// Every record of every pack, with its status (A43). A record that went
|
|
1542
|
+
// into the pile is `read_flagged` where any exception names it by pack and
|
|
1543
|
+
// record position, or its pack's declaration check names it (by deviation
|
|
1544
|
+
// or by content finding), and `read_clean` where neither does; every other
|
|
1545
|
+
// record is `not_read`, with the kind of the exception that says why.
|
|
1546
|
+
const flagged = new Set(named
|
|
1547
|
+
.filter((e) => e.packIndex !== undefined && e.recordIndex !== undefined)
|
|
1548
|
+
.map((e) => `${e.packIndex}:${e.recordIndex}`));
|
|
1549
|
+
const records = slots.map((slot) => {
|
|
1550
|
+
const base = {
|
|
1551
|
+
packRef: slot.packRef,
|
|
1552
|
+
packIndex: slot.position,
|
|
1553
|
+
eventId: slot.eventId,
|
|
1554
|
+
recordIndex: slot.recordIndex,
|
|
1555
|
+
};
|
|
1556
|
+
const byCheck = [...(declarationFindings.get(slot.position)?.get(slot.eventId) ?? [])];
|
|
1557
|
+
if (slot.heldAt === null) {
|
|
1558
|
+
return {
|
|
1559
|
+
...base,
|
|
1560
|
+
status: "not_read",
|
|
1561
|
+
reason: slot.reason,
|
|
1562
|
+
declarationFindings: byCheck,
|
|
1563
|
+
verdict: null,
|
|
1564
|
+
findings: [],
|
|
1565
|
+
failedChecks: [],
|
|
1566
|
+
counterparts: [],
|
|
1567
|
+
role: null,
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const reading = readings[slot.heldAt];
|
|
1571
|
+
return {
|
|
1572
|
+
...base,
|
|
1573
|
+
status: flagged.has(`${slot.position}:${slot.recordIndex}`) || byCheck.length > 0
|
|
1574
|
+
? "read_flagged"
|
|
1575
|
+
: "read_clean",
|
|
1576
|
+
reason: null,
|
|
1577
|
+
declarationFindings: byCheck,
|
|
1578
|
+
verdict: reading.verdict,
|
|
1579
|
+
findings: [...reading.findings],
|
|
1580
|
+
failedChecks: [...reading.failedChecks],
|
|
1581
|
+
counterparts: [...reading.candidates],
|
|
1582
|
+
role: rolesByPack.get(slot.position)?.get(slot.eventId) ?? null,
|
|
1583
|
+
};
|
|
1584
|
+
});
|
|
1456
1585
|
return {
|
|
1457
1586
|
verdict: verdictOf(named, contributed),
|
|
1458
1587
|
exceptions: named,
|
|
@@ -1464,6 +1593,19 @@ export async function readCase(packs) {
|
|
|
1464
1593
|
packs: packReadings,
|
|
1465
1594
|
};
|
|
1466
1595
|
}
|
|
1596
|
+
/**
|
|
1597
|
+
* The records a pack LISTS where this reader does not read them one by one
|
|
1598
|
+
* (A43): the events `resolveTraceEvents` presents, which unwraps a
|
|
1599
|
+
* `sanning.evidence.export/v2` to its inline source bundle, and otherwise
|
|
1600
|
+
* `body.events` as `packRecords` reads it. Listing is not reading: every record
|
|
1601
|
+
* listed here is `not_read`.
|
|
1602
|
+
*/
|
|
1603
|
+
function listedRecords(bundle, content) {
|
|
1604
|
+
const events = resolveTraceEvents(bundle);
|
|
1605
|
+
if (events === undefined)
|
|
1606
|
+
return packRecords(bundle, content);
|
|
1607
|
+
return packRecords({ body: { events } }, content);
|
|
1608
|
+
}
|
|
1467
1609
|
/**
|
|
1468
1610
|
* `body.declaration.declaration_version` where it is one this reader knows,
|
|
1469
1611
|
* and `null` otherwise.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import type { OfferedFile, SkippedFile } from "./file-match.js";
|
|
2
4
|
interface Cli {
|
|
3
5
|
out: (s: string) => void;
|
|
4
6
|
err: (s: string) => void;
|
|
@@ -9,6 +11,30 @@ interface Cli {
|
|
|
9
11
|
* backslash included, so a line with none is printed as it was. */
|
|
10
12
|
export declare function escapeControl(text: string): string;
|
|
11
13
|
export declare function runCli(argv: string[], stream: Cli): Promise<number>;
|
|
14
|
+
export declare const MAX_FOLDER_BYTES: number;
|
|
15
|
+
export declare const TOO_LARGE = "too_large";
|
|
16
|
+
export declare function hashFile(path: string, follow: boolean, budget?: {
|
|
17
|
+
n: number;
|
|
18
|
+
}): Promise<string | null>;
|
|
19
|
+
export declare function readOfferedFiles(paths: readonly string[], cap?: number): Promise<{
|
|
20
|
+
offered: OfferedFile[];
|
|
21
|
+
skipped: SkippedFile[];
|
|
22
|
+
} | string>;
|
|
23
|
+
export interface LoadedLogs {
|
|
24
|
+
content: Map<string, Uint8Array | string>;
|
|
25
|
+
suppliedIds: string[];
|
|
26
|
+
notes: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare const MAX_LOGS_BYTES: number;
|
|
29
|
+
export declare class LogsTooLarge extends Error {
|
|
30
|
+
}
|
|
31
|
+
export declare function readLogFile(path: string, budget: {
|
|
32
|
+
n: number;
|
|
33
|
+
}, opener?: typeof open): Promise<Uint8Array>;
|
|
34
|
+
export declare function loadLogsDirectory(dirPath: string, io: Cli, limits?: {
|
|
35
|
+
bytes: number;
|
|
36
|
+
entries: number;
|
|
37
|
+
}): Promise<LoadedLogs | undefined>;
|
|
12
38
|
/** The error text for an input with no pack in it, word for word the page's. */
|
|
13
39
|
export declare const NO_BUNDLE = "No bundle.json found. An evidence pack contains bundle.json plus logs/.";
|
|
14
40
|
/** A directory nested deeper than this below an argument refuses the input. */
|
|
@@ -29,6 +55,14 @@ export declare class CaseInputError extends Error {
|
|
|
29
55
|
readonly hint: string | null;
|
|
30
56
|
constructor(message: string, hint?: string | null);
|
|
31
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A file `case` reads whole that does not fit in what is left of the run's
|
|
60
|
+
* 1 GiB. Always a `CaseInputError`; the type lets a caller that can carry on
|
|
61
|
+
* without the file (`contents.json`, `witness-cache.json`) tell this refusal
|
|
62
|
+
* from an unreadable file.
|
|
63
|
+
*/
|
|
64
|
+
export declare class WholeReadRefused extends CaseInputError {
|
|
65
|
+
}
|
|
32
66
|
/**
|
|
33
67
|
* `bundle.json` bytes that `readBundleBytes` will not read. `tooDeep` separates
|
|
34
68
|
* the one refusal that is not a parse error; `detail` is the parser's own words,
|
|
@@ -66,7 +100,7 @@ export interface SplitPack {
|
|
|
66
100
|
}
|
|
67
101
|
export interface SplitCase {
|
|
68
102
|
packs: SplitPack[];
|
|
69
|
-
/** Paths that belong to no pack, sorted. Never
|
|
103
|
+
/** Paths that belong to no pack, sorted. Never read as part of a pack; a regular one is hashed, and nothing else is read of it (A41). */
|
|
70
104
|
outside: string[];
|
|
71
105
|
/** The part of `outside` that is not a regular file. Never followed, never opened. */
|
|
72
106
|
notRegular: string[];
|
|
@@ -82,6 +116,13 @@ export interface TreeEntry {
|
|
|
82
116
|
regular: boolean;
|
|
83
117
|
/** The bytes of an entry read out of a case-file `.zip`. */
|
|
84
118
|
data?: Uint8Array | null;
|
|
119
|
+
/**
|
|
120
|
+
* `true` for a symbolic link, on disk or an entry of a `.zip`: the one reason
|
|
121
|
+
* a link is skipped under, here and under `--files`.
|
|
122
|
+
*/
|
|
123
|
+
link?: boolean;
|
|
124
|
+
/** The `.zip` reader's own words for an entry it refused (not a link). */
|
|
125
|
+
refused?: string;
|
|
85
126
|
}
|
|
86
127
|
/**
|
|
87
128
|
* The page's `splitCase` followed by its `assemble` per pack, over the walk's
|
|
@@ -89,14 +130,53 @@ export interface TreeEntry {
|
|
|
89
130
|
* nothing else.
|
|
90
131
|
*/
|
|
91
132
|
export declare function splitCase(tree: Map<string, TreeEntry>, read: (entry: TreeEntry, key: string) => Promise<Uint8Array>): Promise<SplitCase>;
|
|
133
|
+
/**
|
|
134
|
+
* The bytes of the `witness-cache.json` beside a pack's `bundle.json`
|
|
135
|
+
* (evidence-bundle 5.6.6, v1.25, A30), or `null`.
|
|
136
|
+
*
|
|
137
|
+
* The split rule is not changed for it: the file stays outside every pack,
|
|
138
|
+
* listed as such, and is read here, for its contents, and hashed with the other
|
|
139
|
+
* outside files (A41). It is read as a cache only where it is a regular file the split left outside every pack, so a file under another
|
|
140
|
+
* pack's `logs/` is that pack's, and a link is never followed. A file that
|
|
141
|
+
* cannot be read is no cache, never a refusal of the drop. One the run's
|
|
142
|
+
* whole-read total has no room for is pushed onto `refused` when it is given,
|
|
143
|
+
* so the output says so.
|
|
144
|
+
*/
|
|
145
|
+
export declare function witnessCacheBytes(tree: Map<string, TreeEntry>, split: SplitCase, pack: SplitPack, budget?: {
|
|
146
|
+
n: number;
|
|
147
|
+
}, refused?: string[]): Promise<Uint8Array | null>;
|
|
92
148
|
/** `verify`'s reader, with the refusal named by the pack's path. */
|
|
93
149
|
export declare function loadBundle(pack: SplitPack): unknown;
|
|
150
|
+
/**
|
|
151
|
+
* The refusal for a file `case` would hold whole that does not fit in what is
|
|
152
|
+
* left of the 1 GiB a run reads whole. The same words in both kernels.
|
|
153
|
+
*/
|
|
154
|
+
export declare const wholeReadRefusal: (key: string) => string;
|
|
155
|
+
/**
|
|
156
|
+
* Why `contents.json` reads as an unreadable index when the run's whole-read
|
|
157
|
+
* total has no room for it, and when it cannot be read. The same words in both
|
|
158
|
+
* kernels.
|
|
159
|
+
*/
|
|
160
|
+
export declare const INDEX_OVER_BUDGET = "it would take case past the 1 GiB it reads whole in one run, so it was not read";
|
|
161
|
+
export declare const INDEX_NOT_READ = "it could not be read";
|
|
162
|
+
/** The note for a `witness-cache.json` the whole-read total had no room for. */
|
|
163
|
+
export declare const witnessCacheRefusedNote: (path: string) => string;
|
|
164
|
+
/** The refusal for an archive file larger than MAX_ZIP_ARCHIVE_BYTES. */
|
|
165
|
+
export declare const zipFileRefusal: (arg: string) => string;
|
|
94
166
|
/**
|
|
95
167
|
* A regular file's bytes. Opened without following a link and without
|
|
96
168
|
* blocking, then checked again, so a file swapped for a link or a pipe after
|
|
97
169
|
* the walk listed it is refused rather than followed or waited on.
|
|
170
|
+
*
|
|
171
|
+
* `budget` is the bytes a run may still hold whole from files on disk
|
|
172
|
+
* (MAX_FOLDER_BYTES at the start). A file larger than what is left, or that
|
|
173
|
+
* grows past it while it is read, is refused before it is held, so a file of a
|
|
174
|
+
* terabyte under a pack's own names cannot exhaust memory. A `.zip` entry is
|
|
175
|
+
* already in memory, inside the archive's own cap, and is not charged.
|
|
98
176
|
*/
|
|
99
|
-
export declare function readFileEntry(entry: TreeEntry, key: string
|
|
177
|
+
export declare function readFileEntry(entry: TreeEntry, key: string, budget?: {
|
|
178
|
+
n: number;
|
|
179
|
+
}): Promise<Uint8Array>;
|
|
100
180
|
/**
|
|
101
181
|
* Every name under the directories named, keyed `<name>/<path inside>`. Each
|
|
102
182
|
* argument must be a directory, and not a link to one. A `.zip` is refused
|
|
@@ -110,6 +190,25 @@ export declare const CASE_USAGE: string;
|
|
|
110
190
|
* on-chain half that could not be settled is never a clean 0.
|
|
111
191
|
*/
|
|
112
192
|
export declare function caseExitCode(verdict: string, onChainVerdict: string): number;
|
|
193
|
+
export interface CaseFileData {
|
|
194
|
+
root: string;
|
|
195
|
+
index: string;
|
|
196
|
+
case_id: string | null;
|
|
197
|
+
findings: {
|
|
198
|
+
rendered: string;
|
|
199
|
+
}[];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The index comparison, once per argument (5.6.3): each folder or case file
|
|
203
|
+
* whose root holds `contents.json`. Every file under `packs/` and `report/` is
|
|
204
|
+
* hashed for it, in pieces, as bytes, and nothing is parsed but the index. The
|
|
205
|
+
* files share one total of 1 GiB across the run, charged in path order; a file
|
|
206
|
+
* larger than what is left is reported as too large to hash, never as a match
|
|
207
|
+
* and never as absent. It is never an input to the reading.
|
|
208
|
+
*/
|
|
209
|
+
export declare function caseFileReadings(tree: Map<string, TreeEntry>, whole?: {
|
|
210
|
+
n: number;
|
|
211
|
+
}, cap?: number): Promise<CaseFileData[]>;
|
|
113
212
|
/**
|
|
114
213
|
* `proof case <folder>... [--gateways <urls>] [--json]`. Every path out of the
|
|
115
214
|
* command is an exit code from the table: an exception nobody expected exits 2,
|
|
@@ -117,4 +216,8 @@ export declare function caseExitCode(verdict: string, onChainVerdict: string): n
|
|
|
117
216
|
* command.
|
|
118
217
|
*/
|
|
119
218
|
export declare function runCase(argv: readonly string[], stream: Cli): Promise<number>;
|
|
219
|
+
export declare function looseFiles(tree: Map<string, TreeEntry>, split: SplitCase, cap?: number): Promise<{
|
|
220
|
+
offered: OfferedFile[];
|
|
221
|
+
skipped: SkippedFile[];
|
|
222
|
+
}>;
|
|
120
223
|
export {};
|