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

Configure Feed

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

feat: common lexification

+172 -491
-2
common/deno.json
··· 7 7 }, 8 8 "license": "MIT", 9 9 "imports": { 10 - "@ipld/dag-cbor": "npm:@ipld/dag-cbor@^9.2.5", 11 10 "@logtape/file": "jsr:@logtape/file@^1.2.2", 12 11 "@logtape/logtape": "jsr:@logtape/logtape@^1.2.2", 13 - "@std/cbor": "jsr:@std/cbor@^0.1.9", 14 12 "@std/encoding": "jsr:@std/encoding@^1.0.10", 15 13 "@std/fs": "jsr:@std/fs@^1.0.20", 16 14 "multiformats": "npm:multiformats@^13.4.1",
+7 -367
common/ipld-multi.ts
··· 1 - import { decodeCbor } from "@std/cbor"; 2 - import { CID } from "multiformats/cid"; 1 + import { decodeAll } from "@atp/lex/cbor"; 2 + import type { Cid } from "@atp/lex/data"; 3 3 4 - // Define the possible CBOR value types 5 4 export type CborPrimitive = 6 5 | string 7 6 | number ··· 15 14 | CborPrimitive 16 15 | CborArray 17 16 | CborObject 18 - | CID 17 + | Cid 19 18 | Uint8Array; 20 19 21 - // Type for CBOR tag structure 22 - interface CborTag<T = unknown> { 23 - tag: number; 24 - value: T; 25 - } 26 - 27 - // Type guard for CBOR tags 28 - function isCborTag(value: unknown): value is CborTag { 29 - return ( 30 - value !== null && 31 - typeof value === "object" && 32 - "tag" in value && 33 - "value" in value && 34 - typeof (value as CborTag).tag === "number" 35 - ); 36 - } 37 - 38 - // Type guard for CID tag specifically 39 - function isCidTag(value: unknown): value is CborTag<Uint8Array> { 40 - return isCborTag(value) && value.tag === 42 && 41 - value.value instanceof Uint8Array; 42 - } 43 - 44 - // Custom CBOR decoder that handles CIDs and multiple values 45 - class CborMultiDecoder { 46 - private buffer: Uint8Array; 47 - private position: number = 0; 48 - 49 - constructor(encoded: Uint8Array) { 50 - this.buffer = encoded; 51 - } 52 - 53 - private decodeCid(bytes: Uint8Array): CID { 54 - if (bytes[0] !== 0) { 55 - throw new Error("Invalid CID for CBOR tag 42; expected leading 0x00"); 56 - } 57 - return CID.decode(bytes.subarray(1)); // ignore leading 0x00 58 - } 59 - 60 - private decodeValue(): CborValue { 61 - // Find the next complete CBOR value 62 - const remaining = this.buffer.subarray(this.position); 63 - 64 - // Use @std/cbor to decode the next value 65 - // Note: @std/cbor doesn't have built-in support for decoding multiple values 66 - // or custom tags like cborx, so we need to handle this manually 67 - 68 - // For now, we'll decode the entire remaining buffer and handle CID tags manually 69 - const decoded = decodeCbor(remaining); 70 - 71 - // Update position to end of buffer (simplified approach) 72 - this.position = this.buffer.length; 73 - 74 - return this.processValue(decoded); 75 - } 76 - 77 - private processValue(value: unknown): CborValue { 78 - // Handle CID tag 42 if present 79 - if (isCidTag(value)) { 80 - return this.decodeCid(value.value); 81 - } 82 - 83 - // Handle other CBOR tags (convert to regular values for now) 84 - if (isCborTag(value)) { 85 - return this.processValue(value.value); 86 - } 87 - 88 - // Recursively process arrays 89 - if (Array.isArray(value)) { 90 - return value.map((item): CborValue => this.processValue(item)); 91 - } 92 - 93 - // Recursively process objects 94 - if ( 95 - value !== null && typeof value === "object" && 96 - !(value instanceof Uint8Array) 97 - ) { 98 - const result: CborObject = {}; 99 - for (const [key, val] of Object.entries(value)) { 100 - result[key] = this.processValue(val); 101 - } 102 - return result; 103 - } 104 - 105 - // Handle primitives and Uint8Array 106 - if ( 107 - typeof value === "string" || 108 - typeof value === "number" || 109 - typeof value === "bigint" || 110 - typeof value === "boolean" || 111 - value === null || 112 - value === undefined || 113 - value instanceof Uint8Array 114 - ) { 115 - return value; 116 - } 117 - 118 - // Fallback for any other types - shouldn't happen with valid CBOR 119 - throw new Error(`Unsupported CBOR value type: ${typeof value}`); 120 - } 121 - 122 - decodeMultiple(): CborValue[] { 123 - const decoded: CborValue[] = []; 124 - this.position = 0; 125 - 126 - // Parse CBOR values manually to handle concatenated data 127 - while (this.position < this.buffer.length) { 128 - try { 129 - const remaining = this.buffer.subarray(this.position); 130 - if (remaining.length === 0) break; 131 - 132 - // Parse the next CBOR value and track how many bytes we consumed 133 - const startPos = this.position; 134 - const value = this.parseNextCborValue(); 135 - decoded.push(this.processValue(value)); 136 - 137 - // If position didn't advance, we're stuck - break to avoid infinite loop 138 - if (this.position === startPos) { 139 - break; 140 - } 141 - } catch (error) { 142 - throw new Error(`Failed to decode CBOR: ${error}`); 143 - } 144 - } 145 - 146 - return decoded; 147 - } 148 - 149 - private parseNextCborValue(): unknown { 150 - if (this.position >= this.buffer.length) { 151 - throw new Error("Unexpected end of CBOR data"); 152 - } 153 - 154 - const _startPos = this.position; 155 - const byte = this.buffer[this.position]; 156 - const majorType = (byte >> 5) & 0x07; 157 - const additionalInfo = byte & 0x1f; 158 - 159 - this.position++; 160 - 161 - switch (majorType) { 162 - case 0: { // Unsigned integer 163 - return this.readUnsignedInt(additionalInfo); 164 - } 165 - case 1: { // Negative integer 166 - const unsignedInt = this.readUnsignedInt(additionalInfo); 167 - if (typeof unsignedInt === "bigint") { 168 - const negativeValue = -1n - unsignedInt; 169 - // Convert to number if within safe integer range 170 - if (negativeValue >= BigInt(Number.MIN_SAFE_INTEGER)) { 171 - return Number(negativeValue); 172 - } 173 - return negativeValue; 174 - } 175 - return -1 - unsignedInt; 176 - } 177 - case 2: { // Byte string 178 - return this.readByteString(additionalInfo); 179 - } 180 - case 3: { // Text string 181 - return this.readTextString(additionalInfo); 182 - } 183 - case 4: { // Array 184 - return this.readArray(additionalInfo); 185 - } 186 - case 5: { // Map 187 - return this.readMap(additionalInfo); 188 - } 189 - case 6: { // Tag 190 - return this.readTag(additionalInfo); 191 - } 192 - case 7: { // Float/Simple/Break 193 - return this.readFloatOrSimple(additionalInfo); 194 - } 195 - default: 196 - throw new Error(`Unknown CBOR major type: ${majorType}`); 197 - } 198 - } 199 - 200 - private readUnsignedInt(additionalInfo: number): number | bigint { 201 - if (additionalInfo < 24) { 202 - return additionalInfo; 203 - } else if (additionalInfo === 24) { 204 - return this.readUint8(); 205 - } else if (additionalInfo === 25) { 206 - return this.readUint16(); 207 - } else if (additionalInfo === 26) { 208 - return this.readUint32(); 209 - } else if (additionalInfo === 27) { 210 - const bigIntValue = this.readUint64(); 211 - // Convert to number if within safe integer range 212 - if (bigIntValue <= BigInt(Number.MAX_SAFE_INTEGER)) { 213 - return Number(bigIntValue); 214 - } 215 - return bigIntValue; 216 - } else { 217 - throw new Error( 218 - `Invalid additional info for unsigned int: ${additionalInfo}`, 219 - ); 220 - } 221 - } 222 - 223 - private readUint8(): number { 224 - if (this.position >= this.buffer.length) throw new Error("Unexpected end"); 225 - return this.buffer[this.position++]; 226 - } 227 - 228 - private readUint16(): number { 229 - if (this.position + 1 >= this.buffer.length) { 230 - throw new Error("Unexpected end"); 231 - } 232 - const value = (this.buffer[this.position] << 8) | 233 - this.buffer[this.position + 1]; 234 - this.position += 2; 235 - return value; 236 - } 237 - 238 - private readUint32(): number { 239 - if (this.position + 3 >= this.buffer.length) { 240 - throw new Error("Unexpected end"); 241 - } 242 - const value = (this.buffer[this.position] << 24) | 243 - (this.buffer[this.position + 1] << 16) | 244 - (this.buffer[this.position + 2] << 8) | 245 - this.buffer[this.position + 3]; 246 - this.position += 4; 247 - return value >>> 0; // Convert to unsigned 248 - } 249 - 250 - private readUint64(): bigint { 251 - if (this.position + 7 >= this.buffer.length) { 252 - throw new Error("Unexpected end"); 253 - } 254 - let value = 0n; 255 - for (let i = 0; i < 8; i++) { 256 - value = (value << 8n) | BigInt(this.buffer[this.position + i]); 257 - } 258 - this.position += 8; 259 - return value; 260 - } 261 - 262 - private readByteString(additionalInfo: number): Uint8Array { 263 - const length = this.readUnsignedInt(additionalInfo); 264 - if (typeof length === "bigint") { 265 - throw new Error("Byte string too large"); 266 - } 267 - const lengthNum = Number(length); 268 - if (this.position + lengthNum > this.buffer.length) { 269 - throw new Error("Unexpected end of byte string"); 270 - } 271 - const result = this.buffer.subarray( 272 - this.position, 273 - this.position + lengthNum, 274 - ); 275 - this.position += lengthNum; 276 - return result; 277 - } 278 - 279 - private readTextString(additionalInfo: number): string { 280 - const bytes = this.readByteString(additionalInfo); 281 - return new TextDecoder().decode(bytes); 282 - } 283 - 284 - private readArray(additionalInfo: number): unknown[] { 285 - const length = this.readUnsignedInt(additionalInfo); 286 - if (typeof length === "bigint") { 287 - throw new Error("Array too large"); 288 - } 289 - const lengthNum = Number(length); 290 - const result: unknown[] = []; 291 - for (let i = 0; i < lengthNum; i++) { 292 - result.push(this.parseNextCborValue()); 293 - } 294 - return result; 295 - } 296 - 297 - private readMap(additionalInfo: number): Record<string, unknown> { 298 - const length = this.readUnsignedInt(additionalInfo); 299 - if (typeof length === "bigint") { 300 - throw new Error("Map too large"); 301 - } 302 - const lengthNum = Number(length); 303 - const result: Record<string, unknown> = {}; 304 - for (let i = 0; i < lengthNum; i++) { 305 - const key = this.parseNextCborValue(); 306 - const value = this.parseNextCborValue(); 307 - if (typeof key !== "string") { 308 - throw new Error(`Map key must be string, got ${typeof key}`); 309 - } 310 - result[key] = value; 311 - } 312 - return result; 313 - } 314 - 315 - private readTag(additionalInfo: number): { tag: number; value: unknown } { 316 - const tag = this.readUnsignedInt(additionalInfo); 317 - const value = this.parseNextCborValue(); 318 - return { 319 - tag: typeof tag === "bigint" ? Number(tag) : tag, 320 - value, 321 - }; 322 - } 323 - 324 - private readFloatOrSimple(additionalInfo: number): unknown { 325 - if (additionalInfo < 20) { 326 - // Simple values 0-19 are unassigned 327 - throw new Error(`Unassigned simple value: ${additionalInfo}`); 328 - } else if (additionalInfo === 20) { 329 - return false; 330 - } else if (additionalInfo === 21) { 331 - return true; 332 - } else if (additionalInfo === 22) { 333 - return null; 334 - } else if (additionalInfo === 23) { 335 - return undefined; 336 - } else if (additionalInfo === 25) { 337 - // Half-precision float (16-bit) 338 - const value = this.readUint16(); 339 - return this.decodeFloat16(value); 340 - } else if (additionalInfo === 26) { 341 - // Single-precision float (32-bit) 342 - const bytes = new Uint8Array(4); 343 - for (let i = 0; i < 4; i++) { 344 - bytes[i] = this.buffer[this.position + i]; 345 - } 346 - this.position += 4; 347 - return new DataView(bytes.buffer).getFloat32(0, false); 348 - } else if (additionalInfo === 27) { 349 - // Double-precision float (64-bit) 350 - const bytes = new Uint8Array(8); 351 - for (let i = 0; i < 8; i++) { 352 - bytes[i] = this.buffer[this.position + i]; 353 - } 354 - this.position += 8; 355 - return new DataView(bytes.buffer).getFloat64(0, false); 356 - } else { 357 - throw new Error( 358 - `Invalid additional info for float/simple: ${additionalInfo}`, 359 - ); 360 - } 361 - } 362 - 363 - private decodeFloat16(value: number): number { 364 - const sign = (value & 0x8000) ? -1 : 1; 365 - const exponent = (value & 0x7c00) >> 10; 366 - const fraction = value & 0x03ff; 367 - 368 - if (exponent === 0) { 369 - return sign * Math.pow(2, -14) * (fraction / Math.pow(2, 10)); 370 - } else if (exponent === 0x1f) { 371 - return fraction ? NaN : sign * Infinity; 372 - } else { 373 - return sign * Math.pow(2, exponent - 15) * 374 - (1 + fraction / Math.pow(2, 10)); 375 - } 376 - } 377 - } 378 - 379 - // Generic version that allows callers to specify expected return type 380 20 export function cborDecodeMulti<T extends CborValue = CborValue>( 381 21 encoded: Uint8Array, 382 22 ): T[] { 383 - const decoder = new CborMultiDecoder(encoded); 384 - return decoder.decodeMultiple() as T[]; 23 + if (encoded.byteLength === 0) { 24 + return []; 25 + } 26 + return Array.from(decodeAll(encoded)) as T[]; 385 27 } 386 28 387 - // Convenience function for decoding a single value 388 29 export function cborDecodeSingle<T extends CborValue = CborValue>( 389 30 encoded: Uint8Array, 390 31 ): T { ··· 395 36 return results[0]; 396 37 } 397 38 398 - // Type-specific decoders for common use cases 399 39 export function cborDecodeMultiAsObjects(encoded: Uint8Array): CborObject[] { 400 40 return cborDecodeMulti<CborObject>(encoded); 401 41 }
+127 -115
common/ipld.ts
··· 1 - import * as cborCodec from "@ipld/dag-cbor"; 2 - import * as mf from "multiformats"; 3 - import * as Block from "multiformats/block"; 4 - import { CID } from "multiformats/cid"; 5 - import * as rawCodec from "multiformats/codecs/raw"; 6 - import { sha256 } from "multiformats/hashes/sha2"; 7 - import { schema } from "./types.ts"; 8 - import * as check from "./check.ts"; 9 - import { concat, equals, fromString, toString } from "@atp/bytes"; 1 + import { concat, equals } from "@atp/bytes"; 2 + import { 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"; 10 + import { 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"; 20 + import { 21 + encodeLexBytes, 22 + encodeLexLink, 23 + parseLexBytes, 24 + parseLexLink, 25 + } from "@atp/lex/json"; 26 + import { create as createDigest } from "multiformats/hashes/digest"; 27 + 28 + export type CborBlock<T = unknown> = { 29 + cid: Cid; 30 + bytes: Uint8Array; 31 + value: T; 32 + }; 10 33 11 - export const cborEncode = cborCodec.encode; 12 - export const cborDecode = cborCodec.decode; 34 + export const cborEncode = <T = unknown>(data: T): Uint8Array => { 35 + return encodeLexCbor(normalizeLexValue(data) as LexValue); 36 + }; 13 37 14 - export const dataToCborBlock = ( 15 - data: unknown, 16 - ): Promise<mf.BlockView> => { 17 - return Block.encode({ 18 - value: data, 19 - codec: cborCodec, 20 - hasher: sha256, 21 - }); 38 + export const cborDecode = <T = unknown>(bytes: Uint8Array): T => { 39 + return decodeLexCbor(bytes) as T; 40 + }; 41 + 42 + export 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 }; 22 48 }; 23 49 24 - export const cidForCbor = async (data: unknown): Promise<CID> => { 25 - const block = await dataToCborBlock(data); 26 - return block.cid; 50 + export const cidForCbor = async (data: unknown): Promise<Cid> => { 51 + return await cidForCborBytes(cborEncode(data)); 27 52 }; 28 53 29 54 export const isValidCid = (cidStr: string): boolean => { 30 - try { 31 - const parsed = CID.parse(cidStr); 32 - return parsed.toString() === cidStr; 33 - } catch { 34 - return false; 35 - } 55 + return validateCidString(cidStr); 36 56 }; 37 57 38 58 export const cborBytesToRecord = ( 39 59 bytes: Uint8Array, 40 60 ): Record<string, unknown> => { 41 61 const val = cborDecode(bytes); 42 - if (!check.is(val, schema.map)) { 62 + if (!isPlainObject(val)) { 43 63 throw new Error(`Expected object, got: ${val}`); 44 64 } 45 65 return val as Record<string, unknown>; 46 66 }; 47 67 48 68 export const verifyCidForBytes = async ( 49 - cid: CID, 69 + cid: Cid, 50 70 bytes: Uint8Array, 51 71 ): 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 - } 72 + await verifyLexCidForBytes(cid, bytes); 59 73 }; 60 74 61 - export const sha256ToCid = (hash: Uint8Array, codec: number): CID => { 62 - const digest = mf.digest.create(sha256.code, hash); 63 - return CID.createV1(codec, digest); 75 + export const sha256ToCid = (hash: Uint8Array, codec: number): Cid => { 76 + const digest = createDigest(SHA2_256_MULTIHASH_CODE, hash); 77 + return createCid(codec, digest); 64 78 }; 65 79 66 - export const sha256RawToCid = (hash: Uint8Array): CID => { 67 - return sha256ToCid(hash, rawCodec.code); 80 + export const sha256RawToCid = (hash: Uint8Array): Cid => { 81 + return cidForRawHash(hash); 68 82 }; 69 83 70 - // @NOTE: Only supports DASL CIDs 71 - // https://dasl.ing/cid.html 72 - export 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); 84 + export const parseCidFromBytes = (cidBytes: Uint8Array): Cid => { 85 + return parseDaslCidFromBytes(cidBytes); 91 86 }; 92 87 93 88 export class VerifyCidTransform 94 89 extends TransformStream<Uint8Array, Uint8Array> { 95 90 private chunks: Uint8Array[] = []; 96 91 97 - constructor(public cid: CID) { 92 + constructor(public cid: Cid) { 98 93 super({ 99 94 transform: (chunk: Uint8Array, controller) => { 100 95 this.chunks.push(chunk); ··· 123 118 124 119 export class VerifyCidError extends Error { 125 120 constructor( 126 - public expected: CID, 127 - public actual: CID, 121 + public expected: Cid, 122 + public actual: Cid, 128 123 ) { 129 124 super("Bad cid check"); 130 125 } ··· 142 137 143 138 export type IpldValue = 144 139 | JsonValue 145 - | CID 140 + | Cid 146 141 | Uint8Array 147 142 | Array<IpldValue> 148 143 | { [key: string]: IpldValue }; 149 144 150 - // @NOTE avoiding use of check.is() here only because it makes 151 - // these implementations slow, and they often live in hot paths. 152 - 153 145 export const jsonToIpld = (val: JsonValue): IpldValue => { 154 - // walk arrays 155 146 if (Array.isArray(val)) { 156 147 return val.map((item) => jsonToIpld(item)); 157 148 } 158 - // objects 159 149 if (val && typeof val === "object") { 160 150 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"]); 151 + const link = parseLexLink(obj); 152 + if (link) { 153 + return link; 164 154 } 165 - if (typeof obj["$bytes"] === "string" && Object.keys(val).length === 1) { 166 - return fromString(obj["$bytes"], "base64"); 155 + 156 + const bytes = parseLexBytes(obj); 157 + if (bytes) { 158 + return bytes; 167 159 } 168 - // walk plain objects 160 + 169 161 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 - } 162 + for (const [key, value] of Object.entries(obj)) { 163 + toReturn[key] = jsonToIpld(value as JsonValue); 183 164 } 184 165 return toReturn; 185 166 } 186 - // pass through 187 167 return val; 188 168 }; 189 169 190 170 export const ipldToJson = (val: IpldValue): JsonValue => { 191 - // walk arrays 192 171 if (Array.isArray(val)) { 193 172 return val.map((item) => ipldToJson(item)); 194 173 } 195 - // objects 196 174 if (val && typeof val === "object") { 197 - const obj = val as Record<string, unknown>; 198 - // convert bytes 199 175 if (val instanceof Uint8Array) { 200 - return { 201 - $bytes: toString(val, "base64"), 202 - }; 176 + return encodeLexBytes(val); 203 177 } 204 - // convert cids 205 - if (CID.asCID(val)) { 206 - return { 207 - $link: (val as CID).toString(), 208 - }; 178 + 179 + const cid = asCid(val); 180 + if (cid) { 181 + return encodeLexLink(cid); 209 182 } 210 - // walk plain objects 183 + 211 184 const toReturn: Record<string, JsonValue> = {}; 212 - for (const key of Object.keys(val)) { 213 - toReturn[key] = ipldToJson(obj[key]); 185 + for ( 186 + const [key, value] of Object.entries(val as Record<string, IpldValue>) 187 + ) { 188 + toReturn[key] = ipldToJson(value); 214 189 } 215 190 return toReturn; 216 191 } 217 - // pass through 218 192 return val as JsonValue; 219 193 }; 220 194 221 195 export const ipldEquals = (a: IpldValue, b: IpldValue): boolean => { 222 - // walk arrays 223 196 if (Array.isArray(a) && Array.isArray(b)) { 224 197 if (a.length !== b.length) return false; 225 198 for (let i = 0; i < a.length; i++) { ··· 227 200 } 228 201 return true; 229 202 } 230 - // objects 203 + 231 204 if (a && b && typeof a === "object" && typeof b === "object") { 232 - // check bytes 233 205 if (a instanceof Uint8Array && b instanceof Uint8Array) { 234 206 return equals(a, b); 235 207 } 236 - // check cids 237 - if (CID.asCID(a) && CID.asCID(b)) { 238 - return CID.asCID(a)?.equals(CID.asCID(b)) ?? false; 208 + 209 + const cidA = asCid(a); 210 + const cidB = asCid(b); 211 + if (cidA && cidB) { 212 + return cidA.equals(cidB); 239 213 } 240 - // walk plain objects 214 + 241 215 const objA = a as Record<string, IpldValue>; 242 216 const objB = b as Record<string, IpldValue>; 243 217 if (Object.keys(objA).length !== Object.keys(objB).length) return false; ··· 246 220 } 247 221 return true; 248 222 } 223 + 249 224 return a === b; 250 225 }; 226 + 227 + function 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 + }
+32 -1
common/tests/ipld_test.ts
··· 1 1 import * as ui8 from "@atp/bytes"; 2 2 import { 3 + cborBytesToRecord, 3 4 cborDecode, 4 5 cborEncode, 5 6 cidForCbor, 7 + dataToCborBlock, 6 8 ipldEquals, 7 9 ipldToJson, 8 10 jsonToIpld, 9 11 } from "../mod.ts"; 10 12 import { vectors } from "./interop/ipld-vectors.ts"; 11 - import { assert, assertEquals } from "@std/assert"; 13 + import { assert, assertEquals, assertRejects, assertThrows } from "@std/assert"; 12 14 13 15 for (const vector of vectors) { 14 16 Deno.test(`passes test vector: ${vector.name}`, async () => { ··· 26 28 assertEquals(cid.toString(), vector.cid); 27 29 }); 28 30 } 31 + 32 + Deno.test("cidForCbor accepts ArrayBuffer inputs", async () => { 33 + const bytes = ui8.fromString("hello world"); 34 + const arrayBuffer = bytes.buffer.slice( 35 + bytes.byteOffset, 36 + bytes.byteOffset + bytes.byteLength, 37 + ); 38 + 39 + const fromBytes = await cidForCbor(bytes); 40 + const fromBuffer = await cidForCbor(arrayBuffer); 41 + 42 + assertEquals(fromBuffer.toString(), fromBytes.toString()); 43 + }); 44 + 45 + Deno.test("cbor helpers reject undefined object members", async () => { 46 + const input = { a: undefined }; 47 + 48 + assertThrows(() => cborEncode(input), Error); 49 + await assertRejects(() => dataToCborBlock(input), Error); 50 + await assertRejects(() => cidForCbor(input), Error); 51 + }); 52 + 53 + Deno.test("cborBytesToRecord accepts bigint-valued maps", () => { 54 + const input = { big: 9007199254740993n }; 55 + const bytes = cborEncode(input); 56 + 57 + assertEquals(cborDecode(bytes), input); 58 + assertEquals(cborBytesToRecord(bytes), input); 59 + });
+4 -4
common/types.ts
··· 1 - import { CID } from "multiformats/cid"; 1 + import { asCid, type Cid } from "@atp/lex/data"; 2 2 import { z } from "zod"; 3 3 import type { Def } from "./check.ts"; 4 4 5 - const cidSchema = z.unknown().transform((obj, ctx): CID => { 6 - const cid = CID.asCID(obj); 5 + const cidSchema = z.unknown().transform((obj, ctx): Cid => { 6 + const cid = asCid(obj); 7 7 8 8 if (cid == null) { 9 9 ctx.addIssue({ ··· 37 37 cid: { 38 38 name: "cid", 39 39 schema: schema.cid, 40 - } as Def<CID>, 40 + } as Def<Cid>, 41 41 carHeader: { 42 42 name: "CAR header", 43 43 schema: schema.carHeader,
+2 -2
deno.lock
··· 41 41 "npm:@did-plc/server@^0.0.1": "0.0.1_express@4.21.2", 42 42 "npm:@dprint/formatter@~0.5.1": "0.5.1", 43 43 "npm:@dprint/typescript@~0.95.15": "0.95.15", 44 + "npm:@ipld/dag-cbor@*": "9.2.5", 44 45 "npm:@ipld/dag-cbor@^9.2.5": "9.2.5", 45 46 "npm:@opentelemetry/api@^1.9.0": "1.9.0", 46 47 "npm:@types/node@*": "24.2.0", 48 + "npm:cborg@*": "4.2.15", 47 49 "npm:cborg@^4.2.15": "4.2.15", 48 50 "npm:get-port@^7.1.0": "7.1.0", 49 51 "npm:key-encoder@^2.0.3": "2.0.3", ··· 1127 1129 "dependencies": [ 1128 1130 "jsr:@logtape/file@^1.2.2", 1129 1131 "jsr:@logtape/logtape@^1.2.2", 1130 - "jsr:@std/cbor@~0.1.9", 1131 1132 "jsr:@std/encoding@^1.0.10", 1132 1133 "jsr:@std/fs@^1.0.20", 1133 1134 "jsr:@zod/zod@^4.1.13", 1134 - "npm:@ipld/dag-cbor@^9.2.5", 1135 1135 "npm:multiformats@^13.4.1" 1136 1136 ] 1137 1137 },