+1
-1
jsr.json
+1
-1
jsr.json
+1
-1
package.json
+1
-1
package.json
+61
-6
src/cmds/verify.ts
+61
-6
src/cmds/verify.ts
···
8
8
9
9
OPTIONS:
10
10
--dir <path> Bundle directory (default: ./)
11
-
--bundle <num> Bundle number to verify (required)
11
+
--bundle <num> Bundle number to verify (required unless --chain is used)
12
+
--chain Verify entire chain instead of single bundle
13
+
--start <num> Start bundle for chain verification (default: 1)
14
+
--end <num> End bundle for chain verification (default: last bundle)
15
+
16
+
EXAMPLES:
17
+
plcbundle-bun verify --bundle 42
18
+
plcbundle-bun verify --chain
19
+
plcbundle-bun verify --chain --start 1 --end 100
12
20
`);
13
21
return;
14
22
}
···
18
26
options: {
19
27
dir: { type: 'string', default: './' },
20
28
bundle: { type: 'string' },
29
+
chain: { type: 'boolean', default: false },
30
+
start: { type: 'string' },
31
+
end: { type: 'string' },
21
32
},
22
33
strict: false,
23
34
});
24
35
36
+
const dir = (values.dir as string) || './';
37
+
const bundleInstance = new PLCBundle(dir);
38
+
39
+
// Chain verification
40
+
if (values.chain) {
41
+
const stats = await bundleInstance.getStats();
42
+
const start = values.start ? parseInt(values.start as string) : 1;
43
+
const end = values.end ? parseInt(values.end as string) : stats.lastBundle;
44
+
45
+
console.log(`Verifying chain: bundles ${start}-${end}\n`);
46
+
47
+
const startTime = Date.now();
48
+
49
+
const result = await bundleInstance.verifyChain({
50
+
start,
51
+
end,
52
+
onProgress: (current, total) => {
53
+
process.stdout.write(`Verified ${current}/${total} bundles\r`);
54
+
},
55
+
});
56
+
57
+
const elapsed = (Date.now() - startTime) / 1000;
58
+
59
+
console.log('\n');
60
+
61
+
if (result.valid) {
62
+
console.log(`✓ Chain verification passed`);
63
+
console.log(` Verified bundles: ${result.validBundles}`);
64
+
console.log(` Time elapsed: ${elapsed.toFixed(2)}s`);
65
+
} else {
66
+
console.error(`✗ Chain verification failed`);
67
+
console.error(` Valid bundles: ${result.validBundles}`);
68
+
console.error(` Invalid bundles: ${result.invalidBundles}`);
69
+
console.error('');
70
+
71
+
result.errors.forEach(({ bundleNum, errors }) => {
72
+
console.error(`Bundle ${bundleNum}:`);
73
+
errors.forEach(e => console.error(` - ${e}`));
74
+
});
75
+
76
+
process.exit(1);
77
+
}
78
+
79
+
return;
80
+
}
81
+
82
+
// Single bundle verification
25
83
if (!values.bundle || typeof values.bundle !== 'string') {
26
-
console.error('Error: --bundle is required');
84
+
console.error('Error: --bundle is required (or use --chain)');
27
85
process.exit(1);
28
86
}
29
87
30
-
const dir = (values.dir as string) || './';
31
88
const num = parseInt(values.bundle);
32
-
const bundle = new PLCBundle(dir);
33
-
34
-
const result = await bundle.verifyBundle(num);
89
+
const result = await bundleInstance.verifyBundle(num);
35
90
36
91
if (result.valid) {
37
92
console.log(`✓ Bundle ${num} is valid`);
+1
src/index.ts
+1
src/index.ts
+138
-1
src/plcbundle.ts
+138
-1
src/plcbundle.ts
···
6
6
ProcessOptions,
7
7
ProcessStats,
8
8
CloneOptions,
9
-
CloneStats
9
+
CloneStats,
10
+
ChainVerificationResult
10
11
} from './types';
11
12
12
13
/**
···
729
730
updatedAt: index.updated_at,
730
731
};
731
732
}
733
+
734
+
/**
735
+
* Verify the integrity of the entire bundle chain.
736
+
*
737
+
* This method validates:
738
+
* - Each bundle's compressed and content hashes
739
+
* - The chain hash linking each bundle to its parent
740
+
* - The continuity of the chain (no missing bundles)
741
+
* - The genesis bundle has correct initial hash
742
+
*
743
+
* @param options - Verification options
744
+
* @param options.start - First bundle to verify (default: 1)
745
+
* @param options.end - Last bundle to verify (default: last available bundle)
746
+
* @param options.onProgress - Callback for progress updates
747
+
* @returns Promise resolving to chain verification result
748
+
*
749
+
* @example Verify entire chain
750
+
* ```ts
751
+
* const result = await bundle.verifyChain();
752
+
*
753
+
* if (result.valid) {
754
+
* console.log(`✓ All ${result.totalBundles} bundles verified`);
755
+
* } else {
756
+
* console.error(`✗ Chain invalid: ${result.invalidBundles} errors`);
757
+
* result.errors.forEach(({ bundleNum, errors }) => {
758
+
* console.error(`Bundle ${bundleNum}:`);
759
+
* errors.forEach(e => console.error(` - ${e}`));
760
+
* });
761
+
* }
762
+
* ```
763
+
*
764
+
* @example Verify range with progress
765
+
* ```ts
766
+
* const result = await bundle.verifyChain({
767
+
* start: 1,
768
+
* end: 100,
769
+
* onProgress: (current, total) => {
770
+
* console.log(`Verified ${current}/${total} bundles`);
771
+
* }
772
+
* });
773
+
* ```
774
+
*/
775
+
async verifyChain(options: {
776
+
start?: number;
777
+
end?: number;
778
+
onProgress?: (current: number, total: number) => void;
779
+
} = {}): Promise<ChainVerificationResult> {
780
+
const index = await this.loadIndex();
781
+
782
+
const start = options.start || 1;
783
+
const end = options.end || index.last_bundle;
784
+
785
+
// Validate range
786
+
if (start < 1 || end > index.last_bundle || start > end) {
787
+
throw new Error(`Invalid bundle range ${start}-${end} (available: 1-${index.last_bundle})`);
788
+
}
789
+
790
+
const result: ChainVerificationResult = {
791
+
valid: true,
792
+
totalBundles: end - start + 1,
793
+
validBundles: 0,
794
+
invalidBundles: 0,
795
+
errors: [],
796
+
};
797
+
798
+
let previousHash = '';
799
+
800
+
for (let bundleNum = start; bundleNum <= end; bundleNum++) {
801
+
const metadata = index.bundles.find(b => b.bundle_number === bundleNum);
802
+
803
+
if (!metadata) {
804
+
result.valid = false;
805
+
result.invalidBundles++;
806
+
result.errors.push({
807
+
bundleNum,
808
+
errors: ['Bundle missing from index'],
809
+
});
810
+
continue;
811
+
}
812
+
813
+
const bundleErrors: string[] = [];
814
+
815
+
// Verify bundle file hashes
816
+
const verification = await this.verifyBundle(bundleNum);
817
+
if (!verification.valid) {
818
+
bundleErrors.push(...verification.errors);
819
+
}
820
+
821
+
// Verify chain linkage
822
+
if (bundleNum === 1) {
823
+
// Genesis bundle should have empty parent
824
+
if (metadata.parent !== '') {
825
+
bundleErrors.push(`Genesis bundle should have empty parent, got: ${metadata.parent}`);
826
+
}
827
+
828
+
// Verify genesis hash format
829
+
const expectedHash = this.calculateChainHash('', metadata.content_hash, true);
830
+
if (metadata.hash !== expectedHash) {
831
+
bundleErrors.push(`Invalid genesis chain hash: ${metadata.hash} != ${expectedHash}`);
832
+
}
833
+
} else {
834
+
// Verify parent reference
835
+
if (metadata.parent !== previousHash) {
836
+
bundleErrors.push(`Parent hash mismatch: expected ${previousHash}, got ${metadata.parent}`);
837
+
}
838
+
839
+
// Verify chain hash
840
+
const expectedHash = this.calculateChainHash(metadata.parent, metadata.content_hash, false);
841
+
if (metadata.hash !== expectedHash) {
842
+
bundleErrors.push(`Invalid chain hash: ${metadata.hash} != ${expectedHash}`);
843
+
}
844
+
}
845
+
846
+
// Record results
847
+
if (bundleErrors.length > 0) {
848
+
result.valid = false;
849
+
result.invalidBundles++;
850
+
result.errors.push({
851
+
bundleNum,
852
+
errors: bundleErrors,
853
+
});
854
+
} else {
855
+
result.validBundles++;
856
+
}
857
+
858
+
previousHash = metadata.hash;
859
+
860
+
// Report progress
861
+
if (options.onProgress) {
862
+
options.onProgress(bundleNum - start + 1, result.totalBundles);
863
+
}
864
+
}
865
+
866
+
return result;
867
+
}
868
+
732
869
}
+26
src/types.ts
+26
src/types.ts
···
219
219
/** Bytes downloaded so far (including skipped bundles) */
220
220
downloadedBytes: number;
221
221
}
222
+
223
+
/**
224
+
* Result of chain verification.
225
+
*
226
+
* Contains overall validity status and detailed information about
227
+
* any issues found in the bundle chain.
228
+
*/
229
+
export interface ChainVerificationResult {
230
+
/** Whether the entire chain is valid */
231
+
valid: boolean;
232
+
233
+
/** Total number of bundles verified */
234
+
totalBundles: number;
235
+
236
+
/** Number of bundles that passed verification */
237
+
validBundles: number;
238
+
239
+
/** Number of bundles that failed verification */
240
+
invalidBundles: number;
241
+
242
+
/** Detailed errors for each invalid bundle */
243
+
errors: Array<{
244
+
bundleNum: number;
245
+
errors: string[];
246
+
}>;
247
+
}