1/**
2 * Script to create a labeler DID on plc.directory
3 *
4 * Run with: node scripts/create-did.js <labeler-endpoint>
5 * Example: node scripts/create-did.js https://spam-labeler.nate-8fe.workers.dev
6 */
7
8import { secp256k1 } from '@noble/curves/secp256k1';
9import { sha256 } from '@noble/hashes/sha256';
10import * as cbor from '@ipld/dag-cbor';
11import { base32 } from 'multiformats/bases/base32';
12import * as uint8arrays from 'uint8arrays';
13
14const PLC_DIRECTORY = 'https://plc.directory';
15
16// Generate a new secp256k1 keypair
17function generateKeypair() {
18 const privateKey = secp256k1.utils.randomPrivateKey();
19 const publicKey = secp256k1.getPublicKey(privateKey, true); // compressed
20 return { privateKey, publicKey };
21}
22
23// Convert public key to did:key format for secp256k1
24function publicKeyToDidKey(publicKey) {
25 // secp256k1 multicodec prefix is 0xe7 0x01
26 const multicodec = new Uint8Array([0xe7, 0x01, ...publicKey]);
27 const encoded = uint8arrays.toString(multicodec, 'base58btc');
28 return `did:key:z${encoded}`;
29}
30
31// Sign an operation with secp256k1
32async function signOperation(unsigned, privateKey) {
33 const bytes = cbor.encode(unsigned);
34 const hash = sha256(bytes);
35 const sig = secp256k1.sign(hash, privateKey, { lowS: true });
36 const sigBytes = sig.toCompactRawBytes();
37 const sigB64 = uint8arrays.toString(sigBytes, 'base64url');
38 return { ...unsigned, sig: sigB64 };
39}
40
41// Compute DID from genesis operation
42async function didForCreateOp(op) {
43 const bytes = cbor.encode(op);
44 const hash = sha256(bytes);
45 const hashB32 = uint8arrays.toString(hash, 'base32');
46 const truncated = hashB32.slice(0, 24);
47 return `did:plc:${truncated}`;
48}
49
50async function main() {
51 const endpoint = process.argv[2];
52 if (!endpoint) {
53 console.error('Usage: node scripts/create-did.js <labeler-endpoint>');
54 console.error('Example: node scripts/create-did.js https://spam-labeler.nate-8fe.workers.dev');
55 process.exit(1);
56 }
57
58 console.log('Generating secp256k1 keypair...');
59 const { privateKey, publicKey } = generateKeypair();
60 const didKey = publicKeyToDidKey(publicKey);
61
62 console.log('Creating labeler DID operation...');
63 const unsigned = {
64 type: 'plc_operation',
65 alsoKnownAs: [],
66 rotationKeys: [didKey],
67 verificationMethods: {
68 atproto_label: didKey,
69 },
70 services: {
71 atproto_labeler: {
72 type: 'AtprotoLabeler',
73 endpoint: endpoint,
74 },
75 },
76 prev: null,
77 };
78
79 const signed = await signOperation(unsigned, privateKey);
80 const did = await didForCreateOp(signed);
81
82 console.log('\n=== LABELER CREDENTIALS ===\n');
83 console.log('DID:', did);
84 console.log('Private Key (hex):', uint8arrays.toString(privateKey, 'hex'));
85 console.log('Public Key (did:key):', didKey);
86 console.log('\nEndpoint:', endpoint);
87
88 console.log('\n=== SUBMITTING TO PLC DIRECTORY ===\n');
89
90 try {
91 const res = await fetch(`${PLC_DIRECTORY}/${did}`, {
92 method: 'POST',
93 headers: { 'Content-Type': 'application/json' },
94 body: JSON.stringify(signed),
95 });
96
97 if (res.ok) {
98 console.log('SUCCESS! DID created on plc.directory');
99 console.log('\nVerify at:', `${PLC_DIRECTORY}/${did}`);
100
101 console.log('\n=== NEXT STEPS ===\n');
102 console.log('1. Add to wrangler.toml:');
103 console.log(` LABELER_DID = "${did}"`);
104 console.log('\n2. Set the secret:');
105 console.log(` wrangler secret put SIGNING_KEY`);
106 console.log(` (paste the private key hex when prompted)`);
107 } else {
108 const text = await res.text();
109 console.error('Failed to create DID:', res.status, text);
110 }
111 } catch (err) {
112 console.error('Error:', err.message);
113 }
114}
115
116main();