Barazo AppView backend
barazo.forum
1import { Agent } from '@atproto/api'
2import type { NodeOAuthClient } from '@atproto/oauth-client-node'
3import type { Logger } from './logger.js'
4
5// ---------------------------------------------------------------------------
6// Types
7// ---------------------------------------------------------------------------
8
9/** Result of a successful record creation or update on the user's PDS. */
10export interface PdsWriteResult {
11 uri: string
12 cid: string
13}
14
15/** PDS client interface for creating, updating, and deleting AT Protocol records. */
16export interface PdsClient {
17 createRecord(
18 did: string,
19 collection: string,
20 record: Record<string, unknown>
21 ): Promise<PdsWriteResult>
22
23 updateRecord(
24 did: string,
25 collection: string,
26 rkey: string,
27 record: Record<string, unknown>
28 ): Promise<PdsWriteResult>
29
30 deleteRecord(did: string, collection: string, rkey: string): Promise<void>
31
32 /**
33 * Upload a binary blob (e.g. an image) to the user's PDS.
34 * Returns the blob reference object suitable for embedding in records.
35 */
36 uploadBlob(did: string, data: Uint8Array, mimeType: string): Promise<unknown>
37}
38
39// ---------------------------------------------------------------------------
40// Error handling
41// ---------------------------------------------------------------------------
42
43/**
44 * Extract a meaningful message from an unknown PDS error.
45 */
46function pdsErrorMessage(err: unknown): string {
47 if (err instanceof Error) {
48 return err.message
49 }
50 return String(err)
51}
52
53// ---------------------------------------------------------------------------
54// Factory
55// ---------------------------------------------------------------------------
56
57/**
58 * Create a PDS client that uses the OAuth client to restore user sessions
59 * and perform XRPC operations on the user's PDS.
60 *
61 * @param oauthClient - The AT Protocol OAuth client (provides session restore)
62 * @param logger - Pino logger for structured logging
63 */
64export function createPdsClient(oauthClient: NodeOAuthClient, logger: Logger): PdsClient {
65 /**
66 * Restore an authenticated Agent for the given DID.
67 * The OAuth client manages token refresh transparently.
68 */
69 async function getAgent(did: string): Promise<Agent> {
70 const session = await oauthClient.restore(did)
71 return new Agent(session)
72 }
73
74 return {
75 async createRecord(
76 did: string,
77 collection: string,
78 record: Record<string, unknown>
79 ): Promise<PdsWriteResult> {
80 logger.debug({ did, collection }, 'PDS createRecord')
81
82 try {
83 const agent = await getAgent(did)
84 const response = await agent.com.atproto.repo.createRecord({
85 repo: did,
86 collection,
87 record: { $type: collection, ...record },
88 })
89
90 return { uri: response.data.uri, cid: response.data.cid }
91 } catch (err: unknown) {
92 logger.error({ err, did, collection }, 'PDS createRecord failed: %s', pdsErrorMessage(err))
93 throw err
94 }
95 },
96
97 async updateRecord(
98 did: string,
99 collection: string,
100 rkey: string,
101 record: Record<string, unknown>
102 ): Promise<PdsWriteResult> {
103 logger.debug({ did, collection, rkey }, 'PDS updateRecord')
104
105 try {
106 const agent = await getAgent(did)
107 const response = await agent.com.atproto.repo.putRecord({
108 repo: did,
109 collection,
110 rkey,
111 record: { $type: collection, ...record },
112 })
113
114 return { uri: response.data.uri, cid: response.data.cid }
115 } catch (err: unknown) {
116 logger.error(
117 { err, did, collection, rkey },
118 'PDS updateRecord failed: %s',
119 pdsErrorMessage(err)
120 )
121 throw err
122 }
123 },
124
125 async deleteRecord(did: string, collection: string, rkey: string): Promise<void> {
126 logger.debug({ did, collection, rkey }, 'PDS deleteRecord')
127
128 try {
129 const agent = await getAgent(did)
130 await agent.com.atproto.repo.deleteRecord({
131 repo: did,
132 collection,
133 rkey,
134 })
135 } catch (err: unknown) {
136 logger.error(
137 { err, did, collection, rkey },
138 'PDS deleteRecord failed: %s',
139 pdsErrorMessage(err)
140 )
141 throw err
142 }
143 },
144
145 async uploadBlob(did: string, data: Uint8Array, mimeType: string): Promise<unknown> {
146 logger.debug({ did, mimeType, size: data.length }, 'PDS uploadBlob')
147
148 try {
149 const agent = await getAgent(did)
150 const response = await agent.uploadBlob(data, {
151 encoding: mimeType,
152 })
153
154 return response.data.blob
155 } catch (err: unknown) {
156 logger.error({ err, did, mimeType }, 'PDS uploadBlob failed: %s', pdsErrorMessage(err))
157 throw err
158 }
159 },
160 }
161}