mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-06 18:26:56 +00:00
refactor(frontend): delete type system boilerplate
This commit is contained in:
@@ -1,440 +0,0 @@
|
||||
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,
|
||||
createdAppPasswordSchema,
|
||||
createRecordResponseSchema,
|
||||
didDocumentSchema,
|
||||
enableTotpResponseSchema,
|
||||
legacyLoginPreferenceSchema,
|
||||
listPasskeysResponseSchema,
|
||||
listRecordsResponseSchema,
|
||||
listSessionsResponseSchema,
|
||||
listTrustedDevicesResponseSchema,
|
||||
notificationPrefsSchema,
|
||||
passwordStatusSchema,
|
||||
reauthStatusSchema,
|
||||
recordResponseSchema,
|
||||
repoDescriptionSchema,
|
||||
searchAccountsResponseSchema,
|
||||
serverConfigSchema,
|
||||
serverDescriptionSchema,
|
||||
serverStatsSchema,
|
||||
sessionSchema,
|
||||
successResponseSchema,
|
||||
totpSecretSchema,
|
||||
totpStatusSchema,
|
||||
type ValidatedAccountInfo,
|
||||
type ValidatedAppPassword,
|
||||
type ValidatedCreatedAppPassword,
|
||||
type ValidatedCreateRecordResponse,
|
||||
type ValidatedDidDocument,
|
||||
type ValidatedEnableTotpResponse,
|
||||
type ValidatedLegacyLoginPreference,
|
||||
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<string, string>;
|
||||
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<T>(
|
||||
method: string,
|
||||
schema: z.ZodType<T>,
|
||||
options?: XrpcOptions,
|
||||
): Promise<Result<T, ApiError | ValidationError>> {
|
||||
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<string, string> = {};
|
||||
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<Result<ValidatedSession, ApiError | ValidationError>> {
|
||||
return xrpcValidated("com.atproto.server.getSession", sessionSchema, {
|
||||
token,
|
||||
});
|
||||
},
|
||||
|
||||
refreshSession(
|
||||
refreshJwt: RefreshToken,
|
||||
): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
|
||||
return xrpcValidated("com.atproto.server.refreshSession", sessionSchema, {
|
||||
method: "POST",
|
||||
token: refreshJwt,
|
||||
});
|
||||
},
|
||||
|
||||
createSession(
|
||||
identifier: string,
|
||||
password: string,
|
||||
): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
|
||||
return xrpcValidated("com.atproto.server.createSession", sessionSchema, {
|
||||
method: "POST",
|
||||
body: { identifier, password },
|
||||
});
|
||||
},
|
||||
|
||||
describeServer(): Promise<
|
||||
Result<ValidatedServerDescription, ApiError | ValidationError>
|
||||
> {
|
||||
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<Result<ValidatedCreatedAppPassword, ApiError | ValidationError>> {
|
||||
return xrpcValidated(
|
||||
"com.atproto.server.createAppPassword",
|
||||
createdAppPasswordSchema,
|
||||
{
|
||||
method: "POST",
|
||||
token,
|
||||
body: { name, scopes },
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
listSessions(
|
||||
token: AccessToken,
|
||||
): Promise<
|
||||
Result<ValidatedListSessionsResponse, ApiError | ValidationError>
|
||||
> {
|
||||
return xrpcValidated("_account.listSessions", listSessionsResponseSchema, {
|
||||
token,
|
||||
});
|
||||
},
|
||||
|
||||
getTotpStatus(
|
||||
token: AccessToken,
|
||||
): Promise<Result<ValidatedTotpStatus, ApiError | ValidationError>> {
|
||||
return xrpcValidated("com.atproto.server.getTotpStatus", totpStatusSchema, {
|
||||
token,
|
||||
});
|
||||
},
|
||||
|
||||
createTotpSecret(
|
||||
token: AccessToken,
|
||||
): Promise<Result<ValidatedTotpSecret, ApiError | ValidationError>> {
|
||||
return xrpcValidated(
|
||||
"com.atproto.server.createTotpSecret",
|
||||
totpSecretSchema,
|
||||
{
|
||||
method: "POST",
|
||||
token,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
enableTotp(
|
||||
token: AccessToken,
|
||||
code: string,
|
||||
): Promise<Result<ValidatedEnableTotpResponse, ApiError | ValidationError>> {
|
||||
return xrpcValidated(
|
||||
"com.atproto.server.enableTotp",
|
||||
enableTotpResponseSchema,
|
||||
{
|
||||
method: "POST",
|
||||
token,
|
||||
body: { code },
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
listPasskeys(
|
||||
token: AccessToken,
|
||||
): Promise<
|
||||
Result<ValidatedListPasskeysResponse, ApiError | ValidationError>
|
||||
> {
|
||||
return xrpcValidated(
|
||||
"com.atproto.server.listPasskeys",
|
||||
listPasskeysResponseSchema,
|
||||
{ token },
|
||||
);
|
||||
},
|
||||
|
||||
listTrustedDevices(
|
||||
token: AccessToken,
|
||||
): Promise<
|
||||
Result<ValidatedListTrustedDevicesResponse, ApiError | ValidationError>
|
||||
> {
|
||||
return xrpcValidated(
|
||||
"_account.listTrustedDevices",
|
||||
listTrustedDevicesResponseSchema,
|
||||
{ token },
|
||||
);
|
||||
},
|
||||
|
||||
getReauthStatus(
|
||||
token: AccessToken,
|
||||
): Promise<Result<ValidatedReauthStatus, ApiError | ValidationError>> {
|
||||
return xrpcValidated("_account.getReauthStatus", reauthStatusSchema, {
|
||||
token,
|
||||
});
|
||||
},
|
||||
|
||||
getNotificationPrefs(
|
||||
token: AccessToken,
|
||||
): Promise<Result<ValidatedNotificationPrefs, ApiError | ValidationError>> {
|
||||
return xrpcValidated(
|
||||
"_account.getNotificationPrefs",
|
||||
notificationPrefsSchema,
|
||||
{ token },
|
||||
);
|
||||
},
|
||||
|
||||
getDidDocument(
|
||||
token: AccessToken,
|
||||
): Promise<Result<ValidatedDidDocument, ApiError | ValidationError>> {
|
||||
return xrpcValidated("_account.getDidDocument", didDocumentSchema, {
|
||||
token,
|
||||
});
|
||||
},
|
||||
|
||||
describeRepo(
|
||||
token: AccessToken,
|
||||
repo: Did,
|
||||
): Promise<Result<ValidatedRepoDescription, ApiError | ValidationError>> {
|
||||
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<Result<ValidatedListRecordsResponse, ApiError | ValidationError>> {
|
||||
const params: Record<string, string> = { 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<Result<ValidatedRecordResponse, ApiError | ValidationError>> {
|
||||
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<ValidatedCreateRecordResponse, ApiError | ValidationError>
|
||||
> {
|
||||
return xrpcValidated(
|
||||
"com.atproto.repo.createRecord",
|
||||
createRecordResponseSchema,
|
||||
{
|
||||
method: "POST",
|
||||
token,
|
||||
body: { repo, collection, record, rkey },
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
getServerStats(
|
||||
token: AccessToken,
|
||||
): Promise<Result<ValidatedServerStats, ApiError | ValidationError>> {
|
||||
return xrpcValidated("_admin.getServerStats", serverStatsSchema, { token });
|
||||
},
|
||||
|
||||
getServerConfig(): Promise<
|
||||
Result<ValidatedServerConfig, ApiError | ValidationError>
|
||||
> {
|
||||
return xrpcValidated("_server.getConfig", serverConfigSchema);
|
||||
},
|
||||
|
||||
getPasswordStatus(
|
||||
token: AccessToken,
|
||||
): Promise<Result<ValidatedPasswordStatus, ApiError | ValidationError>> {
|
||||
return xrpcValidated("_account.getPasswordStatus", passwordStatusSchema, {
|
||||
token,
|
||||
});
|
||||
},
|
||||
|
||||
changePassword(
|
||||
token: AccessToken,
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
): Promise<Result<ValidatedSuccessResponse, ApiError | ValidationError>> {
|
||||
return xrpcValidated("_account.changePassword", successResponseSchema, {
|
||||
method: "POST",
|
||||
token,
|
||||
body: { currentPassword, newPassword },
|
||||
});
|
||||
},
|
||||
|
||||
getLegacyLoginPreference(
|
||||
token: AccessToken,
|
||||
): Promise<
|
||||
Result<ValidatedLegacyLoginPreference, ApiError | ValidationError>
|
||||
> {
|
||||
return xrpcValidated(
|
||||
"_account.getLegacyLoginPreference",
|
||||
legacyLoginPreferenceSchema,
|
||||
{ token },
|
||||
);
|
||||
},
|
||||
|
||||
getAccountInfo(
|
||||
token: AccessToken,
|
||||
did: Did,
|
||||
): Promise<Result<ValidatedAccountInfo, ApiError | ValidationError>> {
|
||||
return xrpcValidated(
|
||||
"com.atproto.admin.getAccountInfo",
|
||||
accountInfoSchema,
|
||||
{
|
||||
token,
|
||||
params: { did },
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
searchAccounts(
|
||||
token: AccessToken,
|
||||
options?: { handle?: string; cursor?: string; limit?: number },
|
||||
): Promise<
|
||||
Result<ValidatedSearchAccountsResponse, ApiError | ValidationError>
|
||||
> {
|
||||
const params: Record<string, string> = {};
|
||||
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,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export { ValidationError };
|
||||
@@ -88,13 +88,6 @@ type SessionBase = {
|
||||
|
||||
export type Session = SessionBase & ContactState & AccountState;
|
||||
|
||||
export function hasEmail(
|
||||
session: Session,
|
||||
): session is Session & { email: EmailAddress } {
|
||||
return session.contactKind === "email" ||
|
||||
(session.contactKind === "channel" && session.email !== undefined);
|
||||
}
|
||||
|
||||
export function getSessionEmail(session: Session): EmailAddress | undefined {
|
||||
return session.contactKind === "email"
|
||||
? session.email
|
||||
@@ -103,24 +96,6 @@ export function getSessionEmail(session: Session): EmailAddress | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function isEmailVerified(session: Session): boolean {
|
||||
return session.contactKind === "email"
|
||||
? session.emailConfirmed
|
||||
: session.contactKind === "channel"
|
||||
? session.preferredChannelVerified
|
||||
: false;
|
||||
}
|
||||
|
||||
export function isMigrated(
|
||||
session: Session,
|
||||
): session is Session & { accountKind: "migrated" } {
|
||||
return session.accountKind === "migrated";
|
||||
}
|
||||
|
||||
export function isDeactivated(session: Session): boolean {
|
||||
return session.accountKind === "deactivated";
|
||||
}
|
||||
|
||||
export function isActive(session: Session): boolean {
|
||||
return session.accountKind === "active";
|
||||
}
|
||||
@@ -208,17 +183,6 @@ export interface ConfirmSignupResult {
|
||||
preferredChannelVerified?: boolean;
|
||||
}
|
||||
|
||||
export interface ListAppPasswordsResponse {
|
||||
passwords: AppPassword[];
|
||||
}
|
||||
|
||||
export interface AccountInviteCodesResponse {
|
||||
codes: InviteCodeInfo[];
|
||||
}
|
||||
|
||||
export interface CreateInviteCodeResponse {
|
||||
code: InviteCodeBrand;
|
||||
}
|
||||
|
||||
export interface ServerLinks {
|
||||
privacyPolicy?: string;
|
||||
@@ -317,9 +281,6 @@ export interface ListSessionsResponse {
|
||||
sessions: SessionInfo[];
|
||||
}
|
||||
|
||||
export interface RevokeAllSessionsResponse {
|
||||
revokedCount: number;
|
||||
}
|
||||
|
||||
export interface AccountSearchResult {
|
||||
did: Did;
|
||||
|
||||
@@ -25,99 +25,6 @@ export type PublicKeyMultibase = Brand<string, "PublicKeyMultibase">;
|
||||
export type DidKeyString = Brand<string, "DidKeyString">;
|
||||
export type ScopeSet = Brand<string, "ScopeSet">;
|
||||
|
||||
const DID_PLC_REGEX = /^did:plc:[a-z2-7]{24}$/;
|
||||
const DID_WEB_REGEX = /^did:web:.+$/;
|
||||
const HANDLE_REGEX =
|
||||
/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
||||
const AT_URI_REGEX = /^at:\/\/[^/]+\/[^/]+\/[^/]+$/;
|
||||
const CID_REGEX = /^[a-z2-7]{59}$|^baf[a-z2-7]+$/;
|
||||
const NSID_REGEX =
|
||||
/^[a-z]([a-z0-9-]*[a-z0-9])?(\.[a-z]([a-z0-9-]*[a-z0-9])?)+$/;
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const ISO_DATE_REGEX =
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
||||
|
||||
export function isDid(s: string): s is Did {
|
||||
return s.startsWith("did:plc:") || s.startsWith("did:web:");
|
||||
}
|
||||
|
||||
export function isDidPlc(s: string): s is DidPlc {
|
||||
return DID_PLC_REGEX.test(s);
|
||||
}
|
||||
|
||||
export function isDidWeb(s: string): s is DidWeb {
|
||||
return DID_WEB_REGEX.test(s);
|
||||
}
|
||||
|
||||
export function isHandle(s: string): s is Handle {
|
||||
return HANDLE_REGEX.test(s) && s.length <= 253;
|
||||
}
|
||||
|
||||
export function isAtUri(s: string): s is AtUri {
|
||||
return AT_URI_REGEX.test(s);
|
||||
}
|
||||
|
||||
export function isCid(s: string): s is Cid {
|
||||
return CID_REGEX.test(s);
|
||||
}
|
||||
|
||||
export function isNsid(s: string): s is Nsid {
|
||||
return NSID_REGEX.test(s);
|
||||
}
|
||||
|
||||
export function isEmail(s: string): s is EmailAddress {
|
||||
return EMAIL_REGEX.test(s);
|
||||
}
|
||||
|
||||
export function isISODate(s: string): s is ISODateString {
|
||||
return ISO_DATE_REGEX.test(s);
|
||||
}
|
||||
|
||||
export function asDid(s: string): Did {
|
||||
if (!isDid(s)) throw new TypeError(`Invalid DID: ${s}`);
|
||||
return s;
|
||||
}
|
||||
|
||||
export function asDidPlc(s: string): DidPlc {
|
||||
if (!isDidPlc(s)) throw new TypeError(`Invalid DID:PLC: ${s}`);
|
||||
return s as DidPlc;
|
||||
}
|
||||
|
||||
export function asDidWeb(s: string): DidWeb {
|
||||
if (!isDidWeb(s)) throw new TypeError(`Invalid DID:WEB: ${s}`);
|
||||
return s as DidWeb;
|
||||
}
|
||||
|
||||
export function asHandle(s: string): Handle {
|
||||
if (!isHandle(s)) throw new TypeError(`Invalid handle: ${s}`);
|
||||
return s;
|
||||
}
|
||||
|
||||
export function asAtUri(s: string): AtUri {
|
||||
if (!isAtUri(s)) throw new TypeError(`Invalid AT-URI: ${s}`);
|
||||
return s;
|
||||
}
|
||||
|
||||
export function asCid(s: string): Cid {
|
||||
if (!isCid(s)) throw new TypeError(`Invalid CID: ${s}`);
|
||||
return s;
|
||||
}
|
||||
|
||||
export function asNsid(s: string): Nsid {
|
||||
if (!isNsid(s)) throw new TypeError(`Invalid NSID: ${s}`);
|
||||
return s;
|
||||
}
|
||||
|
||||
export function asEmail(s: string): EmailAddress {
|
||||
if (!isEmail(s)) throw new TypeError(`Invalid email: ${s}`);
|
||||
return s;
|
||||
}
|
||||
|
||||
export function asISODate(s: string): ISODateString {
|
||||
if (!isISODate(s)) throw new TypeError(`Invalid ISO date: ${s}`);
|
||||
return s;
|
||||
}
|
||||
|
||||
export function unsafeAsDid(s: string): Did {
|
||||
return s as Did;
|
||||
}
|
||||
@@ -134,26 +41,10 @@ export function unsafeAsRefreshToken(s: string): RefreshToken {
|
||||
return s as RefreshToken;
|
||||
}
|
||||
|
||||
export function unsafeAsServiceToken(s: string): ServiceToken {
|
||||
return s as ServiceToken;
|
||||
}
|
||||
|
||||
export function unsafeAsSetupToken(s: string): SetupToken {
|
||||
return s as SetupToken;
|
||||
}
|
||||
|
||||
export function unsafeAsCid(s: string): Cid {
|
||||
return s as Cid;
|
||||
}
|
||||
|
||||
export function unsafeAsRkey(s: string): Rkey {
|
||||
return s as Rkey;
|
||||
}
|
||||
|
||||
export function unsafeAsAtUri(s: string): AtUri {
|
||||
return s as AtUri;
|
||||
}
|
||||
|
||||
export function unsafeAsNsid(s: string): Nsid {
|
||||
return s as Nsid;
|
||||
}
|
||||
@@ -172,29 +63,7 @@ export function unsafeAsInviteCode(s: string): InviteCode {
|
||||
return s as InviteCode;
|
||||
}
|
||||
|
||||
export function unsafeAsPublicKeyMultibase(s: string): PublicKeyMultibase {
|
||||
return s as PublicKeyMultibase;
|
||||
}
|
||||
|
||||
export function unsafeAsDidKey(s: string): DidKeyString {
|
||||
return s as DidKeyString;
|
||||
}
|
||||
|
||||
export function unsafeAsScopeSet(s: string): ScopeSet {
|
||||
return s as ScopeSet;
|
||||
}
|
||||
|
||||
export function parseAtUri(
|
||||
uri: AtUri,
|
||||
): { repo: Did; collection: Nsid; rkey: Rkey } {
|
||||
const parts = uri.replace("at://", "").split("/");
|
||||
return {
|
||||
repo: unsafeAsDid(parts[0]),
|
||||
collection: unsafeAsNsid(parts[1]),
|
||||
rkey: unsafeAsRkey(parts[2]),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeAtUri(repo: Did, collection: Nsid, rkey: Rkey): AtUri {
|
||||
return `at://${repo}/${collection}/${rkey}` as AtUri;
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
export function assertNever(x: never, message?: string): never {
|
||||
throw new Error(message ?? `Unexpected value: ${JSON.stringify(x)}`);
|
||||
}
|
||||
|
||||
export function exhaustive<T extends string | number | symbol>(
|
||||
value: T,
|
||||
handlers: Record<T, () => void>,
|
||||
): void {
|
||||
const handler = handlers[value];
|
||||
if (handler) {
|
||||
handler();
|
||||
} else {
|
||||
assertNever(value as never, `Unhandled case: ${String(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function exhaustiveMap<T extends string | number | symbol, R>(
|
||||
value: T,
|
||||
handlers: Record<T, () => R>,
|
||||
): R {
|
||||
const handler = handlers[value];
|
||||
if (handler) {
|
||||
return handler();
|
||||
}
|
||||
return assertNever(value as never, `Unhandled case: ${String(value)}`);
|
||||
}
|
||||
|
||||
export async function exhaustiveAsync<T extends string | number | symbol>(
|
||||
value: T,
|
||||
handlers: Record<T, () => Promise<void>>,
|
||||
): Promise<void> {
|
||||
const handler = handlers[value];
|
||||
if (handler) {
|
||||
await handler();
|
||||
} else {
|
||||
assertNever(value as never, `Unhandled case: ${String(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function exhaustiveMapAsync<T extends string | number | symbol, R>(
|
||||
value: T,
|
||||
handlers: Record<T, () => Promise<R>>,
|
||||
): Promise<R> {
|
||||
const handler = handlers[value];
|
||||
if (handler) {
|
||||
return handler();
|
||||
}
|
||||
return assertNever(value as never, `Unhandled case: ${String(value)}`);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from "./result.ts";
|
||||
export * from "./branded.ts";
|
||||
export * from "./exhaustive.ts";
|
||||
export * from "./api.ts";
|
||||
export * from "./routes.ts";
|
||||
@@ -22,90 +22,5 @@ export function isErr<T, E>(
|
||||
return !result.ok;
|
||||
}
|
||||
|
||||
export function map<T, U, E>(
|
||||
result: Result<T, E>,
|
||||
fn: (t: T) => U,
|
||||
): Result<U, E> {
|
||||
return result.ok ? ok(fn(result.value)) : result;
|
||||
}
|
||||
|
||||
export function mapErr<T, E, F>(
|
||||
result: Result<T, E>,
|
||||
fn: (e: E) => F,
|
||||
): Result<T, F> {
|
||||
return result.ok ? result : err(fn(result.error));
|
||||
}
|
||||
|
||||
export function flatMap<T, U, E>(
|
||||
result: Result<T, E>,
|
||||
fn: (t: T) => Result<U, E>,
|
||||
): Result<U, E> {
|
||||
return result.ok ? fn(result.value) : result;
|
||||
}
|
||||
|
||||
export function unwrap<T, E>(result: Result<T, E>): T {
|
||||
if (result.ok) return result.value;
|
||||
throw result.error instanceof Error
|
||||
? result.error
|
||||
: new Error(String(result.error));
|
||||
}
|
||||
|
||||
export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
|
||||
return result.ok ? result.value : defaultValue;
|
||||
}
|
||||
|
||||
export function unwrapOrElse<T, E>(result: Result<T, E>, fn: (e: E) => T): T {
|
||||
return result.ok ? result.value : fn(result.error);
|
||||
}
|
||||
|
||||
export function match<T, E, U>(
|
||||
result: Result<T, E>,
|
||||
handlers: { ok: (t: T) => U; err: (e: E) => U },
|
||||
): U {
|
||||
return result.ok ? handlers.ok(result.value) : handlers.err(result.error);
|
||||
}
|
||||
|
||||
export async function tryAsync<T>(
|
||||
fn: () => Promise<T>,
|
||||
): Promise<Result<T, Error>> {
|
||||
try {
|
||||
return ok(await fn());
|
||||
} catch (e) {
|
||||
return err(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
}
|
||||
|
||||
export async function tryAsyncWith<T, E>(
|
||||
fn: () => Promise<T>,
|
||||
mapError: (e: unknown) => E,
|
||||
): Promise<Result<T, E>> {
|
||||
try {
|
||||
return ok(await fn());
|
||||
} catch (e) {
|
||||
return err(mapError(e));
|
||||
}
|
||||
}
|
||||
|
||||
export function fromNullable<T>(value: T | null | undefined): Result<T, null> {
|
||||
return value != null ? ok(value) : err(null);
|
||||
}
|
||||
|
||||
export function toNullable<T, E>(result: Result<T, E>): T | null {
|
||||
return result.ok ? result.value : null;
|
||||
}
|
||||
|
||||
export function collect<T, E>(results: Result<T, E>[]): Result<T[], E> {
|
||||
const values: T[] = [];
|
||||
for (const result of results) {
|
||||
if (!result.ok) return result;
|
||||
values.push(result.value);
|
||||
}
|
||||
return ok(values);
|
||||
}
|
||||
|
||||
export async function collectAsync<T, E>(
|
||||
results: Promise<Result<T, E>>[],
|
||||
): Promise<Result<T[], E>> {
|
||||
const settled = await Promise.all(results);
|
||||
return collect(settled);
|
||||
}
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
unsafeAsAccessToken,
|
||||
unsafeAsAtUri,
|
||||
unsafeAsCid,
|
||||
unsafeAsDid,
|
||||
unsafeAsEmail,
|
||||
unsafeAsHandle,
|
||||
unsafeAsInviteCode,
|
||||
unsafeAsISODate,
|
||||
unsafeAsNsid,
|
||||
unsafeAsPublicKeyMultibase,
|
||||
unsafeAsRefreshToken,
|
||||
unsafeAsRkey,
|
||||
} from "./branded.ts";
|
||||
|
||||
const did = z.string().transform((s) => unsafeAsDid(s));
|
||||
const handle = z.string().transform((s) => unsafeAsHandle(s));
|
||||
const accessToken = z.string().transform((s) => unsafeAsAccessToken(s));
|
||||
const refreshToken = z.string().transform((s) => unsafeAsRefreshToken(s));
|
||||
const cid = z.string().transform((s) => unsafeAsCid(s));
|
||||
const nsid = z.string().transform((s) => unsafeAsNsid(s));
|
||||
const atUri = z.string().transform((s) => unsafeAsAtUri(s));
|
||||
const _rkey = z.string().transform((s) => unsafeAsRkey(s));
|
||||
const isoDate = z.string().transform((s) => unsafeAsISODate(s));
|
||||
const email = z.string().transform((s) => unsafeAsEmail(s));
|
||||
const inviteCode = z.string().transform((s) => unsafeAsInviteCode(s));
|
||||
const publicKeyMultibase = z.string().transform((s) =>
|
||||
unsafeAsPublicKeyMultibase(s)
|
||||
);
|
||||
|
||||
export const verificationChannel = z.enum([
|
||||
"email",
|
||||
"discord",
|
||||
"telegram",
|
||||
"signal",
|
||||
]);
|
||||
export const didType = z.enum(["plc", "web", "web-external"]);
|
||||
export const accountStatus = z.enum([
|
||||
"active",
|
||||
"deactivated",
|
||||
"migrated",
|
||||
"suspended",
|
||||
"deleted",
|
||||
]);
|
||||
export const sessionType = z.enum(["oauth", "legacy", "app_password"]);
|
||||
export const reauthMethod = z.enum(["password", "totp", "passkey"]);
|
||||
|
||||
export const sessionSchema = z.object({
|
||||
did: did,
|
||||
handle: handle,
|
||||
email: email.optional(),
|
||||
emailConfirmed: z.boolean().optional(),
|
||||
preferredChannel: verificationChannel.optional(),
|
||||
preferredChannelVerified: z.boolean().optional(),
|
||||
isAdmin: z.boolean().optional(),
|
||||
active: z.boolean().optional(),
|
||||
status: accountStatus.optional(),
|
||||
migratedToPds: z.string().optional(),
|
||||
migratedAt: isoDate.optional(),
|
||||
accessJwt: accessToken,
|
||||
refreshJwt: refreshToken,
|
||||
});
|
||||
|
||||
export const serverLinksSchema = z.object({
|
||||
privacyPolicy: z.string().optional(),
|
||||
termsOfService: z.string().optional(),
|
||||
});
|
||||
|
||||
export const serverDescriptionSchema = z.object({
|
||||
availableUserDomains: z.array(z.string()),
|
||||
inviteCodeRequired: z.boolean(),
|
||||
links: serverLinksSchema.optional(),
|
||||
version: z.string().optional(),
|
||||
availableCommsChannels: z.array(verificationChannel).optional(),
|
||||
selfHostedDidWebEnabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const appPasswordSchema = z.object({
|
||||
name: z.string(),
|
||||
createdAt: isoDate,
|
||||
scopes: z.string().optional(),
|
||||
createdByController: z.string().optional(),
|
||||
});
|
||||
|
||||
export const createdAppPasswordSchema = z.object({
|
||||
name: z.string(),
|
||||
password: z.string(),
|
||||
createdAt: isoDate,
|
||||
scopes: z.string().optional(),
|
||||
});
|
||||
|
||||
export const inviteCodeUseSchema = z.object({
|
||||
usedBy: did,
|
||||
usedByHandle: handle.optional(),
|
||||
usedAt: isoDate,
|
||||
});
|
||||
|
||||
export const inviteCodeInfoSchema = z.object({
|
||||
code: inviteCode,
|
||||
available: z.number(),
|
||||
disabled: z.boolean(),
|
||||
forAccount: did,
|
||||
createdBy: did,
|
||||
createdAt: isoDate,
|
||||
uses: z.array(inviteCodeUseSchema),
|
||||
});
|
||||
|
||||
export const sessionInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
sessionType: sessionType,
|
||||
clientName: z.string().nullable(),
|
||||
createdAt: isoDate,
|
||||
expiresAt: isoDate,
|
||||
isCurrent: z.boolean(),
|
||||
});
|
||||
|
||||
export const listSessionsResponseSchema = z.object({
|
||||
sessions: z.array(sessionInfoSchema),
|
||||
});
|
||||
|
||||
export const totpStatusSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
hasBackupCodes: z.boolean(),
|
||||
});
|
||||
|
||||
export const totpSecretSchema = z.object({
|
||||
uri: z.string(),
|
||||
qrBase64: z.string(),
|
||||
});
|
||||
|
||||
export const enableTotpResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
backupCodes: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const passkeyInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
credentialId: z.string(),
|
||||
friendlyName: z.string().nullable(),
|
||||
createdAt: isoDate,
|
||||
lastUsed: isoDate.nullable(),
|
||||
});
|
||||
|
||||
export const listPasskeysResponseSchema = z.object({
|
||||
passkeys: z.array(passkeyInfoSchema),
|
||||
});
|
||||
|
||||
export const trustedDeviceSchema = z.object({
|
||||
id: z.string(),
|
||||
userAgent: z.string().nullable(),
|
||||
friendlyName: z.string().nullable(),
|
||||
trustedAt: isoDate.nullable(),
|
||||
trustedUntil: isoDate.nullable(),
|
||||
lastSeenAt: isoDate,
|
||||
});
|
||||
|
||||
export const listTrustedDevicesResponseSchema = z.object({
|
||||
devices: z.array(trustedDeviceSchema),
|
||||
});
|
||||
|
||||
export const reauthStatusSchema = z.object({
|
||||
requiresReauth: z.boolean(),
|
||||
lastReauthAt: isoDate.nullable(),
|
||||
availableMethods: z.array(reauthMethod),
|
||||
});
|
||||
|
||||
export const reauthResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
reauthAt: isoDate,
|
||||
});
|
||||
|
||||
export const notificationPrefsSchema = z.object({
|
||||
preferredChannel: verificationChannel,
|
||||
email: email,
|
||||
discordUsername: z.string().nullable(),
|
||||
discordVerified: z.boolean(),
|
||||
telegramUsername: z.string().nullable(),
|
||||
telegramVerified: z.boolean(),
|
||||
signalUsername: z.string().nullable(),
|
||||
signalVerified: z.boolean(),
|
||||
});
|
||||
|
||||
export const verificationMethodSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
controller: z.string(),
|
||||
publicKeyMultibase: publicKeyMultibase,
|
||||
});
|
||||
|
||||
export const serviceEndpointSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
serviceEndpoint: z.string(),
|
||||
});
|
||||
|
||||
export const didDocumentSchema = z.object({
|
||||
"@context": z.array(z.string()),
|
||||
id: did,
|
||||
alsoKnownAs: z.array(z.string()),
|
||||
verificationMethod: z.array(verificationMethodSchema),
|
||||
service: z.array(serviceEndpointSchema),
|
||||
});
|
||||
|
||||
export const repoDescriptionSchema = z.object({
|
||||
handle: handle,
|
||||
did: did,
|
||||
didDoc: didDocumentSchema,
|
||||
collections: z.array(nsid),
|
||||
handleIsCorrect: z.boolean(),
|
||||
});
|
||||
|
||||
export const recordInfoSchema = z.object({
|
||||
uri: atUri,
|
||||
cid: cid,
|
||||
value: z.unknown(),
|
||||
});
|
||||
|
||||
export const listRecordsResponseSchema = z.object({
|
||||
records: z.array(recordInfoSchema),
|
||||
cursor: z.string().optional(),
|
||||
});
|
||||
|
||||
export const recordResponseSchema = z.object({
|
||||
uri: atUri,
|
||||
cid: cid,
|
||||
value: z.unknown(),
|
||||
});
|
||||
|
||||
export const createRecordResponseSchema = z.object({
|
||||
uri: atUri,
|
||||
cid: cid,
|
||||
});
|
||||
|
||||
export const serverStatsSchema = z.object({
|
||||
userCount: z.number(),
|
||||
repoCount: z.number(),
|
||||
recordCount: z.number(),
|
||||
blobStorageBytes: z.number(),
|
||||
});
|
||||
|
||||
export const serverConfigSchema = z.object({
|
||||
serverName: z.string(),
|
||||
primaryColor: z.string().nullable(),
|
||||
primaryColorDark: z.string().nullable(),
|
||||
secondaryColor: z.string().nullable(),
|
||||
secondaryColorDark: z.string().nullable(),
|
||||
logoCid: cid.nullable(),
|
||||
});
|
||||
|
||||
export const passwordStatusSchema = z.object({
|
||||
hasPassword: z.boolean(),
|
||||
});
|
||||
|
||||
export const successResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
export const legacyLoginPreferenceSchema = z.object({
|
||||
allowLegacyLogin: z.boolean(),
|
||||
hasMfa: z.boolean(),
|
||||
});
|
||||
|
||||
export const accountInfoSchema = z.object({
|
||||
did: did,
|
||||
handle: handle,
|
||||
email: email.optional(),
|
||||
indexedAt: isoDate,
|
||||
emailConfirmedAt: isoDate.optional(),
|
||||
invitesDisabled: z.boolean().optional(),
|
||||
deactivatedAt: isoDate.optional(),
|
||||
});
|
||||
|
||||
export const searchAccountsResponseSchema = z.object({
|
||||
cursor: z.string().optional(),
|
||||
accounts: z.array(accountInfoSchema),
|
||||
});
|
||||
|
||||
export type ValidatedSession = z.infer<typeof sessionSchema>;
|
||||
export type ValidatedServerDescription = z.infer<
|
||||
typeof serverDescriptionSchema
|
||||
>;
|
||||
export type ValidatedAppPassword = z.infer<typeof appPasswordSchema>;
|
||||
export type ValidatedCreatedAppPassword = z.infer<
|
||||
typeof createdAppPasswordSchema
|
||||
>;
|
||||
export type ValidatedInviteCodeInfo = z.infer<typeof inviteCodeInfoSchema>;
|
||||
export type ValidatedSessionInfo = z.infer<typeof sessionInfoSchema>;
|
||||
export type ValidatedListSessionsResponse = z.infer<
|
||||
typeof listSessionsResponseSchema
|
||||
>;
|
||||
export type ValidatedTotpStatus = z.infer<typeof totpStatusSchema>;
|
||||
export type ValidatedTotpSecret = z.infer<typeof totpSecretSchema>;
|
||||
export type ValidatedEnableTotpResponse = z.infer<
|
||||
typeof enableTotpResponseSchema
|
||||
>;
|
||||
export type ValidatedPasskeyInfo = z.infer<typeof passkeyInfoSchema>;
|
||||
export type ValidatedListPasskeysResponse = z.infer<
|
||||
typeof listPasskeysResponseSchema
|
||||
>;
|
||||
export type ValidatedTrustedDevice = z.infer<typeof trustedDeviceSchema>;
|
||||
export type ValidatedListTrustedDevicesResponse = z.infer<
|
||||
typeof listTrustedDevicesResponseSchema
|
||||
>;
|
||||
export type ValidatedReauthStatus = z.infer<typeof reauthStatusSchema>;
|
||||
export type ValidatedReauthResponse = z.infer<typeof reauthResponseSchema>;
|
||||
export type ValidatedNotificationPrefs = z.infer<
|
||||
typeof notificationPrefsSchema
|
||||
>;
|
||||
export type ValidatedDidDocument = z.infer<typeof didDocumentSchema>;
|
||||
export type ValidatedRepoDescription = z.infer<typeof repoDescriptionSchema>;
|
||||
export type ValidatedListRecordsResponse = z.infer<
|
||||
typeof listRecordsResponseSchema
|
||||
>;
|
||||
export type ValidatedRecordResponse = z.infer<typeof recordResponseSchema>;
|
||||
export type ValidatedCreateRecordResponse = z.infer<
|
||||
typeof createRecordResponseSchema
|
||||
>;
|
||||
export type ValidatedServerStats = z.infer<typeof serverStatsSchema>;
|
||||
export type ValidatedServerConfig = z.infer<typeof serverConfigSchema>;
|
||||
export type ValidatedPasswordStatus = z.infer<typeof passwordStatusSchema>;
|
||||
export type ValidatedSuccessResponse = z.infer<typeof successResponseSchema>;
|
||||
export type ValidatedLegacyLoginPreference = z.infer<
|
||||
typeof legacyLoginPreferenceSchema
|
||||
>;
|
||||
export type ValidatedAccountInfo = z.infer<typeof accountInfoSchema>;
|
||||
export type ValidatedSearchAccountsResponse = z.infer<
|
||||
typeof searchAccountsResponseSchema
|
||||
>;
|
||||
@@ -61,18 +61,6 @@ export function finish(_state: TotpBackup): TotpIdle {
|
||||
return idleState;
|
||||
}
|
||||
|
||||
export function isIdle(state: TotpSetupState): state is TotpIdle {
|
||||
return state.step === "idle";
|
||||
}
|
||||
|
||||
export function isQr(state: TotpSetupState): state is TotpQr {
|
||||
return state.step === "qr";
|
||||
}
|
||||
|
||||
export function isVerify(state: TotpSetupState): state is TotpVerify {
|
||||
return state.step === "verify";
|
||||
}
|
||||
|
||||
export function isBackup(state: TotpSetupState): state is TotpBackup {
|
||||
return state.step === "backup";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user