👁️
5
fork

Configure Feed

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

start on atproto persistance

+264
+213
src/lib/atproto-client.ts
··· 1 + /** 2 + * ATProto client utilities for deck record CRUD operations 3 + * Reads via Slingshot (cached), writes via PDS (authenticated) 4 + */ 5 + 6 + import type { At, Did } from "@atcute/lexicons"; 7 + import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; 8 + import type { ComDeckbelcherDeckList } from "./lexicons/index"; 9 + 10 + const SLINGSHOT_BASE = "https://slingshot.microcosm.blue"; 11 + const COLLECTION = "com.deckbelcher.deck.list"; 12 + 13 + // Branded types for type safety 14 + declare const PdsUrlBrand: unique symbol; 15 + export type PdsUrl = string & { readonly [PdsUrlBrand]: typeof PdsUrlBrand }; 16 + 17 + declare const RkeyBrand: unique symbol; 18 + export type Rkey = string & { readonly [RkeyBrand]: typeof RkeyBrand }; 19 + 20 + export function asPdsUrl(url: string): PdsUrl { 21 + return url as PdsUrl; 22 + } 23 + 24 + export function asRkey(rkey: string): Rkey { 25 + return rkey as Rkey; 26 + } 27 + 28 + export interface DeckRecordResponse { 29 + uri: At.Uri; 30 + cid: string; 31 + value: ComDeckbelcherDeckList.Main; 32 + } 33 + 34 + export interface ListRecordsResponse { 35 + records: DeckRecordResponse[]; 36 + cursor?: string; 37 + } 38 + 39 + export type Result<T, E = Error> = 40 + | { success: true; data: T } 41 + | { success: false; error: E }; 42 + 43 + /** 44 + * Fetch a deck record via Slingshot (cached, public read) 45 + */ 46 + export async function getDeckRecord( 47 + did: Did, 48 + rkey: Rkey, 49 + ): Promise<Result<DeckRecordResponse>> { 50 + try { 51 + const url = new URL(`${SLINGSHOT_BASE}/xrpc/com.atproto.repo.getRecord`); 52 + url.searchParams.set("repo", did); 53 + url.searchParams.set("collection", COLLECTION); 54 + url.searchParams.set("rkey", rkey); 55 + 56 + const response = await fetch(url.toString()); 57 + 58 + if (!response.ok) { 59 + const error = await response.json().catch(() => ({})); 60 + return { 61 + success: false, 62 + error: new Error( 63 + error.message || `Failed to fetch deck: ${response.statusText}`, 64 + ), 65 + }; 66 + } 67 + 68 + const data = await response.json(); 69 + return { success: true, data }; 70 + } catch (error) { 71 + return { 72 + success: false, 73 + error: error instanceof Error ? error : new Error(String(error)), 74 + }; 75 + } 76 + } 77 + 78 + /** 79 + * Create a new deck record (authenticated write to PDS) 80 + */ 81 + export async function createDeckRecord( 82 + agent: OAuthUserAgent, 83 + record: ComDeckbelcherDeckList.Main, 84 + ): Promise<Result<{ uri: At.Uri; cid: string; rkey: Rkey }>> { 85 + try { 86 + const response = await agent.rpc.call("com.atproto.repo.createRecord", { 87 + data: { 88 + repo: agent.did, 89 + collection: COLLECTION, 90 + record, 91 + }, 92 + }); 93 + 94 + if (!response.success) { 95 + return { success: false, error: new Error("Failed to create deck record") }; 96 + } 97 + 98 + // Extract rkey from the URI (at://did:plc:.../collection/rkey) 99 + const uri = response.data.uri; 100 + const rkey = uri.split("/").pop(); 101 + if (!rkey) { 102 + return { 103 + success: false, 104 + error: new Error("Invalid URI returned from createRecord"), 105 + }; 106 + } 107 + 108 + return { 109 + success: true, 110 + data: { ...response.data, rkey: asRkey(rkey) }, 111 + }; 112 + } catch (error) { 113 + return { 114 + success: false, 115 + error: error instanceof Error ? error : new Error(String(error)), 116 + }; 117 + } 118 + } 119 + 120 + /** 121 + * Update an existing deck record (authenticated write to PDS) 122 + */ 123 + export async function updateDeckRecord( 124 + agent: OAuthUserAgent, 125 + rkey: Rkey, 126 + record: ComDeckbelcherDeckList.Main, 127 + ): Promise<Result<{ uri: At.Uri; cid: string }>> { 128 + try { 129 + const response = await agent.rpc.call("com.atproto.repo.putRecord", { 130 + data: { 131 + repo: agent.did, 132 + collection: COLLECTION, 133 + rkey, 134 + record, 135 + }, 136 + }); 137 + 138 + if (!response.success) { 139 + return { success: false, error: new Error("Failed to update deck record") }; 140 + } 141 + 142 + return { success: true, data: response.data }; 143 + } catch (error) { 144 + return { 145 + success: false, 146 + error: error instanceof Error ? error : new Error(String(error)), 147 + }; 148 + } 149 + } 150 + 151 + /** 152 + * List all deck records for a user (direct PDS call) 153 + * Requires PDS URL for the target user 154 + */ 155 + export async function listUserDecks( 156 + pdsUrl: PdsUrl, 157 + did: Did, 158 + ): Promise<Result<ListRecordsResponse>> { 159 + try { 160 + const url = new URL(`${pdsUrl}/xrpc/com.atproto.repo.listRecords`); 161 + url.searchParams.set("repo", did); 162 + url.searchParams.set("collection", COLLECTION); 163 + 164 + const response = await fetch(url.toString()); 165 + 166 + if (!response.ok) { 167 + const error = await response.json().catch(() => ({})); 168 + return { 169 + success: false, 170 + error: new Error( 171 + error.message || `Failed to list decks: ${response.statusText}`, 172 + ), 173 + }; 174 + } 175 + 176 + const data = await response.json(); 177 + return { success: true, data }; 178 + } catch (error) { 179 + return { 180 + success: false, 181 + error: error instanceof Error ? error : new Error(String(error)), 182 + }; 183 + } 184 + } 185 + 186 + /** 187 + * Delete a deck record (authenticated write to PDS) 188 + */ 189 + export async function deleteDeckRecord( 190 + agent: OAuthUserAgent, 191 + rkey: Rkey, 192 + ): Promise<Result<void>> { 193 + try { 194 + const response = await agent.rpc.call("com.atproto.repo.deleteRecord", { 195 + data: { 196 + repo: agent.did, 197 + collection: COLLECTION, 198 + rkey, 199 + }, 200 + }); 201 + 202 + if (!response.success) { 203 + return { success: false, error: new Error("Failed to delete deck record") }; 204 + } 205 + 206 + return { success: true, data: undefined }; 207 + } catch (error) { 208 + return { 209 + success: false, 210 + error: error instanceof Error ? error : new Error(String(error)), 211 + }; 212 + } 213 + }
+51
src/lib/identity.ts
··· 1 + /** 2 + * Identity resolution utilities using Slingshot's resolveMiniDoc endpoint 3 + */ 4 + 5 + import type { Did } from "@atcute/lexicons"; 6 + 7 + export interface MiniDoc { 8 + did: Did; 9 + handle: string; 10 + pds: string; 11 + signing_key: string; 12 + } 13 + 14 + const SLINGSHOT_BASE = "https://slingshot.microcosm.blue"; 15 + 16 + /** 17 + * Resolve a handle or DID to a MiniDoc containing identity information 18 + * Uses Slingshot's cached identity resolver 19 + */ 20 + export async function resolveMiniDoc(identifier: string): Promise<MiniDoc> { 21 + const url = new URL(`${SLINGSHOT_BASE}/xrpc/com.bad-example.identity.resolveMiniDoc`); 22 + url.searchParams.set("identifier", identifier); 23 + 24 + const response = await fetch(url.toString()); 25 + 26 + if (!response.ok) { 27 + const error = await response.json().catch(() => ({})); 28 + throw new Error( 29 + error.message || `Failed to resolve identity: ${response.statusText}`, 30 + ); 31 + } 32 + 33 + return response.json(); 34 + } 35 + 36 + /** 37 + * Extract the PDS URL for a given DID 38 + * Useful for constructing direct PDS requests 39 + */ 40 + export async function getPdsForDid(did: Did): Promise<string> { 41 + const doc = await resolveMiniDoc(did); 42 + return doc.pds; 43 + } 44 + 45 + /** 46 + * Resolve a handle to its DID 47 + */ 48 + export async function resolveHandleToDid(handle: string): Promise<Did> { 49 + const doc = await resolveMiniDoc(handle); 50 + return doc.did; 51 + }