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