Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import type { BlobRef } from "@atproto/lexicon"; 2import type { Directory, File } from "@wisp/lexicons/types/place/wisp/fs"; 3import { CID } from 'multiformats/cid'; 4import { sha256 } from 'multiformats/hashes/sha2'; 5import * as raw from 'multiformats/codecs/raw'; 6import { createHash } from 'crypto'; 7import * as mf from 'multiformats'; 8 9/** 10 * Compute CID (Content Identifier) for blob content 11 * Uses the same algorithm as AT Protocol: CIDv1 with raw codec and SHA-256 12 * Based on @atproto/common/src/ipld.ts sha256RawToCid implementation 13 */ 14export function computeCID(content: Buffer): string { 15 // Use node crypto to compute sha256 hash (same as AT Protocol) 16 const hash = createHash('sha256').update(content).digest(); 17 // Create digest object from hash bytes 18 const digest = mf.digest.create(sha256.code, hash); 19 // Create CIDv1 with raw codec 20 const cid = CID.createV1(raw.code, digest); 21 return cid.toString(); 22} 23 24/** 25 * Extract blob information from a directory tree 26 * Returns a map of file paths to their blob refs and CIDs 27 */ 28export function extractBlobMap( 29 directory: Directory, 30 currentPath: string = '' 31): Map<string, { blobRef: BlobRef; cid: string }> { 32 const blobMap = new Map<string, { blobRef: BlobRef; cid: string }>(); 33 34 for (const entry of directory.entries) { 35 const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 36 37 if ('type' in entry.node && entry.node.type === 'file') { 38 const fileNode = entry.node as File; 39 // AT Protocol SDK returns BlobRef class instances, not plain objects 40 // The ref is a CID instance that can be converted to string 41 if (fileNode.blob && fileNode.blob.ref) { 42 const cidString = fileNode.blob.ref.toString(); 43 blobMap.set(fullPath, { 44 blobRef: fileNode.blob, 45 cid: cidString 46 }); 47 } 48 } else if ('type' in entry.node && entry.node.type === 'directory') { 49 const subMap = extractBlobMap(entry.node as Directory, fullPath); 50 subMap.forEach((value, key) => blobMap.set(key, value)); 51 } 52 // Skip subfs nodes - they don't contain blobs in the main tree 53 } 54 55 return blobMap; 56} 57 58interface IpldLink { 59 $link: string; 60} 61 62interface TypedBlobRef { 63 ref: CID | IpldLink; 64} 65 66interface UntypedBlobRef { 67 cid: string; 68} 69 70function isIpldLink(obj: unknown): obj is IpldLink { 71 return typeof obj === 'object' && obj !== null && '$link' in obj && typeof (obj as IpldLink).$link === 'string'; 72} 73 74function isTypedBlobRef(obj: unknown): obj is TypedBlobRef { 75 return typeof obj === 'object' && obj !== null && 'ref' in obj; 76} 77 78function isUntypedBlobRef(obj: unknown): obj is UntypedBlobRef { 79 return typeof obj === 'object' && obj !== null && 'cid' in obj && typeof (obj as UntypedBlobRef).cid === 'string'; 80} 81 82/** 83 * Extract CID from a blob reference (handles multiple blob ref formats) 84 */ 85export function extractBlobCid(blobRef: unknown): string | null { 86 if (isIpldLink(blobRef)) { 87 return blobRef.$link; 88 } 89 90 if (isTypedBlobRef(blobRef)) { 91 const ref = blobRef.ref; 92 93 const cid = CID.asCID(ref); 94 if (cid) { 95 return cid.toString(); 96 } 97 98 if (isIpldLink(ref)) { 99 return ref.$link; 100 } 101 } 102 103 if (isUntypedBlobRef(blobRef)) { 104 return blobRef.cid; 105 } 106 107 return null; 108}