chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @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)));
}