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 // === CBOR ENCODING === 23 // Minimal deterministic CBOR (RFC 8949) - sorted keys, minimal integers 24 25 export function cborEncode(value) { 26 const parts = [] 27 ··· 36 encodeInteger(val) 37 } else if (typeof val === 'string') { 38 const bytes = new TextEncoder().encode(val) 39 - encodeHead(3, bytes.length) // major type 3 = text string 40 parts.push(...bytes) 41 } else if (val instanceof Uint8Array) { 42 - encodeHead(2, val.length) // major type 2 = byte string 43 parts.push(...val) 44 } else if (Array.isArray(val)) { 45 - encodeHead(4, val.length) // major type 4 = array 46 for (const item of val) encode(item) 47 } else if (typeof val === 'object') { 48 // Sort keys for deterministic encoding 49 const keys = Object.keys(val).sort() 50 - encodeHead(5, keys.length) // major type 5 = map 51 for (const key of keys) { 52 encode(key) 53 encode(val[key]) ··· 55 } 56 } 57 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 function encodeInteger(n) { 77 if (n >= 0) { 78 - encodeHead(0, n) // major type 0 = unsigned int 79 } else { 80 - encodeHead(1, -n - 1) // major type 1 = negative int 81 } 82 } 83 ··· 98 parts.push(CBOR_FALSE) 99 } else if (typeof val === 'number') { 100 if (Number.isInteger(val) && val >= 0) { 101 - encodeHead(0, val) 102 } else if (Number.isInteger(val) && val < 0) { 103 - encodeHead(1, -val - 1) 104 } 105 } else if (typeof val === 'string') { 106 const bytes = new TextEncoder().encode(val) 107 - encodeHead(3, bytes.length) 108 parts.push(...bytes) 109 } else if (val instanceof CID) { 110 // CID - encode with CBOR tag 42 + 0x00 prefix 111 parts.push(0xd8, CBOR_TAG_CID) 112 - encodeHead(2, val.bytes.length + 1) // +1 for 0x00 prefix 113 parts.push(0x00) // multibase identity prefix 114 parts.push(...val.bytes) 115 } else if (val instanceof Uint8Array) { 116 // Regular byte string 117 - encodeHead(2, val.length) 118 parts.push(...val) 119 } else if (Array.isArray(val)) { 120 - encodeHead(4, val.length) 121 for (const item of val) encode(item) 122 } else if (typeof val === 'object') { 123 // DAG-CBOR: sort keys by length first, then lexicographically ··· 126 if (a.length !== b.length) return a.length - b.length 127 return a < b ? -1 : a > b ? 1 : 0 128 }) 129 - encodeHead(5, keys.length) 130 for (const key of keys) { 131 const keyBytes = new TextEncoder().encode(key) 132 - encodeHead(3, keyBytes.length) 133 parts.push(...keyBytes) 134 encode(val[key]) 135 } 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 } 155 } 156 ··· 580 if (val === null || val === undefined) { 581 parts.push(CBOR_NULL) 582 } else if (typeof val === 'number') { 583 - encodeHead(0, val) // unsigned int 584 } else if (val instanceof CID) { 585 // CID - encode with CBOR tag 42 + 0x00 prefix (DAG-CBOR CID link) 586 parts.push(0xd8, CBOR_TAG_CID) 587 - encodeHead(2, val.bytes.length + 1) // +1 for 0x00 prefix 588 parts.push(0x00) // multibase identity prefix 589 parts.push(...val.bytes) 590 } else if (val instanceof Uint8Array) { 591 // Regular bytes 592 - encodeHead(2, val.length) 593 parts.push(...val) 594 } else if (Array.isArray(val)) { 595 - encodeHead(4, val.length) 596 for (const item of val) encode(item) 597 } else if (typeof val === 'object') { 598 // Sort keys for deterministic encoding (DAG-CBOR style) ··· 603 if (a.length !== b.length) return a.length - b.length 604 return a < b ? -1 : a > b ? 1 : 0 605 }) 606 - encodeHead(5, keys.length) 607 for (const key of keys) { 608 // Encode key as text string 609 const keyBytes = new TextEncoder().encode(key) 610 - encodeHead(3, keyBytes.length) 611 parts.push(...keyBytes) 612 // Encode value 613 encode(val[key]) 614 } 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 } 627 } 628
··· 22 // === CBOR ENCODING === 23 // Minimal deterministic CBOR (RFC 8949) - sorted keys, minimal integers 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 + 49 export function cborEncode(value) { 50 const parts = [] 51 ··· 60 encodeInteger(val) 61 } else if (typeof val === 'string') { 62 const bytes = new TextEncoder().encode(val) 63 + encodeHead(parts, 3, bytes.length) // major type 3 = text string 64 parts.push(...bytes) 65 } else if (val instanceof Uint8Array) { 66 + encodeHead(parts, 2, val.length) // major type 2 = byte string 67 parts.push(...val) 68 } else if (Array.isArray(val)) { 69 + encodeHead(parts, 4, val.length) // major type 4 = array 70 for (const item of val) encode(item) 71 } else if (typeof val === 'object') { 72 // Sort keys for deterministic encoding 73 const keys = Object.keys(val).sort() 74 + encodeHead(parts, 5, keys.length) // major type 5 = map 75 for (const key of keys) { 76 encode(key) 77 encode(val[key]) ··· 79 } 80 } 81 82 function encodeInteger(n) { 83 if (n >= 0) { 84 + encodeHead(parts, 0, n) // major type 0 = unsigned int 85 } else { 86 + encodeHead(parts, 1, -n - 1) // major type 1 = negative int 87 } 88 } 89 ··· 104 parts.push(CBOR_FALSE) 105 } else if (typeof val === 'number') { 106 if (Number.isInteger(val) && val >= 0) { 107 + encodeHead(parts, 0, val) 108 } else if (Number.isInteger(val) && val < 0) { 109 + encodeHead(parts, 1, -val - 1) 110 } 111 } else if (typeof val === 'string') { 112 const bytes = new TextEncoder().encode(val) 113 + encodeHead(parts, 3, bytes.length) 114 parts.push(...bytes) 115 } else if (val instanceof CID) { 116 // CID - encode with CBOR tag 42 + 0x00 prefix 117 parts.push(0xd8, CBOR_TAG_CID) 118 + encodeHead(parts, 2, val.bytes.length + 1) // +1 for 0x00 prefix 119 parts.push(0x00) // multibase identity prefix 120 parts.push(...val.bytes) 121 } else if (val instanceof Uint8Array) { 122 // Regular byte string 123 + encodeHead(parts, 2, val.length) 124 parts.push(...val) 125 } else if (Array.isArray(val)) { 126 + encodeHead(parts, 4, val.length) 127 for (const item of val) encode(item) 128 } else if (typeof val === 'object') { 129 // DAG-CBOR: sort keys by length first, then lexicographically ··· 132 if (a.length !== b.length) return a.length - b.length 133 return a < b ? -1 : a > b ? 1 : 0 134 }) 135 + encodeHead(parts, 5, keys.length) 136 for (const key of keys) { 137 const keyBytes = new TextEncoder().encode(key) 138 + encodeHead(parts, 3, keyBytes.length) 139 parts.push(...keyBytes) 140 encode(val[key]) 141 } 142 } 143 } 144 ··· 568 if (val === null || val === undefined) { 569 parts.push(CBOR_NULL) 570 } else if (typeof val === 'number') { 571 + encodeHead(parts, 0, val) // unsigned int 572 } else if (val instanceof CID) { 573 // CID - encode with CBOR tag 42 + 0x00 prefix (DAG-CBOR CID link) 574 parts.push(0xd8, CBOR_TAG_CID) 575 + encodeHead(parts, 2, val.bytes.length + 1) // +1 for 0x00 prefix 576 parts.push(0x00) // multibase identity prefix 577 parts.push(...val.bytes) 578 } else if (val instanceof Uint8Array) { 579 // Regular bytes 580 + encodeHead(parts, 2, val.length) 581 parts.push(...val) 582 } else if (Array.isArray(val)) { 583 + encodeHead(parts, 4, val.length) 584 for (const item of val) encode(item) 585 } else if (typeof val === 'object') { 586 // Sort keys for deterministic encoding (DAG-CBOR style) ··· 591 if (a.length !== b.length) return a.length - b.length 592 return a < b ? -1 : a > b ? 1 : 0 593 }) 594 + encodeHead(parts, 5, keys.length) 595 for (const key of keys) { 596 // Encode key as text string 597 const keyBytes = new TextEncoder().encode(key) 598 + encodeHead(parts, 3, keyBytes.length) 599 parts.push(...keyBytes) 600 // Encode value 601 encode(val[key]) 602 } 603 } 604 } 605