this repo has no description
1const API_BASE = '/xrpc'
2
3export class ApiError extends Error {
4 public did?: string
5 public reauthMethods?: string[]
6 constructor(public status: number, public error: string, message: string, did?: string, reauthMethods?: string[]) {
7 super(message)
8 this.name = 'ApiError'
9 this.did = did
10 this.reauthMethods = reauthMethods
11 }
12}
13
14let tokenRefreshCallback: (() => Promise<string | null>) | null = null
15
16export function setTokenRefreshCallback(callback: () => Promise<string | null>) {
17 tokenRefreshCallback = callback
18}
19
20async function xrpc<T>(method: string, options?: {
21 method?: 'GET' | 'POST'
22 params?: Record<string, string>
23 body?: unknown
24 token?: string
25 skipRetry?: boolean
26}): Promise<T> {
27 const { method: httpMethod = 'GET', params, body, token, skipRetry } = options ?? {}
28 let url = `${API_BASE}/${method}`
29 if (params) {
30 const searchParams = new URLSearchParams(params)
31 url += `?${searchParams}`
32 }
33 const headers: Record<string, string> = {}
34 if (token) {
35 headers['Authorization'] = `Bearer ${token}`
36 }
37 if (body) {
38 headers['Content-Type'] = 'application/json'
39 }
40 const res = await fetch(url, {
41 method: httpMethod,
42 headers,
43 body: body ? JSON.stringify(body) : undefined,
44 })
45 if (!res.ok) {
46 const err = await res.json().catch(() => ({ error: 'Unknown', message: res.statusText }))
47 if (res.status === 401 && err.error === 'AuthenticationFailed' && token && tokenRefreshCallback && !skipRetry) {
48 const newToken = await tokenRefreshCallback()
49 if (newToken && newToken !== token) {
50 return xrpc(method, { ...options, token: newToken, skipRetry: true })
51 }
52 }
53 throw new ApiError(res.status, err.error, err.message, err.did, err.reauthMethods)
54 }
55 return res.json()
56}
57
58export interface Session {
59 did: string
60 handle: string
61 email?: string
62 emailConfirmed?: boolean
63 preferredChannel?: string
64 preferredChannelVerified?: boolean
65 isAdmin?: boolean
66 active?: boolean
67 status?: 'active' | 'deactivated'
68 accessJwt: string
69 refreshJwt: string
70}
71
72export interface AppPassword {
73 name: string
74 createdAt: string
75}
76
77export interface InviteCode {
78 code: string
79 available: number
80 disabled: boolean
81 forAccount: string
82 createdBy: string
83 createdAt: string
84 uses: { usedBy: string; usedAt: string }[]
85}
86
87export type VerificationChannel = 'email' | 'discord' | 'telegram' | 'signal'
88
89export type DidType = 'plc' | 'web' | 'web-external'
90
91export interface CreateAccountParams {
92 handle: string
93 email: string
94 password: string
95 inviteCode?: string
96 didType?: DidType
97 did?: string
98 verificationChannel?: VerificationChannel
99 discordId?: string
100 telegramUsername?: string
101 signalNumber?: string
102}
103
104export interface CreateAccountResult {
105 handle: string
106 did: string
107 verificationRequired: boolean
108 verificationChannel: string
109}
110
111export interface ConfirmSignupResult {
112 accessJwt: string
113 refreshJwt: string
114 handle: string
115 did: string
116 email?: string
117 emailConfirmed?: boolean
118 preferredChannel?: string
119 preferredChannelVerified?: boolean
120}
121
122export const api = {
123 async createAccount(params: CreateAccountParams): Promise<CreateAccountResult> {
124 return xrpc('com.atproto.server.createAccount', {
125 method: 'POST',
126 body: {
127 handle: params.handle,
128 email: params.email,
129 password: params.password,
130 inviteCode: params.inviteCode,
131 didType: params.didType,
132 did: params.did,
133 verificationChannel: params.verificationChannel,
134 discordId: params.discordId,
135 telegramUsername: params.telegramUsername,
136 signalNumber: params.signalNumber,
137 },
138 })
139 },
140
141 async confirmSignup(did: string, verificationCode: string): Promise<ConfirmSignupResult> {
142 return xrpc('com.atproto.server.confirmSignup', {
143 method: 'POST',
144 body: { did, verificationCode },
145 })
146 },
147
148 async resendVerification(did: string): Promise<{ success: boolean }> {
149 return xrpc('com.atproto.server.resendVerification', {
150 method: 'POST',
151 body: { did },
152 })
153 },
154
155 async createSession(identifier: string, password: string): Promise<Session> {
156 return xrpc('com.atproto.server.createSession', {
157 method: 'POST',
158 body: { identifier, password },
159 })
160 },
161
162 async getSession(token: string): Promise<Session> {
163 return xrpc('com.atproto.server.getSession', { token })
164 },
165
166 async refreshSession(refreshJwt: string): Promise<Session> {
167 return xrpc('com.atproto.server.refreshSession', {
168 method: 'POST',
169 token: refreshJwt,
170 })
171 },
172
173 async deleteSession(token: string): Promise<void> {
174 await xrpc('com.atproto.server.deleteSession', {
175 method: 'POST',
176 token,
177 })
178 },
179
180 async listAppPasswords(token: string): Promise<{ passwords: AppPassword[] }> {
181 return xrpc('com.atproto.server.listAppPasswords', { token })
182 },
183
184 async createAppPassword(token: string, name: string): Promise<{ name: string; password: string; createdAt: string }> {
185 return xrpc('com.atproto.server.createAppPassword', {
186 method: 'POST',
187 token,
188 body: { name },
189 })
190 },
191
192 async revokeAppPassword(token: string, name: string): Promise<void> {
193 await xrpc('com.atproto.server.revokeAppPassword', {
194 method: 'POST',
195 token,
196 body: { name },
197 })
198 },
199
200 async getAccountInviteCodes(token: string): Promise<{ codes: InviteCode[] }> {
201 return xrpc('com.atproto.server.getAccountInviteCodes', { token })
202 },
203
204 async createInviteCode(token: string, useCount: number = 1): Promise<{ code: string }> {
205 return xrpc('com.atproto.server.createInviteCode', {
206 method: 'POST',
207 token,
208 body: { useCount },
209 })
210 },
211
212 async requestPasswordReset(email: string): Promise<void> {
213 await xrpc('com.atproto.server.requestPasswordReset', {
214 method: 'POST',
215 body: { email },
216 })
217 },
218
219 async resetPassword(token: string, password: string): Promise<void> {
220 await xrpc('com.atproto.server.resetPassword', {
221 method: 'POST',
222 body: { token, password },
223 })
224 },
225
226 async requestEmailUpdate(token: string, email: string): Promise<{ tokenRequired: boolean }> {
227 return xrpc('com.atproto.server.requestEmailUpdate', {
228 method: 'POST',
229 token,
230 body: { email },
231 })
232 },
233
234 async updateEmail(token: string, email: string, emailToken?: string): Promise<void> {
235 await xrpc('com.atproto.server.updateEmail', {
236 method: 'POST',
237 token,
238 body: { email, token: emailToken },
239 })
240 },
241
242 async updateHandle(token: string, handle: string): Promise<void> {
243 await xrpc('com.atproto.identity.updateHandle', {
244 method: 'POST',
245 token,
246 body: { handle },
247 })
248 },
249
250 async requestAccountDelete(token: string): Promise<void> {
251 await xrpc('com.atproto.server.requestAccountDelete', {
252 method: 'POST',
253 token,
254 })
255 },
256
257 async deleteAccount(did: string, password: string, deleteToken: string): Promise<void> {
258 await xrpc('com.atproto.server.deleteAccount', {
259 method: 'POST',
260 body: { did, password, token: deleteToken },
261 })
262 },
263
264 async describeServer(): Promise<{
265 availableUserDomains: string[]
266 inviteCodeRequired: boolean
267 links?: { privacyPolicy?: string; termsOfService?: string }
268 }> {
269 return xrpc('com.atproto.server.describeServer')
270 },
271
272 async getNotificationPrefs(token: string): Promise<{
273 preferredChannel: string
274 email: string
275 discordId: string | null
276 discordVerified: boolean
277 telegramUsername: string | null
278 telegramVerified: boolean
279 signalNumber: string | null
280 signalVerified: boolean
281 }> {
282 return xrpc('com.tranquil.account.getNotificationPrefs', { token })
283 },
284
285 async updateNotificationPrefs(token: string, prefs: {
286 preferredChannel?: string
287 discordId?: string
288 telegramUsername?: string
289 signalNumber?: string
290 }): Promise<{ success: boolean }> {
291 return xrpc('com.tranquil.account.updateNotificationPrefs', {
292 method: 'POST',
293 token,
294 body: prefs,
295 })
296 },
297
298 async confirmChannelVerification(token: string, channel: string, code: string): Promise<{ success: boolean }> {
299 return xrpc('com.tranquil.account.confirmChannelVerification', {
300 method: 'POST',
301 token,
302 body: { channel, code },
303 })
304 },
305
306 async getNotificationHistory(token: string): Promise<{
307 notifications: Array<{
308 createdAt: string
309 channel: string
310 notificationType: string
311 status: string
312 subject: string | null
313 body: string
314 }>
315 }> {
316 return xrpc('com.tranquil.account.getNotificationHistory', { token })
317 },
318
319 async getServerStats(token: string): Promise<{
320 userCount: number
321 repoCount: number
322 recordCount: number
323 blobStorageBytes: number
324 }> {
325 return xrpc('com.tranquil.admin.getServerStats', { token })
326 },
327
328 async changePassword(token: string, currentPassword: string, newPassword: string): Promise<void> {
329 await xrpc('com.tranquil.account.changePassword', {
330 method: 'POST',
331 token,
332 body: { currentPassword, newPassword },
333 })
334 },
335
336 async removePassword(token: string): Promise<{ success: boolean }> {
337 return xrpc('com.tranquil.account.removePassword', {
338 method: 'POST',
339 token,
340 })
341 },
342
343 async getPasswordStatus(token: string): Promise<{ hasPassword: boolean }> {
344 return xrpc('com.tranquil.account.getPasswordStatus', { token })
345 },
346
347 async getLegacyLoginPreference(token: string): Promise<{ allowLegacyLogin: boolean; hasMfa: boolean }> {
348 return xrpc('com.tranquil.account.getLegacyLoginPreference', { token })
349 },
350
351 async updateLegacyLoginPreference(token: string, allowLegacyLogin: boolean): Promise<{ allowLegacyLogin: boolean }> {
352 return xrpc('com.tranquil.account.updateLegacyLoginPreference', {
353 method: 'POST',
354 token,
355 body: { allowLegacyLogin },
356 })
357 },
358
359 async updateLocale(token: string, preferredLocale: string): Promise<{ preferredLocale: string }> {
360 return xrpc('com.tranquil.account.updateLocale', {
361 method: 'POST',
362 token,
363 body: { preferredLocale },
364 })
365 },
366
367 async listSessions(token: string): Promise<{
368 sessions: Array<{
369 id: string
370 sessionType: string
371 clientName: string | null
372 createdAt: string
373 expiresAt: string
374 isCurrent: boolean
375 }>
376 }> {
377 return xrpc('com.tranquil.account.listSessions', { token })
378 },
379
380 async revokeSession(token: string, sessionId: string): Promise<void> {
381 await xrpc('com.tranquil.account.revokeSession', {
382 method: 'POST',
383 token,
384 body: { sessionId },
385 })
386 },
387
388 async revokeAllSessions(token: string): Promise<{ revokedCount: number }> {
389 return xrpc('com.tranquil.account.revokeAllSessions', {
390 method: 'POST',
391 token,
392 })
393 },
394
395 async searchAccounts(token: string, options?: {
396 handle?: string
397 cursor?: string
398 limit?: number
399 }): Promise<{
400 cursor?: string
401 accounts: Array<{
402 did: string
403 handle: string
404 email?: string
405 indexedAt: string
406 emailConfirmedAt?: string
407 deactivatedAt?: string
408 }>
409 }> {
410 const params: Record<string, string> = {}
411 if (options?.handle) params.handle = options.handle
412 if (options?.cursor) params.cursor = options.cursor
413 if (options?.limit) params.limit = String(options.limit)
414 return xrpc('com.atproto.admin.searchAccounts', { token, params })
415 },
416
417 async getInviteCodes(token: string, options?: {
418 sort?: 'recent' | 'usage'
419 cursor?: string
420 limit?: number
421 }): Promise<{
422 cursor?: string
423 codes: Array<{
424 code: string
425 available: number
426 disabled: boolean
427 forAccount: string
428 createdBy: string
429 createdAt: string
430 uses: Array<{ usedBy: string; usedAt: string }>
431 }>
432 }> {
433 const params: Record<string, string> = {}
434 if (options?.sort) params.sort = options.sort
435 if (options?.cursor) params.cursor = options.cursor
436 if (options?.limit) params.limit = String(options.limit)
437 return xrpc('com.atproto.admin.getInviteCodes', { token, params })
438 },
439
440 async disableInviteCodes(token: string, codes?: string[], accounts?: string[]): Promise<void> {
441 await xrpc('com.atproto.admin.disableInviteCodes', {
442 method: 'POST',
443 token,
444 body: { codes, accounts },
445 })
446 },
447
448 async getAccountInfo(token: string, did: string): Promise<{
449 did: string
450 handle: string
451 email?: string
452 indexedAt: string
453 emailConfirmedAt?: string
454 invitesDisabled?: boolean
455 deactivatedAt?: string
456 }> {
457 return xrpc('com.atproto.admin.getAccountInfo', { token, params: { did } })
458 },
459
460 async disableAccountInvites(token: string, account: string): Promise<void> {
461 await xrpc('com.atproto.admin.disableAccountInvites', {
462 method: 'POST',
463 token,
464 body: { account },
465 })
466 },
467
468 async enableAccountInvites(token: string, account: string): Promise<void> {
469 await xrpc('com.atproto.admin.enableAccountInvites', {
470 method: 'POST',
471 token,
472 body: { account },
473 })
474 },
475
476 async adminDeleteAccount(token: string, did: string): Promise<void> {
477 await xrpc('com.atproto.admin.deleteAccount', {
478 method: 'POST',
479 token,
480 body: { did },
481 })
482 },
483
484 async describeRepo(token: string, repo: string): Promise<{
485 handle: string
486 did: string
487 didDoc: unknown
488 collections: string[]
489 handleIsCorrect: boolean
490 }> {
491 return xrpc('com.atproto.repo.describeRepo', {
492 token,
493 params: { repo },
494 })
495 },
496
497 async listRecords(token: string, repo: string, collection: string, options?: {
498 limit?: number
499 cursor?: string
500 reverse?: boolean
501 }): Promise<{
502 records: Array<{ uri: string; cid: string; value: unknown }>
503 cursor?: string
504 }> {
505 const params: Record<string, string> = { repo, collection }
506 if (options?.limit) params.limit = String(options.limit)
507 if (options?.cursor) params.cursor = options.cursor
508 if (options?.reverse) params.reverse = 'true'
509 return xrpc('com.atproto.repo.listRecords', { token, params })
510 },
511
512 async getRecord(token: string, repo: string, collection: string, rkey: string): Promise<{
513 uri: string
514 cid: string
515 value: unknown
516 }> {
517 return xrpc('com.atproto.repo.getRecord', {
518 token,
519 params: { repo, collection, rkey },
520 })
521 },
522
523 async createRecord(token: string, repo: string, collection: string, record: unknown, rkey?: string): Promise<{
524 uri: string
525 cid: string
526 }> {
527 return xrpc('com.atproto.repo.createRecord', {
528 method: 'POST',
529 token,
530 body: { repo, collection, record, rkey },
531 })
532 },
533
534 async putRecord(token: string, repo: string, collection: string, rkey: string, record: unknown): Promise<{
535 uri: string
536 cid: string
537 }> {
538 return xrpc('com.atproto.repo.putRecord', {
539 method: 'POST',
540 token,
541 body: { repo, collection, rkey, record },
542 })
543 },
544
545 async deleteRecord(token: string, repo: string, collection: string, rkey: string): Promise<void> {
546 await xrpc('com.atproto.repo.deleteRecord', {
547 method: 'POST',
548 token,
549 body: { repo, collection, rkey },
550 })
551 },
552
553 async getTotpStatus(token: string): Promise<{ enabled: boolean; hasBackupCodes: boolean }> {
554 return xrpc('com.atproto.server.getTotpStatus', { token })
555 },
556
557 async createTotpSecret(token: string): Promise<{ uri: string; qrBase64: string }> {
558 return xrpc('com.atproto.server.createTotpSecret', { method: 'POST', token })
559 },
560
561 async enableTotp(token: string, code: string): Promise<{ success: boolean; backupCodes: string[] }> {
562 return xrpc('com.atproto.server.enableTotp', {
563 method: 'POST',
564 token,
565 body: { code },
566 })
567 },
568
569 async disableTotp(token: string, password: string, code: string): Promise<{ success: boolean }> {
570 return xrpc('com.atproto.server.disableTotp', {
571 method: 'POST',
572 token,
573 body: { password, code },
574 })
575 },
576
577 async regenerateBackupCodes(token: string, password: string, code: string): Promise<{ backupCodes: string[] }> {
578 return xrpc('com.atproto.server.regenerateBackupCodes', {
579 method: 'POST',
580 token,
581 body: { password, code },
582 })
583 },
584
585 async startPasskeyRegistration(token: string, friendlyName?: string): Promise<{ options: unknown }> {
586 return xrpc('com.atproto.server.startPasskeyRegistration', {
587 method: 'POST',
588 token,
589 body: { friendlyName },
590 })
591 },
592
593 async finishPasskeyRegistration(token: string, credential: unknown, friendlyName?: string): Promise<{ id: string; credentialId: string }> {
594 return xrpc('com.atproto.server.finishPasskeyRegistration', {
595 method: 'POST',
596 token,
597 body: { credential, friendlyName },
598 })
599 },
600
601 async listPasskeys(token: string): Promise<{
602 passkeys: Array<{
603 id: string
604 credentialId: string
605 friendlyName: string | null
606 createdAt: string
607 lastUsed: string | null
608 }>
609 }> {
610 return xrpc('com.atproto.server.listPasskeys', { token })
611 },
612
613 async deletePasskey(token: string, id: string): Promise<void> {
614 await xrpc('com.atproto.server.deletePasskey', {
615 method: 'POST',
616 token,
617 body: { id },
618 })
619 },
620
621 async updatePasskey(token: string, id: string, friendlyName: string): Promise<void> {
622 await xrpc('com.atproto.server.updatePasskey', {
623 method: 'POST',
624 token,
625 body: { id, friendlyName },
626 })
627 },
628
629 async listTrustedDevices(token: string): Promise<{
630 devices: Array<{
631 id: string
632 userAgent: string | null
633 friendlyName: string | null
634 trustedAt: string | null
635 trustedUntil: string | null
636 lastSeenAt: string
637 }>
638 }> {
639 return xrpc('com.tranquil.account.listTrustedDevices', { token })
640 },
641
642 async revokeTrustedDevice(token: string, deviceId: string): Promise<{ success: boolean }> {
643 return xrpc('com.tranquil.account.revokeTrustedDevice', {
644 method: 'POST',
645 token,
646 body: { deviceId },
647 })
648 },
649
650 async updateTrustedDevice(token: string, deviceId: string, friendlyName: string): Promise<{ success: boolean }> {
651 return xrpc('com.tranquil.account.updateTrustedDevice', {
652 method: 'POST',
653 token,
654 body: { deviceId, friendlyName },
655 })
656 },
657
658 async getReauthStatus(token: string): Promise<{
659 requiresReauth: boolean
660 lastReauthAt: string | null
661 availableMethods: string[]
662 }> {
663 return xrpc('com.tranquil.account.getReauthStatus', { token })
664 },
665
666 async reauthPassword(token: string, password: string): Promise<{ success: boolean; reauthAt: string }> {
667 return xrpc('com.tranquil.account.reauthPassword', {
668 method: 'POST',
669 token,
670 body: { password },
671 })
672 },
673
674 async reauthTotp(token: string, code: string): Promise<{ success: boolean; reauthAt: string }> {
675 return xrpc('com.tranquil.account.reauthTotp', {
676 method: 'POST',
677 token,
678 body: { code },
679 })
680 },
681
682 async reauthPasskeyStart(token: string): Promise<{ options: unknown }> {
683 return xrpc('com.tranquil.account.reauthPasskeyStart', {
684 method: 'POST',
685 token,
686 })
687 },
688
689 async reauthPasskeyFinish(token: string, credential: unknown): Promise<{ success: boolean; reauthAt: string }> {
690 return xrpc('com.tranquil.account.reauthPasskeyFinish', {
691 method: 'POST',
692 token,
693 body: { credential },
694 })
695 },
696
697 async createPasskeyAccount(params: {
698 handle: string
699 email?: string
700 inviteCode?: string
701 didType?: DidType
702 did?: string
703 signingKey?: string
704 verificationChannel?: VerificationChannel
705 discordId?: string
706 telegramUsername?: string
707 signalNumber?: string
708 }): Promise<{
709 did: string
710 handle: string
711 setupToken: string
712 setupExpiresAt: string
713 }> {
714 return xrpc('com.tranquil.account.createPasskeyAccount', {
715 method: 'POST',
716 body: params,
717 })
718 },
719
720 async startPasskeyRegistrationForSetup(did: string, setupToken: string, friendlyName?: string): Promise<{ options: unknown }> {
721 return xrpc('com.tranquil.account.startPasskeyRegistrationForSetup', {
722 method: 'POST',
723 body: { did, setupToken, friendlyName },
724 })
725 },
726
727 async completePasskeySetup(did: string, setupToken: string, passkeyCredential: unknown, passkeyFriendlyName?: string): Promise<{
728 did: string
729 handle: string
730 appPassword: string
731 appPasswordName: string
732 }> {
733 return xrpc('com.tranquil.account.completePasskeySetup', {
734 method: 'POST',
735 body: { did, setupToken, passkeyCredential, passkeyFriendlyName },
736 })
737 },
738
739 async requestPasskeyRecovery(email: string): Promise<{ success: boolean }> {
740 return xrpc('com.tranquil.account.requestPasskeyRecovery', {
741 method: 'POST',
742 body: { email },
743 })
744 },
745
746 async recoverPasskeyAccount(did: string, recoveryToken: string, newPassword: string): Promise<{ success: boolean }> {
747 return xrpc('com.tranquil.account.recoverPasskeyAccount', {
748 method: 'POST',
749 body: { did, recoveryToken, newPassword },
750 })
751 },
752}