atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

Select the types of activity you want to include in your feed.

support pds moover rotation keys

zzstoatzz (May 26, 2026, 8:04 PM -0500) fbac513e e64bdc38

+70 -10
+1
.gitignore
··· 8 8 *.sqlite3-shm 9 9 *.sqlite3-wal 10 10 .env 11 + .env.*
+1 -1
README.md
··· 49 49 ZDS_BLOBSTORE_PATH=/var/lib/zds/blobs \ 50 50 ZDS_HANDLE_DOMAINS='.example.com,example.com' \ 51 51 ZDS_CRAWLERS='https://bsky.network,https://vsky.network' \ 52 - ZDS_PLC_ROTATION_KEY='64-hex-character-secp256k1-secret' \ 52 + ZDS_PLC_ROTATION_KEY='64-hex-secp256k1-secret-or-private-multikey' \ 53 53 ZDS_RECOVERY_DID_KEY='did:key:optionalRecoveryKey' \ 54 54 ZDS_JWT_SECRET='at-least-32-random-bytes-here' \ 55 55 ZDS_ADMIN_TOKEN='another-random-secret' \
+3 -4
docs/operations.md
··· 55 55 56 56 - `ZDS_PUBLIC_URL`: public PDS origin. 57 57 - `ZDS_SERVER_DID`: PDS service DID, usually `did:web:<host>`. 58 - - `ZDS_PLC_ROTATION_KEY`: 32-byte secp256k1 private key as 64 lowercase hex 59 - characters. New `did:plc` accounts use this as the PDS rotation authority, 60 - and recommended DID credentials return its `did:key` instead of reusing the 61 - account signing key. 58 + - `ZDS_PLC_ROTATION_KEY`: PDS rotation authority private key. Accepts the 59 + official PDS 32-byte secp256k1 hex form and the private multikey form emitted 60 + by PDS Moover. New `did:plc` accounts use this as the PDS rotation authority. 62 61 - `ZDS_RECOVERY_DID_KEY`: optional recovery `did:key` returned before the PDS 63 62 rotation key in recommended DID credentials. 64 63 - `ZDS_JWT_SECRET`: stable secret for access and refresh JWTs.
+64 -4
src/atproto/plc.zig
··· 9 9 }; 10 10 11 11 pub fn configuredRotationKeypair() !zat.Keypair { 12 - const hex = config.plcRotationKey() orelse return error.MissingPlcRotationKey; 13 - if (hex.len != 64) return error.InvalidPlcRotationKey; 12 + const key = config.plcRotationKey() orelse return error.MissingPlcRotationKey; 13 + return parsePrivateKey(std.heap.smp_allocator, key) catch error.InvalidPlcRotationKey; 14 + } 15 + 16 + pub fn parsePrivateKey(allocator: std.mem.Allocator, key: []const u8) !zat.Keypair { 17 + if (key.len == 64 and isHex(key)) { 18 + return parseHexPrivateKey(key); 19 + } 20 + if (key.len >= 2 and key[0] == 'z') { 21 + return parsePrivateMultikey(allocator, key); 22 + } 23 + return error.InvalidPrivateKey; 24 + } 25 + 26 + fn parseHexPrivateKey(hex: []const u8) !zat.Keypair { 14 27 var secret: [32]u8 = undefined; 15 - _ = std.fmt.hexToBytes(&secret, hex) catch return error.InvalidPlcRotationKey; 16 - return zat.Keypair.fromSecretKey(.secp256k1, secret) catch error.InvalidPlcRotationKey; 28 + _ = std.fmt.hexToBytes(&secret, hex) catch return error.InvalidPrivateKey; 29 + return zat.Keypair.fromSecretKey(.secp256k1, secret) catch error.InvalidPrivateKey; 30 + } 31 + 32 + fn parsePrivateMultikey(allocator: std.mem.Allocator, key: []const u8) !zat.Keypair { 33 + const decoded = try zat.multibase.decode(allocator, key); 34 + defer allocator.free(decoded); 35 + if (decoded.len != 34) return error.InvalidPrivateKey; 36 + 37 + const key_type: zat.multicodec.KeyType = if (decoded[0] == 0x81 and decoded[1] == 0x26) 38 + .secp256k1 39 + else if (decoded[0] == 0x86 and decoded[1] == 0x26) 40 + .p256 41 + else 42 + return error.InvalidPrivateKey; 43 + 44 + return zat.Keypair.fromSecretKey(key_type, decoded[2..34].*) catch error.InvalidPrivateKey; 45 + } 46 + 47 + fn isHex(input: []const u8) bool { 48 + for (input) |c| switch (c) { 49 + '0'...'9', 'a'...'f', 'A'...'F' => {}, 50 + else => return false, 51 + }; 52 + return true; 17 53 } 18 54 19 55 pub fn rotationDidKeys(allocator: std.mem.Allocator, rotation_key: *const zat.Keypair) ![]const []const u8 { ··· 204 240 const unsigned_bytes = try zat.cbor.encodeAlloc(a, unsigned); 205 241 try zat.multicodec.verifyDidKeySignature(a, did_key, unsigned_bytes, sig_bytes); 206 242 } 243 + 244 + test "PLC private key parser accepts official hex and PDS Moover private multikey" { 245 + const allocator = std.testing.allocator; 246 + const hex = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; 247 + const from_hex = try parsePrivateKey(allocator, hex); 248 + 249 + var secret: [32]u8 = undefined; 250 + _ = try std.fmt.hexToBytes(&secret, hex); 251 + var private_multikey_bytes: [34]u8 = undefined; 252 + private_multikey_bytes[0] = 0x81; 253 + private_multikey_bytes[1] = 0x26; 254 + @memcpy(private_multikey_bytes[2..], &secret); 255 + 256 + const private_multikey = try zat.multibase.encode(allocator, .base58btc, &private_multikey_bytes); 257 + defer allocator.free(private_multikey); 258 + 259 + const from_multikey = try parsePrivateKey(allocator, private_multikey); 260 + const hex_did = try from_hex.did(allocator); 261 + defer allocator.free(hex_did); 262 + const multikey_did = try from_multikey.did(allocator); 263 + defer allocator.free(multikey_did); 264 + 265 + try std.testing.expectEqualStrings(hex_did, multikey_did); 266 + }
+1 -1
src/internal/cli.zig
··· 80 80 pub fn usage() void { 81 81 std.debug.print( 82 82 \\usage: zds [--host HOST] [--port PORT] [--db PATH] [--public-url URL] [--server-did DID] 83 - \\ [--plc-rotation-key HEX] [--recovery-did-key DIDKEY] 83 + \\ [--plc-rotation-key KEY] [--recovery-did-key DIDKEY] 84 84 \\ [--blob-upload-limit BYTES] [--blobstore-path PATH] 85 85 \\ [--handle-domains DOMAINS] [--crawlers URLS] [--jwt-secret SECRET] 86 86 \\ [--admin-token TOKEN] [--invite-required]