Our Personal Data Server from scratch! tranquil.farm
atproto pds rust postgresql fun oauth

refactor(frontend): delete type system boilerplate #68

merged opened by oyster.cafe targeting main from refactor/frontend-deletions
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mhg53l5uj322
-1090
Diff #2
-440
frontend/src/lib/api-validated.ts
··· 1 - import { z } from "zod"; 2 - import { err, ok, type Result } from "./types/result.ts"; 3 - import { ApiError } from "./api.ts"; 4 - import type { 5 - AccessToken, 6 - Did, 7 - Nsid, 8 - RefreshToken, 9 - Rkey, 10 - } from "./types/branded.ts"; 11 - import { 12 - accountInfoSchema, 13 - appPasswordSchema, 14 - createdAppPasswordSchema, 15 - createRecordResponseSchema, 16 - didDocumentSchema, 17 - enableTotpResponseSchema, 18 - legacyLoginPreferenceSchema, 19 - listPasskeysResponseSchema, 20 - listRecordsResponseSchema, 21 - listSessionsResponseSchema, 22 - listTrustedDevicesResponseSchema, 23 - notificationPrefsSchema, 24 - passwordStatusSchema, 25 - reauthStatusSchema, 26 - recordResponseSchema, 27 - repoDescriptionSchema, 28 - searchAccountsResponseSchema, 29 - serverConfigSchema, 30 - serverDescriptionSchema, 31 - serverStatsSchema, 32 - sessionSchema, 33 - successResponseSchema, 34 - totpSecretSchema, 35 - totpStatusSchema, 36 - type ValidatedAccountInfo, 37 - type ValidatedAppPassword, 38 - type ValidatedCreatedAppPassword, 39 - type ValidatedCreateRecordResponse, 40 - type ValidatedDidDocument, 41 - type ValidatedEnableTotpResponse, 42 - type ValidatedLegacyLoginPreference, 43 - type ValidatedListPasskeysResponse, 44 - type ValidatedListRecordsResponse, 45 - type ValidatedListSessionsResponse, 46 - type ValidatedListTrustedDevicesResponse, 47 - type ValidatedNotificationPrefs, 48 - type ValidatedPasswordStatus, 49 - type ValidatedReauthStatus, 50 - type ValidatedRecordResponse, 51 - type ValidatedRepoDescription, 52 - type ValidatedSearchAccountsResponse, 53 - type ValidatedServerConfig, 54 - type ValidatedServerDescription, 55 - type ValidatedServerStats, 56 - type ValidatedSession, 57 - type ValidatedSuccessResponse, 58 - type ValidatedTotpSecret, 59 - type ValidatedTotpStatus, 60 - } from "./types/schemas.ts"; 61 - 62 - const API_BASE = "/xrpc"; 63 - 64 - interface XrpcOptions { 65 - method?: "GET" | "POST"; 66 - params?: Record<string, string>; 67 - body?: unknown; 68 - token?: string; 69 - } 70 - 71 - class ValidationError extends Error { 72 - constructor( 73 - public issues: z.ZodIssue[], 74 - message: string = "API response validation failed", 75 - ) { 76 - super(message); 77 - this.name = "ValidationError"; 78 - } 79 - } 80 - 81 - async function xrpcValidated<T>( 82 - method: string, 83 - schema: z.ZodType<T>, 84 - options?: XrpcOptions, 85 - ): Promise<Result<T, ApiError | ValidationError>> { 86 - const { method: httpMethod = "GET", params, body, token } = options ?? {}; 87 - let url = `${API_BASE}/${method}`; 88 - if (params) { 89 - const searchParams = new URLSearchParams(params); 90 - url += `?${searchParams}`; 91 - } 92 - const headers: Record<string, string> = {}; 93 - if (token) { 94 - headers["Authorization"] = `Bearer ${token}`; 95 - } 96 - if (body) { 97 - headers["Content-Type"] = "application/json"; 98 - } 99 - 100 - try { 101 - const res = await fetch(url, { 102 - method: httpMethod, 103 - headers, 104 - body: body ? JSON.stringify(body) : undefined, 105 - }); 106 - 107 - if (!res.ok) { 108 - const errData = await res.json().catch(() => ({ 109 - error: "Unknown", 110 - message: res.statusText, 111 - })); 112 - return err(new ApiError(res.status, errData.error, errData.message)); 113 - } 114 - 115 - const data = await res.json(); 116 - const parsed = schema.safeParse(data); 117 - 118 - if (!parsed.success) { 119 - return err(new ValidationError(parsed.error.issues)); 120 - } 121 - 122 - return ok(parsed.data); 123 - } catch (e) { 124 - if (e instanceof ApiError || e instanceof ValidationError) { 125 - return err(e); 126 - } 127 - return err( 128 - new ApiError(0, "Unknown", e instanceof Error ? e.message : String(e)), 129 - ); 130 - } 131 - } 132 - 133 - export const validatedApi = { 134 - getSession( 135 - token: AccessToken, 136 - ): Promise<Result<ValidatedSession, ApiError | ValidationError>> { 137 - return xrpcValidated("com.atproto.server.getSession", sessionSchema, { 138 - token, 139 - }); 140 - }, 141 - 142 - refreshSession( 143 - refreshJwt: RefreshToken, 144 - ): Promise<Result<ValidatedSession, ApiError | ValidationError>> { 145 - return xrpcValidated("com.atproto.server.refreshSession", sessionSchema, { 146 - method: "POST", 147 - token: refreshJwt, 148 - }); 149 - }, 150 - 151 - createSession( 152 - identifier: string, 153 - password: string, 154 - ): Promise<Result<ValidatedSession, ApiError | ValidationError>> { 155 - return xrpcValidated("com.atproto.server.createSession", sessionSchema, { 156 - method: "POST", 157 - body: { identifier, password }, 158 - }); 159 - }, 160 - 161 - describeServer(): Promise< 162 - Result<ValidatedServerDescription, ApiError | ValidationError> 163 - > { 164 - return xrpcValidated( 165 - "com.atproto.server.describeServer", 166 - serverDescriptionSchema, 167 - ); 168 - }, 169 - 170 - listAppPasswords( 171 - token: AccessToken, 172 - ): Promise< 173 - Result<{ passwords: ValidatedAppPassword[] }, ApiError | ValidationError> 174 - > { 175 - return xrpcValidated( 176 - "com.atproto.server.listAppPasswords", 177 - z.object({ passwords: z.array(appPasswordSchema) }), 178 - { token }, 179 - ); 180 - }, 181 - 182 - createAppPassword( 183 - token: AccessToken, 184 - name: string, 185 - scopes?: string, 186 - ): Promise<Result<ValidatedCreatedAppPassword, ApiError | ValidationError>> { 187 - return xrpcValidated( 188 - "com.atproto.server.createAppPassword", 189 - createdAppPasswordSchema, 190 - { 191 - method: "POST", 192 - token, 193 - body: { name, scopes }, 194 - }, 195 - ); 196 - }, 197 - 198 - listSessions( 199 - token: AccessToken, 200 - ): Promise< 201 - Result<ValidatedListSessionsResponse, ApiError | ValidationError> 202 - > { 203 - return xrpcValidated("_account.listSessions", listSessionsResponseSchema, { 204 - token, 205 - }); 206 - }, 207 - 208 - getTotpStatus( 209 - token: AccessToken, 210 - ): Promise<Result<ValidatedTotpStatus, ApiError | ValidationError>> { 211 - return xrpcValidated("com.atproto.server.getTotpStatus", totpStatusSchema, { 212 - token, 213 - }); 214 - }, 215 - 216 - createTotpSecret( 217 - token: AccessToken, 218 - ): Promise<Result<ValidatedTotpSecret, ApiError | ValidationError>> { 219 - return xrpcValidated( 220 - "com.atproto.server.createTotpSecret", 221 - totpSecretSchema, 222 - { 223 - method: "POST", 224 - token, 225 - }, 226 - ); 227 - }, 228 - 229 - enableTotp( 230 - token: AccessToken, 231 - code: string, 232 - ): Promise<Result<ValidatedEnableTotpResponse, ApiError | ValidationError>> { 233 - return xrpcValidated( 234 - "com.atproto.server.enableTotp", 235 - enableTotpResponseSchema, 236 - { 237 - method: "POST", 238 - token, 239 - body: { code }, 240 - }, 241 - ); 242 - }, 243 - 244 - listPasskeys( 245 - token: AccessToken, 246 - ): Promise< 247 - Result<ValidatedListPasskeysResponse, ApiError | ValidationError> 248 - > { 249 - return xrpcValidated( 250 - "com.atproto.server.listPasskeys", 251 - listPasskeysResponseSchema, 252 - { token }, 253 - ); 254 - }, 255 - 256 - listTrustedDevices( 257 - token: AccessToken, 258 - ): Promise< 259 - Result<ValidatedListTrustedDevicesResponse, ApiError | ValidationError> 260 - > { 261 - return xrpcValidated( 262 - "_account.listTrustedDevices", 263 - listTrustedDevicesResponseSchema, 264 - { token }, 265 - ); 266 - }, 267 - 268 - getReauthStatus( 269 - token: AccessToken, 270 - ): Promise<Result<ValidatedReauthStatus, ApiError | ValidationError>> { 271 - return xrpcValidated("_account.getReauthStatus", reauthStatusSchema, { 272 - token, 273 - }); 274 - }, 275 - 276 - getNotificationPrefs( 277 - token: AccessToken, 278 - ): Promise<Result<ValidatedNotificationPrefs, ApiError | ValidationError>> { 279 - return xrpcValidated( 280 - "_account.getNotificationPrefs", 281 - notificationPrefsSchema, 282 - { token }, 283 - ); 284 - }, 285 - 286 - getDidDocument( 287 - token: AccessToken, 288 - ): Promise<Result<ValidatedDidDocument, ApiError | ValidationError>> { 289 - return xrpcValidated("_account.getDidDocument", didDocumentSchema, { 290 - token, 291 - }); 292 - }, 293 - 294 - describeRepo( 295 - token: AccessToken, 296 - repo: Did, 297 - ): Promise<Result<ValidatedRepoDescription, ApiError | ValidationError>> { 298 - return xrpcValidated( 299 - "com.atproto.repo.describeRepo", 300 - repoDescriptionSchema, 301 - { 302 - token, 303 - params: { repo }, 304 - }, 305 - ); 306 - }, 307 - 308 - listRecords( 309 - token: AccessToken, 310 - repo: Did, 311 - collection: Nsid, 312 - options?: { limit?: number; cursor?: string; reverse?: boolean }, 313 - ): Promise<Result<ValidatedListRecordsResponse, ApiError | ValidationError>> { 314 - const params: Record<string, string> = { repo, collection }; 315 - if (options?.limit) params.limit = String(options.limit); 316 - if (options?.cursor) params.cursor = options.cursor; 317 - if (options?.reverse) params.reverse = "true"; 318 - return xrpcValidated( 319 - "com.atproto.repo.listRecords", 320 - listRecordsResponseSchema, 321 - { 322 - token, 323 - params, 324 - }, 325 - ); 326 - }, 327 - 328 - getRecord( 329 - token: AccessToken, 330 - repo: Did, 331 - collection: Nsid, 332 - rkey: Rkey, 333 - ): Promise<Result<ValidatedRecordResponse, ApiError | ValidationError>> { 334 - return xrpcValidated("com.atproto.repo.getRecord", recordResponseSchema, { 335 - token, 336 - params: { repo, collection, rkey }, 337 - }); 338 - }, 339 - 340 - createRecord( 341 - token: AccessToken, 342 - repo: Did, 343 - collection: Nsid, 344 - record: unknown, 345 - rkey?: Rkey, 346 - ): Promise< 347 - Result<ValidatedCreateRecordResponse, ApiError | ValidationError> 348 - > { 349 - return xrpcValidated( 350 - "com.atproto.repo.createRecord", 351 - createRecordResponseSchema, 352 - { 353 - method: "POST", 354 - token, 355 - body: { repo, collection, record, rkey }, 356 - }, 357 - ); 358 - }, 359 - 360 - getServerStats( 361 - token: AccessToken, 362 - ): Promise<Result<ValidatedServerStats, ApiError | ValidationError>> { 363 - return xrpcValidated("_admin.getServerStats", serverStatsSchema, { token }); 364 - }, 365 - 366 - getServerConfig(): Promise< 367 - Result<ValidatedServerConfig, ApiError | ValidationError> 368 - > { 369 - return xrpcValidated("_server.getConfig", serverConfigSchema); 370 - }, 371 - 372 - getPasswordStatus( 373 - token: AccessToken, 374 - ): Promise<Result<ValidatedPasswordStatus, ApiError | ValidationError>> { 375 - return xrpcValidated("_account.getPasswordStatus", passwordStatusSchema, { 376 - token, 377 - }); 378 - }, 379 - 380 - changePassword( 381 - token: AccessToken, 382 - currentPassword: string, 383 - newPassword: string, 384 - ): Promise<Result<ValidatedSuccessResponse, ApiError | ValidationError>> { 385 - return xrpcValidated("_account.changePassword", successResponseSchema, { 386 - method: "POST", 387 - token, 388 - body: { currentPassword, newPassword }, 389 - }); 390 - }, 391 - 392 - getLegacyLoginPreference( 393 - token: AccessToken, 394 - ): Promise< 395 - Result<ValidatedLegacyLoginPreference, ApiError | ValidationError> 396 - > { 397 - return xrpcValidated( 398 - "_account.getLegacyLoginPreference", 399 - legacyLoginPreferenceSchema, 400 - { token }, 401 - ); 402 - }, 403 - 404 - getAccountInfo( 405 - token: AccessToken, 406 - did: Did, 407 - ): Promise<Result<ValidatedAccountInfo, ApiError | ValidationError>> { 408 - return xrpcValidated( 409 - "com.atproto.admin.getAccountInfo", 410 - accountInfoSchema, 411 - { 412 - token, 413 - params: { did }, 414 - }, 415 - ); 416 - }, 417 - 418 - searchAccounts( 419 - token: AccessToken, 420 - options?: { handle?: string; cursor?: string; limit?: number }, 421 - ): Promise< 422 - Result<ValidatedSearchAccountsResponse, ApiError | ValidationError> 423 - > { 424 - const params: Record<string, string> = {}; 425 - if (options?.handle) params.handle = options.handle; 426 - if (options?.cursor) params.cursor = options.cursor; 427 - if (options?.limit) params.limit = String(options.limit); 428 - return xrpcValidated( 429 - "com.atproto.admin.searchAccounts", 430 - searchAccountsResponseSchema, 431 - { 432 - token, 433 - params, 434 - }, 435 - ); 436 - }, 437 - 438 - }; 439 - 440 - export { ValidationError };
-39
frontend/src/lib/types/api.ts
··· 88 88 89 89 export type Session = SessionBase & ContactState & AccountState; 90 90 91 - export function hasEmail( 92 - session: Session, 93 - ): session is Session & { email: EmailAddress } { 94 - return session.contactKind === "email" || 95 - (session.contactKind === "channel" && session.email !== undefined); 96 - } 97 - 98 91 export function getSessionEmail(session: Session): EmailAddress | undefined { 99 92 return session.contactKind === "email" 100 93 ? session.email ··· 103 96 : undefined; 104 97 } 105 98 106 - export function isEmailVerified(session: Session): boolean { 107 - return session.contactKind === "email" 108 - ? session.emailConfirmed 109 - : session.contactKind === "channel" 110 - ? session.preferredChannelVerified 111 - : false; 112 - } 113 - 114 - export function isMigrated( 115 - session: Session, 116 - ): session is Session & { accountKind: "migrated" } { 117 - return session.accountKind === "migrated"; 118 - } 119 - 120 - export function isDeactivated(session: Session): boolean { 121 - return session.accountKind === "deactivated"; 122 - } 123 - 124 99 export function isActive(session: Session): boolean { 125 100 return session.accountKind === "active"; 126 101 } ··· 208 183 preferredChannelVerified?: boolean; 209 184 } 210 185 211 - export interface ListAppPasswordsResponse { 212 - passwords: AppPassword[]; 213 - } 214 - 215 - export interface AccountInviteCodesResponse { 216 - codes: InviteCodeInfo[]; 217 - } 218 - 219 - export interface CreateInviteCodeResponse { 220 - code: InviteCodeBrand; 221 - } 222 186 223 187 export interface ServerLinks { 224 188 privacyPolicy?: string; ··· 317 281 sessions: SessionInfo[]; 318 282 } 319 283 320 - export interface RevokeAllSessionsResponse { 321 - revokedCount: number; 322 - } 323 284 324 285 export interface AccountSearchResult { 325 286 did: Did;
-131
frontend/src/lib/types/branded.ts
··· 25 25 export type DidKeyString = Brand<string, "DidKeyString">; 26 26 export type ScopeSet = Brand<string, "ScopeSet">; 27 27 28 - const DID_PLC_REGEX = /^did:plc:[a-z2-7]{24}$/; 29 - const DID_WEB_REGEX = /^did:web:.+$/; 30 - const HANDLE_REGEX = 31 - /^([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])?$/; 32 - const AT_URI_REGEX = /^at:\/\/[^/]+\/[^/]+\/[^/]+$/; 33 - const CID_REGEX = /^[a-z2-7]{59}$|^baf[a-z2-7]+$/; 34 - const NSID_REGEX = 35 - /^[a-z]([a-z0-9-]*[a-z0-9])?(\.[a-z]([a-z0-9-]*[a-z0-9])?)+$/; 36 - const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 37 - const ISO_DATE_REGEX = 38 - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/; 39 - 40 - export function isDid(s: string): s is Did { 41 - return s.startsWith("did:plc:") || s.startsWith("did:web:"); 42 - } 43 - 44 - export function isDidPlc(s: string): s is DidPlc { 45 - return DID_PLC_REGEX.test(s); 46 - } 47 - 48 - export function isDidWeb(s: string): s is DidWeb { 49 - return DID_WEB_REGEX.test(s); 50 - } 51 - 52 - export function isHandle(s: string): s is Handle { 53 - return HANDLE_REGEX.test(s) && s.length <= 253; 54 - } 55 - 56 - export function isAtUri(s: string): s is AtUri { 57 - return AT_URI_REGEX.test(s); 58 - } 59 - 60 - export function isCid(s: string): s is Cid { 61 - return CID_REGEX.test(s); 62 - } 63 - 64 - export function isNsid(s: string): s is Nsid { 65 - return NSID_REGEX.test(s); 66 - } 67 - 68 - export function isEmail(s: string): s is EmailAddress { 69 - return EMAIL_REGEX.test(s); 70 - } 71 - 72 - export function isISODate(s: string): s is ISODateString { 73 - return ISO_DATE_REGEX.test(s); 74 - } 75 - 76 - export function asDid(s: string): Did { 77 - if (!isDid(s)) throw new TypeError(`Invalid DID: ${s}`); 78 - return s; 79 - } 80 - 81 - export function asDidPlc(s: string): DidPlc { 82 - if (!isDidPlc(s)) throw new TypeError(`Invalid DID:PLC: ${s}`); 83 - return s as DidPlc; 84 - } 85 - 86 - export function asDidWeb(s: string): DidWeb { 87 - if (!isDidWeb(s)) throw new TypeError(`Invalid DID:WEB: ${s}`); 88 - return s as DidWeb; 89 - } 90 - 91 - export function asHandle(s: string): Handle { 92 - if (!isHandle(s)) throw new TypeError(`Invalid handle: ${s}`); 93 - return s; 94 - } 95 - 96 - export function asAtUri(s: string): AtUri { 97 - if (!isAtUri(s)) throw new TypeError(`Invalid AT-URI: ${s}`); 98 - return s; 99 - } 100 - 101 - export function asCid(s: string): Cid { 102 - if (!isCid(s)) throw new TypeError(`Invalid CID: ${s}`); 103 - return s; 104 - } 105 - 106 - export function asNsid(s: string): Nsid { 107 - if (!isNsid(s)) throw new TypeError(`Invalid NSID: ${s}`); 108 - return s; 109 - } 110 - 111 - export function asEmail(s: string): EmailAddress { 112 - if (!isEmail(s)) throw new TypeError(`Invalid email: ${s}`); 113 - return s; 114 - } 115 - 116 - export function asISODate(s: string): ISODateString { 117 - if (!isISODate(s)) throw new TypeError(`Invalid ISO date: ${s}`); 118 - return s; 119 - } 120 - 121 28 export function unsafeAsDid(s: string): Did { 122 29 return s as Did; 123 30 } ··· 134 41 return s as RefreshToken; 135 42 } 136 43 137 - export function unsafeAsServiceToken(s: string): ServiceToken { 138 - return s as ServiceToken; 139 - } 140 - 141 - export function unsafeAsSetupToken(s: string): SetupToken { 142 - return s as SetupToken; 143 - } 144 - 145 - export function unsafeAsCid(s: string): Cid { 146 - return s as Cid; 147 - } 148 - 149 44 export function unsafeAsRkey(s: string): Rkey { 150 45 return s as Rkey; 151 46 } 152 47 153 - export function unsafeAsAtUri(s: string): AtUri { 154 - return s as AtUri; 155 - } 156 - 157 48 export function unsafeAsNsid(s: string): Nsid { 158 49 return s as Nsid; 159 50 } ··· 172 63 return s as InviteCode; 173 64 } 174 65 175 - export function unsafeAsPublicKeyMultibase(s: string): PublicKeyMultibase { 176 - return s as PublicKeyMultibase; 177 - } 178 - 179 - export function unsafeAsDidKey(s: string): DidKeyString { 180 - return s as DidKeyString; 181 - } 182 - 183 66 export function unsafeAsScopeSet(s: string): ScopeSet { 184 67 return s as ScopeSet; 185 68 } 186 69 187 - export function parseAtUri( 188 - uri: AtUri, 189 - ): { repo: Did; collection: Nsid; rkey: Rkey } { 190 - const parts = uri.replace("at://", "").split("/"); 191 - return { 192 - repo: unsafeAsDid(parts[0]), 193 - collection: unsafeAsNsid(parts[1]), 194 - rkey: unsafeAsRkey(parts[2]), 195 - }; 196 - } 197 - 198 - export function makeAtUri(repo: Did, collection: Nsid, rkey: Rkey): AtUri { 199 - return `at://${repo}/${collection}/${rkey}` as AtUri; 200 - }
-49
frontend/src/lib/types/exhaustive.ts
··· 1 - export function assertNever(x: never, message?: string): never { 2 - throw new Error(message ?? `Unexpected value: ${JSON.stringify(x)}`); 3 - } 4 - 5 - export function exhaustive<T extends string | number | symbol>( 6 - value: T, 7 - handlers: Record<T, () => void>, 8 - ): void { 9 - const handler = handlers[value]; 10 - if (handler) { 11 - handler(); 12 - } else { 13 - assertNever(value as never, `Unhandled case: ${String(value)}`); 14 - } 15 - } 16 - 17 - export function exhaustiveMap<T extends string | number | symbol, R>( 18 - value: T, 19 - handlers: Record<T, () => R>, 20 - ): R { 21 - const handler = handlers[value]; 22 - if (handler) { 23 - return handler(); 24 - } 25 - return assertNever(value as never, `Unhandled case: ${String(value)}`); 26 - } 27 - 28 - export async function exhaustiveAsync<T extends string | number | symbol>( 29 - value: T, 30 - handlers: Record<T, () => Promise<void>>, 31 - ): Promise<void> { 32 - const handler = handlers[value]; 33 - if (handler) { 34 - await handler(); 35 - } else { 36 - assertNever(value as never, `Unhandled case: ${String(value)}`); 37 - } 38 - } 39 - 40 - export async function exhaustiveMapAsync<T extends string | number | symbol, R>( 41 - value: T, 42 - handlers: Record<T, () => Promise<R>>, 43 - ): Promise<R> { 44 - const handler = handlers[value]; 45 - if (handler) { 46 - return handler(); 47 - } 48 - return assertNever(value as never, `Unhandled case: ${String(value)}`); 49 - }
-5
frontend/src/lib/types/index.ts
··· 1 - export * from "./result.ts"; 2 - export * from "./branded.ts"; 3 - export * from "./exhaustive.ts"; 4 - export * from "./api.ts"; 5 - export * from "./routes.ts";
-85
frontend/src/lib/types/result.ts
··· 22 22 return !result.ok; 23 23 } 24 24 25 - export function map<T, U, E>( 26 - result: Result<T, E>, 27 - fn: (t: T) => U, 28 - ): Result<U, E> { 29 - return result.ok ? ok(fn(result.value)) : result; 30 - } 31 - 32 - export function mapErr<T, E, F>( 33 - result: Result<T, E>, 34 - fn: (e: E) => F, 35 - ): Result<T, F> { 36 - return result.ok ? result : err(fn(result.error)); 37 - } 38 - 39 - export function flatMap<T, U, E>( 40 - result: Result<T, E>, 41 - fn: (t: T) => Result<U, E>, 42 - ): Result<U, E> { 43 - return result.ok ? fn(result.value) : result; 44 - } 45 - 46 - export function unwrap<T, E>(result: Result<T, E>): T { 47 - if (result.ok) return result.value; 48 - throw result.error instanceof Error 49 - ? result.error 50 - : new Error(String(result.error)); 51 - } 52 25 53 - export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T { 54 - return result.ok ? result.value : defaultValue; 55 - } 56 26 57 - export function unwrapOrElse<T, E>(result: Result<T, E>, fn: (e: E) => T): T { 58 - return result.ok ? result.value : fn(result.error); 59 - } 60 - 61 - export function match<T, E, U>( 62 - result: Result<T, E>, 63 - handlers: { ok: (t: T) => U; err: (e: E) => U }, 64 - ): U { 65 - return result.ok ? handlers.ok(result.value) : handlers.err(result.error); 66 - } 67 - 68 - export async function tryAsync<T>( 69 - fn: () => Promise<T>, 70 - ): Promise<Result<T, Error>> { 71 - try { 72 - return ok(await fn()); 73 - } catch (e) { 74 - return err(e instanceof Error ? e : new Error(String(e))); 75 - } 76 - } 77 - 78 - export async function tryAsyncWith<T, E>( 79 - fn: () => Promise<T>, 80 - mapError: (e: unknown) => E, 81 - ): Promise<Result<T, E>> { 82 - try { 83 - return ok(await fn()); 84 - } catch (e) { 85 - return err(mapError(e)); 86 - } 87 - } 88 - 89 - export function fromNullable<T>(value: T | null | undefined): Result<T, null> { 90 - return value != null ? ok(value) : err(null); 91 - } 92 - 93 - export function toNullable<T, E>(result: Result<T, E>): T | null { 94 - return result.ok ? result.value : null; 95 - } 96 - 97 - export function collect<T, E>(results: Result<T, E>[]): Result<T[], E> { 98 - const values: T[] = []; 99 - for (const result of results) { 100 - if (!result.ok) return result; 101 - values.push(result.value); 102 - } 103 - return ok(values); 104 - } 105 - 106 - export async function collectAsync<T, E>( 107 - results: Promise<Result<T, E>>[], 108 - ): Promise<Result<T[], E>> { 109 - const settled = await Promise.all(results); 110 - return collect(settled); 111 - }
-329
frontend/src/lib/types/schemas.ts
··· 1 - import { z } from "zod"; 2 - import { 3 - unsafeAsAccessToken, 4 - unsafeAsAtUri, 5 - unsafeAsCid, 6 - unsafeAsDid, 7 - unsafeAsEmail, 8 - unsafeAsHandle, 9 - unsafeAsInviteCode, 10 - unsafeAsISODate, 11 - unsafeAsNsid, 12 - unsafeAsPublicKeyMultibase, 13 - unsafeAsRefreshToken, 14 - unsafeAsRkey, 15 - } from "./branded.ts"; 16 - 17 - const did = z.string().transform((s) => unsafeAsDid(s)); 18 - const handle = z.string().transform((s) => unsafeAsHandle(s)); 19 - const accessToken = z.string().transform((s) => unsafeAsAccessToken(s)); 20 - const refreshToken = z.string().transform((s) => unsafeAsRefreshToken(s)); 21 - const cid = z.string().transform((s) => unsafeAsCid(s)); 22 - const nsid = z.string().transform((s) => unsafeAsNsid(s)); 23 - const atUri = z.string().transform((s) => unsafeAsAtUri(s)); 24 - const _rkey = z.string().transform((s) => unsafeAsRkey(s)); 25 - const isoDate = z.string().transform((s) => unsafeAsISODate(s)); 26 - const email = z.string().transform((s) => unsafeAsEmail(s)); 27 - const inviteCode = z.string().transform((s) => unsafeAsInviteCode(s)); 28 - const publicKeyMultibase = z.string().transform((s) => 29 - unsafeAsPublicKeyMultibase(s) 30 - ); 31 - 32 - export const verificationChannel = z.enum([ 33 - "email", 34 - "discord", 35 - "telegram", 36 - "signal", 37 - ]); 38 - export const didType = z.enum(["plc", "web", "web-external"]); 39 - export const accountStatus = z.enum([ 40 - "active", 41 - "deactivated", 42 - "migrated", 43 - "suspended", 44 - "deleted", 45 - ]); 46 - export const sessionType = z.enum(["oauth", "legacy", "app_password"]); 47 - export const reauthMethod = z.enum(["password", "totp", "passkey"]); 48 - 49 - export const sessionSchema = z.object({ 50 - did: did, 51 - handle: handle, 52 - email: email.optional(), 53 - emailConfirmed: z.boolean().optional(), 54 - preferredChannel: verificationChannel.optional(), 55 - preferredChannelVerified: z.boolean().optional(), 56 - isAdmin: z.boolean().optional(), 57 - active: z.boolean().optional(), 58 - status: accountStatus.optional(), 59 - migratedToPds: z.string().optional(), 60 - migratedAt: isoDate.optional(), 61 - accessJwt: accessToken, 62 - refreshJwt: refreshToken, 63 - }); 64 - 65 - export const serverLinksSchema = z.object({ 66 - privacyPolicy: z.string().optional(), 67 - termsOfService: z.string().optional(), 68 - }); 69 - 70 - export const serverDescriptionSchema = z.object({ 71 - availableUserDomains: z.array(z.string()), 72 - inviteCodeRequired: z.boolean(), 73 - links: serverLinksSchema.optional(), 74 - version: z.string().optional(), 75 - availableCommsChannels: z.array(verificationChannel).optional(), 76 - selfHostedDidWebEnabled: z.boolean().optional(), 77 - }); 78 - 79 - export const appPasswordSchema = z.object({ 80 - name: z.string(), 81 - createdAt: isoDate, 82 - scopes: z.string().optional(), 83 - createdByController: z.string().optional(), 84 - }); 85 - 86 - export const createdAppPasswordSchema = z.object({ 87 - name: z.string(), 88 - password: z.string(), 89 - createdAt: isoDate, 90 - scopes: z.string().optional(), 91 - }); 92 - 93 - export const inviteCodeUseSchema = z.object({ 94 - usedBy: did, 95 - usedByHandle: handle.optional(), 96 - usedAt: isoDate, 97 - }); 98 - 99 - export const inviteCodeInfoSchema = z.object({ 100 - code: inviteCode, 101 - available: z.number(), 102 - disabled: z.boolean(), 103 - forAccount: did, 104 - createdBy: did, 105 - createdAt: isoDate, 106 - uses: z.array(inviteCodeUseSchema), 107 - }); 108 - 109 - export const sessionInfoSchema = z.object({ 110 - id: z.string(), 111 - sessionType: sessionType, 112 - clientName: z.string().nullable(), 113 - createdAt: isoDate, 114 - expiresAt: isoDate, 115 - isCurrent: z.boolean(), 116 - }); 117 - 118 - export const listSessionsResponseSchema = z.object({ 119 - sessions: z.array(sessionInfoSchema), 120 - }); 121 - 122 - export const totpStatusSchema = z.object({ 123 - enabled: z.boolean(), 124 - hasBackupCodes: z.boolean(), 125 - }); 126 - 127 - export const totpSecretSchema = z.object({ 128 - uri: z.string(), 129 - qrBase64: z.string(), 130 - }); 131 - 132 - export const enableTotpResponseSchema = z.object({ 133 - success: z.boolean(), 134 - backupCodes: z.array(z.string()), 135 - }); 136 - 137 - export const passkeyInfoSchema = z.object({ 138 - id: z.string(), 139 - credentialId: z.string(), 140 - friendlyName: z.string().nullable(), 141 - createdAt: isoDate, 142 - lastUsed: isoDate.nullable(), 143 - }); 144 - 145 - export const listPasskeysResponseSchema = z.object({ 146 - passkeys: z.array(passkeyInfoSchema), 147 - }); 148 - 149 - export const trustedDeviceSchema = z.object({ 150 - id: z.string(), 151 - userAgent: z.string().nullable(), 152 - friendlyName: z.string().nullable(), 153 - trustedAt: isoDate.nullable(), 154 - trustedUntil: isoDate.nullable(), 155 - lastSeenAt: isoDate, 156 - }); 157 - 158 - export const listTrustedDevicesResponseSchema = z.object({ 159 - devices: z.array(trustedDeviceSchema), 160 - }); 161 - 162 - export const reauthStatusSchema = z.object({ 163 - requiresReauth: z.boolean(), 164 - lastReauthAt: isoDate.nullable(), 165 - availableMethods: z.array(reauthMethod), 166 - }); 167 - 168 - export const reauthResponseSchema = z.object({ 169 - success: z.boolean(), 170 - reauthAt: isoDate, 171 - }); 172 - 173 - export const notificationPrefsSchema = z.object({ 174 - preferredChannel: verificationChannel, 175 - email: email, 176 - discordUsername: z.string().nullable(), 177 - discordVerified: z.boolean(), 178 - telegramUsername: z.string().nullable(), 179 - telegramVerified: z.boolean(), 180 - signalUsername: z.string().nullable(), 181 - signalVerified: z.boolean(), 182 - }); 183 - 184 - export const verificationMethodSchema = z.object({ 185 - id: z.string(), 186 - type: z.string(), 187 - controller: z.string(), 188 - publicKeyMultibase: publicKeyMultibase, 189 - }); 190 - 191 - export const serviceEndpointSchema = z.object({ 192 - id: z.string(), 193 - type: z.string(), 194 - serviceEndpoint: z.string(), 195 - }); 196 - 197 - export const didDocumentSchema = z.object({ 198 - "@context": z.array(z.string()), 199 - id: did, 200 - alsoKnownAs: z.array(z.string()), 201 - verificationMethod: z.array(verificationMethodSchema), 202 - service: z.array(serviceEndpointSchema), 203 - }); 204 - 205 - export const repoDescriptionSchema = z.object({ 206 - handle: handle, 207 - did: did, 208 - didDoc: didDocumentSchema, 209 - collections: z.array(nsid), 210 - handleIsCorrect: z.boolean(), 211 - }); 212 - 213 - export const recordInfoSchema = z.object({ 214 - uri: atUri, 215 - cid: cid, 216 - value: z.unknown(), 217 - }); 218 - 219 - export const listRecordsResponseSchema = z.object({ 220 - records: z.array(recordInfoSchema), 221 - cursor: z.string().optional(), 222 - }); 223 - 224 - export const recordResponseSchema = z.object({ 225 - uri: atUri, 226 - cid: cid, 227 - value: z.unknown(), 228 - }); 229 - 230 - export const createRecordResponseSchema = z.object({ 231 - uri: atUri, 232 - cid: cid, 233 - }); 234 - 235 - export const serverStatsSchema = z.object({ 236 - userCount: z.number(), 237 - repoCount: z.number(), 238 - recordCount: z.number(), 239 - blobStorageBytes: z.number(), 240 - }); 241 - 242 - export const serverConfigSchema = z.object({ 243 - serverName: z.string(), 244 - primaryColor: z.string().nullable(), 245 - primaryColorDark: z.string().nullable(), 246 - secondaryColor: z.string().nullable(), 247 - secondaryColorDark: z.string().nullable(), 248 - logoCid: cid.nullable(), 249 - }); 250 - 251 - export const passwordStatusSchema = z.object({ 252 - hasPassword: z.boolean(), 253 - }); 254 - 255 - export const successResponseSchema = z.object({ 256 - success: z.boolean(), 257 - }); 258 - 259 - export const legacyLoginPreferenceSchema = z.object({ 260 - allowLegacyLogin: z.boolean(), 261 - hasMfa: z.boolean(), 262 - }); 263 - 264 - export const accountInfoSchema = z.object({ 265 - did: did, 266 - handle: handle, 267 - email: email.optional(), 268 - indexedAt: isoDate, 269 - emailConfirmedAt: isoDate.optional(), 270 - invitesDisabled: z.boolean().optional(), 271 - deactivatedAt: isoDate.optional(), 272 - }); 273 - 274 - export const searchAccountsResponseSchema = z.object({ 275 - cursor: z.string().optional(), 276 - accounts: z.array(accountInfoSchema), 277 - }); 278 - 279 - export type ValidatedSession = z.infer<typeof sessionSchema>; 280 - export type ValidatedServerDescription = z.infer< 281 - typeof serverDescriptionSchema 282 - >; 283 - export type ValidatedAppPassword = z.infer<typeof appPasswordSchema>; 284 - export type ValidatedCreatedAppPassword = z.infer< 285 - typeof createdAppPasswordSchema 286 - >; 287 - export type ValidatedInviteCodeInfo = z.infer<typeof inviteCodeInfoSchema>; 288 - export type ValidatedSessionInfo = z.infer<typeof sessionInfoSchema>; 289 - export type ValidatedListSessionsResponse = z.infer< 290 - typeof listSessionsResponseSchema 291 - >; 292 - export type ValidatedTotpStatus = z.infer<typeof totpStatusSchema>; 293 - export type ValidatedTotpSecret = z.infer<typeof totpSecretSchema>; 294 - export type ValidatedEnableTotpResponse = z.infer< 295 - typeof enableTotpResponseSchema 296 - >; 297 - export type ValidatedPasskeyInfo = z.infer<typeof passkeyInfoSchema>; 298 - export type ValidatedListPasskeysResponse = z.infer< 299 - typeof listPasskeysResponseSchema 300 - >; 301 - export type ValidatedTrustedDevice = z.infer<typeof trustedDeviceSchema>; 302 - export type ValidatedListTrustedDevicesResponse = z.infer< 303 - typeof listTrustedDevicesResponseSchema 304 - >; 305 - export type ValidatedReauthStatus = z.infer<typeof reauthStatusSchema>; 306 - export type ValidatedReauthResponse = z.infer<typeof reauthResponseSchema>; 307 - export type ValidatedNotificationPrefs = z.infer< 308 - typeof notificationPrefsSchema 309 - >; 310 - export type ValidatedDidDocument = z.infer<typeof didDocumentSchema>; 311 - export type ValidatedRepoDescription = z.infer<typeof repoDescriptionSchema>; 312 - export type ValidatedListRecordsResponse = z.infer< 313 - typeof listRecordsResponseSchema 314 - >; 315 - export type ValidatedRecordResponse = z.infer<typeof recordResponseSchema>; 316 - export type ValidatedCreateRecordResponse = z.infer< 317 - typeof createRecordResponseSchema 318 - >; 319 - export type ValidatedServerStats = z.infer<typeof serverStatsSchema>; 320 - export type ValidatedServerConfig = z.infer<typeof serverConfigSchema>; 321 - export type ValidatedPasswordStatus = z.infer<typeof passwordStatusSchema>; 322 - export type ValidatedSuccessResponse = z.infer<typeof successResponseSchema>; 323 - export type ValidatedLegacyLoginPreference = z.infer< 324 - typeof legacyLoginPreferenceSchema 325 - >; 326 - export type ValidatedAccountInfo = z.infer<typeof accountInfoSchema>; 327 - export type ValidatedSearchAccountsResponse = z.infer< 328 - typeof searchAccountsResponseSchema 329 - >;
-12
frontend/src/lib/types/totp-state.ts
··· 61 61 return idleState; 62 62 } 63 63 64 - export function isIdle(state: TotpSetupState): state is TotpIdle { 65 - return state.step === "idle"; 66 - } 67 - 68 - export function isQr(state: TotpSetupState): state is TotpQr { 69 - return state.step === "qr"; 70 - } 71 - 72 - export function isVerify(state: TotpSetupState): state is TotpVerify { 73 - return state.step === "verify"; 74 - } 75 - 76 64 export function isBackup(state: TotpSetupState): state is TotpBackup { 77 65 return state.step === "backup"; 78 66 }

History

3 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
refactor(frontend): delete type system boilerplate
expand 0 comments
pull request successfully merged
1 commit
expand
refactor(frontend): delete type system boilerplate
expand 0 comments
1 commit
expand
refactor(frontend): delete type system boilerplate
expand 0 comments