// The byte encodings the hosts share: base58btc (did:key), base32 (CIDv1) and DAG-CBOR (attestations, // event-stream frames). No imports; browsers, Node and Workers. const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const B32 = "abcdefghijklmnopqrstuvwxyz234567"; const encoder = new TextEncoder(); export function base58(bytes) { let n = BigInt(`0x${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("") || "0"}`); let out = ""; for (; n > 0n; n /= 58n) out = B58[Number(n % 58n)] + out; for (const byte of bytes) { if (byte) break; out = `1${out}`; } return out; } export function unbase58(text) { let n = 0n; for (const c of text) { const digit = B58.indexOf(c); if (digit < 0) throw new Error("bad base58"); n = n * 58n + BigInt(digit); } const hex = n ? n.toString(16) : ""; const bytes = (hex.length % 2 ? `0${hex}` : hex).match(/../g)?.map((h) => parseInt(h, 16)) ?? []; return Uint8Array.from([...Array(text.length - text.replace(/^1+/, "").length).fill(0), ...bytes]); } /** Lowercase RFC 4648 base32, no padding (a CIDv1's "b" multibase, without the "b"). */ export function toBase32(bytes) { let out = ""; let value = 0; let bits = 0; for (const byte of bytes) { value = (value << 8) | byte; bits += 8; while (bits >= 5) out += B32[(value >>> (bits -= 5)) & 31]; value &= (1 << bits) - 1; } return bits ? out + B32[(value << (5 - bits)) & 31] : out; } export function fromBase32(text) { const bytes = []; let value = 0; let bits = 0; for (const char of text) { const digit = B32.indexOf(char); if (digit < 0) throw new Error("bad base32"); value = (value << 5) | digit; bits += 5; if (bits >= 8) bytes.push((value >>> (bits -= 8)) & 0xff), (value &= (1 << bits) - 1); } return new Uint8Array(bytes); } const fromBase64 = (text) => Uint8Array.from(atob(text.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)); // DAG-CBOR map order: keys by encoded (UTF-8) length, then bytewise. function compareKeys(a, b) { if (a.length !== b.length) return a.length - b.length; const i = a.findIndex((byte, index) => byte !== b[index]); return i < 0 ? 0 : a[i] - b[i]; } /** * A JSON value (atproto's data model) as DAG-CBOR: integers only, undefined map entries left out, * Uint8Array as bytes. links: { $bytes } and { $link } become bytes and CIDs (tag 42), as decodeCbor * returns them; without it they stay plain maps (attestations hash them that way). */ export function dagCbor(value, { links = false } = {}) { const out = []; const head = (major, n) => { if (n < 24) out.push((major << 5) | n); else if (n < 0x100) out.push((major << 5) | 24, n); else if (n < 0x10000) out.push((major << 5) | 25, n >> 8, n & 0xff); else if (n < 0x100000000) out.push((major << 5) | 26, n >>> 24, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff); else { out.push((major << 5) | 27); for (let shift = 56n; shift >= 0n; shift -= 8n) out.push(Number((BigInt(n) >> shift) & 0xffn)); } }; // a loop, not out.push(...b): spreading a large byte string overflows the stack const append = (b) => { for (const x of b) out.push(x); }; const bytes = (b) => (head(2, b.length), append(b)); const item = (v) => { if (v === null || v === undefined) return out.push(0xf6); if (v === false || v === true) return out.push(v ? 0xf5 : 0xf4); if (typeof v === "number") { if (!Number.isSafeInteger(v)) throw new Error("DAG-CBOR in atproto has integers only"); return v >= 0 ? head(0, v) : head(1, -1 - v); } if (typeof v === "string") { const b = encoder.encode(v); return head(3, b.length), append(b); } if (v instanceof Uint8Array) return bytes(v); if (Array.isArray(v)) return head(4, v.length), v.forEach(item); const keys = Object.keys(v).filter((k) => v[k] !== undefined); if (links && keys.length === 1 && keys[0] === "$bytes") return bytes(fromBase64(v.$bytes)); if (links && keys.length === 1 && keys[0] === "$link") return head(6, 42), bytes([0, ...fromBase32(v.$link.slice(1))]); const entries = keys.map((k) => [encoder.encode(k), v[k]]).sort(([a], [b]) => compareKeys(a, b)); head(5, entries.length); for (const [k, x] of entries) head(3, k.length), append(k), item(x); }; item(value); return new Uint8Array(out); }