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 version?: string
269 availableCommsChannels?: string[]
270 }> {
271 return xrpc('com.atproto.server.describeServer')
272 },
273
274 async listRepos(limit?: number): Promise<{
275 repos: Array<{ did: string; head: string; rev: string }>
276 cursor?: string
277 }> {
278 const params: Record<string, string> = {}
279 if (limit) params.limit = String(limit)
280 return xrpc('com.atproto.sync.listRepos', { params })
281 },
282
283 async getNotificationPrefs(token: string): Promise<{
284 preferredChannel: string
285 email: string
286 discordId: string | null
287 discordVerified: boolean
288 telegramUsername: string | null
289 telegramVerified: boolean
290 signalNumber: string | null
291 signalVerified: boolean
292 }> {
293 return xrpc('com.tranquil.account.getNotificationPrefs', { token })
294 },
295
296 async updateNotificationPrefs(token: string, prefs: {
297 preferredChannel?: string
298 discordId?: string
299 telegramUsername?: string
300 signalNumber?: string
301 }): Promise<{ success: boolean }> {
302 return xrpc('com.tranquil.account.updateNotificationPrefs', {
303 method: 'POST',
304 token,
305 body: prefs,
306 })
307 },
308
309 async confirmChannelVerification(token: string, channel: string, code: string): Promise<{ success: boolean }> {
310 return xrpc('com.tranquil.account.confirmChannelVerification', {
311 method: 'POST',
312 token,
313 body: { channel, code },
314 })
315 },
316
317 async getNotificationHistory(token: string): Promise<{
318 notifications: Array<{
319 createdAt: string
320 channel: string
321 notificationType: string
322 status: string
323 subject: string | null
324 body: string
325 }>
326 }> {
327 return xrpc('com.tranquil.account.getNotificationHistory', { token })
328 },
329
330 async getServerStats(token: string): Promise<{
331 userCount: number
332 repoCount: number
333 recordCount: number
334 blobStorageBytes: number
335 }> {
336 return xrpc('com.tranquil.admin.getServerStats', { token })
337 },
338
339 async getServerConfig(): Promise<{
340 serverName: string
341 primaryColor: string | null
342 primaryColorDark: string | null
343 secondaryColor: string | null
344 secondaryColorDark: string | null
345 logoCid: string | null
346 }> {
347 return xrpc('com.tranquil.server.getConfig')
348 },
349
350 async updateServerConfig(
351 token: string,
352 config: {
353 serverName?: string
354 primaryColor?: string
355 primaryColorDark?: string
356 secondaryColor?: string
357 secondaryColorDark?: string
358 logoCid?: string
359 }
360 ): Promise<{ success: boolean }> {
361 return xrpc('com.tranquil.admin.updateServerConfig', {
362 method: 'POST',
363 token,
364 body: config,
365 })
366 },
367
368 async uploadBlob(token: string, file: File): Promise<{ blob: { $type: string; ref: { $link: string }; mimeType: string; size: number } }> {
369 const res = await fetch('/xrpc/com.atproto.repo.uploadBlob', {
370 method: 'POST',
371 headers: {
372 'Authorization': `Bearer ${token}`,
373 'Content-Type': file.type,
374 },
375 body: file,
376 })
377 if (!res.ok) {
378 const err = await res.json().catch(() => ({ error: 'Unknown', message: res.statusText }))
379 throw new ApiError(res.status, err.error, err.message)
380 }
381 return res.json()
382 },
383
384 async changePassword(token: string, currentPassword: string, newPassword: string): Promise<void> {
385 await xrpc('com.tranquil.account.changePassword', {
386 method: 'POST',
387 token,
388 body: { currentPassword, newPassword },
389 })
390 },
391
392 async removePassword(token: string): Promise<{ success: boolean }> {
393 return xrpc('com.tranquil.account.removePassword', {
394 method: 'POST',
395 token,
396 })
397 },
398
399 async getPasswordStatus(token: string): Promise<{ hasPassword: boolean }> {
400 return xrpc('com.tranquil.account.getPasswordStatus', { token })
401 },
402
403 async getLegacyLoginPreference(token: string): Promise<{ allowLegacyLogin: boolean; hasMfa: boolean }> {
404 return xrpc('com.tranquil.account.getLegacyLoginPreference', { token })
405 },
406
407 async updateLegacyLoginPreference(token: string, allowLegacyLogin: boolean): Promise<{ allowLegacyLogin: boolean }> {
408 return xrpc('com.tranquil.account.updateLegacyLoginPreference', {
409 method: 'POST',
410 token,
411 body: { allowLegacyLogin },
412 })
413 },
414
415 async updateLocale(token: string, preferredLocale: string): Promise<{ preferredLocale: string }> {
416 return xrpc('com.tranquil.account.updateLocale', {
417 method: 'POST',
418 token,
419 body: { preferredLocale },
420 })
421 },
422
423 async listSessions(token: string): Promise<{
424 sessions: Array<{
425 id: string
426 sessionType: string
427 clientName: string | null
428 createdAt: string
429 expiresAt: string
430 isCurrent: boolean
431 }>
432 }> {
433 return xrpc('com.tranquil.account.listSessions', { token })
434 },
435
436 async revokeSession(token: string, sessionId: string): Promise<void> {
437 await xrpc('com.tranquil.account.revokeSession', {
438 method: 'POST',
439 token,
440 body: { sessionId },
441 })
442 },
443
444 async revokeAllSessions(token: string): Promise<{ revokedCount: number }> {
445 return xrpc('com.tranquil.account.revokeAllSessions', {
446 method: 'POST',
447 token,
448 })
449 },
450
451 async searchAccounts(token: string, options?: {
452 handle?: string
453 cursor?: string
454 limit?: number
455 }): Promise<{
456 cursor?: string
457 accounts: Array<{
458 did: string
459 handle: string
460 email?: string
461 indexedAt: string
462 emailConfirmedAt?: string
463 deactivatedAt?: string
464 }>
465 }> {
466 const params: Record<string, string> = {}
467 if (options?.handle) params.handle = options.handle
468 if (options?.cursor) params.cursor = options.cursor
469 if (options?.limit) params.limit = String(options.limit)
470 return xrpc('com.atproto.admin.searchAccounts', { token, params })
471 },
472
473 async getInviteCodes(token: string, options?: {
474 sort?: 'recent' | 'usage'
475 cursor?: string
476 limit?: number
477 }): Promise<{
478 cursor?: string
479 codes: Array<{
480 code: string
481 available: number
482 disabled: boolean
483 forAccount: string
484 createdBy: string
485 createdAt: string
486 uses: Array<{ usedBy: string; usedAt: string }>
487 }>
488 }> {
489 const params: Record<string, string> = {}
490 if (options?.sort) params.sort = options.sort
491 if (options?.cursor) params.cursor = options.cursor
492 if (options?.limit) params.limit = String(options.limit)
493 return xrpc('com.atproto.admin.getInviteCodes', { token, params })
494 },
495
496 async disableInviteCodes(token: string, codes?: string[], accounts?: string[]): Promise<void> {
497 await xrpc('com.atproto.admin.disableInviteCodes', {
498 method: 'POST',
499 token,
500 body: { codes, accounts },
501 })
502 },
503
504 async getAccountInfo(token: string, did: string): Promise<{
505 did: string
506 handle: string
507 email?: string
508 indexedAt: string
509 emailConfirmedAt?: string
510 invitesDisabled?: boolean
511 deactivatedAt?: string
512 }> {
513 return xrpc('com.atproto.admin.getAccountInfo', { token, params: { did } })
514 },
515
516 async disableAccountInvites(token: string, account: string): Promise<void> {
517 await xrpc('com.atproto.admin.disableAccountInvites', {
518 method: 'POST',
519 token,
520 body: { account },
521 })
522 },
523
524 async enableAccountInvites(token: string, account: string): Promise<void> {
525 await xrpc('com.atproto.admin.enableAccountInvites', {
526 method: 'POST',
527 token,
528 body: { account },
529 })
530 },
531
532 async adminDeleteAccount(token: string, did: string): Promise<void> {
533 await xrpc('com.atproto.admin.deleteAccount', {
534 method: 'POST',
535 token,
536 body: { did },
537 })
538 },
539
540 async describeRepo(token: string, repo: string): Promise<{
541 handle: string
542 did: string
543 didDoc: unknown
544 collections: string[]
545 handleIsCorrect: boolean
546 }> {
547 return xrpc('com.atproto.repo.describeRepo', {
548 token,
549 params: { repo },
550 })
551 },
552
553 async listRecords(token: string, repo: string, collection: string, options?: {
554 limit?: number
555 cursor?: string
556 reverse?: boolean
557 }): Promise<{
558 records: Array<{ uri: string; cid: string; value: unknown }>
559 cursor?: string
560 }> {
561 const params: Record<string, string> = { repo, collection }
562 if (options?.limit) params.limit = String(options.limit)
563 if (options?.cursor) params.cursor = options.cursor
564 if (options?.reverse) params.reverse = 'true'
565 return xrpc('com.atproto.repo.listRecords', { token, params })
566 },
567
568 async getRecord(token: string, repo: string, collection: string, rkey: string): Promise<{
569 uri: string
570 cid: string
571 value: unknown
572 }> {
573 return xrpc('com.atproto.repo.getRecord', {
574 token,
575 params: { repo, collection, rkey },
576 })
577 },
578
579 async createRecord(token: string, repo: string, collection: string, record: unknown, rkey?: string): Promise<{
580 uri: string
581 cid: string
582 }> {
583 return xrpc('com.atproto.repo.createRecord', {
584 method: 'POST',
585 token,
586 body: { repo, collection, record, rkey },
587 })
588 },
589
590 async putRecord(token: string, repo: string, collection: string, rkey: string, record: unknown): Promise<{
591 uri: string
592 cid: string
593 }> {
594 return xrpc('com.atproto.repo.putRecord', {
595 method: 'POST',
596 token,
597 body: { repo, collection, rkey, record },
598 })
599 },
600
601 async deleteRecord(token: string, repo: string, collection: string, rkey: string): Promise<void> {
602 await xrpc('com.atproto.repo.deleteRecord', {
603 method: 'POST',
604 token,
605 body: { repo, collection, rkey },
606 })
607 },
608
609 async getTotpStatus(token: string): Promise<{ enabled: boolean; hasBackupCodes: boolean }> {
610 return xrpc('com.atproto.server.getTotpStatus', { token })
611 },
612
613 async createTotpSecret(token: string): Promise<{ uri: string; qrBase64: string }> {
614 return xrpc('com.atproto.server.createTotpSecret', { method: 'POST', token })
615 },
616
617 async enableTotp(token: string, code: string): Promise<{ success: boolean; backupCodes: string[] }> {
618 return xrpc('com.atproto.server.enableTotp', {
619 method: 'POST',
620 token,
621 body: { code },
622 })
623 },
624
625 async disableTotp(token: string, password: string, code: string): Promise<{ success: boolean }> {
626 return xrpc('com.atproto.server.disableTotp', {
627 method: 'POST',
628 token,
629 body: { password, code },
630 })
631 },
632
633 async regenerateBackupCodes(token: string, password: string, code: string): Promise<{ backupCodes: string[] }> {
634 return xrpc('com.atproto.server.regenerateBackupCodes', {
635 method: 'POST',
636 token,
637 body: { password, code },
638 })
639 },
640
641 async startPasskeyRegistration(token: string, friendlyName?: string): Promise<{ options: unknown }> {
642 return xrpc('com.atproto.server.startPasskeyRegistration', {
643 method: 'POST',
644 token,
645 body: { friendlyName },
646 })
647 },
648
649 async finishPasskeyRegistration(token: string, credential: unknown, friendlyName?: string): Promise<{ id: string; credentialId: string }> {
650 return xrpc('com.atproto.server.finishPasskeyRegistration', {
651 method: 'POST',
652 token,
653 body: { credential, friendlyName },
654 })
655 },
656
657 async listPasskeys(token: string): Promise<{
658 passkeys: Array<{
659 id: string
660 credentialId: string
661 friendlyName: string | null
662 createdAt: string
663 lastUsed: string | null
664 }>
665 }> {
666 return xrpc('com.atproto.server.listPasskeys', { token })
667 },
668
669 async deletePasskey(token: string, id: string): Promise<void> {
670 await xrpc('com.atproto.server.deletePasskey', {
671 method: 'POST',
672 token,
673 body: { id },
674 })
675 },
676
677 async updatePasskey(token: string, id: string, friendlyName: string): Promise<void> {
678 await xrpc('com.atproto.server.updatePasskey', {
679 method: 'POST',
680 token,
681 body: { id, friendlyName },
682 })
683 },
684
685 async listTrustedDevices(token: string): Promise<{
686 devices: Array<{
687 id: string
688 userAgent: string | null
689 friendlyName: string | null
690 trustedAt: string | null
691 trustedUntil: string | null
692 lastSeenAt: string
693 }>
694 }> {
695 return xrpc('com.tranquil.account.listTrustedDevices', { token })
696 },
697
698 async revokeTrustedDevice(token: string, deviceId: string): Promise<{ success: boolean }> {
699 return xrpc('com.tranquil.account.revokeTrustedDevice', {
700 method: 'POST',
701 token,
702 body: { deviceId },
703 })
704 },
705
706 async updateTrustedDevice(token: string, deviceId: string, friendlyName: string): Promise<{ success: boolean }> {
707 return xrpc('com.tranquil.account.updateTrustedDevice', {
708 method: 'POST',
709 token,
710 body: { deviceId, friendlyName },
711 })
712 },
713
714 async getReauthStatus(token: string): Promise<{
715 requiresReauth: boolean
716 lastReauthAt: string | null
717 availableMethods: string[]
718 }> {
719 return xrpc('com.tranquil.account.getReauthStatus', { token })
720 },
721
722 async reauthPassword(token: string, password: string): Promise<{ success: boolean; reauthAt: string }> {
723 return xrpc('com.tranquil.account.reauthPassword', {
724 method: 'POST',
725 token,
726 body: { password },
727 })
728 },
729
730 async reauthTotp(token: string, code: string): Promise<{ success: boolean; reauthAt: string }> {
731 return xrpc('com.tranquil.account.reauthTotp', {
732 method: 'POST',
733 token,
734 body: { code },
735 })
736 },
737
738 async reauthPasskeyStart(token: string): Promise<{ options: unknown }> {
739 return xrpc('com.tranquil.account.reauthPasskeyStart', {
740 method: 'POST',
741 token,
742 })
743 },
744
745 async reauthPasskeyFinish(token: string, credential: unknown): Promise<{ success: boolean; reauthAt: string }> {
746 return xrpc('com.tranquil.account.reauthPasskeyFinish', {
747 method: 'POST',
748 token,
749 body: { credential },
750 })
751 },
752
753 async createPasskeyAccount(params: {
754 handle: string
755 email?: string
756 inviteCode?: string
757 didType?: DidType
758 did?: string
759 signingKey?: string
760 verificationChannel?: VerificationChannel
761 discordId?: string
762 telegramUsername?: string
763 signalNumber?: string
764 }): Promise<{
765 did: string
766 handle: string
767 setupToken: string
768 setupExpiresAt: string
769 }> {
770 return xrpc('com.tranquil.account.createPasskeyAccount', {
771 method: 'POST',
772 body: params,
773 })
774 },
775
776 async startPasskeyRegistrationForSetup(did: string, setupToken: string, friendlyName?: string): Promise<{ options: unknown }> {
777 return xrpc('com.tranquil.account.startPasskeyRegistrationForSetup', {
778 method: 'POST',
779 body: { did, setupToken, friendlyName },
780 })
781 },
782
783 async completePasskeySetup(did: string, setupToken: string, passkeyCredential: unknown, passkeyFriendlyName?: string): Promise<{
784 did: string
785 handle: string
786 appPassword: string
787 appPasswordName: string
788 }> {
789 return xrpc('com.tranquil.account.completePasskeySetup', {
790 method: 'POST',
791 body: { did, setupToken, passkeyCredential, passkeyFriendlyName },
792 })
793 },
794
795 async requestPasskeyRecovery(email: string): Promise<{ success: boolean }> {
796 return xrpc('com.tranquil.account.requestPasskeyRecovery', {
797 method: 'POST',
798 body: { email },
799 })
800 },
801
802 async recoverPasskeyAccount(did: string, recoveryToken: string, newPassword: string): Promise<{ success: boolean }> {
803 return xrpc('com.tranquil.account.recoverPasskeyAccount', {
804 method: 'POST',
805 body: { did, recoveryToken, newPassword },
806 })
807 },
808}