-1090
Diff
round #2
-440
frontend/src/lib/api-validated.ts
-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
-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
-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
-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
-5
frontend/src/lib/types/index.ts
-85
frontend/src/lib/types/result.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
-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
-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
oyster.cafe
submitted
#2
1 commit
expand
collapse
refactor(frontend): delete type system boilerplate
expand 0 comments
pull request successfully merged
oyster.cafe
submitted
#1
1 commit
expand
collapse
refactor(frontend): delete type system boilerplate
expand 0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
refactor(frontend): delete type system boilerplate