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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
159 lines · 4.1 KB Raw History
// test/helpers/identity.js
// Proper identity setup with PLC registration for e2e tests

import {
  base32Encode,
  base64UrlEncode,
  bytesToHex,
  generateKeyPair,
  importPrivateKey,
  sign,
} from '@pdsjs/core/crypto';
import { cborEncodeDagCbor } from '@pdsjs/core/repo';

const PLC_URL = 'http://localhost:2582';

// Multicodec prefix for P-256 public key (0x1200)
const P256_MULTICODEC = new Uint8Array([0x80, 0x24]);

/**
 * @param {Uint8Array} bytes
 * @returns {string}
 */
function base58btcEncode(bytes) {
  const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
  let zeros = 0;
  for (const b of bytes) {
    if (b === 0) zeros++;
    else break;
  }
  const digits = [0];
  for (const byte of bytes) {
    let carry = byte;
    for (let i = 0; i < digits.length; i++) {
      carry += digits[i] << 8;
      digits[i] = carry % 58;
      carry = (carry / 58) | 0;
    }
    while (carry > 0) {
      digits.push(carry % 58);
      carry = (carry / 58) | 0;
    }
  }
  let result = '1'.repeat(zeros);
  for (let i = digits.length - 1; i >= 0; i--) {
    result += ALPHABET[digits[i]];
  }
  return result;
}

/**
 * @param {Uint8Array} compressedPublicKey
 * @returns {string}
 */
function publicKeyToDidKey(compressedPublicKey) {
  const keyWithCodec = new Uint8Array(
    P256_MULTICODEC.length + compressedPublicKey.length,
  );
  keyWithCodec.set(P256_MULTICODEC);
  keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length);
  return `did:key:z${base58btcEncode(keyWithCodec)}`;
}

/**
 * @param {Uint8Array} data
 * @returns {Promise<Uint8Array>}
 */
async function sha256(data) {
  const hash = await crypto.subtle.digest(
    'SHA-256',
    /** @type {BufferSource} */ (data),
  );
  return new Uint8Array(hash);
}

/**
 * @param {Record<string, unknown>} operation
 * @param {CryptoKey} cryptoKey
 * @returns {Promise<string>}
 */
async function signPlcOperation(operation, cryptoKey) {
  const { sig, ...opWithoutSig } = operation;
  const encoded = cborEncodeDagCbor(opWithoutSig);
  const signature = await sign(cryptoKey, encoded);
  return base64UrlEncode(signature);
}

/**
 * @param {Record<string, unknown>} operation
 * @returns {Promise<string>}
 */
async function deriveDidFromOperation(operation) {
  const encoded = cborEncodeDagCbor(operation);
  const hash = await sha256(encoded);
  return `did:plc:${base32Encode(hash.slice(0, 15))}`;
}

/**
 * Create a properly registered identity for e2e tests
 * @param {object} opts
 * @param {string} opts.pdsUrl - PDS URL (e.g., 'https://host.docker.internal:3443')
 * @param {string} opts.handle - Handle subdomain (e.g., 'test')
 * @returns {Promise<{did: string, privateKeyHex: string, handle: string}>}
 */
export async function createTestIdentity(opts) {
  const { pdsUrl, handle } = opts;

  // Generate keypair
  const keyPair = await generateKeyPair();
  const cryptoKey = await importPrivateKey(keyPair.privateKey);
  const didKey = publicKeyToDidKey(keyPair.publicKey);

  // Build full handle (use hostname, not host, to exclude port)
  const pdsHostname = new URL(pdsUrl).hostname;
  const fullHandle = handle ? `${handle}.${pdsHostname}` : pdsHostname;

  // Create genesis operation
  /** @type {Record<string, unknown>} */
  const operation = {
    type: 'plc_operation',
    rotationKeys: [didKey],
    verificationMethods: {
      atproto: didKey,
    },
    alsoKnownAs: [`at://${fullHandle}`],
    services: {
      atproto_pds: {
        type: 'AtprotoPersonalDataServer',
        endpoint: pdsUrl,
      },
    },
    prev: null,
  };

  // Sign the operation
  operation.sig = await signPlcOperation(operation, cryptoKey);

  // Derive DID from operation
  const did = await deriveDidFromOperation(operation);

  // Register with local PLC
  const plcRes = await fetch(`${PLC_URL}/${encodeURIComponent(did)}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(operation),
  });

  if (!plcRes.ok) {
    const text = await plcRes.text();
    throw new Error(`PLC registration failed: ${plcRes.status} ${text}`);
  }

  return {
    did,
    privateKeyHex: bytesToHex(keyPair.privateKey),
    handle: fullHandle,
  };
}

export { PLC_URL };