// @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} operation - Operation to sign * @param {import('./crypto.js').SigningKey} signingKey - Account signing key * @returns {Promise>} 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} */ export async function computePlcOperationCid(operation) { return cidToString(await createCid(cborEncodeDagCbor(operation))); }