~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/atproto

atproto's primitives in Rust, small and free of I/O

0
0
main
156 lines · 5.7 KB Raw History
// Records what the space alpha's @atproto/space makes of a space meta
// archive (exportSpaceMeta's, which an account's server serves as it
// moves), into space-meta.json beside this: two archives it writes, and
// archives it refuses, each with its refusal. The refused ones are its
// own tests' (space-meta-archive.test.ts, star.test.ts), hand-built as
// they build them.
//
//   node space-meta.mjs <a directory with @atproto/space installed> > space-meta.json
import { pathToFileURL } from 'node:url'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'

const dir = process.argv[2]
const load = (pkg) => import(pathToFileURL(join(dir, 'node_modules', pkg, 'dist/index.js')).href)
const space = await load('@atproto/space')
const { encode } = await load('@atproto/lex-cbor')
const version = JSON.parse(readFileSync(join(dir, 'node_modules/@atproto/space/package.json'))).version

const collect = async (stream) => {
  const chunks = []
  for await (const chunk of stream) chunks.push(chunk)
  return Buffer.concat(chunks)
}
const hex = (b) => Buffer.from(b).toString('hex')
const utf8 = (s) => new TextEncoder().encode(s)
const varint = (n) => {
  const out = []
  while (n >= 0x80) {
    out.push((n & 0x7f) | 0x80)
    n = Math.floor(n / 128)
  }
  out.push(n)
  return Buffer.from(out)
}

const SPACE = 'at://did:plc:harborcycling/space/com.example.forum/general'
const ALICE = 'did:plc:alice'
const LOCAL = 'did:web:localhost%3A2583'
const POLICY = { $type: 'com.atproto.memberspace.policy', publicRead: true, publicWrite: false }
const member = (rkey, read, write) => ({
  key: `com.atproto.memberspace.member/${rkey}`,
  value: { $type: 'com.atproto.memberspace.member', read, write },
})
const WRITER = { rev: '3kbcq3p7ad42b', data: new Uint8Array(32).fill(1), spaceRev: '3kbcq3p7ad42c' }

// What readSpaceMetaStar reads back, as JSON: bytes as hex.
const plain = (archive) => ({
  space: archive.space,
  policy: archive.policy,
  policyState: archive.policyState,
  writers: archive.writers.map((w) => ({ ...w, data: hex(w.data) })),
})

async function written(archive) {
  const bytes = await collect(space.serializeSpaceMeta(archive))
  return { hex: hex(bytes), read: plain(await space.readSpaceMetaStar([bytes])) }
}

// A member's key holds its DID as a record key: a localhost did:web's
// port unencoded (the PDS's didToRecordKey).
const full = await written({
  space: SPACE,
  policy: POLICY,
  policyState: [member('did:web:localhost:2583', true, false), member(ALICE, true, true)],
  writers: [{ did: LOCAL, ...WRITER }, { did: ALICE, ...WRITER }],
})
const empty = await written({
  space: SPACE,
  policy: { $type: 'com.atproto.personalspace.policy' },
  policyState: [],
  writers: [],
})

const META = { $type: 'atproto-space-meta', ver: 1, space: SPACE }
const header = (meta) => Buffer.concat([space.STAR_MAGIC, varint(meta.length), meta])
const pair = (key, value) => Buffer.concat([varint(key.length), key, varint(value.length), value])
// An archive of `pairs`, each key's value encoded, under `meta`.
const raw = (pairs, meta = {}) =>
  Buffer.concat([
    header(encode({ ...META, ...meta })),
    ...pairs.map(([k, v]) => pair(utf8(k), encode(v))),
  ])
const { spaceRev: _, ...noSpaceRev } = WRITER
const memberValue = member(ALICE, true, true).value

const refusals = {
  'bad magic': Buffer.concat([Buffer.from([0x3a, 0x6c, 0x01, 0x00]), raw([['policy', POLICY]]).subarray(4)]),
  'a non-canonical varint': (() => {
    const meta = encode(META)
    return Buffer.concat([space.STAR_MAGIC, Buffer.from([meta.length | 0x80, 0x00]), meta])
  })(),
  'metadata out of canonical order': header(
    Buffer.from([0xa2, 0x65, ...utf8('$type'), 0x61, 0x74, 0x61, 0x61, 0x01]),
  ),
  'metadata without a $type': header(encode({ a: 1 })),
  'bytes after the metadata': header(Buffer.concat([encode(META), Buffer.from([0])])),
  'a pair cut short': Buffer.concat([header(encode(META)), varint(1), Buffer.from([1])]),
  'a missing policy': raw([[`com.atproto.memberspace.member/${ALICE}`, memberValue]]),
  'state before the policy': raw([
    [`com.atproto.memberspace.member/${ALICE}`, memberValue],
    ['policy', POLICY],
  ]),
  'state after the writers': raw([
    ['policy', POLICY],
    [`writer/${ALICE}`, WRITER],
    [`com.atproto.memberspace.member/${ALICE}`, memberValue],
  ]),
  'a key twice': raw([
    ['policy', POLICY],
    [`com.atproto.memberspace.member/${ALICE}`, memberValue],
    [`com.atproto.memberspace.member/${ALICE}`, memberValue],
  ]),
  'the policy twice': raw([
    ['policy', POLICY],
    ['policy', POLICY],
  ]),
  'keys out of order': raw([
    ['policy', POLICY],
    ['writer/did:plc:carol', WRITER],
    ['writer/did:plc:bob', WRITER],
  ]),
  'a writer key without a DID': raw([
    ['policy', POLICY],
    ['writer/bob', WRITER],
  ]),
  'a writer without its space revision': raw([
    ['policy', POLICY],
    [`writer/${ALICE}`, noSpaceRev],
  ]),
  'a writer whose revision is no TID': raw([
    ['policy', POLICY],
    [`writer/${ALICE}`, { ...WRITER, rev: 'yesterday' }],
  ]),
  "a repo's archive": raw([['policy', POLICY]], { $type: 'atproto-space-repo' }),
  'a version to come': raw([['policy', POLICY]], { ver: 2 }),
  'no space': raw([['policy', POLICY]], { space: 'general' }),
  'a value that is no map': Buffer.concat([header(encode(META)), pair(utf8('policy'), encode('memberspace'))]),
}

const refused = []
for (const [why, bytes] of Object.entries(refusals)) {
  const error = await space.readSpaceMetaStar([bytes]).then(
    () => null,
    (e) => e.message,
  )
  if (!error) throw new Error(`the alpha reads ${why}`)
  refused.push({ why, hex: hex(bytes), error })
}

console.log(
  JSON.stringify(
    { generatedFrom: { repo: 'bluesky-social/atproto', package: `@atproto/space@${version}` }, full, empty, refused },
    null,
    2,
  ),
)