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