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}