Suite of AT Protocol TypeScript libraries built on web standards
1import { concat, equals } from "@atp/bytes";
2import {
3 cidForCbor as cidForCborBytes,
4 cidForRawHash,
5 decode as decodeLexCbor,
6 encode as encodeLexCbor,
7 parseCidFromBytes as parseDaslCidFromBytes,
8 verifyCidForBytes as verifyLexCidForBytes,
9} from "@atp/lex/cbor";
10import {
11 asCid,
12 type Cid,
13 createCid,
14 isPlainObject,
15 type LexMap,
16 type LexValue,
17 SHA2_256_MULTIHASH_CODE,
18 validateCidString,
19} from "@atp/lex/data";
20import {
21 encodeLexBytes,
22 encodeLexLink,
23 parseLexBytes,
24 parseLexLink,
25} from "@atp/lex/json";
26import { create as createDigest } from "multiformats/hashes/digest";
27
28export type CborBlock<T = unknown> = {
29 cid: Cid;
30 bytes: Uint8Array;
31 value: T;
32};
33
34export const cborEncode = <T = unknown>(data: T): Uint8Array => {
35 return encodeLexCbor(normalizeLexValue(data) as LexValue);
36};
37
38export const cborDecode = <T = unknown>(bytes: Uint8Array): T => {
39 return decodeLexCbor(bytes) as T;
40};
41
42export const dataToCborBlock = async <T = unknown>(
43 data: T,
44): Promise<CborBlock<T>> => {
45 const bytes = cborEncode(data);
46 const cid = await cidForCborBytes(bytes);
47 return { cid, bytes, value: data };
48};
49
50export const cidForCbor = async (data: unknown): Promise<Cid> => {
51 return await cidForCborBytes(cborEncode(data));
52};
53
54export const isValidCid = (cidStr: string): boolean => {
55 return validateCidString(cidStr);
56};
57
58export const cborBytesToRecord = (
59 bytes: Uint8Array,
60): Record<string, unknown> => {
61 const val = cborDecode(bytes);
62 if (!isPlainObject(val)) {
63 throw new Error(`Expected object, got: ${val}`);
64 }
65 return val as Record<string, unknown>;
66};
67
68export const verifyCidForBytes = async (
69 cid: Cid,
70 bytes: Uint8Array,
71): Promise<void> => {
72 await verifyLexCidForBytes(cid, bytes);
73};
74
75export const sha256ToCid = (hash: Uint8Array, codec: number): Cid => {
76 const digest = createDigest(SHA2_256_MULTIHASH_CODE, hash);
77 return createCid(codec, digest);
78};
79
80export const sha256RawToCid = (hash: Uint8Array): Cid => {
81 return cidForRawHash(hash);
82};
83
84export const parseCidFromBytes = (cidBytes: Uint8Array): Cid => {
85 return parseDaslCidFromBytes(cidBytes);
86};
87
88export class VerifyCidTransform
89 extends TransformStream<Uint8Array, Uint8Array> {
90 private chunks: Uint8Array[] = [];
91
92 constructor(public cid: Cid) {
93 super({
94 transform: (chunk: Uint8Array, controller) => {
95 this.chunks.push(chunk);
96 controller.enqueue(chunk);
97 },
98 flush: async (controller) => {
99 try {
100 const data = concat(this.chunks);
101 const hash = new Uint8Array(
102 await crypto.subtle.digest("SHA-256", new Uint8Array(data)),
103 );
104 const actual = sha256RawToCid(hash);
105 if (!actual.equals(cid)) {
106 controller.error(new VerifyCidError(cid, actual));
107 }
108 } catch (err) {
109 controller.error(asError(err));
110 }
111 },
112 });
113 }
114}
115
116const asError = (err: unknown): Error =>
117 err instanceof Error ? err : new Error("Unexpected error", { cause: err });
118
119export class VerifyCidError extends Error {
120 constructor(
121 public expected: Cid,
122 public actual: Cid,
123 ) {
124 super("Bad cid check");
125 }
126}
127
128export type JsonValue =
129 | boolean
130 | number
131 | string
132 | null
133 | undefined
134 | unknown
135 | Array<JsonValue>
136 | { [key: string]: JsonValue };
137
138export type IpldValue =
139 | JsonValue
140 | Cid
141 | Uint8Array
142 | Array<IpldValue>
143 | { [key: string]: IpldValue };
144
145export const jsonToIpld = (val: JsonValue): IpldValue => {
146 if (Array.isArray(val)) {
147 return val.map((item) => jsonToIpld(item));
148 }
149 if (val && typeof val === "object") {
150 const obj = val as Record<string, unknown>;
151 const link = parseLexLink(obj);
152 if (link) {
153 return link;
154 }
155
156 const bytes = parseLexBytes(obj);
157 if (bytes) {
158 return bytes;
159 }
160
161 const toReturn: Record<string, IpldValue> = {};
162 for (const [key, value] of Object.entries(obj)) {
163 toReturn[key] = jsonToIpld(value as JsonValue);
164 }
165 return toReturn;
166 }
167 return val;
168};
169
170export const ipldToJson = (val: IpldValue): JsonValue => {
171 if (Array.isArray(val)) {
172 return val.map((item) => ipldToJson(item));
173 }
174 if (val && typeof val === "object") {
175 if (val instanceof Uint8Array) {
176 return encodeLexBytes(val);
177 }
178
179 const cid = asCid(val);
180 if (cid) {
181 return encodeLexLink(cid);
182 }
183
184 const toReturn: Record<string, JsonValue> = {};
185 for (
186 const [key, value] of Object.entries(val as Record<string, IpldValue>)
187 ) {
188 toReturn[key] = ipldToJson(value);
189 }
190 return toReturn;
191 }
192 return val as JsonValue;
193};
194
195export const ipldEquals = (a: IpldValue, b: IpldValue): boolean => {
196 if (Array.isArray(a) && Array.isArray(b)) {
197 if (a.length !== b.length) return false;
198 for (let i = 0; i < a.length; i++) {
199 if (!ipldEquals(a[i], b[i])) return false;
200 }
201 return true;
202 }
203
204 if (a && b && typeof a === "object" && typeof b === "object") {
205 if (a instanceof Uint8Array && b instanceof Uint8Array) {
206 return equals(a, b);
207 }
208
209 const cidA = asCid(a);
210 const cidB = asCid(b);
211 if (cidA && cidB) {
212 return cidA.equals(cidB);
213 }
214
215 const objA = a as Record<string, IpldValue>;
216 const objB = b as Record<string, IpldValue>;
217 if (Object.keys(objA).length !== Object.keys(objB).length) return false;
218 for (const key of Object.keys(objA)) {
219 if (!ipldEquals(objA[key], objB[key])) return false;
220 }
221 return true;
222 }
223
224 return a === b;
225};
226
227function normalizeLexValue(input: unknown): LexValue {
228 if (input instanceof Uint8Array) {
229 return input;
230 }
231
232 if (ArrayBuffer.isView(input)) {
233 return new Uint8Array(
234 input.buffer,
235 input.byteOffset,
236 input.byteLength,
237 );
238 }
239
240 if (input instanceof ArrayBuffer) {
241 return new Uint8Array(input);
242 }
243
244 if (Array.isArray(input)) {
245 return input.map((item) => normalizeLexValue(item));
246 }
247
248 const cid = asCid(input);
249 if (cid) {
250 return cid;
251 }
252
253 if (isPlainObject(input)) {
254 const normalized: LexMap = {};
255 for (const [key, value] of Object.entries(input)) {
256 normalized[key] = normalizeLexValue(value);
257 }
258 return normalized;
259 }
260
261 return input as LexValue;
262}