//! Development-only XRPC handlers gated by ZDS_DEV_TOOLS. //! //! These endpoints expose the centralized dev clock so local test rigs can //! fast-forward server time (token expiry, session lifetimes) without //! waiting. They must never be enabled on a production deployment. const std = @import("std"); const clock = @import("../core/clock.zig"); const config = @import("../core/config.zig"); const http_api = @import("../http/api.zig"); const store = @import("../storage/store.zig"); pub fn dispatch(request: *http_api.Request) !void { if (!config.devTools()) { return http_api.xrpcError( request, .not_implemented, "MethodNotImplemented", "Dev tools are disabled on this PDS", ); } const method = xrpcMethod(request.url.raw) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid XRPC path"); }; if (std.mem.eql(u8, method, "dev.zat.debug.getClock")) return getClock(request); if (std.mem.eql(u8, method, "dev.zat.debug.setClock")) return setClock(request); if (std.mem.eql(u8, method, "dev.zat.debug.expireTokens")) return expireTokens(request); return http_api.xrpcError( request, .not_implemented, "MethodNotImplemented", "Unknown dev tools method", ); } /// Force-expire all OAuth access tokens (refresh tokens untouched). This is /// the surgical alternative to setClock for exercising client refresh flows: /// DPoP proofs carry the client's iat, so fast-forwarding the server clock /// past the proof window breaks every proof; expiring the rows does not. fn expireTokens(request: *http_api.Request) !void { try store.expireAllOAuthAccessTokens(clock.now() - 1); return http_api.json(request, .ok, "{\"ok\":true}"); } fn getClock(request: *http_api.Request) !void { var buf: [128]u8 = undefined; const body = try std.fmt.bufPrint(&buf, "{{\"now\":{d},\"offsetSeconds\":{d}}}", .{ clock.now(), clock.offset() }); return http_api.json(request, .ok, body); } fn setClock(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var body_buf: [4096]u8 = undefined; const body = try http_api.readBody(request, &body_buf); const parsed = try http_api.parseJsonBody(request, allocator, body); const offset_seconds = jsonInt(parsed.value, "offsetSeconds") orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing offsetSeconds"); }; clock.setOffsetSeconds(offset_seconds); const body_out = try std.fmt.allocPrint(allocator, "{{\"ok\":true,\"offsetSeconds\":{d}}}", .{clock.offset()}); return http_api.json(request, .ok, body_out); } fn jsonInt(value: std.json.Value, key: []const u8) ?i64 { if (value != .object) return null; const raw = value.object.get(key) orelse return null; return switch (raw) { .integer => |n| n, else => null, }; } fn xrpcMethod(target: []const u8) ?[]const u8 { const prefix = "/xrpc/"; if (!std.mem.startsWith(u8, target, prefix)) return null; const rest = target[prefix.len..]; const end = std.mem.indexOfScalar(u8, rest, '?') orelse rest.len; return rest[0..end]; }