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

Configure Feed

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

Add local PDS smoke checks

zzstoatzz (May 22, 2026, 4:55 PM -0500) fc89f26e 9a0e434e

+181 -9
+29
.tangled/workflows/ci.yml
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + 4 + engine: nixery 5 + 6 + dependencies: 7 + nixpkgs: 8 + - curl 9 + - git 10 + - sqlite 11 + - xz 12 + 13 + steps: 14 + - name: test and smoke 15 + command: | 16 + set -eu 17 + if ! command -v zig >/dev/null 2>&1; then 18 + curl -fsSL https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz -o /tmp/zig.tar.xz 19 + mkdir -p /tmp/zig 20 + tar -xJf /tmp/zig.tar.xz -C /tmp/zig --strip-components=1 21 + export PATH="/tmp/zig:$PATH" 22 + fi 23 + test "$(zig version)" = "0.16.0" 24 + mkdir -p ../../zat.dev 25 + if [ ! -d ../../zat.dev/zat ]; then 26 + git clone --depth=1 https://tangled.org/zat.dev/zat ../../zat.dev/zat 27 + fi 28 + zig build test 29 + tools/smoke.sh
+6 -1
Dockerfile
··· 1 + # syntax=docker/dockerfile:1.7 2 + 1 3 FROM debian:bookworm-slim AS build 2 4 3 5 RUN apt-get update \ ··· 12 14 && rm /tmp/zig.tar.xz 13 15 14 16 ENV PATH="/opt/zig:${PATH}" 17 + ENV ZIG_GLOBAL_CACHE_DIR=/root/.cache/zig 15 18 16 19 WORKDIR /work 17 20 RUN git clone --depth=1 https://tangled.org/zat.dev/zat /work/zat.dev/zat 18 21 19 22 WORKDIR /work/zzstoatzz.io/zds 20 23 COPY . . 21 - RUN zig build -Doptimize=ReleaseSafe 24 + RUN --mount=type=cache,target=/root/.cache/zig \ 25 + --mount=type=cache,target=/work/zzstoatzz.io/zds/.zig-cache \ 26 + zig build -Doptimize=ReleaseSafe 22 27 23 28 FROM debian:bookworm-slim 24 29
+1
README.md
··· 36 36 37 37 ```sh 38 38 zig build test 39 + tools/smoke.sh 39 40 zig zen 40 41 ``` 41 42
+2
build.zig.zon
··· 17 17 "build.zig.zon", 18 18 "src", 19 19 "docs", 20 + "tools", 21 + ".tangled", 20 22 "README.md", 21 23 }, 22 24 }
+32 -5
src/atproto/repo.zig
··· 365 365 else => return err, 366 366 }; 367 367 const cid = try store.putBlob(allocator, io, account, body, mime_type); 368 - const body_out = try std.fmt.allocPrint( 369 - allocator, 370 - "{{\"blob\":{{\"$type\":\"blob\",\"ref\":{{\"$link\":{f}}},\"mimeType\":{f},\"size\":{d}}}}}", 371 - .{ std.json.fmt(cid, .{}), std.json.fmt(mime_type, .{}), body.len }, 372 - ); 368 + const body_out = try uploadBlobResponseJson(allocator, cid, mime_type, body.len); 373 369 return http_api.json(request, .ok, body_out); 370 + } 371 + 372 + fn uploadBlobResponseJson( 373 + allocator: std.mem.Allocator, 374 + cid: []const u8, 375 + mime_type: []const u8, 376 + size: usize, 377 + ) ![]const u8 { 378 + const Link = struct { 379 + @"$link": []const u8, 380 + }; 381 + const Blob = struct { 382 + @"$type": []const u8, 383 + @"ref": Link, 384 + mimeType: []const u8, 385 + size: usize, 386 + }; 387 + const Response = struct { 388 + blob: Blob, 389 + }; 390 + var out: std.Io.Writer.Allocating = .init(allocator); 391 + defer out.deinit(); 392 + try std.json.Stringify.value(Response{ 393 + .blob = .{ 394 + .@"$type" = "blob", 395 + .@"ref" = .{ .@"$link" = cid }, 396 + .mimeType = mime_type, 397 + .size = size, 398 + }, 399 + }, .{}, &out.writer); 400 + return out.toOwnedSlice(); 374 401 } 375 402 376 403 pub fn listMissingBlobs(request: *http.Server.Request) !void {
+30 -3
src/storage/store.zig
··· 532 532 try requireInitialized(); 533 533 534 534 const seq = try nextSeqLocked(); 535 - const rev = try revForSeq(allocator, seq); 536 535 const current = try latestCommitRawLocked(allocator, account.did); 536 + const rev = try revForSeq(allocator, seq, if (current) |root| root.rev else null); 537 537 const repo_car = try readRepoCarLocked(allocator, account.did); 538 538 var tree = if (current) |root| 539 539 try zat.mst.Mst.loadFromBlocks(allocator, repo_car, root.data_cid_raw) ··· 649 649 }; 650 650 } 651 651 652 - pub fn revForSeq(allocator: std.mem.Allocator, seq: u64) ![]const u8 { 653 - const timestamp_us = 1_704_067_200_000_000 + seq; 652 + pub fn revForSeq(allocator: std.mem.Allocator, seq: u64, current_rev: ?[]const u8) ![]const u8 { 653 + var timestamp_us = nowMicros(); 654 + if (current_rev) |rev| { 655 + const current_timestamp = atid.timestampMicros(rev) catch 0; 656 + if (timestamp_us <= current_timestamp) timestamp_us = current_timestamp + 1; 657 + } 654 658 const tid = try atid.encode(timestamp_us, @intCast(seq % 1024)); 655 659 return allocator.dupe(u8, &tid); 660 + } 661 + 662 + fn nowMicros() u64 { 663 + var ts: std.posix.timespec = undefined; 664 + const timestamp = switch (std.posix.errno(std.posix.system.clock_gettime(.REALTIME, &ts))) { 665 + .SUCCESS => ts, 666 + else => std.posix.timespec{ .sec = 0, .nsec = 0 }, 667 + }; 668 + const seconds: u64 = if (timestamp.sec < 0) 0 else @intCast(timestamp.sec); 669 + const nanos: u64 = if (timestamp.nsec < 0) 0 else @intCast(timestamp.nsec); 670 + return seconds * std.time.us_per_s + nanos / std.time.ns_per_us; 656 671 } 657 672 658 673 pub fn get(did: []const u8, collection: []const u8, rkey: []const u8) ?Record { ··· 2394 2409 defer allocator.free(frame); 2395 2410 try std.testing.expect(frame.len > 0); 2396 2411 } 2412 + 2413 + test "repo rev generation does not move behind current head" { 2414 + const allocator = std.testing.allocator; 2415 + const future_tid = try atid.encode(nowMicros() + std.time.us_per_s, 0); 2416 + const rev = try revForSeq(allocator, 42, &future_tid); 2417 + defer allocator.free(rev); 2418 + 2419 + const previous = try atid.timestampMicros(&future_tid); 2420 + const next = try atid.timestampMicros(rev); 2421 + try std.testing.expect(next > previous); 2422 + try std.testing.expect(std.mem.lessThan(u8, &future_tid, rev)); 2423 + }
+81
tools/smoke.sh
··· 1 + #!/usr/bin/env sh 2 + set -eu 3 + 4 + root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) 5 + port="${ZDS_SMOKE_PORT:-2585}" 6 + db="${TMPDIR:-/tmp}/zds-smoke.sqlite3" 7 + blob_root="${TMPDIR:-/tmp}/zds-smoke-blobs" 8 + log="${TMPDIR:-/tmp}/zds-smoke.log" 9 + base="http://127.0.0.1:${port}" 10 + 11 + cleanup() { 12 + if [ -n "${server_pid:-}" ]; then 13 + kill "$server_pid" 2>/dev/null || true 14 + wait "$server_pid" 2>/dev/null || true 15 + fi 16 + } 17 + trap cleanup EXIT INT TERM 18 + 19 + cd "$root" 20 + rm -f "$db" "$db-wal" "$db-shm" "$log" 21 + rm -rf "$blob_root" 22 + mkdir -p "$blob_root" 23 + 24 + zig build 25 + zig build run -- \ 26 + --host 127.0.0.1 \ 27 + --port "$port" \ 28 + --db "$db" \ 29 + --blobstore-path "$blob_root" \ 30 + --public-url "$base" \ 31 + --server-did did:web:localhost \ 32 + --handle-domains .test \ 33 + >"$log" 2>&1 & 34 + server_pid=$! 35 + 36 + i=0 37 + while [ "$i" -lt 80 ]; do 38 + if curl -fsS "$base/xrpc/_health" >/dev/null 2>&1; then 39 + break 40 + fi 41 + i=$((i + 1)) 42 + sleep 0.1 43 + done 44 + 45 + curl -fsS "$base/xrpc/_health" >/dev/null 46 + sqlite3 "$db" "insert into accounts (did, handle, email, password_hash, activated_at, email_confirmed_at) values ('did:plc:smoketest', 'smoke.test', 'smoke@test.com', 'password', unixepoch(), unixepoch())" 47 + 48 + session=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createSession" \ 49 + -H 'content-type: application/json' \ 50 + --data '{"identifier":"smoke.test","password":"password"}') 51 + token=$(printf '%s' "$session" | sed -n 's/.*"accessJwt":"\([^"]*\)".*/\1/p') 52 + test -n "$token" 53 + 54 + create=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.createRecord" \ 55 + -H "authorization: Bearer $token" \ 56 + -H 'content-type: application/json' \ 57 + --data '{"repo":"did:plc:smoketest","collection":"app.bsky.feed.post","rkey":"3smoketest","record":{"$type":"app.bsky.feed.post","text":"smoke","createdAt":"2026-05-22T00:00:00.000Z"}}') 58 + printf '%s' "$create" | grep -q '"uri":"at://did:plc:smoketest/app.bsky.feed.post/3smoketest"' 59 + 60 + records=$(curl -fsS "$base/xrpc/com.atproto.repo.listRecords?repo=did:plc:smoketest&collection=app.bsky.feed.post&limit=10") 61 + printf '%s' "$records" | grep -q '"text":"smoke"' 62 + 63 + latest=$(curl -fsS "$base/xrpc/com.atproto.sync.getLatestCommit?did=did:plc:smoketest") 64 + printf '%s' "$latest" | grep -q '"cid":"' 65 + printf '%s' "$latest" | grep -q '"rev":"' 66 + 67 + repo_car="${TMPDIR:-/tmp}/zds-smoke.car" 68 + code=$(curl -sS -o "$repo_car" -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:smoketest") 69 + test "$code" = "200" 70 + test "$(wc -c < "$repo_car")" -gt 100 71 + 72 + blob_payload="${TMPDIR:-/tmp}/zds-smoke-blob.jpg" 73 + printf '\377\330\377\340zds-smoke' > "$blob_payload" 74 + blob=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.uploadBlob" \ 75 + -H "authorization: Bearer $token" \ 76 + -H 'content-type: image/jpeg' \ 77 + --data-binary "@$blob_payload") 78 + printf '%s' "$blob" | grep -q '"blob":' 79 + printf '%s' "$blob" | grep -q '"mimeType":"image/jpeg"' 80 + 81 + echo "zds smoke ok"