dev vouch dev on at. thats about it atvouch.dev
8
fork

Configure Feed

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

frontend: fix rpc scopes and use them on front page

Luna 8391f46f 6e161005

+66 -72
+1 -1
frontend/public/client-metadata.json
··· 3 3 "client_name": "atvouch", 4 4 "client_uri": "http://127.0.0.1:3051", 5 5 "redirect_uris": ["http://127.0.0.1:3051/"], 6 - "scope": "atproto repo:dev.atvouch.graph.vouch", 6 + "scope": "atproto repo?collection=dev.atvouch.graph.vouch rpc:dev.atvouch.graph.getCurrentUserVouches?aud=* rpc:dev.atvouch.graph.getRemoteVouches?aud=*", 7 7 "grant_types": ["authorization_code", "refresh_token"], 8 8 "response_types": ["code"], 9 9 "token_endpoint_auth_method": "none",
+5 -5
frontend/src/App.tsx
··· 13 13 deleteVouch, 14 14 listVouches, 15 15 checkVouchPaths, 16 - fetchVouchers, 16 + fetchRemoteVouchers, 17 17 resolveDidToHandle, 18 18 type CheckResult, 19 19 type VouchEntry, ··· 380 380 setLoading(true); 381 381 setError(null); 382 382 try { 383 - const dids = await fetchVouchers(agent.sub); 383 + const vouches = await fetchRemoteVouchers(agent); 384 384 const resolved: RemoteVoucher[] = await Promise.all( 385 - dids.map(async (did) => { 385 + vouches.map(async (v) => { 386 386 let handle: string | null = null; 387 387 try { 388 - handle = await resolveDidToHandle(did); 388 + handle = await resolveDidToHandle(v.creatorDid); 389 389 } catch { 390 390 // leave as null 391 391 } 392 - return { did, handle }; 392 + return { did: v.creatorDid, handle }; 393 393 }), 394 394 ); 395 395 setVouchers(resolved);
+60 -66
frontend/src/api.ts
··· 1 - import { XRPC } from "@atcute/client"; 1 + import { XRPC, withProxy } from "@atcute/client"; 2 2 import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 3 4 - // Slingshot: handle <-> DID resolution 4 + // Appview DID — used for proxied XRPC calls through the user's PDS 5 + const APPVIEW_DID = "did:web:api.atvouch.dev"; 6 + const APPVIEW_SERVICE_ID = "atvouch_appview"; 7 + 8 + // Slingshot: DID resolution 5 9 const SLINGSHOT = "https://slingshot.microcosm.blue"; 6 10 // Microcosm: reverse graph queries 7 11 const MICROCOSM = "https://constellation.microcosm.blue"; 8 12 13 + // Create an authenticated XRPC client that proxies through the user's PDS 14 + function appviewProxyRpc(agent: OAuthUserAgent): XRPC { 15 + const rpc = new XRPC({ handler: agent }); 16 + return withProxy(rpc, { type: APPVIEW_SERVICE_ID, service: APPVIEW_DID as `did:${string}` }); 17 + } 18 + 19 + // --- Appview vouch types --- 20 + 21 + interface AppviewVouchView { 22 + uri: string; 23 + creatorDid: string; 24 + targetDid: string; 25 + createdAt: string; 26 + } 27 + 28 + // --- Handle / DID resolution (AT Protocol standard) --- 29 + 9 30 export interface TypeaheadActor { 10 31 did: string; 11 32 handle: string; ··· 23 44 } 24 45 25 46 export async function resolveHandle(handle: string): Promise<string> { 26 - const url = `${SLINGSHOT}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`; 27 - const resp = await fetch(url); 47 + const params = new URLSearchParams({ handle }); 48 + const resp = await fetch(`${SLINGSHOT}/xrpc/com.atproto.identity.resolveHandle?${params}`); 28 49 if (!resp.ok) throw new Error(`Failed to resolve handle: ${resp.status}`); 29 50 const data: { did: string } = await resp.json(); 30 51 return data.did; ··· 38 59 return data.handle; 39 60 } 40 61 62 + // --- Vouch queries --- 63 + 64 + // Fetch who vouched for the current user (via appview proxy) 65 + export async function fetchRemoteVouchers(agent: OAuthUserAgent): Promise<AppviewVouchView[]> { 66 + const rpc = appviewProxyRpc(agent); 67 + const resp = await rpc.get("dev.atvouch.graph.getRemoteVouches" as any, {}); 68 + const data = resp.data as unknown as { vouches: AppviewVouchView[] }; 69 + return data.vouches; 70 + } 71 + 72 + // Fetch who vouches for an arbitrary DID (via Microcosm reverse graph) 41 73 export async function fetchVouchers(targetDID: string): Promise<string[]> { 42 74 const params = new URLSearchParams({ 43 75 target: targetDID, ··· 51 83 const data: { linking_dids: string[] } = await resp.json(); 52 84 return data.linking_dids; 53 85 } 86 + 87 + // --- Session & repo operations (via user's PDS) --- 54 88 55 89 export interface SessionInfo { 56 90 did: string; ··· 124 158 }); 125 159 } 126 160 127 - // List all DIDs the user has vouched for 161 + // List all DIDs the user has vouched for (via appview proxy) 128 162 async function listMyVouches(agent: OAuthUserAgent): Promise<string[]> { 129 - const rpc = new XRPC({ handler: agent }); 130 - const subjects: string[] = []; 131 - let cursor: string | undefined; 132 - 133 - for (;;) { 134 - const resp = await rpc.get("com.atproto.repo.listRecords", { 135 - params: { 136 - repo: agent.sub, 137 - collection: "dev.atvouch.graph.vouch", 138 - limit: 100, 139 - cursor, 140 - }, 141 - }); 142 - 143 - const data = resp.data as unknown as { 144 - records: Array<{ value: { subject: string } }>; 145 - cursor?: string; 146 - }; 147 - 148 - for (const rec of data.records) { 149 - if (rec.value.subject) subjects.push(rec.value.subject); 150 - } 151 - 152 - if (!data.cursor) break; 153 - cursor = data.cursor; 154 - } 155 - 156 - return subjects; 163 + const rpc = appviewProxyRpc(agent); 164 + const resp = await rpc.get("dev.atvouch.graph.getCurrentUserVouches" as any, {}); 165 + const data = resp.data as unknown as { vouches: AppviewVouchView[] }; 166 + return data.vouches.map((v) => v.targetDid); 157 167 } 168 + 169 + // --- Vouch listing (via appview proxy) --- 158 170 159 171 export interface VouchEntry { 160 172 did: string; ··· 165 177 } 166 178 167 179 export async function listVouches(agent: OAuthUserAgent): Promise<VouchEntry[]> { 168 - const rpc = new XRPC({ handler: agent }); 169 - const entries: VouchEntry[] = []; 170 - let cursor: string | undefined; 180 + const rpc = appviewProxyRpc(agent); 171 181 172 - for (;;) { 173 - const resp = await rpc.get("com.atproto.repo.listRecords", { 174 - params: { 175 - repo: agent.sub, 176 - collection: "dev.atvouch.graph.vouch", 177 - limit: 100, 178 - cursor, 179 - }, 180 - }); 182 + const resp = await rpc.get("dev.atvouch.graph.getCurrentUserVouches" as any, {}); 183 + const data = resp.data as unknown as { vouches: AppviewVouchView[] }; 181 184 182 - const data = resp.data as unknown as { 183 - records: Array<{ uri: string; value: { subject: string; createdAt: string } }>; 184 - cursor?: string; 185 + const entries: VouchEntry[] = data.vouches.map((v) => { 186 + const rkey = v.uri.split("/").pop()!; 187 + return { 188 + did: v.targetDid, 189 + handle: null, 190 + createdAt: v.createdAt, 191 + rkey, 192 + valid: rkey.startsWith("did:") && rkey === v.targetDid, 185 193 }; 186 - 187 - for (const rec of data.records) { 188 - const rkey = rec.uri.split("/").pop()!; 189 - const valid = rkey.startsWith("did:") && rkey === rec.value.subject; 190 - entries.push({ 191 - did: rec.value.subject, 192 - handle: null, 193 - createdAt: rec.value.createdAt, 194 - rkey, 195 - valid, 196 - }); 197 - } 198 - 199 - if (!data.cursor) break; 200 - cursor = data.cursor; 201 - } 194 + }); 202 195 203 196 // Resolve DIDs to handles (only for valid entries) 204 197 await Promise.all( ··· 213 206 214 207 return entries; 215 208 } 209 + 210 + // --- Vouch path checking --- 216 211 217 212 export interface CheckResult { 218 213 targetDID: string; ··· 231 226 return { targetDID, directVouch: true, paths: [], handleMap: {} }; 232 227 } 233 228 234 - // Build reverse graph from target (up to 3 levels) 229 + // Build reverse graph from target via Microcosm (up to 3 levels) 235 230 const reverseGraph: Record<string, Set<string>> = {}; 236 231 237 232 // Level 1: who vouches for target ··· 291 286 292 287 return { targetDID, directVouch: false, paths, handleMap }; 293 288 } 294 -