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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
161 lines · 4.5 KB Raw History
// @pdsjs/core/plc - did:plc operation construction and signing
// Shared by the PDS identity endpoints and scripts/setup.js

import {
  base64UrlEncode,
  P256_MULTICODEC,
  SECP256K1_MULTICODEC,
  sign,
} from './crypto.js';
import { cborEncodeDagCbor, cidToString, createCid } from './repo.js';

export { P256_MULTICODEC, SECP256K1_MULTICODEC };

const BASE58BTC_ALPHABET =
  '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

/**
 * Encode bytes as base58btc (Bitcoin alphabet).
 * @param {Uint8Array} bytes
 * @returns {string}
 */
export function base58btcEncode(bytes) {
  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 += BASE58BTC_ALPHABET[digits[i]];
  }
  return result;
}

/**
 * Decode a base58btc (Bitcoin alphabet) string to bytes.
 * @param {string} str
 * @returns {Uint8Array}
 */
export function base58btcDecode(str) {
  let zeros = 0;
  for (const c of str) {
    if (c === '1') zeros++;
    else break;
  }

  const bytes = [0];
  for (const char of str) {
    const value = BASE58BTC_ALPHABET.indexOf(char);
    if (value === -1) {
      throw new Error(`invalid base58btc character: ${char}`);
    }
    let carry = value;
    for (let i = 0; i < bytes.length; i++) {
      carry += bytes[i] * 58;
      bytes[i] = carry & 0xff;
      carry >>= 8;
    }
    while (carry > 0) {
      bytes.push(carry & 0xff);
      carry >>= 8;
    }
  }

  const result = new Uint8Array(zeros + bytes.length);
  result.set(new Uint8Array(zeros), 0);
  for (let i = 0; i < bytes.length; i++) {
    result[zeros + i] = bytes[bytes.length - 1 - i];
  }
  return result;
}

/**
 * Encode a compressed public key as a did:key.
 * @param {Uint8Array} compressedPublicKey - 33-byte compressed point
 * @param {'p256'|'secp256k1'} [curve] - Curve of the key (default p256)
 * @returns {string} did:key identifier
 */
export function publicKeyToDidKey(compressedPublicKey, curve = 'p256') {
  const codec = curve === 'secp256k1' ? SECP256K1_MULTICODEC : P256_MULTICODEC;
  const keyWithCodec = new Uint8Array(
    codec.length + compressedPublicKey.length,
  );
  keyWithCodec.set(codec);
  keyWithCodec.set(compressedPublicKey, codec.length);
  return `did:key:z${base58btcEncode(keyWithCodec)}`;
}

/**
 * Build an unsigned PLC operation.
 * @param {Object} opts
 * @param {string} opts.didKey - Signing/rotation key as did:key
 * @param {string} opts.handle - Handle (without the at:// prefix)
 * @param {string} opts.pdsEndpoint - PDS service endpoint URL
 * @param {string|null} [opts.prev] - CID of the previous operation, null for genesis
 * @param {string[]} [opts.rotationKeys] - Override rotation keys
 * @param {string} [opts.kemDidKey] - The account's `#atproto_kem` key
 * @returns {Object} Unsigned plc_operation
 */
export function buildPlcOperation({
  didKey,
  handle,
  pdsEndpoint,
  prev = null,
  rotationKeys,
  kemDidKey,
}) {
  return {
    type: 'plc_operation',
    rotationKeys: rotationKeys ?? [didKey],
    verificationMethods: kemDidKey
      ? { atproto: didKey, atproto_kem: kemDidKey }
      : { atproto: didKey },
    alsoKnownAs: [`at://${handle}`],
    services: {
      atproto_pds: {
        type: 'AtprotoPersonalDataServer',
        endpoint: pdsEndpoint,
      },
    },
    prev,
  };
}

/**
 * Sign a PLC operation, returning a new object with the `sig` field.
 * The signature covers the DAG-CBOR encoding of the operation without `sig`.
 * @param {Record<string, *>} operation - Operation to sign
 * @param {import('./crypto.js').SigningKey} signingKey - Account signing key
 * @returns {Promise<Record<string, *>>} Operation with base64url `sig`
 */
export async function signPlcOperationObject(operation, signingKey) {
  const { sig: _existing, ...unsigned } = operation;
  const encoded = cborEncodeDagCbor(unsigned);
  const signature = await sign(signingKey, encoded);
  return { ...unsigned, sig: base64UrlEncode(signature) };
}

/**
 * Compute the CID of a PLC operation (CIDv1, dag-cbor, sha256).
 * @param {Object} operation
 * @returns {Promise<string>}
 */
export async function computePlcOperationCid(operation) {
  return cidToString(await createCid(cborEncodeDagCbor(operation)));
}