diff --git a/src/pds.js b/src/pds.js index 141bff5..1ba34ce 100644 --- a/src/pds.js +++ b/src/pds.js @@ -59,6 +59,59 @@ function cborEncode(value) { return new Uint8Array(parts) } +function cborDecode(bytes) { + let offset = 0 + + function read() { + const initial = bytes[offset++] + const major = initial >> 5 + const info = initial & 0x1f + + let length = info + if (info === 24) length = bytes[offset++] + else if (info === 25) { length = (bytes[offset++] << 8) | bytes[offset++] } + else if (info === 26) { + length = (bytes[offset++] << 24) | (bytes[offset++] << 16) | (bytes[offset++] << 8) | bytes[offset++] + } + + switch (major) { + case 0: return length // unsigned int + case 1: return -1 - length // negative int + case 2: { // byte string + const data = bytes.slice(offset, offset + length) + offset += length + return data + } + case 3: { // text string + const data = new TextDecoder().decode(bytes.slice(offset, offset + length)) + offset += length + return data + } + case 4: { // array + const arr = [] + for (let i = 0; i < length; i++) arr.push(read()) + return arr + } + case 5: { // map + const obj = {} + for (let i = 0; i < length; i++) { + const key = read() + obj[key] = read() + } + return obj + } + case 7: { // special + if (info === 20) return false + if (info === 21) return true + if (info === 22) return null + return undefined + } + } + } + + return read() +} + // === CID GENERATION === // dag-cbor (0x71) + sha-256 (0x12) + 32 bytes @@ -534,6 +587,31 @@ export class PersonalDataServer { return Response.json({ error: err.message }, { status: 500 }) } } + if (url.pathname === '/xrpc/com.atproto.repo.getRecord') { + const collection = url.searchParams.get('collection') + const rkey = url.searchParams.get('rkey') + + if (!collection || !rkey) { + return Response.json({ error: 'missing collection or rkey' }, { status: 400 }) + } + + const did = await this.getDid() + const uri = `at://${did}/${collection}/${rkey}` + + const rows = this.sql.exec( + `SELECT cid, value FROM records WHERE uri = ?`, uri + ).toArray() + + if (rows.length === 0) { + return Response.json({ error: 'record not found' }, { status: 404 }) + } + + const row = rows[0] + // Decode CBOR for response + const value = cborDecode(row.value) + + return Response.json({ uri, cid: row.cid, value }) + } return new Response('pds running', { status: 200 }) } } @@ -555,7 +633,7 @@ export default { // Export utilities for testing export { - cborEncode, createCid, cidToString, base32Encode, createTid, + cborEncode, cborDecode, createCid, cidToString, base32Encode, createTid, generateKeyPair, importPrivateKey, sign, bytesToHex, hexToBytes, getKeyDepth } diff --git a/test/pds.test.js b/test/pds.test.js index 1c75291..fb142d9 100644 --- a/test/pds.test.js +++ b/test/pds.test.js @@ -1,7 +1,7 @@ import { test, describe } from 'node:test' import assert from 'node:assert' import { - cborEncode, createCid, cidToString, base32Encode, createTid, + cborEncode, cborDecode, createCid, cidToString, base32Encode, createTid, generateKeyPair, importPrivateKey, sign, bytesToHex, hexToBytes, getKeyDepth } from '../src/pds.js' @@ -221,3 +221,47 @@ describe('MST Key Depth', () => { assert.ok(depth >= 0) }) }) + +describe('CBOR Decoding', () => { + test('decodes what encode produces (roundtrip)', () => { + const original = { hello: 'world', num: 42 } + const encoded = cborEncode(original) + const decoded = cborDecode(encoded) + assert.deepStrictEqual(decoded, original) + }) + + test('decodes null', () => { + const encoded = cborEncode(null) + const decoded = cborDecode(encoded) + assert.strictEqual(decoded, null) + }) + + test('decodes booleans', () => { + assert.strictEqual(cborDecode(cborEncode(true)), true) + assert.strictEqual(cborDecode(cborEncode(false)), false) + }) + + test('decodes integers', () => { + assert.strictEqual(cborDecode(cborEncode(0)), 0) + assert.strictEqual(cborDecode(cborEncode(42)), 42) + assert.strictEqual(cborDecode(cborEncode(255)), 255) + assert.strictEqual(cborDecode(cborEncode(-1)), -1) + assert.strictEqual(cborDecode(cborEncode(-10)), -10) + }) + + test('decodes strings', () => { + assert.strictEqual(cborDecode(cborEncode('hello')), 'hello') + assert.strictEqual(cborDecode(cborEncode('')), '') + }) + + test('decodes arrays', () => { + assert.deepStrictEqual(cborDecode(cborEncode([1, 2, 3])), [1, 2, 3]) + assert.deepStrictEqual(cborDecode(cborEncode([])), []) + }) + + test('decodes nested structures', () => { + const original = { arr: [1, { nested: true }], str: 'test' } + const decoded = cborDecode(cborEncode(original)) + assert.deepStrictEqual(decoded, original) + }) +})