this repo has no description
1import { z } from 'zod'
2import { ok, err, type Result } from './types/result'
3import { ApiError } from './api'
4import type { AccessToken, RefreshToken, Did, Handle, Nsid, Rkey } from './types/branded'
5import {
6 sessionSchema,
7 serverDescriptionSchema,
8 appPasswordSchema,
9 createdAppPasswordSchema,
10 listSessionsResponseSchema,
11 totpStatusSchema,
12 totpSecretSchema,
13 enableTotpResponseSchema,
14 listPasskeysResponseSchema,
15 listTrustedDevicesResponseSchema,
16 reauthStatusSchema,
17 notificationPrefsSchema,
18 didDocumentSchema,
19 repoDescriptionSchema,
20 listRecordsResponseSchema,
21 recordResponseSchema,
22 createRecordResponseSchema,
23 serverStatsSchema,
24 serverConfigSchema,
25 passwordStatusSchema,
26 successResponseSchema,
27 legacyLoginPreferenceSchema,
28 accountInfoSchema,
29 searchAccountsResponseSchema,
30 listBackupsResponseSchema,
31 createBackupResponseSchema,
32 type ValidatedSession,
33 type ValidatedServerDescription,
34 type ValidatedListSessionsResponse,
35 type ValidatedTotpStatus,
36 type ValidatedTotpSecret,
37 type ValidatedEnableTotpResponse,
38 type ValidatedListPasskeysResponse,
39 type ValidatedListTrustedDevicesResponse,
40 type ValidatedReauthStatus,
41 type ValidatedNotificationPrefs,
42 type ValidatedDidDocument,
43 type ValidatedRepoDescription,
44 type ValidatedListRecordsResponse,
45 type ValidatedRecordResponse,
46 type ValidatedCreateRecordResponse,
47 type ValidatedServerStats,
48 type ValidatedServerConfig,
49 type ValidatedPasswordStatus,
50 type ValidatedSuccessResponse,
51 type ValidatedLegacyLoginPreference,
52 type ValidatedAccountInfo,
53 type ValidatedSearchAccountsResponse,
54 type ValidatedListBackupsResponse,
55 type ValidatedCreateBackupResponse,
56 type ValidatedCreatedAppPassword,
57 type ValidatedAppPassword,
58} from './types/schemas'
59
60const API_BASE = '/xrpc'
61
62interface XrpcOptions {
63 method?: 'GET' | 'POST'
64 params?: Record<string, string>
65 body?: unknown
66 token?: string
67}
68
69class ValidationError extends Error {
70 constructor(
71 public issues: z.ZodIssue[],
72 message: string = 'API response validation failed'
73 ) {
74 super(message)
75 this.name = 'ValidationError'
76 }
77}
78
79async function xrpcValidated<T>(
80 method: string,
81 schema: z.ZodType<T>,
82 options?: XrpcOptions
83): Promise<Result<T, ApiError | ValidationError>> {
84 const { method: httpMethod = 'GET', params, body, token } = options ?? {}
85 let url = `${API_BASE}/${method}`
86 if (params) {
87 const searchParams = new URLSearchParams(params)
88 url += `?${searchParams}`
89 }
90 const headers: Record<string, string> = {}
91 if (token) {
92 headers['Authorization'] = `Bearer ${token}`
93 }
94 if (body) {
95 headers['Content-Type'] = 'application/json'
96 }
97
98 try {
99 const res = await fetch(url, {
100 method: httpMethod,
101 headers,
102 body: body ? JSON.stringify(body) : undefined,
103 })
104
105 if (!res.ok) {
106 const errData = await res.json().catch(() => ({
107 error: 'Unknown',
108 message: res.statusText,
109 }))
110 return err(new ApiError(res.status, errData.error, errData.message))
111 }
112
113 const data = await res.json()
114 const parsed = schema.safeParse(data)
115
116 if (!parsed.success) {
117 return err(new ValidationError(parsed.error.issues))
118 }
119
120 return ok(parsed.data)
121 } catch (e) {
122 if (e instanceof ApiError || e instanceof ValidationError) {
123 return err(e)
124 }
125 return err(new ApiError(0, 'Unknown', e instanceof Error ? e.message : String(e)))
126 }
127}
128
129export const validatedApi = {
130 getSession(token: AccessToken): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
131 return xrpcValidated('com.atproto.server.getSession', sessionSchema, { token })
132 },
133
134 refreshSession(refreshJwt: RefreshToken): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
135 return xrpcValidated('com.atproto.server.refreshSession', sessionSchema, {
136 method: 'POST',
137 token: refreshJwt,
138 })
139 },
140
141 createSession(
142 identifier: string,
143 password: string
144 ): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
145 return xrpcValidated('com.atproto.server.createSession', sessionSchema, {
146 method: 'POST',
147 body: { identifier, password },
148 })
149 },
150
151 describeServer(): Promise<Result<ValidatedServerDescription, ApiError | ValidationError>> {
152 return xrpcValidated('com.atproto.server.describeServer', serverDescriptionSchema)
153 },
154
155 listAppPasswords(
156 token: AccessToken
157 ): Promise<Result<{ passwords: ValidatedAppPassword[] }, ApiError | ValidationError>> {
158 return xrpcValidated(
159 'com.atproto.server.listAppPasswords',
160 z.object({ passwords: z.array(appPasswordSchema) }),
161 { token }
162 )
163 },
164
165 createAppPassword(
166 token: AccessToken,
167 name: string,
168 scopes?: string
169 ): Promise<Result<ValidatedCreatedAppPassword, ApiError | ValidationError>> {
170 return xrpcValidated('com.atproto.server.createAppPassword', createdAppPasswordSchema, {
171 method: 'POST',
172 token,
173 body: { name, scopes },
174 })
175 },
176
177 listSessions(token: AccessToken): Promise<Result<ValidatedListSessionsResponse, ApiError | ValidationError>> {
178 return xrpcValidated('_account.listSessions', listSessionsResponseSchema, { token })
179 },
180
181 getTotpStatus(token: AccessToken): Promise<Result<ValidatedTotpStatus, ApiError | ValidationError>> {
182 return xrpcValidated('com.atproto.server.getTotpStatus', totpStatusSchema, { token })
183 },
184
185 createTotpSecret(token: AccessToken): Promise<Result<ValidatedTotpSecret, ApiError | ValidationError>> {
186 return xrpcValidated('com.atproto.server.createTotpSecret', totpSecretSchema, {
187 method: 'POST',
188 token,
189 })
190 },
191
192 enableTotp(
193 token: AccessToken,
194 code: string
195 ): Promise<Result<ValidatedEnableTotpResponse, ApiError | ValidationError>> {
196 return xrpcValidated('com.atproto.server.enableTotp', enableTotpResponseSchema, {
197 method: 'POST',
198 token,
199 body: { code },
200 })
201 },
202
203 listPasskeys(token: AccessToken): Promise<Result<ValidatedListPasskeysResponse, ApiError | ValidationError>> {
204 return xrpcValidated('com.atproto.server.listPasskeys', listPasskeysResponseSchema, { token })
205 },
206
207 listTrustedDevices(
208 token: AccessToken
209 ): Promise<Result<ValidatedListTrustedDevicesResponse, ApiError | ValidationError>> {
210 return xrpcValidated('_account.listTrustedDevices', listTrustedDevicesResponseSchema, { token })
211 },
212
213 getReauthStatus(token: AccessToken): Promise<Result<ValidatedReauthStatus, ApiError | ValidationError>> {
214 return xrpcValidated('_account.getReauthStatus', reauthStatusSchema, { token })
215 },
216
217 getNotificationPrefs(
218 token: AccessToken
219 ): Promise<Result<ValidatedNotificationPrefs, ApiError | ValidationError>> {
220 return xrpcValidated('_account.getNotificationPrefs', notificationPrefsSchema, { token })
221 },
222
223 getDidDocument(token: AccessToken): Promise<Result<ValidatedDidDocument, ApiError | ValidationError>> {
224 return xrpcValidated('_account.getDidDocument', didDocumentSchema, { token })
225 },
226
227 describeRepo(
228 token: AccessToken,
229 repo: Did
230 ): Promise<Result<ValidatedRepoDescription, ApiError | ValidationError>> {
231 return xrpcValidated('com.atproto.repo.describeRepo', repoDescriptionSchema, {
232 token,
233 params: { repo },
234 })
235 },
236
237 listRecords(
238 token: AccessToken,
239 repo: Did,
240 collection: Nsid,
241 options?: { limit?: number; cursor?: string; reverse?: boolean }
242 ): Promise<Result<ValidatedListRecordsResponse, ApiError | ValidationError>> {
243 const params: Record<string, string> = { repo, collection }
244 if (options?.limit) params.limit = String(options.limit)
245 if (options?.cursor) params.cursor = options.cursor
246 if (options?.reverse) params.reverse = 'true'
247 return xrpcValidated('com.atproto.repo.listRecords', listRecordsResponseSchema, {
248 token,
249 params,
250 })
251 },
252
253 getRecord(
254 token: AccessToken,
255 repo: Did,
256 collection: Nsid,
257 rkey: Rkey
258 ): Promise<Result<ValidatedRecordResponse, ApiError | ValidationError>> {
259 return xrpcValidated('com.atproto.repo.getRecord', recordResponseSchema, {
260 token,
261 params: { repo, collection, rkey },
262 })
263 },
264
265 createRecord(
266 token: AccessToken,
267 repo: Did,
268 collection: Nsid,
269 record: unknown,
270 rkey?: Rkey
271 ): Promise<Result<ValidatedCreateRecordResponse, ApiError | ValidationError>> {
272 return xrpcValidated('com.atproto.repo.createRecord', createRecordResponseSchema, {
273 method: 'POST',
274 token,
275 body: { repo, collection, record, rkey },
276 })
277 },
278
279 getServerStats(token: AccessToken): Promise<Result<ValidatedServerStats, ApiError | ValidationError>> {
280 return xrpcValidated('_admin.getServerStats', serverStatsSchema, { token })
281 },
282
283 getServerConfig(): Promise<Result<ValidatedServerConfig, ApiError | ValidationError>> {
284 return xrpcValidated('_server.getConfig', serverConfigSchema)
285 },
286
287 getPasswordStatus(token: AccessToken): Promise<Result<ValidatedPasswordStatus, ApiError | ValidationError>> {
288 return xrpcValidated('_account.getPasswordStatus', passwordStatusSchema, { token })
289 },
290
291 changePassword(
292 token: AccessToken,
293 currentPassword: string,
294 newPassword: string
295 ): Promise<Result<ValidatedSuccessResponse, ApiError | ValidationError>> {
296 return xrpcValidated('_account.changePassword', successResponseSchema, {
297 method: 'POST',
298 token,
299 body: { currentPassword, newPassword },
300 })
301 },
302
303 getLegacyLoginPreference(
304 token: AccessToken
305 ): Promise<Result<ValidatedLegacyLoginPreference, ApiError | ValidationError>> {
306 return xrpcValidated('_account.getLegacyLoginPreference', legacyLoginPreferenceSchema, { token })
307 },
308
309 getAccountInfo(
310 token: AccessToken,
311 did: Did
312 ): Promise<Result<ValidatedAccountInfo, ApiError | ValidationError>> {
313 return xrpcValidated('com.atproto.admin.getAccountInfo', accountInfoSchema, {
314 token,
315 params: { did },
316 })
317 },
318
319 searchAccounts(
320 token: AccessToken,
321 options?: { handle?: string; cursor?: string; limit?: number }
322 ): Promise<Result<ValidatedSearchAccountsResponse, ApiError | ValidationError>> {
323 const params: Record<string, string> = {}
324 if (options?.handle) params.handle = options.handle
325 if (options?.cursor) params.cursor = options.cursor
326 if (options?.limit) params.limit = String(options.limit)
327 return xrpcValidated('com.atproto.admin.searchAccounts', searchAccountsResponseSchema, {
328 token,
329 params,
330 })
331 },
332
333 listBackups(token: AccessToken): Promise<Result<ValidatedListBackupsResponse, ApiError | ValidationError>> {
334 return xrpcValidated('_backup.listBackups', listBackupsResponseSchema, { token })
335 },
336
337 createBackup(token: AccessToken): Promise<Result<ValidatedCreateBackupResponse, ApiError | ValidationError>> {
338 return xrpcValidated('_backup.createBackup', createBackupResponseSchema, {
339 method: 'POST',
340 token,
341 })
342 },
343}
344
345export { ValidationError }