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

Configure Feed

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

Clarify session token labels

zzstoatzz (Jun 9, 2026, 12:04 AM -0500) 653fd54c 811af5cd

+17 -17
+17 -17
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>legacy API sessions</h2> 139 - \\<p class="hint">Active com.atproto.server sessions for this account, including account password and app-password tokens.</p> 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> 140 140 \\<div id="session-items"></div> 141 141 \\</section> 142 142 \\<section class="security-section"> 143 - \\<h2>OAuth app grants</h2> 144 - \\<p class="hint">Active ATProto OAuth grants issued to applications. Passkey OAuth sign-ins appear here.</p> 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> 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 app authorizations and legacy API sessions across accounts hosted on <strong>{s}</strong>.</p> 195 + \\<p>Operator view for OAuth app tokens and createSession 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 grants</span><strong id="grant-count">0</strong></button> 206 - \\<button class="choice" type="button" data-kind="sessions" aria-pressed="false"><span>legacy API sessions</span><strong id="session-count">0</strong></button> 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> 207 207 \\</div> 208 208 \\<div class="filters" id="filters" hidden> 209 - \\<button class="filter" type="button" data-filter="usable" aria-pressed="true">usable now</button> 210 - \\<button class="filter" type="button" data-filter="ended" aria-pressed="false">ended</button> 209 + \\<button class="filter" type="button" data-filter="usable" aria-pressed="true">usable token</button> 210 + \\<button class="filter" type="button" data-filter="ended" aria-pressed="false">expired/revoked</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 grants</h2> 215 - \\<p id="list-hint" class="hint">Apps authorized through ATProto OAuth. Usable means the grant has not expired or been revoked.</p> 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> 216 216 \\</div><div id="pager" class="pager"></div></div> 217 217 \\<div id="items"></div> 218 218 \\</section> ··· 617 617 \\const table=(root,cols,items,row)=>{root.textContent='';if(!items.length)return empty(root,'No active entries.');const wrap=document.createElement('div');wrap.className='table-wrap';const t=document.createElement('table');const thead=document.createElement('thead');const tr=document.createElement('tr');for(const col of cols){const th=document.createElement('th');th.textContent=col;tr.append(th)}thead.append(tr);const tbody=document.createElement('tbody');for(const item of items){const values=row(item);const tr=document.createElement('tr');for(const value of values){const td=document.createElement('td');if(value&&value.nodeType)td.append(value);else td.textContent=value??'';tr.append(td)}tbody.append(tr)}t.append(thead,tbody);wrap.append(t);root.append(wrap)}; 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 - \\const methodLabel=(s)=>{if(s.appPasswordName)return`app password: ${s.appPasswordName}`;if(s.authMethod==='password')return'account password session';if(s.authMethod==='app_password')return'app password session';if(s.authMethod==='app_password_privileged')return'privileged app password session';return s.authMethod}; 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 621 \\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 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 623 \\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)}}; ··· 636 636 \\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)}; 637 637 \\const empty=(text)=>{itemsRoot.textContent='';pagerRoot.textContent='';const p=document.createElement('div');p.className='empty';p.textContent=text;itemsRoot.append(p)}; 638 638 \\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'})}}; 639 - \\const methodLabel=(s)=>{if(s.appPasswordName)return`app password: ${s.appPasswordName}`;if(s.authMethod==='password')return'account password session';if(s.authMethod==='app_password')return'app password session';if(s.authMethod==='app_password_privileged')return'privileged app password session';return s.authMethod}; 639 + \\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}; 640 640 \\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 - \\const pill=(usable)=>{const span=document.createElement('span');span.className=`pill ${usable?'usable':'ended'}`;span.textContent=usable?'usable now':'ended';span.title=usable?'Not revoked and not expired.':'Expired or revoked.';return span}; 641 + \\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 642 \\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}`:'legacy com.atproto.server session');what.append(scope);const grid=document.createElement('div');grid.className='card-grid';if(isGrant){grid.append(stamp('authorized',item.createdAt),stamp('grant 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}; 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 644 \\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 645 \\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 grants':'legacy API sessions';hint.textContent=isGrant?'Apps authorized through ATProto OAuth. Usable means the grant has not expired or been revoked.':'Tokens from com.atproto.server.createSession, including account password and app-password sessions. Passkey sign-in for OAuth appears under OAuth app grants.';const items=shownItems();if(!items.length)return empty(filter==='usable'?'Nothing usable right now.':filter==='ended'?'No ended entries.':'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)}; 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 647 \\chooser.addEventListener('click',(e)=>{const b=e.target.closest('button[data-kind]');if(!b)return;kind=b.dataset.kind;page=0;render()}); 648 648 \\filters.addEventListener('click',(e)=>{const b=e.target.closest('button[data-filter]');if(!b)return;filter=b.dataset.filter;page=0;render()}); 649 - \\form.addEventListener('submit',async(e)=>{e.preventDefault();status.className='status';try{status.textContent='loading authorizations...';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 authorizations');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)}}); 649 + \\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)}}); 650 650 ; 651 651 652 652 test "validates webauthn credential key through tangled dependency" {