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

Configure Feed

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

Use admin token for invite minting

zzstoatzz (May 24, 2026, 5:18 PM -0500) 04884f57 7799d360

+56 -63
+1 -1
README.md
··· 49 49 ZDS_HANDLE_DOMAINS='.example.com,example.com' \ 50 50 ZDS_CRAWLERS='https://bsky.network,https://vsky.network' \ 51 51 ZDS_JWT_SECRET='at-least-32-random-bytes-here' \ 52 + ZDS_ADMIN_TOKEN='another-random-secret' \ 52 53 ZDS_INVITE_REQUIRED=true \ 53 - ZDS_INVITE_ADMIN_DIDS='did:plc:admin...' \ 54 54 zig build run -- \ 55 55 --port 2583 \ 56 56 --db /var/lib/zds/zds.sqlite3 \
+8 -8
docs/invite-codes.md
··· 13 13 - `com.atproto.server.getAccountInviteCodes` lists codes owned by the current 14 14 account and their recorded uses. 15 15 16 - Admin code creation is restricted by `ZDS_INVITE_ADMIN_DIDS`. That is ZDS's 17 - local admin boundary; the official PDS uses its admin-token verifier, while 18 - Tranquil routes invite administration through its own infra/user store traits. 16 + Admin code creation requires `Authorization: Bearer $ZDS_ADMIN_TOKEN`, matching 17 + the official PDS admin-token boundary. Tranquil routes invite administration 18 + through its own infra/user store traits, but it is still privileged 19 + infrastructure behavior rather than normal account auth. 19 20 20 21 ## storage 21 22 ··· 46 47 requests cannot both consume a one-use code. 47 48 48 49 When invite-required mode starts against an empty database, ZDS creates one 49 - bootstrap code and logs it. After that, admins should mint codes through the 50 - XRPC endpoint, for example: 50 + bootstrap code and logs it. After that, mint codes through the XRPC endpoint: 51 51 52 52 ```sh 53 - ZDS_ADMIN_PASSWORD='...' just invite bufo.uk https://pds.zat.dev 53 + ZDS_ADMIN_TOKEN='...' just invite https://pds.zat.dev 54 54 ``` 55 55 56 56 For a multi-use code: 57 57 58 58 ```sh 59 - ZDS_ADMIN_PASSWORD='...' just invite bufo.uk https://pds.zat.dev 3 59 + ZDS_ADMIN_TOKEN='...' just invite https://pds.zat.dev 3 60 60 ``` 61 61 62 62 For a code assigned to a specific DID: 63 63 64 64 ```sh 65 - ZDS_ADMIN_PASSWORD='...' just invite bufo.uk https://pds.zat.dev 1 did:plc:... 65 + ZDS_ADMIN_TOKEN='...' just invite https://pds.zat.dev 1 did:plc:... 66 66 ``` 67 67 68 68 ## reference notes
+2 -2
docs/operations.md
··· 63 63 - `ZDS_BLOB_UPLOAD_LIMIT`: upload body limit. Default: `100000000`. 64 64 - `ZDS_BLOBSTORE_PATH`: disk blobstore root. 65 65 - `ZDS_CRAWLERS`: comma-separated relay crawl targets. 66 + - `ZDS_ADMIN_TOKEN`: bearer token for privileged local PDS administration, 67 + including invite-code minting. 66 68 - `ZDS_INVITE_REQUIRED`: set to `true` to require invite codes for account 67 69 creation. `describeServer` reflects this value. 68 - - `ZDS_INVITE_ADMIN_DIDS`: comma-separated account DIDs allowed to create invite 69 - codes through `com.atproto.server.createInviteCode(s)`. 70 70 71 71 When invites are required and the database has no accounts or invite codes, ZDS 72 72 creates one bootstrap code and logs it during startup. Invite codes are stored
+4 -10
justfile
··· 8 8 smoke: 9 9 tools/smoke.sh 10 10 11 - # mint an invite code from an admin account 12 - invite handle="bufo.uk" pds="https://pds.zat.dev" use_count="1" for_account="": 11 + # mint an invite code with the configured admin token 12 + invite pds="https://pds.zat.dev" use_count="1" for_account="": 13 13 #!/usr/bin/env bash 14 14 set -euo pipefail 15 - : "${ZDS_ADMIN_PASSWORD:?set ZDS_ADMIN_PASSWORD to the admin account password}" 16 - token="$( 17 - curl -fsS "{{pds}}/xrpc/com.atproto.server.createSession" \ 18 - -H 'content-type: application/json' \ 19 - --data "$(jq -nc --arg identifier "{{handle}}" --arg password "$ZDS_ADMIN_PASSWORD" '{identifier:$identifier,password:$password}')" | 20 - jq -r '.accessJwt' 21 - )" 15 + : "${ZDS_ADMIN_TOKEN:?set ZDS_ADMIN_TOKEN to the configured PDS admin token}" 22 16 if [[ -n "{{for_account}}" ]]; then 23 17 body="$(jq -nc --argjson useCount "{{use_count}}" --arg forAccount "{{for_account}}" '{useCount:$useCount,forAccount:$forAccount}')" 24 18 else 25 19 body="$(jq -nc --argjson useCount "{{use_count}}" '{useCount:$useCount}')" 26 20 fi 27 21 curl -fsS "{{pds}}/xrpc/com.atproto.server.createInviteCode" \ 28 - -H "authorization: Bearer $token" \ 22 + -H "authorization: Bearer $ZDS_ADMIN_TOKEN" \ 29 23 -H 'content-type: application/json' \ 30 24 --data "$body" | 31 25 jq -r '.code'
+21 -19
src/atproto/server.zig
··· 135 135 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 136 136 defer arena.deinit(); 137 137 const allocator = arena.allocator(); 138 - const account = requireAdminAccount(request, allocator) catch return; 138 + requireAdminToken(request) catch return; 139 139 var body_buf: [4096]u8 = undefined; 140 140 const body = try http_api.readBody(request, &body_buf); 141 141 const parsed = try http_api.parseJsonBody(request, allocator, body); 142 142 const use_count = jsonInt(parsed.value, "useCount") orelse { 143 143 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing useCount"); 144 144 }; 145 - const for_account = zat.json.getString(parsed.value, "forAccount") orelse account.did; 146 - const code = store.createInviteCode(allocator, config.publicUrl(), use_count, for_account, account.did) catch |err| switch (err) { 145 + const for_account = zat.json.getString(parsed.value, "forAccount") orelse "admin"; 146 + const code = store.createInviteCode(allocator, config.publicUrl(), use_count, for_account, "admin") catch |err| switch (err) { 147 147 error.InvalidUseCount => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "useCount must be greater than zero"), 148 148 else => return err, 149 149 }; ··· 155 155 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 156 156 defer arena.deinit(); 157 157 const allocator = arena.allocator(); 158 - const account = requireAdminAccount(request, allocator) catch return; 158 + requireAdminToken(request) catch return; 159 159 var body_buf: [8192]u8 = undefined; 160 160 const body = try http_api.readBody(request, &body_buf); 161 161 const parsed = try http_api.parseJsonBody(request, allocator, body); ··· 178 178 } 179 179 for (accounts, 0..) |for_account, idx| { 180 180 if (idx != 0) try out.writer.writeByte(','); 181 - writeInviteCodeGroup(allocator, &out.writer, for_account, code_count, use_count, account.did) catch |err| switch (err) { 181 + writeInviteCodeGroup(allocator, &out.writer, for_account, code_count, use_count, "admin") catch |err| switch (err) { 182 182 error.InvalidInviteInput => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "codeCount and useCount must be greater than zero"), 183 183 else => return err, 184 184 }; 185 185 } 186 186 } else { 187 - writeInviteCodeGroup(allocator, &out.writer, account.did, code_count, use_count, account.did) catch |err| switch (err) { 187 + writeInviteCodeGroup(allocator, &out.writer, "admin", code_count, use_count, "admin") catch |err| switch (err) { 188 188 error.InvalidInviteInput => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "codeCount and useCount must be greater than zero"), 189 189 else => return err, 190 190 }; ··· 724 724 }; 725 725 } 726 726 727 - fn requireAdminAccount(request: *http.Server.Request, allocator: std.mem.Allocator) !auth.Account { 728 - const account = try requireAccount(request, allocator); 729 - if (!isInviteAdmin(account.did)) { 730 - try http_api.xrpcError(request, .forbidden, "AdminRequired", "Admin account required"); 727 + fn requireAdminToken(request: *http.Server.Request) !void { 728 + const expected = config.adminToken() orelse { 729 + try http_api.xrpcError(request, .forbidden, "AdminRequired", "Admin token not configured"); 730 + return error.HandledResponse; 731 + }; 732 + const auth_header = http_api.headerValue(request, "authorization") orelse { 733 + try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"); 734 + return error.HandledResponse; 735 + }; 736 + if (!std.ascii.startsWithIgnoreCase(auth_header, "bearer ")) { 737 + try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"); 731 738 return error.HandledResponse; 732 739 } 733 - return account; 734 - } 735 - 736 - fn isInviteAdmin(did: []const u8) bool { 737 - var parts = std.mem.splitScalar(u8, config.inviteAdminDids(), ','); 738 - while (parts.next()) |part| { 739 - const trimmed = std.mem.trim(u8, part, " \t\r\n"); 740 - if (std.mem.eql(u8, trimmed, did)) return true; 740 + const provided = std.mem.trim(u8, auth_header["bearer ".len..], " \t"); 741 + if (!std.mem.eql(u8, provided, expected)) { 742 + try http_api.xrpcError(request, .forbidden, "AdminRequired", "Admin token required"); 743 + return error.HandledResponse; 741 744 } 742 - return false; 743 745 } 744 746 745 747 fn expiresInTenMinutes() i64 {
+9 -9
src/core/config.zig
··· 10 10 var handle_domains_value: []const u8 = ".test"; 11 11 var crawlers_value: []const u8 = "https://bsky.network,https://vsky.network"; 12 12 var jwt_secret_value: []const u8 = "zds-local-development-jwt-secret-change-me"; 13 + var admin_token_value: ?[]const u8 = null; 13 14 var invite_required_value: bool = false; 14 - var invite_admin_dids_value: []const u8 = ""; 15 15 var log_level_value: LogLevel = .info; 16 16 17 17 pub const LogLevel = enum(u2) { ··· 60 60 return jwt_secret_value; 61 61 } 62 62 63 - pub fn inviteRequired() bool { 64 - return invite_required_value; 63 + pub fn adminToken() ?[]const u8 { 64 + return admin_token_value; 65 65 } 66 66 67 - pub fn inviteAdminDids() []const u8 { 68 - return invite_admin_dids_value; 67 + pub fn inviteRequired() bool { 68 + return invite_required_value; 69 69 } 70 70 71 71 pub fn logLevel() LogLevel { ··· 116 116 jwt_secret_value = value; 117 117 } 118 118 119 - pub fn setInviteRequired(value: bool) void { 120 - invite_required_value = value; 119 + pub fn setAdminToken(value: ?[]const u8) void { 120 + admin_token_value = value; 121 121 } 122 122 123 - pub fn setInviteAdminDids(value: []const u8) void { 124 - invite_admin_dids_value = value; 123 + pub fn setInviteRequired(value: bool) void { 124 + invite_required_value = value; 125 125 } 126 126 127 127 pub fn setLogLevel(value: LogLevel) void {
+7 -10
src/internal/cli.zig
··· 14 14 handle_domains: ?[]const u8 = null, 15 15 crawlers: ?[]const u8 = null, 16 16 jwt_secret: ?[]const u8 = null, 17 + admin_token: ?[]const u8 = null, 17 18 invite_required: bool = false, 18 - invite_admin_dids: ?[]const u8 = null, 19 19 log_level: ?[]const u8 = null, 20 20 }; 21 21 ··· 29 29 MissingEmailFrom, 30 30 MissingHandleDomains, 31 31 MissingHost, 32 + MissingAdminToken, 32 33 MissingJwtSecret, 33 34 MissingLogLevel, 34 35 MissingPlcDirectory, ··· 36 37 MissingPublicUrl, 37 38 MissingResendApiKey, 38 39 MissingServerDid, 39 - MissingInviteAdminDids, 40 40 UnknownArgument, 41 41 } || std.fmt.ParseIntError; 42 42 ··· 53 53 .handle_domains = env("ZDS_HANDLE_DOMAINS"), 54 54 .crawlers = env("ZDS_CRAWLERS"), 55 55 .jwt_secret = env("ZDS_JWT_SECRET"), 56 + .admin_token = env("ZDS_ADMIN_TOKEN"), 56 57 .invite_required = envBool("ZDS_INVITE_REQUIRED"), 57 - .invite_admin_dids = env("ZDS_INVITE_ADMIN_DIDS"), 58 58 .log_level = env("ZDS_LOG_LEVEL"), 59 59 }; 60 60 if (envBool("ZDS_DEBUG")) options.log_level = "debug"; ··· 76 76 \\usage: zds [--host HOST] [--port PORT] [--db PATH] [--public-url URL] [--server-did DID] 77 77 \\ [--blob-upload-limit BYTES] [--blobstore-path PATH] 78 78 \\ [--handle-domains DOMAINS] [--crawlers URLS] [--jwt-secret SECRET] 79 - \\ [--invite-required] [--invite-admin-dids DIDS] 79 + \\ [--admin-token TOKEN] [--invite-required] 80 80 \\ [--log-level error|info|debug] [--debug] 81 81 \\ 82 82 \\Runs a local PDS-shaped HTTP server. ··· 147 147 options.jwt_secret = args.next() orelse return error.MissingJwtSecret; 148 148 return true; 149 149 } 150 - if (std.mem.eql(u8, arg, "--invite-admin-dids")) { 151 - options.invite_admin_dids = args.next() orelse return error.MissingInviteAdminDids; 150 + if (std.mem.eql(u8, arg, "--admin-token")) { 151 + options.admin_token = args.next() orelse return error.MissingAdminToken; 152 152 return true; 153 153 } 154 154 if (std.mem.eql(u8, arg, "--log-level")) { ··· 173 173 options.blob_upload_limit = try std.fmt.parseInt(usize, arg["--blob-upload-limit=".len..], 10); 174 174 return true; 175 175 } 176 - if (std.mem.startsWith(u8, arg, "--invite-admin-dids=")) { 177 - options.invite_admin_dids = arg["--invite-admin-dids=".len..]; 178 - return true; 179 - } 180 176 return false; 181 177 } 182 178 ··· 197 193 .{ .flag = "--handle-domains=", .field = "handle_domains" }, 198 194 .{ .flag = "--crawlers=", .field = "crawlers" }, 199 195 .{ .flag = "--jwt-secret=", .field = "jwt_secret" }, 196 + .{ .flag = "--admin-token=", .field = "admin_token" }, 200 197 .{ .flag = "--log-level=", .field = "log_level" }, 201 198 }; 202 199
+1 -1
src/main.zig
··· 29 29 if (options.handle_domains) |value| zds.core.config.setHandleDomains(value); 30 30 if (options.crawlers) |value| zds.core.config.setCrawlers(value); 31 31 if (options.jwt_secret) |value| zds.core.config.setJwtSecret(value); 32 + zds.core.config.setAdminToken(options.admin_token); 32 33 zds.core.config.setInviteRequired(options.invite_required); 33 - if (options.invite_admin_dids) |value| zds.core.config.setInviteAdminDids(value); 34 34 if (options.log_level) |value| { 35 35 const parsed = zds.core.config.parseLogLevel(value) orelse return error.InvalidLogLevel; 36 36 zds.core.config.setLogLevel(parsed);
+3 -3
tools/smoke.sh
··· 31 31 --server-did did:web:localhost \ 32 32 --handle-domains .test \ 33 33 --invite-required \ 34 - --invite-admin-dids did:plc:smoketest \ 34 + --admin-token smoke-admin-token \ 35 35 >"$log" 2>&1 & 36 36 server_pid=$! 37 37 ··· 68 68 test "$missing_invite_status" = "400" 69 69 grep -q '"error":"InvalidInviteCode"' /tmp/zds-missing-invite.json 70 70 invite=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createInviteCode" \ 71 - -H "authorization: Bearer $token" \ 71 + -H "authorization: Bearer smoke-admin-token" \ 72 72 -H 'content-type: application/json' \ 73 - --data '{"useCount":1}') 73 + --data '{"useCount":1,"forAccount":"did:plc:smoketest"}') 74 74 printf '%s' "$invite" | grep -q '"code":"127-0-0-1-' 75 75 invites=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.server.getAccountInviteCodes") 76 76 printf '%s' "$invites" | grep -q '"available":1'