A zero-dependency AT Protocol Personal Data Server written in JavaScript
atproto pds

refactor: consolidate CBOR encodeHead into shared helper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+44 -67
src
+44 -67
src/pds.js
··· 22 22 // === CBOR ENCODING === 23 23 // Minimal deterministic CBOR (RFC 8949) - sorted keys, minimal integers 24 24 25 + /** 26 + * Encode CBOR type header (major type + length) 27 + * @param {number[]} parts - Array to push bytes to 28 + * @param {number} majorType - CBOR major type (0-7) 29 + * @param {number} length - Value or length to encode 30 + */ 31 + function encodeHead(parts, majorType, length) { 32 + const mt = majorType << 5 33 + if (length < 24) { 34 + parts.push(mt | length) 35 + } else if (length < 256) { 36 + parts.push(mt | 24, length) 37 + } else if (length < 65536) { 38 + parts.push(mt | 25, length >> 8, length & 0xff) 39 + } else if (length < 4294967296) { 40 + // Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow 41 + parts.push(mt | 26, 42 + Math.floor(length / 0x1000000) & 0xff, 43 + Math.floor(length / 0x10000) & 0xff, 44 + Math.floor(length / 0x100) & 0xff, 45 + length & 0xff) 46 + } 47 + } 48 + 25 49 export function cborEncode(value) { 26 50 const parts = [] 27 51 ··· 36 60 encodeInteger(val) 37 61 } else if (typeof val === 'string') { 38 62 const bytes = new TextEncoder().encode(val) 39 - encodeHead(3, bytes.length) // major type 3 = text string 63 + encodeHead(parts, 3, bytes.length) // major type 3 = text string 40 64 parts.push(...bytes) 41 65 } else if (val instanceof Uint8Array) { 42 - encodeHead(2, val.length) // major type 2 = byte string 66 + encodeHead(parts, 2, val.length) // major type 2 = byte string 43 67 parts.push(...val) 44 68 } else if (Array.isArray(val)) { 45 - encodeHead(4, val.length) // major type 4 = array 69 + encodeHead(parts, 4, val.length) // major type 4 = array 46 70 for (const item of val) encode(item) 47 71 } else if (typeof val === 'object') { 48 72 // Sort keys for deterministic encoding 49 73 const keys = Object.keys(val).sort() 50 - encodeHead(5, keys.length) // major type 5 = map 74 + encodeHead(parts, 5, keys.length) // major type 5 = map 51 75 for (const key of keys) { 52 76 encode(key) 53 77 encode(val[key]) ··· 55 79 } 56 80 } 57 81 58 - function encodeHead(majorType, length) { 59 - const mt = majorType << 5 60 - if (length < 24) { 61 - parts.push(mt | length) 62 - } else if (length < 256) { 63 - parts.push(mt | 24, length) 64 - } else if (length < 65536) { 65 - parts.push(mt | 25, length >> 8, length & 0xff) 66 - } else if (length < 4294967296) { 67 - // Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow 68 - parts.push(mt | 26, 69 - Math.floor(length / 0x1000000) & 0xff, 70 - Math.floor(length / 0x10000) & 0xff, 71 - Math.floor(length / 0x100) & 0xff, 72 - length & 0xff) 73 - } 74 - } 75 - 76 82 function encodeInteger(n) { 77 83 if (n >= 0) { 78 - encodeHead(0, n) // major type 0 = unsigned int 84 + encodeHead(parts, 0, n) // major type 0 = unsigned int 79 85 } else { 80 - encodeHead(1, -n - 1) // major type 1 = negative int 86 + encodeHead(parts, 1, -n - 1) // major type 1 = negative int 81 87 } 82 88 } 83 89 ··· 98 104 parts.push(CBOR_FALSE) 99 105 } else if (typeof val === 'number') { 100 106 if (Number.isInteger(val) && val >= 0) { 101 - encodeHead(0, val) 107 + encodeHead(parts, 0, val) 102 108 } else if (Number.isInteger(val) && val < 0) { 103 - encodeHead(1, -val - 1) 109 + encodeHead(parts, 1, -val - 1) 104 110 } 105 111 } else if (typeof val === 'string') { 106 112 const bytes = new TextEncoder().encode(val) 107 - encodeHead(3, bytes.length) 113 + encodeHead(parts, 3, bytes.length) 108 114 parts.push(...bytes) 109 115 } else if (val instanceof CID) { 110 116 // CID - encode with CBOR tag 42 + 0x00 prefix 111 117 parts.push(0xd8, CBOR_TAG_CID) 112 - encodeHead(2, val.bytes.length + 1) // +1 for 0x00 prefix 118 + encodeHead(parts, 2, val.bytes.length + 1) // +1 for 0x00 prefix 113 119 parts.push(0x00) // multibase identity prefix 114 120 parts.push(...val.bytes) 115 121 } else if (val instanceof Uint8Array) { 116 122 // Regular byte string 117 - encodeHead(2, val.length) 123 + encodeHead(parts, 2, val.length) 118 124 parts.push(...val) 119 125 } else if (Array.isArray(val)) { 120 - encodeHead(4, val.length) 126 + encodeHead(parts, 4, val.length) 121 127 for (const item of val) encode(item) 122 128 } else if (typeof val === 'object') { 123 129 // DAG-CBOR: sort keys by length first, then lexicographically ··· 126 132 if (a.length !== b.length) return a.length - b.length 127 133 return a < b ? -1 : a > b ? 1 : 0 128 134 }) 129 - encodeHead(5, keys.length) 135 + encodeHead(parts, 5, keys.length) 130 136 for (const key of keys) { 131 137 const keyBytes = new TextEncoder().encode(key) 132 - encodeHead(3, keyBytes.length) 138 + encodeHead(parts, 3, keyBytes.length) 133 139 parts.push(...keyBytes) 134 140 encode(val[key]) 135 141 } 136 - } 137 - } 138 - 139 - function encodeHead(majorType, length) { 140 - const mt = majorType << 5 141 - if (length < 24) { 142 - parts.push(mt | length) 143 - } else if (length < 256) { 144 - parts.push(mt | 24, length) 145 - } else if (length < 65536) { 146 - parts.push(mt | 25, length >> 8, length & 0xff) 147 - } else if (length < 4294967296) { 148 - // Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow 149 - parts.push(mt | 26, 150 - Math.floor(length / 0x1000000) & 0xff, 151 - Math.floor(length / 0x10000) & 0xff, 152 - Math.floor(length / 0x100) & 0xff, 153 - length & 0xff) 154 142 } 155 143 } 156 144 ··· 580 568 if (val === null || val === undefined) { 581 569 parts.push(CBOR_NULL) 582 570 } else if (typeof val === 'number') { 583 - encodeHead(0, val) // unsigned int 571 + encodeHead(parts, 0, val) // unsigned int 584 572 } else if (val instanceof CID) { 585 573 // CID - encode with CBOR tag 42 + 0x00 prefix (DAG-CBOR CID link) 586 574 parts.push(0xd8, CBOR_TAG_CID) 587 - encodeHead(2, val.bytes.length + 1) // +1 for 0x00 prefix 575 + encodeHead(parts, 2, val.bytes.length + 1) // +1 for 0x00 prefix 588 576 parts.push(0x00) // multibase identity prefix 589 577 parts.push(...val.bytes) 590 578 } else if (val instanceof Uint8Array) { 591 579 // Regular bytes 592 - encodeHead(2, val.length) 580 + encodeHead(parts, 2, val.length) 593 581 parts.push(...val) 594 582 } else if (Array.isArray(val)) { 595 - encodeHead(4, val.length) 583 + encodeHead(parts, 4, val.length) 596 584 for (const item of val) encode(item) 597 585 } else if (typeof val === 'object') { 598 586 // Sort keys for deterministic encoding (DAG-CBOR style) ··· 603 591 if (a.length !== b.length) return a.length - b.length 604 592 return a < b ? -1 : a > b ? 1 : 0 605 593 }) 606 - encodeHead(5, keys.length) 594 + encodeHead(parts, 5, keys.length) 607 595 for (const key of keys) { 608 596 // Encode key as text string 609 597 const keyBytes = new TextEncoder().encode(key) 610 - encodeHead(3, keyBytes.length) 598 + encodeHead(parts, 3, keyBytes.length) 611 599 parts.push(...keyBytes) 612 600 // Encode value 613 601 encode(val[key]) 614 602 } 615 - } 616 - } 617 - 618 - function encodeHead(majorType, length) { 619 - const mt = majorType << 5 620 - if (length < 24) { 621 - parts.push(mt | length) 622 - } else if (length < 256) { 623 - parts.push(mt | 24, length) 624 - } else if (length < 65536) { 625 - parts.push(mt | 25, length >> 8, length & 0xff) 626 603 } 627 604 } 628 605