const 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,
\\
\\
\\
\\
\\
\\zds sessions
\\
\\
\\
\\
\\
\\zds
\\sessions
\\Operator view for app sign-ins and direct API token families across accounts hosted on {s}.
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\OAuth app sessions
\\
App sign-ins issued by the OAuth flow. New rows show whether authorization used a passkey or password.
\\
\\
\\
\\
\\
\\
\\
\\
, .{ 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);
}