import { z } from "zod"; import { err, ok, type Result } from "./types/result.ts"; import { ApiError } from "./api.ts"; import type { AccessToken, Did, Nsid, RefreshToken, Rkey, } from "./types/branded.ts"; import { accountInfoSchema, appPasswordSchema, createBackupResponseSchema, createdAppPasswordSchema, createRecordResponseSchema, didDocumentSchema, enableTotpResponseSchema, legacyLoginPreferenceSchema, listBackupsResponseSchema, listPasskeysResponseSchema, listRecordsResponseSchema, listSessionsResponseSchema, listTrustedDevicesResponseSchema, notificationPrefsSchema, passwordStatusSchema, reauthStatusSchema, recordResponseSchema, repoDescriptionSchema, searchAccountsResponseSchema, serverConfigSchema, serverDescriptionSchema, serverStatsSchema, sessionSchema, successResponseSchema, totpSecretSchema, totpStatusSchema, type ValidatedAccountInfo, type ValidatedAppPassword, type ValidatedCreateBackupResponse, type ValidatedCreatedAppPassword, type ValidatedCreateRecordResponse, type ValidatedDidDocument, type ValidatedEnableTotpResponse, type ValidatedLegacyLoginPreference, type ValidatedListBackupsResponse, type ValidatedListPasskeysResponse, type ValidatedListRecordsResponse, type ValidatedListSessionsResponse, type ValidatedListTrustedDevicesResponse, type ValidatedNotificationPrefs, type ValidatedPasswordStatus, type ValidatedReauthStatus, type ValidatedRecordResponse, type ValidatedRepoDescription, type ValidatedSearchAccountsResponse, type ValidatedServerConfig, type ValidatedServerDescription, type ValidatedServerStats, type ValidatedSession, type ValidatedSuccessResponse, type ValidatedTotpSecret, type ValidatedTotpStatus, } from "./types/schemas.ts"; const API_BASE = "/xrpc"; interface XrpcOptions { method?: "GET" | "POST"; params?: Record; body?: unknown; token?: string; } class ValidationError extends Error { constructor( public issues: z.ZodIssue[], message: string = "API response validation failed", ) { super(message); this.name = "ValidationError"; } } async function xrpcValidated( method: string, schema: z.ZodType, options?: XrpcOptions, ): Promise> { const { method: httpMethod = "GET", params, body, token } = options ?? {}; let url = `${API_BASE}/${method}`; if (params) { const searchParams = new URLSearchParams(params); url += `?${searchParams}`; } const headers: Record = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } if (body) { headers["Content-Type"] = "application/json"; } try { const res = await fetch(url, { method: httpMethod, headers, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const errData = await res.json().catch(() => ({ error: "Unknown", message: res.statusText, })); return err(new ApiError(res.status, errData.error, errData.message)); } const data = await res.json(); const parsed = schema.safeParse(data); if (!parsed.success) { return err(new ValidationError(parsed.error.issues)); } return ok(parsed.data); } catch (e) { if (e instanceof ApiError || e instanceof ValidationError) { return err(e); } return err( new ApiError(0, "Unknown", e instanceof Error ? e.message : String(e)), ); } } export const validatedApi = { getSession( token: AccessToken, ): Promise> { return xrpcValidated("com.atproto.server.getSession", sessionSchema, { token, }); }, refreshSession( refreshJwt: RefreshToken, ): Promise> { return xrpcValidated("com.atproto.server.refreshSession", sessionSchema, { method: "POST", token: refreshJwt, }); }, createSession( identifier: string, password: string, ): Promise> { return xrpcValidated("com.atproto.server.createSession", sessionSchema, { method: "POST", body: { identifier, password }, }); }, describeServer(): Promise< Result > { return xrpcValidated( "com.atproto.server.describeServer", serverDescriptionSchema, ); }, listAppPasswords( token: AccessToken, ): Promise< Result<{ passwords: ValidatedAppPassword[] }, ApiError | ValidationError> > { return xrpcValidated( "com.atproto.server.listAppPasswords", z.object({ passwords: z.array(appPasswordSchema) }), { token }, ); }, createAppPassword( token: AccessToken, name: string, scopes?: string, ): Promise> { return xrpcValidated( "com.atproto.server.createAppPassword", createdAppPasswordSchema, { method: "POST", token, body: { name, scopes }, }, ); }, listSessions( token: AccessToken, ): Promise< Result > { return xrpcValidated("_account.listSessions", listSessionsResponseSchema, { token, }); }, getTotpStatus( token: AccessToken, ): Promise> { return xrpcValidated("com.atproto.server.getTotpStatus", totpStatusSchema, { token, }); }, createTotpSecret( token: AccessToken, ): Promise> { return xrpcValidated( "com.atproto.server.createTotpSecret", totpSecretSchema, { method: "POST", token, }, ); }, enableTotp( token: AccessToken, code: string, ): Promise> { return xrpcValidated( "com.atproto.server.enableTotp", enableTotpResponseSchema, { method: "POST", token, body: { code }, }, ); }, listPasskeys( token: AccessToken, ): Promise< Result > { return xrpcValidated( "com.atproto.server.listPasskeys", listPasskeysResponseSchema, { token }, ); }, listTrustedDevices( token: AccessToken, ): Promise< Result > { return xrpcValidated( "_account.listTrustedDevices", listTrustedDevicesResponseSchema, { token }, ); }, getReauthStatus( token: AccessToken, ): Promise> { return xrpcValidated("_account.getReauthStatus", reauthStatusSchema, { token, }); }, getNotificationPrefs( token: AccessToken, ): Promise> { return xrpcValidated( "_account.getNotificationPrefs", notificationPrefsSchema, { token }, ); }, getDidDocument( token: AccessToken, ): Promise> { return xrpcValidated("_account.getDidDocument", didDocumentSchema, { token, }); }, describeRepo( token: AccessToken, repo: Did, ): Promise> { return xrpcValidated( "com.atproto.repo.describeRepo", repoDescriptionSchema, { token, params: { repo }, }, ); }, listRecords( token: AccessToken, repo: Did, collection: Nsid, options?: { limit?: number; cursor?: string; reverse?: boolean }, ): Promise> { const params: Record = { repo, collection }; if (options?.limit) params.limit = String(options.limit); if (options?.cursor) params.cursor = options.cursor; if (options?.reverse) params.reverse = "true"; return xrpcValidated( "com.atproto.repo.listRecords", listRecordsResponseSchema, { token, params, }, ); }, getRecord( token: AccessToken, repo: Did, collection: Nsid, rkey: Rkey, ): Promise> { return xrpcValidated("com.atproto.repo.getRecord", recordResponseSchema, { token, params: { repo, collection, rkey }, }); }, createRecord( token: AccessToken, repo: Did, collection: Nsid, record: unknown, rkey?: Rkey, ): Promise< Result > { return xrpcValidated( "com.atproto.repo.createRecord", createRecordResponseSchema, { method: "POST", token, body: { repo, collection, record, rkey }, }, ); }, getServerStats( token: AccessToken, ): Promise> { return xrpcValidated("_admin.getServerStats", serverStatsSchema, { token }); }, getServerConfig(): Promise< Result > { return xrpcValidated("_server.getConfig", serverConfigSchema); }, getPasswordStatus( token: AccessToken, ): Promise> { return xrpcValidated("_account.getPasswordStatus", passwordStatusSchema, { token, }); }, changePassword( token: AccessToken, currentPassword: string, newPassword: string, ): Promise> { return xrpcValidated("_account.changePassword", successResponseSchema, { method: "POST", token, body: { currentPassword, newPassword }, }); }, getLegacyLoginPreference( token: AccessToken, ): Promise< Result > { return xrpcValidated( "_account.getLegacyLoginPreference", legacyLoginPreferenceSchema, { token }, ); }, getAccountInfo( token: AccessToken, did: Did, ): Promise> { return xrpcValidated( "com.atproto.admin.getAccountInfo", accountInfoSchema, { token, params: { did }, }, ); }, searchAccounts( token: AccessToken, options?: { handle?: string; cursor?: string; limit?: number }, ): Promise< Result > { const params: Record = {}; if (options?.handle) params.handle = options.handle; if (options?.cursor) params.cursor = options.cursor; if (options?.limit) params.limit = String(options.limit); return xrpcValidated( "com.atproto.admin.searchAccounts", searchAccountsResponseSchema, { token, params, }, ); }, listBackups( token: AccessToken, ): Promise> { return xrpcValidated("_backup.listBackups", listBackupsResponseSchema, { token, }); }, createBackup( token: AccessToken, ): Promise< Result > { return xrpcValidated("_backup.createBackup", createBackupResponseSchema, { method: "POST", token, }); }, }; export { ValidationError };