Suite of AT Protocol TypeScript libraries built on web standards
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};