atproto pds in zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531const std = @import("std");const auth = @import("../auth/tokens.zig");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");const webauthn = @import("webauthn");const zat = @import("zat");
const http = std.http;
const request_uri_prefix = "urn:ietf:params:oauth:request_uri:";const challenge_ttl_seconds: i64 = 300;
const LoginChallenge = struct { value: []const u8, expires_at: i64, named: bool,};
pub const CredentialKey = struct { bytes: []const u8,
pub fn validate(self: CredentialKey) !void { _ = try webauthn.cose.parseEc2PublicKey(self.bytes); }};
pub fn buildAssertionMessage(allocator: std.mem.Allocator, authenticator_data: []const u8, client_data_json: []const u8) ![]u8 { var client_hash: [32]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(client_data_json, &client_hash, .{});
const out = try allocator.alloc(u8, authenticator_data.len + client_hash.len); @memcpy(out[0..authenticator_data.len], authenticator_data); @memcpy(out[authenticator_data.len..], &client_hash); return out;}
pub fn verifyAssertionSignature(credential_public_key: []const u8, signature_der: []const u8, authenticator_data: []const u8, client_data_json: []const u8, allocator: std.mem.Allocator) !void { const message = try buildAssertionMessage(allocator, authenticator_data, client_data_json); defer allocator.free(message); try webauthn.crypto.verifyWebAuthnEs256(credential_public_key, signature_der, message);}
pub fn redirectToSecurity(request: *http_api.Request) !void { const headers = [_]http.Header{ .{ .name = "location", .value = "/account/security" }, .{ .name = "access-control-allow-origin", .value = "*" }, .{ .name = "connection", .value = "close" }, }; try http_api.respondNowClose(request, .see_other, "", &headers);}
pub fn adminSessionsPage(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const public_url = config.publicUrl(); const public_host = try htmlEscape(allocator, displayHost(public_url)); const body = try std.fmt.allocPrint(allocator, \\<!doctype html> \\<html lang="en"> \\<head> \\<meta charset="utf-8"> \\<meta name="viewport" content="width=device-width, initial-scale=1"> \\<title>zds sessions</title> \\<link rel="icon" href="/favicon.svg" type="image/svg+xml"> \\<style> \\:root{{color-scheme:dark;--bg:#070807;--panel:#101510;--line:#263326;--text:#f1f2ec;--muted:#a7a299;--accent:#8fb0ff;--green:#37c978;--field:#101410;--bad:#ffb4a8}} \\@media (prefers-color-scheme:light){{:root{{--bg:#f6f4ed;--panel:#fffdf7;--line:#d8d0c0;--text:#161412;--muted:#625c53;--accent:#315dcb;--green:#1b7340;--field:#fffaf1;--bad:#9f1d1d;color-scheme:light}}}} \\*{{box-sizing:border-box}}body{{margin:0;min-height:100vh;background:radial-gradient(circle at 18% 0,color-mix(in srgb,var(--green) 20%,transparent),transparent 34%),var(--bg);color:var(--text);font:15px/1.55 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace}} \\.shell{{width:min(100%,780px);margin:0 auto;padding:28px 16px 42px}}a{{color:inherit}}.brand{{display:inline-flex;margin-bottom:58px;text-decoration:none;font-weight:800}}h1{{margin:0;font-size:clamp(46px,16vw,96px);line-height:.88;letter-spacing:0}}p{{color:var(--muted);margin:14px 0 0;max-width:60ch}}strong{{color:var(--text)}} \\.panel{{margin-top:30px;border:1px solid var(--line);border-radius:10px;background:linear-gradient(135deg,color-mix(in srgb,var(--green) 10%,transparent),transparent 46%),linear-gradient(315deg,color-mix(in srgb,var(--accent) 8%,transparent),transparent 44%),var(--panel);padding:18px}} \\label{{display:block;margin:15px 0 6px;font-weight:760}}input{{width:100%;font:inherit;color:var(--text);background:var(--field);border:1px solid var(--line);border-radius:9px;padding:12px}}button{{width:100%;margin-top:16px;border:1px solid color-mix(in srgb,var(--accent) 60%,var(--line));border-radius:10px;background:var(--accent);color:#061021;font:inherit;font-weight:800;padding:12px;cursor:pointer}} \\.status{{min-height:1.5em;color:var(--muted)}}.error{{color:var(--bad)}}.toolbar{{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin-top:14px}}.hint{{font-size:.92rem;color:var(--muted);margin:8px 0 0}}.timezone{{font-size:.82rem;color:var(--muted);margin-top:4px}}.chooser{{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:18px}}.choice{{width:auto;margin:0;text-align:left;border:1px solid var(--line);border-radius:9px;background:color-mix(in srgb,var(--field) 70%,transparent);color:var(--text);padding:11px;cursor:pointer}}.choice[aria-pressed="true"]{{border-color:color-mix(in srgb,var(--accent) 72%,var(--line));background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 17%,transparent),color-mix(in srgb,var(--field) 78%,transparent))}}.choice span{{display:block;color:var(--muted);font-size:.78rem;font-weight:500}}.choice strong{{display:block;margin-top:4px;font-size:1.35rem}}.filters{{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}}.filter{{width:auto;margin:0;padding:7px 10px;border-radius:999px;background:transparent;color:var(--muted);border-color:var(--line)}}.filter[aria-pressed="true"]{{color:#061021;background:var(--accent);border-color:var(--accent)}}.section{{border-top:1px solid var(--line);padding-top:16px;margin-top:18px}}.section-head{{display:flex;gap:12px;align-items:end;justify-content:space-between}}.section h2{{font-size:1rem;margin:0}}.list{{display:grid;gap:9px;margin-top:10px}}.session-card{{border:1px solid var(--line);border-radius:9px;background:color-mix(in srgb,var(--field) 76%,transparent);padding:11px 12px}}.session-card.usable{{border-color:color-mix(in srgb,var(--green) 44%,var(--line))}}.session-card.ended{{opacity:.82}}.card-top{{display:flex;align-items:start;justify-content:space-between;gap:12px}}.who,.what{{font-weight:800;overflow-wrap:anywhere}}.did,.scope{{display:block;margin-top:2px;color:var(--muted);font-size:.82rem;overflow-wrap:anywhere}}.pill{{display:inline-flex;align-items:center;border:1px solid var(--line);border-radius:999px;padding:3px 8px;font-size:.78rem;font-weight:800;white-space:nowrap}}.pill.usable{{color:#061021;background:var(--green);border-color:var(--green)}}.pill.ended{{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 55%,var(--line));background:color-mix(in srgb,var(--bad) 7%,transparent)}}.card-grid{{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}}.stamp{{border-top:1px solid color-mix(in srgb,var(--line) 70%,transparent);padding-top:7px;color:var(--text)}}.stamp span{{display:block;color:var(--muted);font-size:.72rem}}.pager{{display:flex;align-items:center;gap:8px;justify-content:flex-end;color:var(--muted);font-size:.82rem}}.pager button{{width:auto;margin:0;padding:6px 9px;background:transparent;color:var(--text);border-color:var(--line)}}.empty{{border:1px dashed var(--line);border-radius:9px;padding:14px;color:var(--muted);margin-top:10px}}@media (max-width:680px){{.chooser{{grid-template-columns:1fr}}.card-grid{{grid-template-columns:1fr}}.section-head{{display:block}}.pager{{justify-content:flex-start;margin-top:8px}}}} \\</style> \\</head> \\<body> \\<main class="shell"> \\<a class="brand" href="/">zds</a> \\<h1>sessions</h1> \\<p>Operator view for app sign-ins and direct API token families across accounts hosted on <strong>{s}</strong>.</p> \\<section class="panel"> \\<form id="admin-form"> \\<label for="token">admin token</label> \\<input id="token" name="token" type="password" autocomplete="off" required> \\<button>load sessions</button> \\</form> \\<p id="status" class="status"></p> \\<p id="timezone" class="timezone"></p> \\<div class="chooser" id="kind-chooser" hidden> \\<button class="choice" type="button" data-kind="grants" aria-pressed="true"><span>OAuth app sessions</span><strong id="grant-count">0</strong></button> \\<button class="choice" type="button" data-kind="sessions" aria-pressed="false"><span>direct API tokens</span><strong id="session-count">0</strong></button> \\</div> \\<div class="filters" id="filters" hidden> \\<button class="filter" type="button" data-filter="usable" aria-pressed="true" title="Show rows that can continue authenticating. For OAuth app sessions: current refresh token is not revoked. For direct API token families: not revoked and either access or refresh token is still unexpired.">active</button> \\<button class="filter" type="button" data-filter="ended" aria-pressed="false" title="Show rows that cannot currently authenticate requests because they are expired or revoked.">inactive</button> \\<button class="filter" type="button" data-filter="all" aria-pressed="false" title="Show active and inactive rows together.">all</button> \\</div> \\<section class="section"> \\<div class="section-head"><div><h2 id="list-title">OAuth app sessions</h2> \\<p id="list-hint" class="hint">App sign-ins issued by the OAuth flow. New rows show whether authorization used a passkey or password.</p> \\</div><div id="pager" class="pager"></div></div> \\<div id="items"></div> \\</section> \\</section> \\</main> \\<script>{s}</script> \\</body> \\</html> , .{ public_host, adminSessionsScript }); try respondHtml(request, .ok, body);}
pub fn xrpcStartRegistration(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const account = requireBearerAccount(request, allocator) catch return; const parsed = try readJson(request, allocator);
const challenge = try newChallenge(allocator); try store.putWebAuthnChallenge(account.did, "registration", challenge, "{}", now() + challenge_ttl_seconds); const rp_id = try rpId(allocator); const user_id = try userId(allocator, account.did); const passkeys = try store.listPasskeys(allocator, account.did); const exclude = try credentialsJson(allocator, passkeys); const friendly = requiredString(parsed.value, "friendlyName") orelse account.handle; const body = try std.fmt.allocPrint(allocator, \\{{"options":{{"publicKey":{{"rp":{{"name":"zds","id":{f}}},"user":{{"id":{f},"name":{f},"displayName":{f}}},"challenge":{f},"pubKeyCredParams":[{{"type":"public-key","alg":-7}}],"timeout":60000,"excludeCredentials":{s},"authenticatorSelection":{{"residentKey":"required","requireResidentKey":true,"userVerification":"preferred"}},"attestation":"none"}}}}}} , .{ std.json.fmt(rp_id, .{}), std.json.fmt(user_id, .{}), std.json.fmt(account.handle, .{}), std.json.fmt(friendly, .{}), std.json.fmt(challenge, .{}), exclude, }); try http_api.json(request, .ok, body);}
pub fn xrpcFinishRegistration(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const account = requireBearerAccount(request, allocator) catch return; const parsed = try readJson(request, allocator); const challenge = (try store.getWebAuthnChallenge(allocator, account.did, "registration")) orelse return jsonError(request, .bad_request, "Missing registration challenge"); if (challenge.expires_at < now()) return jsonError(request, .bad_request, "Registration challenge expired");
const credential = try registrationResponse(parsed.value); const verified = webauthn.registration.verify(allocator, credential, .{ .challenge = challenge.challenge, .origin = config.publicUrl(), .rp_id = try rpId(allocator), .user_verification = .preferred, }) catch return jsonError(request, .bad_request, "Invalid passkey registration response"); const friendly = requiredString(parsed.value, "friendlyName"); const id = try store.savePasskey(allocator, account.did, verified.credential_id, verified.credential_public_key, verified.sign_count, friendly); try store.deleteWebAuthnChallenge(account.did, "registration"); const credential_id = try webauthn.base64url.encodeAlloc(allocator, verified.credential_id); const body = try std.fmt.allocPrint(allocator, "{{\"id\":{f},\"credentialId\":{f}}}", .{ std.json.fmt(id, .{}), std.json.fmt(credential_id, .{}) }); try http_api.json(request, .ok, body);}
pub fn xrpcList(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const account = requireBearerAccount(request, allocator) catch return; const passkeys = try store.listPasskeys(allocator, account.did); const body = try std.fmt.allocPrint(allocator, "{{\"passkeys\":{s}}}", .{try passkeysMetadataJson(allocator, passkeys)}); try http_api.json(request, .ok, body);}
pub fn xrpcDelete(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const account = requireBearerAccount(request, allocator) catch return; const parsed = try readJson(request, allocator); const id = requiredString(parsed.value, "id") orelse return jsonError(request, .bad_request, "Missing passkey id"); try store.deletePasskey(account.did, id); try http_api.json(request, .ok, "{}");}
pub fn xrpcUpdate(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const account = requireBearerAccount(request, allocator) catch return; const parsed = try readJson(request, allocator); const id = requiredString(parsed.value, "id") orelse return jsonError(request, .bad_request, "Missing passkey id"); const friendly_name = requiredString(parsed.value, "friendlyName") orelse return jsonError(request, .bad_request, "Missing friendlyName"); try store.updatePasskeyName(account.did, id, friendly_name); try http_api.json(request, .ok, "{}");}
pub fn loginStart(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const parsed = try readJson(request, allocator); const request_uri = requiredString(parsed.value, "request_uri") orelse return jsonError(request, .bad_request, "Missing request_uri"); const request_id = requestIdFromUri(request_uri) orelse return jsonError(request, .bad_request, "Invalid request_uri"); const oauth_request = (try store.getOAuthRequest(allocator, request_id)) orelse return jsonError(request, .bad_request, "Unknown request_uri"); if (oauth_request.expires_at < now()) return jsonError(request, .bad_request, "Expired request_uri"); const identifier = nonEmptyString(parsed.value, "identifier") orelse oauth_request.login_hint; const challenge = try newChallenge(allocator); if (identifier == null) { try store.putDiscoverableWebAuthnChallenge(request_id, challenge, now() + challenge_ttl_seconds); const body = try std.fmt.allocPrint(allocator, \\{{"publicKey":{{"challenge":{f},"rpId":{f},"timeout":60000,"userVerification":"required"}}}} , .{ std.json.fmt(challenge, .{}), std.json.fmt(try rpId(allocator), .{}) }); try http_api.json(request, .ok, body); return; }
const account = (try store.findAccount(allocator, identifier.?)) orelse return jsonError(request, .not_found, "Account not found"); const passkeys = try store.listPasskeys(allocator, account.did); if (passkeys.len == 0) return jsonError(request, .bad_request, "No passkeys are registered for this account"); try store.putWebAuthnChallenge(account.did, "login", challenge, request_id, now() + challenge_ttl_seconds); const allow = try credentialsJson(allocator, passkeys); const body = try std.fmt.allocPrint(allocator, \\{{"publicKey":{{"challenge":{f},"rpId":{f},"allowCredentials":{s},"timeout":60000,"userVerification":"required"}}}} , .{ std.json.fmt(challenge, .{}), std.json.fmt(try rpId(allocator), .{}), allow }); try http_api.json(request, .ok, body);}
pub fn loginFinish(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const parsed = try readJson(request, allocator); const request_uri = requiredString(parsed.value, "request_uri") orelse return jsonError(request, .bad_request, "Missing request_uri"); const request_id = requestIdFromUri(request_uri) orelse return jsonError(request, .bad_request, "Invalid request_uri"); const oauth_request = (try store.getOAuthRequest(allocator, request_id)) orelse return jsonError(request, .bad_request, "Unknown request_uri"); if (oauth_request.expires_at < now()) return jsonError(request, .bad_request, "Expired request_uri");
const credential = try assertionResponse(parsed.value); const credential_id = try webauthn.base64url.decodeAlloc(allocator, credential.raw_id); const passkey = (try store.getPasskeyByCredentialId(allocator, credential_id)) orelse return jsonError(request, .unauthorized, "Unknown passkey"); const login_challenge = findLoginChallenge(allocator, passkey.did, request_id) catch |err| switch (err) { error.MissingLoginChallenge => return jsonError(request, .bad_request, "Missing login challenge"), else => return err, }; if (login_challenge.expires_at < now()) return jsonError(request, .bad_request, "Login challenge expired");
const assertion = webauthn.assertion.verify(allocator, credential, .{ .challenge = login_challenge.value, .origin = config.publicUrl(), .rp_id = try rpId(allocator), .credential_public_key = passkey.public_key, .known_sign_count = passkey.sign_count, .user_verification = .required, }) catch return jsonError(request, .unauthorized, "Invalid passkey assertion");
const code = try store.randomToken(allocator, "", 16); try store.updatePasskeyUse(passkey.id, assertion.recommended_sign_count); if (login_challenge.named) { try store.deleteWebAuthnChallenge(passkey.did, "login"); } else { try store.deleteDiscoverableWebAuthnChallenge(request_id); } try store.authorizeOAuthRequest(oauth_request.request_id, passkey.did, code, "passkey"); const redirect_uri = try authorizationRedirect(allocator, oauth_request.redirect_uri, code, oauth_request.state, oauth_request.response_mode); const body = try std.fmt.allocPrint(allocator, "{{\"redirect_uri\":{f}}}", .{std.json.fmt(redirect_uri, .{})}); try http_api.json(request, .ok, body);}
fn readJson(request: *http_api.Request, allocator: std.mem.Allocator) !std.json.Parsed(std.json.Value) { const body = try http_api.readBodyAlloc(request, allocator, 64 * 1024); return std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch { try jsonError(request, .bad_request, "Expected JSON body"); return error.InvalidJson; };}
fn requiredString(value: std.json.Value, key: []const u8) ?[]const u8 { return zat.json.getString(value, key);}
fn nonEmptyString(value: std.json.Value, key: []const u8) ?[]const u8 { const raw = requiredString(value, key) orelse return null; const trimmed = std.mem.trim(u8, raw, " \t\r\n"); if (trimmed.len == 0) return null; return trimmed;}
fn findLoginChallenge(allocator: std.mem.Allocator, did: []const u8, request_id: []const u8) !LoginChallenge { if (try store.getWebAuthnChallenge(allocator, did, "login")) |candidate| { if (std.mem.eql(u8, candidate.state_json, request_id)) { return .{ .value = candidate.challenge, .expires_at = candidate.expires_at, .named = true, }; } } if (try store.getDiscoverableWebAuthnChallenge(allocator, request_id)) |discoverable| { return .{ .value = discoverable.challenge, .expires_at = discoverable.expires_at, .named = false, }; } return error.MissingLoginChallenge;}
fn registrationResponse(value: std.json.Value) !webauthn.registration.Response { const root = try objectValue(value); const credential = root.get("credential") orelse return error.MissingField; const credential_object = try objectValue(credential); const response = credential_object.get("response") orelse return error.MissingField; return .{ .id = zat.json.getString(credential, "id") orelse return error.MissingField, .raw_id = zat.json.getString(credential, "rawId") orelse return error.MissingField, .client_data_json = zat.json.getString(response, "clientDataJSON") orelse return error.MissingField, .attestation_object = zat.json.getString(response, "attestationObject") orelse return error.MissingField, };}
fn assertionResponse(value: std.json.Value) !webauthn.assertion.Response { const root = try objectValue(value); const credential = root.get("credential") orelse return error.MissingField; const credential_object = try objectValue(credential); const response = credential_object.get("response") orelse return error.MissingField; return .{ .id = zat.json.getString(credential, "id") orelse return error.MissingField, .raw_id = zat.json.getString(credential, "rawId") orelse return error.MissingField, .client_data_json = zat.json.getString(response, "clientDataJSON") orelse return error.MissingField, .authenticator_data = zat.json.getString(response, "authenticatorData") orelse return error.MissingField, .signature = zat.json.getString(response, "signature") orelse return error.MissingField, };}
fn objectValue(value: std.json.Value) !std.json.ObjectMap { return switch (value) { .object => |object| object, else => error.MissingField, };}
fn credentialsJson(allocator: std.mem.Allocator, passkeys: []const store.Passkey) ![]const u8 { const Descriptor = struct { type: []const u8 = "public-key", id: []const u8, };
var descriptors: std.ArrayList(Descriptor) = .empty; for (passkeys) |passkey| { const id = try webauthn.base64url.encodeAlloc(allocator, passkey.credential_id); try descriptors.append(allocator, .{ .id = id }); } return std.json.Stringify.valueAlloc(allocator, descriptors.items, .{});}
fn passkeysMetadataJson(allocator: std.mem.Allocator, passkeys: []const store.Passkey) ![]const u8 { const Metadata = struct { id: []const u8, credentialId: []const u8, friendlyName: []const u8, createdAt: i64, lastUsed: ?i64, };
var items: std.ArrayList(Metadata) = .empty; for (passkeys) |passkey| { const credential_id = try webauthn.base64url.encodeAlloc(allocator, passkey.credential_id); const friendly_name = passkey.friendly_name orelse ""; try items.append(allocator, .{ .id = passkey.id, .credentialId = credential_id, .friendlyName = friendly_name, .createdAt = passkey.created_at, .lastUsed = passkey.last_used, }); } return std.json.Stringify.valueAlloc(allocator, items.items, .{});}
fn newChallenge(allocator: std.mem.Allocator) ![]u8 { var bytes: [32]u8 = undefined; store.randomBytes(&bytes); return webauthn.base64url.encodeAlloc(allocator, &bytes);}
fn userId(allocator: std.mem.Allocator, did: []const u8) ![]u8 { var hash: [32]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(did, &hash, .{}); return webauthn.base64url.encodeAlloc(allocator, &hash);}
fn rpId(allocator: std.mem.Allocator) ![]const u8 { const url = config.publicUrl(); const without_scheme = if (std.mem.startsWith(u8, url, "https://")) url["https://".len..] else if (std.mem.startsWith(u8, url, "http://")) url["http://".len..] else url; const host_end = std.mem.indexOfAny(u8, without_scheme, "/:") orelse without_scheme.len; return allocator.dupe(u8, without_scheme[0..host_end]);}
fn displayHost(url: []const u8) []const u8 { const without_scheme = if (std.mem.startsWith(u8, url, "https://")) url["https://".len..] else if (std.mem.startsWith(u8, url, "http://")) url["http://".len..] else url; const end = std.mem.indexOfScalar(u8, without_scheme, '/') orelse without_scheme.len; return without_scheme[0..end];}
fn requestIdFromUri(request_uri: []const u8) ?[]const u8 { if (!std.mem.startsWith(u8, request_uri, request_uri_prefix)) return null; return request_uri[request_uri_prefix.len..];}
fn authorizationRedirect(allocator: std.mem.Allocator, redirect_uri: []const u8, code: []const u8, state: []const u8, response_mode: []const u8) ![]const u8 { const sep: u8 = if (std.mem.eql(u8, response_mode, "fragment")) '#' else if (std.mem.indexOfScalar(u8, redirect_uri, '?') == null) '?' else '&'; return std.fmt.allocPrint(allocator, "{s}{c}code={s}&iss={s}&state={s}", .{ redirect_uri, sep, try percentEncode(allocator, code), try percentEncode(allocator, config.publicUrl()), try percentEncode(allocator, state), });}
fn percentEncode(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { var out: std.ArrayList(u8) = .empty; for (input) |c| { if ((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '-' or c == '_' or c == '.' or c == '~') { try out.append(allocator, c); } else { const encoded = try std.fmt.allocPrint(allocator, "%{X:0>2}", .{c}); try out.appendSlice(allocator, encoded); } } return out.toOwnedSlice(allocator);}
fn now() i64 { return clock.now();}
fn jsonError(request: *http_api.Request, status: http.Status, message: []const u8) !void { var buf: [512]u8 = undefined; const body = try std.fmt.bufPrint(&buf, "{{\"error\":{f}}}", .{std.json.fmt(message, .{})}); try http_api.json(request, status, body);}
fn requireBearerAccount(request: *http_api.Request, allocator: std.mem.Allocator) !auth.Account { return http_api.requireBearerAccount(request, allocator) catch |err| { switch (err) { error.AuthRequired => try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"), error.InvalidToken => try http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid token"), } return error.HandledResponse; };}
fn respondHtml(request: *http_api.Request, status: http.Status, body: []const u8) !void { const headers = [_]http.Header{ .{ .name = "content-type", .value = "text/html; charset=utf-8" }, .{ .name = "access-control-allow-origin", .value = "*" }, .{ .name = "connection", .value = "close" }, }; try http_api.respond(request, status, body, &headers);}
fn htmlEscape(allocator: std.mem.Allocator, value: []const u8) ![]const u8 { var out: std.ArrayList(u8) = .empty; for (value) |c| switch (c) { '&' => try out.appendSlice(allocator, "&"), '<' => try out.appendSlice(allocator, "<"), '>' => try out.appendSlice(allocator, ">"), '"' => try out.appendSlice(allocator, """), '\'' => try out.appendSlice(allocator, "'"), else => try out.append(allocator, c), }; return out.toOwnedSlice(allocator);}
const adminSessionsScript = \\const status=document.querySelector('#status'),form=document.querySelector('#admin-form'),chooser=document.querySelector('#kind-chooser'),filters=document.querySelector('#filters'),itemsRoot=document.querySelector('#items'),pagerRoot=document.querySelector('#pager'),title=document.querySelector('#list-title'),hint=document.querySelector('#list-hint'),timezone=document.querySelector('#timezone'),grantCount=document.querySelector('#grant-count'),sessionCount=document.querySelector('#session-count'); \\const pageSize=8; \\let sessions=[],grants=[],kind='grants',filter='usable',page=0; \\timezone.textContent=`times shown in ${Intl.DateTimeFormat().resolvedOptions().timeZone || 'your local timezone'}`; \\const fail=async(r,msg)=>{if(r.ok)return r.json();let body={};try{body=await r.json()}catch{}throw new Error(body.message||body.error||msg)}; \\const empty=(text)=>{itemsRoot.textContent='';pagerRoot.textContent='';const p=document.createElement('div');p.className='empty';p.textContent=text;itemsRoot.append(p)}; \\const time=(value)=>{if(!value)return{short:'never',full:'never'};const d=new Date(value);return{short:d.toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}),full:d.toLocaleString([], {dateStyle:'full',timeStyle:'long'})}}; \\const methodLabel=(s)=>{if(s.appPasswordName)return`app password: ${s.appPasswordName}`;if(s.authMethod==='password')return'account password token';if(s.authMethod==='app_password')return'app password token';if(s.authMethod==='app_password_privileged')return'privileged app password token';return s.authMethod}; \\const shownItems=()=>{const source=kind==='grants'?grants:sessions;if(filter==='all')return source;if(filter==='usable')return source.filter(item=>item.active);return source.filter(item=>!item.active)}; \\const activeTitle=(isGrant)=>isGrant?'OAuth app session is active when its current refresh token has not been revoked. The one-hour access token may still need refresh.':'Direct API token family is active when it has not been revoked and either its access token or refresh token is still unexpired.'; \\const inactiveTitle=(isGrant)=>isGrant?'OAuth app session is inactive when its token row has been revoked, usually by refresh rotation or explicit revocation.':'Direct API token family is inactive when it is revoked or both access and refresh tokens are expired.'; \\const pill=(active,isGrant)=>{const span=document.createElement('span');span.className=`pill ${active?'usable':'ended'}`;span.textContent=active?'active':'inactive';span.title=active?activeTitle(isGrant):inactiveTitle(isGrant);return span}; \\const stamp=(label,value)=>{const div=document.createElement('div');div.className='stamp';const span=document.createElement('span');span.textContent=label;const t=time(value);const body=document.createElement('time');body.textContent=t.short;body.title=t.full;body.dateTime=value||'';div.append(span,body);return div}; \\const oauthAuthMethod=(g)=>g.authMethod==='passkey'?'passkey':g.authMethod==='password'?'password':'unknown method'; \\const card=(item)=>{const isGrant=kind==='grants';const el=document.createElement('article');el.className=`session-card ${item.active?'usable':'ended'}`;const top=document.createElement('div');top.className='card-top';const main=document.createElement('div');const who=document.createElement('div');who.className='who';who.textContent=item.handle||item.did||'unknown account';const sub=document.createElement('span');sub.className='did';sub.textContent=item.did||'';main.append(who,sub);top.append(main,pill(item.active,isGrant));const what=document.createElement('div');what.className='what';what.textContent=isGrant?item.clientId:methodLabel(item);const scope=document.createElement('span');scope.className='scope';scope.textContent=isGrant?`${oauthAuthMethod(item)} authorization · ${item.scope}`:(item.controllerDid?`controller ${item.controllerDid}`:'com.atproto.server.createSession token family');what.append(scope);const grid=document.createElement('div');grid.className='card-grid';if(isGrant){grid.append(stamp('authorized',item.createdAt),stamp('access token expires',item.expiresAt),stamp('revoked',item.revokedAt))}else{grid.append(stamp('created',item.createdAt),stamp('last used',item.lastUsedAt),stamp('refresh token expires',item.refreshExpiresAt))}el.append(top,what,grid);return el}; \\const renderPager=(total)=>{pagerRoot.textContent='';if(total<=pageSize)return;const pages=Math.ceil(total/pageSize);const prev=document.createElement('button');prev.type='button';prev.textContent='prev';prev.disabled=page===0;prev.onclick=()=>{page--;render()};const label=document.createElement('span');label.textContent=`${page+1}/${pages}`;const next=document.createElement('button');next.type='button';next.textContent='next';next.disabled=page>=pages-1;next.onclick=()=>{page++;render()};pagerRoot.append(prev,label,next)}; \\const renderControls=()=>{grantCount.textContent=`${grants.filter(g=>g.active).length} active / ${grants.length} total`;grantCount.title='OAuth active means the current refresh token has not been revoked; the access token may need refresh.';sessionCount.textContent=`${sessions.filter(s=>s.active).length} active / ${sessions.length} total`;sessionCount.title='Direct API active means the token family has not been revoked and either access or refresh token is still unexpired.';for(const b of chooser.querySelectorAll('button'))b.setAttribute('aria-pressed',String(b.dataset.kind===kind));for(const b of filters.querySelectorAll('button'))b.setAttribute('aria-pressed',String(b.dataset.filter===filter))}; \\const render=()=>{renderControls();const isGrant=kind==='grants';title.textContent=isGrant?'OAuth app sessions':'direct API tokens';hint.textContent=isGrant?'App sign-ins issued by the OAuth flow. Active means the current refresh token has not been revoked; the access token may still need refresh. New rows show passkey or password authorization; older rows may show unknown method.':'Token families from com.atproto.server.createSession. Active means the family has not been revoked and still has an unexpired access or refresh token. These are separate from OAuth app sign-ins.';const items=shownItems();if(!items.length)return empty(filter==='usable'?'No active rows.':filter==='ended'?'No inactive rows.':'No entries.');const maxPage=Math.max(0,Math.ceil(items.length/pageSize)-1);if(page>maxPage)page=maxPage;itemsRoot.textContent='';const list=document.createElement('div');list.className='list';for(const item of items.slice(page*pageSize,page*pageSize+pageSize))list.append(card(item));itemsRoot.append(list);renderPager(items.length)}; \\chooser.addEventListener('click',(e)=>{const b=e.target.closest('button[data-kind]');if(!b)return;kind=b.dataset.kind;page=0;render()}); \\filters.addEventListener('click',(e)=>{const b=e.target.closest('button[data-filter]');if(!b)return;filter=b.dataset.filter;page=0;render()}); \\form.addEventListener('submit',async(e)=>{e.preventDefault();status.className='status';try{status.textContent='loading token rows...';const token=form.token.value.trim();const data=await fail(await fetch('/xrpc/dev.zat.admin.listSessions?active=false&limit=500',{headers:{authorization:`Bearer ${token}`}}),'failed to load token rows');sessions=data.sessions||[];grants=data.oauthGrants||[];chooser.hidden=false;filters.hidden=false;kind='grants';filter=grants.some(g=>g.active)?'usable':grants.length?'ended':'all';page=0;render();status.textContent=''}catch(err){status.className='status error';status.textContent=err.message||String(err)}});;
test "validates webauthn credential key through tangled dependency" { const key = try webauthn.base64url.decodeAlloc(std.testing.allocator, "pQECAyYgASFYIDNDxl6djmZTEhKfw1B5jiSdcFUsTKuyPpks-4jTpA5aIlggF5oAEvUgwjYE6o0sPzL6G27d72m3lM2-yPAMOajmYoE"); defer std.testing.allocator.free(key); try (CredentialKey{ .bytes = key }).validate();}
test "verifies real webauthn assertion signature through zds adapter" { const allocator = std.testing.allocator; const key = try webauthn.base64url.decodeAlloc(allocator, "pQECAyYgASFYIDNDxl6djmZTEhKfw1B5jiSdcFUsTKuyPpks-4jTpA5aIlggF5oAEvUgwjYE6o0sPzL6G27d72m3lM2-yPAMOajmYoE"); defer allocator.free(key); const auth_data = try webauthn.base64url.decodeAlloc(allocator, "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA"); defer allocator.free(auth_data); const client_data_json = try webauthn.base64url.decodeAlloc(allocator, "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoibGgwR1c2OEZKZW03NWxBNV9sRTZKTmU4dlo2ODdsdmhaQmtrY0RzUVB5byIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0"); defer allocator.free(client_data_json); const signature = try webauthn.base64url.decodeAlloc(allocator, "MEYCIQDQ-pXZQT9yjPsXT_m47W-iTFAIRgBVOCBhwl6kU--0RwIhAKcJJhxipw6tsIR0ULRgvQAhTaeIXk_V29wKOqbfP1oL"); defer allocator.free(signature);
try verifyAssertionSignature(key, signature, auth_data, client_data_json, allocator);}