⚡ Zero-dependency plcbundle library exclusively for Bun

verify --chain

+1 -1
jsr.json
··· 1 1 { 2 2 "name": "@atscan/plcbundle-bun", 3 - "version": "0.9.2", 3 + "version": "0.9.3", 4 4 "license": "MIT", 5 5 "exports": "./src/index.ts" 6 6 }
+1 -1
package.json
··· 1 1 { 2 2 "name": "@atscan/plcbundle-bun", 3 - "version": "0.9.2", 3 + "version": "0.9.3", 4 4 "type": "module", 5 5 "description": "Bun library for working with DID PLC bundle archives (plcbundle)", 6 6 "main": "./src/index.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
··· 37 37 ProcessOptions, 38 38 CloneOptions, 39 39 CloneStats, 40 + ChainVerificationResult, 40 41 } from './types';
+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
··· 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 + }