atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

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

Show OAuth authorization method in sessions

zzstoatzz (Jun 9, 2026, 10:41 AM -0500) 97841e02 653fd54c

+47 -33
+5 -5
src/atproto/oauth.zig
··· 344 344 if (!auth.passwordMatches(account, password)) return oauthError(request, .unauthorized, "access_denied", "Invalid identifier or password"); 345 345 346 346 const code = try store.randomToken(allocator, "", 16); 347 - try store.authorizeOAuthRequest(oauth_request.request_id, account.did, code); 347 + try store.authorizeOAuthRequest(oauth_request.request_id, account.did, code, "password"); 348 348 const redirect_uri = try authorizationRedirect(allocator, oauth_request.redirect_uri, code, oauth_request.state, oauth_request.response_mode); 349 349 if (wants_json or std.mem.indexOf(u8, content_type, "application/json") != null) { 350 350 const response = try std.fmt.allocPrint(allocator, "{{\"redirect_uri\":{f}}}", .{std.json.fmt(redirect_uri, .{})}); ··· 434 434 } 435 435 const did = oauth_request.sub orelse return oauthError(request, .bad_request, "invalid_grant", "Code was not authorized"); 436 436 const account = (try store.findAccount(allocator, did)) orelse return oauthError(request, .bad_request, "invalid_grant", "Account not found"); 437 - try issueTokenResponse(request, allocator, account, oauth_request.client_id, oauth_request.scope, null); 437 + try issueTokenResponse(request, allocator, account, oauth_request.client_id, oauth_request.scope, null, oauth_request.auth_method); 438 438 } 439 439 440 440 fn refreshToken(request: *http_api.Request, allocator: std.mem.Allocator, params: anytype) !void { ··· 453 453 return oauthError(request, .bad_request, "invalid_grant", "Invalid refresh token"); 454 454 } 455 455 const account = (try store.findAccount(allocator, token_row.did)) orelse return oauthError(request, .bad_request, "invalid_grant", "Account not found"); 456 - try issueTokenResponse(request, allocator, account, token_row.client_id, token_row.scope, refresh); 456 + try issueTokenResponse(request, allocator, account, token_row.client_id, token_row.scope, refresh, token_row.auth_method); 457 457 } 458 458 459 - fn issueTokenResponse(request: *http_api.Request, allocator: std.mem.Allocator, account: auth.Account, client_id: []const u8, scope: []const u8, revoke_old: ?[]const u8) !void { 459 + fn issueTokenResponse(request: *http_api.Request, allocator: std.mem.Allocator, account: auth.Account, client_id: []const u8, scope: []const u8, revoke_old: ?[]const u8, auth_method: ?[]const u8) !void { 460 460 if (revoke_old) |old| try store.revokeOAuthToken(old); 461 461 const access = try auth.createSessionJwt(allocator, "access", account); 462 462 const refresh = try auth.createSessionJwt(allocator, "refresh", account); ··· 465 465 log.err("oauth token scope_expand failed client={s} did={s} scope={s} err={s}\n", .{ client_id, account.did, scope, @errorName(err) }); 466 466 return oauthError(request, .bad_request, "invalid_scope", "Failed to resolve requested permission set"); 467 467 }; 468 - try store.putOAuthToken(account.did, client_id, token_scope, access, refresh, expires_at); 468 + try store.putOAuthToken(account.did, client_id, token_scope, access, refresh, expires_at, auth_method); 469 469 const body = try std.fmt.allocPrint(allocator, "{{\"access_token\":{f},\"refresh_token\":{f},\"token_type\":\"DPoP\",\"expires_in\":{d},\"scope\":{f},\"sub\":{f}}}", .{ 470 470 std.json.fmt(access, .{}), 471 471 std.json.fmt(refresh, .{}),
+3 -1
src/atproto/server.zig
··· 596 596 const created_at = try isoTimestamp(allocator, grant.created_at); 597 597 const expires_at = try isoTimestamp(allocator, grant.expires_at); 598 598 try writer.print( 599 - "{{\"did\":{f},\"handle\":{f},\"clientId\":{f},\"scope\":{f},\"createdAt\":{f},\"expiresAt\":{f},\"revokedAt\":", 599 + "{{\"did\":{f},\"handle\":{f},\"clientId\":{f},\"scope\":{f},\"createdAt\":{f},\"expiresAt\":{f},\"authMethod\":", 600 600 .{ 601 601 std.json.fmt(grant.did, .{}), 602 602 std.json.fmt(grant.handle, .{}), ··· 606 606 std.json.fmt(expires_at, .{}), 607 607 }, 608 608 ); 609 + try writeOptionalString(writer, grant.auth_method); 610 + try writer.writeAll(",\"revokedAt\":"); 609 611 try writeOptionalTimestamp(allocator, writer, grant.revoked_at); 610 612 try writer.print(",\"active\":{}}}", .{grant.active}); 611 613 }
+14 -13
src/internal/passkeys.zig
··· 135 135 \\<div id="generated-app-password" class="generated" aria-live="polite"></div> 136 136 \\</section> 137 137 \\<section class="security-section"> 138 - \\<h2>password/app-password API tokens</h2> 139 - \\<p class="hint">Active token families created by com.atproto.server.createSession. OAuth passkey sign-ins are separate.</p> 138 + \\<h2>direct API tokens</h2> 139 + \\<p class="hint">Active token families created by com.atproto.server.createSession. These are separate from OAuth app sign-ins.</p> 140 140 \\<div id="session-items"></div> 141 141 \\</section> 142 142 \\<section class="security-section"> 143 - \\<h2>OAuth app tokens</h2> 144 - \\<p class="hint">Current token rows issued to OAuth clients. Passkey OAuth sign-ins appear here, not under createSession tokens.</p> 143 + \\<h2>OAuth app sessions</h2> 144 + \\<p class="hint">App sign-ins issued by the OAuth flow. New rows show whether authorization used a passkey or password.</p> 145 145 \\<div id="oauth-grant-items"></div> 146 146 \\</section> 147 147 \\</section> ··· 192 192 \\<main class="shell"> 193 193 \\<a class="brand" href="/">zds</a> 194 194 \\<h1>sessions</h1> 195 - \\<p>Operator view for OAuth app tokens and createSession token families across accounts hosted on <strong>{s}</strong>.</p> 195 + \\<p>Operator view for app sign-ins and direct API token families across accounts hosted on <strong>{s}</strong>.</p> 196 196 \\<section class="panel"> 197 197 \\<form id="admin-form"> 198 198 \\<label for="token">admin token</label> ··· 202 202 \\<p id="status" class="status"></p> 203 203 \\<p id="timezone" class="timezone"></p> 204 204 \\<div class="chooser" id="kind-chooser" hidden> 205 - \\<button class="choice" type="button" data-kind="grants" aria-pressed="true"><span>OAuth app tokens</span><strong id="grant-count">0</strong></button> 206 - \\<button class="choice" type="button" data-kind="sessions" aria-pressed="false"><span>password/app-password API tokens</span><strong id="session-count">0</strong></button> 205 + \\<button class="choice" type="button" data-kind="grants" aria-pressed="true"><span>OAuth app sessions</span><strong id="grant-count">0</strong></button> 206 + \\<button class="choice" type="button" data-kind="sessions" aria-pressed="false"><span>direct API tokens</span><strong id="session-count">0</strong></button> 207 207 \\</div> 208 208 \\<div class="filters" id="filters" hidden> 209 209 \\<button class="filter" type="button" data-filter="usable" aria-pressed="true">usable token</button> ··· 211 211 \\<button class="filter" type="button" data-filter="all" aria-pressed="false">all</button> 212 212 \\</div> 213 213 \\<section class="section"> 214 - \\<div class="section-head"><div><h2 id="list-title">OAuth app tokens</h2> 215 - \\<p id="list-hint" class="hint">Current OAuth token rows. Usable means this token row has not expired or been revoked.</p> 214 + \\<div class="section-head"><div><h2 id="list-title">OAuth app sessions</h2> 215 + \\<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> 216 216 \\</div><div id="pager" class="pager"></div></div> 217 217 \\<div id="items"></div> 218 218 \\</section> ··· 375 375 } else { 376 376 try store.deleteDiscoverableWebAuthnChallenge(request_id); 377 377 } 378 - try store.authorizeOAuthRequest(oauth_request.request_id, passkey.did, code); 378 + try store.authorizeOAuthRequest(oauth_request.request_id, passkey.did, code, "passkey"); 379 379 const redirect_uri = try authorizationRedirect(allocator, oauth_request.redirect_uri, code, oauth_request.state, oauth_request.response_mode); 380 380 const body = try std.fmt.allocPrint(allocator, "{{\"redirect_uri\":{f}}}", .{std.json.fmt(redirect_uri, .{})}); 381 381 try http_api.json(request, .ok, body); ··· 618 618 \\const mono=(text)=>{const span=document.createElement('span');span.className='mono';span.textContent=text||'';return span}; 619 619 \\const time=(value)=>value?new Date(value).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):'never'; 620 620 \\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}; 621 + \\const oauthAuthMethod=(g)=>g.authMethod==='passkey'?'passkey':g.authMethod==='password'?'password':'unknown method'; 621 622 \\const showSessions=(items)=>table(sessionRoot,['method','created','last used','refresh expires'],items,s=>[methodLabel(s),time(s.createdAt),time(s.lastUsedAt),time(s.refreshExpiresAt)]); 622 - \\const showOAuthGrants=(items)=>table(oauthGrantRoot,['client','created','expires','scope'],items,g=>[mono(g.clientId),time(g.createdAt),time(g.expiresAt),mono(g.scope)]); 623 + \\const showOAuthGrants=(items)=>table(oauthGrantRoot,['client','authorized with','created','expires','scope'],items,g=>[mono(g.clientId),oauthAuthMethod(g),time(g.createdAt),time(g.expiresAt),mono(g.scope)]); 623 624 \\const showPasskeys=(items)=>{itemsRoot.textContent='';if(!items.length){const p=document.createElement('p');p.className='hint';p.textContent='No passkeys are saved for this account yet.';itemsRoot.append(p);return}for(const item of items){const view=credentialRow(item.friendlyName||'unnamed passkey',`last used: ${when(item.lastUsed)}`);const rename=document.createElement('button');rename.type='button';rename.textContent='rename';rename.onclick=async()=>{const name=prompt('Passkey name',item.friendlyName||'');if(!name)return;await fail(await authed('/xrpc/com.atproto.server.updatePasskey',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({id:item.id,friendlyName:name})}),'rename failed');await load()};const del=document.createElement('button');del.type='button';del.className='danger';del.textContent='delete';del.onclick=async()=>{if(!confirm('Delete this passkey from this PDS? Your browser or password manager may still keep its copy.'))return;await fail(await authed('/xrpc/com.atproto.server.deletePasskey',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({id:item.id})}),'delete failed');await load()};view.actions.append(rename,del);itemsRoot.append(view.row)}}; 624 625 \\const showAppPasswords=(items)=>{appPasswordRoot.textContent='';if(!items.length){const p=document.createElement('p');p.className='hint';p.textContent='No app passwords are active for this account.';appPasswordRoot.append(p);return}for(const item of items){const meta=`created ${item.createdAt}${item.privileged?' · privileged':''}`;const view=credentialRow(item.name,meta);const revoke=document.createElement('button');revoke.type='button';revoke.className='danger';revoke.textContent='revoke';revoke.onclick=async()=>{if(!confirm(`Revoke app password "${item.name}"? Existing sessions created with it will be revoked too.`))return;await fail(await authed('/xrpc/com.atproto.server.revokeAppPassword',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:item.name})}),'revoke failed');generatedAppPassword.classList.remove('on');generatedAppPassword.textContent='';await load()};view.actions.append(revoke);appPasswordRoot.append(view.row)}}; 625 626 \\const load=async()=>{const [passkeys,passwords,sessions]=await Promise.all([fail(await authed('/xrpc/com.atproto.server.listPasskeys'),'failed to load passkeys'),fail(await authed('/xrpc/com.atproto.server.listAppPasswords'),'failed to load app passwords'),fail(await authed('/xrpc/dev.zat.account.listSessions'),'failed to load sessions')]);showPasskeys(passkeys.passkeys||[]);showAppPasswords(passwords.passwords||[]);showSessions(sessions.sessions||[]);showOAuthGrants(sessions.oauthGrants||[])}; ··· 640 641 \\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)}; 641 642 \\const pill=(usable)=>{const span=document.createElement('span');span.className=`pill ${usable?'usable':'ended'}`;span.textContent=usable?'usable token':'expired/revoked';span.title=usable?'Not revoked and not expired.':'Expired or revoked.';return span}; 642 643 \\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}; 643 - \\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));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?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('issued',item.createdAt),stamp('token row 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}; 644 + \\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));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('token row 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}; 644 645 \\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)}; 645 646 \\const renderControls=()=>{grantCount.textContent=`${grants.filter(g=>g.active).length} usable / ${grants.length} total`;sessionCount.textContent=`${sessions.filter(s=>s.active).length} usable / ${sessions.length} total`;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))}; 646 - \\const render=()=>{renderControls();const isGrant=kind==='grants';title.textContent=isGrant?'OAuth app tokens':'password/app-password API tokens';hint.textContent=isGrant?'Current OAuth token rows. This is not yet a durable OAuth grant ledger; passkey OAuth sign-ins appear here.':'Token families from com.atproto.server.createSession, including account password and app-password tokens. These are separate from OAuth passkey sign-ins.';const items=shownItems();if(!items.length)return empty(filter==='usable'?'No usable token rows.':filter==='ended'?'No expired or revoked 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)}; 647 + \\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. New rows show passkey or password authorization; older rows may show unknown method.':'Token families from com.atproto.server.createSession. These are separate from OAuth passkey sign-ins.';const items=shownItems();if(!items.length)return empty(filter==='usable'?'No usable token rows.':filter==='ended'?'No expired or revoked 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)}; 647 648 \\chooser.addEventListener('click',(e)=>{const b=e.target.closest('button[data-kind]');if(!b)return;kind=b.dataset.kind;page=0;render()}); 648 649 \\filters.addEventListener('click',(e)=>{const b=e.target.closest('button[data-filter]');if(!b)return;filter=b.dataset.filter;page=0;render()}); 649 650 \\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='usable';page=0;render();status.textContent=''}catch(err){status.className='status error';status.textContent=err.message||String(err)}});
+25 -14
src/storage/store.zig
··· 78 78 expires_at: i64, 79 79 sub: ?[]const u8, 80 80 code: ?[]const u8, 81 + auth_method: ?[]const u8, 81 82 }; 82 83 83 84 pub const OAuthToken = struct { ··· 88 89 refresh_token: []const u8, 89 90 expires_at: i64, 90 91 revoked: bool, 92 + auth_method: ?[]const u8, 91 93 }; 92 94 93 95 pub const SessionInfo = struct { ··· 115 117 expires_at: i64, 116 118 revoked_at: ?i64, 117 119 active: bool, 120 + auth_method: ?[]const u8, 118 121 }; 119 122 120 123 pub const SessionTokenRow = struct { ··· 975 978 defer db_mutex.unlock(store_io); 976 979 try requireInitialized(); 977 980 const row = try conn.row( 978 - \\SELECT request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, response_mode, login_hint, dpop_jkt, expires_at, sub, code 981 + \\SELECT request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, response_mode, login_hint, dpop_jkt, expires_at, sub, code, auth_method 979 982 \\FROM oauth_requests 980 983 \\WHERE request_id = ? 981 984 , .{request_id}); ··· 984 987 return try oauthRequestFromRow(row.?, allocator); 985 988 } 986 989 987 - pub fn authorizeOAuthRequest(request_id: []const u8, did: []const u8, code: []const u8) !void { 990 + pub fn authorizeOAuthRequest(request_id: []const u8, did: []const u8, code: []const u8, auth_method: []const u8) !void { 988 991 db_mutex.lockUncancelable(store_io); 989 992 defer db_mutex.unlock(store_io); 990 993 try requireInitialized(); 991 994 try conn.exec( 992 995 \\UPDATE oauth_requests 993 - \\SET sub = ?, code = ? 996 + \\SET sub = ?, code = ?, auth_method = ? 994 997 \\WHERE request_id = ? 995 - , .{ did, code, request_id }); 998 + , .{ did, code, auth_method, request_id }); 996 999 } 997 1000 998 1001 pub fn consumeOAuthCode(allocator: std.mem.Allocator, code: []const u8) !?OAuthRequest { ··· 1000 1003 defer db_mutex.unlock(store_io); 1001 1004 try requireInitialized(); 1002 1005 const row = try conn.row( 1003 - \\SELECT request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, response_mode, login_hint, dpop_jkt, expires_at, sub, code 1006 + \\SELECT request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, response_mode, login_hint, dpop_jkt, expires_at, sub, code, auth_method 1004 1007 \\FROM oauth_requests 1005 1008 \\WHERE code = ? 1006 1009 , .{code}); ··· 1018 1021 access_token: []const u8, 1019 1022 refresh_token: []const u8, 1020 1023 expires_at: i64, 1024 + auth_method: ?[]const u8, 1021 1025 ) !void { 1022 1026 db_mutex.lockUncancelable(store_io); 1023 1027 defer db_mutex.unlock(store_io); 1024 1028 try requireInitialized(); 1025 1029 try conn.exec( 1026 - \\INSERT INTO oauth_tokens (access_token, refresh_token, did, client_id, scope, expires_at) 1027 - \\VALUES (?, ?, ?, ?, ?, ?) 1028 - , .{ access_token, refresh_token, did, client_id, scope, expires_at }); 1030 + \\INSERT INTO oauth_tokens (access_token, refresh_token, did, client_id, scope, expires_at, auth_method) 1031 + \\VALUES (?, ?, ?, ?, ?, ?, ?) 1032 + , .{ access_token, refresh_token, did, client_id, scope, expires_at, auth_method }); 1029 1033 } 1030 1034 1031 1035 pub fn getOAuthToken(allocator: std.mem.Allocator, token: []const u8) !?OAuthToken { ··· 1033 1037 defer db_mutex.unlock(store_io); 1034 1038 try requireInitialized(); 1035 1039 const row = try conn.row( 1036 - \\SELECT did, client_id, scope, access_token, refresh_token, expires_at, revoked_at 1040 + \\SELECT did, client_id, scope, access_token, refresh_token, expires_at, revoked_at, auth_method 1037 1041 \\FROM oauth_tokens 1038 1042 \\WHERE access_token = ? OR refresh_token = ? 1039 1043 \\ORDER BY created_at DESC ··· 1049 1053 .refresh_token = try allocator.dupe(u8, row.?.text(4)), 1050 1054 .expires_at = row.?.int(5), 1051 1055 .revoked = row.?.nullableInt(6) != null, 1056 + .auth_method = if (row.?.nullableText(7)) |text| try allocator.dupe(u8, text) else null, 1052 1057 }; 1053 1058 } 1054 1059 ··· 1068 1073 defer db_mutex.unlock(store_io); 1069 1074 try requireInitialized(); 1070 1075 const sql = if (active_only) 1071 - \\SELECT t.did, a.handle, t.client_id, t.scope, t.created_at, t.expires_at, t.revoked_at, 1076 + \\SELECT t.did, a.handle, t.client_id, t.scope, t.created_at, t.expires_at, t.revoked_at, t.auth_method, 1072 1077 \\ CASE WHEN t.revoked_at IS NULL AND t.expires_at > unixepoch() THEN 1 ELSE 0 END AS active 1073 1078 \\FROM oauth_tokens t 1074 1079 \\JOIN accounts a ON a.did = t.did ··· 1076 1081 \\ORDER BY t.created_at DESC 1077 1082 \\LIMIT ? 1078 1083 else 1079 - \\SELECT t.did, a.handle, t.client_id, t.scope, t.created_at, t.expires_at, t.revoked_at, 1084 + \\SELECT t.did, a.handle, t.client_id, t.scope, t.created_at, t.expires_at, t.revoked_at, t.auth_method, 1080 1085 \\ CASE WHEN t.revoked_at IS NULL AND t.expires_at > unixepoch() THEN 1 ELSE 0 END AS active 1081 1086 \\FROM oauth_tokens t 1082 1087 \\JOIN accounts a ON a.did = t.did ··· 1094 1099 defer db_mutex.unlock(store_io); 1095 1100 try requireInitialized(); 1096 1101 const sql = if (active_only) 1097 - \\SELECT t.did, a.handle, t.client_id, t.scope, t.created_at, t.expires_at, t.revoked_at, 1102 + \\SELECT t.did, a.handle, t.client_id, t.scope, t.created_at, t.expires_at, t.revoked_at, t.auth_method, 1098 1103 \\ CASE WHEN t.revoked_at IS NULL AND t.expires_at > unixepoch() THEN 1 ELSE 0 END AS active 1099 1104 \\FROM oauth_tokens t 1100 1105 \\JOIN accounts a ON a.did = t.did ··· 1102 1107 \\ORDER BY t.created_at DESC 1103 1108 \\LIMIT ? 1104 1109 else 1105 - \\SELECT t.did, a.handle, t.client_id, t.scope, t.created_at, t.expires_at, t.revoked_at, 1110 + \\SELECT t.did, a.handle, t.client_id, t.scope, t.created_at, t.expires_at, t.revoked_at, t.auth_method, 1106 1111 \\ CASE WHEN t.revoked_at IS NULL AND t.expires_at > unixepoch() THEN 1 ELSE 0 END AS active 1107 1112 \\FROM oauth_tokens t 1108 1113 \\JOIN accounts a ON a.did = t.did ··· 3554 3559 conn.execNoArgs("ALTER TABLE accounts ADD COLUMN signing_key BLOB") catch {}; 3555 3560 conn.execNoArgs("ALTER TABLE oauth_requests ADD COLUMN login_hint TEXT") catch {}; 3556 3561 conn.execNoArgs("ALTER TABLE oauth_requests ADD COLUMN response_mode TEXT NOT NULL DEFAULT 'query'") catch {}; 3562 + conn.execNoArgs("ALTER TABLE oauth_requests ADD COLUMN auth_method TEXT") catch {}; 3563 + conn.execNoArgs("ALTER TABLE oauth_tokens ADD COLUMN auth_method TEXT") catch {}; 3557 3564 conn.execNoArgs("ALTER TABLE session_tokens ADD COLUMN app_password_name TEXT") catch {}; 3558 3565 conn.execNoArgs("ALTER TABLE permissioned_space_records ADD COLUMN repo_rev TEXT NOT NULL DEFAULT ''") catch {}; 3559 3566 try migrateBlobTable(); ··· 4450 4457 .expires_at = row.int(10), 4451 4458 .sub = if (row.nullableText(11)) |text| try allocator.dupe(u8, text) else null, 4452 4459 .code = if (row.nullableText(12)) |text| try allocator.dupe(u8, text) else null, 4460 + .auth_method = if (row.nullableText(13)) |text| try allocator.dupe(u8, text) else null, 4453 4461 }; 4454 4462 } 4455 4463 ··· 4497 4505 .created_at = row.int(4), 4498 4506 .expires_at = row.int(5), 4499 4507 .revoked_at = row.nullableInt(6), 4500 - .active = row.int(7) != 0, 4508 + .auth_method = if (row.nullableText(7)) |text| try allocator.dupe(u8, text) else null, 4509 + .active = row.int(8) != 0, 4501 4510 }); 4502 4511 } 4503 4512 return out.toOwnedSlice(allocator); ··· 5085 5094 \\ expires_at INTEGER NOT NULL, 5086 5095 \\ sub TEXT, 5087 5096 \\ code TEXT UNIQUE, 5097 + \\ auth_method TEXT, 5088 5098 \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()) 5089 5099 \\) 5090 5100 , ··· 5095 5105 \\ client_id TEXT NOT NULL, 5096 5106 \\ scope TEXT NOT NULL, 5097 5107 \\ expires_at INTEGER NOT NULL, 5108 + \\ auth_method TEXT, 5098 5109 \\ revoked_at INTEGER, 5099 5110 \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()) 5100 5111 \\)