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 migration trace logging

zzstoatzz (May 21, 2026, 11:24 AM -0500) da32c0fa 9fae0144

+151
+131
dev/migration-trace.mjs
··· 1 + #!/usr/bin/env node 2 + 3 + const oldHandle = mustEnv("OLD_HANDLE"); 4 + const password = mustEnv("OLD_PASSWORD"); 5 + const newEmail = mustEnv("NEW_EMAIL"); 6 + const newPds = process.env.NEW_PDS ?? "https://pds.zat.dev"; 7 + const newHandle = process.env.NEW_HANDLE ?? oldHandle; 8 + const sourcePdsOverride = process.env.SOURCE_PDS; 9 + 10 + function mustEnv(name) { 11 + const value = process.env[name]; 12 + if (!value) { 13 + console.error(`missing ${name}`); 14 + process.exit(2); 15 + } 16 + return value; 17 + } 18 + 19 + function cleanHandle(handle) { 20 + return handle.replace("@", "").trim().replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, ""); 21 + } 22 + 23 + async function requestJson(label, method, baseUrl, nsid, { params, body, token } = {}) { 24 + const url = new URL(`/xrpc/${nsid}`, baseUrl); 25 + if (params) { 26 + for (const [key, value] of Object.entries(params)) { 27 + if (value != null) url.searchParams.set(key, value); 28 + } 29 + } 30 + const headers = { accept: "application/json" }; 31 + if (body !== undefined) headers["content-type"] = "application/json"; 32 + if (token) headers.authorization = `Bearer ${token}`; 33 + const started = Date.now(); 34 + const res = await fetch(url, { 35 + method, 36 + headers, 37 + body: body === undefined ? undefined : JSON.stringify(body), 38 + }); 39 + const text = await res.text(); 40 + let data = null; 41 + if (text) { 42 + try { 43 + data = JSON.parse(text); 44 + } catch { 45 + data = { raw: text.slice(0, 200) }; 46 + } 47 + } 48 + const error = data?.error ? ` error=${data.error}` : ""; 49 + console.log(`${label}: ${method} ${url} -> ${res.status} ${res.statusText}${error} (${Date.now() - started}ms)`); 50 + if (!res.ok) { 51 + const err = new Error(data?.message ?? `${res.status} ${res.statusText}`); 52 + err.status = res.status; 53 + err.data = data; 54 + throw err; 55 + } 56 + return data; 57 + } 58 + 59 + async function resolveMiniDoc(handle) { 60 + const url = new URL("https://slingshot.microcosm.blue/xrpc/blue.microcosm.identity.resolveMiniDoc"); 61 + url.searchParams.set("identifier", handle); 62 + const res = await fetch(url); 63 + if (!res.ok) throw new Error(`resolveMiniDoc failed: ${res.status}`); 64 + return res.json(); 65 + } 66 + 67 + async function main() { 68 + const handle = cleanHandle(oldHandle); 69 + const mini = sourcePdsOverride 70 + ? { did: null, handle, pds: sourcePdsOverride } 71 + : await resolveMiniDoc(handle); 72 + 73 + console.log(`source handle=${handle}`); 74 + if (mini.did) console.log(`source did=${mini.did}`); 75 + console.log(`source pds=${mini.pds}`); 76 + console.log(`target pds=${newPds}`); 77 + console.log(`target handle=${newHandle}`); 78 + 79 + const oldSession = await requestJson("old login", "POST", mini.pds, "com.atproto.server.createSession", { 80 + body: { identifier: handle, password }, 81 + }); 82 + const did = oldSession.did; 83 + console.log(`old login ok did=${did} handle=${oldSession.handle}`); 84 + 85 + const newDesc = await requestJson("new describe", "GET", newPds, "com.atproto.server.describeServer"); 86 + console.log(`new pds did=${newDesc.did}`); 87 + 88 + const serviceAuth = await requestJson("old service auth", "GET", mini.pds, "com.atproto.server.getServiceAuth", { 89 + token: oldSession.accessJwt, 90 + params: { 91 + aud: newDesc.did, 92 + lxm: "com.atproto.server.createAccount", 93 + }, 94 + }); 95 + console.log("old service auth ok"); 96 + 97 + try { 98 + await requestJson("new repo status before create", "GET", newPds, "com.atproto.sync.getRepoStatus", { 99 + params: { did }, 100 + }); 101 + } catch (err) { 102 + console.log(`new repo status before create expected miss: ${err.status} ${err.data?.error ?? err.message}`); 103 + } 104 + 105 + const created = await requestJson("new create account", "POST", newPds, "com.atproto.server.createAccount", { 106 + token: serviceAuth.token, 107 + body: { 108 + did, 109 + handle: newHandle, 110 + email: newEmail, 111 + password, 112 + }, 113 + }); 114 + console.log(`new create account ok did=${created.did} handle=${created.handle}`); 115 + 116 + const byDid = await requestJson("new login by did", "POST", newPds, "com.atproto.server.createSession", { 117 + body: { identifier: did, password }, 118 + }); 119 + console.log(`new login by did ok active=${byDid.active}`); 120 + 121 + const byHandle = await requestJson("new login by handle", "POST", newPds, "com.atproto.server.createSession", { 122 + body: { identifier: newHandle, password }, 123 + }); 124 + console.log(`new login by handle ok active=${byHandle.active}`); 125 + } 126 + 127 + main().catch((err) => { 128 + console.error(`trace failed: ${err.message}`); 129 + if (err.data) console.error(JSON.stringify(err.data, null, 2)); 130 + process.exit(1); 131 + });
+20
src/atproto/server.zig
··· 78 78 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "new DID creation is not wired yet"); 79 79 }; 80 80 if (zat.Did.parse(did) == null) { 81 + std.debug.print("xrpc createAccount rejected invalid_did did={s} handle={s}\n", .{ did, handle }); 81 82 return http_api.xrpcError(request, .bad_request, "InvalidDid", "invalid did"); 82 83 } 84 + std.debug.print("xrpc createAccount attempt did={s} handle={s}\n", .{ did, handle }); 83 85 try verifyCreateAccountServiceAuth(request, allocator, did); 84 86 85 87 const account = store.createAccount(allocator, handle, email, password, did, false) catch { 88 + std.debug.print("xrpc createAccount rejected account_exists_or_store_error did={s} handle={s}\n", .{ did, handle }); 86 89 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "account already exists"); 87 90 }; 91 + std.debug.print("xrpc createAccount ok did={s} handle={s}\n", .{ account.did, account.handle }); 88 92 const access = try auth.createDevJwt(allocator, "access", account); 89 93 const refresh = try auth.createDevJwt(allocator, "refresh", account); 90 94 const body_out = try std.fmt.allocPrint( ··· 137 141 138 142 fn verifyCreateAccountServiceAuth(request: *http.Server.Request, allocator: std.mem.Allocator, did: []const u8) !void { 139 143 const raw_header = http_api.headerValue(request, "authorization") orelse { 144 + std.debug.print("xrpc createAccount service_auth missing did={s}\n", .{did}); 140 145 return http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "service auth required to migrate an existing did"); 141 146 }; 142 147 if (!std.ascii.startsWithIgnoreCase(raw_header, "bearer ")) { 148 + std.debug.print("xrpc createAccount service_auth malformed did={s}\n", .{did}); 143 149 return http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "service auth required to migrate an existing did"); 144 150 } 145 151 const token = std.mem.trim(u8, raw_header["bearer ".len..], " \t"); 146 152 var jwt = zat.Jwt.parse(allocator, token) catch { 153 + std.debug.print("xrpc createAccount service_auth invalid_jwt did={s}\n", .{did}); 147 154 return http_api.xrpcError(request, .unauthorized, "InvalidToken", "invalid service auth token"); 148 155 }; 149 156 defer jwt.deinit(); 150 157 151 158 const issuer_did = issuerDid(jwt.payload.iss); 152 159 if (!std.mem.eql(u8, issuer_did, did)) { 160 + std.debug.print("xrpc createAccount service_auth issuer_mismatch did={s} issuer={s}\n", .{ did, issuer_did }); 153 161 return http_api.xrpcError(request, .unauthorized, "InvalidToken", "jwt issuer does not match did"); 154 162 } 155 163 if (!std.mem.eql(u8, jwt.payload.aud, config.serverDid())) { 164 + std.debug.print("xrpc createAccount service_auth aud_mismatch did={s} aud={s} expected={s}\n", .{ did, jwt.payload.aud, config.serverDid() }); 156 165 return http_api.xrpcError(request, .unauthorized, "InvalidToken", "jwt audience does not match service did"); 157 166 } 158 167 if (jwt.isExpired(store.currentIo())) { 168 + std.debug.print("xrpc createAccount service_auth expired did={s}\n", .{did}); 159 169 return http_api.xrpcError(request, .bad_request, "ExpiredToken", "token expired"); 160 170 } 161 171 if (jwt.payload.lxm) |lxm| { 162 172 if (!std.mem.eql(u8, lxm, "com.atproto.server.createAccount") and !std.mem.eql(u8, lxm, "*")) { 173 + std.debug.print("xrpc createAccount service_auth lxm_mismatch did={s} lxm={s}\n", .{ did, lxm }); 163 174 return http_api.xrpcError(request, .unauthorized, "InvalidToken", "jwt lxm does not match com.atproto.server.createAccount"); 164 175 } 165 176 } else { 177 + std.debug.print("xrpc createAccount service_auth missing_lxm did={s}\n", .{did}); 166 178 return http_api.xrpcError(request, .unauthorized, "InvalidToken", "jwt lxm is required"); 167 179 } 168 180 169 181 var resolver = zat.DidResolver.init(store.currentIo(), allocator); 170 182 defer resolver.deinit(); 171 183 var doc = resolver.resolve(zat.Did.parse(issuer_did).?) catch { 184 + std.debug.print("xrpc createAccount service_auth did_resolution_failed did={s}\n", .{did}); 172 185 return http_api.xrpcError(request, .bad_gateway, "DidResolutionFailed", "could not resolve jwt issuer did"); 173 186 }; 174 187 defer doc.deinit(); 175 188 const signing_key = doc.signingKey() orelse { 189 + std.debug.print("xrpc createAccount service_auth missing_signing_key did={s}\n", .{did}); 176 190 return http_api.xrpcError(request, .bad_gateway, "DidResolutionFailed", "missing signing key in issuer did doc"); 177 191 }; 178 192 jwt.verify(signing_key.public_key_multibase) catch { 193 + std.debug.print("xrpc createAccount service_auth signature_mismatch did={s}\n", .{did}); 179 194 return http_api.xrpcError(request, .unauthorized, "InvalidToken", "jwt signature does not match jwt issuer"); 180 195 }; 196 + std.debug.print("xrpc createAccount service_auth ok did={s}\n", .{did}); 181 197 } 182 198 183 199 fn issuerDid(iss: []const u8) []const u8 { ··· 201 217 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing password"); 202 218 }; 203 219 220 + std.debug.print("xrpc createSession attempt identifier={s}\n", .{identifier}); 204 221 const account = (store.findAccount(arena.allocator(), identifier) catch null) orelse { 222 + std.debug.print("xrpc createSession rejected account_not_found identifier={s}\n", .{identifier}); 205 223 return http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Invalid identifier or password"); 206 224 }; 207 225 if (!auth.passwordMatches(account, password)) { 226 + std.debug.print("xrpc createSession rejected password_mismatch identifier={s} did={s} handle={s}\n", .{ identifier, account.did, account.handle }); 208 227 return http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Invalid identifier or password"); 209 228 } 229 + std.debug.print("xrpc createSession ok identifier={s} did={s} handle={s}\n", .{ identifier, account.did, account.handle }); 210 230 211 231 const access = try auth.createDevJwt(arena.allocator(), "access", account); 212 232 const refresh = try auth.createDevJwt(arena.allocator(), "refresh", account);