@sanning/proof 0.15.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/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: { kind: CONTENT_WITHHELD_BY_DECLARATION, packRef: pack.ref, count: recordsWithheld },
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
- note(RECORD_PROFILE_QUARANTINED, { packRef: pack.ref, eventId: record.eventId });
1339
+ skip(RECORD_PROFILE_QUARANTINED);
1247
1340
  continue;
1248
1341
  }
1249
1342
  if (!graded.envelopeOk) {
1250
- note(RECORD_ENVELOPE_DOES_NOT_VERIFY, {
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
- note(RECORD_BYTES_WITHHELD, {
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
- note(RECORD_BYTES_UNREADABLE, {
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
- packRef: pack.ref,
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
- packRef: pack.ref,
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
- packRef: pack.ref,
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
- records.push({
1318
- packRef,
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, { packRef, eventId });
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, { packRef, eventId });
1443
+ note(RECORD_MALFORMED, at);
1364
1444
  continue;
1365
1445
  }
1366
- note(COUNTERPART_MALFORMED, { packRef, eventId });
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
- ? { packRef, eventId, counterpartyKey: named, count: setAside }
1406
- : { packRef, eventId, counterpartyKey: named });
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
- packRef,
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
- packRef,
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] = { kind: PACK_UNDECLARED, packRef: slot.ref };
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({ position: slot.position, note: { kind: PACK_UNDECLARED, packRef: slot.ref } });
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 opened. */
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): Promise<Uint8Array>;
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 {};