polls on atproto pollz.waow.tech
atproto zig
0
fork

Configure Feed

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

zig zen pass: remove dead code, fix profile cache, bound DPoP retry

- replace GPA (debug allocator) with page_allocator
- remove sendJsonWithCredentials alias (was identical to sendJson)
- fix profile cache: re-fetch when stale instead of returning stale data forever
- convert recursive pdsAuthedRequest DPoP nonce retry to bounded loop (max 1 retry)
- remove base64urlEncodeBytes alias in oauth.zig (was identical to base64urlEncode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz a421c02a 7773ea78

+75 -83
+62 -64
backend/src/http.zig
··· 494 494 \\{{"did":"{s}","handle":"{s}"}} 495 495 , .{ session.did, session.handle }); 496 496 497 - try sendJsonWithCredentials(request, body.items); 497 + try sendJson(request, body.items); 498 498 } 499 499 500 500 fn handleLogout(request: *http.Server.Request) !void { ··· 581 581 return; 582 582 }; 583 583 584 - try sendJsonWithCredentials(request, result); 584 + try sendJson(request, result); 585 585 } 586 586 587 587 fn handleVote(request: *http.Server.Request) !void { ··· 642 642 return; 643 643 }; 644 644 645 - try sendJsonWithCredentials(request, result); 645 + try sendJson(request, result); 646 646 } 647 647 648 648 fn handleDeletePoll(request: *http.Server.Request, uri_encoded: []const u8) !void { ··· 704 704 // also delete from local DB 705 705 db.deletePoll(poll_uri); 706 706 707 - try sendJsonWithCredentials(request, "{\"ok\":true}"); 707 + try sendJson(request, "{\"ok\":true}"); 708 708 } 709 709 710 710 // --- profile resolution --- ··· 741 741 if (now - profile.fetched_at < PROFILE_CACHE_SECS) { 742 742 return profile; 743 743 } 744 - // stale — serve it but refresh in background would be nice 745 - // for now just return stale data, fetch will happen on next miss 746 - return profile; 744 + // stale — re-fetch synchronously 745 + fetchAndCacheProfile(alloc, did); 746 + return db.getProfile(did) orelse profile; 747 747 } 748 748 749 749 // cache miss — fetch synchronously ··· 1019 1019 }); 1020 1020 } 1021 1021 1022 - fn sendJsonWithCredentials(request: *http.Server.Request, body: []const u8) !void { 1023 - try sendJson(request, body); 1024 - } 1025 - 1026 1022 fn sendCorsHeaders(request: *http.Server.Request, body: []const u8) !void { 1027 1023 try request.respond(body, .{ 1028 1024 .status = .no_content, ··· 1392 1388 const ath = try oauth.accessTokenHash(alloc, session.access_token); 1393 1389 defer alloc.free(ath); 1394 1390 1395 - const dpop_proof = try oauth.createDpopProof( 1396 - alloc, 1397 - &dpop_keypair, 1398 - method_str, 1399 - url, 1400 - if (session.dpop_pds_nonce.len > 0) session.dpop_pds_nonce else null, 1401 - ath, 1402 - ); 1403 - defer alloc.free(dpop_proof); 1391 + var nonce: ?[]const u8 = if (session.dpop_pds_nonce.len > 0) session.dpop_pds_nonce else null; 1404 1392 1405 - var auth_header_buf: [4096]u8 = undefined; 1406 - const auth_header = std.fmt.bufPrint(&auth_header_buf, "DPoP {s}", .{session.access_token}) catch return error.AuthHeaderTooLong; 1393 + // try once, retry once on DPoP nonce error (matching PAR/token pattern) 1394 + for (0..2) |_| { 1395 + const dpop_proof = try oauth.createDpopProof(alloc, &dpop_keypair, method_str, url, nonce, ath); 1396 + defer alloc.free(dpop_proof); 1407 1397 1408 - const http_method: http.Method = if (mem.eql(u8, method_str, "POST")) .POST else .GET; 1398 + var auth_header_buf: [4096]u8 = undefined; 1399 + const auth_header = std.fmt.bufPrint(&auth_header_buf, "DPoP {s}", .{session.access_token}) catch return error.AuthHeaderTooLong; 1409 1400 1410 - var client: std.http.Client = .{ .allocator = alloc }; 1411 - defer client.deinit(); 1401 + const http_method: http.Method = if (mem.eql(u8, method_str, "POST")) .POST else .GET; 1412 1402 1413 - var req = try client.request(http_method, try std.Uri.parse(url), .{ 1414 - .extra_headers = &.{ 1415 - .{ .name = "Authorization", .value = auth_header }, 1416 - .{ .name = "DPoP", .value = dpop_proof }, 1417 - }, 1418 - .headers = .{ 1419 - .content_type = .{ .override = "application/json" }, 1420 - }, 1421 - }); 1422 - defer req.deinit(); 1403 + var client: std.http.Client = .{ .allocator = alloc }; 1404 + defer client.deinit(); 1423 1405 1424 - if (body) |b| { 1425 - req.transfer_encoding = .{ .content_length = b.len }; 1426 - var body_writer = try req.sendBodyUnflushed(&.{}); 1427 - try body_writer.writer.writeAll(b); 1428 - try body_writer.end(); 1429 - try req.connection.?.flush(); 1430 - } else { 1431 - try req.sendBodiless(); 1432 - } 1406 + var req = try client.request(http_method, try std.Uri.parse(url), .{ 1407 + .extra_headers = &.{ 1408 + .{ .name = "Authorization", .value = auth_header }, 1409 + .{ .name = "DPoP", .value = dpop_proof }, 1410 + }, 1411 + .headers = .{ 1412 + .content_type = .{ .override = "application/json" }, 1413 + }, 1414 + }); 1415 + defer req.deinit(); 1433 1416 1434 - var redirect_buf: [1]u8 = undefined; 1435 - var response = req.receiveHead(&redirect_buf) catch return error.FetchFailed; 1417 + if (body) |b| { 1418 + req.transfer_encoding = .{ .content_length = b.len }; 1419 + var body_writer = try req.sendBodyUnflushed(&.{}); 1420 + try body_writer.writer.writeAll(b); 1421 + try body_writer.end(); 1422 + try req.connection.?.flush(); 1423 + } else { 1424 + try req.sendBodiless(); 1425 + } 1436 1426 1437 - // extract DPoP-Nonce from response headers 1438 - var header_iter = response.head.iterateHeaders(); 1439 - while (header_iter.next()) |header| { 1440 - if (std.ascii.eqlIgnoreCase(header.name, "dpop-nonce")) { 1441 - db.updateSessionNonce(session.did, .pds, header.value); 1427 + var redirect_buf: [1]u8 = undefined; 1428 + var response = req.receiveHead(&redirect_buf) catch return error.FetchFailed; 1429 + 1430 + // check for DPoP nonce in response 1431 + var new_nonce: ?[]const u8 = null; 1432 + var header_iter = response.head.iterateHeaders(); 1433 + while (header_iter.next()) |header| { 1434 + if (std.ascii.eqlIgnoreCase(header.name, "dpop-nonce")) { 1435 + new_nonce = header.value; 1436 + break; 1437 + } 1438 + } 1442 1439 1443 - // handle DPoP nonce retry 1440 + if (new_nonce) |n| { 1441 + db.updateSessionNonce(session.did, .pds, n); 1444 1442 if (isDpopNonceErrorStatus(response.head.status)) { 1445 - var updated_session = session; 1446 - updated_session.dpop_pds_nonce = header.value; 1447 - return pdsAuthedRequest(alloc, updated_session, method_str, path, body); 1443 + nonce = try alloc.dupe(u8, n); 1444 + continue; // retry with new nonce 1448 1445 } 1449 - break; 1450 1446 } 1447 + 1448 + var aw: std.Io.Writer.Allocating = .init(alloc); 1449 + const reader = response.reader(&.{}); 1450 + _ = reader.streamRemaining(&aw.writer) catch { 1451 + aw.deinit(); 1452 + return error.FetchFailed; 1453 + }; 1454 + 1455 + return aw.toOwnedSlice() catch error.FetchFailed; 1451 1456 } 1452 1457 1453 - var aw: std.Io.Writer.Allocating = .init(alloc); 1454 - const reader = response.reader(&.{}); 1455 - _ = reader.streamRemaining(&aw.writer) catch { 1456 - aw.deinit(); 1457 - return error.FetchFailed; 1458 - }; 1459 - 1460 - return aw.toOwnedSlice() catch error.FetchFailed; 1458 + return error.DpopNonceRetryExhausted; 1461 1459 } 1462 1460 1463 1461 fn isDpopNonceError(status: http.Status, body: []const u8) bool {
+1 -3
backend/src/main.zig
··· 13 13 const SOCKET_TIMEOUT_SECS = 30; 14 14 15 15 pub fn main() !void { 16 - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 17 - defer _ = gpa.deinit(); 18 - const allocator = gpa.allocator(); 16 + const allocator = std.heap.page_allocator; 19 17 20 18 // init sqlite - use DATA_PATH env or default to /data/pollz.db 21 19 const db_path = posix.getenv("DATA_PATH") orelse "/data/pollz.db";
+12 -16
backend/src/oauth.zig
··· 23 23 defer allocator.free(signing_input); 24 24 25 25 const sig = try keypair.sign(signing_input); 26 - const sig_b64 = try base64urlEncodeBytes(allocator, &sig.bytes); 26 + const sig_b64 = try base64urlEncode(allocator, &sig.bytes); 27 27 defer allocator.free(sig_b64); 28 28 29 29 return std.fmt.allocPrint(allocator, "{s}.{s}", .{ signing_input, sig_b64 }); ··· 87 87 const kp = Scheme.KeyPair.fromSecretKey(sk) catch return error.InvalidSecretKey; 88 88 const uncompressed = kp.public_key.toUncompressedSec1(); 89 89 90 - const x_b64 = try base64urlEncodeBytes(allocator, uncompressed[1..33]); 90 + const x_b64 = try base64urlEncode(allocator, uncompressed[1..33]); 91 91 defer allocator.free(x_b64); 92 - const y_b64 = try base64urlEncodeBytes(allocator, uncompressed[33..65]); 92 + const y_b64 = try base64urlEncode(allocator, uncompressed[33..65]); 93 93 defer allocator.free(y_b64); 94 94 95 95 const input = try std.fmt.allocPrint(allocator, ··· 99 99 100 100 var hash: [32]u8 = undefined; 101 101 crypto.hash.sha2.Sha256.hash(input, &hash, .{}); 102 - return base64urlEncodeBytes(allocator, &hash); 102 + return base64urlEncode(allocator, &hash); 103 103 } 104 104 105 105 /// create a `private_key_jwt` client assertion for token endpoint auth. ··· 138 138 pub fn generatePkceChallenge(allocator: Allocator, verifier: []const u8) ![]u8 { 139 139 var hash: [32]u8 = undefined; 140 140 crypto.hash.sha2.Sha256.hash(verifier, &hash, .{}); 141 - return base64urlEncodeBytes(allocator, &hash); 141 + return base64urlEncode(allocator, &hash); 142 142 } 143 143 144 144 /// generate a random PKCE code verifier (43 chars, base64url-encoded 32 random bytes). ··· 146 146 pub fn generatePkceVerifier(allocator: Allocator) ![]u8 { 147 147 var random_bytes: [32]u8 = undefined; 148 148 crypto.random.bytes(&random_bytes); 149 - return base64urlEncodeBytes(allocator, &random_bytes); 149 + return base64urlEncode(allocator, &random_bytes); 150 150 } 151 151 152 152 /// generate a random state parameter. ··· 154 154 pub fn generateState(allocator: Allocator) ![]u8 { 155 155 var random_bytes: [16]u8 = undefined; 156 156 crypto.random.bytes(&random_bytes); 157 - return base64urlEncodeBytes(allocator, &random_bytes); 157 + return base64urlEncode(allocator, &random_bytes); 158 158 } 159 159 160 160 // --- JWK --- ··· 172 172 const x = uncompressed[1..33]; 173 173 const y = uncompressed[33..65]; 174 174 175 - const x_b64 = try base64urlEncodeBytes(allocator, x); 175 + const x_b64 = try base64urlEncode(allocator, x); 176 176 defer allocator.free(x_b64); 177 177 178 - const y_b64 = try base64urlEncodeBytes(allocator, y); 178 + const y_b64 = try base64urlEncode(allocator, y); 179 179 defer allocator.free(y_b64); 180 180 181 181 // JWK thumbprint (RFC 7638): SHA-256 of canonical JSON with members in lexicographic order ··· 186 186 187 187 var thumbprint_hash: [32]u8 = undefined; 188 188 crypto.hash.sha2.Sha256.hash(thumbprint_input, &thumbprint_hash, .{}); 189 - const kid = try base64urlEncodeBytes(allocator, &thumbprint_hash); 189 + const kid = try base64urlEncode(allocator, &thumbprint_hash); 190 190 defer allocator.free(kid); 191 191 192 192 return std.fmt.allocPrint(allocator, ··· 212 212 pub fn accessTokenHash(allocator: Allocator, access_token: []const u8) ![]u8 { 213 213 var hash: [32]u8 = undefined; 214 214 crypto.hash.sha2.Sha256.hash(access_token, &hash, .{}); 215 - return base64urlEncodeBytes(allocator, &hash); 215 + return base64urlEncode(allocator, &hash); 216 216 } 217 217 218 218 // --- form URL encoding --- ··· 242 242 return buf; 243 243 } 244 244 245 - fn base64urlEncodeBytes(allocator: Allocator, data: []const u8) ![]u8 { 246 - return base64urlEncode(allocator, data); 247 - } 248 - 249 245 fn generateJti(allocator: Allocator) ![]u8 { 250 246 var random_bytes: [16]u8 = undefined; 251 247 crypto.random.bytes(&random_bytes); 252 - return base64urlEncodeBytes(allocator, &random_bytes); 248 + return base64urlEncode(allocator, &random_bytes); 253 249 } 254 250 255 251 fn percentEncode(allocator: Allocator, buf: *std.ArrayList(u8), input: []const u8) !void {