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 signingKey?: string 99 verificationChannel?: VerificationChannel 100 discordId?: string 101 telegramUsername?: string 102 signalNumber?: string 103} 104 105export interface CreateAccountResult { 106 handle: string 107 did: string 108 verificationRequired: boolean 109 verificationChannel: string 110} 111 112export interface ConfirmSignupResult { 113 accessJwt: string 114 refreshJwt: string 115 handle: string 116 did: string 117 email?: string 118 emailConfirmed?: boolean 119 preferredChannel?: string 120 preferredChannelVerified?: boolean 121} 122 123export const api = { 124 async createAccount(params: CreateAccountParams, byodToken?: string): Promise<CreateAccountResult> { 125 const url = `${API_BASE}/com.atproto.server.createAccount` 126 const headers: Record<string, string> = { 'Content-Type': 'application/json' } 127 if (byodToken) { 128 headers['Authorization'] = `Bearer ${byodToken}` 129 } 130 const response = await fetch(url, { 131 method: 'POST', 132 headers, 133 body: JSON.stringify({ 134 handle: params.handle, 135 email: params.email, 136 password: params.password, 137 inviteCode: params.inviteCode, 138 didType: params.didType, 139 did: params.did, 140 signingKey: params.signingKey, 141 verificationChannel: params.verificationChannel, 142 discordId: params.discordId, 143 telegramUsername: params.telegramUsername, 144 signalNumber: params.signalNumber, 145 }), 146 }) 147 const data = await response.json() 148 if (!response.ok) { 149 throw new ApiError(data.error, data.message, response.status) 150 } 151 return data 152 }, 153 154 async confirmSignup(did: string, verificationCode: string): Promise<ConfirmSignupResult> { 155 return xrpc('com.atproto.server.confirmSignup', { 156 method: 'POST', 157 body: { did, verificationCode }, 158 }) 159 }, 160 161 async resendVerification(did: string): Promise<{ success: boolean }> { 162 return xrpc('com.atproto.server.resendVerification', { 163 method: 'POST', 164 body: { did }, 165 }) 166 }, 167 168 async createSession(identifier: string, password: string): Promise<Session> { 169 return xrpc('com.atproto.server.createSession', { 170 method: 'POST', 171 body: { identifier, password }, 172 }) 173 }, 174 175 async getSession(token: string): Promise<Session> { 176 return xrpc('com.atproto.server.getSession', { token }) 177 }, 178 179 async refreshSession(refreshJwt: string): Promise<Session> { 180 return xrpc('com.atproto.server.refreshSession', { 181 method: 'POST', 182 token: refreshJwt, 183 }) 184 }, 185 186 async deleteSession(token: string): Promise<void> { 187 await xrpc('com.atproto.server.deleteSession', { 188 method: 'POST', 189 token, 190 }) 191 }, 192 193 async listAppPasswords(token: string): Promise<{ passwords: AppPassword[] }> { 194 return xrpc('com.atproto.server.listAppPasswords', { token }) 195 }, 196 197 async createAppPassword(token: string, name: string): Promise<{ name: string; password: string; createdAt: string }> { 198 return xrpc('com.atproto.server.createAppPassword', { 199 method: 'POST', 200 token, 201 body: { name }, 202 }) 203 }, 204 205 async revokeAppPassword(token: string, name: string): Promise<void> { 206 await xrpc('com.atproto.server.revokeAppPassword', { 207 method: 'POST', 208 token, 209 body: { name }, 210 }) 211 }, 212 213 async getAccountInviteCodes(token: string): Promise<{ codes: InviteCode[] }> { 214 return xrpc('com.atproto.server.getAccountInviteCodes', { token }) 215 }, 216 217 async createInviteCode(token: string, useCount: number = 1): Promise<{ code: string }> { 218 return xrpc('com.atproto.server.createInviteCode', { 219 method: 'POST', 220 token, 221 body: { useCount }, 222 }) 223 }, 224 225 async requestPasswordReset(email: string): Promise<void> { 226 await xrpc('com.atproto.server.requestPasswordReset', { 227 method: 'POST', 228 body: { email }, 229 }) 230 }, 231 232 async resetPassword(token: string, password: string): Promise<void> { 233 await xrpc('com.atproto.server.resetPassword', { 234 method: 'POST', 235 body: { token, password }, 236 }) 237 }, 238 239 async requestEmailUpdate(token: string, email: string): Promise<{ tokenRequired: boolean }> { 240 return xrpc('com.atproto.server.requestEmailUpdate', { 241 method: 'POST', 242 token, 243 body: { email }, 244 }) 245 }, 246 247 async updateEmail(token: string, email: string, emailToken?: string): Promise<void> { 248 await xrpc('com.atproto.server.updateEmail', { 249 method: 'POST', 250 token, 251 body: { email, token: emailToken }, 252 }) 253 }, 254 255 async updateHandle(token: string, handle: string): Promise<void> { 256 await xrpc('com.atproto.identity.updateHandle', { 257 method: 'POST', 258 token, 259 body: { handle }, 260 }) 261 }, 262 263 async requestAccountDelete(token: string): Promise<void> { 264 await xrpc('com.atproto.server.requestAccountDelete', { 265 method: 'POST', 266 token, 267 }) 268 }, 269 270 async deleteAccount(did: string, password: string, deleteToken: string): Promise<void> { 271 await xrpc('com.atproto.server.deleteAccount', { 272 method: 'POST', 273 body: { did, password, token: deleteToken }, 274 }) 275 }, 276 277 async describeServer(): Promise<{ 278 availableUserDomains: string[] 279 inviteCodeRequired: boolean 280 links?: { privacyPolicy?: string; termsOfService?: string } 281 version?: string 282 availableCommsChannels?: string[] 283 }> { 284 return xrpc('com.atproto.server.describeServer') 285 }, 286 287 async listRepos(limit?: number): Promise<{ 288 repos: Array<{ did: string; head: string; rev: string }> 289 cursor?: string 290 }> { 291 const params: Record<string, string> = {} 292 if (limit) params.limit = String(limit) 293 return xrpc('com.atproto.sync.listRepos', { params }) 294 }, 295 296 async getNotificationPrefs(token: string): Promise<{ 297 preferredChannel: string 298 email: string 299 discordId: string | null 300 discordVerified: boolean 301 telegramUsername: string | null 302 telegramVerified: boolean 303 signalNumber: string | null 304 signalVerified: boolean 305 }> { 306 return xrpc('com.tranquil.account.getNotificationPrefs', { token }) 307 }, 308 309 async updateNotificationPrefs(token: string, prefs: { 310 preferredChannel?: string 311 discordId?: string 312 telegramUsername?: string 313 signalNumber?: string 314 }): Promise<{ success: boolean }> { 315 return xrpc('com.tranquil.account.updateNotificationPrefs', { 316 method: 'POST', 317 token, 318 body: prefs, 319 }) 320 }, 321 322 async confirmChannelVerification(token: string, channel: string, identifier: string, code: string): Promise<{ success: boolean }> { 323 return xrpc('com.tranquil.account.confirmChannelVerification', { 324 method: 'POST', 325 token, 326 body: { channel, identifier, code }, 327 }) 328 }, 329 330 async getNotificationHistory(token: string): Promise<{ 331 notifications: Array<{ 332 createdAt: string 333 channel: string 334 notificationType: string 335 status: string 336 subject: string | null 337 body: string 338 }> 339 }> { 340 return xrpc('com.tranquil.account.getNotificationHistory', { token }) 341 }, 342 343 async getServerStats(token: string): Promise<{ 344 userCount: number 345 repoCount: number 346 recordCount: number 347 blobStorageBytes: number 348 }> { 349 return xrpc('com.tranquil.admin.getServerStats', { token }) 350 }, 351 352 async getServerConfig(): Promise<{ 353 serverName: string 354 primaryColor: string | null 355 primaryColorDark: string | null 356 secondaryColor: string | null 357 secondaryColorDark: string | null 358 logoCid: string | null 359 }> { 360 return xrpc('com.tranquil.server.getConfig') 361 }, 362 363 async updateServerConfig( 364 token: string, 365 config: { 366 serverName?: string 367 primaryColor?: string 368 primaryColorDark?: string 369 secondaryColor?: string 370 secondaryColorDark?: string 371 logoCid?: string 372 } 373 ): Promise<{ success: boolean }> { 374 return xrpc('com.tranquil.admin.updateServerConfig', { 375 method: 'POST', 376 token, 377 body: config, 378 }) 379 }, 380 381 async uploadBlob(token: string, file: File): Promise<{ blob: { $type: string; ref: { $link: string }; mimeType: string; size: number } }> { 382 const res = await fetch('/xrpc/com.atproto.repo.uploadBlob', { 383 method: 'POST', 384 headers: { 385 'Authorization': `Bearer ${token}`, 386 'Content-Type': file.type, 387 }, 388 body: file, 389 }) 390 if (!res.ok) { 391 const err = await res.json().catch(() => ({ error: 'Unknown', message: res.statusText })) 392 throw new ApiError(res.status, err.error, err.message) 393 } 394 return res.json() 395 }, 396 397 async changePassword(token: string, currentPassword: string, newPassword: string): Promise<void> { 398 await xrpc('com.tranquil.account.changePassword', { 399 method: 'POST', 400 token, 401 body: { currentPassword, newPassword }, 402 }) 403 }, 404 405 async removePassword(token: string): Promise<{ success: boolean }> { 406 return xrpc('com.tranquil.account.removePassword', { 407 method: 'POST', 408 token, 409 }) 410 }, 411 412 async getPasswordStatus(token: string): Promise<{ hasPassword: boolean }> { 413 return xrpc('com.tranquil.account.getPasswordStatus', { token }) 414 }, 415 416 async getLegacyLoginPreference(token: string): Promise<{ allowLegacyLogin: boolean; hasMfa: boolean }> { 417 return xrpc('com.tranquil.account.getLegacyLoginPreference', { token }) 418 }, 419 420 async updateLegacyLoginPreference(token: string, allowLegacyLogin: boolean): Promise<{ allowLegacyLogin: boolean }> { 421 return xrpc('com.tranquil.account.updateLegacyLoginPreference', { 422 method: 'POST', 423 token, 424 body: { allowLegacyLogin }, 425 }) 426 }, 427 428 async updateLocale(token: string, preferredLocale: string): Promise<{ preferredLocale: string }> { 429 return xrpc('com.tranquil.account.updateLocale', { 430 method: 'POST', 431 token, 432 body: { preferredLocale }, 433 }) 434 }, 435 436 async listSessions(token: string): Promise<{ 437 sessions: Array<{ 438 id: string 439 sessionType: string 440 clientName: string | null 441 createdAt: string 442 expiresAt: string 443 isCurrent: boolean 444 }> 445 }> { 446 return xrpc('com.tranquil.account.listSessions', { token }) 447 }, 448 449 async revokeSession(token: string, sessionId: string): Promise<void> { 450 await xrpc('com.tranquil.account.revokeSession', { 451 method: 'POST', 452 token, 453 body: { sessionId }, 454 }) 455 }, 456 457 async revokeAllSessions(token: string): Promise<{ revokedCount: number }> { 458 return xrpc('com.tranquil.account.revokeAllSessions', { 459 method: 'POST', 460 token, 461 }) 462 }, 463 464 async searchAccounts(token: string, options?: { 465 handle?: string 466 cursor?: string 467 limit?: number 468 }): Promise<{ 469 cursor?: string 470 accounts: Array<{ 471 did: string 472 handle: string 473 email?: string 474 indexedAt: string 475 emailConfirmedAt?: string 476 deactivatedAt?: string 477 }> 478 }> { 479 const params: Record<string, string> = {} 480 if (options?.handle) params.handle = options.handle 481 if (options?.cursor) params.cursor = options.cursor 482 if (options?.limit) params.limit = String(options.limit) 483 return xrpc('com.atproto.admin.searchAccounts', { token, params }) 484 }, 485 486 async getInviteCodes(token: string, options?: { 487 sort?: 'recent' | 'usage' 488 cursor?: string 489 limit?: number 490 }): Promise<{ 491 cursor?: string 492 codes: Array<{ 493 code: string 494 available: number 495 disabled: boolean 496 forAccount: string 497 createdBy: string 498 createdAt: string 499 uses: Array<{ usedBy: string; usedAt: string }> 500 }> 501 }> { 502 const params: Record<string, string> = {} 503 if (options?.sort) params.sort = options.sort 504 if (options?.cursor) params.cursor = options.cursor 505 if (options?.limit) params.limit = String(options.limit) 506 return xrpc('com.atproto.admin.getInviteCodes', { token, params }) 507 }, 508 509 async disableInviteCodes(token: string, codes?: string[], accounts?: string[]): Promise<void> { 510 await xrpc('com.atproto.admin.disableInviteCodes', { 511 method: 'POST', 512 token, 513 body: { codes, accounts }, 514 }) 515 }, 516 517 async getAccountInfo(token: string, did: string): Promise<{ 518 did: string 519 handle: string 520 email?: string 521 indexedAt: string 522 emailConfirmedAt?: string 523 invitesDisabled?: boolean 524 deactivatedAt?: string 525 }> { 526 return xrpc('com.atproto.admin.getAccountInfo', { token, params: { did } }) 527 }, 528 529 async disableAccountInvites(token: string, account: string): Promise<void> { 530 await xrpc('com.atproto.admin.disableAccountInvites', { 531 method: 'POST', 532 token, 533 body: { account }, 534 }) 535 }, 536 537 async enableAccountInvites(token: string, account: string): Promise<void> { 538 await xrpc('com.atproto.admin.enableAccountInvites', { 539 method: 'POST', 540 token, 541 body: { account }, 542 }) 543 }, 544 545 async adminDeleteAccount(token: string, did: string): Promise<void> { 546 await xrpc('com.atproto.admin.deleteAccount', { 547 method: 'POST', 548 token, 549 body: { did }, 550 }) 551 }, 552 553 async describeRepo(token: string, repo: string): Promise<{ 554 handle: string 555 did: string 556 didDoc: unknown 557 collections: string[] 558 handleIsCorrect: boolean 559 }> { 560 return xrpc('com.atproto.repo.describeRepo', { 561 token, 562 params: { repo }, 563 }) 564 }, 565 566 async listRecords(token: string, repo: string, collection: string, options?: { 567 limit?: number 568 cursor?: string 569 reverse?: boolean 570 }): Promise<{ 571 records: Array<{ uri: string; cid: string; value: unknown }> 572 cursor?: string 573 }> { 574 const params: Record<string, string> = { repo, collection } 575 if (options?.limit) params.limit = String(options.limit) 576 if (options?.cursor) params.cursor = options.cursor 577 if (options?.reverse) params.reverse = 'true' 578 return xrpc('com.atproto.repo.listRecords', { token, params }) 579 }, 580 581 async getRecord(token: string, repo: string, collection: string, rkey: string): Promise<{ 582 uri: string 583 cid: string 584 value: unknown 585 }> { 586 return xrpc('com.atproto.repo.getRecord', { 587 token, 588 params: { repo, collection, rkey }, 589 }) 590 }, 591 592 async createRecord(token: string, repo: string, collection: string, record: unknown, rkey?: string): Promise<{ 593 uri: string 594 cid: string 595 }> { 596 return xrpc('com.atproto.repo.createRecord', { 597 method: 'POST', 598 token, 599 body: { repo, collection, record, rkey }, 600 }) 601 }, 602 603 async putRecord(token: string, repo: string, collection: string, rkey: string, record: unknown): Promise<{ 604 uri: string 605 cid: string 606 }> { 607 return xrpc('com.atproto.repo.putRecord', { 608 method: 'POST', 609 token, 610 body: { repo, collection, rkey, record }, 611 }) 612 }, 613 614 async deleteRecord(token: string, repo: string, collection: string, rkey: string): Promise<void> { 615 await xrpc('com.atproto.repo.deleteRecord', { 616 method: 'POST', 617 token, 618 body: { repo, collection, rkey }, 619 }) 620 }, 621 622 async getTotpStatus(token: string): Promise<{ enabled: boolean; hasBackupCodes: boolean }> { 623 return xrpc('com.atproto.server.getTotpStatus', { token }) 624 }, 625 626 async createTotpSecret(token: string): Promise<{ uri: string; qrBase64: string }> { 627 return xrpc('com.atproto.server.createTotpSecret', { method: 'POST', token }) 628 }, 629 630 async enableTotp(token: string, code: string): Promise<{ success: boolean; backupCodes: string[] }> { 631 return xrpc('com.atproto.server.enableTotp', { 632 method: 'POST', 633 token, 634 body: { code }, 635 }) 636 }, 637 638 async disableTotp(token: string, password: string, code: string): Promise<{ success: boolean }> { 639 return xrpc('com.atproto.server.disableTotp', { 640 method: 'POST', 641 token, 642 body: { password, code }, 643 }) 644 }, 645 646 async regenerateBackupCodes(token: string, password: string, code: string): Promise<{ backupCodes: string[] }> { 647 return xrpc('com.atproto.server.regenerateBackupCodes', { 648 method: 'POST', 649 token, 650 body: { password, code }, 651 }) 652 }, 653 654 async startPasskeyRegistration(token: string, friendlyName?: string): Promise<{ options: unknown }> { 655 return xrpc('com.atproto.server.startPasskeyRegistration', { 656 method: 'POST', 657 token, 658 body: { friendlyName }, 659 }) 660 }, 661 662 async finishPasskeyRegistration(token: string, credential: unknown, friendlyName?: string): Promise<{ id: string; credentialId: string }> { 663 return xrpc('com.atproto.server.finishPasskeyRegistration', { 664 method: 'POST', 665 token, 666 body: { credential, friendlyName }, 667 }) 668 }, 669 670 async listPasskeys(token: string): Promise<{ 671 passkeys: Array<{ 672 id: string 673 credentialId: string 674 friendlyName: string | null 675 createdAt: string 676 lastUsed: string | null 677 }> 678 }> { 679 return xrpc('com.atproto.server.listPasskeys', { token }) 680 }, 681 682 async deletePasskey(token: string, id: string): Promise<void> { 683 await xrpc('com.atproto.server.deletePasskey', { 684 method: 'POST', 685 token, 686 body: { id }, 687 }) 688 }, 689 690 async updatePasskey(token: string, id: string, friendlyName: string): Promise<void> { 691 await xrpc('com.atproto.server.updatePasskey', { 692 method: 'POST', 693 token, 694 body: { id, friendlyName }, 695 }) 696 }, 697 698 async listTrustedDevices(token: string): Promise<{ 699 devices: Array<{ 700 id: string 701 userAgent: string | null 702 friendlyName: string | null 703 trustedAt: string | null 704 trustedUntil: string | null 705 lastSeenAt: string 706 }> 707 }> { 708 return xrpc('com.tranquil.account.listTrustedDevices', { token }) 709 }, 710 711 async revokeTrustedDevice(token: string, deviceId: string): Promise<{ success: boolean }> { 712 return xrpc('com.tranquil.account.revokeTrustedDevice', { 713 method: 'POST', 714 token, 715 body: { deviceId }, 716 }) 717 }, 718 719 async updateTrustedDevice(token: string, deviceId: string, friendlyName: string): Promise<{ success: boolean }> { 720 return xrpc('com.tranquil.account.updateTrustedDevice', { 721 method: 'POST', 722 token, 723 body: { deviceId, friendlyName }, 724 }) 725 }, 726 727 async getReauthStatus(token: string): Promise<{ 728 requiresReauth: boolean 729 lastReauthAt: string | null 730 availableMethods: string[] 731 }> { 732 return xrpc('com.tranquil.account.getReauthStatus', { token }) 733 }, 734 735 async reauthPassword(token: string, password: string): Promise<{ success: boolean; reauthAt: string }> { 736 return xrpc('com.tranquil.account.reauthPassword', { 737 method: 'POST', 738 token, 739 body: { password }, 740 }) 741 }, 742 743 async reauthTotp(token: string, code: string): Promise<{ success: boolean; reauthAt: string }> { 744 return xrpc('com.tranquil.account.reauthTotp', { 745 method: 'POST', 746 token, 747 body: { code }, 748 }) 749 }, 750 751 async reauthPasskeyStart(token: string): Promise<{ options: unknown }> { 752 return xrpc('com.tranquil.account.reauthPasskeyStart', { 753 method: 'POST', 754 token, 755 }) 756 }, 757 758 async reauthPasskeyFinish(token: string, credential: unknown): Promise<{ success: boolean; reauthAt: string }> { 759 return xrpc('com.tranquil.account.reauthPasskeyFinish', { 760 method: 'POST', 761 token, 762 body: { credential }, 763 }) 764 }, 765 766 async reserveSigningKey(did?: string): Promise<{ signingKey: string }> { 767 return xrpc('com.atproto.server.reserveSigningKey', { 768 method: 'POST', 769 body: { did }, 770 }) 771 }, 772 773 async getRecommendedDidCredentials(token: string): Promise<{ 774 rotationKeys?: string[] 775 alsoKnownAs?: string[] 776 verificationMethods?: { atproto?: string } 777 services?: { atproto_pds?: { type: string; endpoint: string } } 778 }> { 779 return xrpc('com.atproto.identity.getRecommendedDidCredentials', { token }) 780 }, 781 782 async activateAccount(token: string): Promise<void> { 783 await xrpc('com.atproto.server.activateAccount', { 784 method: 'POST', 785 token, 786 }) 787 }, 788 789 async createPasskeyAccount(params: { 790 handle: string 791 email?: string 792 inviteCode?: string 793 didType?: DidType 794 did?: string 795 signingKey?: string 796 verificationChannel?: VerificationChannel 797 discordId?: string 798 telegramUsername?: string 799 signalNumber?: string 800 }, byodToken?: string): Promise<{ 801 did: string 802 handle: string 803 setupToken: string 804 setupExpiresAt: string 805 }> { 806 const url = `${API_BASE}/com.tranquil.account.createPasskeyAccount` 807 const headers: Record<string, string> = { 808 'Content-Type': 'application/json' 809 } 810 if (byodToken) { 811 headers['Authorization'] = `Bearer ${byodToken}` 812 } 813 const res = await fetch(url, { 814 method: 'POST', 815 headers, 816 body: JSON.stringify(params), 817 }) 818 if (!res.ok) { 819 const err = await res.json().catch(() => ({ error: 'Unknown', message: res.statusText })) 820 throw new ApiError(res.status, err.error, err.message) 821 } 822 return res.json() 823 }, 824 825 async startPasskeyRegistrationForSetup(did: string, setupToken: string, friendlyName?: string): Promise<{ options: unknown }> { 826 return xrpc('com.tranquil.account.startPasskeyRegistrationForSetup', { 827 method: 'POST', 828 body: { did, setupToken, friendlyName }, 829 }) 830 }, 831 832 async completePasskeySetup(did: string, setupToken: string, passkeyCredential: unknown, passkeyFriendlyName?: string): Promise<{ 833 did: string 834 handle: string 835 appPassword: string 836 appPasswordName: string 837 }> { 838 return xrpc('com.tranquil.account.completePasskeySetup', { 839 method: 'POST', 840 body: { did, setupToken, passkeyCredential, passkeyFriendlyName }, 841 }) 842 }, 843 844 async requestPasskeyRecovery(email: string): Promise<{ success: boolean }> { 845 return xrpc('com.tranquil.account.requestPasskeyRecovery', { 846 method: 'POST', 847 body: { email }, 848 }) 849 }, 850 851 async recoverPasskeyAccount(did: string, recoveryToken: string, newPassword: string): Promise<{ success: boolean }> { 852 return xrpc('com.tranquil.account.recoverPasskeyAccount', { 853 method: 'POST', 854 body: { did, recoveryToken, newPassword }, 855 }) 856 }, 857 858 async verifyMigrationEmail(token: string, email: string): Promise<{ success: boolean; did: string }> { 859 return xrpc('com.atproto.server.verifyMigrationEmail', { 860 method: 'POST', 861 body: { token, email }, 862 }) 863 }, 864 865 async resendMigrationVerification(email: string): Promise<{ sent: boolean }> { 866 return xrpc('com.atproto.server.resendMigrationVerification', { 867 method: 'POST', 868 body: { email }, 869 }) 870 }, 871 872 async verifyToken(token: string, identifier: string, accessToken?: string): Promise<{ 873 success: boolean 874 did: string 875 purpose: string 876 channel: string 877 }> { 878 return xrpc('com.tranquil.account.verifyToken', { 879 method: 'POST', 880 body: { token, identifier }, 881 token: accessToken, 882 }) 883 }, 884}