Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

at lex 250 lines 6.8 kB view raw
1import * as cborCodec from "@ipld/dag-cbor"; 2import * as mf from "multiformats"; 3import * as Block from "multiformats/block"; 4import { CID } from "multiformats/cid"; 5import * as rawCodec from "multiformats/codecs/raw"; 6import { sha256 } from "multiformats/hashes/sha2"; 7import { schema } from "./types.ts"; 8import * as check from "./check.ts"; 9import { concat, equals, fromString, toString } from "@atp/bytes"; 10 11export const cborEncode = cborCodec.encode; 12export const cborDecode = cborCodec.decode; 13 14export const dataToCborBlock = ( 15 data: unknown, 16): Promise<mf.BlockView> => { 17 return Block.encode({ 18 value: data, 19 codec: cborCodec, 20 hasher: sha256, 21 }); 22}; 23 24export const cidForCbor = async (data: unknown): Promise<CID> => { 25 const block = await dataToCborBlock(data); 26 return block.cid; 27}; 28 29export const isValidCid = (cidStr: string): boolean => { 30 try { 31 const parsed = CID.parse(cidStr); 32 return parsed.toString() === cidStr; 33 } catch { 34 return false; 35 } 36}; 37 38export const cborBytesToRecord = ( 39 bytes: Uint8Array, 40): Record<string, unknown> => { 41 const val = cborDecode(bytes); 42 if (!check.is(val, schema.map)) { 43 throw new Error(`Expected object, got: ${val}`); 44 } 45 return val as Record<string, unknown>; 46}; 47 48export const verifyCidForBytes = async ( 49 cid: CID, 50 bytes: Uint8Array, 51): Promise<void> => { 52 const digest = await sha256.digest(bytes); 53 const expected = CID.createV1(cid.code, digest); 54 if (!cid.equals(expected)) { 55 throw new Error( 56 `Not a valid CID for bytes. Expected: ${expected} Got: ${cid}`, 57 ); 58 } 59}; 60 61export const sha256ToCid = (hash: Uint8Array, codec: number): CID => { 62 const digest = mf.digest.create(sha256.code, hash); 63 return CID.createV1(codec, digest); 64}; 65 66export const sha256RawToCid = (hash: Uint8Array): CID => { 67 return sha256ToCid(hash, rawCodec.code); 68}; 69 70// @NOTE: Only supports DASL CIDs 71// https://dasl.ing/cid.html 72export const parseCidFromBytes = (cidBytes: Uint8Array): CID => { 73 const version = cidBytes[0]; 74 if (version !== 0x01) { 75 throw new Error(`Unsupported CID version: ${version}`); 76 } 77 const codec = cidBytes[1]; 78 if (codec !== 0x55 && codec !== 0x71) { 79 throw new Error(`Unsupported CID codec: ${codec}`); 80 } 81 const hashType = cidBytes[2]; 82 if (hashType !== 0x12) { 83 throw new Error(`Unsupported CID hash function: ${hashType}`); 84 } 85 const hashLength = cidBytes[3]; 86 if (hashLength !== 32) { 87 throw new Error(`Unexpected CID hash length: ${hashLength}`); 88 } 89 const rest = cidBytes.slice(4); 90 return sha256ToCid(rest, codec); 91}; 92 93export class VerifyCidTransform 94 extends TransformStream<Uint8Array, Uint8Array> { 95 private chunks: Uint8Array[] = []; 96 97 constructor(public cid: CID) { 98 super({ 99 transform: (chunk: Uint8Array, controller) => { 100 this.chunks.push(chunk); 101 controller.enqueue(chunk); 102 }, 103 flush: async (controller) => { 104 try { 105 const data = concat(this.chunks); 106 const hash = new Uint8Array( 107 await crypto.subtle.digest("SHA-256", new Uint8Array(data)), 108 ); 109 const actual = sha256RawToCid(hash); 110 if (!actual.equals(cid)) { 111 controller.error(new VerifyCidError(cid, actual)); 112 } 113 } catch (err) { 114 controller.error(asError(err)); 115 } 116 }, 117 }); 118 } 119} 120 121const asError = (err: unknown): Error => 122 err instanceof Error ? err : new Error("Unexpected error", { cause: err }); 123 124export class VerifyCidError extends Error { 125 constructor( 126 public expected: CID, 127 public actual: CID, 128 ) { 129 super("Bad cid check"); 130 } 131} 132 133export type JsonValue = 134 | boolean 135 | number 136 | string 137 | null 138 | undefined 139 | unknown 140 | Array<JsonValue> 141 | { [key: string]: JsonValue }; 142 143export type IpldValue = 144 | JsonValue 145 | CID 146 | Uint8Array 147 | Array<IpldValue> 148 | { [key: string]: IpldValue }; 149 150// @NOTE avoiding use of check.is() here only because it makes 151// these implementations slow, and they often live in hot paths. 152 153export const jsonToIpld = (val: JsonValue): IpldValue => { 154 // walk arrays 155 if (Array.isArray(val)) { 156 return val.map((item) => jsonToIpld(item)); 157 } 158 // objects 159 if (val && typeof val === "object") { 160 const obj = val as Record<string, unknown>; 161 // check for dag json values 162 if (typeof obj["$link"] === "string" && Object.keys(val).length === 1) { 163 return CID.parse(obj["$link"]); 164 } 165 if (typeof obj["$bytes"] === "string" && Object.keys(val).length === 1) { 166 return fromString(obj["$bytes"], "base64"); 167 } 168 // walk plain objects 169 const toReturn: Record<string, IpldValue> = {}; 170 for (const key of Object.keys(val)) { 171 const value = obj[key]; 172 if ( 173 value && 174 typeof value === "object" && 175 !Array.isArray(value) && 176 typeof (value as Record<string, unknown>)["$link"] === "string" && 177 Object.keys(value).length === 1 178 ) { 179 toReturn[key] = CID.parse((value as { $link: string }).$link); 180 } else { 181 toReturn[key] = jsonToIpld(value); 182 } 183 } 184 return toReturn; 185 } 186 // pass through 187 return val; 188}; 189 190export const ipldToJson = (val: IpldValue): JsonValue => { 191 // walk arrays 192 if (Array.isArray(val)) { 193 return val.map((item) => ipldToJson(item)); 194 } 195 // objects 196 if (val && typeof val === "object") { 197 const obj = val as Record<string, unknown>; 198 // convert bytes 199 if (val instanceof Uint8Array) { 200 return { 201 $bytes: toString(val, "base64"), 202 }; 203 } 204 // convert cids 205 if (CID.asCID(val)) { 206 return { 207 $link: (val as CID).toString(), 208 }; 209 } 210 // walk plain objects 211 const toReturn: Record<string, JsonValue> = {}; 212 for (const key of Object.keys(val)) { 213 toReturn[key] = ipldToJson(obj[key]); 214 } 215 return toReturn; 216 } 217 // pass through 218 return val as JsonValue; 219}; 220 221export const ipldEquals = (a: IpldValue, b: IpldValue): boolean => { 222 // walk arrays 223 if (Array.isArray(a) && Array.isArray(b)) { 224 if (a.length !== b.length) return false; 225 for (let i = 0; i < a.length; i++) { 226 if (!ipldEquals(a[i], b[i])) return false; 227 } 228 return true; 229 } 230 // objects 231 if (a && b && typeof a === "object" && typeof b === "object") { 232 // check bytes 233 if (a instanceof Uint8Array && b instanceof Uint8Array) { 234 return equals(a, b); 235 } 236 // check cids 237 if (CID.asCID(a) && CID.asCID(b)) { 238 return CID.asCID(a)?.equals(CID.asCID(b)) ?? false; 239 } 240 // walk plain objects 241 const objA = a as Record<string, IpldValue>; 242 const objB = b as Record<string, IpldValue>; 243 if (Object.keys(objA).length !== Object.keys(objB).length) return false; 244 for (const key of Object.keys(objA)) { 245 if (!ipldEquals(objA[key], objB[key])) return false; 246 } 247 return true; 248 } 249 return a === b; 250};