self::OP_TYPE, 'rotationKeys' => $rotationKeys, 'verificationMethods' => [ 'atproto' => $signingKey, ], 'alsoKnownAs' => [ 'at://' . $handle, ], 'services' => [ 'atproto_pds' => [ 'type' => 'AtprotoPersonalDataServer', 'endpoint' => rtrim($pdsEndpoint, '/'), ], ], 'prev' => null, ]; return $this->signOp($unsigned, $signer); } public function signOp(array $unsignedOp, Keypair $signer): array { unset($unsignedOp['sig']); $bytes = $this->cbor->encode($unsignedOp); $sig = $signer->sign($bytes); $unsignedOp['sig'] = self::base64UrlEncode($sig); return $unsignedOp; } public function didForOp(array $signedOp): string { $bytes = $this->cbor->encode($signedOp); $hash = hash('sha256', $bytes, true); $b32 = Base32::encode($hash); return 'did:plc:' . substr($b32, 0, 24); } public function submit(string $did, array $signedOp): void { $url = rtrim($this->plcDirectoryUrl, '/') . '/' . rawurlencode($did); try { $response = $this->httpClient->request('POST', $url, [ 'json' => $signedOp, 'timeout' => 15, 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], ]); } catch (GuzzleException $e) { throw new PlcDirectoryClientException( "Failed to submit plcOp for {$did}: " . $e->getMessage(), 0, $e, ); } $status = $response->getStatusCode(); if ($status < 200 || $status >= 300) { throw new PlcDirectoryClientException( "PLC directory rejected operation for {$did}: HTTP {$status} {$response->getBody()}" ); } } public static function base64UrlEncode(string $bytes): string { return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='); } }