this repo has no description
1import { 2 defs, 3 type IndexedEntry, 4 normalizeOp, 5 type Operation, 6} from "@atcute/did-plc"; 7import { 8 P256PrivateKey, 9 parsePrivateMultikey, 10 Secp256k1PrivateKey, 11 Secp256k1PrivateKeyExportable, 12} from "@atcute/crypto"; 13import * as CBOR from "@atcute/cbor"; 14import { fromBase16, toBase64Url } from "@atcute/multibase"; 15 16export type PrivateKey = P256PrivateKey | Secp256k1PrivateKey; 17 18export interface KeypairInfo { 19 type: "private_key"; 20 didPublicKey: `did:key:${string}`; 21 keypair: PrivateKey; 22} 23 24export interface PlcService { 25 type: string; 26 endpoint: string; 27} 28 29export interface PlcOperationData { 30 type: "plc_operation"; 31 prev: string; 32 alsoKnownAs: string[]; 33 rotationKeys: string[]; 34 services: Record<string, PlcService>; 35 verificationMethods: Record<string, string>; 36 sig?: string; 37} 38 39const jsonToB64Url = (obj: unknown): string => { 40 const enc = new TextEncoder(); 41 const json = JSON.stringify(obj); 42 return toBase64Url(enc.encode(json)); 43}; 44 45export class PlcOps { 46 private plcDirectoryUrl: string; 47 48 constructor(plcDirectoryUrl = "https://plc.directory") { 49 this.plcDirectoryUrl = plcDirectoryUrl; 50 } 51 52 async getPlcAuditLogs(did: string): Promise<IndexedEntry[]> { 53 const response = await fetch(`${this.plcDirectoryUrl}/${did}/log/audit`); 54 if (!response.ok) { 55 throw new Error(`Failed to fetch PLC audit logs: ${response.status}`); 56 } 57 const json = await response.json(); 58 return defs.indexedEntryLog.parse(json); 59 } 60 61 async getLastPlcOpFromPlc( 62 did: string, 63 ): Promise<{ lastOperation: Operation; base: IndexedEntry }> { 64 const logs = await this.getPlcAuditLogs(did); 65 const lastOp = logs.at(-1); 66 if (!lastOp) { 67 throw new Error("No PLC operations found for this DID"); 68 } 69 return { lastOperation: normalizeOp(lastOp.operation), base: lastOp }; 70 } 71 72 async getCurrentRotationKeysForUser(did: string): Promise<string[]> { 73 const { lastOperation } = await this.getLastPlcOpFromPlc(did); 74 return lastOperation.rotationKeys || []; 75 } 76 77 async createNewSecp256k1Keypair(): Promise< 78 { privateKey: string; publicKey: `did:key:${string}` } 79 > { 80 const keypair = await Secp256k1PrivateKeyExportable.createKeypair(); 81 const publicKey = await keypair.exportPublicKey("did"); 82 const privateKey = await keypair.exportPrivateKey("multikey"); 83 return { privateKey, publicKey }; 84 } 85 86 async getKeyPair( 87 privateKeyString: string, 88 type: "secp256k1" | "p256" = "secp256k1", 89 ): Promise<KeypairInfo> { 90 const HEX_REGEX = /^[0-9a-f]+$/i; 91 const MULTIKEY_REGEX = /^z[a-km-zA-HJ-NP-Z1-9]+$/; 92 let keypair: PrivateKey | undefined; 93 94 const trimmed = privateKeyString.trim(); 95 96 if (HEX_REGEX.test(trimmed) && trimmed.length === 64) { 97 const privateKeyBytes = fromBase16(trimmed); 98 if (type === "p256") { 99 keypair = await P256PrivateKey.importRaw(privateKeyBytes); 100 } else { 101 keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes); 102 } 103 } else if (MULTIKEY_REGEX.test(trimmed)) { 104 const match = parsePrivateMultikey(trimmed); 105 const privateKeyBytes = match.privateKeyBytes; 106 if (match.type === "p256") { 107 keypair = await P256PrivateKey.importRaw(privateKeyBytes); 108 } else if (match.type === "secp256k1") { 109 keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes); 110 } else { 111 throw new Error(`Unsupported key type: ${match.type}`); 112 } 113 } else { 114 throw new Error( 115 "Invalid key format. Expected 64-char hex or multikey format.", 116 ); 117 } 118 119 if (!keypair) { 120 throw new Error("Failed to parse private key"); 121 } 122 123 return { 124 type: "private_key", 125 didPublicKey: await keypair.exportPublicKey("did"), 126 keypair, 127 }; 128 } 129 130 async signAndPublishNewOp( 131 did: string, 132 signingRotationKey: PrivateKey, 133 alsoKnownAs: string[], 134 rotationKeys: string[], 135 pds: string, 136 verificationKey: string, 137 prev: string, 138 ): Promise<void> { 139 const rotationKeysToUse = [...new Set(rotationKeys)]; 140 if (rotationKeysToUse.length === 0) { 141 throw new Error("No rotation keys provided"); 142 } 143 if (rotationKeysToUse.length > 5) { 144 throw new Error("Maximum 5 rotation keys allowed"); 145 } 146 147 const operation: PlcOperationData = { 148 type: "plc_operation", 149 prev, 150 alsoKnownAs, 151 rotationKeys: rotationKeysToUse, 152 services: { 153 atproto_pds: { 154 type: "AtprotoPersonalDataServer", 155 endpoint: pds, 156 }, 157 }, 158 verificationMethods: { 159 atproto: verificationKey, 160 }, 161 }; 162 163 const opBytes = CBOR.encode(operation); 164 const sigBytes = await signingRotationKey.sign(opBytes); 165 const signature = toBase64Url(sigBytes); 166 167 const signedOperation = { 168 ...operation, 169 sig: signature, 170 }; 171 172 await this.pushPlcOperation(did, signedOperation); 173 } 174 175 async pushPlcOperation( 176 did: string, 177 operation: PlcOperationData, 178 ): Promise<void> { 179 const response = await fetch(`${this.plcDirectoryUrl}/${did}`, { 180 method: "POST", 181 headers: { 182 "Content-Type": "application/json", 183 }, 184 body: JSON.stringify(operation), 185 }); 186 187 if (!response.ok) { 188 const contentType = response.headers.get("content-type"); 189 if (contentType?.includes("application/json")) { 190 const json = await response.json(); 191 if ( 192 typeof json === "object" && json !== null && 193 typeof json.message === "string" 194 ) { 195 throw new Error(json.message); 196 } 197 } 198 throw new Error(`PLC directory returned HTTP ${response.status}`); 199 } 200 } 201 202 async createServiceAuthToken( 203 iss: string, 204 aud: string, 205 keypair: PrivateKey, 206 lxm: string, 207 ): Promise<string> { 208 const iat = Math.floor(Date.now() / 1000); 209 const exp = iat + 60; 210 211 const jti = (() => { 212 const bytes = new Uint8Array(16); 213 crypto.getRandomValues(bytes); 214 return Array.from(bytes) 215 .map((b) => b.toString(16).padStart(2, "0")) 216 .join(""); 217 })(); 218 219 const header = { typ: "JWT", alg: "ES256K" }; 220 const payload = { iat, iss, aud, exp, lxm, jti }; 221 222 const headerB64 = jsonToB64Url(header); 223 const payloadB64 = jsonToB64Url(payload); 224 const toSignStr = `${headerB64}.${payloadB64}`; 225 226 const toSignBytes = new TextEncoder().encode(toSignStr); 227 const sigBytes = await keypair.sign(toSignBytes); 228 const sigB64 = toBase64Url(sigBytes); 229 230 return `${toSignStr}.${sigB64}`; 231 } 232 233 async signPlcOperationWithCredentials( 234 did: string, 235 signingKey: PrivateKey, 236 credentials: { 237 rotationKeys?: string[]; 238 alsoKnownAs?: string[]; 239 verificationMethods?: Record<string, string>; 240 services?: Record<string, PlcService>; 241 }, 242 additionalRotationKeys: string[], 243 prevCid: string, 244 ): Promise<void> { 245 const rotationKeys = [ 246 ...new Set([ 247 ...(additionalRotationKeys || []), 248 ...(credentials.rotationKeys || []), 249 ]), 250 ]; 251 252 if (rotationKeys.length === 0) { 253 throw new Error("No rotation keys provided"); 254 } 255 if (rotationKeys.length > 5) { 256 throw new Error("Maximum 5 rotation keys allowed"); 257 } 258 259 const operation: PlcOperationData = { 260 type: "plc_operation", 261 prev: prevCid, 262 alsoKnownAs: credentials.alsoKnownAs || [], 263 rotationKeys, 264 services: credentials.services || {}, 265 verificationMethods: credentials.verificationMethods || {}, 266 }; 267 268 const opBytes = CBOR.encode(operation); 269 const sigBytes = await signingKey.sign(opBytes); 270 const signature = toBase64Url(sigBytes); 271 272 const signedOperation = { 273 ...operation, 274 sig: signature, 275 }; 276 277 await this.pushPlcOperation(did, signedOperation); 278 } 279} 280 281export const plcOps = new PlcOps();