1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// 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);
}