@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/compose.js
CHANGED
|
@@ -51,6 +51,27 @@ function normalizeAttestation(rec) {
|
|
|
51
51
|
cloned.signature = attestationSignatureToHex(cloned.signature);
|
|
52
52
|
return cloned;
|
|
53
53
|
}
|
|
54
|
+
// True when `value` holds a number that is NaN or +-Infinity. Iterative, so a
|
|
55
|
+
// deeply nested input cannot overflow the stack before `jcs` bounds it.
|
|
56
|
+
function hasNonFiniteNumber(value) {
|
|
57
|
+
const stack = [value];
|
|
58
|
+
while (stack.length > 0) {
|
|
59
|
+
const v = stack.pop();
|
|
60
|
+
if (typeof v === "number") {
|
|
61
|
+
if (!Number.isFinite(v))
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
else if (Array.isArray(v)) {
|
|
65
|
+
for (const x of v)
|
|
66
|
+
stack.push(x);
|
|
67
|
+
}
|
|
68
|
+
else if (v !== null && typeof v === "object") {
|
|
69
|
+
for (const x of Object.values(v))
|
|
70
|
+
stack.push(x);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
54
75
|
// Compose an attested evidence export (evidence-export.md §5).
|
|
55
76
|
//
|
|
56
77
|
// 1. Normalize + transcode the attestation records (boundary hex, above).
|
|
@@ -73,6 +94,17 @@ export async function composeExport(sourceBundle, attestations, exporterKey, opt
|
|
|
73
94
|
if (typeof exporterKey?.privateKey !== "string" || exporterKey.privateKey.length === 0) {
|
|
74
95
|
throw new Error("composeExport: exporterKey.privateKey (32-byte hex seed) is required");
|
|
75
96
|
}
|
|
97
|
+
// An over-long integer reads as Infinity (proof#217), which `jcs` refuses in
|
|
98
|
+
// the runtime's own words. Say it once, in the kernel's, before any
|
|
99
|
+
// verification: both kernels write this sentence, with no function-name prefix, so the CLI line is byte-identical.
|
|
100
|
+
if (hasNonFiniteNumber(sourceBundle)) {
|
|
101
|
+
throw new Error("the source bundle carries a number that is not a finite JSON number " +
|
|
102
|
+
"(an integer past the 1.8e308 range, or too long to read)");
|
|
103
|
+
}
|
|
104
|
+
if (hasNonFiniteNumber(attestations)) {
|
|
105
|
+
throw new Error("the attestations carry a number that is not a finite JSON number " +
|
|
106
|
+
"(an integer past the 1.8e308 range, or too long to read)");
|
|
107
|
+
}
|
|
76
108
|
if (options.generatedAt !== undefined && !isRfc3339UtcInstant(options.generatedAt)) {
|
|
77
109
|
// The verifier reads this as `malformed generated_at` (evidence-bundle §2,
|
|
78
110
|
// A31), so composing one would sign an export this kernel rejects.
|
package/dist/evidence.js
CHANGED
|
@@ -21,6 +21,7 @@ import { deriveOperatorAddress, ed25519Verify, hexToBytes,
|
|
|
21
21
|
// body: two copies of one rule is how two kernels drift apart one edit at a
|
|
22
22
|
// time, which is the whole defect class this fix is about.
|
|
23
23
|
isHexField, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
24
|
+
import { safeInteger, signedLeafCount } from "./grading.js";
|
|
24
25
|
import { isConformantHandoffRecord, isHandoffNamespace } from "./handoff.js";
|
|
25
26
|
import { leafHash, verifyInclusion } from "./merkle.js";
|
|
26
27
|
import { DuplicateKeyError, MAX_CANONICAL_DEPTH, contentHashes, exceedsDepth, isRfc3339UtcInstant, jcs, jcsParse, verifyEnvelope, } from "./verifier.js";
|
|
@@ -683,6 +684,7 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
683
684
|
const cpErrors = [];
|
|
684
685
|
let envelopeOk = false;
|
|
685
686
|
let merkleRootOk = false;
|
|
687
|
+
let leafCountOk = false;
|
|
686
688
|
// Decode event_bytes first: in v2 the join key is DERIVED from it, so it
|
|
687
689
|
// is an input to identity, not just to the payload binding below.
|
|
688
690
|
let eventBytes;
|
|
@@ -770,6 +772,15 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
770
772
|
cpErrors.push(`checkpoint merkle_root mismatch: claimed=${jsonStr(cp.merkle_root)} ` +
|
|
771
773
|
`committed=${jsonStr(committedRoot)}`);
|
|
772
774
|
}
|
|
775
|
+
// §5.1, A38 (proof#197), decided by Phil 2026-10-06: fail closed. The
|
|
776
|
+
// count the record signs is what every inclusion bound to this
|
|
777
|
+
// checkpoint is held to, so a record that signs no integer count fails
|
|
778
|
+
// the checkpoint, as a record that signs no merkle_root does. Read by
|
|
779
|
+
// value through the one reader the inclusion compare and the
|
|
780
|
+
// completeness notes share, so `3.0` and `3e0` are 3.
|
|
781
|
+
leafCountOk = signedLeafCount(cp) !== null;
|
|
782
|
+
if (!leafCountOk)
|
|
783
|
+
cpErrors.push("checkpoint record leaf_count is not an integer");
|
|
773
784
|
}
|
|
774
785
|
catch (e) {
|
|
775
786
|
cpErrors.push(`checkpoint record is not JSON${thrownDetail(e)}`);
|
|
@@ -788,7 +799,7 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
788
799
|
// standing rule is that malformed is a failure while absent is
|
|
789
800
|
// undetermined (the same rule disclosed `content` follows). Without
|
|
790
801
|
// this term the vector `trace-v2-malformed-anchor-ref` verified GREEN.
|
|
791
|
-
ok: envelopeOk && merkleRootOk && keyed.errors.length === 0 && !duplicateKey,
|
|
802
|
+
ok: envelopeOk && merkleRootOk && leafCountOk && keyed.errors.length === 0 && !duplicateKey,
|
|
792
803
|
errors: cpErrors,
|
|
793
804
|
});
|
|
794
805
|
if (keyed.anchorRefMalformed) {
|
|
@@ -919,30 +930,61 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
919
930
|
}
|
|
920
931
|
const checkpointBound = cp !== undefined;
|
|
921
932
|
let inclusionOk = false;
|
|
933
|
+
// §5.1, A38 (proof#197): the tree walked must be the tree the checkpoint
|
|
934
|
+
// record signed, the same rule and the same sentence as the stamp path's
|
|
935
|
+
// rule 3. RFC 9162's walk reads one audit path as valid at more than one
|
|
936
|
+
// (leaf_index, leaf_count), so without this a real proof at (2, 3) also
|
|
937
|
+
// verified as (1, 2) or (4, 5). A signed count that cannot be read is not
|
|
938
|
+
// compared here: it has already failed its checkpoint (above), so the
|
|
939
|
+
// bundle cannot verify. One reader of the signed count, shared with the
|
|
940
|
+
// completeness notes, so the verdict and the grade never read two
|
|
941
|
+
// different numbers.
|
|
942
|
+
//
|
|
943
|
+
// Every number here is read BY VALUE (`Number.isSafeInteger`), the model
|
|
944
|
+
// JCS signs, and the Python kernel reads the same way (`_safe_integer`):
|
|
945
|
+
// JCS writes `3.0` as `3`, so a stated `3.0` is the integer 3 under the
|
|
946
|
+
// same wrapper signature. A value that is not a safe integer is malformed,
|
|
947
|
+
// with one sentence that prints no value, because the two kernels cannot
|
|
948
|
+
// print `2.5`, `1e400` or `9007199254740993` the same way.
|
|
949
|
+
const signedCount = cp ? signedLeafCount(cp) : null;
|
|
922
950
|
if (incl && checkpointBound && cp) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
// value and reports the decode failure, so substituting "" here had
|
|
928
|
-
// the two kernels say different things about one bundle (TS: "the
|
|
929
|
-
// proof did not reconstruct the checkpoint root", which is not what
|
|
930
|
-
// happened: there was no claimed root to reconstruct).
|
|
931
|
-
hexToBytes(cp.merkle_root));
|
|
932
|
-
if (!inclusionOk)
|
|
933
|
-
evErrors.push("RFC 9162 inclusion proof did not reconstruct the checkpoint root");
|
|
934
|
-
// Defense in depth: the claimed leaf_hash MUST be the hash of the
|
|
935
|
-
// event's own signed envelope bytes (profile §6) — otherwise a valid
|
|
936
|
-
// inclusion proof for an UNRELATED leaf could be smuggled in.
|
|
937
|
-
const expectedLeaf = await leafHash(utf8(jcs(ev.envelope)));
|
|
938
|
-
const expectedLeafHex = toHex(expectedLeaf);
|
|
939
|
-
if (expectedLeafHex !== (incl.leaf_hash ?? "").toLowerCase()) {
|
|
940
|
-
inclusionOk = false;
|
|
941
|
-
evErrors.push(`inclusion leaf_hash does not match SHA-256(0x00 || JCS(event envelope))`);
|
|
942
|
-
}
|
|
951
|
+
const statedCount = safeInteger(incl.leaf_count);
|
|
952
|
+
const statedIndex = safeInteger(incl.leaf_index);
|
|
953
|
+
if (statedCount === null) {
|
|
954
|
+
evErrors.push("inclusion leaf_count is not an integer");
|
|
943
955
|
}
|
|
944
|
-
|
|
945
|
-
evErrors.push(
|
|
956
|
+
else if (statedIndex === null) {
|
|
957
|
+
evErrors.push("inclusion leaf_index is not an integer");
|
|
958
|
+
}
|
|
959
|
+
else if (signedCount !== null && statedCount !== signedCount) {
|
|
960
|
+
evErrors.push(`inclusion leaf_count ${statedCount} does not equal ` +
|
|
961
|
+
`the checkpoint's committed leaf_count ${signedCount}`);
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
try {
|
|
965
|
+
inclusionOk = await verifyInclusion(hexToBytes(incl.leaf_hash), statedIndex, statedCount, (incl.audit_path ?? []).map((h) => hexToBytes(h)),
|
|
966
|
+
// No `?? ""` fallback. A checkpoint that claims NO root must not be
|
|
967
|
+
// handed an invented empty one: the Python kernel refuses the same
|
|
968
|
+
// value and reports the decode failure, so substituting "" here had
|
|
969
|
+
// the two kernels say different things about one bundle (TS: "the
|
|
970
|
+
// proof did not reconstruct the checkpoint root", which is not what
|
|
971
|
+
// happened: there was no claimed root to reconstruct).
|
|
972
|
+
hexToBytes(cp.merkle_root));
|
|
973
|
+
if (!inclusionOk)
|
|
974
|
+
evErrors.push("RFC 9162 inclusion proof did not reconstruct the checkpoint root");
|
|
975
|
+
// Defense in depth: the claimed leaf_hash MUST be the hash of the
|
|
976
|
+
// event's own signed envelope bytes (profile §6) — otherwise a valid
|
|
977
|
+
// inclusion proof for an UNRELATED leaf could be smuggled in.
|
|
978
|
+
const expectedLeaf = await leafHash(utf8(jcs(ev.envelope)));
|
|
979
|
+
const expectedLeafHex = toHex(expectedLeaf);
|
|
980
|
+
if (expectedLeafHex !== (incl.leaf_hash ?? "").toLowerCase()) {
|
|
981
|
+
inclusionOk = false;
|
|
982
|
+
evErrors.push(`inclusion leaf_hash does not match SHA-256(0x00 || JCS(event envelope))`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
catch (e) {
|
|
986
|
+
evErrors.push(`inclusion verify error${thrownDetail(e)}`);
|
|
987
|
+
}
|
|
946
988
|
}
|
|
947
989
|
}
|
|
948
990
|
// Content (raw-log) binding — shared with every other event-carrying body.
|
|
@@ -1143,8 +1185,16 @@ function isLowerHex(s, nibbles) {
|
|
|
1143
1185
|
return false;
|
|
1144
1186
|
return /^[0-9a-f]+$/.test(s);
|
|
1145
1187
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1188
|
+
/**
|
|
1189
|
+
* A JSON number read BY VALUE as a non-negative safe integer, or null.
|
|
1190
|
+
* `safeInteger`, the reader every leaf count and leaf index in this kernel
|
|
1191
|
+
* goes through (A38), mirrored by `_non_neg_integer` in
|
|
1192
|
+
* `src/sanning_proof/evidence.py`. `Number.isInteger` alone accepted `1e+21`,
|
|
1193
|
+
* JCS's own spelling of a count past 2**53 - 1, which Python refused.
|
|
1194
|
+
*/
|
|
1195
|
+
function nonNegInteger(n) {
|
|
1196
|
+
const v = safeInteger(n);
|
|
1197
|
+
return v !== null && v >= 0 ? v : null;
|
|
1148
1198
|
}
|
|
1149
1199
|
/**
|
|
1150
1200
|
* Parse and check one stamp's witnessed bytes.
|
|
@@ -1382,12 +1432,13 @@ async function verifyStampEntry(raw, seen) {
|
|
|
1382
1432
|
else {
|
|
1383
1433
|
base.merkleRoot = body.merkle_root;
|
|
1384
1434
|
}
|
|
1385
|
-
|
|
1435
|
+
const stampCount = nonNegInteger(body.leaf_count);
|
|
1436
|
+
if (stampCount === null) {
|
|
1386
1437
|
errors.push("stamp body leaf_count is not a non-negative integer");
|
|
1387
1438
|
base.bodyOk = false;
|
|
1388
1439
|
}
|
|
1389
1440
|
else {
|
|
1390
|
-
base.leafCount =
|
|
1441
|
+
base.leafCount = stampCount;
|
|
1391
1442
|
}
|
|
1392
1443
|
// --- the sub-tree ranges --------------------------------------------------
|
|
1393
1444
|
base.subtreesOk = checkSubtrees(body, base, errors);
|
|
@@ -1426,14 +1477,16 @@ function checkSubtrees(body, out, errors) {
|
|
|
1426
1477
|
continue;
|
|
1427
1478
|
}
|
|
1428
1479
|
const s = v;
|
|
1429
|
-
|
|
1480
|
+
const firstLeaf = nonNegInteger(s.first_leaf);
|
|
1481
|
+
const subCount = nonNegInteger(s.leaf_count);
|
|
1482
|
+
if (firstLeaf === null) {
|
|
1430
1483
|
errors.push(`sub-tree ${ref} first_leaf is not a non-negative integer`);
|
|
1431
1484
|
shapeOk = false;
|
|
1432
1485
|
}
|
|
1433
1486
|
// A zero-leaf sub-tree is not a slice of anything, and admitting one lets
|
|
1434
1487
|
// an arbitrary number of empty ranges sit inside a tiling that still adds
|
|
1435
1488
|
// up.
|
|
1436
|
-
if (
|
|
1489
|
+
if (subCount === null || subCount === 0) {
|
|
1437
1490
|
errors.push(`sub-tree ${ref} leaf_count is not a positive integer`);
|
|
1438
1491
|
shapeOk = false;
|
|
1439
1492
|
}
|
|
@@ -1445,8 +1498,8 @@ function checkSubtrees(body, out, errors) {
|
|
|
1445
1498
|
continue;
|
|
1446
1499
|
subtrees.push({
|
|
1447
1500
|
ref,
|
|
1448
|
-
firstLeaf:
|
|
1449
|
-
leafCount:
|
|
1501
|
+
firstLeaf: firstLeaf,
|
|
1502
|
+
leafCount: subCount,
|
|
1450
1503
|
root: s.root,
|
|
1451
1504
|
});
|
|
1452
1505
|
}
|
|
@@ -1454,11 +1507,14 @@ function checkSubtrees(body, out, errors) {
|
|
|
1454
1507
|
out.subtrees = subtrees;
|
|
1455
1508
|
if (!shapeOk)
|
|
1456
1509
|
return false;
|
|
1457
|
-
const total =
|
|
1458
|
-
if (
|
|
1510
|
+
const total = out.leafCount;
|
|
1511
|
+
if (total === null)
|
|
1459
1512
|
return false; // already reported by the caller
|
|
1460
|
-
|
|
1461
|
-
|
|
1513
|
+
// Summed exactly. Each count is a safe integer but their sum need not be,
|
|
1514
|
+
// and a float sum past 2**53 prints a different number than Python's
|
|
1515
|
+
// unbounded one in the sentence below.
|
|
1516
|
+
const sum = subtrees.reduce((n, s) => n + BigInt(s.leafCount), 0n);
|
|
1517
|
+
if (sum !== BigInt(total)) {
|
|
1462
1518
|
errors.push(`sub-tree leaf_count sum ${sum} does not equal the stamp's leaf_count ${total}`);
|
|
1463
1519
|
return false;
|
|
1464
1520
|
}
|
|
@@ -1715,13 +1771,27 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1715
1771
|
// Rule 3: the tree walked must be the tree the stamp committed to. A
|
|
1716
1772
|
// proof walked against a different leaf_count is a proof about a
|
|
1717
1773
|
// different tree, and RFC 9162's path shape depends on the size.
|
|
1718
|
-
|
|
1719
|
-
|
|
1774
|
+
//
|
|
1775
|
+
// Read BY VALUE, as the anchor-trace path reads them (A38), and as the
|
|
1776
|
+
// Python kernel does (`_safe_integer`): the wrapper signs JCS(body),
|
|
1777
|
+
// which writes `3.0` as `3`, so a stated `3.0` is 3 under the same
|
|
1778
|
+
// signature. Anything that is not a safe integer is malformed, with a
|
|
1779
|
+
// sentence that prints no value.
|
|
1780
|
+
const statedCount = safeInteger(incl.leaf_count);
|
|
1781
|
+
const statedIndex = safeInteger(incl.leaf_index);
|
|
1782
|
+
if (statedCount === null) {
|
|
1783
|
+
evErrors.push("inclusion leaf_count is not an integer");
|
|
1784
|
+
}
|
|
1785
|
+
else if (statedIndex === null) {
|
|
1786
|
+
evErrors.push("inclusion leaf_index is not an integer");
|
|
1787
|
+
}
|
|
1788
|
+
else if (st.leafCount !== null && statedCount !== st.leafCount) {
|
|
1789
|
+
evErrors.push(`inclusion leaf_count ${statedCount} does not equal the stamp's committed ` +
|
|
1720
1790
|
`leaf_count ${st.leafCount}`);
|
|
1721
1791
|
}
|
|
1722
1792
|
else {
|
|
1723
1793
|
try {
|
|
1724
|
-
inclusionOk = await verifyInclusion(hexToBytes(incl.leaf_hash),
|
|
1794
|
+
inclusionOk = await verifyInclusion(hexToBytes(incl.leaf_hash), statedIndex, statedCount, (incl.audit_path ?? []).map((h) => hexToBytes(h)), hexToBytes(st.merkleRoot ?? ""));
|
|
1725
1795
|
if (!inclusionOk) {
|
|
1726
1796
|
evErrors.push("RFC 9162 inclusion proof did not reconstruct the stamp root");
|
|
1727
1797
|
}
|
package/dist/file-match.d.ts
CHANGED
|
@@ -47,6 +47,13 @@ export declare const FILE_MATCH_SCOPE: string;
|
|
|
47
47
|
/** Why a record's commitments could not be read, in the order they are tested. */
|
|
48
48
|
export declare const NOT_READ_REASONS: readonly ["no_records", "record_unreadable", "record_not_verified", "record_withheld", "no_content_hash", "handoff_record_malformed", "message_withheld"];
|
|
49
49
|
export type NotReadReason = (typeof NOT_READ_REASONS)[number];
|
|
50
|
+
/**
|
|
51
|
+
* Why a file was named in the report and never hashed (5.7.5). A skipped file
|
|
52
|
+
* is listed, never dropped: every file a command found is matched, unmatched or
|
|
53
|
+
* skipped, so "no match" is never read as "no file was there".
|
|
54
|
+
*/
|
|
55
|
+
export declare const SKIPPED_REASONS: readonly ["symbolic_link", "not_regular", "unreadable", "too_large", "refused"];
|
|
56
|
+
export type SkippedReason = (typeof SKIPPED_REASONS)[number];
|
|
50
57
|
/** One commitment a signed record makes to a file's bytes. */
|
|
51
58
|
export interface FileCommitment {
|
|
52
59
|
packRef: string | null;
|
|
@@ -56,6 +63,14 @@ export interface FileCommitment {
|
|
|
56
63
|
contentHash: string;
|
|
57
64
|
signer: string;
|
|
58
65
|
signedAt: string | null;
|
|
66
|
+
/**
|
|
67
|
+
* The pack, by its position in the packs `matchCaseFiles` was handed (the
|
|
68
|
+
* same position the case reading's `packIndex` names), or `null` from
|
|
69
|
+
* `matchFiles`, which reads one pack (A43).
|
|
70
|
+
*/
|
|
71
|
+
packIndex: number | null;
|
|
72
|
+
/** The record, by its position in the events the pack presents (A43). */
|
|
73
|
+
recordIndex: number;
|
|
59
74
|
}
|
|
60
75
|
export interface MatchedFile {
|
|
61
76
|
name: string;
|
|
@@ -66,10 +81,24 @@ export interface UnmatchedFile {
|
|
|
66
81
|
name: string;
|
|
67
82
|
sha256: string;
|
|
68
83
|
}
|
|
84
|
+
/** A file a command found and did not hash, named with the reason. */
|
|
85
|
+
export interface SkippedFile {
|
|
86
|
+
name: string;
|
|
87
|
+
reason: SkippedReason;
|
|
88
|
+
/** The reader's own words, for a `.zip` entry it refused (`refused`). */
|
|
89
|
+
detail?: string;
|
|
90
|
+
}
|
|
69
91
|
export interface NotRead {
|
|
70
92
|
packRef: string | null;
|
|
71
93
|
eventId: string | null;
|
|
72
94
|
reason: NotReadReason;
|
|
95
|
+
/** As on `FileCommitment` (A43). */
|
|
96
|
+
packIndex: number | null;
|
|
97
|
+
/**
|
|
98
|
+
* As on `FileCommitment`, or `null` where the pack presents no events at all
|
|
99
|
+
* (`no_records`).
|
|
100
|
+
*/
|
|
101
|
+
recordIndex: number | null;
|
|
73
102
|
}
|
|
74
103
|
export interface FileMatchReading {
|
|
75
104
|
offered: number;
|
|
@@ -78,12 +107,27 @@ export interface FileMatchReading {
|
|
|
78
107
|
notOffered: FileCommitment[];
|
|
79
108
|
notRead: NotRead[];
|
|
80
109
|
scope: string;
|
|
110
|
+
/**
|
|
111
|
+
* Files found and not hashed. The matching functions leave it empty: it is
|
|
112
|
+
* the caller's, because only a caller that reads a folder knows a link.
|
|
113
|
+
*/
|
|
114
|
+
skipped?: SkippedFile[];
|
|
81
115
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
116
|
+
/**
|
|
117
|
+
* A file handed in beside a pack. `name` is a label and is never compared. The
|
|
118
|
+
* file is its `bytes`, or its `sha256` (64 lowercase hex characters) where the
|
|
119
|
+
* caller has already hashed it: a command streams a file of any size rather
|
|
120
|
+
* than hold it.
|
|
121
|
+
*/
|
|
122
|
+
export type OfferedFile = {
|
|
84
123
|
name: string;
|
|
85
124
|
bytes: Uint8Array;
|
|
86
|
-
|
|
125
|
+
sha256?: undefined;
|
|
126
|
+
} | {
|
|
127
|
+
name: string;
|
|
128
|
+
sha256: string;
|
|
129
|
+
bytes?: undefined;
|
|
130
|
+
};
|
|
87
131
|
/** One pack of a case, for `matchCaseFiles`. */
|
|
88
132
|
export interface FileMatchPack {
|
|
89
133
|
ref: string;
|
|
@@ -101,5 +145,20 @@ export declare function matchFiles(bundle: unknown, files: readonly OfferedFile[
|
|
|
101
145
|
export declare function matchCaseFiles(packs: readonly FileMatchPack[], files: readonly OfferedFile[]): Promise<FileMatchReading>;
|
|
102
146
|
/** The reading as the `files` member of `--json`, identical in both kernels. */
|
|
103
147
|
export declare function fileMatchToJson(reading: FileMatchReading): Record<string, unknown>;
|
|
104
|
-
/**
|
|
105
|
-
|
|
148
|
+
/**
|
|
149
|
+
* The commitments no offered file matched and no disclosed log answers.
|
|
150
|
+
*
|
|
151
|
+
* `contents` is each pack's side content by `pack_ref`. A record whose own pack
|
|
152
|
+
* discloses bytes that hash to its `content_hash` is accounted for by the pack,
|
|
153
|
+
* and is not returned. Only the text report's count reads this; the reading, and
|
|
154
|
+
* `--json`, still list every commitment no file matched.
|
|
155
|
+
*/
|
|
156
|
+
export declare function notAccountedFor(reading: FileMatchReading, contents: ReadonlyMap<string, SideContentMap | null | undefined>): Promise<FileCommitment[]>;
|
|
157
|
+
/**
|
|
158
|
+
* The text report's lines for the reading, identical in both kernels.
|
|
159
|
+
*
|
|
160
|
+
* By default every commitment no file matched is a line. `notAmong` is the case
|
|
161
|
+
* command's report when no `--files` was given: one count line for the recorded
|
|
162
|
+
* files no loose file matched, and no line for each (A41).
|
|
163
|
+
*/
|
|
164
|
+
export declare function renderFileMatch(reading: FileMatchReading, notAmong?: number): string[];
|