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