-1
Cargo.toml
-1
Cargo.toml
+1
frontend/deno.json
+1
frontend/deno.json
···
3
3
"dev": "deno run -A npm:vite",
4
4
"build": "deno run -A npm:vite build",
5
5
"preview": "deno run -A npm:vite preview",
6
+
"check": "deno run -A npm:svelte-check --tsconfig ./tsconfig.json",
6
7
"test": "deno run -A npm:vitest",
7
8
"test:run": "deno run -A npm:vitest run",
8
9
"test:watch": "deno run -A npm:vitest watch",
+31
frontend/deno.lock
+31
frontend/deno.lock
···
12
12
"npm:@testing-library/user-event@^14.6.1": "14.6.1_@testing-library+dom@10.4.1",
13
13
"npm:jsdom@^25.0.1": "25.0.1",
14
14
"npm:multiformats@^13.4.2": "13.4.2",
15
+
"npm:svelte-check@*": "4.3.5_svelte@5.46.1__acorn@8.15.0_typescript@5.9.3",
16
+
"npm:svelte-check@^4.3.5": "4.3.5_svelte@5.46.1__acorn@8.15.0_typescript@5.9.3",
15
17
"npm:svelte-i18n@^4.0.1": "4.0.1_svelte@5.46.1__acorn@8.15.0",
16
18
"npm:svelte@^5.46.1": "5.46.1_acorn@8.15.0",
19
+
"npm:typescript@^5.9.3": "5.9.3",
17
20
"npm:vite@*": "7.3.0_picomatch@4.0.3",
18
21
"npm:vite@^7.3.0": "7.3.0_picomatch@4.0.3",
19
22
"npm:vitest@*": "4.0.16_jsdom@25.0.1_vite@7.3.0__picomatch@4.0.3",
···
765
768
"chai@6.2.2": {
766
769
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="
767
770
},
771
+
"chokidar@4.0.3": {
772
+
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
773
+
"dependencies": [
774
+
"readdirp"
775
+
]
776
+
},
768
777
"cli-color@2.0.4": {
769
778
"integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==",
770
779
"dependencies": [
···
1271
1280
"react-is@17.0.2": {
1272
1281
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
1273
1282
},
1283
+
"readdirp@4.1.2": {
1284
+
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="
1285
+
},
1274
1286
"redent@3.0.0": {
1275
1287
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
1276
1288
"dependencies": [
···
1349
1361
"min-indent"
1350
1362
]
1351
1363
},
1364
+
"svelte-check@4.3.5_svelte@5.46.1__acorn@8.15.0_typescript@5.9.3": {
1365
+
"integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==",
1366
+
"dependencies": [
1367
+
"@jridgewell/trace-mapping",
1368
+
"chokidar",
1369
+
"fdir",
1370
+
"picocolors",
1371
+
"sade",
1372
+
"svelte",
1373
+
"typescript"
1374
+
],
1375
+
"bin": true
1376
+
},
1352
1377
"svelte-i18n@4.0.1_svelte@5.46.1__acorn@8.15.0": {
1353
1378
"integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==",
1354
1379
"dependencies": [
···
1443
1468
},
1444
1469
"type@2.7.3": {
1445
1470
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
1471
+
},
1472
+
"typescript@5.9.3": {
1473
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1474
+
"bin": true
1446
1475
},
1447
1476
"unicode-segmenter@0.14.5": {
1448
1477
"integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="
···
1565
1594
"npm:@testing-library/user-event@^14.6.1",
1566
1595
"npm:jsdom@^25.0.1",
1567
1596
"npm:multiformats@^13.4.2",
1597
+
"npm:svelte-check@^4.3.5",
1568
1598
"npm:svelte-i18n@^4.0.1",
1569
1599
"npm:svelte@^5.46.1",
1600
+
"npm:typescript@^5.9.3",
1570
1601
"npm:vite@^7.3.0",
1571
1602
"npm:vitest@^4.0.16",
1572
1603
"npm:zod@^4.3.5"
+2
frontend/package.json
+2
frontend/package.json
+3
-3
frontend/src/App.svelte
+3
-3
frontend/src/App.svelte
···
53
53
initServerConfig()
54
54
initAuth().then(({ oauthLoginCompleted }) => {
55
55
if (oauthLoginCompleted) {
56
-
navigate('/dashboard', true)
56
+
navigate('/dashboard', { replace: true })
57
57
}
58
58
oauthCallbackPending = false
59
59
})
···
64
64
const path = getCurrentPath()
65
65
if (path === '/') {
66
66
if (auth.kind === 'authenticated') {
67
-
navigate('/dashboard', true)
67
+
navigate('/dashboard', { replace: true })
68
68
} else {
69
-
navigate('/login', true)
69
+
navigate('/login', { replace: true })
70
70
}
71
71
}
72
72
})
+1
-1
frontend/src/components/ReauthModal.svelte
+1
-1
frontend/src/components/ReauthModal.svelte
···
106
106
return
107
107
}
108
108
const { options } = await api.reauthPasskeyStart(token)
109
-
const publicKeyOptions = prepareRequestOptions(options as WebAuthnRequestOptionsResponse)
109
+
const publicKeyOptions = prepareRequestOptions(options as unknown as WebAuthnRequestOptionsResponse)
110
110
const credential = await navigator.credentials.get({
111
111
publicKey: publicKeyOptions
112
112
})
+1
frontend/src/components/migration/InboundWizard.svelte
+1
frontend/src/components/migration/InboundWizard.svelte
+1
frontend/src/components/migration/OfflineInboundWizard.svelte
+1
frontend/src/components/migration/OfflineInboundWizard.svelte
+293
-176
frontend/src/lib/api-validated.ts
+293
-176
frontend/src/lib/api-validated.ts
···
1
-
import { z } from 'zod'
2
-
import { ok, err, type Result } from './types/result'
3
-
import { ApiError } from './api'
4
-
import type { AccessToken, RefreshToken, Did, Handle, Nsid, Rkey } from './types/branded'
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";
5
11
import {
6
-
sessionSchema,
7
-
serverDescriptionSchema,
12
+
accountInfoSchema,
8
13
appPasswordSchema,
14
+
createBackupResponseSchema,
9
15
createdAppPasswordSchema,
10
-
listSessionsResponseSchema,
11
-
totpStatusSchema,
12
-
totpSecretSchema,
16
+
createRecordResponseSchema,
17
+
didDocumentSchema,
13
18
enableTotpResponseSchema,
19
+
legacyLoginPreferenceSchema,
20
+
listBackupsResponseSchema,
14
21
listPasskeysResponseSchema,
22
+
listRecordsResponseSchema,
23
+
listSessionsResponseSchema,
15
24
listTrustedDevicesResponseSchema,
25
+
notificationPrefsSchema,
26
+
passwordStatusSchema,
16
27
reauthStatusSchema,
17
-
notificationPrefsSchema,
18
-
didDocumentSchema,
28
+
recordResponseSchema,
19
29
repoDescriptionSchema,
20
-
listRecordsResponseSchema,
21
-
recordResponseSchema,
22
-
createRecordResponseSchema,
30
+
searchAccountsResponseSchema,
31
+
serverConfigSchema,
32
+
serverDescriptionSchema,
23
33
serverStatsSchema,
24
-
serverConfigSchema,
25
-
passwordStatusSchema,
34
+
sessionSchema,
26
35
successResponseSchema,
27
-
legacyLoginPreferenceSchema,
28
-
accountInfoSchema,
29
-
searchAccountsResponseSchema,
30
-
listBackupsResponseSchema,
31
-
createBackupResponseSchema,
32
-
type ValidatedSession,
33
-
type ValidatedServerDescription,
34
-
type ValidatedListSessionsResponse,
35
-
type ValidatedTotpStatus,
36
-
type ValidatedTotpSecret,
36
+
totpSecretSchema,
37
+
totpStatusSchema,
38
+
type ValidatedAccountInfo,
39
+
type ValidatedAppPassword,
40
+
type ValidatedCreateBackupResponse,
41
+
type ValidatedCreatedAppPassword,
42
+
type ValidatedCreateRecordResponse,
43
+
type ValidatedDidDocument,
37
44
type ValidatedEnableTotpResponse,
45
+
type ValidatedLegacyLoginPreference,
46
+
type ValidatedListBackupsResponse,
38
47
type ValidatedListPasskeysResponse,
48
+
type ValidatedListRecordsResponse,
49
+
type ValidatedListSessionsResponse,
39
50
type ValidatedListTrustedDevicesResponse,
40
-
type ValidatedReauthStatus,
41
51
type ValidatedNotificationPrefs,
42
-
type ValidatedDidDocument,
52
+
type ValidatedPasswordStatus,
53
+
type ValidatedReauthStatus,
54
+
type ValidatedRecordResponse,
43
55
type ValidatedRepoDescription,
44
-
type ValidatedListRecordsResponse,
45
-
type ValidatedRecordResponse,
46
-
type ValidatedCreateRecordResponse,
47
-
type ValidatedServerStats,
56
+
type ValidatedSearchAccountsResponse,
48
57
type ValidatedServerConfig,
49
-
type ValidatedPasswordStatus,
58
+
type ValidatedServerDescription,
59
+
type ValidatedServerStats,
60
+
type ValidatedSession,
50
61
type ValidatedSuccessResponse,
51
-
type ValidatedLegacyLoginPreference,
52
-
type ValidatedAccountInfo,
53
-
type ValidatedSearchAccountsResponse,
54
-
type ValidatedListBackupsResponse,
55
-
type ValidatedCreateBackupResponse,
56
-
type ValidatedCreatedAppPassword,
57
-
type ValidatedAppPassword,
58
-
} from './types/schemas'
62
+
type ValidatedTotpSecret,
63
+
type ValidatedTotpStatus,
64
+
} from "./types/schemas.ts";
59
65
60
-
const API_BASE = '/xrpc'
66
+
const API_BASE = "/xrpc";
61
67
62
68
interface XrpcOptions {
63
-
method?: 'GET' | 'POST'
64
-
params?: Record<string, string>
65
-
body?: unknown
66
-
token?: string
69
+
method?: "GET" | "POST";
70
+
params?: Record<string, string>;
71
+
body?: unknown;
72
+
token?: string;
67
73
}
68
74
69
75
class ValidationError extends Error {
70
76
constructor(
71
77
public issues: z.ZodIssue[],
72
-
message: string = 'API response validation failed'
78
+
message: string = "API response validation failed",
73
79
) {
74
-
super(message)
75
-
this.name = 'ValidationError'
80
+
super(message);
81
+
this.name = "ValidationError";
76
82
}
77
83
}
78
84
79
85
async function xrpcValidated<T>(
80
86
method: string,
81
87
schema: z.ZodType<T>,
82
-
options?: XrpcOptions
88
+
options?: XrpcOptions,
83
89
): Promise<Result<T, ApiError | ValidationError>> {
84
-
const { method: httpMethod = 'GET', params, body, token } = options ?? {}
85
-
let url = `${API_BASE}/${method}`
90
+
const { method: httpMethod = "GET", params, body, token } = options ?? {};
91
+
let url = `${API_BASE}/${method}`;
86
92
if (params) {
87
-
const searchParams = new URLSearchParams(params)
88
-
url += `?${searchParams}`
93
+
const searchParams = new URLSearchParams(params);
94
+
url += `?${searchParams}`;
89
95
}
90
-
const headers: Record<string, string> = {}
96
+
const headers: Record<string, string> = {};
91
97
if (token) {
92
-
headers['Authorization'] = `Bearer ${token}`
98
+
headers["Authorization"] = `Bearer ${token}`;
93
99
}
94
100
if (body) {
95
-
headers['Content-Type'] = 'application/json'
101
+
headers["Content-Type"] = "application/json";
96
102
}
97
103
98
104
try {
···
100
106
method: httpMethod,
101
107
headers,
102
108
body: body ? JSON.stringify(body) : undefined,
103
-
})
109
+
});
104
110
105
111
if (!res.ok) {
106
112
const errData = await res.json().catch(() => ({
107
-
error: 'Unknown',
113
+
error: "Unknown",
108
114
message: res.statusText,
109
-
}))
110
-
return err(new ApiError(res.status, errData.error, errData.message))
115
+
}));
116
+
return err(new ApiError(res.status, errData.error, errData.message));
111
117
}
112
118
113
-
const data = await res.json()
114
-
const parsed = schema.safeParse(data)
119
+
const data = await res.json();
120
+
const parsed = schema.safeParse(data);
115
121
116
122
if (!parsed.success) {
117
-
return err(new ValidationError(parsed.error.issues))
123
+
return err(new ValidationError(parsed.error.issues));
118
124
}
119
125
120
-
return ok(parsed.data)
126
+
return ok(parsed.data);
121
127
} catch (e) {
122
128
if (e instanceof ApiError || e instanceof ValidationError) {
123
-
return err(e)
129
+
return err(e);
124
130
}
125
-
return err(new ApiError(0, 'Unknown', e instanceof Error ? e.message : String(e)))
131
+
return err(
132
+
new ApiError(0, "Unknown", e instanceof Error ? e.message : String(e)),
133
+
);
126
134
}
127
135
}
128
136
129
137
export const validatedApi = {
130
-
getSession(token: AccessToken): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
131
-
return xrpcValidated('com.atproto.server.getSession', sessionSchema, { token })
138
+
getSession(
139
+
token: AccessToken,
140
+
): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
141
+
return xrpcValidated("com.atproto.server.getSession", sessionSchema, {
142
+
token,
143
+
});
132
144
},
133
145
134
-
refreshSession(refreshJwt: RefreshToken): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
135
-
return xrpcValidated('com.atproto.server.refreshSession', sessionSchema, {
136
-
method: 'POST',
146
+
refreshSession(
147
+
refreshJwt: RefreshToken,
148
+
): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
149
+
return xrpcValidated("com.atproto.server.refreshSession", sessionSchema, {
150
+
method: "POST",
137
151
token: refreshJwt,
138
-
})
152
+
});
139
153
},
140
154
141
155
createSession(
142
156
identifier: string,
143
-
password: string
157
+
password: string,
144
158
): Promise<Result<ValidatedSession, ApiError | ValidationError>> {
145
-
return xrpcValidated('com.atproto.server.createSession', sessionSchema, {
146
-
method: 'POST',
159
+
return xrpcValidated("com.atproto.server.createSession", sessionSchema, {
160
+
method: "POST",
147
161
body: { identifier, password },
148
-
})
162
+
});
149
163
},
150
164
151
-
describeServer(): Promise<Result<ValidatedServerDescription, ApiError | ValidationError>> {
152
-
return xrpcValidated('com.atproto.server.describeServer', serverDescriptionSchema)
165
+
describeServer(): Promise<
166
+
Result<ValidatedServerDescription, ApiError | ValidationError>
167
+
> {
168
+
return xrpcValidated(
169
+
"com.atproto.server.describeServer",
170
+
serverDescriptionSchema,
171
+
);
153
172
},
154
173
155
174
listAppPasswords(
156
-
token: AccessToken
157
-
): Promise<Result<{ passwords: ValidatedAppPassword[] }, ApiError | ValidationError>> {
175
+
token: AccessToken,
176
+
): Promise<
177
+
Result<{ passwords: ValidatedAppPassword[] }, ApiError | ValidationError>
178
+
> {
158
179
return xrpcValidated(
159
-
'com.atproto.server.listAppPasswords',
180
+
"com.atproto.server.listAppPasswords",
160
181
z.object({ passwords: z.array(appPasswordSchema) }),
161
-
{ token }
162
-
)
182
+
{ token },
183
+
);
163
184
},
164
185
165
186
createAppPassword(
166
187
token: AccessToken,
167
188
name: string,
168
-
scopes?: string
189
+
scopes?: string,
169
190
): Promise<Result<ValidatedCreatedAppPassword, ApiError | ValidationError>> {
170
-
return xrpcValidated('com.atproto.server.createAppPassword', createdAppPasswordSchema, {
171
-
method: 'POST',
172
-
token,
173
-
body: { name, scopes },
174
-
})
191
+
return xrpcValidated(
192
+
"com.atproto.server.createAppPassword",
193
+
createdAppPasswordSchema,
194
+
{
195
+
method: "POST",
196
+
token,
197
+
body: { name, scopes },
198
+
},
199
+
);
175
200
},
176
201
177
-
listSessions(token: AccessToken): Promise<Result<ValidatedListSessionsResponse, ApiError | ValidationError>> {
178
-
return xrpcValidated('_account.listSessions', listSessionsResponseSchema, { token })
202
+
listSessions(
203
+
token: AccessToken,
204
+
): Promise<
205
+
Result<ValidatedListSessionsResponse, ApiError | ValidationError>
206
+
> {
207
+
return xrpcValidated("_account.listSessions", listSessionsResponseSchema, {
208
+
token,
209
+
});
179
210
},
180
211
181
-
getTotpStatus(token: AccessToken): Promise<Result<ValidatedTotpStatus, ApiError | ValidationError>> {
182
-
return xrpcValidated('com.atproto.server.getTotpStatus', totpStatusSchema, { token })
212
+
getTotpStatus(
213
+
token: AccessToken,
214
+
): Promise<Result<ValidatedTotpStatus, ApiError | ValidationError>> {
215
+
return xrpcValidated("com.atproto.server.getTotpStatus", totpStatusSchema, {
216
+
token,
217
+
});
183
218
},
184
219
185
-
createTotpSecret(token: AccessToken): Promise<Result<ValidatedTotpSecret, ApiError | ValidationError>> {
186
-
return xrpcValidated('com.atproto.server.createTotpSecret', totpSecretSchema, {
187
-
method: 'POST',
188
-
token,
189
-
})
220
+
createTotpSecret(
221
+
token: AccessToken,
222
+
): Promise<Result<ValidatedTotpSecret, ApiError | ValidationError>> {
223
+
return xrpcValidated(
224
+
"com.atproto.server.createTotpSecret",
225
+
totpSecretSchema,
226
+
{
227
+
method: "POST",
228
+
token,
229
+
},
230
+
);
190
231
},
191
232
192
233
enableTotp(
193
234
token: AccessToken,
194
-
code: string
235
+
code: string,
195
236
): Promise<Result<ValidatedEnableTotpResponse, ApiError | ValidationError>> {
196
-
return xrpcValidated('com.atproto.server.enableTotp', enableTotpResponseSchema, {
197
-
method: 'POST',
198
-
token,
199
-
body: { code },
200
-
})
237
+
return xrpcValidated(
238
+
"com.atproto.server.enableTotp",
239
+
enableTotpResponseSchema,
240
+
{
241
+
method: "POST",
242
+
token,
243
+
body: { code },
244
+
},
245
+
);
201
246
},
202
247
203
-
listPasskeys(token: AccessToken): Promise<Result<ValidatedListPasskeysResponse, ApiError | ValidationError>> {
204
-
return xrpcValidated('com.atproto.server.listPasskeys', listPasskeysResponseSchema, { token })
248
+
listPasskeys(
249
+
token: AccessToken,
250
+
): Promise<
251
+
Result<ValidatedListPasskeysResponse, ApiError | ValidationError>
252
+
> {
253
+
return xrpcValidated(
254
+
"com.atproto.server.listPasskeys",
255
+
listPasskeysResponseSchema,
256
+
{ token },
257
+
);
205
258
},
206
259
207
260
listTrustedDevices(
208
-
token: AccessToken
209
-
): Promise<Result<ValidatedListTrustedDevicesResponse, ApiError | ValidationError>> {
210
-
return xrpcValidated('_account.listTrustedDevices', listTrustedDevicesResponseSchema, { token })
261
+
token: AccessToken,
262
+
): Promise<
263
+
Result<ValidatedListTrustedDevicesResponse, ApiError | ValidationError>
264
+
> {
265
+
return xrpcValidated(
266
+
"_account.listTrustedDevices",
267
+
listTrustedDevicesResponseSchema,
268
+
{ token },
269
+
);
211
270
},
212
271
213
-
getReauthStatus(token: AccessToken): Promise<Result<ValidatedReauthStatus, ApiError | ValidationError>> {
214
-
return xrpcValidated('_account.getReauthStatus', reauthStatusSchema, { token })
272
+
getReauthStatus(
273
+
token: AccessToken,
274
+
): Promise<Result<ValidatedReauthStatus, ApiError | ValidationError>> {
275
+
return xrpcValidated("_account.getReauthStatus", reauthStatusSchema, {
276
+
token,
277
+
});
215
278
},
216
279
217
280
getNotificationPrefs(
218
-
token: AccessToken
281
+
token: AccessToken,
219
282
): Promise<Result<ValidatedNotificationPrefs, ApiError | ValidationError>> {
220
-
return xrpcValidated('_account.getNotificationPrefs', notificationPrefsSchema, { token })
283
+
return xrpcValidated(
284
+
"_account.getNotificationPrefs",
285
+
notificationPrefsSchema,
286
+
{ token },
287
+
);
221
288
},
222
289
223
-
getDidDocument(token: AccessToken): Promise<Result<ValidatedDidDocument, ApiError | ValidationError>> {
224
-
return xrpcValidated('_account.getDidDocument', didDocumentSchema, { token })
290
+
getDidDocument(
291
+
token: AccessToken,
292
+
): Promise<Result<ValidatedDidDocument, ApiError | ValidationError>> {
293
+
return xrpcValidated("_account.getDidDocument", didDocumentSchema, {
294
+
token,
295
+
});
225
296
},
226
297
227
298
describeRepo(
228
299
token: AccessToken,
229
-
repo: Did
300
+
repo: Did,
230
301
): Promise<Result<ValidatedRepoDescription, ApiError | ValidationError>> {
231
-
return xrpcValidated('com.atproto.repo.describeRepo', repoDescriptionSchema, {
232
-
token,
233
-
params: { repo },
234
-
})
302
+
return xrpcValidated(
303
+
"com.atproto.repo.describeRepo",
304
+
repoDescriptionSchema,
305
+
{
306
+
token,
307
+
params: { repo },
308
+
},
309
+
);
235
310
},
236
311
237
312
listRecords(
238
313
token: AccessToken,
239
314
repo: Did,
240
315
collection: Nsid,
241
-
options?: { limit?: number; cursor?: string; reverse?: boolean }
316
+
options?: { limit?: number; cursor?: string; reverse?: boolean },
242
317
): Promise<Result<ValidatedListRecordsResponse, ApiError | ValidationError>> {
243
-
const params: Record<string, string> = { repo, collection }
244
-
if (options?.limit) params.limit = String(options.limit)
245
-
if (options?.cursor) params.cursor = options.cursor
246
-
if (options?.reverse) params.reverse = 'true'
247
-
return xrpcValidated('com.atproto.repo.listRecords', listRecordsResponseSchema, {
248
-
token,
249
-
params,
250
-
})
318
+
const params: Record<string, string> = { repo, collection };
319
+
if (options?.limit) params.limit = String(options.limit);
320
+
if (options?.cursor) params.cursor = options.cursor;
321
+
if (options?.reverse) params.reverse = "true";
322
+
return xrpcValidated(
323
+
"com.atproto.repo.listRecords",
324
+
listRecordsResponseSchema,
325
+
{
326
+
token,
327
+
params,
328
+
},
329
+
);
251
330
},
252
331
253
332
getRecord(
254
333
token: AccessToken,
255
334
repo: Did,
256
335
collection: Nsid,
257
-
rkey: Rkey
336
+
rkey: Rkey,
258
337
): Promise<Result<ValidatedRecordResponse, ApiError | ValidationError>> {
259
-
return xrpcValidated('com.atproto.repo.getRecord', recordResponseSchema, {
338
+
return xrpcValidated("com.atproto.repo.getRecord", recordResponseSchema, {
260
339
token,
261
340
params: { repo, collection, rkey },
262
-
})
341
+
});
263
342
},
264
343
265
344
createRecord(
···
267
346
repo: Did,
268
347
collection: Nsid,
269
348
record: unknown,
270
-
rkey?: Rkey
271
-
): Promise<Result<ValidatedCreateRecordResponse, ApiError | ValidationError>> {
272
-
return xrpcValidated('com.atproto.repo.createRecord', createRecordResponseSchema, {
273
-
method: 'POST',
274
-
token,
275
-
body: { repo, collection, record, rkey },
276
-
})
349
+
rkey?: Rkey,
350
+
): Promise<
351
+
Result<ValidatedCreateRecordResponse, ApiError | ValidationError>
352
+
> {
353
+
return xrpcValidated(
354
+
"com.atproto.repo.createRecord",
355
+
createRecordResponseSchema,
356
+
{
357
+
method: "POST",
358
+
token,
359
+
body: { repo, collection, record, rkey },
360
+
},
361
+
);
277
362
},
278
363
279
-
getServerStats(token: AccessToken): Promise<Result<ValidatedServerStats, ApiError | ValidationError>> {
280
-
return xrpcValidated('_admin.getServerStats', serverStatsSchema, { token })
364
+
getServerStats(
365
+
token: AccessToken,
366
+
): Promise<Result<ValidatedServerStats, ApiError | ValidationError>> {
367
+
return xrpcValidated("_admin.getServerStats", serverStatsSchema, { token });
281
368
},
282
369
283
-
getServerConfig(): Promise<Result<ValidatedServerConfig, ApiError | ValidationError>> {
284
-
return xrpcValidated('_server.getConfig', serverConfigSchema)
370
+
getServerConfig(): Promise<
371
+
Result<ValidatedServerConfig, ApiError | ValidationError>
372
+
> {
373
+
return xrpcValidated("_server.getConfig", serverConfigSchema);
285
374
},
286
375
287
-
getPasswordStatus(token: AccessToken): Promise<Result<ValidatedPasswordStatus, ApiError | ValidationError>> {
288
-
return xrpcValidated('_account.getPasswordStatus', passwordStatusSchema, { token })
376
+
getPasswordStatus(
377
+
token: AccessToken,
378
+
): Promise<Result<ValidatedPasswordStatus, ApiError | ValidationError>> {
379
+
return xrpcValidated("_account.getPasswordStatus", passwordStatusSchema, {
380
+
token,
381
+
});
289
382
},
290
383
291
384
changePassword(
292
385
token: AccessToken,
293
386
currentPassword: string,
294
-
newPassword: string
387
+
newPassword: string,
295
388
): Promise<Result<ValidatedSuccessResponse, ApiError | ValidationError>> {
296
-
return xrpcValidated('_account.changePassword', successResponseSchema, {
297
-
method: 'POST',
389
+
return xrpcValidated("_account.changePassword", successResponseSchema, {
390
+
method: "POST",
298
391
token,
299
392
body: { currentPassword, newPassword },
300
-
})
393
+
});
301
394
},
302
395
303
396
getLegacyLoginPreference(
304
-
token: AccessToken
305
-
): Promise<Result<ValidatedLegacyLoginPreference, ApiError | ValidationError>> {
306
-
return xrpcValidated('_account.getLegacyLoginPreference', legacyLoginPreferenceSchema, { token })
397
+
token: AccessToken,
398
+
): Promise<
399
+
Result<ValidatedLegacyLoginPreference, ApiError | ValidationError>
400
+
> {
401
+
return xrpcValidated(
402
+
"_account.getLegacyLoginPreference",
403
+
legacyLoginPreferenceSchema,
404
+
{ token },
405
+
);
307
406
},
308
407
309
408
getAccountInfo(
310
409
token: AccessToken,
311
-
did: Did
410
+
did: Did,
312
411
): Promise<Result<ValidatedAccountInfo, ApiError | ValidationError>> {
313
-
return xrpcValidated('com.atproto.admin.getAccountInfo', accountInfoSchema, {
314
-
token,
315
-
params: { did },
316
-
})
412
+
return xrpcValidated(
413
+
"com.atproto.admin.getAccountInfo",
414
+
accountInfoSchema,
415
+
{
416
+
token,
417
+
params: { did },
418
+
},
419
+
);
317
420
},
318
421
319
422
searchAccounts(
320
423
token: AccessToken,
321
-
options?: { handle?: string; cursor?: string; limit?: number }
322
-
): Promise<Result<ValidatedSearchAccountsResponse, ApiError | ValidationError>> {
323
-
const params: Record<string, string> = {}
324
-
if (options?.handle) params.handle = options.handle
325
-
if (options?.cursor) params.cursor = options.cursor
326
-
if (options?.limit) params.limit = String(options.limit)
327
-
return xrpcValidated('com.atproto.admin.searchAccounts', searchAccountsResponseSchema, {
328
-
token,
329
-
params,
330
-
})
424
+
options?: { handle?: string; cursor?: string; limit?: number },
425
+
): Promise<
426
+
Result<ValidatedSearchAccountsResponse, ApiError | ValidationError>
427
+
> {
428
+
const params: Record<string, string> = {};
429
+
if (options?.handle) params.handle = options.handle;
430
+
if (options?.cursor) params.cursor = options.cursor;
431
+
if (options?.limit) params.limit = String(options.limit);
432
+
return xrpcValidated(
433
+
"com.atproto.admin.searchAccounts",
434
+
searchAccountsResponseSchema,
435
+
{
436
+
token,
437
+
params,
438
+
},
439
+
);
331
440
},
332
441
333
-
listBackups(token: AccessToken): Promise<Result<ValidatedListBackupsResponse, ApiError | ValidationError>> {
334
-
return xrpcValidated('_backup.listBackups', listBackupsResponseSchema, { token })
442
+
listBackups(
443
+
token: AccessToken,
444
+
): Promise<Result<ValidatedListBackupsResponse, ApiError | ValidationError>> {
445
+
return xrpcValidated("_backup.listBackups", listBackupsResponseSchema, {
446
+
token,
447
+
});
335
448
},
336
449
337
-
createBackup(token: AccessToken): Promise<Result<ValidatedCreateBackupResponse, ApiError | ValidationError>> {
338
-
return xrpcValidated('_backup.createBackup', createBackupResponseSchema, {
339
-
method: 'POST',
450
+
createBackup(
451
+
token: AccessToken,
452
+
): Promise<
453
+
Result<ValidatedCreateBackupResponse, ApiError | ValidationError>
454
+
> {
455
+
return xrpcValidated("_backup.createBackup", createBackupResponseSchema, {
456
+
method: "POST",
340
457
token,
341
-
})
458
+
});
342
459
},
343
-
}
460
+
};
344
461
345
-
export { ValidationError }
462
+
export { ValidationError };
+818
-678
frontend/src/lib/api.ts
+818
-678
frontend/src/lib/api.ts
···
1
-
import { ok, err, type Result } from './types/result'
1
+
import { err, ok, type Result } from "./types/result.ts";
2
2
import type {
3
+
AccessToken,
3
4
Did,
5
+
EmailAddress,
4
6
Handle,
5
-
AccessToken,
7
+
Nsid,
6
8
RefreshToken,
7
-
Cid,
8
9
Rkey,
9
-
AtUri,
10
-
Nsid,
11
-
ISODateString,
12
-
EmailAddress,
13
-
InviteCode as InviteCodeBrand,
14
-
} from './types/branded'
10
+
} from "./types/branded.ts";
15
11
import {
12
+
unsafeAsAccessToken,
16
13
unsafeAsDid,
14
+
unsafeAsEmail,
17
15
unsafeAsHandle,
18
-
unsafeAsAccessToken,
19
-
unsafeAsRefreshToken,
20
-
unsafeAsCid,
21
16
unsafeAsISODate,
22
-
unsafeAsEmail,
23
-
unsafeAsInviteCode,
24
-
} from './types/branded'
17
+
unsafeAsRefreshToken,
18
+
} from "./types/branded.ts";
25
19
import type {
26
-
Session,
27
-
DidDocument,
20
+
AccountInfo,
21
+
ApiErrorCode,
28
22
AppPassword,
23
+
CompletePasskeySetupResponse,
24
+
ConfirmSignupResult,
25
+
CreateAccountParams,
26
+
CreateAccountResult,
27
+
CreateBackupResponse,
29
28
CreatedAppPassword,
30
-
InviteCodeInfo,
31
-
ServerDescription,
32
-
NotificationPrefs,
33
-
NotificationHistoryResponse,
34
-
ServerStats,
35
-
ServerConfig,
36
-
UploadBlobResponse,
37
-
ListSessionsResponse,
38
-
SearchAccountsResponse,
39
-
GetInviteCodesResponse,
40
-
AccountInfo,
41
-
RepoDescription,
42
-
ListRecordsResponse,
43
-
RecordResponse,
44
29
CreateRecordResponse,
45
-
TotpStatus,
46
-
TotpSecret,
30
+
DidDocument,
31
+
DidType,
32
+
EmailUpdateResponse,
47
33
EnableTotpResponse,
48
-
RegenerateBackupCodesResponse,
49
-
ListPasskeysResponse,
50
-
StartPasskeyRegistrationResponse,
51
34
FinishPasskeyRegistrationResponse,
35
+
GetInviteCodesResponse,
36
+
InviteCodeInfo,
37
+
LegacyLoginPreference,
38
+
ListBackupsResponse,
39
+
ListPasskeysResponse,
40
+
ListRecordsResponse,
41
+
ListReposResponse,
42
+
ListSessionsResponse,
52
43
ListTrustedDevicesResponse,
44
+
NotificationHistoryResponse,
45
+
NotificationPrefs,
46
+
PasskeyAccountCreateResponse,
47
+
PasswordStatus,
48
+
ReauthPasskeyStartResponse,
49
+
ReauthResponse,
53
50
ReauthStatus,
54
-
ReauthResponse,
55
-
ReauthPasskeyStartResponse,
51
+
RecommendedDidCredentials,
52
+
RecordResponse,
53
+
RegenerateBackupCodesResponse,
54
+
RepoDescription,
55
+
ResendMigrationVerificationResponse,
56
56
ReserveSigningKeyResponse,
57
-
RecommendedDidCredentials,
58
-
PasskeyAccountCreateResponse,
59
-
CompletePasskeySetupResponse,
60
-
VerifyTokenResponse,
61
-
ListBackupsResponse,
62
-
CreateBackupResponse,
57
+
SearchAccountsResponse,
58
+
ServerConfig,
59
+
ServerDescription,
60
+
ServerStats,
61
+
Session,
63
62
SetBackupEnabledResponse,
64
-
EmailUpdateResponse,
65
-
LegacyLoginPreference,
63
+
StartPasskeyRegistrationResponse,
64
+
SuccessResponse,
65
+
TotpSecret,
66
+
TotpStatus,
66
67
UpdateLegacyLoginResponse,
67
68
UpdateLocaleResponse,
68
-
PasswordStatus,
69
-
SuccessResponse,
70
-
CheckEmailVerifiedResponse,
71
-
VerifyMigrationEmailResponse,
72
-
ResendMigrationVerificationResponse,
73
-
ListReposResponse,
69
+
UploadBlobResponse,
74
70
VerificationChannel,
75
-
DidType,
76
-
ApiErrorCode,
77
-
VerificationMethod as VerificationMethodType,
78
-
CreateAccountParams,
79
-
CreateAccountResult,
80
-
ConfirmSignupResult,
81
-
} from './types/api'
71
+
VerifyMigrationEmailResponse,
72
+
VerifyTokenResponse,
73
+
} from "./types/api.ts";
82
74
83
-
const API_BASE = '/xrpc'
75
+
const API_BASE = "/xrpc";
84
76
85
77
export class ApiError extends Error {
86
-
public did?: Did
87
-
public reauthMethods?: string[]
78
+
public did?: Did;
79
+
public reauthMethods?: string[];
88
80
constructor(
89
81
public status: number,
90
82
public error: ApiErrorCode,
···
92
84
did?: string,
93
85
reauthMethods?: string[],
94
86
) {
95
-
super(message)
96
-
this.name = 'ApiError'
97
-
this.did = did ? unsafeAsDid(did) : undefined
98
-
this.reauthMethods = reauthMethods
87
+
super(message);
88
+
this.name = "ApiError";
89
+
this.did = did ? unsafeAsDid(did) : undefined;
90
+
this.reauthMethods = reauthMethods;
99
91
}
100
92
}
101
93
102
-
let tokenRefreshCallback: (() => Promise<string | null>) | null = null
94
+
let tokenRefreshCallback: (() => Promise<string | null>) | null = null;
103
95
104
96
export function setTokenRefreshCallback(
105
97
callback: () => Promise<string | null>,
106
98
) {
107
-
tokenRefreshCallback = callback
99
+
tokenRefreshCallback = callback;
108
100
}
109
101
110
102
interface XrpcOptions {
111
-
method?: 'GET' | 'POST'
112
-
params?: Record<string, string>
113
-
body?: unknown
114
-
token?: string
115
-
skipRetry?: boolean
103
+
method?: "GET" | "POST";
104
+
params?: Record<string, string>;
105
+
body?: unknown;
106
+
token?: string;
107
+
skipRetry?: boolean;
116
108
}
117
109
118
110
async function xrpc<T>(method: string, options?: XrpcOptions): Promise<T> {
119
-
const { method: httpMethod = 'GET', params, body, token, skipRetry } =
120
-
options ?? {}
121
-
let url = `${API_BASE}/${method}`
111
+
const { method: httpMethod = "GET", params, body, token, skipRetry } =
112
+
options ?? {};
113
+
let url = `${API_BASE}/${method}`;
122
114
if (params) {
123
-
const searchParams = new URLSearchParams(params)
124
-
url += `?${searchParams}`
115
+
const searchParams = new URLSearchParams(params);
116
+
url += `?${searchParams}`;
125
117
}
126
-
const headers: Record<string, string> = {}
118
+
const headers: Record<string, string> = {};
127
119
if (token) {
128
-
headers['Authorization'] = `Bearer ${token}`
120
+
headers["Authorization"] = `Bearer ${token}`;
129
121
}
130
122
if (body) {
131
-
headers['Content-Type'] = 'application/json'
123
+
headers["Content-Type"] = "application/json";
132
124
}
133
125
const res = await fetch(url, {
134
126
method: httpMethod,
135
127
headers,
136
128
body: body ? JSON.stringify(body) : undefined,
137
-
})
129
+
});
138
130
if (!res.ok) {
139
131
const errData = await res.json().catch(() => ({
140
-
error: 'Unknown',
132
+
error: "Unknown",
141
133
message: res.statusText,
142
-
}))
134
+
}));
143
135
if (
144
136
res.status === 401 &&
145
-
(errData.error === 'AuthenticationFailed' || errData.error === 'ExpiredToken') &&
137
+
(errData.error === "AuthenticationFailed" ||
138
+
errData.error === "ExpiredToken") &&
146
139
token && tokenRefreshCallback && !skipRetry
147
140
) {
148
-
const newToken = await tokenRefreshCallback()
141
+
const newToken = await tokenRefreshCallback();
149
142
if (newToken && newToken !== token) {
150
-
return xrpc(method, { ...options, token: newToken, skipRetry: true })
143
+
return xrpc(method, { ...options, token: newToken, skipRetry: true });
151
144
}
152
145
}
153
146
throw new ApiError(
···
156
149
errData.message,
157
150
errData.did,
158
151
errData.reauthMethods,
159
-
)
152
+
);
160
153
}
161
-
return res.json()
154
+
return res.json();
162
155
}
163
156
164
157
async function xrpcResult<T>(
165
158
method: string,
166
-
options?: XrpcOptions
159
+
options?: XrpcOptions,
167
160
): Promise<Result<T, ApiError>> {
168
161
try {
169
-
const value = await xrpc<T>(method, options)
170
-
return ok(value)
162
+
const value = await xrpc<T>(method, options);
163
+
return ok(value);
171
164
} catch (e) {
172
165
if (e instanceof ApiError) {
173
-
return err(e)
166
+
return err(e);
174
167
}
175
-
return err(new ApiError(0, 'Unknown', e instanceof Error ? e.message : String(e)))
168
+
return err(
169
+
new ApiError(0, "Unknown", e instanceof Error ? e.message : String(e)),
170
+
);
176
171
}
177
172
}
178
173
179
174
export interface VerificationMethod {
180
-
id: string
181
-
type: string
182
-
publicKeyMultibase: string
175
+
id: string;
176
+
type: string;
177
+
publicKeyMultibase: string;
183
178
}
184
179
185
-
export type { Session, DidDocument, AppPassword, InviteCodeInfo as InviteCode }
186
-
export type { VerificationChannel, DidType, CreateAccountParams, CreateAccountResult, ConfirmSignupResult }
180
+
export type { AppPassword, DidDocument, InviteCodeInfo as InviteCode, Session };
181
+
export type {
182
+
ConfirmSignupResult,
183
+
CreateAccountParams,
184
+
CreateAccountResult,
185
+
DidType,
186
+
VerificationChannel,
187
+
};
187
188
188
189
function castSession(raw: unknown): Session {
189
-
const s = raw as Record<string, unknown>
190
+
const s = raw as Record<string, unknown>;
190
191
return {
191
192
did: unsafeAsDid(s.did as string),
192
193
handle: unsafeAsHandle(s.handle as string),
···
196
197
preferredChannelVerified: s.preferredChannelVerified as boolean | undefined,
197
198
isAdmin: s.isAdmin as boolean | undefined,
198
199
active: s.active as boolean | undefined,
199
-
status: s.status as Session['status'],
200
+
status: s.status as Session["status"],
200
201
migratedToPds: s.migratedToPds as string | undefined,
201
-
migratedAt: s.migratedAt ? unsafeAsISODate(s.migratedAt as string) : undefined,
202
+
migratedAt: s.migratedAt
203
+
? unsafeAsISODate(s.migratedAt as string)
204
+
: undefined,
202
205
accessJwt: unsafeAsAccessToken(s.accessJwt as string),
203
206
refreshJwt: unsafeAsRefreshToken(s.refreshJwt as string),
204
-
}
207
+
};
205
208
}
206
209
207
210
export const api = {
···
209
212
params: CreateAccountParams,
210
213
byodToken?: string,
211
214
): Promise<CreateAccountResult> {
212
-
const url = `${API_BASE}/com.atproto.server.createAccount`
215
+
const url = `${API_BASE}/com.atproto.server.createAccount`;
213
216
const headers: Record<string, string> = {
214
-
'Content-Type': 'application/json',
215
-
}
217
+
"Content-Type": "application/json",
218
+
};
216
219
if (byodToken) {
217
-
headers['Authorization'] = `Bearer ${byodToken}`
220
+
headers["Authorization"] = `Bearer ${byodToken}`;
218
221
}
219
222
const response = await fetch(url, {
220
-
method: 'POST',
223
+
method: "POST",
221
224
headers,
222
225
body: JSON.stringify({
223
226
handle: params.handle,
···
232
235
telegramUsername: params.telegramUsername,
233
236
signalNumber: params.signalNumber,
234
237
}),
235
-
})
236
-
const data = await response.json()
238
+
});
239
+
const data = await response.json();
237
240
if (!response.ok) {
238
-
throw new ApiError(response.status, data.error, data.message)
241
+
throw new ApiError(response.status, data.error, data.message);
239
242
}
240
-
return data
243
+
return data;
241
244
},
242
245
243
246
async createAccountWithServiceAuth(
244
247
serviceAuthToken: string,
245
248
params: {
246
-
did: Did
247
-
handle: Handle
248
-
email: EmailAddress
249
-
password: string
250
-
inviteCode?: string
249
+
did: Did;
250
+
handle: Handle;
251
+
email: EmailAddress;
252
+
password: string;
253
+
inviteCode?: string;
251
254
},
252
255
): Promise<Session> {
253
-
const url = `${API_BASE}/com.atproto.server.createAccount`
256
+
const url = `${API_BASE}/com.atproto.server.createAccount`;
254
257
const response = await fetch(url, {
255
-
method: 'POST',
258
+
method: "POST",
256
259
headers: {
257
-
'Content-Type': 'application/json',
258
-
'Authorization': `Bearer ${serviceAuthToken}`,
260
+
"Content-Type": "application/json",
261
+
"Authorization": `Bearer ${serviceAuthToken}`,
259
262
},
260
263
body: JSON.stringify({
261
264
did: params.did,
···
264
267
password: params.password,
265
268
inviteCode: params.inviteCode,
266
269
}),
267
-
})
268
-
const data = await response.json()
270
+
});
271
+
const data = await response.json();
269
272
if (!response.ok) {
270
-
throw new ApiError(response.status, data.error, data.message)
273
+
throw new ApiError(response.status, data.error, data.message);
271
274
}
272
-
return castSession(data)
275
+
return castSession(data);
273
276
},
274
277
275
278
confirmSignup(
276
279
did: Did,
277
280
verificationCode: string,
278
281
): Promise<ConfirmSignupResult> {
279
-
return xrpc('com.atproto.server.confirmSignup', {
280
-
method: 'POST',
282
+
return xrpc("com.atproto.server.confirmSignup", {
283
+
method: "POST",
281
284
body: { did, verificationCode },
282
-
})
285
+
});
283
286
},
284
287
285
288
resendVerification(did: Did): Promise<{ success: boolean }> {
286
-
return xrpc('com.atproto.server.resendVerification', {
287
-
method: 'POST',
289
+
return xrpc("com.atproto.server.resendVerification", {
290
+
method: "POST",
288
291
body: { did },
289
-
})
292
+
});
290
293
},
291
294
292
295
async createSession(identifier: string, password: string): Promise<Session> {
293
-
const raw = await xrpc<unknown>('com.atproto.server.createSession', {
294
-
method: 'POST',
296
+
const raw = await xrpc<unknown>("com.atproto.server.createSession", {
297
+
method: "POST",
295
298
body: { identifier, password },
296
-
})
297
-
return castSession(raw)
299
+
});
300
+
return castSession(raw);
298
301
},
299
302
300
303
checkEmailVerified(identifier: string): Promise<{ verified: boolean }> {
301
-
return xrpc('_checkEmailVerified', {
302
-
method: 'POST',
304
+
return xrpc("_checkEmailVerified", {
305
+
method: "POST",
303
306
body: { identifier },
304
-
})
307
+
});
305
308
},
306
309
307
310
async getSession(token: AccessToken): Promise<Session> {
308
-
const raw = await xrpc<unknown>('com.atproto.server.getSession', { token })
309
-
return castSession(raw)
311
+
const raw = await xrpc<unknown>("com.atproto.server.getSession", { token });
312
+
return castSession(raw);
310
313
},
311
314
312
315
async refreshSession(refreshJwt: RefreshToken): Promise<Session> {
313
-
const raw = await xrpc<unknown>('com.atproto.server.refreshSession', {
314
-
method: 'POST',
316
+
const raw = await xrpc<unknown>("com.atproto.server.refreshSession", {
317
+
method: "POST",
315
318
token: refreshJwt,
316
-
})
317
-
return castSession(raw)
319
+
});
320
+
return castSession(raw);
318
321
},
319
322
320
323
async deleteSession(token: AccessToken): Promise<void> {
321
-
await xrpc('com.atproto.server.deleteSession', {
322
-
method: 'POST',
324
+
await xrpc("com.atproto.server.deleteSession", {
325
+
method: "POST",
323
326
token,
324
-
})
327
+
});
325
328
},
326
329
327
330
listAppPasswords(token: AccessToken): Promise<{ passwords: AppPassword[] }> {
328
-
return xrpc('com.atproto.server.listAppPasswords', { token })
331
+
return xrpc("com.atproto.server.listAppPasswords", { token });
329
332
},
330
333
331
334
createAppPassword(
···
333
336
name: string,
334
337
scopes?: string,
335
338
): Promise<CreatedAppPassword> {
336
-
return xrpc('com.atproto.server.createAppPassword', {
337
-
method: 'POST',
339
+
return xrpc("com.atproto.server.createAppPassword", {
340
+
method: "POST",
338
341
token,
339
342
body: { name, scopes },
340
-
})
343
+
});
341
344
},
342
345
343
346
async revokeAppPassword(token: AccessToken, name: string): Promise<void> {
344
-
await xrpc('com.atproto.server.revokeAppPassword', {
345
-
method: 'POST',
347
+
await xrpc("com.atproto.server.revokeAppPassword", {
348
+
method: "POST",
346
349
token,
347
350
body: { name },
348
-
})
351
+
});
349
352
},
350
353
351
-
getAccountInviteCodes(token: AccessToken): Promise<{ codes: InviteCodeInfo[] }> {
352
-
return xrpc('com.atproto.server.getAccountInviteCodes', { token })
354
+
getAccountInviteCodes(
355
+
token: AccessToken,
356
+
): Promise<{ codes: InviteCodeInfo[] }> {
357
+
return xrpc("com.atproto.server.getAccountInviteCodes", { token });
353
358
},
354
359
355
360
createInviteCode(
356
361
token: AccessToken,
357
362
useCount: number = 1,
358
363
): Promise<{ code: string }> {
359
-
return xrpc('com.atproto.server.createInviteCode', {
360
-
method: 'POST',
364
+
return xrpc("com.atproto.server.createInviteCode", {
365
+
method: "POST",
361
366
token,
362
367
body: { useCount },
363
-
})
368
+
});
364
369
},
365
370
366
371
async requestPasswordReset(email: EmailAddress): Promise<void> {
367
-
await xrpc('com.atproto.server.requestPasswordReset', {
368
-
method: 'POST',
372
+
await xrpc("com.atproto.server.requestPasswordReset", {
373
+
method: "POST",
369
374
body: { email },
370
-
})
375
+
});
371
376
},
372
377
373
378
async resetPassword(token: string, password: string): Promise<void> {
374
-
await xrpc('com.atproto.server.resetPassword', {
375
-
method: 'POST',
379
+
await xrpc("com.atproto.server.resetPassword", {
380
+
method: "POST",
376
381
body: { token, password },
377
-
})
382
+
});
378
383
},
379
384
380
385
requestEmailUpdate(token: AccessToken): Promise<EmailUpdateResponse> {
381
-
return xrpc('com.atproto.server.requestEmailUpdate', {
382
-
method: 'POST',
386
+
return xrpc("com.atproto.server.requestEmailUpdate", {
387
+
method: "POST",
383
388
token,
384
-
})
389
+
});
385
390
},
386
391
387
392
async updateEmail(
···
389
394
email: string,
390
395
emailToken?: string,
391
396
): Promise<void> {
392
-
await xrpc('com.atproto.server.updateEmail', {
393
-
method: 'POST',
397
+
await xrpc("com.atproto.server.updateEmail", {
398
+
method: "POST",
394
399
token,
395
400
body: { email, token: emailToken },
396
-
})
401
+
});
397
402
},
398
403
399
404
async updateHandle(token: AccessToken, handle: Handle): Promise<void> {
400
-
await xrpc('com.atproto.identity.updateHandle', {
401
-
method: 'POST',
405
+
await xrpc("com.atproto.identity.updateHandle", {
406
+
method: "POST",
402
407
token,
403
408
body: { handle },
404
-
})
409
+
});
405
410
},
406
411
407
412
async requestAccountDelete(token: AccessToken): Promise<void> {
408
-
await xrpc('com.atproto.server.requestAccountDelete', {
409
-
method: 'POST',
413
+
await xrpc("com.atproto.server.requestAccountDelete", {
414
+
method: "POST",
410
415
token,
411
-
})
416
+
});
412
417
},
413
418
414
419
async deleteAccount(
···
416
421
password: string,
417
422
deleteToken: string,
418
423
): Promise<void> {
419
-
await xrpc('com.atproto.server.deleteAccount', {
420
-
method: 'POST',
424
+
await xrpc("com.atproto.server.deleteAccount", {
425
+
method: "POST",
421
426
body: { did, password, token: deleteToken },
422
-
})
427
+
});
423
428
},
424
429
425
430
describeServer(): Promise<ServerDescription> {
426
-
return xrpc('com.atproto.server.describeServer')
431
+
return xrpc("com.atproto.server.describeServer");
427
432
},
428
433
429
434
listRepos(limit?: number): Promise<ListReposResponse> {
430
-
const params: Record<string, string> = {}
431
-
if (limit) params.limit = String(limit)
432
-
return xrpc('com.atproto.sync.listRepos', { params })
435
+
const params: Record<string, string> = {};
436
+
if (limit) params.limit = String(limit);
437
+
return xrpc("com.atproto.sync.listRepos", { params });
433
438
},
434
439
435
440
getNotificationPrefs(token: AccessToken): Promise<NotificationPrefs> {
436
-
return xrpc('_account.getNotificationPrefs', { token })
441
+
return xrpc("_account.getNotificationPrefs", { token });
437
442
},
438
443
439
444
updateNotificationPrefs(token: AccessToken, prefs: {
440
-
preferredChannel?: string
441
-
discordId?: string
442
-
telegramUsername?: string
443
-
signalNumber?: string
445
+
preferredChannel?: string;
446
+
discordId?: string;
447
+
telegramUsername?: string;
448
+
signalNumber?: string;
444
449
}): Promise<SuccessResponse> {
445
-
return xrpc('_account.updateNotificationPrefs', {
446
-
method: 'POST',
450
+
return xrpc("_account.updateNotificationPrefs", {
451
+
method: "POST",
447
452
token,
448
453
body: prefs,
449
-
})
454
+
});
450
455
},
451
456
452
457
confirmChannelVerification(
···
455
460
identifier: string,
456
461
code: string,
457
462
): Promise<SuccessResponse> {
458
-
return xrpc('_account.confirmChannelVerification', {
459
-
method: 'POST',
463
+
return xrpc("_account.confirmChannelVerification", {
464
+
method: "POST",
460
465
token,
461
466
body: { channel, identifier, code },
462
-
})
467
+
});
463
468
},
464
469
465
-
getNotificationHistory(token: AccessToken): Promise<NotificationHistoryResponse> {
466
-
return xrpc('_account.getNotificationHistory', { token })
470
+
getNotificationHistory(
471
+
token: AccessToken,
472
+
): Promise<NotificationHistoryResponse> {
473
+
return xrpc("_account.getNotificationHistory", { token });
467
474
},
468
475
469
476
getServerStats(token: AccessToken): Promise<ServerStats> {
470
-
return xrpc('_admin.getServerStats', { token })
477
+
return xrpc("_admin.getServerStats", { token });
471
478
},
472
479
473
480
getServerConfig(): Promise<ServerConfig> {
474
-
return xrpc('_server.getConfig')
481
+
return xrpc("_server.getConfig");
475
482
},
476
483
477
484
updateServerConfig(
478
485
token: AccessToken,
479
486
config: {
480
-
serverName?: string
481
-
primaryColor?: string
482
-
primaryColorDark?: string
483
-
secondaryColor?: string
484
-
secondaryColorDark?: string
485
-
logoCid?: string
487
+
serverName?: string;
488
+
primaryColor?: string;
489
+
primaryColorDark?: string;
490
+
secondaryColor?: string;
491
+
secondaryColorDark?: string;
492
+
logoCid?: string;
486
493
},
487
494
): Promise<SuccessResponse> {
488
-
return xrpc('_admin.updateServerConfig', {
489
-
method: 'POST',
495
+
return xrpc("_admin.updateServerConfig", {
496
+
method: "POST",
490
497
token,
491
498
body: config,
492
-
})
499
+
});
493
500
},
494
501
495
-
async uploadBlob(token: AccessToken, file: File): Promise<UploadBlobResponse> {
496
-
const res = await fetch('/xrpc/com.atproto.repo.uploadBlob', {
497
-
method: 'POST',
502
+
async uploadBlob(
503
+
token: AccessToken,
504
+
file: File,
505
+
): Promise<UploadBlobResponse> {
506
+
const res = await fetch("/xrpc/com.atproto.repo.uploadBlob", {
507
+
method: "POST",
498
508
headers: {
499
-
'Authorization': `Bearer ${token}`,
500
-
'Content-Type': file.type,
509
+
"Authorization": `Bearer ${token}`,
510
+
"Content-Type": file.type,
501
511
},
502
512
body: file,
503
-
})
513
+
});
504
514
if (!res.ok) {
505
515
const errData = await res.json().catch(() => ({
506
-
error: 'Unknown',
516
+
error: "Unknown",
507
517
message: res.statusText,
508
-
}))
509
-
throw new ApiError(res.status, errData.error, errData.message)
518
+
}));
519
+
throw new ApiError(res.status, errData.error, errData.message);
510
520
}
511
-
return res.json()
521
+
return res.json();
512
522
},
513
523
514
524
async changePassword(
···
516
526
currentPassword: string,
517
527
newPassword: string,
518
528
): Promise<void> {
519
-
await xrpc('_account.changePassword', {
520
-
method: 'POST',
529
+
await xrpc("_account.changePassword", {
530
+
method: "POST",
521
531
token,
522
532
body: { currentPassword, newPassword },
523
-
})
533
+
});
524
534
},
525
535
526
536
removePassword(token: AccessToken): Promise<SuccessResponse> {
527
-
return xrpc('_account.removePassword', {
528
-
method: 'POST',
537
+
return xrpc("_account.removePassword", {
538
+
method: "POST",
529
539
token,
530
-
})
540
+
});
531
541
},
532
542
533
543
getPasswordStatus(token: AccessToken): Promise<PasswordStatus> {
534
-
return xrpc('_account.getPasswordStatus', { token })
544
+
return xrpc("_account.getPasswordStatus", { token });
535
545
},
536
546
537
547
getLegacyLoginPreference(token: AccessToken): Promise<LegacyLoginPreference> {
538
-
return xrpc('_account.getLegacyLoginPreference', { token })
548
+
return xrpc("_account.getLegacyLoginPreference", { token });
539
549
},
540
550
541
551
updateLegacyLoginPreference(
542
552
token: AccessToken,
543
553
allowLegacyLogin: boolean,
544
554
): Promise<UpdateLegacyLoginResponse> {
545
-
return xrpc('_account.updateLegacyLoginPreference', {
546
-
method: 'POST',
555
+
return xrpc("_account.updateLegacyLoginPreference", {
556
+
method: "POST",
547
557
token,
548
558
body: { allowLegacyLogin },
549
-
})
559
+
});
550
560
},
551
561
552
-
updateLocale(token: AccessToken, preferredLocale: string): Promise<UpdateLocaleResponse> {
553
-
return xrpc('_account.updateLocale', {
554
-
method: 'POST',
562
+
updateLocale(
563
+
token: AccessToken,
564
+
preferredLocale: string,
565
+
): Promise<UpdateLocaleResponse> {
566
+
return xrpc("_account.updateLocale", {
567
+
method: "POST",
555
568
token,
556
569
body: { preferredLocale },
557
-
})
570
+
});
558
571
},
559
572
560
573
listSessions(token: AccessToken): Promise<ListSessionsResponse> {
561
-
return xrpc('_account.listSessions', { token })
574
+
return xrpc("_account.listSessions", { token });
562
575
},
563
576
564
577
async revokeSession(token: AccessToken, sessionId: string): Promise<void> {
565
-
await xrpc('_account.revokeSession', {
566
-
method: 'POST',
578
+
await xrpc("_account.revokeSession", {
579
+
method: "POST",
567
580
token,
568
581
body: { sessionId },
569
-
})
582
+
});
570
583
},
571
584
572
585
revokeAllSessions(token: AccessToken): Promise<{ revokedCount: number }> {
573
-
return xrpc('_account.revokeAllSessions', {
574
-
method: 'POST',
586
+
return xrpc("_account.revokeAllSessions", {
587
+
method: "POST",
575
588
token,
576
-
})
589
+
});
577
590
},
578
591
579
592
searchAccounts(token: AccessToken, options?: {
580
-
handle?: string
581
-
cursor?: string
582
-
limit?: number
593
+
handle?: string;
594
+
cursor?: string;
595
+
limit?: number;
583
596
}): Promise<SearchAccountsResponse> {
584
-
const params: Record<string, string> = {}
585
-
if (options?.handle) params.handle = options.handle
586
-
if (options?.cursor) params.cursor = options.cursor
587
-
if (options?.limit) params.limit = String(options.limit)
588
-
return xrpc('com.atproto.admin.searchAccounts', { token, params })
597
+
const params: Record<string, string> = {};
598
+
if (options?.handle) params.handle = options.handle;
599
+
if (options?.cursor) params.cursor = options.cursor;
600
+
if (options?.limit) params.limit = String(options.limit);
601
+
return xrpc("com.atproto.admin.searchAccounts", { token, params });
589
602
},
590
603
591
604
getInviteCodes(token: AccessToken, options?: {
592
-
sort?: 'recent' | 'usage'
593
-
cursor?: string
594
-
limit?: number
605
+
sort?: "recent" | "usage";
606
+
cursor?: string;
607
+
limit?: number;
595
608
}): Promise<GetInviteCodesResponse> {
596
-
const params: Record<string, string> = {}
597
-
if (options?.sort) params.sort = options.sort
598
-
if (options?.cursor) params.cursor = options.cursor
599
-
if (options?.limit) params.limit = String(options.limit)
600
-
return xrpc('com.atproto.admin.getInviteCodes', { token, params })
609
+
const params: Record<string, string> = {};
610
+
if (options?.sort) params.sort = options.sort;
611
+
if (options?.cursor) params.cursor = options.cursor;
612
+
if (options?.limit) params.limit = String(options.limit);
613
+
return xrpc("com.atproto.admin.getInviteCodes", { token, params });
601
614
},
602
615
603
616
async disableInviteCodes(
···
605
618
codes?: string[],
606
619
accounts?: string[],
607
620
): Promise<void> {
608
-
await xrpc('com.atproto.admin.disableInviteCodes', {
609
-
method: 'POST',
621
+
await xrpc("com.atproto.admin.disableInviteCodes", {
622
+
method: "POST",
610
623
token,
611
624
body: { codes, accounts },
612
-
})
625
+
});
613
626
},
614
627
615
628
getAccountInfo(token: AccessToken, did: Did): Promise<AccountInfo> {
616
-
return xrpc('com.atproto.admin.getAccountInfo', { token, params: { did } })
629
+
return xrpc("com.atproto.admin.getAccountInfo", { token, params: { did } });
617
630
},
618
631
619
632
async disableAccountInvites(token: AccessToken, account: Did): Promise<void> {
620
-
await xrpc('com.atproto.admin.disableAccountInvites', {
621
-
method: 'POST',
633
+
await xrpc("com.atproto.admin.disableAccountInvites", {
634
+
method: "POST",
622
635
token,
623
636
body: { account },
624
-
})
637
+
});
625
638
},
626
639
627
640
async enableAccountInvites(token: AccessToken, account: Did): Promise<void> {
628
-
await xrpc('com.atproto.admin.enableAccountInvites', {
629
-
method: 'POST',
641
+
await xrpc("com.atproto.admin.enableAccountInvites", {
642
+
method: "POST",
630
643
token,
631
644
body: { account },
632
-
})
645
+
});
633
646
},
634
647
635
648
async adminDeleteAccount(token: AccessToken, did: Did): Promise<void> {
636
-
await xrpc('com.atproto.admin.deleteAccount', {
637
-
method: 'POST',
649
+
await xrpc("com.atproto.admin.deleteAccount", {
650
+
method: "POST",
638
651
token,
639
652
body: { did },
640
-
})
653
+
});
641
654
},
642
655
643
656
describeRepo(token: AccessToken, repo: Did): Promise<RepoDescription> {
644
-
return xrpc('com.atproto.repo.describeRepo', {
657
+
return xrpc("com.atproto.repo.describeRepo", {
645
658
token,
646
659
params: { repo },
647
-
})
660
+
});
648
661
},
649
662
650
663
listRecords(token: AccessToken, repo: Did, collection: Nsid, options?: {
651
-
limit?: number
652
-
cursor?: string
653
-
reverse?: boolean
664
+
limit?: number;
665
+
cursor?: string;
666
+
reverse?: boolean;
654
667
}): Promise<ListRecordsResponse> {
655
-
const params: Record<string, string> = { repo, collection }
656
-
if (options?.limit) params.limit = String(options.limit)
657
-
if (options?.cursor) params.cursor = options.cursor
658
-
if (options?.reverse) params.reverse = 'true'
659
-
return xrpc('com.atproto.repo.listRecords', { token, params })
668
+
const params: Record<string, string> = { repo, collection };
669
+
if (options?.limit) params.limit = String(options.limit);
670
+
if (options?.cursor) params.cursor = options.cursor;
671
+
if (options?.reverse) params.reverse = "true";
672
+
return xrpc("com.atproto.repo.listRecords", { token, params });
660
673
},
661
674
662
675
getRecord(
···
665
678
collection: Nsid,
666
679
rkey: Rkey,
667
680
): Promise<RecordResponse> {
668
-
return xrpc('com.atproto.repo.getRecord', {
681
+
return xrpc("com.atproto.repo.getRecord", {
669
682
token,
670
683
params: { repo, collection, rkey },
671
-
})
684
+
});
672
685
},
673
686
674
687
createRecord(
···
678
691
record: unknown,
679
692
rkey?: Rkey,
680
693
): Promise<CreateRecordResponse> {
681
-
return xrpc('com.atproto.repo.createRecord', {
682
-
method: 'POST',
694
+
return xrpc("com.atproto.repo.createRecord", {
695
+
method: "POST",
683
696
token,
684
697
body: { repo, collection, record, rkey },
685
-
})
698
+
});
686
699
},
687
700
688
701
putRecord(
···
692
705
rkey: Rkey,
693
706
record: unknown,
694
707
): Promise<CreateRecordResponse> {
695
-
return xrpc('com.atproto.repo.putRecord', {
696
-
method: 'POST',
708
+
return xrpc("com.atproto.repo.putRecord", {
709
+
method: "POST",
697
710
token,
698
711
body: { repo, collection, rkey, record },
699
-
})
712
+
});
700
713
},
701
714
702
715
async deleteRecord(
···
705
718
collection: Nsid,
706
719
rkey: Rkey,
707
720
): Promise<void> {
708
-
await xrpc('com.atproto.repo.deleteRecord', {
709
-
method: 'POST',
721
+
await xrpc("com.atproto.repo.deleteRecord", {
722
+
method: "POST",
710
723
token,
711
724
body: { repo, collection, rkey },
712
-
})
725
+
});
713
726
},
714
727
715
728
getTotpStatus(token: AccessToken): Promise<TotpStatus> {
716
-
return xrpc('com.atproto.server.getTotpStatus', { token })
729
+
return xrpc("com.atproto.server.getTotpStatus", { token });
717
730
},
718
731
719
732
createTotpSecret(token: AccessToken): Promise<TotpSecret> {
720
-
return xrpc('com.atproto.server.createTotpSecret', {
721
-
method: 'POST',
733
+
return xrpc("com.atproto.server.createTotpSecret", {
734
+
method: "POST",
722
735
token,
723
-
})
736
+
});
724
737
},
725
738
726
739
enableTotp(token: AccessToken, code: string): Promise<EnableTotpResponse> {
727
-
return xrpc('com.atproto.server.enableTotp', {
728
-
method: 'POST',
740
+
return xrpc("com.atproto.server.enableTotp", {
741
+
method: "POST",
729
742
token,
730
743
body: { code },
731
-
})
744
+
});
732
745
},
733
746
734
747
disableTotp(
···
736
749
password: string,
737
750
code: string,
738
751
): Promise<SuccessResponse> {
739
-
return xrpc('com.atproto.server.disableTotp', {
740
-
method: 'POST',
752
+
return xrpc("com.atproto.server.disableTotp", {
753
+
method: "POST",
741
754
token,
742
755
body: { password, code },
743
-
})
756
+
});
744
757
},
745
758
746
759
regenerateBackupCodes(
···
748
761
password: string,
749
762
code: string,
750
763
): Promise<RegenerateBackupCodesResponse> {
751
-
return xrpc('com.atproto.server.regenerateBackupCodes', {
752
-
method: 'POST',
764
+
return xrpc("com.atproto.server.regenerateBackupCodes", {
765
+
method: "POST",
753
766
token,
754
767
body: { password, code },
755
-
})
768
+
});
756
769
},
757
770
758
771
startPasskeyRegistration(
759
772
token: AccessToken,
760
773
friendlyName?: string,
761
774
): Promise<StartPasskeyRegistrationResponse> {
762
-
return xrpc('com.atproto.server.startPasskeyRegistration', {
763
-
method: 'POST',
775
+
return xrpc("com.atproto.server.startPasskeyRegistration", {
776
+
method: "POST",
764
777
token,
765
778
body: { friendlyName },
766
-
})
779
+
});
767
780
},
768
781
769
782
finishPasskeyRegistration(
···
771
784
credential: unknown,
772
785
friendlyName?: string,
773
786
): Promise<FinishPasskeyRegistrationResponse> {
774
-
return xrpc('com.atproto.server.finishPasskeyRegistration', {
775
-
method: 'POST',
787
+
return xrpc("com.atproto.server.finishPasskeyRegistration", {
788
+
method: "POST",
776
789
token,
777
790
body: { credential, friendlyName },
778
-
})
791
+
});
779
792
},
780
793
781
794
listPasskeys(token: AccessToken): Promise<ListPasskeysResponse> {
782
-
return xrpc('com.atproto.server.listPasskeys', { token })
795
+
return xrpc("com.atproto.server.listPasskeys", { token });
783
796
},
784
797
785
798
async deletePasskey(token: AccessToken, id: string): Promise<void> {
786
-
await xrpc('com.atproto.server.deletePasskey', {
787
-
method: 'POST',
799
+
await xrpc("com.atproto.server.deletePasskey", {
800
+
method: "POST",
788
801
token,
789
802
body: { id },
790
-
})
803
+
});
791
804
},
792
805
793
806
async updatePasskey(
···
795
808
id: string,
796
809
friendlyName: string,
797
810
): Promise<void> {
798
-
await xrpc('com.atproto.server.updatePasskey', {
799
-
method: 'POST',
811
+
await xrpc("com.atproto.server.updatePasskey", {
812
+
method: "POST",
800
813
token,
801
814
body: { id, friendlyName },
802
-
})
815
+
});
803
816
},
804
817
805
818
listTrustedDevices(token: AccessToken): Promise<ListTrustedDevicesResponse> {
806
-
return xrpc('_account.listTrustedDevices', { token })
819
+
return xrpc("_account.listTrustedDevices", { token });
807
820
},
808
821
809
-
revokeTrustedDevice(token: AccessToken, deviceId: string): Promise<SuccessResponse> {
810
-
return xrpc('_account.revokeTrustedDevice', {
811
-
method: 'POST',
822
+
revokeTrustedDevice(
823
+
token: AccessToken,
824
+
deviceId: string,
825
+
): Promise<SuccessResponse> {
826
+
return xrpc("_account.revokeTrustedDevice", {
827
+
method: "POST",
812
828
token,
813
829
body: { deviceId },
814
-
})
830
+
});
815
831
},
816
832
817
833
updateTrustedDevice(
···
819
835
deviceId: string,
820
836
friendlyName: string,
821
837
): Promise<SuccessResponse> {
822
-
return xrpc('_account.updateTrustedDevice', {
823
-
method: 'POST',
838
+
return xrpc("_account.updateTrustedDevice", {
839
+
method: "POST",
824
840
token,
825
841
body: { deviceId, friendlyName },
826
-
})
842
+
});
827
843
},
828
844
829
845
getReauthStatus(token: AccessToken): Promise<ReauthStatus> {
830
-
return xrpc('_account.getReauthStatus', { token })
846
+
return xrpc("_account.getReauthStatus", { token });
831
847
},
832
848
833
-
reauthPassword(token: AccessToken, password: string): Promise<ReauthResponse> {
834
-
return xrpc('_account.reauthPassword', {
835
-
method: 'POST',
849
+
reauthPassword(
850
+
token: AccessToken,
851
+
password: string,
852
+
): Promise<ReauthResponse> {
853
+
return xrpc("_account.reauthPassword", {
854
+
method: "POST",
836
855
token,
837
856
body: { password },
838
-
})
857
+
});
839
858
},
840
859
841
860
reauthTotp(token: AccessToken, code: string): Promise<ReauthResponse> {
842
-
return xrpc('_account.reauthTotp', {
843
-
method: 'POST',
861
+
return xrpc("_account.reauthTotp", {
862
+
method: "POST",
844
863
token,
845
864
body: { code },
846
-
})
865
+
});
847
866
},
848
867
849
868
reauthPasskeyStart(token: AccessToken): Promise<ReauthPasskeyStartResponse> {
850
-
return xrpc('_account.reauthPasskeyStart', {
851
-
method: 'POST',
869
+
return xrpc("_account.reauthPasskeyStart", {
870
+
method: "POST",
852
871
token,
853
-
})
872
+
});
854
873
},
855
874
856
-
reauthPasskeyFinish(token: AccessToken, credential: unknown): Promise<ReauthResponse> {
857
-
return xrpc('_account.reauthPasskeyFinish', {
858
-
method: 'POST',
875
+
reauthPasskeyFinish(
876
+
token: AccessToken,
877
+
credential: unknown,
878
+
): Promise<ReauthResponse> {
879
+
return xrpc("_account.reauthPasskeyFinish", {
880
+
method: "POST",
859
881
token,
860
882
body: { credential },
861
-
})
883
+
});
862
884
},
863
885
864
886
reserveSigningKey(did?: Did): Promise<ReserveSigningKeyResponse> {
865
-
return xrpc('com.atproto.server.reserveSigningKey', {
866
-
method: 'POST',
887
+
return xrpc("com.atproto.server.reserveSigningKey", {
888
+
method: "POST",
867
889
body: { did },
868
-
})
890
+
});
869
891
},
870
892
871
-
getRecommendedDidCredentials(token: AccessToken): Promise<RecommendedDidCredentials> {
872
-
return xrpc('com.atproto.identity.getRecommendedDidCredentials', { token })
893
+
getRecommendedDidCredentials(
894
+
token: AccessToken,
895
+
): Promise<RecommendedDidCredentials> {
896
+
return xrpc("com.atproto.identity.getRecommendedDidCredentials", { token });
873
897
},
874
898
875
899
async activateAccount(token: AccessToken): Promise<void> {
876
-
await xrpc('com.atproto.server.activateAccount', {
877
-
method: 'POST',
900
+
await xrpc("com.atproto.server.activateAccount", {
901
+
method: "POST",
878
902
token,
879
-
})
903
+
});
880
904
},
881
905
882
906
async createPasskeyAccount(params: {
883
-
handle: Handle
884
-
email?: EmailAddress
885
-
inviteCode?: string
886
-
didType?: DidType
887
-
did?: Did
888
-
signingKey?: string
889
-
verificationChannel?: VerificationChannel
890
-
discordId?: string
891
-
telegramUsername?: string
892
-
signalNumber?: string
907
+
handle: Handle;
908
+
email?: EmailAddress;
909
+
inviteCode?: string;
910
+
didType?: DidType;
911
+
did?: Did;
912
+
signingKey?: string;
913
+
verificationChannel?: VerificationChannel;
914
+
discordId?: string;
915
+
telegramUsername?: string;
916
+
signalNumber?: string;
893
917
}, byodToken?: string): Promise<PasskeyAccountCreateResponse> {
894
-
const url = `${API_BASE}/_account.createPasskeyAccount`
918
+
const url = `${API_BASE}/_account.createPasskeyAccount`;
895
919
const headers: Record<string, string> = {
896
-
'Content-Type': 'application/json',
897
-
}
920
+
"Content-Type": "application/json",
921
+
};
898
922
if (byodToken) {
899
-
headers['Authorization'] = `Bearer ${byodToken}`
923
+
headers["Authorization"] = `Bearer ${byodToken}`;
900
924
}
901
925
const res = await fetch(url, {
902
-
method: 'POST',
926
+
method: "POST",
903
927
headers,
904
928
body: JSON.stringify(params),
905
-
})
929
+
});
906
930
if (!res.ok) {
907
931
const errData = await res.json().catch(() => ({
908
-
error: 'Unknown',
932
+
error: "Unknown",
909
933
message: res.statusText,
910
-
}))
911
-
throw new ApiError(res.status, errData.error, errData.message)
934
+
}));
935
+
throw new ApiError(res.status, errData.error, errData.message);
912
936
}
913
-
return res.json()
937
+
return res.json();
914
938
},
915
939
916
940
startPasskeyRegistrationForSetup(
···
918
942
setupToken: string,
919
943
friendlyName?: string,
920
944
): Promise<StartPasskeyRegistrationResponse> {
921
-
return xrpc('_account.startPasskeyRegistrationForSetup', {
922
-
method: 'POST',
945
+
return xrpc("_account.startPasskeyRegistrationForSetup", {
946
+
method: "POST",
923
947
body: { did, setupToken, friendlyName },
924
-
})
948
+
});
925
949
},
926
950
927
951
completePasskeySetup(
···
930
954
passkeyCredential: unknown,
931
955
passkeyFriendlyName?: string,
932
956
): Promise<CompletePasskeySetupResponse> {
933
-
return xrpc('_account.completePasskeySetup', {
934
-
method: 'POST',
957
+
return xrpc("_account.completePasskeySetup", {
958
+
method: "POST",
935
959
body: { did, setupToken, passkeyCredential, passkeyFriendlyName },
936
-
})
960
+
});
937
961
},
938
962
939
963
requestPasskeyRecovery(email: EmailAddress): Promise<SuccessResponse> {
940
-
return xrpc('_account.requestPasskeyRecovery', {
941
-
method: 'POST',
964
+
return xrpc("_account.requestPasskeyRecovery", {
965
+
method: "POST",
942
966
body: { email },
943
-
})
967
+
});
944
968
},
945
969
946
970
recoverPasskeyAccount(
···
948
972
recoveryToken: string,
949
973
newPassword: string,
950
974
): Promise<SuccessResponse> {
951
-
return xrpc('_account.recoverPasskeyAccount', {
952
-
method: 'POST',
975
+
return xrpc("_account.recoverPasskeyAccount", {
976
+
method: "POST",
953
977
body: { did, recoveryToken, newPassword },
954
-
})
978
+
});
955
979
},
956
980
957
-
verifyMigrationEmail(token: string, email: EmailAddress): Promise<VerifyMigrationEmailResponse> {
958
-
return xrpc('com.atproto.server.verifyMigrationEmail', {
959
-
method: 'POST',
981
+
verifyMigrationEmail(
982
+
token: string,
983
+
email: EmailAddress,
984
+
): Promise<VerifyMigrationEmailResponse> {
985
+
return xrpc("com.atproto.server.verifyMigrationEmail", {
986
+
method: "POST",
960
987
body: { token, email },
961
-
})
988
+
});
962
989
},
963
990
964
-
resendMigrationVerification(email: EmailAddress): Promise<ResendMigrationVerificationResponse> {
965
-
return xrpc('com.atproto.server.resendMigrationVerification', {
966
-
method: 'POST',
991
+
resendMigrationVerification(
992
+
email: EmailAddress,
993
+
): Promise<ResendMigrationVerificationResponse> {
994
+
return xrpc("com.atproto.server.resendMigrationVerification", {
995
+
method: "POST",
967
996
body: { email },
968
-
})
997
+
});
969
998
},
970
999
971
1000
verifyToken(
···
973
1002
identifier: string,
974
1003
accessToken?: AccessToken,
975
1004
): Promise<VerifyTokenResponse> {
976
-
return xrpc('_account.verifyToken', {
977
-
method: 'POST',
1005
+
return xrpc("_account.verifyToken", {
1006
+
method: "POST",
978
1007
body: { token, identifier },
979
1008
token: accessToken,
980
-
})
1009
+
});
981
1010
},
982
1011
983
1012
getDidDocument(token: AccessToken): Promise<DidDocument> {
984
-
return xrpc('_account.getDidDocument', { token })
1013
+
return xrpc("_account.getDidDocument", { token });
985
1014
},
986
1015
987
1016
updateDidDocument(
988
1017
token: AccessToken,
989
1018
params: {
990
-
verificationMethods?: VerificationMethod[]
991
-
alsoKnownAs?: string[]
992
-
serviceEndpoint?: string
1019
+
verificationMethods?: VerificationMethod[];
1020
+
alsoKnownAs?: string[];
1021
+
serviceEndpoint?: string;
993
1022
},
994
1023
): Promise<SuccessResponse> {
995
-
return xrpc('_account.updateDidDocument', {
996
-
method: 'POST',
1024
+
return xrpc("_account.updateDidDocument", {
1025
+
method: "POST",
997
1026
token,
998
1027
body: params,
999
-
})
1028
+
});
1000
1029
},
1001
1030
1002
-
async deactivateAccount(token: AccessToken, deleteAfter?: string): Promise<void> {
1003
-
await xrpc('com.atproto.server.deactivateAccount', {
1004
-
method: 'POST',
1031
+
async deactivateAccount(
1032
+
token: AccessToken,
1033
+
deleteAfter?: string,
1034
+
): Promise<void> {
1035
+
await xrpc("com.atproto.server.deactivateAccount", {
1036
+
method: "POST",
1005
1037
token,
1006
1038
body: { deleteAfter },
1007
-
})
1039
+
});
1008
1040
},
1009
1041
1010
1042
async getRepo(token: AccessToken, did: Did): Promise<ArrayBuffer> {
1011
-
const url = `${API_BASE}/com.atproto.sync.getRepo?did=${encodeURIComponent(did)}`
1043
+
const url = `${API_BASE}/com.atproto.sync.getRepo?did=${
1044
+
encodeURIComponent(did)
1045
+
}`;
1012
1046
const res = await fetch(url, {
1013
1047
headers: { Authorization: `Bearer ${token}` },
1014
-
})
1048
+
});
1015
1049
if (!res.ok) {
1016
1050
const errData = await res.json().catch(() => ({
1017
-
error: 'Unknown',
1051
+
error: "Unknown",
1018
1052
message: res.statusText,
1019
-
}))
1020
-
throw new ApiError(res.status, errData.error, errData.message)
1053
+
}));
1054
+
throw new ApiError(res.status, errData.error, errData.message);
1021
1055
}
1022
-
return res.arrayBuffer()
1056
+
return res.arrayBuffer();
1023
1057
},
1024
1058
1025
1059
listBackups(token: AccessToken): Promise<ListBackupsResponse> {
1026
-
return xrpc('_backup.listBackups', { token })
1060
+
return xrpc("_backup.listBackups", { token });
1027
1061
},
1028
1062
1029
1063
async getBackup(token: AccessToken, id: string): Promise<Blob> {
1030
-
const url = `${API_BASE}/_backup.getBackup?id=${encodeURIComponent(id)}`
1064
+
const url = `${API_BASE}/_backup.getBackup?id=${encodeURIComponent(id)}`;
1031
1065
const res = await fetch(url, {
1032
1066
headers: { Authorization: `Bearer ${token}` },
1033
-
})
1067
+
});
1034
1068
if (!res.ok) {
1035
1069
const errData = await res.json().catch(() => ({
1036
-
error: 'Unknown',
1070
+
error: "Unknown",
1037
1071
message: res.statusText,
1038
-
}))
1039
-
throw new ApiError(res.status, errData.error, errData.message)
1072
+
}));
1073
+
throw new ApiError(res.status, errData.error, errData.message);
1040
1074
}
1041
-
return res.blob()
1075
+
return res.blob();
1042
1076
},
1043
1077
1044
1078
createBackup(token: AccessToken): Promise<CreateBackupResponse> {
1045
-
return xrpc('_backup.createBackup', {
1046
-
method: 'POST',
1079
+
return xrpc("_backup.createBackup", {
1080
+
method: "POST",
1047
1081
token,
1048
-
})
1082
+
});
1049
1083
},
1050
1084
1051
1085
async deleteBackup(token: AccessToken, id: string): Promise<void> {
1052
-
await xrpc('_backup.deleteBackup', {
1053
-
method: 'POST',
1086
+
await xrpc("_backup.deleteBackup", {
1087
+
method: "POST",
1054
1088
token,
1055
1089
params: { id },
1056
-
})
1090
+
});
1057
1091
},
1058
1092
1059
-
setBackupEnabled(token: AccessToken, enabled: boolean): Promise<SetBackupEnabledResponse> {
1060
-
return xrpc('_backup.setEnabled', {
1061
-
method: 'POST',
1093
+
setBackupEnabled(
1094
+
token: AccessToken,
1095
+
enabled: boolean,
1096
+
): Promise<SetBackupEnabledResponse> {
1097
+
return xrpc("_backup.setEnabled", {
1098
+
method: "POST",
1062
1099
token,
1063
1100
body: { enabled },
1064
-
})
1101
+
});
1065
1102
},
1066
1103
1067
1104
async importRepo(token: AccessToken, car: Uint8Array): Promise<void> {
1068
-
const url = `${API_BASE}/com.atproto.repo.importRepo`
1105
+
const url = `${API_BASE}/com.atproto.repo.importRepo`;
1069
1106
const res = await fetch(url, {
1070
-
method: 'POST',
1107
+
method: "POST",
1071
1108
headers: {
1072
1109
Authorization: `Bearer ${token}`,
1073
-
'Content-Type': 'application/vnd.ipld.car',
1110
+
"Content-Type": "application/vnd.ipld.car",
1074
1111
},
1075
-
body: car,
1076
-
})
1112
+
body: car as unknown as BodyInit,
1113
+
});
1077
1114
if (!res.ok) {
1078
1115
const errData = await res.json().catch(() => ({
1079
-
error: 'Unknown',
1116
+
error: "Unknown",
1080
1117
message: res.statusText,
1081
-
}))
1082
-
throw new ApiError(res.status, errData.error, errData.message)
1118
+
}));
1119
+
throw new ApiError(res.status, errData.error, errData.message);
1083
1120
}
1084
1121
},
1085
-
}
1122
+
};
1086
1123
1087
1124
export const typedApi = {
1088
1125
createSession(
1089
1126
identifier: string,
1090
-
password: string
1127
+
password: string,
1091
1128
): Promise<Result<Session, ApiError>> {
1092
-
return xrpcResult<Session>('com.atproto.server.createSession', {
1093
-
method: 'POST',
1129
+
return xrpcResult<Session>("com.atproto.server.createSession", {
1130
+
method: "POST",
1094
1131
body: { identifier, password },
1095
-
}).then(r => r.ok ? ok(castSession(r.value)) : r)
1132
+
}).then((r) => r.ok ? ok(castSession(r.value)) : r);
1096
1133
},
1097
1134
1098
1135
getSession(token: AccessToken): Promise<Result<Session, ApiError>> {
1099
-
return xrpcResult<Session>('com.atproto.server.getSession', { token })
1100
-
.then(r => r.ok ? ok(castSession(r.value)) : r)
1136
+
return xrpcResult<Session>("com.atproto.server.getSession", { token })
1137
+
.then((r) => r.ok ? ok(castSession(r.value)) : r);
1101
1138
},
1102
1139
1103
1140
refreshSession(refreshJwt: RefreshToken): Promise<Result<Session, ApiError>> {
1104
-
return xrpcResult<Session>('com.atproto.server.refreshSession', {
1105
-
method: 'POST',
1141
+
return xrpcResult<Session>("com.atproto.server.refreshSession", {
1142
+
method: "POST",
1106
1143
token: refreshJwt,
1107
-
}).then(r => r.ok ? ok(castSession(r.value)) : r)
1144
+
}).then((r) => r.ok ? ok(castSession(r.value)) : r);
1108
1145
},
1109
1146
1110
1147
describeServer(): Promise<Result<ServerDescription, ApiError>> {
1111
-
return xrpcResult('com.atproto.server.describeServer')
1148
+
return xrpcResult("com.atproto.server.describeServer");
1112
1149
},
1113
1150
1114
-
listAppPasswords(token: AccessToken): Promise<Result<{ passwords: AppPassword[] }, ApiError>> {
1115
-
return xrpcResult('com.atproto.server.listAppPasswords', { token })
1151
+
listAppPasswords(
1152
+
token: AccessToken,
1153
+
): Promise<Result<{ passwords: AppPassword[] }, ApiError>> {
1154
+
return xrpcResult("com.atproto.server.listAppPasswords", { token });
1116
1155
},
1117
1156
1118
1157
createAppPassword(
1119
1158
token: AccessToken,
1120
1159
name: string,
1121
-
scopes?: string
1160
+
scopes?: string,
1122
1161
): Promise<Result<CreatedAppPassword, ApiError>> {
1123
-
return xrpcResult('com.atproto.server.createAppPassword', {
1124
-
method: 'POST',
1162
+
return xrpcResult("com.atproto.server.createAppPassword", {
1163
+
method: "POST",
1125
1164
token,
1126
1165
body: { name, scopes },
1127
-
})
1166
+
});
1128
1167
},
1129
1168
1130
-
revokeAppPassword(token: AccessToken, name: string): Promise<Result<void, ApiError>> {
1131
-
return xrpcResult<void>('com.atproto.server.revokeAppPassword', {
1132
-
method: 'POST',
1169
+
revokeAppPassword(
1170
+
token: AccessToken,
1171
+
name: string,
1172
+
): Promise<Result<void, ApiError>> {
1173
+
return xrpcResult<void>("com.atproto.server.revokeAppPassword", {
1174
+
method: "POST",
1133
1175
token,
1134
1176
body: { name },
1135
-
})
1177
+
});
1136
1178
},
1137
1179
1138
-
listSessions(token: AccessToken): Promise<Result<ListSessionsResponse, ApiError>> {
1139
-
return xrpcResult('_account.listSessions', { token })
1180
+
listSessions(
1181
+
token: AccessToken,
1182
+
): Promise<Result<ListSessionsResponse, ApiError>> {
1183
+
return xrpcResult("_account.listSessions", { token });
1140
1184
},
1141
1185
1142
-
revokeSession(token: AccessToken, sessionId: string): Promise<Result<void, ApiError>> {
1143
-
return xrpcResult<void>('_account.revokeSession', {
1144
-
method: 'POST',
1186
+
revokeSession(
1187
+
token: AccessToken,
1188
+
sessionId: string,
1189
+
): Promise<Result<void, ApiError>> {
1190
+
return xrpcResult<void>("_account.revokeSession", {
1191
+
method: "POST",
1145
1192
token,
1146
1193
body: { sessionId },
1147
-
})
1194
+
});
1148
1195
},
1149
1196
1150
1197
getTotpStatus(token: AccessToken): Promise<Result<TotpStatus, ApiError>> {
1151
-
return xrpcResult('com.atproto.server.getTotpStatus', { token })
1198
+
return xrpcResult("com.atproto.server.getTotpStatus", { token });
1152
1199
},
1153
1200
1154
1201
createTotpSecret(token: AccessToken): Promise<Result<TotpSecret, ApiError>> {
1155
-
return xrpcResult('com.atproto.server.createTotpSecret', {
1156
-
method: 'POST',
1202
+
return xrpcResult("com.atproto.server.createTotpSecret", {
1203
+
method: "POST",
1157
1204
token,
1158
-
})
1205
+
});
1159
1206
},
1160
1207
1161
-
enableTotp(token: AccessToken, code: string): Promise<Result<EnableTotpResponse, ApiError>> {
1162
-
return xrpcResult('com.atproto.server.enableTotp', {
1163
-
method: 'POST',
1208
+
enableTotp(
1209
+
token: AccessToken,
1210
+
code: string,
1211
+
): Promise<Result<EnableTotpResponse, ApiError>> {
1212
+
return xrpcResult("com.atproto.server.enableTotp", {
1213
+
method: "POST",
1164
1214
token,
1165
1215
body: { code },
1166
-
})
1216
+
});
1167
1217
},
1168
1218
1169
1219
disableTotp(
1170
1220
token: AccessToken,
1171
1221
password: string,
1172
-
code: string
1222
+
code: string,
1173
1223
): Promise<Result<SuccessResponse, ApiError>> {
1174
-
return xrpcResult('com.atproto.server.disableTotp', {
1175
-
method: 'POST',
1224
+
return xrpcResult("com.atproto.server.disableTotp", {
1225
+
method: "POST",
1176
1226
token,
1177
1227
body: { password, code },
1178
-
})
1228
+
});
1179
1229
},
1180
1230
1181
-
listPasskeys(token: AccessToken): Promise<Result<ListPasskeysResponse, ApiError>> {
1182
-
return xrpcResult('com.atproto.server.listPasskeys', { token })
1231
+
listPasskeys(
1232
+
token: AccessToken,
1233
+
): Promise<Result<ListPasskeysResponse, ApiError>> {
1234
+
return xrpcResult("com.atproto.server.listPasskeys", { token });
1183
1235
},
1184
1236
1185
-
deletePasskey(token: AccessToken, id: string): Promise<Result<void, ApiError>> {
1186
-
return xrpcResult<void>('com.atproto.server.deletePasskey', {
1187
-
method: 'POST',
1237
+
deletePasskey(
1238
+
token: AccessToken,
1239
+
id: string,
1240
+
): Promise<Result<void, ApiError>> {
1241
+
return xrpcResult<void>("com.atproto.server.deletePasskey", {
1242
+
method: "POST",
1188
1243
token,
1189
1244
body: { id },
1190
-
})
1245
+
});
1191
1246
},
1192
1247
1193
-
listTrustedDevices(token: AccessToken): Promise<Result<ListTrustedDevicesResponse, ApiError>> {
1194
-
return xrpcResult('_account.listTrustedDevices', { token })
1248
+
listTrustedDevices(
1249
+
token: AccessToken,
1250
+
): Promise<Result<ListTrustedDevicesResponse, ApiError>> {
1251
+
return xrpcResult("_account.listTrustedDevices", { token });
1195
1252
},
1196
1253
1197
1254
getReauthStatus(token: AccessToken): Promise<Result<ReauthStatus, ApiError>> {
1198
-
return xrpcResult('_account.getReauthStatus', { token })
1255
+
return xrpcResult("_account.getReauthStatus", { token });
1199
1256
},
1200
1257
1201
-
getNotificationPrefs(token: AccessToken): Promise<Result<NotificationPrefs, ApiError>> {
1202
-
return xrpcResult('_account.getNotificationPrefs', { token })
1258
+
getNotificationPrefs(
1259
+
token: AccessToken,
1260
+
): Promise<Result<NotificationPrefs, ApiError>> {
1261
+
return xrpcResult("_account.getNotificationPrefs", { token });
1203
1262
},
1204
1263
1205
-
updateHandle(token: AccessToken, handle: Handle): Promise<Result<void, ApiError>> {
1206
-
return xrpcResult<void>('com.atproto.identity.updateHandle', {
1207
-
method: 'POST',
1264
+
updateHandle(
1265
+
token: AccessToken,
1266
+
handle: Handle,
1267
+
): Promise<Result<void, ApiError>> {
1268
+
return xrpcResult<void>("com.atproto.identity.updateHandle", {
1269
+
method: "POST",
1208
1270
token,
1209
1271
body: { handle },
1210
-
})
1272
+
});
1211
1273
},
1212
1274
1213
-
describeRepo(token: AccessToken, repo: Did): Promise<Result<RepoDescription, ApiError>> {
1214
-
return xrpcResult('com.atproto.repo.describeRepo', {
1275
+
describeRepo(
1276
+
token: AccessToken,
1277
+
repo: Did,
1278
+
): Promise<Result<RepoDescription, ApiError>> {
1279
+
return xrpcResult("com.atproto.repo.describeRepo", {
1215
1280
token,
1216
1281
params: { repo },
1217
-
})
1282
+
});
1218
1283
},
1219
1284
1220
1285
listRecords(
1221
1286
token: AccessToken,
1222
1287
repo: Did,
1223
1288
collection: Nsid,
1224
-
options?: { limit?: number; cursor?: string; reverse?: boolean }
1289
+
options?: { limit?: number; cursor?: string; reverse?: boolean },
1225
1290
): Promise<Result<ListRecordsResponse, ApiError>> {
1226
-
const params: Record<string, string> = { repo, collection }
1227
-
if (options?.limit) params.limit = String(options.limit)
1228
-
if (options?.cursor) params.cursor = options.cursor
1229
-
if (options?.reverse) params.reverse = 'true'
1230
-
return xrpcResult('com.atproto.repo.listRecords', { token, params })
1291
+
const params: Record<string, string> = { repo, collection };
1292
+
if (options?.limit) params.limit = String(options.limit);
1293
+
if (options?.cursor) params.cursor = options.cursor;
1294
+
if (options?.reverse) params.reverse = "true";
1295
+
return xrpcResult("com.atproto.repo.listRecords", { token, params });
1231
1296
},
1232
1297
1233
1298
getRecord(
1234
1299
token: AccessToken,
1235
1300
repo: Did,
1236
1301
collection: Nsid,
1237
-
rkey: Rkey
1302
+
rkey: Rkey,
1238
1303
): Promise<Result<RecordResponse, ApiError>> {
1239
-
return xrpcResult('com.atproto.repo.getRecord', {
1304
+
return xrpcResult("com.atproto.repo.getRecord", {
1240
1305
token,
1241
1306
params: { repo, collection, rkey },
1242
-
})
1307
+
});
1243
1308
},
1244
1309
1245
1310
deleteRecord(
1246
1311
token: AccessToken,
1247
1312
repo: Did,
1248
1313
collection: Nsid,
1249
-
rkey: Rkey
1314
+
rkey: Rkey,
1250
1315
): Promise<Result<void, ApiError>> {
1251
-
return xrpcResult<void>('com.atproto.repo.deleteRecord', {
1252
-
method: 'POST',
1316
+
return xrpcResult<void>("com.atproto.repo.deleteRecord", {
1317
+
method: "POST",
1253
1318
token,
1254
1319
body: { repo, collection, rkey },
1255
-
})
1320
+
});
1256
1321
},
1257
1322
1258
1323
searchAccounts(
1259
1324
token: AccessToken,
1260
-
options?: { handle?: string; cursor?: string; limit?: number }
1325
+
options?: { handle?: string; cursor?: string; limit?: number },
1261
1326
): Promise<Result<SearchAccountsResponse, ApiError>> {
1262
-
const params: Record<string, string> = {}
1263
-
if (options?.handle) params.handle = options.handle
1264
-
if (options?.cursor) params.cursor = options.cursor
1265
-
if (options?.limit) params.limit = String(options.limit)
1266
-
return xrpcResult('com.atproto.admin.searchAccounts', { token, params })
1327
+
const params: Record<string, string> = {};
1328
+
if (options?.handle) params.handle = options.handle;
1329
+
if (options?.cursor) params.cursor = options.cursor;
1330
+
if (options?.limit) params.limit = String(options.limit);
1331
+
return xrpcResult("com.atproto.admin.searchAccounts", { token, params });
1267
1332
},
1268
1333
1269
-
getAccountInfo(token: AccessToken, did: Did): Promise<Result<AccountInfo, ApiError>> {
1270
-
return xrpcResult('com.atproto.admin.getAccountInfo', { token, params: { did } })
1334
+
getAccountInfo(
1335
+
token: AccessToken,
1336
+
did: Did,
1337
+
): Promise<Result<AccountInfo, ApiError>> {
1338
+
return xrpcResult("com.atproto.admin.getAccountInfo", {
1339
+
token,
1340
+
params: { did },
1341
+
});
1271
1342
},
1272
1343
1273
1344
getServerStats(token: AccessToken): Promise<Result<ServerStats, ApiError>> {
1274
-
return xrpcResult('_admin.getServerStats', { token })
1345
+
return xrpcResult("_admin.getServerStats", { token });
1275
1346
},
1276
1347
1277
-
listBackups(token: AccessToken): Promise<Result<ListBackupsResponse, ApiError>> {
1278
-
return xrpcResult('_backup.listBackups', { token })
1348
+
listBackups(
1349
+
token: AccessToken,
1350
+
): Promise<Result<ListBackupsResponse, ApiError>> {
1351
+
return xrpcResult("_backup.listBackups", { token });
1279
1352
},
1280
1353
1281
-
createBackup(token: AccessToken): Promise<Result<CreateBackupResponse, ApiError>> {
1282
-
return xrpcResult('_backup.createBackup', {
1283
-
method: 'POST',
1354
+
createBackup(
1355
+
token: AccessToken,
1356
+
): Promise<Result<CreateBackupResponse, ApiError>> {
1357
+
return xrpcResult("_backup.createBackup", {
1358
+
method: "POST",
1284
1359
token,
1285
-
})
1360
+
});
1286
1361
},
1287
1362
1288
1363
getDidDocument(token: AccessToken): Promise<Result<DidDocument, ApiError>> {
1289
-
return xrpcResult('_account.getDidDocument', { token })
1364
+
return xrpcResult("_account.getDidDocument", { token });
1290
1365
},
1291
1366
1292
1367
deleteSession(token: AccessToken): Promise<Result<void, ApiError>> {
1293
-
return xrpcResult<void>('com.atproto.server.deleteSession', {
1294
-
method: 'POST',
1368
+
return xrpcResult<void>("com.atproto.server.deleteSession", {
1369
+
method: "POST",
1295
1370
token,
1296
-
})
1371
+
});
1297
1372
},
1298
1373
1299
-
revokeAllSessions(token: AccessToken): Promise<Result<{ revokedCount: number }, ApiError>> {
1300
-
return xrpcResult('_account.revokeAllSessions', {
1301
-
method: 'POST',
1374
+
revokeAllSessions(
1375
+
token: AccessToken,
1376
+
): Promise<Result<{ revokedCount: number }, ApiError>> {
1377
+
return xrpcResult("_account.revokeAllSessions", {
1378
+
method: "POST",
1302
1379
token,
1303
-
})
1380
+
});
1304
1381
},
1305
1382
1306
-
getAccountInviteCodes(token: AccessToken): Promise<Result<{ codes: InviteCodeInfo[] }, ApiError>> {
1307
-
return xrpcResult('com.atproto.server.getAccountInviteCodes', { token })
1383
+
getAccountInviteCodes(
1384
+
token: AccessToken,
1385
+
): Promise<Result<{ codes: InviteCodeInfo[] }, ApiError>> {
1386
+
return xrpcResult("com.atproto.server.getAccountInviteCodes", { token });
1308
1387
},
1309
1388
1310
-
createInviteCode(token: AccessToken, useCount: number = 1): Promise<Result<{ code: string }, ApiError>> {
1311
-
return xrpcResult('com.atproto.server.createInviteCode', {
1312
-
method: 'POST',
1389
+
createInviteCode(
1390
+
token: AccessToken,
1391
+
useCount: number = 1,
1392
+
): Promise<Result<{ code: string }, ApiError>> {
1393
+
return xrpcResult("com.atproto.server.createInviteCode", {
1394
+
method: "POST",
1313
1395
token,
1314
1396
body: { useCount },
1315
-
})
1397
+
});
1316
1398
},
1317
1399
1318
1400
changePassword(
1319
1401
token: AccessToken,
1320
1402
currentPassword: string,
1321
-
newPassword: string
1403
+
newPassword: string,
1322
1404
): Promise<Result<void, ApiError>> {
1323
-
return xrpcResult<void>('_account.changePassword', {
1324
-
method: 'POST',
1405
+
return xrpcResult<void>("_account.changePassword", {
1406
+
method: "POST",
1325
1407
token,
1326
1408
body: { currentPassword, newPassword },
1327
-
})
1409
+
});
1328
1410
},
1329
1411
1330
-
getPasswordStatus(token: AccessToken): Promise<Result<PasswordStatus, ApiError>> {
1331
-
return xrpcResult('_account.getPasswordStatus', { token })
1412
+
getPasswordStatus(
1413
+
token: AccessToken,
1414
+
): Promise<Result<PasswordStatus, ApiError>> {
1415
+
return xrpcResult("_account.getPasswordStatus", { token });
1332
1416
},
1333
1417
1334
1418
getServerConfig(): Promise<Result<ServerConfig, ApiError>> {
1335
-
return xrpcResult('_server.getConfig')
1419
+
return xrpcResult("_server.getConfig");
1336
1420
},
1337
1421
1338
-
getLegacyLoginPreference(token: AccessToken): Promise<Result<LegacyLoginPreference, ApiError>> {
1339
-
return xrpcResult('_account.getLegacyLoginPreference', { token })
1422
+
getLegacyLoginPreference(
1423
+
token: AccessToken,
1424
+
): Promise<Result<LegacyLoginPreference, ApiError>> {
1425
+
return xrpcResult("_account.getLegacyLoginPreference", { token });
1340
1426
},
1341
1427
1342
1428
updateLegacyLoginPreference(
1343
1429
token: AccessToken,
1344
-
allowLegacyLogin: boolean
1430
+
allowLegacyLogin: boolean,
1345
1431
): Promise<Result<UpdateLegacyLoginResponse, ApiError>> {
1346
-
return xrpcResult('_account.updateLegacyLoginPreference', {
1347
-
method: 'POST',
1432
+
return xrpcResult("_account.updateLegacyLoginPreference", {
1433
+
method: "POST",
1348
1434
token,
1349
1435
body: { allowLegacyLogin },
1350
-
})
1436
+
});
1351
1437
},
1352
1438
1353
-
getNotificationHistory(token: AccessToken): Promise<Result<NotificationHistoryResponse, ApiError>> {
1354
-
return xrpcResult('_account.getNotificationHistory', { token })
1439
+
getNotificationHistory(
1440
+
token: AccessToken,
1441
+
): Promise<Result<NotificationHistoryResponse, ApiError>> {
1442
+
return xrpcResult("_account.getNotificationHistory", { token });
1355
1443
},
1356
1444
1357
1445
updateNotificationPrefs(
1358
1446
token: AccessToken,
1359
1447
prefs: {
1360
-
preferredChannel?: string
1361
-
discordId?: string
1362
-
telegramUsername?: string
1363
-
signalNumber?: string
1364
-
}
1448
+
preferredChannel?: string;
1449
+
discordId?: string;
1450
+
telegramUsername?: string;
1451
+
signalNumber?: string;
1452
+
},
1365
1453
): Promise<Result<SuccessResponse, ApiError>> {
1366
-
return xrpcResult('_account.updateNotificationPrefs', {
1367
-
method: 'POST',
1454
+
return xrpcResult("_account.updateNotificationPrefs", {
1455
+
method: "POST",
1368
1456
token,
1369
1457
body: prefs,
1370
-
})
1458
+
});
1371
1459
},
1372
1460
1373
-
revokeTrustedDevice(token: AccessToken, deviceId: string): Promise<Result<SuccessResponse, ApiError>> {
1374
-
return xrpcResult('_account.revokeTrustedDevice', {
1375
-
method: 'POST',
1461
+
revokeTrustedDevice(
1462
+
token: AccessToken,
1463
+
deviceId: string,
1464
+
): Promise<Result<SuccessResponse, ApiError>> {
1465
+
return xrpcResult("_account.revokeTrustedDevice", {
1466
+
method: "POST",
1376
1467
token,
1377
1468
body: { deviceId },
1378
-
})
1469
+
});
1379
1470
},
1380
1471
1381
1472
updateTrustedDevice(
1382
1473
token: AccessToken,
1383
1474
deviceId: string,
1384
-
friendlyName: string
1475
+
friendlyName: string,
1385
1476
): Promise<Result<SuccessResponse, ApiError>> {
1386
-
return xrpcResult('_account.updateTrustedDevice', {
1387
-
method: 'POST',
1477
+
return xrpcResult("_account.updateTrustedDevice", {
1478
+
method: "POST",
1388
1479
token,
1389
1480
body: { deviceId, friendlyName },
1390
-
})
1481
+
});
1391
1482
},
1392
1483
1393
-
reauthPassword(token: AccessToken, password: string): Promise<Result<ReauthResponse, ApiError>> {
1394
-
return xrpcResult('_account.reauthPassword', {
1395
-
method: 'POST',
1484
+
reauthPassword(
1485
+
token: AccessToken,
1486
+
password: string,
1487
+
): Promise<Result<ReauthResponse, ApiError>> {
1488
+
return xrpcResult("_account.reauthPassword", {
1489
+
method: "POST",
1396
1490
token,
1397
1491
body: { password },
1398
-
})
1492
+
});
1399
1493
},
1400
1494
1401
-
reauthTotp(token: AccessToken, code: string): Promise<Result<ReauthResponse, ApiError>> {
1402
-
return xrpcResult('_account.reauthTotp', {
1403
-
method: 'POST',
1495
+
reauthTotp(
1496
+
token: AccessToken,
1497
+
code: string,
1498
+
): Promise<Result<ReauthResponse, ApiError>> {
1499
+
return xrpcResult("_account.reauthTotp", {
1500
+
method: "POST",
1404
1501
token,
1405
1502
body: { code },
1406
-
})
1503
+
});
1407
1504
},
1408
1505
1409
-
reauthPasskeyStart(token: AccessToken): Promise<Result<ReauthPasskeyStartResponse, ApiError>> {
1410
-
return xrpcResult('_account.reauthPasskeyStart', {
1411
-
method: 'POST',
1506
+
reauthPasskeyStart(
1507
+
token: AccessToken,
1508
+
): Promise<Result<ReauthPasskeyStartResponse, ApiError>> {
1509
+
return xrpcResult("_account.reauthPasskeyStart", {
1510
+
method: "POST",
1412
1511
token,
1413
-
})
1512
+
});
1414
1513
},
1415
1514
1416
-
reauthPasskeyFinish(token: AccessToken, credential: unknown): Promise<Result<ReauthResponse, ApiError>> {
1417
-
return xrpcResult('_account.reauthPasskeyFinish', {
1418
-
method: 'POST',
1515
+
reauthPasskeyFinish(
1516
+
token: AccessToken,
1517
+
credential: unknown,
1518
+
): Promise<Result<ReauthResponse, ApiError>> {
1519
+
return xrpcResult("_account.reauthPasskeyFinish", {
1520
+
method: "POST",
1419
1521
token,
1420
1522
body: { credential },
1421
-
})
1523
+
});
1422
1524
},
1423
1525
1424
-
confirmSignup(did: Did, verificationCode: string): Promise<Result<ConfirmSignupResult, ApiError>> {
1425
-
return xrpcResult('com.atproto.server.confirmSignup', {
1426
-
method: 'POST',
1526
+
confirmSignup(
1527
+
did: Did,
1528
+
verificationCode: string,
1529
+
): Promise<Result<ConfirmSignupResult, ApiError>> {
1530
+
return xrpcResult("com.atproto.server.confirmSignup", {
1531
+
method: "POST",
1427
1532
body: { did, verificationCode },
1428
-
})
1533
+
});
1429
1534
},
1430
1535
1431
-
resendVerification(did: Did): Promise<Result<{ success: boolean }, ApiError>> {
1432
-
return xrpcResult('com.atproto.server.resendVerification', {
1433
-
method: 'POST',
1536
+
resendVerification(
1537
+
did: Did,
1538
+
): Promise<Result<{ success: boolean }, ApiError>> {
1539
+
return xrpcResult("com.atproto.server.resendVerification", {
1540
+
method: "POST",
1434
1541
body: { did },
1435
-
})
1542
+
});
1436
1543
},
1437
1544
1438
-
requestEmailUpdate(token: AccessToken): Promise<Result<EmailUpdateResponse, ApiError>> {
1439
-
return xrpcResult('com.atproto.server.requestEmailUpdate', {
1440
-
method: 'POST',
1545
+
requestEmailUpdate(
1546
+
token: AccessToken,
1547
+
): Promise<Result<EmailUpdateResponse, ApiError>> {
1548
+
return xrpcResult("com.atproto.server.requestEmailUpdate", {
1549
+
method: "POST",
1441
1550
token,
1442
-
})
1551
+
});
1443
1552
},
1444
1553
1445
-
updateEmail(token: AccessToken, email: string, emailToken?: string): Promise<Result<void, ApiError>> {
1446
-
return xrpcResult<void>('com.atproto.server.updateEmail', {
1447
-
method: 'POST',
1554
+
updateEmail(
1555
+
token: AccessToken,
1556
+
email: string,
1557
+
emailToken?: string,
1558
+
): Promise<Result<void, ApiError>> {
1559
+
return xrpcResult<void>("com.atproto.server.updateEmail", {
1560
+
method: "POST",
1448
1561
token,
1449
1562
body: { email, token: emailToken },
1450
-
})
1563
+
});
1451
1564
},
1452
1565
1453
1566
requestAccountDelete(token: AccessToken): Promise<Result<void, ApiError>> {
1454
-
return xrpcResult<void>('com.atproto.server.requestAccountDelete', {
1455
-
method: 'POST',
1567
+
return xrpcResult<void>("com.atproto.server.requestAccountDelete", {
1568
+
method: "POST",
1456
1569
token,
1457
-
})
1570
+
});
1458
1571
},
1459
1572
1460
-
deleteAccount(did: Did, password: string, deleteToken: string): Promise<Result<void, ApiError>> {
1461
-
return xrpcResult<void>('com.atproto.server.deleteAccount', {
1462
-
method: 'POST',
1573
+
deleteAccount(
1574
+
did: Did,
1575
+
password: string,
1576
+
deleteToken: string,
1577
+
): Promise<Result<void, ApiError>> {
1578
+
return xrpcResult<void>("com.atproto.server.deleteAccount", {
1579
+
method: "POST",
1463
1580
body: { did, password, token: deleteToken },
1464
-
})
1581
+
});
1465
1582
},
1466
1583
1467
1584
updateDidDocument(
1468
1585
token: AccessToken,
1469
1586
params: {
1470
-
verificationMethods?: VerificationMethod[]
1471
-
alsoKnownAs?: string[]
1472
-
serviceEndpoint?: string
1473
-
}
1587
+
verificationMethods?: VerificationMethod[];
1588
+
alsoKnownAs?: string[];
1589
+
serviceEndpoint?: string;
1590
+
},
1474
1591
): Promise<Result<SuccessResponse, ApiError>> {
1475
-
return xrpcResult('_account.updateDidDocument', {
1476
-
method: 'POST',
1592
+
return xrpcResult("_account.updateDidDocument", {
1593
+
method: "POST",
1477
1594
token,
1478
1595
body: params,
1479
-
})
1596
+
});
1480
1597
},
1481
1598
1482
-
deactivateAccount(token: AccessToken, deleteAfter?: string): Promise<Result<void, ApiError>> {
1483
-
return xrpcResult<void>('com.atproto.server.deactivateAccount', {
1484
-
method: 'POST',
1599
+
deactivateAccount(
1600
+
token: AccessToken,
1601
+
deleteAfter?: string,
1602
+
): Promise<Result<void, ApiError>> {
1603
+
return xrpcResult<void>("com.atproto.server.deactivateAccount", {
1604
+
method: "POST",
1485
1605
token,
1486
1606
body: { deleteAfter },
1487
-
})
1607
+
});
1488
1608
},
1489
1609
1490
1610
activateAccount(token: AccessToken): Promise<Result<void, ApiError>> {
1491
-
return xrpcResult<void>('com.atproto.server.activateAccount', {
1492
-
method: 'POST',
1611
+
return xrpcResult<void>("com.atproto.server.activateAccount", {
1612
+
method: "POST",
1493
1613
token,
1494
-
})
1614
+
});
1495
1615
},
1496
1616
1497
-
setBackupEnabled(token: AccessToken, enabled: boolean): Promise<Result<SetBackupEnabledResponse, ApiError>> {
1498
-
return xrpcResult('_backup.setEnabled', {
1499
-
method: 'POST',
1617
+
setBackupEnabled(
1618
+
token: AccessToken,
1619
+
enabled: boolean,
1620
+
): Promise<Result<SetBackupEnabledResponse, ApiError>> {
1621
+
return xrpcResult("_backup.setEnabled", {
1622
+
method: "POST",
1500
1623
token,
1501
1624
body: { enabled },
1502
-
})
1625
+
});
1503
1626
},
1504
1627
1505
-
deleteBackup(token: AccessToken, id: string): Promise<Result<void, ApiError>> {
1506
-
return xrpcResult<void>('_backup.deleteBackup', {
1507
-
method: 'POST',
1628
+
deleteBackup(
1629
+
token: AccessToken,
1630
+
id: string,
1631
+
): Promise<Result<void, ApiError>> {
1632
+
return xrpcResult<void>("_backup.deleteBackup", {
1633
+
method: "POST",
1508
1634
token,
1509
1635
params: { id },
1510
-
})
1636
+
});
1511
1637
},
1512
1638
1513
1639
createRecord(
···
1515
1641
repo: Did,
1516
1642
collection: Nsid,
1517
1643
record: unknown,
1518
-
rkey?: Rkey
1644
+
rkey?: Rkey,
1519
1645
): Promise<Result<CreateRecordResponse, ApiError>> {
1520
-
return xrpcResult('com.atproto.repo.createRecord', {
1521
-
method: 'POST',
1646
+
return xrpcResult("com.atproto.repo.createRecord", {
1647
+
method: "POST",
1522
1648
token,
1523
1649
body: { repo, collection, record, rkey },
1524
-
})
1650
+
});
1525
1651
},
1526
1652
1527
1653
putRecord(
···
1529
1655
repo: Did,
1530
1656
collection: Nsid,
1531
1657
rkey: Rkey,
1532
-
record: unknown
1658
+
record: unknown,
1533
1659
): Promise<Result<CreateRecordResponse, ApiError>> {
1534
-
return xrpcResult('com.atproto.repo.putRecord', {
1535
-
method: 'POST',
1660
+
return xrpcResult("com.atproto.repo.putRecord", {
1661
+
method: "POST",
1536
1662
token,
1537
1663
body: { repo, collection, rkey, record },
1538
-
})
1664
+
});
1539
1665
},
1540
1666
1541
1667
getInviteCodes(
1542
1668
token: AccessToken,
1543
-
options?: { sort?: 'recent' | 'usage'; cursor?: string; limit?: number }
1669
+
options?: { sort?: "recent" | "usage"; cursor?: string; limit?: number },
1544
1670
): Promise<Result<GetInviteCodesResponse, ApiError>> {
1545
-
const params: Record<string, string> = {}
1546
-
if (options?.sort) params.sort = options.sort
1547
-
if (options?.cursor) params.cursor = options.cursor
1548
-
if (options?.limit) params.limit = String(options.limit)
1549
-
return xrpcResult('com.atproto.admin.getInviteCodes', { token, params })
1671
+
const params: Record<string, string> = {};
1672
+
if (options?.sort) params.sort = options.sort;
1673
+
if (options?.cursor) params.cursor = options.cursor;
1674
+
if (options?.limit) params.limit = String(options.limit);
1675
+
return xrpcResult("com.atproto.admin.getInviteCodes", { token, params });
1550
1676
},
1551
1677
1552
-
disableAccountInvites(token: AccessToken, account: Did): Promise<Result<void, ApiError>> {
1553
-
return xrpcResult<void>('com.atproto.admin.disableAccountInvites', {
1554
-
method: 'POST',
1678
+
disableAccountInvites(
1679
+
token: AccessToken,
1680
+
account: Did,
1681
+
): Promise<Result<void, ApiError>> {
1682
+
return xrpcResult<void>("com.atproto.admin.disableAccountInvites", {
1683
+
method: "POST",
1555
1684
token,
1556
1685
body: { account },
1557
-
})
1686
+
});
1558
1687
},
1559
1688
1560
-
enableAccountInvites(token: AccessToken, account: Did): Promise<Result<void, ApiError>> {
1561
-
return xrpcResult<void>('com.atproto.admin.enableAccountInvites', {
1562
-
method: 'POST',
1689
+
enableAccountInvites(
1690
+
token: AccessToken,
1691
+
account: Did,
1692
+
): Promise<Result<void, ApiError>> {
1693
+
return xrpcResult<void>("com.atproto.admin.enableAccountInvites", {
1694
+
method: "POST",
1563
1695
token,
1564
1696
body: { account },
1565
-
})
1697
+
});
1566
1698
},
1567
1699
1568
-
adminDeleteAccount(token: AccessToken, did: Did): Promise<Result<void, ApiError>> {
1569
-
return xrpcResult<void>('com.atproto.admin.deleteAccount', {
1570
-
method: 'POST',
1700
+
adminDeleteAccount(
1701
+
token: AccessToken,
1702
+
did: Did,
1703
+
): Promise<Result<void, ApiError>> {
1704
+
return xrpcResult<void>("com.atproto.admin.deleteAccount", {
1705
+
method: "POST",
1571
1706
token,
1572
1707
body: { did },
1573
-
})
1708
+
});
1574
1709
},
1575
1710
1576
1711
startPasskeyRegistration(
1577
1712
token: AccessToken,
1578
-
friendlyName?: string
1713
+
friendlyName?: string,
1579
1714
): Promise<Result<StartPasskeyRegistrationResponse, ApiError>> {
1580
-
return xrpcResult('com.atproto.server.startPasskeyRegistration', {
1581
-
method: 'POST',
1715
+
return xrpcResult("com.atproto.server.startPasskeyRegistration", {
1716
+
method: "POST",
1582
1717
token,
1583
1718
body: { friendlyName },
1584
-
})
1719
+
});
1585
1720
},
1586
1721
1587
1722
finishPasskeyRegistration(
1588
1723
token: AccessToken,
1589
1724
credential: unknown,
1590
-
friendlyName?: string
1725
+
friendlyName?: string,
1591
1726
): Promise<Result<FinishPasskeyRegistrationResponse, ApiError>> {
1592
-
return xrpcResult('com.atproto.server.finishPasskeyRegistration', {
1593
-
method: 'POST',
1727
+
return xrpcResult("com.atproto.server.finishPasskeyRegistration", {
1728
+
method: "POST",
1594
1729
token,
1595
1730
body: { credential, friendlyName },
1596
-
})
1731
+
});
1597
1732
},
1598
1733
1599
1734
updatePasskey(
1600
1735
token: AccessToken,
1601
1736
id: string,
1602
-
friendlyName: string
1737
+
friendlyName: string,
1603
1738
): Promise<Result<void, ApiError>> {
1604
-
return xrpcResult<void>('com.atproto.server.updatePasskey', {
1605
-
method: 'POST',
1739
+
return xrpcResult<void>("com.atproto.server.updatePasskey", {
1740
+
method: "POST",
1606
1741
token,
1607
1742
body: { id, friendlyName },
1608
-
})
1743
+
});
1609
1744
},
1610
1745
1611
1746
regenerateBackupCodes(
1612
1747
token: AccessToken,
1613
1748
password: string,
1614
-
code: string
1749
+
code: string,
1615
1750
): Promise<Result<RegenerateBackupCodesResponse, ApiError>> {
1616
-
return xrpcResult('com.atproto.server.regenerateBackupCodes', {
1617
-
method: 'POST',
1751
+
return xrpcResult("com.atproto.server.regenerateBackupCodes", {
1752
+
method: "POST",
1618
1753
token,
1619
1754
body: { password, code },
1620
-
})
1755
+
});
1621
1756
},
1622
1757
1623
-
updateLocale(token: AccessToken, preferredLocale: string): Promise<Result<UpdateLocaleResponse, ApiError>> {
1624
-
return xrpcResult('_account.updateLocale', {
1625
-
method: 'POST',
1758
+
updateLocale(
1759
+
token: AccessToken,
1760
+
preferredLocale: string,
1761
+
): Promise<Result<UpdateLocaleResponse, ApiError>> {
1762
+
return xrpcResult("_account.updateLocale", {
1763
+
method: "POST",
1626
1764
token,
1627
1765
body: { preferredLocale },
1628
-
})
1766
+
});
1629
1767
},
1630
1768
1631
1769
confirmChannelVerification(
1632
1770
token: AccessToken,
1633
1771
channel: string,
1634
1772
identifier: string,
1635
-
code: string
1773
+
code: string,
1636
1774
): Promise<Result<SuccessResponse, ApiError>> {
1637
-
return xrpcResult('_account.confirmChannelVerification', {
1638
-
method: 'POST',
1775
+
return xrpcResult("_account.confirmChannelVerification", {
1776
+
method: "POST",
1639
1777
token,
1640
1778
body: { channel, identifier, code },
1641
-
})
1779
+
});
1642
1780
},
1643
1781
1644
-
removePassword(token: AccessToken): Promise<Result<SuccessResponse, ApiError>> {
1645
-
return xrpcResult('_account.removePassword', {
1646
-
method: 'POST',
1782
+
removePassword(
1783
+
token: AccessToken,
1784
+
): Promise<Result<SuccessResponse, ApiError>> {
1785
+
return xrpcResult("_account.removePassword", {
1786
+
method: "POST",
1647
1787
token,
1648
-
})
1788
+
});
1649
1789
},
1650
-
}
1790
+
};
+104
-78
frontend/src/lib/auth.svelte.ts
+104
-78
frontend/src/lib/auth.svelte.ts
···
1
1
import {
2
2
api,
3
3
ApiError,
4
-
typedApi,
5
4
type CreateAccountParams,
6
5
type CreateAccountResult,
7
-
} from "./api";
8
-
import type { Session } from "./types/api";
6
+
typedApi,
7
+
} from "./api.ts";
8
+
import type { Session } from "./types/api.ts";
9
9
import {
10
+
type AccessToken,
10
11
type Did,
11
12
type Handle,
12
-
type AccessToken,
13
13
type RefreshToken,
14
+
unsafeAsAccessToken,
14
15
unsafeAsDid,
15
16
unsafeAsHandle,
16
-
unsafeAsAccessToken,
17
17
unsafeAsRefreshToken,
18
-
} from "./types/branded";
19
-
import { type Result, ok, err, isOk, isErr, map } from "./types/result";
20
-
import { assertNever } from "./types/exhaustive";
18
+
} from "./types/branded.ts";
19
+
import { err, isErr, isOk, ok, type Result } from "./types/result.ts";
20
+
import { assertNever } from "./types/exhaustive.ts";
21
21
import {
22
22
checkForOAuthCallback,
23
23
clearOAuthCallbackParams,
24
24
handleOAuthCallback,
25
25
refreshOAuthToken,
26
26
startOAuthLogin,
27
-
} from "./oauth";
28
-
import { setLocale, type SupportedLocale } from "./i18n";
27
+
} from "./oauth.ts";
28
+
import { setLocale, type SupportedLocale } from "./i18n.ts";
29
29
30
30
const STORAGE_KEY = "tranquil_pds_session";
31
31
const ACCOUNTS_KEY = "tranquil_pds_accounts";
···
64
64
65
65
export type AuthState =
66
66
| {
67
-
readonly kind: "unauthenticated";
68
-
readonly savedAccounts: readonly SavedAccount[];
69
-
}
67
+
readonly kind: "unauthenticated";
68
+
readonly savedAccounts: readonly SavedAccount[];
69
+
}
70
70
| {
71
-
readonly kind: "loading";
72
-
readonly savedAccounts: readonly SavedAccount[];
73
-
readonly previousSession: Session | null;
74
-
}
71
+
readonly kind: "loading";
72
+
readonly savedAccounts: readonly SavedAccount[];
73
+
readonly previousSession: Session | null;
74
+
}
75
75
| {
76
-
readonly kind: "authenticated";
77
-
readonly session: Session;
78
-
readonly savedAccounts: readonly SavedAccount[];
79
-
}
76
+
readonly kind: "authenticated";
77
+
readonly session: Session;
78
+
readonly savedAccounts: readonly SavedAccount[];
79
+
}
80
80
| {
81
-
readonly kind: "error";
82
-
readonly error: AuthError;
83
-
readonly savedAccounts: readonly SavedAccount[];
84
-
};
81
+
readonly kind: "error";
82
+
readonly error: AuthError;
83
+
readonly savedAccounts: readonly SavedAccount[];
84
+
};
85
85
86
86
function createUnauthenticated(
87
87
savedAccounts: readonly SavedAccount[],
···
170
170
}
171
171
const accounts: SavedAccount[] = parsed
172
172
.filter(
173
-
(a): a is { did: string; handle: string; accessJwt: string; refreshJwt: string } =>
173
+
(
174
+
a,
175
+
): a is {
176
+
did: string;
177
+
handle: string;
178
+
accessJwt: string;
179
+
refreshJwt: string;
180
+
} =>
174
181
typeof a === "object" &&
175
182
a !== null &&
176
183
typeof a.did === "string" &&
···
272
279
const currentSession = state.current.session;
273
280
try {
274
281
const tokens = await refreshOAuthToken(currentSession.refreshJwt);
275
-
const sessionInfo = await api.getSession(tokens.access_token);
282
+
const sessionInfo = await api.getSession(
283
+
unsafeAsAccessToken(tokens.access_token),
284
+
);
276
285
const session: Session = {
277
286
...sessionInfo,
278
-
accessJwt: tokens.access_token,
279
-
refreshJwt: tokens.refresh_token || currentSession.refreshJwt,
287
+
accessJwt: unsafeAsAccessToken(tokens.access_token),
288
+
refreshJwt: tokens.refresh_token
289
+
? unsafeAsRefreshToken(tokens.refresh_token)
290
+
: currentSession.refreshJwt,
280
291
};
281
292
setAuthenticated(session);
282
293
return session.accessJwt;
···
285
296
}
286
297
}
287
298
288
-
import { setTokenRefreshCallback } from "./api";
299
+
import { setTokenRefreshCallback } from "./api.ts";
289
300
290
301
export async function initAuth(): Promise<{ oauthLoginCompleted: boolean }> {
291
302
setTokenRefreshCallback(tryRefreshToken);
···
300
311
oauthCallback.code,
301
312
oauthCallback.state,
302
313
);
303
-
const sessionInfo = await api.getSession(tokens.access_token);
314
+
const sessionInfo = await api.getSession(
315
+
unsafeAsAccessToken(tokens.access_token),
316
+
);
304
317
const session: Session = {
305
318
...sessionInfo,
306
-
accessJwt: tokens.access_token,
307
-
refreshJwt: tokens.refresh_token || "",
319
+
accessJwt: unsafeAsAccessToken(tokens.access_token),
320
+
refreshJwt: unsafeAsRefreshToken(tokens.refresh_token || ""),
308
321
};
309
322
setAuthenticated(session);
310
-
applyLocaleFromSession(sessionInfo);
323
+
applyLocaleFromSession(session);
311
324
return { oauthLoginCompleted: true };
312
325
} catch (e) {
313
-
setError({ type: "oauth", message: e instanceof Error ? e.message : "OAuth login failed" });
326
+
setError({
327
+
type: "oauth",
328
+
message: e instanceof Error ? e.message : "OAuth login failed",
329
+
});
314
330
return { oauthLoginCompleted: false };
315
331
}
316
332
}
···
318
334
const stored = loadSessionFromStorage();
319
335
if (stored) {
320
336
try {
321
-
const sessionInfo = await api.getSession(stored.accessJwt);
337
+
const sessionInfo = await api.getSession(
338
+
unsafeAsAccessToken(stored.accessJwt),
339
+
);
322
340
const session: Session = {
323
341
...sessionInfo,
324
-
accessJwt: stored.accessJwt,
325
-
refreshJwt: stored.refreshJwt,
342
+
accessJwt: unsafeAsAccessToken(stored.accessJwt),
343
+
refreshJwt: unsafeAsRefreshToken(stored.refreshJwt),
326
344
};
327
345
setAuthenticated(session);
328
-
applyLocaleFromSession(sessionInfo);
346
+
applyLocaleFromSession(session);
329
347
} catch (e) {
330
348
if (e instanceof ApiError && e.status === 401) {
331
349
try {
332
350
const tokens = await refreshOAuthToken(stored.refreshJwt);
333
-
const sessionInfo = await api.getSession(tokens.access_token);
351
+
const sessionInfo = await api.getSession(
352
+
unsafeAsAccessToken(tokens.access_token),
353
+
);
334
354
const session: Session = {
335
355
...sessionInfo,
336
-
accessJwt: tokens.access_token,
337
-
refreshJwt: tokens.refresh_token || stored.refreshJwt,
356
+
accessJwt: unsafeAsAccessToken(tokens.access_token),
357
+
refreshJwt: tokens.refresh_token
358
+
? unsafeAsRefreshToken(tokens.refresh_token)
359
+
: unsafeAsRefreshToken(stored.refreshJwt),
338
360
};
339
361
setAuthenticated(session);
340
-
applyLocaleFromSession(sessionInfo);
362
+
applyLocaleFromSession(session);
341
363
} catch (refreshError) {
342
364
console.error("Token refresh failed during init:", refreshError);
343
365
setUnauthenticated();
···
359
381
password: string,
360
382
): Promise<Result<Session, AuthError>> {
361
383
const currentState = state.current;
362
-
const previousSession =
363
-
currentState.kind === "authenticated" ? currentState.session : null;
384
+
const previousSession = currentState.kind === "authenticated"
385
+
? currentState.session
386
+
: null;
364
387
setLoading(previousSession);
365
388
366
389
const result = await typedApi.createSession(identifier, password);
···
398
421
}
399
422
400
423
export async function confirmSignup(
401
-
did: string,
424
+
did: Did,
402
425
verificationCode: string,
403
426
): Promise<Result<Session, AuthError>> {
404
427
setLoading();
405
428
try {
406
429
const result = await api.confirmSignup(did, verificationCode);
407
-
const session: Session = {
408
-
did: result.did,
409
-
handle: result.handle,
410
-
accessJwt: result.accessJwt,
411
-
refreshJwt: result.refreshJwt,
412
-
email: result.email,
413
-
emailConfirmed: result.emailConfirmed,
414
-
preferredChannel: result.preferredChannel,
415
-
preferredChannelVerified: result.preferredChannelVerified,
416
-
};
417
-
setAuthenticated(session);
418
-
return ok(session);
430
+
setAuthenticated(result);
431
+
return ok(result);
419
432
} catch (e) {
420
433
const error = toAuthError(e);
421
434
setError(error);
···
424
437
}
425
438
426
439
export async function resendVerification(
427
-
did: string,
440
+
did: Did,
428
441
): Promise<Result<void, AuthError>> {
429
442
try {
430
443
await api.resendVerification(did);
···
441
454
refreshJwt: string;
442
455
}): void {
443
456
const newSession: Session = {
444
-
did: session.did,
445
-
handle: session.handle,
446
-
accessJwt: session.accessJwt,
447
-
refreshJwt: session.refreshJwt,
457
+
did: unsafeAsDid(session.did),
458
+
handle: unsafeAsHandle(session.handle),
459
+
accessJwt: unsafeAsAccessToken(session.accessJwt),
460
+
refreshJwt: unsafeAsRefreshToken(session.refreshJwt),
448
461
};
449
462
setAuthenticated(newSession);
450
463
}
···
483
496
setLoading();
484
497
485
498
try {
486
-
const sessionInfo = await api.getSession(account.accessJwt as string);
499
+
const sessionInfo = await api.getSession(account.accessJwt);
487
500
const session: Session = {
488
501
...sessionInfo,
489
-
accessJwt: account.accessJwt as string,
490
-
refreshJwt: account.refreshJwt as string,
502
+
accessJwt: account.accessJwt,
503
+
refreshJwt: account.refreshJwt,
491
504
};
492
505
setAuthenticated(session);
493
506
return ok(session);
494
507
} catch (e) {
495
508
if (e instanceof ApiError && e.status === 401) {
496
509
try {
497
-
const tokens = await refreshOAuthToken(account.refreshJwt as string);
498
-
const sessionInfo = await api.getSession(tokens.access_token);
510
+
const tokens = await refreshOAuthToken(account.refreshJwt);
511
+
const sessionInfo = await api.getSession(
512
+
unsafeAsAccessToken(tokens.access_token),
513
+
);
499
514
const session: Session = {
500
515
...sessionInfo,
501
-
accessJwt: tokens.access_token,
502
-
refreshJwt: tokens.refresh_token || (account.refreshJwt as string),
516
+
accessJwt: unsafeAsAccessToken(tokens.access_token),
517
+
refreshJwt: tokens.refresh_token
518
+
? unsafeAsRefreshToken(tokens.refresh_token)
519
+
: account.refreshJwt,
503
520
};
504
521
setAuthenticated(session);
505
522
return ok(session);
···
555
572
556
573
export function getToken(): AccessToken | null {
557
574
if (state.current.kind === "authenticated") {
558
-
return unsafeAsAccessToken(state.current.session.accessJwt);
575
+
return state.current.session.accessJwt;
559
576
}
560
577
return null;
561
578
}
···
565
582
const currentSession = state.current.session;
566
583
try {
567
584
await api.getSession(currentSession.accessJwt);
568
-
return unsafeAsAccessToken(currentSession.accessJwt);
585
+
return currentSession.accessJwt;
569
586
} catch (e) {
570
587
if (e instanceof ApiError && e.status === 401) {
571
588
try {
572
589
const tokens = await refreshOAuthToken(currentSession.refreshJwt);
573
-
const sessionInfo = await api.getSession(tokens.access_token);
590
+
const sessionInfo = await api.getSession(
591
+
unsafeAsAccessToken(tokens.access_token),
592
+
);
574
593
const session: Session = {
575
594
...sessionInfo,
576
-
accessJwt: tokens.access_token,
577
-
refreshJwt: tokens.refresh_token || currentSession.refreshJwt,
595
+
accessJwt: unsafeAsAccessToken(tokens.access_token),
596
+
refreshJwt: tokens.refresh_token
597
+
? unsafeAsRefreshToken(tokens.refresh_token)
598
+
: currentSession.refreshJwt,
578
599
};
579
600
setAuthenticated(session);
580
-
return unsafeAsAccessToken(session.accessJwt);
601
+
return session.accessJwt;
581
602
} catch {
582
603
return null;
583
604
}
···
604
625
605
626
export function matchAuthState<T>(handlers: {
606
627
unauthenticated: (accounts: readonly SavedAccount[]) => T;
607
-
loading: (accounts: readonly SavedAccount[], previousSession: Session | null) => T;
628
+
loading: (
629
+
accounts: readonly SavedAccount[],
630
+
previousSession: Session | null,
631
+
) => T;
608
632
authenticated: (session: Session, accounts: readonly SavedAccount[]) => T;
609
633
error: (error: AuthError, accounts: readonly SavedAccount[]) => T;
610
634
}): T {
···
633
657
if (newState.loading) {
634
658
setState(createLoading(accounts, newState.session));
635
659
} else if (newState.error) {
636
-
setState(createError({ type: "unknown", message: newState.error }, accounts));
660
+
setState(
661
+
createError({ type: "unknown", message: newState.error }, accounts),
662
+
);
637
663
} else if (newState.session) {
638
664
setState(createAuthenticated(newState.session, accounts));
639
665
} else {
+7
-4
frontend/src/lib/crypto.ts
+7
-4
frontend/src/lib/crypto.ts
···
11
11
}
12
12
13
13
export function generateKeypair(): Keypair {
14
-
const privateKey = secp.utils.randomPrivateKey();
14
+
const privateKey = secp.utils.randomSecretKey();
15
15
const publicKey = secp.getPublicKey(privateKey, true);
16
16
17
17
const multicodecKey = new Uint8Array(
···
35
35
const bytes = typeof data === "string"
36
36
? new TextEncoder().encode(data)
37
37
: data;
38
-
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')
38
+
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join(
39
+
"",
40
+
);
39
41
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
40
42
}
41
43
···
67
69
const msgBytes = new TextEncoder().encode(message);
68
70
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBytes);
69
71
const msgHash = new Uint8Array(hashBuffer);
70
-
const signature = await secp.signAsync(msgHash, privateKey);
71
-
const sigBytes = signature.toCompactRawBytes();
72
+
const sigBytes = await secp.signAsync(msgHash, privateKey, {
73
+
prehash: false,
74
+
});
72
75
const signatureEncoded = base64UrlEncode(sigBytes);
73
76
74
77
return `${message}.${signatureEncoded}`;
+11
-6
frontend/src/lib/migration/atproto-client.ts
+11
-6
frontend/src/lib/migration/atproto-client.ts
···
14
14
ServerDescription,
15
15
Session,
16
16
StartPasskeyRegistrationResponse,
17
-
} from "./types";
17
+
} from "./types.ts";
18
18
19
19
function apiLog(
20
20
method: string,
···
101
101
let requestBody: BodyInit | undefined;
102
102
if (rawBody) {
103
103
headers["Content-Type"] = contentType ?? "application/octet-stream";
104
-
requestBody = rawBody;
104
+
requestBody = rawBody as BodyInit;
105
105
} else if (body) {
106
106
headers["Content-Type"] = "application/json";
107
107
requestBody = JSON.stringify(body);
···
231
231
did: string,
232
232
cid: string,
233
233
): Promise<{ data: Uint8Array; contentType: string }> {
234
-
const url = `${this.baseUrl}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`;
234
+
const url = `${this.baseUrl}/xrpc/com.atproto.sync.getBlob?did=${
235
+
encodeURIComponent(did)
236
+
}&cid=${encodeURIComponent(cid)}`;
235
237
const headers: Record<string, string> = {};
236
238
if (this.accessToken) {
237
239
headers["Authorization"] = `Bearer ${this.accessToken}`;
···
244
246
}));
245
247
throw new Error(err.message || err.error || res.statusText);
246
248
}
247
-
const contentType = res.headers.get("content-type") || "application/octet-stream";
249
+
const contentType = res.headers.get("content-type") ||
250
+
"application/octet-stream";
248
251
const data = new Uint8Array(await res.arrayBuffer());
249
252
return { data, contentType };
250
253
}
···
600
603
601
604
export function base64UrlEncode(buffer: Uint8Array | ArrayBuffer): string {
602
605
const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
603
-
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')
606
+
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join(
607
+
"",
608
+
);
604
609
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(
605
610
/=+$/,
606
611
"",
···
632
637
id: base64UrlDecode(cred.id as string),
633
638
}),
634
639
),
635
-
} as PublicKeyCredentialCreationOptions;
640
+
} as unknown as PublicKeyCredentialCreationOptions;
636
641
}
637
642
638
643
async function computeAccessTokenHash(accessToken: string): Promise<string> {
+10
-4
frontend/src/lib/migration/blob-migration.ts
+10
-4
frontend/src/lib/migration/blob-migration.ts
···
1
-
import type { AtprotoClient } from "./atproto-client";
2
-
import type { MigrationProgress } from "./types";
1
+
import type { AtprotoClient } from "./atproto-client.ts";
2
+
import type { MigrationProgress } from "./types.ts";
3
3
4
4
export interface BlobMigrationResult {
5
5
migrated: number;
···
85
85
});
86
86
87
87
console.log("[blob-migration] Fetching blob", cid, "from source");
88
-
const { data: blobData, contentType } = await sourceClient.getBlobWithContentType(userDid, cid);
88
+
const { data: blobData, contentType } = await sourceClient
89
+
.getBlobWithContentType(userDid, cid);
89
90
console.log(
90
91
"[blob-migration] Got blob",
91
92
cid,
···
95
96
contentType,
96
97
);
97
98
await localClient.uploadBlob(blobData, contentType);
98
-
console.log("[blob-migration] Uploaded blob", cid, "with contentType:", contentType);
99
+
console.log(
100
+
"[blob-migration] Uploaded blob",
101
+
cid,
102
+
"with contentType:",
103
+
contentType,
104
+
);
99
105
migrated++;
100
106
onProgress({ blobsMigrated: migrated });
101
107
} catch (e) {
+5
-5
frontend/src/lib/migration/flow.svelte.ts
+5
-5
frontend/src/lib/migration/flow.svelte.ts
···
5
5
PasskeyAccountSetup,
6
6
ServerDescription,
7
7
StoredMigrationState,
8
-
} from "./types";
8
+
} from "./types.ts";
9
9
import {
10
10
AtprotoClient,
11
11
clearDPoPKey,
···
21
21
loadDPoPKey,
22
22
resolvePdsUrl,
23
23
saveDPoPKey,
24
-
} from "./atproto-client";
24
+
} from "./atproto-client.ts";
25
25
import {
26
26
clearMigrationState,
27
27
saveMigrationState,
28
28
updateProgress,
29
29
updateStep,
30
-
} from "./storage";
31
-
import { migrateBlobs as migrateBlobsUtil } from "./blob-migration";
30
+
} from "./storage.ts";
31
+
import { migrateBlobs as migrateBlobsUtil } from "./blob-migration.ts";
32
32
33
33
function migrationLog(stage: string, data?: Record<string, unknown>) {
34
34
const timestamp = new Date().toISOString();
···
94
94
}
95
95
}
96
96
97
-
function setError(error: string) {
97
+
function setError(error: string | null) {
98
98
state.error = error;
99
99
saveMigrationState(state);
100
100
}
+28
-19
frontend/src/lib/migration/offline-flow.svelte.ts
+28
-19
frontend/src/lib/migration/offline-flow.svelte.ts
···
4
4
OfflineInboundMigrationState,
5
5
OfflineInboundStep,
6
6
ServerDescription,
7
-
} from "./types";
7
+
} from "./types.ts";
8
8
import {
9
9
AtprotoClient,
10
10
base64UrlEncode,
11
11
createLocalClient,
12
12
prepareWebAuthnCreationOptions,
13
-
} from "./atproto-client";
14
-
import { api } from "../api";
15
-
import { type KeypairInfo, plcOps, type PrivateKey } from "./plc-ops";
16
-
import { migrateBlobs as migrateBlobsUtil } from "./blob-migration";
13
+
} from "./atproto-client.ts";
14
+
import { api } from "../api.ts";
15
+
import { type KeypairInfo, plcOps, type PrivateKey } from "./plc-ops.ts";
16
+
import { migrateBlobs as migrateBlobsUtil } from "./blob-migration.ts";
17
17
import { Secp256k1PrivateKeyExportable } from "@atcute/crypto";
18
+
import {
19
+
unsafeAsAccessToken,
20
+
unsafeAsDid,
21
+
unsafeAsEmail,
22
+
unsafeAsHandle,
23
+
} from "../types/branded.ts";
18
24
19
25
const OFFLINE_STORAGE_KEY = "tranquil_offline_migration_state";
20
26
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
···
303
309
const createResult = await api.createAccountWithServiceAuth(
304
310
serviceAuthToken,
305
311
{
306
-
did: state.userDid,
307
-
handle: fullHandle,
308
-
email: state.targetEmail,
312
+
did: unsafeAsDid(state.userDid),
313
+
handle: unsafeAsHandle(fullHandle),
314
+
email: unsafeAsEmail(state.targetEmail),
309
315
password: state.targetPassword,
310
316
inviteCode: state.inviteCode || undefined,
311
317
},
···
326
332
: `${state.targetHandle}.${serverInfo.availableUserDomains[0]}`;
327
333
328
334
const createResult = await api.createPasskeyAccount({
329
-
did: state.userDid,
330
-
handle: fullHandle,
331
-
email: state.targetEmail,
335
+
did: unsafeAsDid(state.userDid),
336
+
handle: unsafeAsHandle(fullHandle),
337
+
email: unsafeAsEmail(state.targetEmail),
332
338
inviteCode: state.inviteCode || undefined,
333
339
}, serviceAuthToken);
334
340
···
349
355
const prevCid = base.cid;
350
356
351
357
const credentials = await api.getRecommendedDidCredentials(
352
-
state.localAccessToken,
358
+
unsafeAsAccessToken(state.localAccessToken),
353
359
);
354
360
355
361
await plcOps.signPlcOperationWithCredentials(
···
374
380
}
375
381
376
382
setProgress({ currentOperation: "Importing repository..." });
377
-
await api.importRepo(state.localAccessToken, state.carFile);
383
+
await api.importRepo(
384
+
unsafeAsAccessToken(state.localAccessToken),
385
+
state.carFile,
386
+
);
378
387
setProgress({ repoImported: true });
379
388
}
380
389
···
384
393
}
385
394
386
395
const localClient = createLocalClient();
387
-
localClient.setAccessToken(state.localAccessToken);
396
+
localClient.setAccessToken(unsafeAsAccessToken(state.localAccessToken));
388
397
389
398
if (state.oldPdsUrl) {
390
399
setProgress({
···
436
445
}
437
446
438
447
setProgress({ currentOperation: "Activating account..." });
439
-
await api.activateAccount(state.localAccessToken);
448
+
await api.activateAccount(unsafeAsAccessToken(state.localAccessToken));
440
449
setProgress({ activated: true });
441
450
}
442
451
···
445
454
setError(null);
446
455
447
456
try {
448
-
await api.verifyMigrationEmail(token, state.targetEmail);
457
+
await api.verifyMigrationEmail(token, unsafeAsEmail(state.targetEmail));
449
458
450
459
if (state.authMethod === "passkey") {
451
460
setStep("passkey-setup");
···
474
483
}
475
484
476
485
async function resendEmailVerification(): Promise<void> {
477
-
await api.resendMigrationVerification(state.targetEmail);
486
+
await api.resendMigrationVerification(unsafeAsEmail(state.targetEmail));
478
487
}
479
488
480
489
let checkingEmailVerification = false;
···
518
527
}
519
528
520
529
return api.startPasskeyRegistrationForSetup(
521
-
state.userDid,
530
+
unsafeAsDid(state.userDid),
522
531
state.passkeySetupToken,
523
532
);
524
533
}
···
560
569
};
561
570
562
571
const result = await api.completePasskeySetup(
563
-
state.userDid,
572
+
unsafeAsDid(state.userDid),
564
573
state.passkeySetupToken,
565
574
credentialData,
566
575
passkeyName,
+7
-2
frontend/src/lib/migration/plc-ops.ts
+7
-2
frontend/src/lib/migration/plc-ops.ts
···
28
28
29
29
export interface PlcOperationData {
30
30
type: "plc_operation";
31
-
prev: string;
31
+
prev: string | null;
32
32
alsoKnownAs: string[];
33
33
rotationKeys: string[];
34
34
services: Record<string, PlcService>;
···
65
65
const lastOp = logs.at(-1);
66
66
if (!lastOp) {
67
67
throw new Error("No PLC operations found for this DID");
68
+
}
69
+
if (lastOp.operation.type === "plc_tombstone") {
70
+
throw new Error("DID has been tombstoned");
68
71
}
69
72
return { lastOperation: normalizeOp(lastOp.operation), base: lastOp };
70
73
}
···
108
111
} else if (match.type === "secp256k1") {
109
112
keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes);
110
113
} else {
111
-
throw new Error(`Unsupported key type: ${match.type}`);
114
+
throw new Error(
115
+
`Unsupported key type: ${(match as { type: string }).type}`,
116
+
);
112
117
}
113
118
} else {
114
119
throw new Error(
+9
-15
frontend/src/lib/migration/storage.ts
+9
-15
frontend/src/lib/migration/storage.ts
···
2
2
MigrationDirection,
3
3
MigrationState,
4
4
StoredMigrationState,
5
-
} from "./types";
6
-
import { clearDPoPKey } from "./atproto-client";
5
+
} from "./types.ts";
6
+
import { clearDPoPKey } from "./atproto-client.ts";
7
7
8
8
const STORAGE_KEY = "tranquil_migration_state";
9
9
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
···
12
12
const storedState: StoredMigrationState = {
13
13
version: 1,
14
14
direction: state.direction,
15
-
step: state.direction === "inbound" ? state.step : state.step,
15
+
step: state.step,
16
16
startedAt: new Date().toISOString(),
17
-
sourcePdsUrl: state.direction === "inbound"
18
-
? state.sourcePdsUrl
19
-
: globalThis.location.origin,
20
-
targetPdsUrl: state.direction === "inbound"
21
-
? globalThis.location.origin
22
-
: state.targetPdsUrl,
23
-
sourceDid: state.direction === "inbound" ? state.sourceDid : "",
24
-
sourceHandle: state.direction === "inbound" ? state.sourceHandle : "",
17
+
sourcePdsUrl: state.sourcePdsUrl,
18
+
targetPdsUrl: globalThis.location.origin,
19
+
sourceDid: state.sourceDid,
20
+
sourceHandle: state.sourceHandle,
25
21
targetHandle: state.targetHandle,
26
22
targetEmail: state.targetEmail,
27
-
authMethod: state.direction === "inbound" ? state.authMethod : undefined,
28
-
passkeySetupToken: state.direction === "inbound"
29
-
? state.passkeySetupToken ?? undefined
30
-
: undefined,
23
+
authMethod: state.authMethod,
24
+
passkeySetupToken: state.passkeySetupToken ?? undefined,
31
25
progress: {
32
26
repoExported: state.progress.repoExported,
33
27
repoImported: state.progress.repoImported,
+3
-1
frontend/src/lib/oauth.ts
+3
-1
frontend/src/lib/oauth.ts
···
34
34
35
35
function base64UrlEncode(buffer: ArrayBuffer): string {
36
36
const bytes = new Uint8Array(buffer);
37
-
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')
37
+
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join(
38
+
"",
39
+
);
38
40
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(
39
41
/=+$/,
40
42
"",
+15
-8
frontend/src/lib/registration/flow.svelte.ts
+15
-8
frontend/src/lib/registration/flow.svelte.ts
···
1
-
import { api, ApiError } from "../api";
2
-
import { setSession } from "../auth.svelte";
1
+
import { api, ApiError } from "../api.ts";
2
+
import { setSession } from "../auth.svelte.ts";
3
3
import {
4
4
createServiceJwt,
5
5
generateDidDocument,
6
6
generateKeypair,
7
-
} from "../crypto";
7
+
} from "../crypto.ts";
8
+
import {
9
+
unsafeAsDid,
10
+
unsafeAsEmail,
11
+
unsafeAsHandle,
12
+
} from "../types/branded.ts";
8
13
import type {
9
14
AccountResult,
10
15
ExternalDidWebState,
···
12
17
RegistrationMode,
13
18
RegistrationStep,
14
19
SessionState,
15
-
} from "./types";
20
+
} from "./types.ts";
16
21
17
22
export interface RegistrationFlowState {
18
23
mode: RegistrationMode;
···
100
105
101
106
if (keyMode === "reserved") {
102
107
const result = await api.reserveSigningKey(
103
-
state.info.externalDid!.trim(),
108
+
unsafeAsDid(state.info.externalDid!.trim()),
104
109
);
105
110
state.externalDidWeb.reservedSigningKey = result.signingKey;
106
111
publicKeyMultibase = result.signingKey.replace("did:key:", "");
···
207
212
}
208
213
209
214
const result = await api.createPasskeyAccount({
210
-
handle: state.info.handle.trim(),
211
-
email: state.info.email?.trim() || undefined,
215
+
handle: unsafeAsHandle(state.info.handle.trim()),
216
+
email: state.info.email?.trim()
217
+
? unsafeAsEmail(state.info.email.trim())
218
+
: undefined,
212
219
inviteCode: state.info.inviteCode?.trim() || undefined,
213
220
didType: state.info.didType,
214
221
did: state.info.didType === "web-external"
215
-
? state.info.externalDid!.trim()
222
+
? unsafeAsDid(state.info.externalDid!.trim())
216
223
: undefined,
217
224
signingKey: state.info.didType === "web-external" &&
218
225
state.externalDidWeb.keyMode === "reserved"
+11
-5
frontend/src/lib/registration/types.ts
+11
-5
frontend/src/lib/registration/types.ts
···
1
-
import type { DidType, VerificationChannel } from "../api";
1
+
import type { DidType, VerificationChannel } from "../api.ts";
2
+
import type {
3
+
AccessToken,
4
+
Did,
5
+
Handle,
6
+
RefreshToken,
7
+
} from "../types/branded.ts";
2
8
3
9
export type RegistrationMode = "password" | "passkey";
4
10
···
37
43
}
38
44
39
45
export interface AccountResult {
40
-
did: string;
41
-
handle: string;
46
+
did: Did;
47
+
handle: Handle;
42
48
setupToken?: string;
43
49
appPassword?: string;
44
50
appPasswordName?: string;
45
51
}
46
52
47
53
export interface SessionState {
48
-
accessJwt: string;
49
-
refreshJwt: string;
54
+
accessJwt: AccessToken;
55
+
refreshJwt: RefreshToken;
50
56
}
+11
-7
frontend/src/lib/router.svelte.ts
+11
-7
frontend/src/lib/router.svelte.ts
···
1
1
import {
2
-
routes,
2
+
buildUrl,
3
+
isValidRoute,
4
+
parseRouteParams,
3
5
type Route,
4
6
type RouteParams,
7
+
routes,
5
8
type RoutesWithParams,
6
-
buildUrl,
7
-
parseRouteParams,
8
-
isValidRoute,
9
-
} from "./types/routes";
9
+
} from "./types/routes.ts";
10
10
11
11
const APP_BASE = "/app";
12
12
···
120
120
}
121
121
122
122
export type RouteMatch =
123
-
| { readonly matched: true; readonly route: Route; readonly params: URLSearchParams }
123
+
| {
124
+
readonly matched: true;
125
+
readonly route: Route;
126
+
readonly params: URLSearchParams;
127
+
}
124
128
| { readonly matched: false };
125
129
126
130
export function match(): RouteMatch {
···
135
139
return { matched: false };
136
140
}
137
141
138
-
export { routes, type Route, type RouteParams, type RoutesWithParams };
142
+
export { type Route, type RouteParams, routes, type RoutesWithParams };
+26
-26
frontend/src/lib/toast.svelte.ts
+26
-26
frontend/src/lib/toast.svelte.ts
···
1
-
export type ToastType = 'success' | 'error' | 'warning' | 'info'
1
+
export type ToastType = "success" | "error" | "warning" | "info";
2
2
3
3
export interface Toast {
4
-
id: number
5
-
type: ToastType
6
-
message: string
7
-
duration: number
8
-
dismissing?: boolean
4
+
id: number;
5
+
type: ToastType;
6
+
message: string;
7
+
duration: number;
8
+
dismissing?: boolean;
9
9
}
10
10
11
-
let nextId = 0
12
-
let toasts = $state<Toast[]>([])
11
+
let nextId = 0;
12
+
let toasts = $state<Toast[]>([]);
13
13
14
14
export function getToasts(): readonly Toast[] {
15
-
return toasts
15
+
return toasts;
16
16
}
17
17
18
18
export function showToast(
19
19
type: ToastType,
20
20
message: string,
21
-
duration = 5000
21
+
duration = 5000,
22
22
): number {
23
-
const id = nextId++
24
-
toasts = [...toasts, { id, type, message, duration }]
23
+
const id = nextId++;
24
+
toasts = [...toasts, { id, type, message, duration }];
25
25
26
26
if (duration > 0) {
27
27
setTimeout(() => {
28
-
dismissToast(id)
29
-
}, duration)
28
+
dismissToast(id);
29
+
}, duration);
30
30
}
31
31
32
-
return id
32
+
return id;
33
33
}
34
34
35
35
export function dismissToast(id: number): void {
36
-
const toast = toasts.find(t => t.id === id)
37
-
if (!toast || toast.dismissing) return
36
+
const toast = toasts.find((t) => t.id === id);
37
+
if (!toast || toast.dismissing) return;
38
38
39
-
toasts = toasts.map(t => t.id === id ? { ...t, dismissing: true } : t)
39
+
toasts = toasts.map((t) => t.id === id ? { ...t, dismissing: true } : t);
40
40
41
41
setTimeout(() => {
42
-
toasts = toasts.filter(t => t.id !== id)
43
-
}, 150)
42
+
toasts = toasts.filter((t) => t.id !== id);
43
+
}, 150);
44
44
}
45
45
46
46
export function clearAllToasts(): void {
47
-
toasts = []
47
+
toasts = [];
48
48
}
49
49
50
50
export function success(message: string, duration?: number): number {
51
-
return showToast('success', message, duration)
51
+
return showToast("success", message, duration);
52
52
}
53
53
54
54
export function error(message: string, duration?: number): number {
55
-
return showToast('error', message, duration)
55
+
return showToast("error", message, duration);
56
56
}
57
57
58
58
export function warning(message: string, duration?: number): number {
59
-
return showToast('warning', message, duration)
59
+
return showToast("warning", message, duration);
60
60
}
61
61
62
62
export function info(message: string, duration?: number): number {
63
-
return showToast('info', message, duration)
63
+
return showToast("info", message, duration);
64
64
}
65
65
66
66
export const toast = {
···
71
71
info,
72
72
dismiss: dismissToast,
73
73
clear: clearAllToasts,
74
-
}
74
+
};
+272
-263
frontend/src/lib/types/api.ts
+272
-263
frontend/src/lib/types/api.ts
···
1
1
import type {
2
-
Did,
3
-
Handle,
4
2
AccessToken,
5
-
RefreshToken,
3
+
AtUri,
6
4
Cid,
7
-
Rkey,
8
-
AtUri,
9
-
Nsid,
10
-
ISODateString,
5
+
Did,
11
6
EmailAddress,
7
+
Handle,
12
8
InviteCode as InviteCodeBrand,
9
+
ISODateString,
10
+
Nsid,
13
11
PublicKeyMultibase,
14
-
} from './branded'
12
+
RefreshToken,
13
+
} from "./branded.ts";
15
14
16
15
export type ApiErrorCode =
17
-
| 'InvalidRequest'
18
-
| 'AuthenticationRequired'
19
-
| 'ExpiredToken'
20
-
| 'InvalidToken'
21
-
| 'AccountNotFound'
22
-
| 'HandleNotAvailable'
23
-
| 'InvalidHandle'
24
-
| 'InvalidPassword'
25
-
| 'RateLimitExceeded'
26
-
| 'InternalServerError'
27
-
| 'AccountTakedown'
28
-
| 'AccountDeactivated'
29
-
| 'AccountNotVerified'
30
-
| 'RepoNotFound'
31
-
| 'RecordNotFound'
32
-
| 'BlobNotFound'
33
-
| 'InvalidInviteCode'
34
-
| 'DuplicateCreate'
35
-
| 'Unknown'
16
+
| "InvalidRequest"
17
+
| "AuthenticationRequired"
18
+
| "ExpiredToken"
19
+
| "InvalidToken"
20
+
| "AccountNotFound"
21
+
| "HandleNotAvailable"
22
+
| "InvalidHandle"
23
+
| "InvalidPassword"
24
+
| "RateLimitExceeded"
25
+
| "InternalServerError"
26
+
| "AccountTakedown"
27
+
| "AccountDeactivated"
28
+
| "AccountNotVerified"
29
+
| "RepoNotFound"
30
+
| "RecordNotFound"
31
+
| "BlobNotFound"
32
+
| "InvalidInviteCode"
33
+
| "DuplicateCreate"
34
+
| "ReauthRequired"
35
+
| "MfaVerificationRequired"
36
+
| "RecoveryLinkExpired"
37
+
| "InvalidRecoveryLink"
38
+
| "Unknown";
36
39
37
-
export type AccountStatus = 'active' | 'deactivated' | 'migrated' | 'suspended' | 'deleted'
40
+
export type AccountStatus =
41
+
| "active"
42
+
| "deactivated"
43
+
| "migrated"
44
+
| "suspended"
45
+
| "deleted";
38
46
39
-
export type SessionType = 'oauth' | 'legacy' | 'app_password'
47
+
export type SessionType = "oauth" | "legacy" | "app_password";
40
48
41
-
export type VerificationChannel = 'email' | 'discord' | 'telegram' | 'signal'
49
+
export type VerificationChannel = "email" | "discord" | "telegram" | "signal";
42
50
43
-
export type DidType = 'plc' | 'web' | 'web-external'
51
+
export type DidType = "plc" | "web" | "web-external";
44
52
45
-
export type ReauthMethod = 'password' | 'totp' | 'passkey'
53
+
export type ReauthMethod = "password" | "totp" | "passkey";
46
54
47
55
export interface Session {
48
-
did: Did
49
-
handle: Handle
50
-
email?: EmailAddress
51
-
emailConfirmed?: boolean
52
-
preferredChannel?: VerificationChannel
53
-
preferredChannelVerified?: boolean
54
-
isAdmin?: boolean
55
-
active?: boolean
56
-
status?: AccountStatus
57
-
migratedToPds?: string
58
-
migratedAt?: ISODateString
59
-
accessJwt: AccessToken
60
-
refreshJwt: RefreshToken
56
+
did: Did;
57
+
handle: Handle;
58
+
email?: EmailAddress;
59
+
emailConfirmed?: boolean;
60
+
preferredChannel?: VerificationChannel;
61
+
preferredChannelVerified?: boolean;
62
+
preferredLocale?: string | null;
63
+
isAdmin?: boolean;
64
+
active?: boolean;
65
+
status?: AccountStatus;
66
+
migratedToPds?: string;
67
+
migratedAt?: ISODateString;
68
+
accessJwt: AccessToken;
69
+
refreshJwt: RefreshToken;
61
70
}
62
71
63
72
export interface VerificationMethod {
64
-
id: string
65
-
type: string
66
-
controller: string
67
-
publicKeyMultibase: PublicKeyMultibase
73
+
id: string;
74
+
type: string;
75
+
controller: string;
76
+
publicKeyMultibase: PublicKeyMultibase;
68
77
}
69
78
70
79
export interface ServiceEndpoint {
71
-
id: string
72
-
type: string
73
-
serviceEndpoint: string
80
+
id: string;
81
+
type: string;
82
+
serviceEndpoint: string;
74
83
}
75
84
76
85
export interface DidDocument {
77
-
'@context': string[]
78
-
id: Did
79
-
alsoKnownAs: string[]
80
-
verificationMethod: VerificationMethod[]
81
-
service: ServiceEndpoint[]
86
+
"@context": string[];
87
+
id: Did;
88
+
alsoKnownAs: string[];
89
+
verificationMethod: VerificationMethod[];
90
+
service: ServiceEndpoint[];
82
91
}
83
92
84
93
export interface AppPassword {
85
-
name: string
86
-
createdAt: ISODateString
87
-
scopes?: string
88
-
createdByController?: string
94
+
name: string;
95
+
createdAt: ISODateString;
96
+
scopes?: string;
97
+
createdByController?: string;
89
98
}
90
99
91
100
export interface CreatedAppPassword {
92
-
name: string
93
-
password: string
94
-
createdAt: ISODateString
95
-
scopes?: string
101
+
name: string;
102
+
password: string;
103
+
createdAt: ISODateString;
104
+
scopes?: string;
96
105
}
97
106
98
107
export interface InviteCodeUse {
99
-
usedBy: Did
100
-
usedByHandle?: Handle
101
-
usedAt: ISODateString
108
+
usedBy: Did;
109
+
usedByHandle?: Handle;
110
+
usedAt: ISODateString;
102
111
}
103
112
104
113
export interface InviteCodeInfo {
105
-
code: InviteCodeBrand
106
-
available: number
107
-
disabled: boolean
108
-
forAccount: Did
109
-
createdBy: Did
110
-
createdAt: ISODateString
111
-
uses: InviteCodeUse[]
114
+
code: InviteCodeBrand;
115
+
available: number;
116
+
disabled: boolean;
117
+
forAccount: Did;
118
+
createdBy: Did;
119
+
createdAt: ISODateString;
120
+
uses: InviteCodeUse[];
112
121
}
113
122
114
123
export interface CreateAccountParams {
115
-
handle: string
116
-
email: string
117
-
password: string
118
-
inviteCode?: string
119
-
didType?: DidType
120
-
did?: string
121
-
signingKey?: string
122
-
verificationChannel?: VerificationChannel
123
-
discordId?: string
124
-
telegramUsername?: string
125
-
signalNumber?: string
124
+
handle: string;
125
+
email: string;
126
+
password: string;
127
+
inviteCode?: string;
128
+
didType?: DidType;
129
+
did?: string;
130
+
signingKey?: string;
131
+
verificationChannel?: VerificationChannel;
132
+
discordId?: string;
133
+
telegramUsername?: string;
134
+
signalNumber?: string;
126
135
}
127
136
128
137
export interface CreateAccountResult {
129
-
handle: Handle
130
-
did: Did
131
-
verificationRequired: boolean
132
-
verificationChannel: VerificationChannel
138
+
handle: Handle;
139
+
did: Did;
140
+
verificationRequired: boolean;
141
+
verificationChannel: VerificationChannel;
133
142
}
134
143
135
144
export interface ConfirmSignupResult {
136
-
accessJwt: AccessToken
137
-
refreshJwt: RefreshToken
138
-
handle: Handle
139
-
did: Did
140
-
email?: EmailAddress
141
-
emailConfirmed?: boolean
142
-
preferredChannel?: VerificationChannel
143
-
preferredChannelVerified?: boolean
145
+
accessJwt: AccessToken;
146
+
refreshJwt: RefreshToken;
147
+
handle: Handle;
148
+
did: Did;
149
+
email?: EmailAddress;
150
+
emailConfirmed?: boolean;
151
+
preferredChannel?: VerificationChannel;
152
+
preferredChannelVerified?: boolean;
144
153
}
145
154
146
155
export interface ListAppPasswordsResponse {
147
-
passwords: AppPassword[]
156
+
passwords: AppPassword[];
148
157
}
149
158
150
159
export interface AccountInviteCodesResponse {
151
-
codes: InviteCodeInfo[]
160
+
codes: InviteCodeInfo[];
152
161
}
153
162
154
163
export interface CreateInviteCodeResponse {
155
-
code: InviteCodeBrand
164
+
code: InviteCodeBrand;
156
165
}
157
166
158
167
export interface ServerLinks {
159
-
privacyPolicy?: string
160
-
termsOfService?: string
168
+
privacyPolicy?: string;
169
+
termsOfService?: string;
161
170
}
162
171
163
172
export interface ServerDescription {
164
-
availableUserDomains: string[]
165
-
inviteCodeRequired: boolean
166
-
links?: ServerLinks
167
-
version?: string
168
-
availableCommsChannels?: VerificationChannel[]
169
-
selfHostedDidWebEnabled?: boolean
173
+
availableUserDomains: string[];
174
+
inviteCodeRequired: boolean;
175
+
links?: ServerLinks;
176
+
version?: string;
177
+
availableCommsChannels?: VerificationChannel[];
178
+
selfHostedDidWebEnabled?: boolean;
170
179
}
171
180
172
181
export interface RepoInfo {
173
-
did: Did
174
-
head: Cid
175
-
rev: string
182
+
did: Did;
183
+
head: Cid;
184
+
rev: string;
176
185
}
177
186
178
187
export interface ListReposResponse {
179
-
repos: RepoInfo[]
180
-
cursor?: string
188
+
repos: RepoInfo[];
189
+
cursor?: string;
181
190
}
182
191
183
192
export interface NotificationPrefs {
184
-
preferredChannel: VerificationChannel
185
-
email: EmailAddress
186
-
discordId: string | null
187
-
discordVerified: boolean
188
-
telegramUsername: string | null
189
-
telegramVerified: boolean
190
-
signalNumber: string | null
191
-
signalVerified: boolean
193
+
preferredChannel: VerificationChannel;
194
+
email: EmailAddress;
195
+
discordId: string | null;
196
+
discordVerified: boolean;
197
+
telegramUsername: string | null;
198
+
telegramVerified: boolean;
199
+
signalNumber: string | null;
200
+
signalVerified: boolean;
192
201
}
193
202
194
203
export interface NotificationHistoryItem {
195
-
createdAt: ISODateString
196
-
channel: VerificationChannel
197
-
notificationType: string
198
-
status: string
199
-
subject: string | null
200
-
body: string
204
+
createdAt: ISODateString;
205
+
channel: VerificationChannel;
206
+
notificationType: string;
207
+
status: string;
208
+
subject: string | null;
209
+
body: string;
201
210
}
202
211
203
212
export interface NotificationHistoryResponse {
204
-
notifications: NotificationHistoryItem[]
213
+
notifications: NotificationHistoryItem[];
205
214
}
206
215
207
216
export interface ServerStats {
208
-
userCount: number
209
-
repoCount: number
210
-
recordCount: number
211
-
blobStorageBytes: number
217
+
userCount: number;
218
+
repoCount: number;
219
+
recordCount: number;
220
+
blobStorageBytes: number;
212
221
}
213
222
214
223
export interface ServerConfig {
215
-
serverName: string
216
-
primaryColor: string | null
217
-
primaryColorDark: string | null
218
-
secondaryColor: string | null
219
-
secondaryColorDark: string | null
220
-
logoCid: Cid | null
224
+
serverName: string;
225
+
primaryColor: string | null;
226
+
primaryColorDark: string | null;
227
+
secondaryColor: string | null;
228
+
secondaryColorDark: string | null;
229
+
logoCid: Cid | null;
221
230
}
222
231
223
232
export interface BlobRef {
224
-
$type: 'blob'
225
-
ref: { $link: Cid }
226
-
mimeType: string
227
-
size: number
233
+
$type: "blob";
234
+
ref: { $link: Cid };
235
+
mimeType: string;
236
+
size: number;
228
237
}
229
238
230
239
export interface UploadBlobResponse {
231
-
blob: BlobRef
240
+
blob: BlobRef;
232
241
}
233
242
234
243
export interface SessionInfo {
235
-
id: string
236
-
sessionType: SessionType
237
-
clientName: string | null
238
-
createdAt: ISODateString
239
-
expiresAt: ISODateString
240
-
isCurrent: boolean
244
+
id: string;
245
+
sessionType: SessionType;
246
+
clientName: string | null;
247
+
createdAt: ISODateString;
248
+
expiresAt: ISODateString;
249
+
isCurrent: boolean;
241
250
}
242
251
243
252
export interface ListSessionsResponse {
244
-
sessions: SessionInfo[]
253
+
sessions: SessionInfo[];
245
254
}
246
255
247
256
export interface RevokeAllSessionsResponse {
248
-
revokedCount: number
257
+
revokedCount: number;
249
258
}
250
259
251
260
export interface AccountSearchResult {
252
-
did: Did
253
-
handle: Handle
254
-
email?: EmailAddress
255
-
indexedAt: ISODateString
256
-
emailConfirmedAt?: ISODateString
257
-
deactivatedAt?: ISODateString
261
+
did: Did;
262
+
handle: Handle;
263
+
email?: EmailAddress;
264
+
indexedAt: ISODateString;
265
+
emailConfirmedAt?: ISODateString;
266
+
deactivatedAt?: ISODateString;
258
267
}
259
268
260
269
export interface SearchAccountsResponse {
261
-
cursor?: string
262
-
accounts: AccountSearchResult[]
270
+
cursor?: string;
271
+
accounts: AccountSearchResult[];
263
272
}
264
273
265
274
export interface AdminInviteCodeUse {
266
-
usedBy: Did
267
-
usedAt: ISODateString
275
+
usedBy: Did;
276
+
usedAt: ISODateString;
268
277
}
269
278
270
279
export interface AdminInviteCode {
271
-
code: InviteCodeBrand
272
-
available: number
273
-
disabled: boolean
274
-
forAccount: Did
275
-
createdBy: Did
276
-
createdAt: ISODateString
277
-
uses: AdminInviteCodeUse[]
280
+
code: InviteCodeBrand;
281
+
available: number;
282
+
disabled: boolean;
283
+
forAccount: Did;
284
+
createdBy: Did;
285
+
createdAt: ISODateString;
286
+
uses: AdminInviteCodeUse[];
278
287
}
279
288
280
289
export interface GetInviteCodesResponse {
281
-
cursor?: string
282
-
codes: AdminInviteCode[]
290
+
cursor?: string;
291
+
codes: AdminInviteCode[];
283
292
}
284
293
285
294
export interface AccountInfo {
286
-
did: Did
287
-
handle: Handle
288
-
email?: EmailAddress
289
-
indexedAt: ISODateString
290
-
emailConfirmedAt?: ISODateString
291
-
invitesDisabled?: boolean
292
-
deactivatedAt?: ISODateString
295
+
did: Did;
296
+
handle: Handle;
297
+
email?: EmailAddress;
298
+
indexedAt: ISODateString;
299
+
emailConfirmedAt?: ISODateString;
300
+
invitesDisabled?: boolean;
301
+
deactivatedAt?: ISODateString;
293
302
}
294
303
295
304
export interface RepoDescription {
296
-
handle: Handle
297
-
did: Did
298
-
didDoc: DidDocument
299
-
collections: Nsid[]
300
-
handleIsCorrect: boolean
305
+
handle: Handle;
306
+
did: Did;
307
+
didDoc: DidDocument;
308
+
collections: Nsid[];
309
+
handleIsCorrect: boolean;
301
310
}
302
311
303
312
export interface RecordInfo {
304
-
uri: AtUri
305
-
cid: Cid
306
-
value: unknown
313
+
uri: AtUri;
314
+
cid: Cid;
315
+
value: unknown;
307
316
}
308
317
309
318
export interface ListRecordsResponse {
310
-
records: RecordInfo[]
311
-
cursor?: string
319
+
records: RecordInfo[];
320
+
cursor?: string;
312
321
}
313
322
314
323
export interface RecordResponse {
315
-
uri: AtUri
316
-
cid: Cid
317
-
value: unknown
324
+
uri: AtUri;
325
+
cid: Cid;
326
+
value: unknown;
318
327
}
319
328
320
329
export interface CreateRecordResponse {
321
-
uri: AtUri
322
-
cid: Cid
330
+
uri: AtUri;
331
+
cid: Cid;
323
332
}
324
333
325
334
export interface TotpStatus {
326
-
enabled: boolean
327
-
hasBackupCodes: boolean
335
+
enabled: boolean;
336
+
hasBackupCodes: boolean;
328
337
}
329
338
330
339
export interface TotpSecret {
331
-
uri: string
332
-
qrBase64: string
340
+
uri: string;
341
+
qrBase64: string;
333
342
}
334
343
335
344
export interface EnableTotpResponse {
336
-
success: boolean
337
-
backupCodes: string[]
345
+
success: boolean;
346
+
backupCodes: string[];
338
347
}
339
348
340
349
export interface RegenerateBackupCodesResponse {
341
-
backupCodes: string[]
350
+
backupCodes: string[];
342
351
}
343
352
344
353
export interface PasskeyInfo {
345
-
id: string
346
-
credentialId: string
347
-
friendlyName: string | null
348
-
createdAt: ISODateString
349
-
lastUsed: ISODateString | null
354
+
id: string;
355
+
credentialId: string;
356
+
friendlyName: string | null;
357
+
createdAt: ISODateString;
358
+
lastUsed: ISODateString | null;
350
359
}
351
360
352
361
export interface ListPasskeysResponse {
353
-
passkeys: PasskeyInfo[]
362
+
passkeys: PasskeyInfo[];
354
363
}
355
364
356
365
export interface StartPasskeyRegistrationResponse {
357
-
options: PublicKeyCredentialCreationOptions
366
+
options: PublicKeyCredentialCreationOptions;
358
367
}
359
368
360
369
export interface FinishPasskeyRegistrationResponse {
361
-
id: string
362
-
credentialId: string
370
+
id: string;
371
+
credentialId: string;
363
372
}
364
373
365
374
export interface TrustedDevice {
366
-
id: string
367
-
userAgent: string | null
368
-
friendlyName: string | null
369
-
trustedAt: ISODateString | null
370
-
trustedUntil: ISODateString | null
371
-
lastSeenAt: ISODateString
375
+
id: string;
376
+
userAgent: string | null;
377
+
friendlyName: string | null;
378
+
trustedAt: ISODateString | null;
379
+
trustedUntil: ISODateString | null;
380
+
lastSeenAt: ISODateString;
372
381
}
373
382
374
383
export interface ListTrustedDevicesResponse {
375
-
devices: TrustedDevice[]
384
+
devices: TrustedDevice[];
376
385
}
377
386
378
387
export interface ReauthStatus {
379
-
requiresReauth: boolean
380
-
lastReauthAt: ISODateString | null
381
-
availableMethods: ReauthMethod[]
388
+
requiresReauth: boolean;
389
+
lastReauthAt: ISODateString | null;
390
+
availableMethods: ReauthMethod[];
382
391
}
383
392
384
393
export interface ReauthResponse {
385
-
success: boolean
386
-
reauthAt: ISODateString
394
+
success: boolean;
395
+
reauthAt: ISODateString;
387
396
}
388
397
389
398
export interface ReauthPasskeyStartResponse {
390
-
options: PublicKeyCredentialRequestOptions
399
+
options: PublicKeyCredentialRequestOptions;
391
400
}
392
401
393
402
export interface ReserveSigningKeyResponse {
394
-
signingKey: PublicKeyMultibase
403
+
signingKey: PublicKeyMultibase;
395
404
}
396
405
397
406
export interface RecommendedDidCredentials {
398
-
rotationKeys?: PublicKeyMultibase[]
399
-
alsoKnownAs?: string[]
400
-
verificationMethods?: { atproto?: PublicKeyMultibase }
401
-
services?: { atproto_pds?: { type: string; endpoint: string } }
407
+
rotationKeys?: PublicKeyMultibase[];
408
+
alsoKnownAs?: string[];
409
+
verificationMethods?: { atproto?: PublicKeyMultibase };
410
+
services?: { atproto_pds?: { type: string; endpoint: string } };
402
411
}
403
412
404
413
export interface PasskeyAccountCreateResponse {
405
-
did: Did
406
-
handle: Handle
407
-
setupToken: string
408
-
setupExpiresAt: ISODateString
414
+
did: Did;
415
+
handle: Handle;
416
+
setupToken: string;
417
+
setupExpiresAt: ISODateString;
409
418
}
410
419
411
420
export interface CompletePasskeySetupResponse {
412
-
did: Did
413
-
handle: Handle
414
-
appPassword: string
415
-
appPasswordName: string
421
+
did: Did;
422
+
handle: Handle;
423
+
appPassword: string;
424
+
appPasswordName: string;
416
425
}
417
426
418
427
export interface VerifyTokenResponse {
419
-
success: boolean
420
-
did: Did
421
-
purpose: string
422
-
channel: VerificationChannel
428
+
success: boolean;
429
+
did: Did;
430
+
purpose: string;
431
+
channel: VerificationChannel;
423
432
}
424
433
425
434
export interface BackupInfo {
426
-
id: string
427
-
repoRev: string
428
-
repoRootCid: Cid
429
-
blockCount: number
430
-
sizeBytes: number
431
-
createdAt: ISODateString
435
+
id: string;
436
+
repoRev: string;
437
+
repoRootCid: Cid;
438
+
blockCount: number;
439
+
sizeBytes: number;
440
+
createdAt: ISODateString;
432
441
}
433
442
434
443
export interface ListBackupsResponse {
435
-
backups: BackupInfo[]
436
-
backupEnabled: boolean
444
+
backups: BackupInfo[];
445
+
backupEnabled: boolean;
437
446
}
438
447
439
448
export interface CreateBackupResponse {
440
-
id: string
441
-
repoRev: string
442
-
sizeBytes: number
443
-
blockCount: number
449
+
id: string;
450
+
repoRev: string;
451
+
sizeBytes: number;
452
+
blockCount: number;
444
453
}
445
454
446
455
export interface SetBackupEnabledResponse {
447
-
enabled: boolean
456
+
enabled: boolean;
448
457
}
449
458
450
459
export interface EmailUpdateResponse {
451
-
tokenRequired: boolean
460
+
tokenRequired: boolean;
452
461
}
453
462
454
463
export interface LegacyLoginPreference {
455
-
allowLegacyLogin: boolean
456
-
hasMfa: boolean
464
+
allowLegacyLogin: boolean;
465
+
hasMfa: boolean;
457
466
}
458
467
459
468
export interface UpdateLegacyLoginResponse {
460
-
allowLegacyLogin: boolean
469
+
allowLegacyLogin: boolean;
461
470
}
462
471
463
472
export interface UpdateLocaleResponse {
464
-
preferredLocale: string
473
+
preferredLocale: string;
465
474
}
466
475
467
476
export interface PasswordStatus {
468
-
hasPassword: boolean
477
+
hasPassword: boolean;
469
478
}
470
479
471
480
export interface SuccessResponse {
472
-
success: boolean
481
+
success: boolean;
473
482
}
474
483
475
484
export interface CheckEmailVerifiedResponse {
476
-
verified: boolean
485
+
verified: boolean;
477
486
}
478
487
479
488
export interface VerifyMigrationEmailResponse {
480
-
success: boolean
481
-
did: Did
489
+
success: boolean;
490
+
did: Did;
482
491
}
483
492
484
493
export interface ResendMigrationVerificationResponse {
485
-
sent: boolean
494
+
sent: boolean;
486
495
}
+80
-73
frontend/src/lib/types/branded.ts
+80
-73
frontend/src/lib/types/branded.ts
···
1
-
declare const __brand: unique symbol
1
+
declare const __brand: unique symbol;
2
2
3
-
type Brand<T, B extends string> = T & { readonly [__brand]: B }
3
+
type Brand<T, B extends string> = T & { readonly [__brand]: B };
4
4
5
-
export type Did = Brand<string, 'Did'>
6
-
export type DidPlc = Brand<Did, 'DidPlc'>
7
-
export type DidWeb = Brand<Did, 'DidWeb'>
5
+
export type Did = Brand<string, "Did">;
6
+
export type DidPlc = Brand<Did, "DidPlc">;
7
+
export type DidWeb = Brand<Did, "DidWeb">;
8
8
9
-
export type Handle = Brand<string, 'Handle'>
10
-
export type AccessToken = Brand<string, 'AccessToken'>
11
-
export type RefreshToken = Brand<string, 'RefreshToken'>
12
-
export type ServiceToken = Brand<string, 'ServiceToken'>
13
-
export type SetupToken = Brand<string, 'SetupToken'>
9
+
export type Handle = Brand<string, "Handle">;
10
+
export type AccessToken = Brand<string, "AccessToken">;
11
+
export type RefreshToken = Brand<string, "RefreshToken">;
12
+
export type ServiceToken = Brand<string, "ServiceToken">;
13
+
export type SetupToken = Brand<string, "SetupToken">;
14
14
15
-
export type Cid = Brand<string, 'Cid'>
16
-
export type Rkey = Brand<string, 'Rkey'>
17
-
export type AtUri = Brand<string, 'AtUri'>
18
-
export type Nsid = Brand<string, 'Nsid'>
15
+
export type Cid = Brand<string, "Cid">;
16
+
export type Rkey = Brand<string, "Rkey">;
17
+
export type AtUri = Brand<string, "AtUri">;
18
+
export type Nsid = Brand<string, "Nsid">;
19
19
20
-
export type ISODateString = Brand<string, 'ISODateString'>
21
-
export type EmailAddress = Brand<string, 'EmailAddress'>
22
-
export type InviteCode = Brand<string, 'InviteCode'>
20
+
export type ISODateString = Brand<string, "ISODateString">;
21
+
export type EmailAddress = Brand<string, "EmailAddress">;
22
+
export type InviteCode = Brand<string, "InviteCode">;
23
23
24
-
export type PublicKeyMultibase = Brand<string, 'PublicKeyMultibase'>
25
-
export type DidKeyString = Brand<string, 'DidKeyString'>
24
+
export type PublicKeyMultibase = Brand<string, "PublicKeyMultibase">;
25
+
export type DidKeyString = Brand<string, "DidKeyString">;
26
26
27
-
const DID_PLC_REGEX = /^did:plc:[a-z2-7]{24}$/
28
-
const DID_WEB_REGEX = /^did:web:.+$/
29
-
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])?$/
30
-
const AT_URI_REGEX = /^at:\/\/[^/]+\/[^/]+\/[^/]+$/
31
-
const CID_REGEX = /^[a-z2-7]{59}$|^baf[a-z2-7]+$/
32
-
const NSID_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?(\.[a-z]([a-z0-9-]*[a-z0-9])?)+$/
33
-
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
34
-
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/
27
+
const DID_PLC_REGEX = /^did:plc:[a-z2-7]{24}$/;
28
+
const DID_WEB_REGEX = /^did:web:.+$/;
29
+
const HANDLE_REGEX =
30
+
/^([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])?$/;
31
+
const AT_URI_REGEX = /^at:\/\/[^/]+\/[^/]+\/[^/]+$/;
32
+
const CID_REGEX = /^[a-z2-7]{59}$|^baf[a-z2-7]+$/;
33
+
const NSID_REGEX =
34
+
/^[a-z]([a-z0-9-]*[a-z0-9])?(\.[a-z]([a-z0-9-]*[a-z0-9])?)+$/;
35
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
36
+
const ISO_DATE_REGEX =
37
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
35
38
36
39
export function isDid(s: string): s is Did {
37
-
return s.startsWith('did:plc:') || s.startsWith('did:web:')
40
+
return s.startsWith("did:plc:") || s.startsWith("did:web:");
38
41
}
39
42
40
43
export function isDidPlc(s: string): s is DidPlc {
41
-
return DID_PLC_REGEX.test(s)
44
+
return DID_PLC_REGEX.test(s);
42
45
}
43
46
44
47
export function isDidWeb(s: string): s is DidWeb {
45
-
return DID_WEB_REGEX.test(s)
48
+
return DID_WEB_REGEX.test(s);
46
49
}
47
50
48
51
export function isHandle(s: string): s is Handle {
49
-
return HANDLE_REGEX.test(s) && s.length <= 253
52
+
return HANDLE_REGEX.test(s) && s.length <= 253;
50
53
}
51
54
52
55
export function isAtUri(s: string): s is AtUri {
53
-
return AT_URI_REGEX.test(s)
56
+
return AT_URI_REGEX.test(s);
54
57
}
55
58
56
59
export function isCid(s: string): s is Cid {
57
-
return CID_REGEX.test(s)
60
+
return CID_REGEX.test(s);
58
61
}
59
62
60
63
export function isNsid(s: string): s is Nsid {
61
-
return NSID_REGEX.test(s)
64
+
return NSID_REGEX.test(s);
62
65
}
63
66
64
67
export function isEmail(s: string): s is EmailAddress {
65
-
return EMAIL_REGEX.test(s)
68
+
return EMAIL_REGEX.test(s);
66
69
}
67
70
68
71
export function isISODate(s: string): s is ISODateString {
69
-
return ISO_DATE_REGEX.test(s)
72
+
return ISO_DATE_REGEX.test(s);
70
73
}
71
74
72
75
export function asDid(s: string): Did {
73
-
if (!isDid(s)) throw new TypeError(`Invalid DID: ${s}`)
74
-
return s
76
+
if (!isDid(s)) throw new TypeError(`Invalid DID: ${s}`);
77
+
return s;
75
78
}
76
79
77
80
export function asDidPlc(s: string): DidPlc {
78
-
if (!isDidPlc(s)) throw new TypeError(`Invalid DID:PLC: ${s}`)
79
-
return s as DidPlc
81
+
if (!isDidPlc(s)) throw new TypeError(`Invalid DID:PLC: ${s}`);
82
+
return s as DidPlc;
80
83
}
81
84
82
85
export function asDidWeb(s: string): DidWeb {
83
-
if (!isDidWeb(s)) throw new TypeError(`Invalid DID:WEB: ${s}`)
84
-
return s as DidWeb
86
+
if (!isDidWeb(s)) throw new TypeError(`Invalid DID:WEB: ${s}`);
87
+
return s as DidWeb;
85
88
}
86
89
87
90
export function asHandle(s: string): Handle {
88
-
if (!isHandle(s)) throw new TypeError(`Invalid handle: ${s}`)
89
-
return s
91
+
if (!isHandle(s)) throw new TypeError(`Invalid handle: ${s}`);
92
+
return s;
90
93
}
91
94
92
95
export function asAtUri(s: string): AtUri {
93
-
if (!isAtUri(s)) throw new TypeError(`Invalid AT-URI: ${s}`)
94
-
return s
96
+
if (!isAtUri(s)) throw new TypeError(`Invalid AT-URI: ${s}`);
97
+
return s;
95
98
}
96
99
97
100
export function asCid(s: string): Cid {
98
-
if (!isCid(s)) throw new TypeError(`Invalid CID: ${s}`)
99
-
return s
101
+
if (!isCid(s)) throw new TypeError(`Invalid CID: ${s}`);
102
+
return s;
100
103
}
101
104
102
105
export function asNsid(s: string): Nsid {
103
-
if (!isNsid(s)) throw new TypeError(`Invalid NSID: ${s}`)
104
-
return s
106
+
if (!isNsid(s)) throw new TypeError(`Invalid NSID: ${s}`);
107
+
return s;
105
108
}
106
109
107
110
export function asEmail(s: string): EmailAddress {
108
-
if (!isEmail(s)) throw new TypeError(`Invalid email: ${s}`)
109
-
return s
111
+
if (!isEmail(s)) throw new TypeError(`Invalid email: ${s}`);
112
+
return s;
110
113
}
111
114
112
115
export function asISODate(s: string): ISODateString {
113
-
if (!isISODate(s)) throw new TypeError(`Invalid ISO date: ${s}`)
114
-
return s
116
+
if (!isISODate(s)) throw new TypeError(`Invalid ISO date: ${s}`);
117
+
return s;
115
118
}
116
119
117
120
export function unsafeAsDid(s: string): Did {
118
-
return s as Did
121
+
return s as Did;
119
122
}
120
123
121
124
export function unsafeAsHandle(s: string): Handle {
122
-
return s as Handle
125
+
return s as Handle;
123
126
}
124
127
125
128
export function unsafeAsAccessToken(s: string): AccessToken {
126
-
return s as AccessToken
129
+
return s as AccessToken;
127
130
}
128
131
129
132
export function unsafeAsRefreshToken(s: string): RefreshToken {
130
-
return s as RefreshToken
133
+
return s as RefreshToken;
131
134
}
132
135
133
136
export function unsafeAsServiceToken(s: string): ServiceToken {
134
-
return s as ServiceToken
137
+
return s as ServiceToken;
135
138
}
136
139
137
140
export function unsafeAsSetupToken(s: string): SetupToken {
138
-
return s as SetupToken
141
+
return s as SetupToken;
139
142
}
140
143
141
144
export function unsafeAsCid(s: string): Cid {
142
-
return s as Cid
145
+
return s as Cid;
143
146
}
144
147
145
148
export function unsafeAsRkey(s: string): Rkey {
146
-
return s as Rkey
149
+
return s as Rkey;
147
150
}
148
151
149
152
export function unsafeAsAtUri(s: string): AtUri {
150
-
return s as AtUri
153
+
return s as AtUri;
151
154
}
152
155
153
156
export function unsafeAsNsid(s: string): Nsid {
154
-
return s as Nsid
157
+
return s as Nsid;
155
158
}
156
159
157
160
export function unsafeAsISODate(s: string): ISODateString {
158
-
return s as ISODateString
161
+
return s as ISODateString;
159
162
}
160
163
164
+
export const unsafeAsISODateString = unsafeAsISODate;
165
+
161
166
export function unsafeAsEmail(s: string): EmailAddress {
162
-
return s as EmailAddress
167
+
return s as EmailAddress;
163
168
}
164
169
165
170
export function unsafeAsInviteCode(s: string): InviteCode {
166
-
return s as InviteCode
171
+
return s as InviteCode;
167
172
}
168
173
169
174
export function unsafeAsPublicKeyMultibase(s: string): PublicKeyMultibase {
170
-
return s as PublicKeyMultibase
175
+
return s as PublicKeyMultibase;
171
176
}
172
177
173
178
export function unsafeAsDidKey(s: string): DidKeyString {
174
-
return s as DidKeyString
179
+
return s as DidKeyString;
175
180
}
176
181
177
-
export function parseAtUri(uri: AtUri): { repo: Did; collection: Nsid; rkey: Rkey } {
178
-
const parts = uri.replace('at://', '').split('/')
182
+
export function parseAtUri(
183
+
uri: AtUri,
184
+
): { repo: Did; collection: Nsid; rkey: Rkey } {
185
+
const parts = uri.replace("at://", "").split("/");
179
186
return {
180
187
repo: unsafeAsDid(parts[0]),
181
188
collection: unsafeAsNsid(parts[1]),
182
189
rkey: unsafeAsRkey(parts[2]),
183
-
}
190
+
};
184
191
}
185
192
186
193
export function makeAtUri(repo: Did, collection: Nsid, rkey: Rkey): AtUri {
187
-
return `at://${repo}/${collection}/${rkey}` as AtUri
194
+
return `at://${repo}/${collection}/${rkey}` as AtUri;
188
195
}
+17
-17
frontend/src/lib/types/exhaustive.ts
+17
-17
frontend/src/lib/types/exhaustive.ts
···
1
1
export function assertNever(x: never, message?: string): never {
2
-
throw new Error(message ?? `Unexpected value: ${JSON.stringify(x)}`)
2
+
throw new Error(message ?? `Unexpected value: ${JSON.stringify(x)}`);
3
3
}
4
4
5
5
export function exhaustive<T extends string | number | symbol>(
6
6
value: T,
7
-
handlers: Record<T, () => void>
7
+
handlers: Record<T, () => void>,
8
8
): void {
9
-
const handler = handlers[value]
9
+
const handler = handlers[value];
10
10
if (handler) {
11
-
handler()
11
+
handler();
12
12
} else {
13
-
assertNever(value as never, `Unhandled case: ${String(value)}`)
13
+
assertNever(value as never, `Unhandled case: ${String(value)}`);
14
14
}
15
15
}
16
16
17
17
export function exhaustiveMap<T extends string | number | symbol, R>(
18
18
value: T,
19
-
handlers: Record<T, () => R>
19
+
handlers: Record<T, () => R>,
20
20
): R {
21
-
const handler = handlers[value]
21
+
const handler = handlers[value];
22
22
if (handler) {
23
-
return handler()
23
+
return handler();
24
24
}
25
-
return assertNever(value as never, `Unhandled case: ${String(value)}`)
25
+
return assertNever(value as never, `Unhandled case: ${String(value)}`);
26
26
}
27
27
28
28
export async function exhaustiveAsync<T extends string | number | symbol>(
29
29
value: T,
30
-
handlers: Record<T, () => Promise<void>>
30
+
handlers: Record<T, () => Promise<void>>,
31
31
): Promise<void> {
32
-
const handler = handlers[value]
32
+
const handler = handlers[value];
33
33
if (handler) {
34
-
await handler()
34
+
await handler();
35
35
} else {
36
-
assertNever(value as never, `Unhandled case: ${String(value)}`)
36
+
assertNever(value as never, `Unhandled case: ${String(value)}`);
37
37
}
38
38
}
39
39
40
40
export async function exhaustiveMapAsync<T extends string | number | symbol, R>(
41
41
value: T,
42
-
handlers: Record<T, () => Promise<R>>
42
+
handlers: Record<T, () => Promise<R>>,
43
43
): Promise<R> {
44
-
const handler = handlers[value]
44
+
const handler = handlers[value];
45
45
if (handler) {
46
-
return handler()
46
+
return handler();
47
47
}
48
-
return assertNever(value as never, `Unhandled case: ${String(value)}`)
48
+
return assertNever(value as never, `Unhandled case: ${String(value)}`);
49
49
}
+5
-5
frontend/src/lib/types/index.ts
+5
-5
frontend/src/lib/types/index.ts
···
1
-
export * from './result'
2
-
export * from './branded'
3
-
export * from './exhaustive'
4
-
export * from './api'
5
-
export * from './routes'
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";
+51
-34
frontend/src/lib/types/result.ts
+51
-34
frontend/src/lib/types/result.ts
···
1
1
export type Result<T, E = Error> =
2
2
| { ok: true; value: T }
3
-
| { ok: false; error: E }
3
+
| { ok: false; error: E };
4
4
5
5
export function ok<T>(value: T): Result<T, never> {
6
-
return { ok: true, value }
6
+
return { ok: true, value };
7
7
}
8
8
9
9
export function err<E>(error: E): Result<never, E> {
10
-
return { ok: false, error }
10
+
return { ok: false, error };
11
11
}
12
12
13
-
export function isOk<T, E>(result: Result<T, E>): result is { ok: true; value: T } {
14
-
return result.ok
13
+
export function isOk<T, E>(
14
+
result: Result<T, E>,
15
+
): result is { ok: true; value: T } {
16
+
return result.ok;
15
17
}
16
18
17
-
export function isErr<T, E>(result: Result<T, E>): result is { ok: false; error: E } {
18
-
return !result.ok
19
+
export function isErr<T, E>(
20
+
result: Result<T, E>,
21
+
): result is { ok: false; error: E } {
22
+
return !result.ok;
19
23
}
20
24
21
-
export function map<T, U, E>(result: Result<T, E>, fn: (t: T) => U): Result<U, E> {
22
-
return result.ok ? ok(fn(result.value)) : result
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;
23
30
}
24
31
25
-
export function mapErr<T, E, F>(result: Result<T, E>, fn: (e: E) => F): Result<T, F> {
26
-
return result.ok ? result : err(fn(result.error))
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));
27
37
}
28
38
29
-
export function flatMap<T, U, E>(result: Result<T, E>, fn: (t: T) => Result<U, E>): Result<U, E> {
30
-
return result.ok ? fn(result.value) : result
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;
31
44
}
32
45
33
46
export function unwrap<T, E>(result: Result<T, E>): T {
34
-
if (result.ok) return result.value
35
-
throw result.error instanceof Error ? result.error : new Error(String(result.error))
47
+
if (result.ok) return result.value;
48
+
throw result.error instanceof Error
49
+
? result.error
50
+
: new Error(String(result.error));
36
51
}
37
52
38
53
export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
39
-
return result.ok ? result.value : defaultValue
54
+
return result.ok ? result.value : defaultValue;
40
55
}
41
56
42
57
export function unwrapOrElse<T, E>(result: Result<T, E>, fn: (e: E) => T): T {
43
-
return result.ok ? result.value : fn(result.error)
58
+
return result.ok ? result.value : fn(result.error);
44
59
}
45
60
46
61
export function match<T, E, U>(
47
62
result: Result<T, E>,
48
-
handlers: { ok: (t: T) => U; err: (e: E) => U }
63
+
handlers: { ok: (t: T) => U; err: (e: E) => U },
49
64
): U {
50
-
return result.ok ? handlers.ok(result.value) : handlers.err(result.error)
65
+
return result.ok ? handlers.ok(result.value) : handlers.err(result.error);
51
66
}
52
67
53
-
export async function tryAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {
68
+
export async function tryAsync<T>(
69
+
fn: () => Promise<T>,
70
+
): Promise<Result<T, Error>> {
54
71
try {
55
-
return ok(await fn())
72
+
return ok(await fn());
56
73
} catch (e) {
57
-
return err(e instanceof Error ? e : new Error(String(e)))
74
+
return err(e instanceof Error ? e : new Error(String(e)));
58
75
}
59
76
}
60
77
61
78
export async function tryAsyncWith<T, E>(
62
79
fn: () => Promise<T>,
63
-
mapError: (e: unknown) => E
80
+
mapError: (e: unknown) => E,
64
81
): Promise<Result<T, E>> {
65
82
try {
66
-
return ok(await fn())
83
+
return ok(await fn());
67
84
} catch (e) {
68
-
return err(mapError(e))
85
+
return err(mapError(e));
69
86
}
70
87
}
71
88
72
89
export function fromNullable<T>(value: T | null | undefined): Result<T, null> {
73
-
return value != null ? ok(value) : err(null)
90
+
return value != null ? ok(value) : err(null);
74
91
}
75
92
76
93
export function toNullable<T, E>(result: Result<T, E>): T | null {
77
-
return result.ok ? result.value : null
94
+
return result.ok ? result.value : null;
78
95
}
79
96
80
97
export function collect<T, E>(results: Result<T, E>[]): Result<T[], E> {
81
-
const values: T[] = []
98
+
const values: T[] = [];
82
99
for (const result of results) {
83
-
if (!result.ok) return result
84
-
values.push(result.value)
100
+
if (!result.ok) return result;
101
+
values.push(result.value);
85
102
}
86
-
return ok(values)
103
+
return ok(values);
87
104
}
88
105
89
106
export async function collectAsync<T, E>(
90
-
results: Promise<Result<T, E>>[]
107
+
results: Promise<Result<T, E>>[],
91
108
): Promise<Result<T[], E>> {
92
-
const settled = await Promise.all(results)
93
-
return collect(settled)
109
+
const settled = await Promise.all(results);
110
+
return collect(settled);
94
111
}
+58
-58
frontend/src/lib/types/routes.ts
+58
-58
frontend/src/lib/types/routes.ts
···
1
1
export const routes = {
2
-
login: '/login',
3
-
register: '/register',
4
-
registerPasskey: '/register-passkey',
5
-
dashboard: '/dashboard',
6
-
settings: '/settings',
7
-
security: '/security',
8
-
sessions: '/sessions',
9
-
appPasswords: '/app-passwords',
10
-
trustedDevices: '/trusted-devices',
11
-
inviteCodes: '/invite-codes',
12
-
comms: '/comms',
13
-
repo: '/repo',
14
-
controllers: '/controllers',
15
-
delegationAudit: '/delegation-audit',
16
-
actAs: '/act-as',
17
-
didDocument: '/did-document',
18
-
migrate: '/migrate',
19
-
admin: '/admin',
20
-
verify: '/verify',
21
-
resetPassword: '/reset-password',
22
-
recoverPasskey: '/recover-passkey',
23
-
requestPasskeyRecovery: '/request-passkey-recovery',
24
-
oauthLogin: '/oauth/login',
25
-
oauthConsent: '/oauth/consent',
26
-
oauthAccounts: '/oauth/accounts',
27
-
oauth2fa: '/oauth/2fa',
28
-
oauthTotp: '/oauth/totp',
29
-
oauthPasskey: '/oauth/passkey',
30
-
oauthDelegation: '/oauth/delegation',
31
-
oauthError: '/oauth/error',
32
-
} as const
2
+
login: "/login",
3
+
register: "/register",
4
+
registerPasskey: "/register-passkey",
5
+
dashboard: "/dashboard",
6
+
settings: "/settings",
7
+
security: "/security",
8
+
sessions: "/sessions",
9
+
appPasswords: "/app-passwords",
10
+
trustedDevices: "/trusted-devices",
11
+
inviteCodes: "/invite-codes",
12
+
comms: "/comms",
13
+
repo: "/repo",
14
+
controllers: "/controllers",
15
+
delegationAudit: "/delegation-audit",
16
+
actAs: "/act-as",
17
+
didDocument: "/did-document",
18
+
migrate: "/migrate",
19
+
admin: "/admin",
20
+
verify: "/verify",
21
+
resetPassword: "/reset-password",
22
+
recoverPasskey: "/recover-passkey",
23
+
requestPasskeyRecovery: "/request-passkey-recovery",
24
+
oauthLogin: "/oauth/login",
25
+
oauthConsent: "/oauth/consent",
26
+
oauthAccounts: "/oauth/accounts",
27
+
oauth2fa: "/oauth/2fa",
28
+
oauthTotp: "/oauth/totp",
29
+
oauthPasskey: "/oauth/passkey",
30
+
oauthDelegation: "/oauth/delegation",
31
+
oauthError: "/oauth/error",
32
+
} as const;
33
33
34
-
export type Route = (typeof routes)[keyof typeof routes]
34
+
export type Route = (typeof routes)[keyof typeof routes];
35
35
36
-
export type RouteKey = keyof typeof routes
36
+
export type RouteKey = keyof typeof routes;
37
37
38
38
export function isValidRoute(path: string): path is Route {
39
-
return Object.values(routes).includes(path as Route)
39
+
return Object.values(routes).includes(path as Route);
40
40
}
41
41
42
42
export interface RouteParams {
43
-
[routes.verify]: { token?: string; email?: string }
44
-
[routes.resetPassword]: { token?: string }
45
-
[routes.recoverPasskey]: { token?: string; did?: string }
46
-
[routes.oauthLogin]: { request_uri?: string; error?: string }
47
-
[routes.oauthConsent]: { request_uri?: string; client_id?: string }
48
-
[routes.oauthAccounts]: { request_uri?: string }
49
-
[routes.oauth2fa]: { request_uri?: string; channel?: string }
50
-
[routes.oauthTotp]: { request_uri?: string }
51
-
[routes.oauthPasskey]: { request_uri?: string }
52
-
[routes.oauthDelegation]: { request_uri?: string; delegated_did?: string }
53
-
[routes.oauthError]: { error?: string; error_description?: string }
54
-
[routes.migrate]: { code?: string; state?: string }
43
+
[routes.verify]: { token?: string; email?: string };
44
+
[routes.resetPassword]: { token?: string };
45
+
[routes.recoverPasskey]: { token?: string; did?: string };
46
+
[routes.oauthLogin]: { request_uri?: string; error?: string };
47
+
[routes.oauthConsent]: { request_uri?: string; client_id?: string };
48
+
[routes.oauthAccounts]: { request_uri?: string };
49
+
[routes.oauth2fa]: { request_uri?: string; channel?: string };
50
+
[routes.oauthTotp]: { request_uri?: string };
51
+
[routes.oauthPasskey]: { request_uri?: string };
52
+
[routes.oauthDelegation]: { request_uri?: string; delegated_did?: string };
53
+
[routes.oauthError]: { error?: string; error_description?: string };
54
+
[routes.migrate]: { code?: string; state?: string };
55
55
}
56
56
57
-
export type RoutesWithParams = keyof RouteParams
57
+
export type RoutesWithParams = keyof RouteParams;
58
58
59
59
export function buildUrl<R extends Route>(
60
60
route: R,
61
-
params?: R extends RoutesWithParams ? RouteParams[R] : never
61
+
params?: R extends RoutesWithParams ? RouteParams[R] : never,
62
62
): string {
63
-
if (!params) return route
64
-
const searchParams = new URLSearchParams()
63
+
if (!params) return route;
64
+
const searchParams = new URLSearchParams();
65
65
for (const [key, value] of Object.entries(params)) {
66
66
if (value != null) {
67
-
searchParams.set(key, String(value))
67
+
searchParams.set(key, String(value));
68
68
}
69
69
}
70
-
const queryString = searchParams.toString()
71
-
return queryString ? `${route}?${queryString}` : route
70
+
const queryString = searchParams.toString();
71
+
return queryString ? `${route}?${queryString}` : route;
72
72
}
73
73
74
74
export function parseRouteParams<R extends RoutesWithParams>(
75
-
route: R
75
+
_route: R,
76
76
): RouteParams[R] {
77
-
const params = new URLSearchParams(globalThis.location.search)
78
-
const result: Record<string, string> = {}
77
+
const params = new URLSearchParams(globalThis.location.search);
78
+
const result: Record<string, string> = {};
79
79
for (const [key, value] of params.entries()) {
80
-
result[key] = value
80
+
result[key] = value;
81
81
}
82
-
return result as RouteParams[R]
82
+
return result as RouteParams[R];
83
83
}
+135
-110
frontend/src/lib/types/schemas.ts
+135
-110
frontend/src/lib/types/schemas.ts
···
1
-
import { z } from 'zod'
2
-
import type {
3
-
Did,
4
-
Handle,
5
-
AccessToken,
6
-
RefreshToken,
7
-
Cid,
8
-
Nsid,
9
-
AtUri,
10
-
Rkey,
11
-
ISODateString,
12
-
EmailAddress,
13
-
InviteCode,
14
-
PublicKeyMultibase,
15
-
} from './branded'
1
+
import { z } from "zod";
16
2
import {
17
-
unsafeAsDid,
18
-
unsafeAsHandle,
19
3
unsafeAsAccessToken,
20
-
unsafeAsRefreshToken,
21
-
unsafeAsCid,
22
-
unsafeAsNsid,
23
4
unsafeAsAtUri,
24
-
unsafeAsRkey,
25
-
unsafeAsISODate,
5
+
unsafeAsCid,
6
+
unsafeAsDid,
26
7
unsafeAsEmail,
8
+
unsafeAsHandle,
27
9
unsafeAsInviteCode,
10
+
unsafeAsISODate,
11
+
unsafeAsNsid,
28
12
unsafeAsPublicKeyMultibase,
29
-
} from './branded'
13
+
unsafeAsRefreshToken,
14
+
unsafeAsRkey,
15
+
} from "./branded.ts";
30
16
31
-
const did = z.string().transform((s) => unsafeAsDid(s))
32
-
const handle = z.string().transform((s) => unsafeAsHandle(s))
33
-
const accessToken = z.string().transform((s) => unsafeAsAccessToken(s))
34
-
const refreshToken = z.string().transform((s) => unsafeAsRefreshToken(s))
35
-
const cid = z.string().transform((s) => unsafeAsCid(s))
36
-
const nsid = z.string().transform((s) => unsafeAsNsid(s))
37
-
const atUri = z.string().transform((s) => unsafeAsAtUri(s))
38
-
const rkey = z.string().transform((s) => unsafeAsRkey(s))
39
-
const isoDate = z.string().transform((s) => unsafeAsISODate(s))
40
-
const email = z.string().transform((s) => unsafeAsEmail(s))
41
-
const inviteCode = z.string().transform((s) => unsafeAsInviteCode(s))
42
-
const publicKeyMultibase = z.string().transform((s) => unsafeAsPublicKeyMultibase(s))
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
+
);
43
31
44
-
export const verificationChannel = z.enum(['email', 'discord', 'telegram', 'signal'])
45
-
export const didType = z.enum(['plc', 'web', 'web-external'])
46
-
export const accountStatus = z.enum(['active', 'deactivated', 'migrated', 'suspended', 'deleted'])
47
-
export const sessionType = z.enum(['oauth', 'legacy', 'app_password'])
48
-
export const reauthMethod = z.enum(['password', 'totp', 'passkey'])
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"]);
49
48
50
49
export const sessionSchema = z.object({
51
50
did: did,
···
61
60
migratedAt: isoDate.optional(),
62
61
accessJwt: accessToken,
63
62
refreshJwt: refreshToken,
64
-
})
63
+
});
65
64
66
65
export const serverLinksSchema = z.object({
67
66
privacyPolicy: z.string().optional(),
68
67
termsOfService: z.string().optional(),
69
-
})
68
+
});
70
69
71
70
export const serverDescriptionSchema = z.object({
72
71
availableUserDomains: z.array(z.string()),
···
75
74
version: z.string().optional(),
76
75
availableCommsChannels: z.array(verificationChannel).optional(),
77
76
selfHostedDidWebEnabled: z.boolean().optional(),
78
-
})
77
+
});
79
78
80
79
export const appPasswordSchema = z.object({
81
80
name: z.string(),
82
81
createdAt: isoDate,
83
82
scopes: z.string().optional(),
84
83
createdByController: z.string().optional(),
85
-
})
84
+
});
86
85
87
86
export const createdAppPasswordSchema = z.object({
88
87
name: z.string(),
89
88
password: z.string(),
90
89
createdAt: isoDate,
91
90
scopes: z.string().optional(),
92
-
})
91
+
});
93
92
94
93
export const inviteCodeUseSchema = z.object({
95
94
usedBy: did,
96
95
usedByHandle: handle.optional(),
97
96
usedAt: isoDate,
98
-
})
97
+
});
99
98
100
99
export const inviteCodeInfoSchema = z.object({
101
100
code: inviteCode,
···
105
104
createdBy: did,
106
105
createdAt: isoDate,
107
106
uses: z.array(inviteCodeUseSchema),
108
-
})
107
+
});
109
108
110
109
export const sessionInfoSchema = z.object({
111
110
id: z.string(),
···
114
113
createdAt: isoDate,
115
114
expiresAt: isoDate,
116
115
isCurrent: z.boolean(),
117
-
})
116
+
});
118
117
119
118
export const listSessionsResponseSchema = z.object({
120
119
sessions: z.array(sessionInfoSchema),
121
-
})
120
+
});
122
121
123
122
export const totpStatusSchema = z.object({
124
123
enabled: z.boolean(),
125
124
hasBackupCodes: z.boolean(),
126
-
})
125
+
});
127
126
128
127
export const totpSecretSchema = z.object({
129
128
uri: z.string(),
130
129
qrBase64: z.string(),
131
-
})
130
+
});
132
131
133
132
export const enableTotpResponseSchema = z.object({
134
133
success: z.boolean(),
135
134
backupCodes: z.array(z.string()),
136
-
})
135
+
});
137
136
138
137
export const passkeyInfoSchema = z.object({
139
138
id: z.string(),
···
141
140
friendlyName: z.string().nullable(),
142
141
createdAt: isoDate,
143
142
lastUsed: isoDate.nullable(),
144
-
})
143
+
});
145
144
146
145
export const listPasskeysResponseSchema = z.object({
147
146
passkeys: z.array(passkeyInfoSchema),
148
-
})
147
+
});
149
148
150
149
export const trustedDeviceSchema = z.object({
151
150
id: z.string(),
···
154
153
trustedAt: isoDate.nullable(),
155
154
trustedUntil: isoDate.nullable(),
156
155
lastSeenAt: isoDate,
157
-
})
156
+
});
158
157
159
158
export const listTrustedDevicesResponseSchema = z.object({
160
159
devices: z.array(trustedDeviceSchema),
161
-
})
160
+
});
162
161
163
162
export const reauthStatusSchema = z.object({
164
163
requiresReauth: z.boolean(),
165
164
lastReauthAt: isoDate.nullable(),
166
165
availableMethods: z.array(reauthMethod),
167
-
})
166
+
});
168
167
169
168
export const reauthResponseSchema = z.object({
170
169
success: z.boolean(),
171
170
reauthAt: isoDate,
172
-
})
171
+
});
173
172
174
173
export const notificationPrefsSchema = z.object({
175
174
preferredChannel: verificationChannel,
···
180
179
telegramVerified: z.boolean(),
181
180
signalNumber: z.string().nullable(),
182
181
signalVerified: z.boolean(),
183
-
})
182
+
});
184
183
185
184
export const verificationMethodSchema = z.object({
186
185
id: z.string(),
187
186
type: z.string(),
188
187
controller: z.string(),
189
188
publicKeyMultibase: publicKeyMultibase,
190
-
})
189
+
});
191
190
192
191
export const serviceEndpointSchema = z.object({
193
192
id: z.string(),
194
193
type: z.string(),
195
194
serviceEndpoint: z.string(),
196
-
})
195
+
});
197
196
198
197
export const didDocumentSchema = z.object({
199
-
'@context': z.array(z.string()),
198
+
"@context": z.array(z.string()),
200
199
id: did,
201
200
alsoKnownAs: z.array(z.string()),
202
201
verificationMethod: z.array(verificationMethodSchema),
203
202
service: z.array(serviceEndpointSchema),
204
-
})
203
+
});
205
204
206
205
export const repoDescriptionSchema = z.object({
207
206
handle: handle,
···
209
208
didDoc: didDocumentSchema,
210
209
collections: z.array(nsid),
211
210
handleIsCorrect: z.boolean(),
212
-
})
211
+
});
213
212
214
213
export const recordInfoSchema = z.object({
215
214
uri: atUri,
216
215
cid: cid,
217
216
value: z.unknown(),
218
-
})
217
+
});
219
218
220
219
export const listRecordsResponseSchema = z.object({
221
220
records: z.array(recordInfoSchema),
222
221
cursor: z.string().optional(),
223
-
})
222
+
});
224
223
225
224
export const recordResponseSchema = z.object({
226
225
uri: atUri,
227
226
cid: cid,
228
227
value: z.unknown(),
229
-
})
228
+
});
230
229
231
230
export const createRecordResponseSchema = z.object({
232
231
uri: atUri,
233
232
cid: cid,
234
-
})
233
+
});
235
234
236
235
export const serverStatsSchema = z.object({
237
236
userCount: z.number(),
238
237
repoCount: z.number(),
239
238
recordCount: z.number(),
240
239
blobStorageBytes: z.number(),
241
-
})
240
+
});
242
241
243
242
export const serverConfigSchema = z.object({
244
243
serverName: z.string(),
···
247
246
secondaryColor: z.string().nullable(),
248
247
secondaryColorDark: z.string().nullable(),
249
248
logoCid: cid.nullable(),
250
-
})
249
+
});
251
250
252
251
export const passwordStatusSchema = z.object({
253
252
hasPassword: z.boolean(),
254
-
})
253
+
});
255
254
256
255
export const successResponseSchema = z.object({
257
256
success: z.boolean(),
258
-
})
257
+
});
259
258
260
259
export const legacyLoginPreferenceSchema = z.object({
261
260
allowLegacyLogin: z.boolean(),
262
261
hasMfa: z.boolean(),
263
-
})
262
+
});
264
263
265
264
export const accountInfoSchema = z.object({
266
265
did: did,
···
270
269
emailConfirmedAt: isoDate.optional(),
271
270
invitesDisabled: z.boolean().optional(),
272
271
deactivatedAt: isoDate.optional(),
273
-
})
272
+
});
274
273
275
274
export const searchAccountsResponseSchema = z.object({
276
275
cursor: z.string().optional(),
277
276
accounts: z.array(accountInfoSchema),
278
-
})
277
+
});
279
278
280
279
export const backupInfoSchema = z.object({
281
280
id: z.string(),
···
284
283
blockCount: z.number(),
285
284
sizeBytes: z.number(),
286
285
createdAt: isoDate,
287
-
})
286
+
});
288
287
289
288
export const listBackupsResponseSchema = z.object({
290
289
backups: z.array(backupInfoSchema),
291
290
backupEnabled: z.boolean(),
292
-
})
291
+
});
293
292
294
293
export const createBackupResponseSchema = z.object({
295
294
id: z.string(),
296
295
repoRev: z.string(),
297
296
sizeBytes: z.number(),
298
297
blockCount: z.number(),
299
-
})
298
+
});
300
299
301
-
export type ValidatedSession = z.infer<typeof sessionSchema>
302
-
export type ValidatedServerDescription = z.infer<typeof serverDescriptionSchema>
303
-
export type ValidatedAppPassword = z.infer<typeof appPasswordSchema>
304
-
export type ValidatedCreatedAppPassword = z.infer<typeof createdAppPasswordSchema>
305
-
export type ValidatedInviteCodeInfo = z.infer<typeof inviteCodeInfoSchema>
306
-
export type ValidatedSessionInfo = z.infer<typeof sessionInfoSchema>
307
-
export type ValidatedListSessionsResponse = z.infer<typeof listSessionsResponseSchema>
308
-
export type ValidatedTotpStatus = z.infer<typeof totpStatusSchema>
309
-
export type ValidatedTotpSecret = z.infer<typeof totpSecretSchema>
310
-
export type ValidatedEnableTotpResponse = z.infer<typeof enableTotpResponseSchema>
311
-
export type ValidatedPasskeyInfo = z.infer<typeof passkeyInfoSchema>
312
-
export type ValidatedListPasskeysResponse = z.infer<typeof listPasskeysResponseSchema>
313
-
export type ValidatedTrustedDevice = z.infer<typeof trustedDeviceSchema>
314
-
export type ValidatedListTrustedDevicesResponse = z.infer<typeof listTrustedDevicesResponseSchema>
315
-
export type ValidatedReauthStatus = z.infer<typeof reauthStatusSchema>
316
-
export type ValidatedReauthResponse = z.infer<typeof reauthResponseSchema>
317
-
export type ValidatedNotificationPrefs = z.infer<typeof notificationPrefsSchema>
318
-
export type ValidatedDidDocument = z.infer<typeof didDocumentSchema>
319
-
export type ValidatedRepoDescription = z.infer<typeof repoDescriptionSchema>
320
-
export type ValidatedListRecordsResponse = z.infer<typeof listRecordsResponseSchema>
321
-
export type ValidatedRecordResponse = z.infer<typeof recordResponseSchema>
322
-
export type ValidatedCreateRecordResponse = z.infer<typeof createRecordResponseSchema>
323
-
export type ValidatedServerStats = z.infer<typeof serverStatsSchema>
324
-
export type ValidatedServerConfig = z.infer<typeof serverConfigSchema>
325
-
export type ValidatedPasswordStatus = z.infer<typeof passwordStatusSchema>
326
-
export type ValidatedSuccessResponse = z.infer<typeof successResponseSchema>
327
-
export type ValidatedLegacyLoginPreference = z.infer<typeof legacyLoginPreferenceSchema>
328
-
export type ValidatedAccountInfo = z.infer<typeof accountInfoSchema>
329
-
export type ValidatedSearchAccountsResponse = z.infer<typeof searchAccountsResponseSchema>
330
-
export type ValidatedBackupInfo = z.infer<typeof backupInfoSchema>
331
-
export type ValidatedListBackupsResponse = z.infer<typeof listBackupsResponseSchema>
332
-
export type ValidatedCreateBackupResponse = z.infer<typeof createBackupResponseSchema>
300
+
export type ValidatedSession = z.infer<typeof sessionSchema>;
301
+
export type ValidatedServerDescription = z.infer<
302
+
typeof serverDescriptionSchema
303
+
>;
304
+
export type ValidatedAppPassword = z.infer<typeof appPasswordSchema>;
305
+
export type ValidatedCreatedAppPassword = z.infer<
306
+
typeof createdAppPasswordSchema
307
+
>;
308
+
export type ValidatedInviteCodeInfo = z.infer<typeof inviteCodeInfoSchema>;
309
+
export type ValidatedSessionInfo = z.infer<typeof sessionInfoSchema>;
310
+
export type ValidatedListSessionsResponse = z.infer<
311
+
typeof listSessionsResponseSchema
312
+
>;
313
+
export type ValidatedTotpStatus = z.infer<typeof totpStatusSchema>;
314
+
export type ValidatedTotpSecret = z.infer<typeof totpSecretSchema>;
315
+
export type ValidatedEnableTotpResponse = z.infer<
316
+
typeof enableTotpResponseSchema
317
+
>;
318
+
export type ValidatedPasskeyInfo = z.infer<typeof passkeyInfoSchema>;
319
+
export type ValidatedListPasskeysResponse = z.infer<
320
+
typeof listPasskeysResponseSchema
321
+
>;
322
+
export type ValidatedTrustedDevice = z.infer<typeof trustedDeviceSchema>;
323
+
export type ValidatedListTrustedDevicesResponse = z.infer<
324
+
typeof listTrustedDevicesResponseSchema
325
+
>;
326
+
export type ValidatedReauthStatus = z.infer<typeof reauthStatusSchema>;
327
+
export type ValidatedReauthResponse = z.infer<typeof reauthResponseSchema>;
328
+
export type ValidatedNotificationPrefs = z.infer<
329
+
typeof notificationPrefsSchema
330
+
>;
331
+
export type ValidatedDidDocument = z.infer<typeof didDocumentSchema>;
332
+
export type ValidatedRepoDescription = z.infer<typeof repoDescriptionSchema>;
333
+
export type ValidatedListRecordsResponse = z.infer<
334
+
typeof listRecordsResponseSchema
335
+
>;
336
+
export type ValidatedRecordResponse = z.infer<typeof recordResponseSchema>;
337
+
export type ValidatedCreateRecordResponse = z.infer<
338
+
typeof createRecordResponseSchema
339
+
>;
340
+
export type ValidatedServerStats = z.infer<typeof serverStatsSchema>;
341
+
export type ValidatedServerConfig = z.infer<typeof serverConfigSchema>;
342
+
export type ValidatedPasswordStatus = z.infer<typeof passwordStatusSchema>;
343
+
export type ValidatedSuccessResponse = z.infer<typeof successResponseSchema>;
344
+
export type ValidatedLegacyLoginPreference = z.infer<
345
+
typeof legacyLoginPreferenceSchema
346
+
>;
347
+
export type ValidatedAccountInfo = z.infer<typeof accountInfoSchema>;
348
+
export type ValidatedSearchAccountsResponse = z.infer<
349
+
typeof searchAccountsResponseSchema
350
+
>;
351
+
export type ValidatedBackupInfo = z.infer<typeof backupInfoSchema>;
352
+
export type ValidatedListBackupsResponse = z.infer<
353
+
typeof listBackupsResponseSchema
354
+
>;
355
+
export type ValidatedCreateBackupResponse = z.infer<
356
+
typeof createBackupResponseSchema
357
+
>;
+99
-84
frontend/src/lib/utils/array.ts
+99
-84
frontend/src/lib/utils/array.ts
···
1
-
import type { Option } from './option'
1
+
import type { Option } from "./option.ts";
2
2
3
3
export function first<T>(arr: readonly T[]): Option<T> {
4
-
return arr[0] ?? null
4
+
return arr[0] ?? null;
5
5
}
6
6
7
7
export function last<T>(arr: readonly T[]): Option<T> {
8
-
return arr[arr.length - 1] ?? null
8
+
return arr[arr.length - 1] ?? null;
9
9
}
10
10
11
11
export function at<T>(arr: readonly T[], index: number): Option<T> {
12
-
if (index < 0) index = arr.length + index
13
-
return arr[index] ?? null
12
+
if (index < 0) index = arr.length + index;
13
+
return arr[index] ?? null;
14
14
}
15
15
16
-
export function find<T>(arr: readonly T[], predicate: (t: T) => boolean): Option<T> {
17
-
return arr.find(predicate) ?? null
16
+
export function find<T>(
17
+
arr: readonly T[],
18
+
predicate: (t: T) => boolean,
19
+
): Option<T> {
20
+
return arr.find(predicate) ?? null;
18
21
}
19
22
20
-
export function findMap<T, U>(arr: readonly T[], fn: (t: T) => Option<U>): Option<U> {
23
+
export function findMap<T, U>(
24
+
arr: readonly T[],
25
+
fn: (t: T) => Option<U>,
26
+
): Option<U> {
21
27
for (const item of arr) {
22
-
const result = fn(item)
23
-
if (result != null) return result
28
+
const result = fn(item);
29
+
if (result != null) return result;
24
30
}
25
-
return null
31
+
return null;
26
32
}
27
33
28
-
export function findIndex<T>(arr: readonly T[], predicate: (t: T) => boolean): Option<number> {
29
-
const index = arr.findIndex(predicate)
30
-
return index >= 0 ? index : null
34
+
export function findIndex<T>(
35
+
arr: readonly T[],
36
+
predicate: (t: T) => boolean,
37
+
): Option<number> {
38
+
const index = arr.findIndex(predicate);
39
+
return index >= 0 ? index : null;
31
40
}
32
41
33
42
export function partition<T>(
34
43
arr: readonly T[],
35
-
predicate: (t: T) => boolean
44
+
predicate: (t: T) => boolean,
36
45
): [T[], T[]] {
37
-
const pass: T[] = []
38
-
const fail: T[] = []
46
+
const pass: T[] = [];
47
+
const fail: T[] = [];
39
48
for (const item of arr) {
40
49
if (predicate(item)) {
41
-
pass.push(item)
50
+
pass.push(item);
42
51
} else {
43
-
fail.push(item)
52
+
fail.push(item);
44
53
}
45
54
}
46
-
return [pass, fail]
55
+
return [pass, fail];
47
56
}
48
57
49
58
export function groupBy<T, K extends string | number>(
50
59
arr: readonly T[],
51
-
keyFn: (t: T) => K
60
+
keyFn: (t: T) => K,
52
61
): Record<K, T[]> {
53
-
const result = {} as Record<K, T[]>
62
+
const result = {} as Record<K, T[]>;
54
63
for (const item of arr) {
55
-
const key = keyFn(item)
64
+
const key = keyFn(item);
56
65
if (!result[key]) {
57
-
result[key] = []
66
+
result[key] = [];
58
67
}
59
-
result[key].push(item)
68
+
result[key].push(item);
60
69
}
61
-
return result
70
+
return result;
62
71
}
63
72
64
73
export function unique<T>(arr: readonly T[]): T[] {
65
-
return [...new Set(arr)]
74
+
return [...new Set(arr)];
66
75
}
67
76
68
77
export function uniqueBy<T, K>(arr: readonly T[], keyFn: (t: T) => K): T[] {
69
-
const seen = new Set<K>()
70
-
const result: T[] = []
78
+
const seen = new Set<K>();
79
+
const result: T[] = [];
71
80
for (const item of arr) {
72
-
const key = keyFn(item)
81
+
const key = keyFn(item);
73
82
if (!seen.has(key)) {
74
-
seen.add(key)
75
-
result.push(item)
83
+
seen.add(key);
84
+
result.push(item);
76
85
}
77
86
}
78
-
return result
87
+
return result;
79
88
}
80
89
81
-
export function sortBy<T>(arr: readonly T[], keyFn: (t: T) => number | string): T[] {
90
+
export function sortBy<T>(
91
+
arr: readonly T[],
92
+
keyFn: (t: T) => number | string,
93
+
): T[] {
82
94
return [...arr].sort((a, b) => {
83
-
const ka = keyFn(a)
84
-
const kb = keyFn(b)
85
-
if (ka < kb) return -1
86
-
if (ka > kb) return 1
87
-
return 0
88
-
})
95
+
const ka = keyFn(a);
96
+
const kb = keyFn(b);
97
+
if (ka < kb) return -1;
98
+
if (ka > kb) return 1;
99
+
return 0;
100
+
});
89
101
}
90
102
91
-
export function sortByDesc<T>(arr: readonly T[], keyFn: (t: T) => number | string): T[] {
103
+
export function sortByDesc<T>(
104
+
arr: readonly T[],
105
+
keyFn: (t: T) => number | string,
106
+
): T[] {
92
107
return [...arr].sort((a, b) => {
93
-
const ka = keyFn(a)
94
-
const kb = keyFn(b)
95
-
if (ka > kb) return -1
96
-
if (ka < kb) return 1
97
-
return 0
98
-
})
108
+
const ka = keyFn(a);
109
+
const kb = keyFn(b);
110
+
if (ka > kb) return -1;
111
+
if (ka < kb) return 1;
112
+
return 0;
113
+
});
99
114
}
100
115
101
116
export function chunk<T>(arr: readonly T[], size: number): T[][] {
102
-
const result: T[][] = []
117
+
const result: T[][] = [];
103
118
for (let i = 0; i < arr.length; i += size) {
104
-
result.push(arr.slice(i, i + size))
119
+
result.push(arr.slice(i, i + size));
105
120
}
106
-
return result
121
+
return result;
107
122
}
108
123
109
124
export function zip<T, U>(a: readonly T[], b: readonly U[]): [T, U][] {
110
-
const length = Math.min(a.length, b.length)
111
-
const result: [T, U][] = []
125
+
const length = Math.min(a.length, b.length);
126
+
const result: [T, U][] = [];
112
127
for (let i = 0; i < length; i++) {
113
-
result.push([a[i], b[i]])
128
+
result.push([a[i], b[i]]);
114
129
}
115
-
return result
130
+
return result;
116
131
}
117
132
118
133
export function zipWith<T, U, R>(
119
134
a: readonly T[],
120
135
b: readonly U[],
121
-
fn: (t: T, u: U) => R
136
+
fn: (t: T, u: U) => R,
122
137
): R[] {
123
-
const length = Math.min(a.length, b.length)
124
-
const result: R[] = []
138
+
const length = Math.min(a.length, b.length);
139
+
const result: R[] = [];
125
140
for (let i = 0; i < length; i++) {
126
-
result.push(fn(a[i], b[i]))
141
+
result.push(fn(a[i], b[i]));
127
142
}
128
-
return result
143
+
return result;
129
144
}
130
145
131
146
export function intersperse<T>(arr: readonly T[], separator: T): T[] {
132
-
if (arr.length <= 1) return [...arr]
133
-
const result: T[] = [arr[0]]
147
+
if (arr.length <= 1) return [...arr];
148
+
const result: T[] = [arr[0]];
134
149
for (let i = 1; i < arr.length; i++) {
135
-
result.push(separator, arr[i])
150
+
result.push(separator, arr[i]);
136
151
}
137
-
return result
152
+
return result;
138
153
}
139
154
140
155
export function range(start: number, end: number): number[] {
141
-
const result: number[] = []
156
+
const result: number[] = [];
142
157
for (let i = start; i < end; i++) {
143
-
result.push(i)
158
+
result.push(i);
144
159
}
145
-
return result
160
+
return result;
146
161
}
147
162
148
163
export function isEmpty<T>(arr: readonly T[]): boolean {
149
-
return arr.length === 0
164
+
return arr.length === 0;
150
165
}
151
166
152
167
export function isNonEmpty<T>(arr: readonly T[]): arr is [T, ...T[]] {
153
-
return arr.length > 0
168
+
return arr.length > 0;
154
169
}
155
170
156
171
export function sum(arr: readonly number[]): number {
157
-
return arr.reduce((acc, n) => acc + n, 0)
172
+
return arr.reduce((acc, n) => acc + n, 0);
158
173
}
159
174
160
175
export function sumBy<T>(arr: readonly T[], fn: (t: T) => number): number {
161
-
return arr.reduce((acc, t) => acc + fn(t), 0)
176
+
return arr.reduce((acc, t) => acc + fn(t), 0);
162
177
}
163
178
164
179
export function maxBy<T>(arr: readonly T[], fn: (t: T) => number): Option<T> {
165
-
if (arr.length === 0) return null
166
-
let max = arr[0]
167
-
let maxValue = fn(max)
180
+
if (arr.length === 0) return null;
181
+
let max = arr[0];
182
+
let maxValue = fn(max);
168
183
for (let i = 1; i < arr.length; i++) {
169
-
const value = fn(arr[i])
184
+
const value = fn(arr[i]);
170
185
if (value > maxValue) {
171
-
max = arr[i]
172
-
maxValue = value
186
+
max = arr[i];
187
+
maxValue = value;
173
188
}
174
189
}
175
-
return max
190
+
return max;
176
191
}
177
192
178
193
export function minBy<T>(arr: readonly T[], fn: (t: T) => number): Option<T> {
179
-
if (arr.length === 0) return null
180
-
let min = arr[0]
181
-
let minValue = fn(min)
194
+
if (arr.length === 0) return null;
195
+
let min = arr[0];
196
+
let minValue = fn(min);
182
197
for (let i = 1; i < arr.length; i++) {
183
-
const value = fn(arr[i])
198
+
const value = fn(arr[i]);
184
199
if (value < minValue) {
185
-
min = arr[i]
186
-
minValue = value
200
+
min = arr[i];
201
+
minValue = value;
187
202
}
188
203
}
189
-
return min
204
+
return min;
190
205
}
+103
-104
frontend/src/lib/utils/async.ts
+103
-104
frontend/src/lib/utils/async.ts
···
1
-
import { ok, err, type Result } from '../types/result'
1
+
import { err, type Result } from "../types/result.ts";
2
2
3
3
export function debounce<T extends (...args: Parameters<T>) => void>(
4
4
fn: T,
5
-
ms: number
5
+
ms: number,
6
6
): T & { cancel: () => void } {
7
-
let timeoutId: ReturnType<typeof setTimeout> | null = null
7
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
8
8
9
9
const debounced = ((...args: Parameters<T>) => {
10
-
if (timeoutId) clearTimeout(timeoutId)
10
+
if (timeoutId) clearTimeout(timeoutId);
11
11
timeoutId = setTimeout(() => {
12
-
fn(...args)
13
-
timeoutId = null
14
-
}, ms)
15
-
}) as T & { cancel: () => void }
12
+
fn(...args);
13
+
timeoutId = null;
14
+
}, ms);
15
+
}) as T & { cancel: () => void };
16
16
17
17
debounced.cancel = () => {
18
18
if (timeoutId) {
19
-
clearTimeout(timeoutId)
20
-
timeoutId = null
19
+
clearTimeout(timeoutId);
20
+
timeoutId = null;
21
21
}
22
-
}
22
+
};
23
23
24
-
return debounced
24
+
return debounced;
25
25
}
26
26
27
27
export function throttle<T extends (...args: Parameters<T>) => void>(
28
28
fn: T,
29
-
ms: number
29
+
ms: number,
30
30
): T {
31
-
let lastCall = 0
32
-
let timeoutId: ReturnType<typeof setTimeout> | null = null
31
+
let lastCall = 0;
32
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
33
33
34
34
return ((...args: Parameters<T>) => {
35
-
const now = Date.now()
36
-
const remaining = ms - (now - lastCall)
35
+
const now = Date.now();
36
+
const remaining = ms - (now - lastCall);
37
37
38
38
if (remaining <= 0) {
39
39
if (timeoutId) {
40
-
clearTimeout(timeoutId)
41
-
timeoutId = null
40
+
clearTimeout(timeoutId);
41
+
timeoutId = null;
42
42
}
43
-
lastCall = now
44
-
fn(...args)
43
+
lastCall = now;
44
+
fn(...args);
45
45
} else if (!timeoutId) {
46
46
timeoutId = setTimeout(() => {
47
-
lastCall = Date.now()
48
-
timeoutId = null
49
-
fn(...args)
50
-
}, remaining)
47
+
lastCall = Date.now();
48
+
timeoutId = null;
49
+
fn(...args);
50
+
}, remaining);
51
51
}
52
-
}) as T
52
+
}) as T;
53
53
}
54
54
55
55
export function sleep(ms: number): Promise<void> {
56
-
return new Promise((resolve) => setTimeout(resolve, ms))
56
+
return new Promise((resolve) => setTimeout(resolve, ms));
57
57
}
58
58
59
59
export async function retry<T>(
60
60
fn: () => Promise<T>,
61
61
options: {
62
-
attempts?: number
63
-
delay?: number
64
-
backoff?: number
65
-
shouldRetry?: (error: unknown, attempt: number) => boolean
66
-
} = {}
62
+
attempts?: number;
63
+
delay?: number;
64
+
backoff?: number;
65
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
66
+
} = {},
67
67
): Promise<T> {
68
68
const {
69
69
attempts = 3,
70
70
delay = 1000,
71
71
backoff = 2,
72
72
shouldRetry = () => true,
73
-
} = options
73
+
} = options;
74
74
75
-
let lastError: unknown
76
-
let currentDelay = delay
75
+
let lastError: unknown;
76
+
let currentDelay = delay;
77
77
78
78
for (let attempt = 1; attempt <= attempts; attempt++) {
79
79
try {
80
-
return await fn()
80
+
return await fn();
81
81
} catch (error) {
82
-
lastError = error
82
+
lastError = error;
83
83
if (attempt === attempts || !shouldRetry(error, attempt)) {
84
-
throw error
84
+
throw error;
85
85
}
86
-
await sleep(currentDelay)
87
-
currentDelay *= backoff
86
+
await sleep(currentDelay);
87
+
currentDelay *= backoff;
88
88
}
89
89
}
90
90
91
-
throw lastError
91
+
throw lastError;
92
92
}
93
93
94
94
export async function retryResult<T, E>(
95
95
fn: () => Promise<Result<T, E>>,
96
96
options: {
97
-
attempts?: number
98
-
delay?: number
99
-
backoff?: number
100
-
shouldRetry?: (error: E, attempt: number) => boolean
101
-
} = {}
97
+
attempts?: number;
98
+
delay?: number;
99
+
backoff?: number;
100
+
shouldRetry?: (error: E, attempt: number) => boolean;
101
+
} = {},
102
102
): Promise<Result<T, E>> {
103
103
const {
104
104
attempts = 3,
105
105
delay = 1000,
106
106
backoff = 2,
107
107
shouldRetry = () => true,
108
-
} = options
108
+
} = options;
109
109
110
-
let lastResult: Result<T, E> | null = null
111
-
let currentDelay = delay
110
+
let lastResult: Result<T, E> | null = null;
111
+
let currentDelay = delay;
112
112
113
113
for (let attempt = 1; attempt <= attempts; attempt++) {
114
-
const result = await fn()
115
-
lastResult = result
114
+
const result = await fn();
115
+
lastResult = result;
116
116
117
117
if (result.ok) {
118
-
return result
118
+
return result;
119
119
}
120
120
121
121
if (attempt === attempts || !shouldRetry(result.error, attempt)) {
122
-
return result
122
+
return result;
123
123
}
124
124
125
-
await sleep(currentDelay)
126
-
currentDelay *= backoff
125
+
await sleep(currentDelay);
126
+
currentDelay *= backoff;
127
127
}
128
128
129
-
return lastResult!
129
+
return lastResult!;
130
130
}
131
131
132
132
export function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
133
133
return new Promise((resolve, reject) => {
134
134
const timeoutId = setTimeout(() => {
135
-
reject(new Error(`Timeout after ${ms}ms`))
136
-
}, ms)
135
+
reject(new Error(`Timeout after ${ms}ms`));
136
+
}, ms);
137
137
138
138
promise
139
139
.then((value) => {
140
-
clearTimeout(timeoutId)
141
-
resolve(value)
140
+
clearTimeout(timeoutId);
141
+
resolve(value);
142
142
})
143
143
.catch((error) => {
144
-
clearTimeout(timeoutId)
145
-
reject(error)
146
-
})
147
-
})
144
+
clearTimeout(timeoutId);
145
+
reject(error);
146
+
});
147
+
});
148
148
}
149
149
150
150
export async function timeoutResult<T>(
151
151
promise: Promise<Result<T, Error>>,
152
-
ms: number
152
+
ms: number,
153
153
): Promise<Result<T, Error>> {
154
154
try {
155
-
return await timeout(promise, ms)
155
+
return await timeout(promise, ms);
156
156
} catch (e) {
157
-
return err(e instanceof Error ? e : new Error(String(e)))
157
+
return err(e instanceof Error ? e : new Error(String(e)));
158
158
}
159
159
}
160
160
161
161
export async function parallel<T>(
162
162
tasks: (() => Promise<T>)[],
163
-
concurrency: number
163
+
concurrency: number,
164
164
): Promise<T[]> {
165
-
const results: T[] = []
166
-
const executing: Promise<void>[] = []
165
+
const results: T[] = [];
166
+
const executing: Promise<void>[] = [];
167
167
168
168
for (const task of tasks) {
169
169
const p = task().then((result) => {
170
-
results.push(result)
171
-
})
170
+
results.push(result);
171
+
});
172
172
173
-
executing.push(p)
173
+
executing.push(p);
174
174
175
175
if (executing.length >= concurrency) {
176
-
await Promise.race(executing)
176
+
await Promise.race(executing);
177
177
executing.splice(
178
178
executing.findIndex((e) => e === p),
179
-
1
180
-
)
179
+
1,
180
+
);
181
181
}
182
182
}
183
183
184
-
await Promise.all(executing)
185
-
return results
184
+
await Promise.all(executing);
185
+
return results;
186
186
}
187
187
188
188
export async function mapParallel<T, U>(
189
189
items: T[],
190
190
fn: (item: T, index: number) => Promise<U>,
191
-
concurrency: number
191
+
concurrency: number,
192
192
): Promise<U[]> {
193
-
const results: U[] = new Array(items.length)
194
-
const executing: Promise<void>[] = []
193
+
const results: U[] = new Array(items.length);
194
+
const executing: Promise<void>[] = [];
195
195
196
196
for (let i = 0; i < items.length; i++) {
197
-
const index = i
197
+
const index = i;
198
198
const p = fn(items[index], index).then((result) => {
199
-
results[index] = result
200
-
})
199
+
results[index] = result;
200
+
});
201
201
202
-
executing.push(p)
202
+
executing.push(p);
203
203
204
204
if (executing.length >= concurrency) {
205
-
await Promise.race(executing)
205
+
await Promise.race(executing);
206
206
const doneIndex = executing.findIndex(
207
-
(e) =>
208
-
(e as Promise<void> & { _done?: boolean })._done !== false
209
-
)
207
+
(e) => (e as Promise<void> & { _done?: boolean })._done !== false,
208
+
);
210
209
if (doneIndex >= 0) {
211
-
executing.splice(doneIndex, 1)
210
+
executing.splice(doneIndex, 1);
212
211
}
213
212
}
214
213
}
215
214
216
-
await Promise.all(executing)
217
-
return results
215
+
await Promise.all(executing);
216
+
return results;
218
217
}
219
218
220
219
export function createAbortable<T>(
221
-
fn: (signal: AbortSignal) => Promise<T>
220
+
fn: (signal: AbortSignal) => Promise<T>,
222
221
): { promise: Promise<T>; abort: () => void } {
223
-
const controller = new AbortController()
222
+
const controller = new AbortController();
224
223
return {
225
224
promise: fn(controller.signal),
226
225
abort: () => controller.abort(),
227
-
}
226
+
};
228
227
}
229
228
230
229
export interface Deferred<T> {
231
-
promise: Promise<T>
232
-
resolve: (value: T) => void
233
-
reject: (error: unknown) => void
230
+
promise: Promise<T>;
231
+
resolve: (value: T) => void;
232
+
reject: (error: unknown) => void;
234
233
}
235
234
236
235
export function deferred<T>(): Deferred<T> {
237
-
let resolve!: (value: T) => void
238
-
let reject!: (error: unknown) => void
236
+
let resolve!: (value: T) => void;
237
+
let reject!: (error: unknown) => void;
239
238
240
239
const promise = new Promise<T>((res, rej) => {
241
-
resolve = res
242
-
reject = rej
243
-
})
240
+
resolve = res;
241
+
reject = rej;
242
+
});
244
243
245
-
return { promise, resolve, reject }
244
+
return { promise, resolve, reject };
246
245
}
+27
-3
frontend/src/lib/utils/index.ts
+27
-3
frontend/src/lib/utils/index.ts
···
1
-
export * from './option'
2
-
export * from './array'
3
-
export * from './async'
1
+
export * from "./option.ts";
2
+
export {
3
+
at,
4
+
chunk,
5
+
find,
6
+
findIndex,
7
+
findMap,
8
+
first,
9
+
groupBy,
10
+
intersperse,
11
+
isEmpty,
12
+
isNonEmpty,
13
+
last,
14
+
maxBy,
15
+
minBy,
16
+
partition,
17
+
range,
18
+
sortBy,
19
+
sortByDesc,
20
+
sum,
21
+
sumBy,
22
+
unique,
23
+
uniqueBy,
24
+
zip as zipArrays,
25
+
zipWith as zipArraysWith,
26
+
} from "./array.ts";
27
+
export * from "./async.ts";
+31
-25
frontend/src/lib/utils/option.ts
+31
-25
frontend/src/lib/utils/option.ts
···
1
-
export type Option<T> = T | null | undefined
1
+
export type Option<T> = T | null | undefined;
2
2
3
3
export function isSome<T>(opt: Option<T>): opt is T {
4
-
return opt != null
4
+
return opt != null;
5
5
}
6
6
7
7
export function isNone<T>(opt: Option<T>): opt is null | undefined {
8
-
return opt == null
8
+
return opt == null;
9
9
}
10
10
11
11
export function map<T, U>(opt: Option<T>, fn: (t: T) => U): Option<U> {
12
-
return isSome(opt) ? fn(opt) : null
12
+
return isSome(opt) ? fn(opt) : null;
13
13
}
14
14
15
-
export function flatMap<T, U>(opt: Option<T>, fn: (t: T) => Option<U>): Option<U> {
16
-
return isSome(opt) ? fn(opt) : null
15
+
export function flatMap<T, U>(
16
+
opt: Option<T>,
17
+
fn: (t: T) => Option<U>,
18
+
): Option<U> {
19
+
return isSome(opt) ? fn(opt) : null;
17
20
}
18
21
19
-
export function filter<T>(opt: Option<T>, predicate: (t: T) => boolean): Option<T> {
20
-
return isSome(opt) && predicate(opt) ? opt : null
22
+
export function filter<T>(
23
+
opt: Option<T>,
24
+
predicate: (t: T) => boolean,
25
+
): Option<T> {
26
+
return isSome(opt) && predicate(opt) ? opt : null;
21
27
}
22
28
23
29
export function getOrElse<T>(opt: Option<T>, defaultValue: T): T {
24
-
return isSome(opt) ? opt : defaultValue
30
+
return isSome(opt) ? opt : defaultValue;
25
31
}
26
32
27
33
export function getOrElseLazy<T>(opt: Option<T>, fn: () => T): T {
28
-
return isSome(opt) ? opt : fn()
34
+
return isSome(opt) ? opt : fn();
29
35
}
30
36
31
37
export function getOrThrow<T>(opt: Option<T>, error?: string | Error): T {
32
-
if (isSome(opt)) return opt
33
-
if (error instanceof Error) throw error
34
-
throw new Error(error ?? 'Expected value but got null/undefined')
38
+
if (isSome(opt)) return opt;
39
+
if (error instanceof Error) throw error;
40
+
throw new Error(error ?? "Expected value but got null/undefined");
35
41
}
36
42
37
43
export function tap<T>(opt: Option<T>, fn: (t: T) => void): Option<T> {
38
-
if (isSome(opt)) fn(opt)
39
-
return opt
44
+
if (isSome(opt)) fn(opt);
45
+
return opt;
40
46
}
41
47
42
48
export function match<T, U>(
43
49
opt: Option<T>,
44
-
handlers: { some: (t: T) => U; none: () => U }
50
+
handlers: { some: (t: T) => U; none: () => U },
45
51
): U {
46
-
return isSome(opt) ? handlers.some(opt) : handlers.none()
52
+
return isSome(opt) ? handlers.some(opt) : handlers.none();
47
53
}
48
54
49
55
export function toArray<T>(opt: Option<T>): T[] {
50
-
return isSome(opt) ? [opt] : []
56
+
return isSome(opt) ? [opt] : [];
51
57
}
52
58
53
59
export function fromArray<T>(arr: T[]): Option<T> {
54
-
return arr.length > 0 ? arr[0] : null
60
+
return arr.length > 0 ? arr[0] : null;
55
61
}
56
62
57
63
export function zip<T, U>(a: Option<T>, b: Option<U>): Option<[T, U]> {
58
-
return isSome(a) && isSome(b) ? [a, b] : null
64
+
return isSome(a) && isSome(b) ? [a, b] : null;
59
65
}
60
66
61
67
export function zipWith<T, U, R>(
62
68
a: Option<T>,
63
69
b: Option<U>,
64
-
fn: (t: T, u: U) => R
70
+
fn: (t: T, u: U) => R,
65
71
): Option<R> {
66
-
return isSome(a) && isSome(b) ? fn(a, b) : null
72
+
return isSome(a) && isSome(b) ? fn(a, b) : null;
67
73
}
68
74
69
75
export function or<T>(a: Option<T>, b: Option<T>): Option<T> {
70
-
return isSome(a) ? a : b
76
+
return isSome(a) ? a : b;
71
77
}
72
78
73
79
export function orLazy<T>(a: Option<T>, fn: () => Option<T>): Option<T> {
74
-
return isSome(a) ? a : fn()
80
+
return isSome(a) ? a : fn();
75
81
}
76
82
77
83
export function and<T, U>(a: Option<T>, b: Option<U>): Option<U> {
78
-
return isSome(a) ? b : null
84
+
return isSome(a) ? b : null;
79
85
}
+125
-99
frontend/src/lib/validation.ts
+125
-99
frontend/src/lib/validation.ts
···
1
-
import { ok, err, type Result } from './types/result'
1
+
import { err, ok, type Result } from "./types/result.ts";
2
2
import {
3
+
type AtUri,
4
+
type Cid,
3
5
type Did,
4
6
type DidPlc,
5
7
type DidWeb,
6
-
type Handle,
7
8
type EmailAddress,
8
-
type AtUri,
9
-
type Cid,
10
-
type Nsid,
11
-
type ISODateString,
9
+
type Handle,
10
+
isAtUri,
11
+
isCid,
12
12
isDid,
13
13
isDidPlc,
14
14
isDidWeb,
15
-
isHandle,
16
15
isEmail,
17
-
isAtUri,
18
-
isCid,
16
+
isHandle,
17
+
isISODate,
19
18
isNsid,
20
-
isISODate,
21
-
} from './types/branded'
19
+
type ISODateString,
20
+
type Nsid,
21
+
} from "./types/branded.ts";
22
22
23
23
export class ValidationError extends Error {
24
24
constructor(
25
25
message: string,
26
26
public readonly field?: string,
27
-
public readonly value?: unknown
27
+
public readonly value?: unknown,
28
28
) {
29
-
super(message)
30
-
this.name = 'ValidationError'
29
+
super(message);
30
+
this.name = "ValidationError";
31
31
}
32
32
}
33
33
34
34
export function parseDid(s: string): Result<Did, ValidationError> {
35
35
if (isDid(s)) {
36
-
return ok(s)
36
+
return ok(s);
37
37
}
38
-
return err(new ValidationError(`Invalid DID: ${s}`, 'did', s))
38
+
return err(new ValidationError(`Invalid DID: ${s}`, "did", s));
39
39
}
40
40
41
41
export function parseDidPlc(s: string): Result<DidPlc, ValidationError> {
42
42
if (isDidPlc(s)) {
43
-
return ok(s)
43
+
return ok(s);
44
44
}
45
-
return err(new ValidationError(`Invalid DID:PLC: ${s}`, 'did', s))
45
+
return err(new ValidationError(`Invalid DID:PLC: ${s}`, "did", s));
46
46
}
47
47
48
48
export function parseDidWeb(s: string): Result<DidWeb, ValidationError> {
49
49
if (isDidWeb(s)) {
50
-
return ok(s)
50
+
return ok(s);
51
51
}
52
-
return err(new ValidationError(`Invalid DID:WEB: ${s}`, 'did', s))
52
+
return err(new ValidationError(`Invalid DID:WEB: ${s}`, "did", s));
53
53
}
54
54
55
55
export function parseHandle(s: string): Result<Handle, ValidationError> {
56
-
const trimmed = s.trim().toLowerCase()
56
+
const trimmed = s.trim().toLowerCase();
57
57
if (isHandle(trimmed)) {
58
-
return ok(trimmed)
58
+
return ok(trimmed);
59
59
}
60
-
return err(new ValidationError(`Invalid handle: ${s}`, 'handle', s))
60
+
return err(new ValidationError(`Invalid handle: ${s}`, "handle", s));
61
61
}
62
62
63
63
export function parseEmail(s: string): Result<EmailAddress, ValidationError> {
64
-
const trimmed = s.trim().toLowerCase()
64
+
const trimmed = s.trim().toLowerCase();
65
65
if (isEmail(trimmed)) {
66
-
return ok(trimmed)
66
+
return ok(trimmed);
67
67
}
68
-
return err(new ValidationError(`Invalid email: ${s}`, 'email', s))
68
+
return err(new ValidationError(`Invalid email: ${s}`, "email", s));
69
69
}
70
70
71
71
export function parseAtUri(s: string): Result<AtUri, ValidationError> {
72
72
if (isAtUri(s)) {
73
-
return ok(s)
73
+
return ok(s);
74
74
}
75
-
return err(new ValidationError(`Invalid AT-URI: ${s}`, 'uri', s))
75
+
return err(new ValidationError(`Invalid AT-URI: ${s}`, "uri", s));
76
76
}
77
77
78
78
export function parseCid(s: string): Result<Cid, ValidationError> {
79
79
if (isCid(s)) {
80
-
return ok(s)
80
+
return ok(s);
81
81
}
82
-
return err(new ValidationError(`Invalid CID: ${s}`, 'cid', s))
82
+
return err(new ValidationError(`Invalid CID: ${s}`, "cid", s));
83
83
}
84
84
85
85
export function parseNsid(s: string): Result<Nsid, ValidationError> {
86
86
if (isNsid(s)) {
87
-
return ok(s)
87
+
return ok(s);
88
88
}
89
-
return err(new ValidationError(`Invalid NSID: ${s}`, 'nsid', s))
89
+
return err(new ValidationError(`Invalid NSID: ${s}`, "nsid", s));
90
90
}
91
91
92
-
export function parseISODate(s: string): Result<ISODateString, ValidationError> {
92
+
export function parseISODate(
93
+
s: string,
94
+
): Result<ISODateString, ValidationError> {
93
95
if (isISODate(s)) {
94
-
return ok(s)
96
+
return ok(s);
95
97
}
96
-
return err(new ValidationError(`Invalid ISO date: ${s}`, 'date', s))
98
+
return err(new ValidationError(`Invalid ISO date: ${s}`, "date", s));
97
99
}
98
100
99
101
export interface PasswordValidationResult {
100
-
valid: boolean
101
-
errors: string[]
102
-
strength: 'weak' | 'fair' | 'good' | 'strong'
102
+
valid: boolean;
103
+
errors: string[];
104
+
strength: "weak" | "fair" | "good" | "strong";
103
105
}
104
106
105
107
export function validatePassword(password: string): PasswordValidationResult {
106
-
const errors: string[] = []
108
+
const errors: string[] = [];
107
109
108
110
if (password.length < 8) {
109
-
errors.push('Password must be at least 8 characters')
111
+
errors.push("Password must be at least 8 characters");
110
112
}
111
113
if (password.length > 256) {
112
-
errors.push('Password must be at most 256 characters')
114
+
errors.push("Password must be at most 256 characters");
113
115
}
114
116
if (!/[a-z]/.test(password)) {
115
-
errors.push('Password must contain a lowercase letter')
117
+
errors.push("Password must contain a lowercase letter");
116
118
}
117
119
if (!/[A-Z]/.test(password)) {
118
-
errors.push('Password must contain an uppercase letter')
120
+
errors.push("Password must contain an uppercase letter");
119
121
}
120
122
if (!/\d/.test(password)) {
121
-
errors.push('Password must contain a number')
123
+
errors.push("Password must contain a number");
122
124
}
123
125
124
-
let strength: PasswordValidationResult['strength'] = 'weak'
126
+
let strength: PasswordValidationResult["strength"] = "weak";
125
127
if (errors.length === 0) {
126
-
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
127
-
const isLong = password.length >= 12
128
-
const isVeryLong = password.length >= 16
128
+
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
129
+
const isLong = password.length >= 12;
130
+
const isVeryLong = password.length >= 16;
129
131
130
132
if (isVeryLong && hasSpecial) {
131
-
strength = 'strong'
133
+
strength = "strong";
132
134
} else if (isLong || hasSpecial) {
133
-
strength = 'good'
135
+
strength = "good";
134
136
} else {
135
-
strength = 'fair'
137
+
strength = "fair";
136
138
}
137
139
}
138
140
···
140
142
valid: errors.length === 0,
141
143
errors,
142
144
strength,
143
-
}
145
+
};
144
146
}
145
147
146
-
export function validateHandle(handle: string): Result<Handle, ValidationError> {
147
-
const trimmed = handle.trim().toLowerCase()
148
+
export function validateHandle(
149
+
handle: string,
150
+
): Result<Handle, ValidationError> {
151
+
const trimmed = handle.trim().toLowerCase();
148
152
149
153
if (trimmed.length < 3) {
150
-
return err(new ValidationError('Handle must be at least 3 characters', 'handle', handle))
154
+
return err(
155
+
new ValidationError(
156
+
"Handle must be at least 3 characters",
157
+
"handle",
158
+
handle,
159
+
),
160
+
);
151
161
}
152
162
153
163
if (trimmed.length > 253) {
154
-
return err(new ValidationError('Handle must be at most 253 characters', 'handle', handle))
164
+
return err(
165
+
new ValidationError(
166
+
"Handle must be at most 253 characters",
167
+
"handle",
168
+
handle,
169
+
),
170
+
);
155
171
}
156
172
157
173
if (!isHandle(trimmed)) {
158
-
return err(new ValidationError('Invalid handle format', 'handle', handle))
174
+
return err(new ValidationError("Invalid handle format", "handle", handle));
159
175
}
160
176
161
-
return ok(trimmed)
177
+
return ok(trimmed);
162
178
}
163
179
164
-
export function validateInviteCode(code: string): Result<string, ValidationError> {
165
-
const trimmed = code.trim()
180
+
export function validateInviteCode(
181
+
code: string,
182
+
): Result<string, ValidationError> {
183
+
const trimmed = code.trim();
166
184
167
185
if (trimmed.length === 0) {
168
-
return err(new ValidationError('Invite code is required', 'inviteCode', code))
186
+
return err(
187
+
new ValidationError("Invite code is required", "inviteCode", code),
188
+
);
169
189
}
170
190
171
-
const pattern = /^[a-zA-Z0-9-]+$/
191
+
const pattern = /^[a-zA-Z0-9-]+$/;
172
192
if (!pattern.test(trimmed)) {
173
-
return err(new ValidationError('Invalid invite code format', 'inviteCode', code))
193
+
return err(
194
+
new ValidationError("Invalid invite code format", "inviteCode", code),
195
+
);
174
196
}
175
197
176
-
return ok(trimmed)
198
+
return ok(trimmed);
177
199
}
178
200
179
-
export function validateTotpCode(code: string): Result<string, ValidationError> {
180
-
const trimmed = code.trim().replace(/\s/g, '')
201
+
export function validateTotpCode(
202
+
code: string,
203
+
): Result<string, ValidationError> {
204
+
const trimmed = code.trim().replace(/\s/g, "");
181
205
182
206
if (!/^\d{6}$/.test(trimmed)) {
183
-
return err(new ValidationError('TOTP code must be 6 digits', 'code', code))
207
+
return err(new ValidationError("TOTP code must be 6 digits", "code", code));
184
208
}
185
209
186
-
return ok(trimmed)
210
+
return ok(trimmed);
187
211
}
188
212
189
-
export function validateBackupCode(code: string): Result<string, ValidationError> {
190
-
const trimmed = code.trim().replace(/\s/g, '').toLowerCase()
213
+
export function validateBackupCode(
214
+
code: string,
215
+
): Result<string, ValidationError> {
216
+
const trimmed = code.trim().replace(/\s/g, "").toLowerCase();
191
217
192
218
if (!/^[a-z0-9]{8}$/.test(trimmed)) {
193
-
return err(new ValidationError('Invalid backup code format', 'code', code))
219
+
return err(new ValidationError("Invalid backup code format", "code", code));
194
220
}
195
221
196
-
return ok(trimmed)
222
+
return ok(trimmed);
197
223
}
198
224
199
225
export interface FormValidation<T> {
200
-
validate: () => Result<T, ValidationError[]>
226
+
validate: () => Result<T, ValidationError[]>;
201
227
field: <K extends keyof T>(
202
228
key: K,
203
-
validator: (value: unknown) => Result<T[K], ValidationError>
204
-
) => FormValidation<T>
229
+
validator: (value: unknown) => Result<T[K], ValidationError>,
230
+
) => FormValidation<T>;
205
231
optional: <K extends keyof T>(
206
232
key: K,
207
-
validator: (value: unknown) => Result<T[K], ValidationError>
208
-
) => FormValidation<T>
233
+
validator: (value: unknown) => Result<T[K], ValidationError>,
234
+
) => FormValidation<T>;
209
235
}
210
236
211
237
export function createFormValidation<T extends Record<string, unknown>>(
212
-
data: Record<string, unknown>
238
+
data: Record<string, unknown>,
213
239
): FormValidation<T> {
214
240
const validators: Array<{
215
-
key: string
216
-
validator: (value: unknown) => Result<unknown, ValidationError>
217
-
optional: boolean
218
-
}> = []
241
+
key: string;
242
+
validator: (value: unknown) => Result<unknown, ValidationError>;
243
+
optional: boolean;
244
+
}> = [];
219
245
220
246
const builder: FormValidation<T> = {
221
247
field: (key, validator) => {
222
-
validators.push({ key: key as string, validator, optional: false })
223
-
return builder
248
+
validators.push({ key: key as string, validator, optional: false });
249
+
return builder;
224
250
},
225
251
optional: (key, validator) => {
226
-
validators.push({ key: key as string, validator, optional: true })
227
-
return builder
252
+
validators.push({ key: key as string, validator, optional: true });
253
+
return builder;
228
254
},
229
255
validate: () => {
230
-
const errors: ValidationError[] = []
231
-
const result: Record<string, unknown> = {}
256
+
const errors: ValidationError[] = [];
257
+
const result: Record<string, unknown> = {};
232
258
233
259
for (const { key, validator, optional } of validators) {
234
-
const value = data[key]
260
+
const value = data[key];
235
261
236
-
if (value == null || value === '') {
262
+
if (value == null || value === "") {
237
263
if (!optional) {
238
-
errors.push(new ValidationError(`${key} is required`, key))
264
+
errors.push(new ValidationError(`${key} is required`, key));
239
265
}
240
-
continue
266
+
continue;
241
267
}
242
268
243
-
const validated = validator(value)
269
+
const validated = validator(value);
244
270
if (validated.ok) {
245
-
result[key] = validated.value
271
+
result[key] = validated.value;
246
272
} else {
247
-
errors.push(validated.error)
273
+
errors.push(validated.error);
248
274
}
249
275
}
250
276
251
277
if (errors.length > 0) {
252
-
return err(errors)
278
+
return err(errors);
253
279
}
254
280
255
-
return ok(result as T)
281
+
return ok(result as T);
256
282
},
257
-
}
283
+
};
258
284
259
-
return builder
285
+
return builder;
260
286
}
+64
-62
frontend/src/lib/webauthn.ts
+64
-62
frontend/src/lib/webauthn.ts
···
1
1
export interface PublicKeyCredentialDescriptorJSON {
2
-
type: 'public-key'
3
-
id: string
4
-
transports?: AuthenticatorTransport[]
2
+
type: "public-key";
3
+
id: string;
4
+
transports?: AuthenticatorTransport[];
5
5
}
6
6
7
7
export interface PublicKeyCredentialUserEntityJSON {
8
-
id: string
9
-
name: string
10
-
displayName: string
8
+
id: string;
9
+
name: string;
10
+
displayName: string;
11
11
}
12
12
13
13
export interface PublicKeyCredentialRpEntityJSON {
14
-
name: string
15
-
id?: string
14
+
name: string;
15
+
id?: string;
16
16
}
17
17
18
18
export interface PublicKeyCredentialParametersJSON {
19
-
type: 'public-key'
20
-
alg: number
19
+
type: "public-key";
20
+
alg: number;
21
21
}
22
22
23
23
export interface AuthenticatorSelectionCriteriaJSON {
24
-
authenticatorAttachment?: AuthenticatorAttachment
25
-
residentKey?: ResidentKeyRequirement
26
-
requireResidentKey?: boolean
27
-
userVerification?: UserVerificationRequirement
24
+
authenticatorAttachment?: AuthenticatorAttachment;
25
+
residentKey?: ResidentKeyRequirement;
26
+
requireResidentKey?: boolean;
27
+
userVerification?: UserVerificationRequirement;
28
28
}
29
29
30
30
export interface PublicKeyCredentialCreationOptionsJSON {
31
-
rp: PublicKeyCredentialRpEntityJSON
32
-
user: PublicKeyCredentialUserEntityJSON
33
-
challenge: string
34
-
pubKeyCredParams: PublicKeyCredentialParametersJSON[]
35
-
timeout?: number
36
-
excludeCredentials?: PublicKeyCredentialDescriptorJSON[]
37
-
authenticatorSelection?: AuthenticatorSelectionCriteriaJSON
38
-
attestation?: AttestationConveyancePreference
31
+
rp: PublicKeyCredentialRpEntityJSON;
32
+
user: PublicKeyCredentialUserEntityJSON;
33
+
challenge: string;
34
+
pubKeyCredParams: PublicKeyCredentialParametersJSON[];
35
+
timeout?: number;
36
+
excludeCredentials?: PublicKeyCredentialDescriptorJSON[];
37
+
authenticatorSelection?: AuthenticatorSelectionCriteriaJSON;
38
+
attestation?: AttestationConveyancePreference;
39
39
}
40
40
41
41
export interface PublicKeyCredentialRequestOptionsJSON {
42
-
challenge: string
43
-
timeout?: number
44
-
rpId?: string
45
-
allowCredentials?: PublicKeyCredentialDescriptorJSON[]
46
-
userVerification?: UserVerificationRequirement
42
+
challenge: string;
43
+
timeout?: number;
44
+
rpId?: string;
45
+
allowCredentials?: PublicKeyCredentialDescriptorJSON[];
46
+
userVerification?: UserVerificationRequirement;
47
47
}
48
48
49
49
export interface WebAuthnCreationOptionsResponse {
50
-
publicKey: PublicKeyCredentialCreationOptionsJSON
50
+
publicKey: PublicKeyCredentialCreationOptionsJSON;
51
51
}
52
52
53
53
export interface WebAuthnRequestOptionsResponse {
54
-
publicKey: PublicKeyCredentialRequestOptionsJSON
54
+
publicKey: PublicKeyCredentialRequestOptionsJSON;
55
55
}
56
56
57
57
export interface CredentialAssertionJSON {
58
-
id: string
59
-
type: string
60
-
rawId: string
58
+
id: string;
59
+
type: string;
60
+
rawId: string;
61
61
response: {
62
-
clientDataJSON: string
63
-
authenticatorData: string
64
-
signature: string
65
-
userHandle: string | null
66
-
}
62
+
clientDataJSON: string;
63
+
authenticatorData: string;
64
+
signature: string;
65
+
userHandle: string | null;
66
+
};
67
67
}
68
68
69
69
export interface CredentialAttestationJSON {
70
-
id: string
71
-
type: string
72
-
rawId: string
70
+
id: string;
71
+
type: string;
72
+
rawId: string;
73
73
response: {
74
-
clientDataJSON: string
75
-
attestationObject: string
76
-
}
74
+
clientDataJSON: string;
75
+
attestationObject: string;
76
+
};
77
77
}
78
78
79
79
export function base64UrlToArrayBuffer(base64url: string): ArrayBuffer {
80
-
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/')
81
-
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4)
82
-
const binary = atob(padded)
83
-
return Uint8Array.from(binary, (char) => char.charCodeAt(0)).buffer
80
+
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
81
+
const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
82
+
const binary = atob(padded);
83
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0)).buffer;
84
84
}
85
85
86
86
export function arrayBufferToBase64Url(buffer: ArrayBuffer): string {
87
-
const bytes = new Uint8Array(buffer)
88
-
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')
89
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
87
+
const bytes = new Uint8Array(buffer);
88
+
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join(
89
+
"",
90
+
);
91
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
90
92
}
91
93
92
94
export function prepareCreationOptions(
93
-
options: WebAuthnCreationOptionsResponse
95
+
options: WebAuthnCreationOptionsResponse,
94
96
): PublicKeyCredentialCreationOptions {
95
-
const pk = options.publicKey
97
+
const pk = options.publicKey;
96
98
return {
97
99
...pk,
98
100
challenge: base64UrlToArrayBuffer(pk.challenge),
···
104
106
...cred,
105
107
id: base64UrlToArrayBuffer(cred.id),
106
108
})),
107
-
}
109
+
};
108
110
}
109
111
110
112
export function prepareRequestOptions(
111
-
options: WebAuthnRequestOptionsResponse
113
+
options: WebAuthnRequestOptionsResponse,
112
114
): PublicKeyCredentialRequestOptions {
113
-
const pk = options.publicKey
115
+
const pk = options.publicKey;
114
116
return {
115
117
...pk,
116
118
challenge: base64UrlToArrayBuffer(pk.challenge),
···
118
120
...cred,
119
121
id: base64UrlToArrayBuffer(cred.id),
120
122
})),
121
-
}
123
+
};
122
124
}
123
125
124
126
export function serializeAttestationResponse(
125
-
credential: PublicKeyCredential
127
+
credential: PublicKeyCredential,
126
128
): CredentialAttestationJSON {
127
-
const response = credential.response as AuthenticatorAttestationResponse
129
+
const response = credential.response as AuthenticatorAttestationResponse;
128
130
return {
129
131
id: credential.id,
130
132
type: credential.type,
···
133
135
clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON),
134
136
attestationObject: arrayBufferToBase64Url(response.attestationObject),
135
137
},
136
-
}
138
+
};
137
139
}
138
140
139
141
export function serializeAssertionResponse(
140
-
credential: PublicKeyCredential
142
+
credential: PublicKeyCredential,
141
143
): CredentialAssertionJSON {
142
-
const response = credential.response as AuthenticatorAssertionResponse
144
+
const response = credential.response as AuthenticatorAssertionResponse;
143
145
return {
144
146
id: credential.id,
145
147
type: credential.type,
···
152
154
? arrayBufferToBase64Url(response.userHandle)
153
155
: null,
154
156
},
155
-
}
157
+
};
156
158
}
+6
-5
frontend/src/routes/Admin.svelte
+6
-5
frontend/src/routes/Admin.svelte
···
5
5
import { api, ApiError } from '../lib/api'
6
6
import { _ } from '../lib/i18n'
7
7
import { formatDate, formatDateTime } from '../lib/date'
8
+
import { unsafeAsDid } from '../lib/types/branded'
8
9
import type { Session } from '../lib/types/api'
9
10
import { toast } from '../lib/toast.svelte'
10
11
···
257
258
if (!session) return
258
259
userDetailLoading = true
259
260
try {
260
-
selectedUser = await api.getAccountInfo(session.accessJwt, did)
261
+
selectedUser = await api.getAccountInfo(session.accessJwt, unsafeAsDid(did))
261
262
} catch (e) {
262
263
toast.error(e instanceof ApiError ? e.message : $_('admin.failedToLoadUserDetails'))
263
264
} finally {
···
272
273
userActionLoading = true
273
274
try {
274
275
if (selectedUser.invitesDisabled) {
275
-
await api.enableAccountInvites(session.accessJwt, selectedUser.did)
276
+
await api.enableAccountInvites(session.accessJwt, unsafeAsDid(selectedUser.did))
276
277
selectedUser = { ...selectedUser, invitesDisabled: false }
277
278
toast.success($_('admin.invitesEnabled'))
278
279
} else {
279
-
await api.disableAccountInvites(session.accessJwt, selectedUser.did)
280
+
await api.disableAccountInvites(session.accessJwt, unsafeAsDid(selectedUser.did))
280
281
selectedUser = { ...selectedUser, invitesDisabled: true }
281
282
toast.success($_('admin.invitesDisabled'))
282
283
}
···
291
292
if (!confirm($_('admin.deleteConfirm', { values: { handle: selectedUser.handle } }))) return
292
293
userActionLoading = true
293
294
try {
294
-
await api.adminDeleteAccount(session.accessJwt, selectedUser.did)
295
+
await api.adminDeleteAccount(session.accessJwt, unsafeAsDid(selectedUser.did))
295
296
users = users.filter(u => u.did !== selectedUser!.did)
296
297
selectedUser = null
297
298
toast.success($_('admin.userDeleted'))
···
639
640
</div>
640
641
</div>
641
642
{/if}
642
-
{:else if auth.loading}
643
+
{:else if authLoading}
643
644
<div class="loading">{$_('admin.loading')}</div>
644
645
{/if}
645
646
<style>
+5
-1
frontend/src/routes/Dashboard.svelte
+5
-1
frontend/src/routes/Dashboard.svelte
···
80
80
$effect(() => {
81
81
if (dropdownOpen) {
82
82
document.addEventListener('click', closeDropdown)
83
-
return () => document.removeEventListener('click', closeDropdown)
83
+
}
84
+
return () => {
85
+
if (dropdownOpen) {
86
+
document.removeEventListener('click', closeDropdown)
87
+
}
84
88
}
85
89
})
86
90
</script>
+1
-1
frontend/src/routes/Login.svelte
+1
-1
frontend/src/routes/Login.svelte
+2
-1
frontend/src/routes/RecoverPasskey.svelte
+2
-1
frontend/src/routes/RecoverPasskey.svelte
···
2
2
import { navigate, routes } from '../lib/router.svelte'
3
3
import { api, ApiError } from '../lib/api'
4
4
import { _ } from '../lib/i18n'
5
+
import { unsafeAsDid } from '../lib/types/branded'
5
6
6
7
let newPassword = $state('')
7
8
let confirmPassword = $state('')
···
44
45
error = null
45
46
46
47
try {
47
-
await api.recoverPasskeyAccount(did, token, newPassword)
48
+
await api.recoverPasskeyAccount(unsafeAsDid(did), token, newPassword)
48
49
success = true
49
50
} catch (err) {
50
51
if (err instanceof ApiError) {
+2
-2
frontend/src/routes/RegisterPasskey.svelte
+2
-2
frontend/src/routes/RegisterPasskey.svelte
···
12
12
import {
13
13
prepareCreationOptions,
14
14
serializeAttestationResponse,
15
-
type WebAuthnCreationOptionsResponse,
15
+
type PublicKeyCredentialCreationOptionsJSON,
16
16
} from '../lib/webauthn'
17
17
18
18
let serverInfo = $state<{
···
126
126
passkeyName || undefined
127
127
)
128
128
129
-
const publicKeyOptions = prepareCreationOptions(options as WebAuthnCreationOptionsResponse)
129
+
const publicKeyOptions = prepareCreationOptions({ publicKey: options as unknown as PublicKeyCredentialCreationOptionsJSON })
130
130
const credential = await navigator.credentials.create({
131
131
publicKey: publicKeyOptions
132
132
})
+11
-10
frontend/src/routes/RepoExplorer.svelte
+11
-10
frontend/src/routes/RepoExplorer.svelte
···
4
4
import { api, ApiError } from '../lib/api'
5
5
import { _, locale } from '../lib/i18n'
6
6
import type { Session } from '../lib/types/api'
7
+
import { unsafeAsNsid, unsafeAsRkey } from '../lib/types/branded'
7
8
8
9
const auth = $derived(getAuthState())
9
10
···
75
76
loading = true
76
77
error = null
77
78
try {
78
-
const result = await api.listRecords(session.accessJwt, session.did, collection, { limit: 50 })
79
+
const result = await api.listRecords(session.accessJwt, session.did, unsafeAsNsid(collection), { limit: 50 })
79
80
records = result.records.map(r => ({
80
81
...r,
81
82
rkey: r.uri.split('/').pop()!
···
91
92
if (!session || !selectedCollection || !recordsCursor || loadingMore) return
92
93
loadingMore = true
93
94
try {
94
-
const result = await api.listRecords(session.accessJwt, session.did, selectedCollection, {
95
+
const result = await api.listRecords(session.accessJwt, session.did, unsafeAsNsid(selectedCollection), {
95
96
limit: 50,
96
97
cursor: recordsCursor
97
98
})
···
180
181
const result = await api.createRecord(
181
182
session.accessJwt,
182
183
session.did,
183
-
newCollection.trim(),
184
+
unsafeAsNsid(newCollection.trim()),
184
185
record,
185
-
newRkey.trim() || undefined
186
+
newRkey.trim() ? unsafeAsRkey(newRkey.trim()) : undefined
186
187
)
187
188
success = $_('repoExplorer.recordCreated', { values: { uri: result.uri } })
188
189
await loadCollections()
···
204
205
await api.putRecord(
205
206
session.accessJwt,
206
207
session.did,
207
-
selectedCollection,
208
-
selectedRecord.rkey,
208
+
unsafeAsNsid(selectedCollection),
209
+
unsafeAsRkey(selectedRecord.rkey),
209
210
record
210
211
)
211
212
success = $_('repoExplorer.recordUpdated')
212
213
const updated = await api.getRecord(
213
214
session.accessJwt,
214
215
session.did,
215
-
selectedCollection,
216
-
selectedRecord.rkey
216
+
unsafeAsNsid(selectedCollection),
217
+
unsafeAsRkey(selectedRecord.rkey)
217
218
)
218
219
selectedRecord = { ...updated, rkey: selectedRecord.rkey }
219
220
recordJson = JSON.stringify(updated.value, null, 2)
···
232
233
await api.deleteRecord(
233
234
session.accessJwt,
234
235
session.did,
235
-
selectedCollection,
236
-
selectedRecord.rkey
236
+
unsafeAsNsid(selectedCollection),
237
+
unsafeAsRkey(selectedRecord.rkey)
237
238
)
238
239
success = $_('repoExplorer.recordDeleted')
239
240
selectedRecord = null
+2
-1
frontend/src/routes/RequestPasskeyRecovery.svelte
+2
-1
frontend/src/routes/RequestPasskeyRecovery.svelte
···
2
2
import { navigate, routes, getFullUrl } from '../lib/router.svelte'
3
3
import { api, ApiError } from '../lib/api'
4
4
import { _ } from '../lib/i18n'
5
+
import { unsafeAsEmail } from '../lib/types/branded'
5
6
6
7
let identifier = $state('')
7
8
let submitting = $state(false)
···
14
15
error = null
15
16
16
17
try {
17
-
await api.requestPasskeyRecovery(identifier)
18
+
await api.requestPasskeyRecovery(unsafeAsEmail(identifier))
18
19
success = true
19
20
} catch (err) {
20
21
if (err instanceof ApiError) {
+2
-1
frontend/src/routes/ResetPassword.svelte
+2
-1
frontend/src/routes/ResetPassword.svelte
···
4
4
import { getAuthState } from '../lib/auth.svelte'
5
5
import { _ } from '../lib/i18n'
6
6
import type { Session } from '../lib/types/api'
7
+
import { unsafeAsEmail } from '../lib/types/branded'
7
8
8
9
const auth = $derived(getAuthState())
9
10
···
35
36
error = null
36
37
success = null
37
38
try {
38
-
await api.requestPasswordReset(email)
39
+
await api.requestPasswordReset(unsafeAsEmail(email))
39
40
tokenSent = true
40
41
success = $_('resetPassword.codeSent')
41
42
} catch (e) {
+1
-1
frontend/src/routes/Security.svelte
+1
-1
frontend/src/routes/Security.svelte
···
303
303
addingPasskey = true
304
304
try {
305
305
const { options } = await api.startPasskeyRegistration(session.accessJwt, newPasskeyName || undefined)
306
-
const publicKeyOptions = prepareCreationOptions(options as WebAuthnCreationOptionsResponse)
306
+
const publicKeyOptions = prepareCreationOptions(options as unknown as WebAuthnCreationOptionsResponse)
307
307
const credential = await navigator.credentials.create({
308
308
publicKey: publicKeyOptions
309
309
})
+2
-1
frontend/src/routes/Settings.svelte
+2
-1
frontend/src/routes/Settings.svelte
···
5
5
import { api, ApiError } from '../lib/api'
6
6
import { locale, setLocale, getSupportedLocales, localeNames, _, type SupportedLocale } from '../lib/i18n'
7
7
import { isOk } from '../lib/types/result'
8
+
import { unsafeAsHandle } from '../lib/types/branded'
8
9
import type { Session } from '../lib/types/api'
9
10
import { toast } from '../lib/toast.svelte'
10
11
···
113
114
const fullHandle = showBYOHandle
114
115
? newHandle
115
116
: `${newHandle}.${pdsHostname}`
116
-
await api.updateHandle(session.accessJwt, fullHandle)
117
+
await api.updateHandle(session.accessJwt, unsafeAsHandle(fullHandle))
117
118
await refreshSession()
118
119
toast.success($_('settings.messages.handleUpdated'))
119
120
newHandle = ''
+13
-7
frontend/src/routes/Verify.svelte
+13
-7
frontend/src/routes/Verify.svelte
···
5
5
import { navigate, routes, getFullUrl } from '../lib/router.svelte'
6
6
import { _ } from '../lib/i18n'
7
7
import type { Session } from '../lib/types/api'
8
+
import { unsafeAsDid, unsafeAsEmail, type Did } from '../lib/types/branded'
8
9
9
10
const STORAGE_KEY = 'tranquil_pds_pending_verification'
10
11
11
12
interface PendingVerification {
12
-
did: string
13
+
did: Did
13
14
handle: string
14
15
channel: string
15
16
}
···
66
67
const stored = localStorage.getItem(STORAGE_KEY)
67
68
if (stored) {
68
69
try {
69
-
pendingVerification = JSON.parse(stored)
70
+
const parsed = JSON.parse(stored)
71
+
pendingVerification = {
72
+
did: unsafeAsDid(parsed.did),
73
+
handle: parsed.handle,
74
+
channel: parsed.channel,
75
+
}
70
76
} catch {
71
77
pendingVerification = null
72
78
}
···
114
120
const result = await api.verifyToken(
115
121
verificationCode.trim(),
116
122
identifier.trim(),
117
-
auth.session?.accessJwt
123
+
session?.accessJwt
118
124
)
119
125
success = true
120
126
successPurpose = result.purpose
···
137
143
async function handleEmailUpdate() {
138
144
if (!verificationCode.trim() || !newEmail.trim()) return
139
145
140
-
if (!auth.session) {
146
+
if (!session) {
141
147
error = $_('verify.emailUpdateRequiresAuth')
142
148
return
143
149
}
···
146
152
error = null
147
153
148
154
try {
149
-
await api.updateEmail(auth.session.accessJwt, newEmail.trim(), verificationCode.trim())
155
+
await api.updateEmail(session.accessJwt, newEmail.trim(), verificationCode.trim())
150
156
success = true
151
157
successPurpose = 'email-update'
152
158
successChannel = 'email'
···
185
191
error = null
186
192
187
193
try {
188
-
await api.resendMigrationVerification(identifier.trim())
194
+
await api.resendMigrationVerification(unsafeAsEmail(identifier.trim()))
189
195
resendMessage = $_('verify.codeResentDetail')
190
196
} catch (e) {
191
197
error = e instanceof Error ? e.message : 'Failed to resend verification'
···
250
256
<h1>{$_('verify.emailUpdateTitle')}</h1>
251
257
<p class="subtitle">{$_('verify.emailUpdateSubtitle')}</p>
252
258
253
-
{#if !auth.session}
259
+
{#if !session}
254
260
<div class="message warning">{$_('verify.emailUpdateRequiresAuth')}</div>
255
261
<div class="actions">
256
262
<a href="/app/login" class="btn">{$_('verify.signIn')}</a>
+4
-3
frontend/src/tests/AppPasswords.test.ts
+4
-3
frontend/src/tests/AppPasswords.test.ts
···
10
10
setupAuthenticatedUser,
11
11
setupFetchMock,
12
12
setupUnauthenticatedUser,
13
-
} from "./mocks";
13
+
} from "./mocks.ts";
14
+
import { unsafeAsISODateString } from "../lib/types/branded.ts";
14
15
describe("AppPasswords", () => {
15
16
beforeEach(() => {
16
17
clearMocks();
···
81
82
const testPasswords = [
82
83
mockData.appPassword({
83
84
name: "Graysky",
84
-
createdAt: "2024-01-15T10:00:00Z",
85
+
createdAt: unsafeAsISODateString("2024-01-15T10:00:00Z"),
85
86
}),
86
87
mockData.appPassword({
87
88
name: "Skeets",
88
-
createdAt: "2024-02-20T15:30:00Z",
89
+
createdAt: unsafeAsISODateString("2024-02-20T15:30:00Z"),
89
90
}),
90
91
];
91
92
beforeEach(() => {
+21
-11
frontend/src/tests/Login.test.ts
+21
-11
frontend/src/tests/Login.test.ts
···
7
7
mockData,
8
8
mockEndpoint,
9
9
setupFetchMock,
10
-
} from "./mocks";
11
-
import { _testSetState, type SavedAccount } from "../lib/auth.svelte";
10
+
} from "./mocks.ts";
11
+
import { _testSetState, type SavedAccount } from "../lib/auth.svelte.ts";
12
+
import {
13
+
unsafeAsAccessToken,
14
+
unsafeAsDid,
15
+
unsafeAsHandle,
16
+
unsafeAsRefreshToken,
17
+
} from "../lib/types/branded.ts";
12
18
13
19
describe("Login", () => {
14
20
beforeEach(() => {
···
65
71
describe("with saved accounts", () => {
66
72
const savedAccounts: SavedAccount[] = [
67
73
{
68
-
did: "did:web:test.tranquil.dev:u:alice",
69
-
handle: "alice.test.tranquil.dev",
70
-
accessJwt: "mock-jwt-alice",
71
-
refreshJwt: "mock-refresh-alice",
74
+
did: unsafeAsDid("did:web:test.tranquil.dev:u:alice"),
75
+
handle: unsafeAsHandle("alice.test.tranquil.dev"),
76
+
accessJwt: unsafeAsAccessToken("mock-jwt-alice"),
77
+
refreshJwt: unsafeAsRefreshToken("mock-refresh-alice"),
72
78
},
73
79
{
74
-
did: "did:web:test.tranquil.dev:u:bob",
75
-
handle: "bob.test.tranquil.dev",
76
-
accessJwt: "mock-jwt-bob",
77
-
refreshJwt: "mock-refresh-bob",
80
+
did: unsafeAsDid("did:web:test.tranquil.dev:u:bob"),
81
+
handle: unsafeAsHandle("bob.test.tranquil.dev"),
82
+
accessJwt: unsafeAsAccessToken("mock-jwt-bob"),
83
+
refreshJwt: unsafeAsRefreshToken("mock-refresh-bob"),
78
84
},
79
85
];
80
86
···
88
94
mockEndpoint(
89
95
"com.atproto.server.getSession",
90
96
() =>
91
-
jsonResponse(mockData.session({ handle: "alice.test.tranquil.dev" })),
97
+
jsonResponse(
98
+
mockData.session({
99
+
handle: unsafeAsHandle("alice.test.tranquil.dev"),
100
+
}),
101
+
),
92
102
);
93
103
});
94
104
+35
-4
frontend/src/tests/migration/storage.test.ts
+35
-4
frontend/src/tests/migration/storage.test.ts
···
8
8
setError,
9
9
updateProgress,
10
10
updateStep,
11
-
} from "../../lib/migration/storage";
11
+
} from "../../lib/migration/storage.ts";
12
12
import type {
13
13
InboundMigrationState,
14
-
OutboundMigrationState,
15
-
} from "../../lib/migration/types";
14
+
MigrationState,
15
+
} from "../../lib/migration/types.ts";
16
+
17
+
interface OutboundMigrationState {
18
+
direction: "outbound";
19
+
step: string;
20
+
localDid: string;
21
+
localHandle: string;
22
+
targetPdsUrl: string;
23
+
targetPdsDid: string;
24
+
targetHandle: string;
25
+
targetEmail: string;
26
+
targetPassword: string;
27
+
inviteCode: string;
28
+
targetAccessToken: string | null;
29
+
targetRefreshToken: string | null;
30
+
serviceAuthToken: string | null;
31
+
plcToken: string;
32
+
progress: {
33
+
repoExported: boolean;
34
+
repoImported: boolean;
35
+
blobsTotal: number;
36
+
blobsMigrated: number;
37
+
blobsFailed: string[];
38
+
prefsMigrated: boolean;
39
+
plcSigned: boolean;
40
+
activated: boolean;
41
+
deactivated: boolean;
42
+
currentOperation: string;
43
+
};
44
+
error: string | null;
45
+
targetServerInfo: unknown;
46
+
}
16
47
17
48
const STORAGE_KEY = "tranquil_migration_state";
18
49
const DPOP_KEY_STORAGE = "migration_dpop_key";
···
140
171
step: "review",
141
172
});
142
173
143
-
saveMigrationState(state);
174
+
saveMigrationState(state as unknown as MigrationState);
144
175
145
176
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)!);
146
177
expect(stored.version).toBe(1);
+21
-12
frontend/src/tests/mocks.ts
+21
-12
frontend/src/tests/mocks.ts
···
1
1
import { vi } from "vitest";
2
-
import type { AppPassword, InviteCode, Session } from "../lib/api";
3
-
import { _testSetState } from "../lib/auth.svelte";
2
+
import type { AppPassword, InviteCode, Session } from "../lib/api.ts";
3
+
import { _testSetState } from "../lib/auth.svelte.ts";
4
+
import {
5
+
unsafeAsAccessToken,
6
+
unsafeAsDid,
7
+
unsafeAsEmail,
8
+
unsafeAsHandle,
9
+
unsafeAsInviteCode,
10
+
unsafeAsISODateString,
11
+
unsafeAsRefreshToken,
12
+
} from "../lib/types/branded.ts";
4
13
5
14
const originalPushState = globalThis.history.pushState.bind(globalThis.history);
6
15
const originalReplaceState = globalThis.history.replaceState.bind(
···
144
153
}
145
154
export const mockData = {
146
155
session: (overrides?: Partial<Session>): Session => ({
147
-
did: "did:web:test.tranquil.dev:u:testuser",
148
-
handle: "testuser.test.tranquil.dev",
149
-
email: "test@example.com",
156
+
did: unsafeAsDid("did:web:test.tranquil.dev:u:testuser"),
157
+
handle: unsafeAsHandle("testuser.test.tranquil.dev"),
158
+
email: unsafeAsEmail("test@example.com"),
150
159
emailConfirmed: true,
151
-
accessJwt: "mock-access-jwt-token",
152
-
refreshJwt: "mock-refresh-jwt-token",
160
+
accessJwt: unsafeAsAccessToken("mock-access-jwt-token"),
161
+
refreshJwt: unsafeAsRefreshToken("mock-refresh-jwt-token"),
153
162
...overrides,
154
163
}),
155
164
appPassword: (overrides?: Partial<AppPassword>): AppPassword => ({
156
165
name: "Test App",
157
-
createdAt: new Date().toISOString(),
166
+
createdAt: unsafeAsISODateString(new Date().toISOString()),
158
167
...overrides,
159
168
}),
160
169
inviteCode: (overrides?: Partial<InviteCode>): InviteCode => ({
161
-
code: "test-invite-123",
170
+
code: unsafeAsInviteCode("test-invite-123"),
162
171
available: 1,
163
172
disabled: false,
164
-
forAccount: "did:web:test.tranquil.dev:u:testuser",
165
-
createdBy: "did:web:test.tranquil.dev:u:testuser",
166
-
createdAt: new Date().toISOString(),
173
+
forAccount: unsafeAsDid("did:web:test.tranquil.dev:u:testuser"),
174
+
createdBy: unsafeAsDid("did:web:test.tranquil.dev:u:testuser"),
175
+
createdAt: unsafeAsISODateString(new Date().toISOString()),
167
176
uses: [],
168
177
...overrides,
169
178
}),
+4
-4
frontend/src/tests/utils.ts
+4
-4
frontend/src/tests/utils.ts
···
1
-
import { render, type RenderResult } from "@testing-library/svelte";
1
+
import { render } from "@testing-library/svelte";
2
2
import { tick } from "svelte";
3
3
import type { ComponentType } from "svelte";
4
4
5
-
export async function renderAndWait<T extends ComponentType>(
6
-
component: T,
5
+
export async function renderAndWait(
6
+
component: ComponentType,
7
7
options?: Parameters<typeof render>[1],
8
-
): Promise<RenderResult<T>> {
8
+
) {
9
9
const result = render(component, options);
10
10
await tick();
11
11
await new Promise((resolve) => setTimeout(resolve, 0));
+31
frontend/tsconfig.json
+31
frontend/tsconfig.json
···
1
+
{
2
+
"compilerOptions": {
3
+
"target": "ESNext",
4
+
"module": "ESNext",
5
+
"moduleResolution": "bundler",
6
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
7
+
"types": ["svelte", "vite/client"],
8
+
"strict": true,
9
+
"noImplicitAny": true,
10
+
"strictNullChecks": true,
11
+
"strictFunctionTypes": true,
12
+
"strictBindCallApply": true,
13
+
"strictPropertyInitialization": true,
14
+
"noImplicitThis": true,
15
+
"useUnknownInCatchVariables": true,
16
+
"alwaysStrict": true,
17
+
"noUnusedLocals": false,
18
+
"noUnusedParameters": false,
19
+
"noImplicitReturns": true,
20
+
"noFallthroughCasesInSwitch": true,
21
+
"noImplicitOverride": true,
22
+
"allowImportingTsExtensions": true,
23
+
"resolveJsonModule": true,
24
+
"isolatedModules": true,
25
+
"verbatimModuleSyntax": true,
26
+
"skipLibCheck": true,
27
+
"noEmit": true
28
+
},
29
+
"include": ["src/**/*"],
30
+
"exclude": ["node_modules", "dist"]
31
+
}
+2
justfile
+2
justfile
+8
-4
src/api/actor/preferences.rs
+8
-4
src/api/actor/preferences.rs
···
70
70
let prefs = match prefs_result {
71
71
Ok(rows) => rows,
72
72
Err(_) => {
73
-
return ApiError::InternalError(Some("Failed to fetch preferences".into())).into_response();
73
+
return ApiError::InternalError(Some("Failed to fetch preferences".into()))
74
+
.into_response();
74
75
}
75
76
};
76
77
let mut personal_details_pref: Option<Value> = None;
···
192
193
let mut tx = match state.db.begin().await {
193
194
Ok(tx) => tx,
194
195
Err(_) => {
195
-
return ApiError::InternalError(Some("Failed to start transaction".into())).into_response();
196
+
return ApiError::InternalError(Some("Failed to start transaction".into()))
197
+
.into_response();
196
198
}
197
199
};
198
200
let delete_result = sqlx::query!(
···
225
227
.await;
226
228
if insert_result.is_err() {
227
229
let _ = tx.rollback().await;
228
-
return ApiError::InternalError(Some("Failed to save preference".into())).into_response();
230
+
return ApiError::InternalError(Some("Failed to save preference".into()))
231
+
.into_response();
229
232
}
230
233
}
231
234
if tx.commit().await.is_err() {
232
-
return ApiError::InternalError(Some("Failed to commit transaction".into())).into_response();
235
+
return ApiError::InternalError(Some("Failed to commit transaction".into()))
236
+
.into_response();
233
237
}
234
238
StatusCode::OK.into_response()
235
239
}
+12
-5
src/api/admin/account/delete.rs
+12
-5
src/api/admin/account/delete.rs
···
1
-
use crate::api::error::ApiError;
2
1
use crate::api::EmptyResponse;
2
+
use crate::api::error::ApiError;
3
3
use crate::auth::BearerAuthAdmin;
4
4
use crate::state::AppState;
5
5
use crate::types::Did;
···
47
47
.await
48
48
{
49
49
error!("Failed to delete session tokens for {}: {:?}", did, e);
50
-
return ApiError::InternalError(Some("Failed to delete session tokens".into())).into_response();
50
+
return ApiError::InternalError(Some("Failed to delete session tokens".into()))
51
+
.into_response();
51
52
}
52
53
if let Err(e) = sqlx::query!("DELETE FROM used_refresh_tokens WHERE session_id IN (SELECT id FROM session_tokens WHERE did = $1)", did.as_str())
53
54
.execute(&mut *tx)
···
84
85
"Failed to delete app passwords for user {}: {:?}",
85
86
user_id, e
86
87
);
87
-
return ApiError::InternalError(Some("Failed to delete app passwords".into())).into_response();
88
+
return ApiError::InternalError(Some("Failed to delete app passwords".into()))
89
+
.into_response();
88
90
}
89
91
if let Err(e) = sqlx::query!(
90
92
"DELETE FROM invite_code_uses WHERE used_by_user = $1",
···
128
130
error!("Failed to commit account deletion transaction: {:?}", e);
129
131
return ApiError::InternalError(Some("Failed to commit deletion".into())).into_response();
130
132
}
131
-
if let Err(e) =
132
-
crate::api::repo::record::sequence_account_event(&state, did.as_str(), false, Some("deleted")).await
133
+
if let Err(e) = crate::api::repo::record::sequence_account_event(
134
+
&state,
135
+
did.as_str(),
136
+
false,
137
+
Some("deleted"),
138
+
)
139
+
.await
133
140
{
134
141
warn!(
135
142
"Failed to sequence account deletion event for {}: {}",
+5
-1
src/api/admin/account/email.rs
+5
-1
src/api/admin/account/email.rs
···
74
74
let result = crate::comms::enqueue_comms(&state.db, item).await;
75
75
match result {
76
76
Ok(_) => {
77
-
tracing::info!("Admin email queued for {} ({})", handle, input.recipient_did);
77
+
tracing::info!(
78
+
"Admin email queued for {} ({})",
79
+
handle,
80
+
input.recipient_did
81
+
);
78
82
(StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()
79
83
}
80
84
Err(e) => {
+16
-7
src/api/admin/account/update.rs
+16
-7
src/api/admin/account/update.rs
···
1
-
use crate::api::error::ApiError;
2
1
use crate::api::EmptyResponse;
2
+
use crate::api::error::ApiError;
3
3
use crate::auth::BearerAuthAdmin;
4
4
use crate::state::AppState;
5
5
use crate::types::{Did, PlainPassword};
···
87
87
if let Ok(Some(_)) = existing {
88
88
return ApiError::HandleTaken.into_response();
89
89
}
90
-
let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did.as_str())
91
-
.execute(&state.db)
92
-
.await;
90
+
let result = sqlx::query!(
91
+
"UPDATE users SET handle = $1 WHERE did = $2",
92
+
handle,
93
+
did.as_str()
94
+
)
95
+
.execute(&state.db)
96
+
.await;
93
97
match result {
94
98
Ok(r) => {
95
99
if r.rows_affected() == 0 {
···
99
103
let _ = state.cache.delete(&format!("handle:{}", old)).await;
100
104
}
101
105
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
102
-
if let Err(e) =
103
-
crate::api::repo::record::sequence_identity_event(&state, did.as_str(), Some(&handle)).await
106
+
if let Err(e) = crate::api::repo::record::sequence_identity_event(
107
+
&state,
108
+
did.as_str(),
109
+
Some(&handle),
110
+
)
111
+
.await
104
112
{
105
113
warn!(
106
114
"Failed to sequence identity event for admin handle update: {}",
107
115
e
108
116
);
109
117
}
110
-
if let Err(e) = crate::api::identity::did::update_plc_handle(&state, did.as_str(), &handle).await
118
+
if let Err(e) =
119
+
crate::api::identity::did::update_plc_handle(&state, did.as_str(), &handle).await
111
120
{
112
121
warn!("Failed to update PLC handle for admin handle update: {}", e);
113
122
}
+2
-4
src/api/admin/status.rs
+2
-4
src/api/admin/status.rs
···
119
119
let did = match ¶ms.did {
120
120
Some(d) => d,
121
121
None => {
122
-
return ApiError::InvalidRequest(
123
-
"Must provide a did to request blob state".into(),
124
-
)
125
-
.into_response();
122
+
return ApiError::InvalidRequest("Must provide a did to request blob state".into())
123
+
.into_response();
126
124
}
127
125
};
128
126
let blob = sqlx::query!(
+6
-3
src/api/age_assurance.rs
+6
-3
src/api/age_assurance.rs
···
50
50
}
51
51
};
52
52
53
-
let row = match sqlx::query!("SELECT created_at FROM users WHERE did = $1", &auth_user.did)
54
-
.fetch_optional(&state.db)
55
-
.await
53
+
let row = match sqlx::query!(
54
+
"SELECT created_at FROM users WHERE did = $1",
55
+
&auth_user.did
56
+
)
57
+
.fetch_optional(&state.db)
58
+
.await
56
59
{
57
60
Ok(r) => {
58
61
tracing::debug!(?r, "age assurance: query result");
+7
-4
src/api/backup.rs
+7
-4
src/api/backup.rs
···
144
144
Ok(bytes) => bytes,
145
145
Err(e) => {
146
146
error!("Failed to fetch backup from storage: {:?}", e);
147
-
return ApiError::InternalError(Some("Failed to retrieve backup".into())).into_response();
147
+
return ApiError::InternalError(Some("Failed to retrieve backup".into()))
148
+
.into_response();
148
149
}
149
150
};
150
151
···
223
224
Ok(bytes) => bytes,
224
225
Err(e) => {
225
226
error!("Failed to generate CAR: {:?}", e);
226
-
return ApiError::InternalError(Some("Failed to generate backup".into())).into_response();
227
+
return ApiError::InternalError(Some("Failed to generate backup".into()))
228
+
.into_response();
227
229
}
228
230
};
229
231
···
448
450
449
451
info!(did = %auth.0.did, enabled = input.enabled, "Updated backup_enabled setting");
450
452
451
-
EnabledResponse::new(input.enabled).into_response()
453
+
EnabledResponse::response(input.enabled).into_response()
452
454
}
453
455
454
456
pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Response {
···
575
577
576
578
if let Err(e) = zip.finish() {
577
579
error!("Failed to finish zip: {:?}", e);
578
-
return ApiError::InternalError(Some("Failed to create zip file".into())).into_response();
580
+
return ApiError::InternalError(Some("Failed to create zip file".into()))
581
+
.into_response();
579
582
}
580
583
}
581
584
+11
-4
src/api/delegation.rs
+11
-4
src/api/delegation.rs
···
39
39
Ok(c) => c,
40
40
Err(e) => {
41
41
tracing::error!("Failed to list controllers: {:?}", e);
42
-
return ApiError::InternalError(Some("Failed to list controllers".into())).into_response();
42
+
return ApiError::InternalError(Some("Failed to list controllers".into()))
43
+
.into_response();
43
44
}
44
45
};
45
46
···
269
270
Ok(false) => ApiError::DelegationNotFound.into_response(),
270
271
Err(e) => {
271
272
tracing::error!("Failed to update controller scopes: {:?}", e);
272
-
ApiError::InternalError(Some("Failed to update controller scopes".into())).into_response()
273
+
ApiError::InternalError(Some("Failed to update controller scopes".into()))
274
+
.into_response()
273
275
}
274
276
}
275
277
}
···
357
359
Ok(e) => e,
358
360
Err(e) => {
359
361
tracing::error!("Failed to get audit log: {:?}", e);
360
-
return ApiError::InternalError(Some("Failed to get audit log".into())).into_response();
362
+
return ApiError::InternalError(Some("Failed to get audit log".into()))
363
+
.into_response();
361
364
}
362
365
};
363
366
···
762
765
763
766
info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created");
764
767
765
-
Json(CreateDelegatedAccountResponse { did: did.into(), handle: handle.into() }).into_response()
768
+
Json(CreateDelegatedAccountResponse {
769
+
did: did.into(),
770
+
handle: handle.into(),
771
+
})
772
+
.into_response()
766
773
}
+10
-15
src/api/error.rs
+10
-15
src/api/error.rs
···
115
115
Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => {
116
116
StatusCode::BAD_GATEWAY
117
117
}
118
-
Self::ServiceUnavailable(_) | Self::BackupsDisabled => {
119
-
StatusCode::SERVICE_UNAVAILABLE
120
-
}
118
+
Self::ServiceUnavailable(_) | Self::BackupsDisabled => StatusCode::SERVICE_UNAVAILABLE,
121
119
Self::UpstreamTimeout => StatusCode::GATEWAY_TIMEOUT,
122
120
Self::UpstreamError { status, .. } => {
123
121
StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY)
···
155
153
| Self::SubjectNotFound
156
154
| Self::BlobNotFound(_)
157
155
| Self::NotFoundMsg(_) => StatusCode::NOT_FOUND,
158
-
Self::RepoTakendown
159
-
| Self::RepoDeactivated
160
-
| Self::RepoNotFound(_) => StatusCode::BAD_REQUEST,
161
-
Self::InvalidSwap(_) | Self::TotpAlreadyEnabled => {
162
-
StatusCode::CONFLICT
156
+
Self::RepoTakendown | Self::RepoDeactivated | Self::RepoNotFound(_) => {
157
+
StatusCode::BAD_REQUEST
163
158
}
159
+
Self::InvalidSwap(_) | Self::TotpAlreadyEnabled => StatusCode::CONFLICT,
164
160
Self::InvalidRequest(_)
165
161
| Self::InvalidHandle(_)
166
162
| Self::HandleNotAvailable(_)
···
435
431
}
436
432
}
437
433
438
-
439
434
impl From<sqlx::Error> for ApiError {
440
435
fn from(e: sqlx::Error) -> Self {
441
436
tracing::error!("Database error: {:?}", e);
···
522
517
VerifyError::UnsupportedVersion => {
523
518
Self::InvalidRequest("This verification code version is not supported".to_string())
524
519
}
525
-
VerifyError::Expired => {
526
-
Self::InvalidRequest("The verification code has expired. Please request a new one.".to_string())
527
-
}
520
+
VerifyError::Expired => Self::InvalidRequest(
521
+
"The verification code has expired. Please request a new one.".to_string(),
522
+
),
528
523
VerifyError::InvalidSignature => {
529
524
Self::InvalidRequest("The verification code is invalid".to_string())
530
525
}
···
565
560
PlcError::NotFound => Self::NotFoundMsg("DID not found in PLC directory".into()),
566
561
PlcError::Tombstoned => Self::InvalidRequest("DID is tombstoned".into()),
567
562
PlcError::Timeout => Self::UpstreamTimeout,
568
-
PlcError::CircuitBreakerOpen => {
569
-
Self::ServiceUnavailable(Some("PLC directory service temporarily unavailable".into()))
570
-
}
563
+
PlcError::CircuitBreakerOpen => Self::ServiceUnavailable(Some(
564
+
"PLC directory service temporarily unavailable".into(),
565
+
)),
571
566
PlcError::Http(err) => {
572
567
tracing::error!("PLC HTTP error: {:?}", err);
573
568
Self::UpstreamErrorMsg("Failed to communicate with PLC directory".into())
+4
-2
src/api/identity/account.rs
+4
-2
src/api/identity/account.rs
···
12
12
http::{HeaderMap, StatusCode},
13
13
response::{IntoResponse, Response},
14
14
};
15
-
use serde_json::json;
16
15
use bcrypt::{DEFAULT_COST, hash};
17
16
use jacquard::types::{integer::LimitedU32, string::Tid};
18
17
use jacquard_repo::{mst::Mst, storage::BlockStore};
19
18
use k256::{SecretKey, ecdsa::SigningKey};
20
19
use rand::rngs::OsRng;
21
20
use serde::{Deserialize, Serialize};
21
+
use serde_json::json;
22
22
use std::sync::Arc;
23
23
use tracing::{debug, error, info, warn};
24
24
···
90
90
.await
91
91
{
92
92
warn!(ip = %client_ip, "Account creation rate limit exceeded");
93
-
return ApiError::RateLimitExceeded(Some("Too many account creation attempts. Please try again later.".into(),))
93
+
return ApiError::RateLimitExceeded(Some(
94
+
"Too many account creation attempts. Please try again later.".into(),
95
+
))
94
96
.into_response();
95
97
}
96
98
+18
-11
src/api/identity/did.rs
+18
-11
src/api/identity/did.rs
···
38
38
}
39
39
let cache_key = format!("handle:{}", handle);
40
40
if let Some(did) = state.cache.get(&cache_key).await {
41
-
return DidResponse::new(did).into_response();
41
+
return DidResponse::response(did).into_response();
42
42
}
43
43
let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle)
44
44
.fetch_optional(&state.db)
···
49
49
.cache
50
50
.set(&cache_key, &row.did, std::time::Duration::from_secs(300))
51
51
.await;
52
-
DidResponse::new(row.did).into_response()
52
+
DidResponse::response(row.did).into_response()
53
53
}
54
54
Ok(None) => match crate::handle::resolve_handle(handle).await {
55
55
Ok(did) => {
···
57
57
.cache
58
58
.set(&cache_key, &did, std::time::Duration::from_secs(300))
59
59
.await;
60
-
DidResponse::new(did).into_response()
60
+
DidResponse::response(did).into_response()
61
61
}
62
62
Err(_) => ApiError::HandleNotFound.into_response(),
63
63
},
···
627
627
.check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did)
628
628
.await
629
629
{
630
-
return ApiError::RateLimitExceeded(Some("Too many handle updates. Try again later.".into(),))
630
+
return ApiError::RateLimitExceeded(Some(
631
+
"Too many handle updates. Try again later.".into(),
632
+
))
631
633
.into_response();
632
634
}
633
635
if !state
···
663
665
.into_response();
664
666
}
665
667
if segment.starts_with('-') || segment.ends_with('-') {
666
-
return ApiError::InvalidHandle(Some("Handle segment cannot start or end with hyphen".into(),))
667
-
.into_response();
668
+
return ApiError::InvalidHandle(Some(
669
+
"Handle segment cannot start or end with hyphen".into(),
670
+
))
671
+
.into_response();
668
672
}
669
673
}
670
674
if crate::moderation::has_explicit_slur(&new_handle) {
···
695
699
return EmptyResponse::ok().into_response();
696
700
}
697
701
if short_part.contains('.') {
698
-
return ApiError::InvalidHandle(Some("Nested subdomains are not allowed. Use a simple handle without dots.".into(),))
699
-
.into_response();
702
+
return ApiError::InvalidHandle(Some(
703
+
"Nested subdomains are not allowed. Use a simple handle without dots.".into(),
704
+
))
705
+
.into_response();
700
706
}
701
707
if short_part.len() < 3 {
702
708
return ApiError::InvalidHandle(Some("Handle too short".into())).into_response();
···
721
727
return ApiError::HandleNotAvailable(None).into_response();
722
728
}
723
729
Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
724
-
return ApiError::HandleNotAvailable(Some(
725
-
format!("Handle points to different DID. Expected {}, got {}", expected, actual),
726
-
))
730
+
return ApiError::HandleNotAvailable(Some(format!(
731
+
"Handle points to different DID. Expected {}, got {}",
732
+
expected, actual
733
+
)))
727
734
.into_response();
728
735
}
729
736
Err(e) => {
+2
-1
src/api/identity/plc/sign.rs
+2
-1
src/api/identity/plc/sign.rs
···
120
120
{
121
121
Ok(Some(row)) => row,
122
122
_ => {
123
-
return ApiError::InternalError(Some("User signing key not found".into())).into_response();
123
+
return ApiError::InternalError(Some("User signing key not found".into()))
124
+
.into_response();
124
125
}
125
126
};
126
127
let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
+2
-1
src/api/identity/plc/submit.rs
+2
-1
src/api/identity/plc/submit.rs
···
75
75
{
76
76
Ok(Some(row)) => row,
77
77
_ => {
78
-
return ApiError::InternalError(Some("User signing key not found".into())).into_response();
78
+
return ApiError::InternalError(Some("User signing key not found".into()))
79
+
.into_response();
79
80
}
80
81
};
81
82
let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
+1
-1
src/api/mod.rs
+1
-1
src/api/mod.rs
···
17
17
pub mod verification;
18
18
19
19
pub use error::ApiError;
20
+
pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_did, validate_limit};
20
21
pub use responses::{
21
22
DidResponse, EmptyResponse, EnabledResponse, HasPasswordResponse, OptionsResponse,
22
23
StatusResponse, SuccessResponse, TokenRequiredResponse, VerifiedResponse,
23
24
};
24
-
pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_did, validate_limit};
+4
-2
src/api/moderation/mod.rs
+4
-2
src/api/moderation/mod.rs
···
111
111
}
112
112
Err(e) => {
113
113
error!(error = ?e, "DB error fetching user key for report");
114
-
return ApiError::AuthenticationFailed(Some("Failed to get signing key".into()))
115
-
.into_response();
114
+
return ApiError::AuthenticationFailed(Some(
115
+
"Failed to get signing key".into(),
116
+
))
117
+
.into_response();
116
118
}
117
119
}
118
120
}
+38
-40
src/api/notification_prefs.rs
+38
-40
src/api/notification_prefs.rs
···
38
38
return ApiError::AuthenticationFailed(None).into_response();
39
39
}
40
40
};
41
-
let row =
42
-
match sqlx::query(
43
-
r#"
41
+
let row = match sqlx::query(
42
+
r#"
44
43
SELECT
45
44
email,
46
45
preferred_comms_channel::text as channel,
···
53
52
FROM users
54
53
WHERE did = $1
55
54
"#,
56
-
)
57
-
.bind(&user.did)
58
-
.fetch_one(&state.db)
59
-
.await
60
-
{
61
-
Ok(r) => r,
62
-
Err(e) => {
63
-
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response()
64
-
}
65
-
};
55
+
)
56
+
.bind(&user.did)
57
+
.fetch_one(&state.db)
58
+
.await
59
+
{
60
+
Ok(r) => r,
61
+
Err(e) => {
62
+
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response();
63
+
}
64
+
};
66
65
let email: String = row.get("email");
67
66
let channel: String = row.get("channel");
68
67
let discord_id: Option<String> = row.get("discord_id");
···
125
124
{
126
125
Ok(id) => id,
127
126
Err(e) => {
128
-
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response()
127
+
return ApiError::InternalError(Some(format!("Database error: {}", e)))
128
+
.into_response();
129
129
}
130
130
};
131
131
132
-
let rows =
133
-
match sqlx::query!(
134
-
r#"
132
+
let rows = match sqlx::query!(
133
+
r#"
135
134
SELECT
136
135
created_at,
137
136
channel as "channel: String",
···
144
143
ORDER BY created_at DESC
145
144
LIMIT 50
146
145
"#,
147
-
user_id
148
-
)
149
-
.fetch_all(&state.db)
150
-
.await
151
-
{
152
-
Ok(r) => r,
153
-
Err(e) => {
154
-
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response()
155
-
}
156
-
};
146
+
user_id
147
+
)
148
+
.fetch_all(&state.db)
149
+
.await
150
+
{
151
+
Ok(r) => r,
152
+
Err(e) => {
153
+
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response();
154
+
}
155
+
};
157
156
158
157
let sensitive_types = [
159
158
"email_verification",
···
270
269
}
271
270
};
272
271
273
-
let user_row =
274
-
match sqlx::query!(
275
-
"SELECT id, handle, email FROM users WHERE did = $1",
276
-
&user.did
277
-
)
278
-
.fetch_one(&state.db)
279
-
.await
280
-
{
281
-
Ok(row) => row,
282
-
Err(e) => {
283
-
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response()
284
-
}
285
-
};
272
+
let user_row = match sqlx::query!(
273
+
"SELECT id, handle, email FROM users WHERE did = $1",
274
+
&user.did
275
+
)
276
+
.fetch_one(&state.db)
277
+
.await
278
+
{
279
+
Ok(row) => row,
280
+
Err(e) => {
281
+
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response();
282
+
}
283
+
};
286
284
287
285
let user_id = user_row.id;
288
286
let handle = user_row.handle;
+3
-3
src/api/proxy.rs
+3
-3
src/api/proxy.rs
···
186
186
) -> Response {
187
187
// This layer is nested under /xrpc in an axum router so the extracted uri will look like /<method> and thus we can just strip the /
188
188
let method = uri.path().trim_start_matches("/");
189
-
if is_protected_method(&method) {
189
+
if is_protected_method(method) {
190
190
warn!(method = %method, "Attempted to proxy protected method");
191
191
return ApiError::InvalidRequest(format!("Cannot proxy protected method: {}", method))
192
192
.into_response();
···
226
226
auth_user.is_oauth,
227
227
auth_user.scope.as_deref(),
228
228
&resolved.did,
229
-
&method,
229
+
method,
230
230
) {
231
231
return e;
232
232
}
···
235
235
match crate::auth::create_service_token(
236
236
&auth_user.did,
237
237
&resolved.did,
238
-
&method,
238
+
method,
239
239
&key_bytes,
240
240
) {
241
241
Ok(new_token) => {
+7
-6
src/api/repo/blob.rs
+7
-6
src/api/repo/blob.rs
···
29
29
);
30
30
}
31
31
detected
32
+
} else if client_hint == "*/*" || client_hint.is_empty() {
33
+
warn!(
34
+
"Could not detect MIME type and client sent invalid hint: '{}'",
35
+
client_hint
36
+
);
37
+
"application/octet-stream".to_string()
32
38
} else {
33
-
if client_hint == "*/*" || client_hint.is_empty() {
34
-
warn!("Could not detect MIME type and client sent invalid hint: '{}'", client_hint);
35
-
"application/octet-stream".to_string()
36
-
} else {
37
-
client_hint.to_string()
38
-
}
39
+
client_hint.to_string()
39
40
}
40
41
}
41
42
+9
-9
src/api/repo/import.rs
+9
-9
src/api/repo/import.rs
···
1
+
use crate::api::EmptyResponse;
1
2
use crate::api::error::ApiError;
2
3
use crate::api::repo::record::create_signed_commit;
3
-
use crate::api::EmptyResponse;
4
4
use crate::state::AppState;
5
5
use crate::sync::import::{ImportError, apply_import, parse_car};
6
6
use crate::sync::verify::CarVerifier;
···
371
371
ApiError::InvalidRequest(format!("Referenced block not found in CAR: {}", cid))
372
372
.into_response()
373
373
}
374
-
Err(ImportError::ConcurrentModification) => ApiError::InvalidSwap(Some("Repository is being modified by another operation, please retry".into(),))
374
+
Err(ImportError::ConcurrentModification) => ApiError::InvalidSwap(Some(
375
+
"Repository is being modified by another operation, please retry".into(),
376
+
))
375
377
.into_response(),
376
378
Err(ImportError::VerificationFailed(ve)) => {
377
379
ApiError::InvalidRequest(format!("CAR verification failed: {}", ve)).into_response()
378
380
}
379
-
Err(ImportError::DidMismatch { car_did, auth_did }) => {
380
-
ApiError::InvalidRequest(format!(
381
-
"CAR is for {} but authenticated as {}",
382
-
car_did, auth_did
383
-
))
384
-
.into_response()
385
-
}
381
+
Err(ImportError::DidMismatch { car_did, auth_did }) => ApiError::InvalidRequest(format!(
382
+
"CAR is for {} but authenticated as {}",
383
+
car_did, auth_did
384
+
))
385
+
.into_response(),
386
386
Err(e) => {
387
387
error!("Import error: {:?}", e);
388
388
ApiError::InternalError(None).into_response()
+19
-15
src/api/repo/record/batch.rs
+19
-15
src/api/repo/record/batch.rs
···
205
205
}
206
206
}
207
207
208
-
let user_id: uuid::Uuid = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did.as_str())
209
-
.fetch_optional(&state.db)
210
-
.await
211
-
{
212
-
Ok(Some(id)) => id,
213
-
_ => return ApiError::InternalError(Some("User not found".into())).into_response(),
214
-
};
208
+
let user_id: uuid::Uuid =
209
+
match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did.as_str())
210
+
.fetch_optional(&state.db)
211
+
.await
212
+
{
213
+
Ok(Some(id)) => id,
214
+
_ => return ApiError::InternalError(Some("User not found".into())).into_response(),
215
+
};
215
216
let root_cid_str: String = match sqlx::query_scalar!(
216
217
"SELECT repo_root_cid FROM repos WHERE user_id = $1",
217
218
user_id
···
225
226
let current_root_cid = match Cid::from_str(&root_cid_str) {
226
227
Ok(c) => c,
227
228
Err(_) => {
228
-
return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response()
229
+
return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response();
229
230
}
230
231
};
231
232
if let Some(swap_commit) = &input.swap_commit
···
281
282
Ok(c) => c,
282
283
Err(_) => {
283
284
return ApiError::InternalError(Some("Failed to store record".into()))
284
-
.into_response()
285
+
.into_response();
285
286
}
286
287
};
287
288
let key = format!("{}/{}", collection, rkey);
···
290
291
Ok(m) => m,
291
292
Err(_) => {
292
293
return ApiError::InternalError(Some("Failed to add to MST".into()))
293
-
.into_response()
294
+
.into_response();
294
295
}
295
296
};
296
297
let uri = AtUri::from_parts(&did, collection, &rkey);
···
335
336
Ok(c) => c,
336
337
Err(_) => {
337
338
return ApiError::InternalError(Some("Failed to store record".into()))
338
-
.into_response()
339
+
.into_response();
339
340
}
340
341
};
341
342
let key = format!("{}/{}", collection, rkey);
···
345
346
Ok(m) => m,
346
347
Err(_) => {
347
348
return ApiError::InternalError(Some("Failed to update MST".into()))
348
-
.into_response()
349
+
.into_response();
349
350
}
350
351
};
351
352
let uri = AtUri::from_parts(&did, collection, rkey);
···
369
370
Ok(m) => m,
370
371
Err(_) => {
371
372
return ApiError::InternalError(Some("Failed to delete from MST".into()))
372
-
.into_response()
373
+
.into_response();
373
374
}
374
375
};
375
376
results.push(WriteResult::DeleteResult {});
···
383
384
}
384
385
let new_mst_root = match mst.persist().await {
385
386
Ok(c) => c,
386
-
Err(_) => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(),
387
+
Err(_) => {
388
+
return ApiError::InternalError(Some("Failed to persist MST".into())).into_response();
389
+
}
387
390
};
388
391
let mut relevant_blocks = std::collections::BTreeMap::new();
389
392
for key in &modified_keys {
···
432
435
Ok(res) => res,
433
436
Err(e) => {
434
437
error!("Commit failed: {}", e);
435
-
return ApiError::InternalError(Some("Failed to commit changes".into())).into_response();
438
+
return ApiError::InternalError(Some("Failed to commit changes".into()))
439
+
.into_response();
436
440
}
437
441
};
438
442
+4
-2
src/api/repo/record/delete.rs
+4
-2
src/api/repo/record/delete.rs
···
97
97
let expected_cid = Cid::from_str(swap_record_str).ok();
98
98
let actual_cid = mst.get(&key).await.ok().flatten();
99
99
if expected_cid != actual_cid {
100
-
return ApiError::InvalidSwap(Some("Record has been modified or does not exist".into()))
101
-
.into_response();
100
+
return ApiError::InvalidSwap(Some(
101
+
"Record has been modified or does not exist".into(),
102
+
))
103
+
.into_response();
102
104
}
103
105
}
104
106
let prev_record_cid = mst.get(&key).await.ok().flatten();
+16
-9
src/api/repo/record/write.rs
+16
-9
src/api/repo/record/write.rs
···
138
138
ApiError::InternalError(None).into_response()
139
139
})?
140
140
.ok_or_else(|| ApiError::InternalError(Some("Repo root not found".into())).into_response())?;
141
-
let current_root_cid = Cid::from_str(&root_cid_str)
142
-
.map_err(|_| ApiError::InternalError(Some("Invalid repo root CID".into())).into_response())?;
141
+
let current_root_cid = Cid::from_str(&root_cid_str).map_err(|_| {
142
+
ApiError::InternalError(Some("Invalid repo root CID".into())).into_response()
143
+
})?;
143
144
Ok(RepoWriteAuth {
144
145
did: auth_user.did.clone(),
145
146
user_id,
···
247
248
let record_cid = match tracking_store.put(&record_bytes).await {
248
249
Ok(c) => c,
249
250
_ => {
250
-
return ApiError::InternalError(Some("Failed to save record block".into())).into_response()
251
+
return ApiError::InternalError(Some("Failed to save record block".into()))
252
+
.into_response();
251
253
}
252
254
};
253
255
let key = format!("{}/{}", input.collection, rkey);
···
442
444
let expected_cid = Cid::from_str(swap_record_str).ok();
443
445
let actual_cid = mst.get(&key).await.ok().flatten();
444
446
if expected_cid != actual_cid {
445
-
return ApiError::InvalidSwap(Some("Record has been modified or does not exist".into()))
446
-
.into_response();
447
+
return ApiError::InvalidSwap(Some(
448
+
"Record has been modified or does not exist".into(),
449
+
))
450
+
.into_response();
447
451
}
448
452
}
449
453
let existing_cid = mst.get(&key).await.ok().flatten();
···
455
459
let record_cid = match tracking_store.put(&record_bytes).await {
456
460
Ok(c) => c,
457
461
_ => {
458
-
return ApiError::InternalError(Some("Failed to save record block".into())).into_response()
462
+
return ApiError::InternalError(Some("Failed to save record block".into()))
463
+
.into_response();
459
464
}
460
465
};
461
466
if existing_cid == Some(record_cid) {
···
474
479
match mst.update(&key, record_cid).await {
475
480
Ok(m) => m,
476
481
Err(_) => {
477
-
return ApiError::InternalError(Some("Failed to update MST".into())).into_response()
482
+
return ApiError::InternalError(Some("Failed to update MST".into()))
483
+
.into_response();
478
484
}
479
485
}
480
486
} else {
481
487
match mst.add(&key, record_cid).await {
482
488
Ok(m) => m,
483
489
Err(_) => {
484
-
return ApiError::InternalError(Some("Failed to add to MST".into())).into_response()
490
+
return ApiError::InternalError(Some("Failed to add to MST".into()))
491
+
.into_response();
485
492
}
486
493
}
487
494
};
488
495
let new_mst_root = match new_mst.persist().await {
489
496
Ok(c) => c,
490
497
Err(_) => {
491
-
return ApiError::InternalError(Some("Failed to persist MST".into())).into_response()
498
+
return ApiError::InternalError(Some("Failed to persist MST".into())).into_response();
492
499
}
493
500
};
494
501
let op = if existing_cid.is_some() {
+13
-9
src/api/responses.rs
+13
-9
src/api/responses.rs
···
28
28
}
29
29
30
30
impl DidResponse {
31
-
pub fn new(did: impl Into<Did>) -> impl IntoResponse {
31
+
pub fn response(did: impl Into<Did>) -> impl IntoResponse {
32
32
Json(Self { did: did.into() })
33
33
}
34
34
}
···
40
40
}
41
41
42
42
impl TokenRequiredResponse {
43
-
pub fn new(required: bool) -> impl IntoResponse {
44
-
Json(Self { token_required: required })
43
+
pub fn response(required: bool) -> impl IntoResponse {
44
+
Json(Self {
45
+
token_required: required,
46
+
})
45
47
}
46
48
}
47
49
···
52
54
}
53
55
54
56
impl HasPasswordResponse {
55
-
pub fn new(has_password: bool) -> impl IntoResponse {
57
+
pub fn response(has_password: bool) -> impl IntoResponse {
56
58
Json(Self { has_password })
57
59
}
58
60
}
···
63
65
}
64
66
65
67
impl VerifiedResponse {
66
-
pub fn new(verified: bool) -> impl IntoResponse {
68
+
pub fn response(verified: bool) -> impl IntoResponse {
67
69
Json(Self { verified })
68
70
}
69
71
}
···
74
76
}
75
77
76
78
impl EnabledResponse {
77
-
pub fn new(enabled: bool) -> impl IntoResponse {
79
+
pub fn response(enabled: bool) -> impl IntoResponse {
78
80
Json(Self { enabled })
79
81
}
80
82
}
···
85
87
}
86
88
87
89
impl StatusResponse {
88
-
pub fn new(status: impl Into<String>) -> impl IntoResponse {
89
-
Json(Self { status: status.into() })
90
+
pub fn response(status: impl Into<String>) -> impl IntoResponse {
91
+
Json(Self {
92
+
status: status.into(),
93
+
})
90
94
}
91
95
}
92
96
···
97
101
}
98
102
99
103
impl DidDocumentResponse {
100
-
pub fn new(did_document: serde_json::Value) -> impl IntoResponse {
104
+
pub fn response(did_document: serde_json::Value) -> impl IntoResponse {
101
105
Json(Self { did_document })
102
106
}
103
107
}
+25
-13
src/api/server/account_status.rs
+25
-13
src/api/server/account_status.rs
···
1
-
use crate::api::error::ApiError;
2
1
use crate::api::EmptyResponse;
2
+
use crate::api::error::ApiError;
3
3
use crate::cache::Cache;
4
4
use crate::plc::PlcClient;
5
5
use crate::state::AppState;
···
74
74
return ApiError::InternalError(None).into_response();
75
75
}
76
76
};
77
-
let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did.as_str())
78
-
.fetch_optional(&state.db)
79
-
.await;
77
+
let user_status = sqlx::query!(
78
+
"SELECT deactivated_at FROM users WHERE did = $1",
79
+
did.as_str()
80
+
)
81
+
.fetch_optional(&state.db)
82
+
.await;
80
83
let deactivated_at = match user_status {
81
84
Ok(Some(row)) => row.deactivated_at,
82
85
_ => None,
···
399
402
);
400
403
let did_validation_start = std::time::Instant::now();
401
404
if let Err(e) =
402
-
assert_valid_did_document_for_service(&state.db, state.cache.clone(), did.as_str(), true).await
405
+
assert_valid_did_document_for_service(&state.db, state.cache.clone(), did.as_str(), true)
406
+
.await
403
407
{
404
408
info!(
405
409
"[MIGRATION] activateAccount: DID document validation FAILED for {} (took {:?})",
···
423
427
"[MIGRATION] activateAccount: Activating account did={} handle={:?}",
424
428
did, handle
425
429
);
426
-
let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did.as_str())
427
-
.execute(&state.db)
428
-
.await;
430
+
let result = sqlx::query!(
431
+
"UPDATE users SET deactivated_at = NULL WHERE did = $1",
432
+
did.as_str()
433
+
)
434
+
.execute(&state.db)
435
+
.await;
429
436
match result {
430
437
Ok(_) => {
431
438
info!(
···
440
447
did
441
448
);
442
449
if let Err(e) =
443
-
crate::api::repo::record::sequence_account_event(&state, did.as_str(), true, None).await
450
+
crate::api::repo::record::sequence_account_event(&state, did.as_str(), true, None)
451
+
.await
444
452
{
445
453
warn!(
446
454
"[MIGRATION] activateAccount: Failed to sequence account activation event: {}",
···
453
461
"[MIGRATION] activateAccount: Sequencing identity event for did={} handle={:?}",
454
462
did, handle
455
463
);
456
-
if let Err(e) =
457
-
crate::api::repo::record::sequence_identity_event(&state, did.as_str(), handle.as_deref())
458
-
.await
464
+
if let Err(e) = crate::api::repo::record::sequence_identity_event(
465
+
&state,
466
+
did.as_str(),
467
+
handle.as_deref(),
468
+
)
469
+
.await
459
470
{
460
471
warn!(
461
472
"[MIGRATION] activateAccount: Failed to sequence identity event for activation: {}",
···
644
655
let did = validated.did.clone();
645
656
646
657
if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, did.as_str()).await {
647
-
return crate::api::server::reauth::legacy_mfa_required_response(&state.db, did.as_str()).await;
658
+
return crate::api::server::reauth::legacy_mfa_required_response(&state.db, did.as_str())
659
+
.await;
648
660
}
649
661
650
662
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did.as_str())
+1
-1
src/api/server/app_password.rs
+1
-1
src/api/server/app_password.rs
+2
-2
src/api/server/email.rs
+2
-2
src/api/server/email.rs
···
77
77
}
78
78
79
79
info!("Email update requested for user {}", user.id);
80
-
TokenRequiredResponse::new(token_required).into_response()
80
+
TokenRequiredResponse::response(token_required).into_response()
81
81
}
82
82
83
83
#[derive(Deserialize)]
···
375
375
.await;
376
376
377
377
match user {
378
-
Ok(Some(row)) => VerifiedResponse::new(row.email_verified).into_response(),
378
+
Ok(Some(row)) => VerifiedResponse::response(row.email_verified).into_response(),
379
379
Ok(None) => ApiError::AccountNotFound.into_response(),
380
380
Err(e) => {
381
381
error!("DB error checking email verified: {:?}", e);
+3
-1
src/api/server/invite.rs
+3
-1
src/api/server/invite.rs
···
53
53
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
54
54
}
55
55
56
-
let for_account = input.for_account.unwrap_or_else(|| auth_user.did.to_string());
56
+
let for_account = input
57
+
.for_account
58
+
.unwrap_or_else(|| auth_user.did.to_string());
57
59
let code = gen_invite_code();
58
60
59
61
match sqlx::query!(
+18
-15
src/api/server/passkey_account.rs
+18
-15
src/api/server/passkey_account.rs
···
102
102
.await
103
103
{
104
104
warn!(ip = %client_ip, "Account creation rate limit exceeded");
105
-
return ApiError::RateLimitExceeded(Some("Too many account creation attempts. Please try again later.".into(),))
105
+
return ApiError::RateLimitExceeded(Some(
106
+
"Too many account creation attempts. Please try again later.".into(),
107
+
))
106
108
.into_response();
107
109
}
108
110
···
352
354
Ok(r) => r,
353
355
Err(e) => {
354
356
error!("Error creating PLC genesis operation: {:?}", e);
355
-
return ApiError::InternalError(Some("Failed to create PLC operation".into()))
356
-
.into_response();
357
+
return ApiError::InternalError(Some(
358
+
"Failed to create PLC operation".into(),
359
+
))
360
+
.into_response();
357
361
}
358
362
};
359
363
···
759
763
}
760
764
};
761
765
762
-
let reg_state = match crate::auth::webauthn::load_registration_state(&state.db, &input.did)
763
-
.await
764
-
{
765
-
Ok(Some(s)) => s,
766
-
Ok(None) => {
767
-
return ApiError::NoChallengeInProgress.into_response();
768
-
}
769
-
Err(e) => {
770
-
error!("Error loading registration state: {:?}", e);
771
-
return ApiError::InternalError(None).into_response();
772
-
}
773
-
};
766
+
let reg_state =
767
+
match crate::auth::webauthn::load_registration_state(&state.db, &input.did).await {
768
+
Ok(Some(s)) => s,
769
+
Ok(None) => {
770
+
return ApiError::NoChallengeInProgress.into_response();
771
+
}
772
+
Err(e) => {
773
+
error!("Error loading registration state: {:?}", e);
774
+
return ApiError::InternalError(None).into_response();
775
+
}
776
+
};
774
777
775
778
let credential: webauthn_rs::prelude::RegisterPublicKeyCredential =
776
779
match serde_json::from_value(input.passkey_credential) {
+3
-1
src/api/server/password.rs
+3
-1
src/api/server/password.rs
···
340
340
.await;
341
341
342
342
match user {
343
-
Ok(Some(row)) => HasPasswordResponse::new(row.has_password.unwrap_or(false)).into_response(),
343
+
Ok(Some(row)) => {
344
+
HasPasswordResponse::response(row.has_password.unwrap_or(false)).into_response()
345
+
}
344
346
Ok(None) => ApiError::AccountNotFound.into_response(),
345
347
Err(e) => {
346
348
error!("DB error: {:?}", e);
+9
-4
src/api/server/reauth.rs
+9
-4
src/api/server/reauth.rs
···
69
69
auth: BearerAuth,
70
70
Json(input): Json<PasswordReauthInput>,
71
71
) -> Response {
72
-
let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", &*&auth.0.did)
73
-
.fetch_optional(&state.db)
74
-
.await;
72
+
let user = sqlx::query!(
73
+
"SELECT password_hash FROM users WHERE did = $1",
74
+
&*&auth.0.did
75
+
)
76
+
.fetch_optional(&state.db)
77
+
.await;
75
78
76
79
let password_hash = match user {
77
80
Ok(Some(row)) => row.password_hash,
···
138
141
.await
139
142
{
140
143
warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded");
141
-
return ApiError::RateLimitExceeded(Some("Too many verification attempts. Please try again in a few minutes.".into(),))
144
+
return ApiError::RateLimitExceeded(Some(
145
+
"Too many verification attempts. Please try again in a few minutes.".into(),
146
+
))
142
147
.into_response();
143
148
}
144
149
+5
-3
src/api/server/service_auth.rs
+5
-3
src/api/server/service_auth.rs
···
1
-
use crate::types::Did;
2
1
use crate::AccountStatus;
3
2
use crate::api::error::ApiError;
4
3
use crate::state::AppState;
4
+
use crate::types::Did;
5
5
use axum::{
6
6
Json,
7
7
extract::{Query, State},
···
165
165
}
166
166
Err(e) => {
167
167
error!(error = ?e, "DB error fetching user key");
168
-
return ApiError::AuthenticationFailed(Some("Failed to get signing key".into()))
169
-
.into_response();
168
+
return ApiError::AuthenticationFailed(Some(
169
+
"Failed to get signing key".into(),
170
+
))
171
+
.into_response();
170
172
}
171
173
}
172
174
}
+4
-7
src/api/server/session.rs
+4
-7
src/api/server/session.rs
···
479
479
}
480
480
};
481
481
if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() {
482
-
return ApiError::AuthenticationFailed(Some("Invalid refresh token".into())).into_response();
482
+
return ApiError::AuthenticationFailed(Some("Invalid refresh token".into()))
483
+
.into_response();
483
484
}
484
485
let new_access_meta = match crate::auth::create_access_token_with_delegation(
485
486
&session_row.did,
···
566
567
let pds_hostname =
567
568
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
568
569
let handle = full_handle(&u.handle, &pds_hostname);
569
-
let account_state = AccountState::from_db_fields(
570
-
u.deactivated_at,
571
-
u.takedown_ref.clone(),
572
-
None,
573
-
None,
574
-
);
570
+
let account_state =
571
+
AccountState::from_db_fields(u.deactivated_at, u.takedown_ref.clone(), None, None);
575
572
let mut response = json!({
576
573
"accessJwt": new_access_meta.token,
577
574
"refreshJwt": new_refresh_meta.token,
+26
-13
src/api/server/totp.rs
+26
-13
src/api/server/totp.rs
···
28
28
}
29
29
30
30
pub async fn create_totp_secret(State(state): State<AppState>, auth: BearerAuth) -> Response {
31
-
let existing = sqlx::query_scalar!("SELECT verified FROM user_totp WHERE did = $1", &*&auth.0.did)
32
-
.fetch_optional(&state.db)
33
-
.await;
31
+
let existing = sqlx::query_scalar!(
32
+
"SELECT verified FROM user_totp WHERE did = $1",
33
+
&*&auth.0.did
34
+
)
35
+
.fetch_optional(&state.db)
36
+
.await;
34
37
35
38
if let Ok(Some(true)) = existing {
36
39
return ApiError::TotpAlreadyEnabled.into_response();
···
58
61
Ok(qr) => qr,
59
62
Err(e) => {
60
63
error!("Failed to generate QR code: {:?}", e);
61
-
return ApiError::InternalError(Some("Failed to generate QR code".into())).into_response();
64
+
return ApiError::InternalError(Some("Failed to generate QR code".into()))
65
+
.into_response();
62
66
}
63
67
};
64
68
···
247
251
return ApiError::RateLimitExceeded(None).into_response();
248
252
}
249
253
250
-
let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", &*&auth.0.did)
251
-
.fetch_optional(&state.db)
252
-
.await;
254
+
let user = sqlx::query!(
255
+
"SELECT password_hash FROM users WHERE did = $1",
256
+
&*&auth.0.did
257
+
)
258
+
.fetch_optional(&state.db)
259
+
.await;
253
260
254
261
let password_hash = match user {
255
262
Ok(Some(row)) => row.password_hash,
···
346
353
}
347
354
348
355
pub async fn get_totp_status(State(state): State<AppState>, auth: BearerAuth) -> Response {
349
-
let totp_row = sqlx::query!("SELECT verified FROM user_totp WHERE did = $1", &*&auth.0.did)
350
-
.fetch_optional(&state.db)
351
-
.await;
356
+
let totp_row = sqlx::query!(
357
+
"SELECT verified FROM user_totp WHERE did = $1",
358
+
&*&auth.0.did
359
+
)
360
+
.fetch_optional(&state.db)
361
+
.await;
352
362
353
363
let enabled = match totp_row {
354
364
Ok(Some(row)) => row.verified,
···
401
411
return ApiError::RateLimitExceeded(None).into_response();
402
412
}
403
413
404
-
let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", &*&auth.0.did)
405
-
.fetch_optional(&state.db)
406
-
.await;
414
+
let user = sqlx::query!(
415
+
"SELECT password_hash FROM users WHERE did = $1",
416
+
&*&auth.0.did
417
+
)
418
+
.fetch_optional(&state.db)
419
+
.await;
407
420
408
421
let password_hash = match user {
409
422
Ok(Some(row)) => row.password_hash,
+6
-3
src/api/server/trusted_devices.rs
+6
-3
src/api/server/trusted_devices.rs
···
1
-
use crate::api::error::ApiError;
2
1
use crate::api::SuccessResponse;
2
+
use crate::api::error::ApiError;
3
3
use axum::{
4
4
Json,
5
5
extract::State,
···
87
87
let devices = rows
88
88
.into_iter()
89
89
.map(|row| {
90
-
let trust_state = DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until);
90
+
let trust_state =
91
+
DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until);
91
92
TrustedDevice {
92
93
id: row.id,
93
94
user_agent: row.user_agent,
···
230
231
}
231
232
232
233
pub async fn is_device_trusted(db: &PgPool, device_id: &str, did: &str) -> bool {
233
-
get_device_trust_state(db, device_id, did).await.is_trusted()
234
+
get_device_trust_state(db, device_id, did)
235
+
.await
236
+
.is_trusted()
234
237
}
235
238
236
239
pub async fn trust_device(db: &PgPool, device_id: &str) -> Result<(), sqlx::Error> {
+1
-1
src/api/server/verify_email.rs
+1
-1
src/api/server/verify_email.rs
+13
-3
src/api/validation.rs
+13
-3
src/api/validation.rs
···
80
80
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81
81
match self {
82
82
Self::Empty => write!(f, "Email cannot be empty"),
83
-
Self::TooLong => write!(f, "Email exceeds maximum length of {} characters", MAX_EMAIL_LENGTH),
83
+
Self::TooLong => write!(
84
+
f,
85
+
"Email exceeds maximum length of {} characters",
86
+
MAX_EMAIL_LENGTH
87
+
),
84
88
Self::MissingAtSign => write!(f, "Email must contain @"),
85
89
Self::EmptyLocalPart => write!(f, "Email local part cannot be empty"),
86
90
Self::LocalPartTooLong => write!(f, "Email local part exceeds maximum length"),
···
115
119
}
116
120
117
121
pub fn local_part(&self) -> &str {
118
-
self.0.rsplitn(2, '@').nth(1).unwrap_or("")
122
+
self.0
123
+
.rsplit_once('@')
124
+
.map(|(local, _)| local)
125
+
.unwrap_or("")
119
126
}
120
127
121
128
pub fn domain(&self) -> &str {
122
-
self.0.rsplitn(2, '@').next().unwrap_or("")
129
+
self.0
130
+
.rsplit_once('@')
131
+
.map(|(_, domain)| domain)
132
+
.unwrap_or("")
123
133
}
124
134
}
125
135
+6
-2
src/auth/extractor.rs
+6
-2
src/auth/extractor.rs
···
146
146
Err(_) => Err(AuthError::AuthenticationFailed),
147
147
}
148
148
} else {
149
-
match validate_bearer_token_cached(&state.db, state.cache.as_ref(), &extracted.token).await {
149
+
match validate_bearer_token_cached(&state.db, state.cache.as_ref(), &extracted.token)
150
+
.await
151
+
{
150
152
Ok(user) => Ok(BearerAuth(user)),
151
153
Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated),
152
154
Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown),
···
262
264
Err(_) => return Err(AuthError::AuthenticationFailed),
263
265
}
264
266
} else {
265
-
match validate_bearer_token_cached(&state.db, state.cache.as_ref(), &extracted.token).await {
267
+
match validate_bearer_token_cached(&state.db, state.cache.as_ref(), &extracted.token)
268
+
.await
269
+
{
266
270
Ok(user) => user,
267
271
Err(TokenValidationError::AccountDeactivated) => {
268
272
return Err(AuthError::AccountDeactivated);
+3
-5
src/auth/mod.rs
+3
-5
src/auth/mod.rs
···
3
3
use std::fmt;
4
4
use std::time::Duration;
5
5
6
-
use crate::types::Did;
7
6
use crate::AccountStatus;
8
7
use crate::cache::Cache;
9
8
use crate::oauth::scopes::ScopePermissions;
9
+
use crate::types::Did;
10
10
11
11
pub mod extractor;
12
12
pub mod scope_check;
···
334
334
.act
335
335
.as_ref()
336
336
.map(|a| Did::new_unchecked(a.sub.clone()));
337
-
let status = AccountStatus::from_db_fields(
338
-
takedown_ref.as_deref(),
339
-
deactivated_at,
340
-
);
337
+
let status =
338
+
AccountStatus::from_db_fields(takedown_ref.as_deref(), deactivated_at);
341
339
return Ok(AuthenticatedUser {
342
340
did: Did::new_unchecked(did.clone()),
343
341
key_bytes: Some(decrypted_key),
+2
-2
src/lib.rs
+2
-2
src/lib.rs
···
24
24
pub mod validation;
25
25
26
26
use api::proxy::XrpcProxyLayer;
27
-
pub use sync::util::AccountStatus;
28
-
pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey};
29
27
use axum::{
30
28
Json, Router,
31
29
extract::DefaultBodyLimit,
···
36
34
use http::StatusCode;
37
35
use serde_json::json;
38
36
use state::AppState;
37
+
pub use sync::util::AccountStatus;
39
38
use tower::ServiceBuilder;
40
39
use tower_http::cors::{Any, CorsLayer};
41
40
use tower_http::services::{ServeDir, ServeFile};
41
+
pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey};
42
42
43
43
pub fn app(state: AppState) -> Router {
44
44
let xrpc_router = Router::new()
+3
-1
src/oauth/db/request.rs
+3
-1
src/oauth/db/request.rs
···
1
-
use super::super::{AuthFlowState, AuthorizationRequestParameters, ClientAuth, OAuthError, RequestData};
1
+
use super::super::{
2
+
AuthFlowState, AuthorizationRequestParameters, ClientAuth, OAuthError, RequestData,
3
+
};
2
4
use super::helpers::{from_json, to_json};
3
5
use sqlx::PgPool;
4
6
+29
-8
src/oauth/db/token.rs
+29
-8
src/oauth/db/token.rs
···
4
4
use sqlx::PgPool;
5
5
6
6
pub enum RefreshTokenLookup {
7
-
Valid { db_id: i32, token_data: TokenData },
8
-
InGracePeriod { db_id: i32, token_data: TokenData, rotated_at: DateTime<Utc> },
9
-
Used { original_token_id: i32 },
10
-
Expired { db_id: i32 },
7
+
Valid {
8
+
db_id: i32,
9
+
token_data: TokenData,
10
+
},
11
+
InGracePeriod {
12
+
db_id: i32,
13
+
token_data: TokenData,
14
+
rotated_at: DateTime<Utc>,
15
+
},
16
+
Used {
17
+
original_token_id: i32,
18
+
},
19
+
Expired {
20
+
db_id: i32,
21
+
},
11
22
NotFound,
12
23
}
13
24
···
16
27
match self {
17
28
RefreshTokenLookup::Valid { .. } => RefreshTokenState::Valid,
18
29
RefreshTokenLookup::InGracePeriod { rotated_at, .. } => {
19
-
RefreshTokenState::InGracePeriod { rotated_at: *rotated_at }
30
+
RefreshTokenState::InGracePeriod {
31
+
rotated_at: *rotated_at,
32
+
}
20
33
}
21
34
RefreshTokenLookup::Used { .. } => RefreshTokenState::Used { at: Utc::now() },
22
35
RefreshTokenLookup::Expired { .. } => RefreshTokenState::Expired,
···
30
43
refresh_token: &str,
31
44
) -> Result<RefreshTokenLookup, OAuthError> {
32
45
if let Some(token_id) = check_refresh_token_used(pool, refresh_token).await? {
33
-
if let Some((db_id, token_data)) = get_token_by_previous_refresh_token(pool, refresh_token).await? {
46
+
if let Some((db_id, token_data)) =
47
+
get_token_by_previous_refresh_token(pool, refresh_token).await?
48
+
{
34
49
let rotated_at = token_data.updated_at;
35
-
return Ok(RefreshTokenLookup::InGracePeriod { db_id, token_data, rotated_at });
50
+
return Ok(RefreshTokenLookup::InGracePeriod {
51
+
db_id,
52
+
token_data,
53
+
rotated_at,
54
+
});
36
55
}
37
-
return Ok(RefreshTokenLookup::Used { original_token_id: token_id });
56
+
return Ok(RefreshTokenLookup::Used {
57
+
original_token_id: token_id,
58
+
});
38
59
}
39
60
40
61
match get_token_by_refresh_token(pool, refresh_token).await? {
+26
-9
src/oauth/endpoints/token/grants.rs
+26
-9
src/oauth/endpoints/token/grants.rs
···
24
24
dpop_proof: Option<String>,
25
25
) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> {
26
26
let (code, code_verifier, redirect_uri) = match request.grant {
27
-
TokenGrant::AuthorizationCode { code, code_verifier, redirect_uri } => {
28
-
(code, code_verifier, redirect_uri)
27
+
TokenGrant::AuthorizationCode {
28
+
code,
29
+
code_verifier,
30
+
redirect_uri,
31
+
} => (code, code_verifier, redirect_uri),
32
+
_ => {
33
+
return Err(OAuthError::InvalidRequest(
34
+
"Expected authorization_code grant".to_string(),
35
+
));
29
36
}
30
-
_ => return Err(OAuthError::InvalidRequest("Expected authorization_code grant".to_string())),
31
37
};
32
38
let auth_request = db::consume_authorization_request_by_code(&state.db, &code)
33
39
.await?
···
53
59
let did = flow_state.did().unwrap().to_string();
54
60
let client_metadata_cache = ClientMetadataCache::new(3600);
55
61
let client_metadata = client_metadata_cache.get(&auth_request.client_id).await?;
56
-
let client_auth = if let (Some(assertion), Some(assertion_type)) =
57
-
(&request.client_auth.client_assertion, &request.client_auth.client_assertion_type)
58
-
{
62
+
let client_auth = if let (Some(assertion), Some(assertion_type)) = (
63
+
&request.client_auth.client_assertion,
64
+
&request.client_auth.client_assertion_type,
65
+
) {
59
66
if assertion_type != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
60
67
return Err(OAuthError::InvalidClient(
61
68
"Unsupported client_assertion_type".to_string(),
···
198
205
) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> {
199
206
let refresh_token_str = match request.grant {
200
207
TokenGrant::RefreshToken { refresh_token } => refresh_token,
201
-
_ => return Err(OAuthError::InvalidRequest("Expected refresh_token grant".to_string())),
208
+
_ => {
209
+
return Err(OAuthError::InvalidRequest(
210
+
"Expected refresh_token grant".to_string(),
211
+
));
212
+
}
202
213
};
203
214
let token_prefix = &refresh_token_str[..std::cmp::min(16, refresh_token_str.len())];
204
215
tracing::info!(
···
213
224
214
225
let (db_id, token_data) = match lookup {
215
226
RefreshTokenLookup::Valid { db_id, token_data } => (db_id, token_data),
216
-
RefreshTokenLookup::InGracePeriod { db_id: _, token_data, rotated_at } => {
227
+
RefreshTokenLookup::InGracePeriod {
228
+
db_id: _,
229
+
token_data,
230
+
rotated_at,
231
+
} => {
217
232
tracing::info!(
218
233
refresh_token_prefix = %token_prefix,
219
234
rotated_at = %rotated_at,
···
262
277
}
263
278
RefreshTokenLookup::NotFound => {
264
279
tracing::warn!(refresh_token_prefix = %token_prefix, "Refresh token not found");
265
-
return Err(OAuthError::InvalidGrant("Invalid refresh token".to_string()));
280
+
return Err(OAuthError::InvalidGrant(
281
+
"Invalid refresh token".to_string(),
282
+
));
266
283
}
267
284
};
268
285
let dpop_jkt = if let Some(proof) = &dpop_proof {
+3
-1
src/oauth/endpoints/token/mod.rs
+3
-1
src/oauth/endpoints/token/mod.rs
···
13
13
pub use introspect::{
14
14
IntrospectRequest, IntrospectResponse, RevokeRequest, introspect_token, revoke_token,
15
15
};
16
-
pub use types::{ClientAuthParams, GrantType, TokenGrant, TokenRequest, TokenResponse, ValidatedTokenRequest};
16
+
pub use types::{
17
+
ClientAuthParams, GrantType, TokenGrant, TokenRequest, TokenResponse, ValidatedTokenRequest,
18
+
};
17
19
18
20
fn extract_client_ip(headers: &HeaderMap) -> String {
19
21
if let Some(forwarded) = headers.get("x-forwarded-for")
+9
-3
src/oauth/endpoints/token/types.rs
+9
-3
src/oauth/endpoints/token/types.rs
···
101
101
let grant = match self.grant_type {
102
102
GrantType::AuthorizationCode => {
103
103
let code = self.code.ok_or_else(|| {
104
-
OAuthError::InvalidRequest("code is required for authorization_code grant".to_string())
104
+
OAuthError::InvalidRequest(
105
+
"code is required for authorization_code grant".to_string(),
106
+
)
105
107
})?;
106
108
let code_verifier = self.code_verifier.ok_or_else(|| {
107
-
OAuthError::InvalidRequest("code_verifier is required for authorization_code grant".to_string())
109
+
OAuthError::InvalidRequest(
110
+
"code_verifier is required for authorization_code grant".to_string(),
111
+
)
108
112
})?;
109
113
TokenGrant::AuthorizationCode {
110
114
code,
···
114
118
}
115
119
GrantType::RefreshToken => {
116
120
let refresh_token = self.refresh_token.ok_or_else(|| {
117
-
OAuthError::InvalidRequest("refresh_token is required for refresh_token grant".to_string())
121
+
OAuthError::InvalidRequest(
122
+
"refresh_token is required for refresh_token grant".to_string(),
123
+
)
118
124
})?;
119
125
TokenGrant::RefreshToken { refresh_token }
120
126
}
+3
-1
src/oauth/scopes/parser.rs
+3
-1
src/oauth/scopes/parser.rs
···
144
144
.split('&')
145
145
.filter_map(|part| part.split_once('='))
146
146
.fold(HashMap::new(), |mut acc, (key, value)| {
147
-
acc.entry(key.to_string()).or_default().push(value.to_string());
147
+
acc.entry(key.to_string())
148
+
.or_default()
149
+
.push(value.to_string());
148
150
acc
149
151
})
150
152
}
+21
-5
src/oauth/types.rs
+21
-5
src/oauth/types.rs
···
249
249
#[derive(Debug, Clone, PartialEq, Eq)]
250
250
pub enum AuthFlowState {
251
251
Pending,
252
-
Authenticated { did: String, device_id: Option<String> },
253
-
Authorized { did: String, device_id: Option<String>, code: String },
252
+
Authenticated {
253
+
did: String,
254
+
device_id: Option<String>,
255
+
},
256
+
Authorized {
257
+
did: String,
258
+
device_id: Option<String>,
259
+
code: String,
260
+
},
254
261
Expired,
255
262
}
256
263
···
324
331
AuthFlowState::Pending => write!(f, "pending"),
325
332
AuthFlowState::Authenticated { did, .. } => write!(f, "authenticated ({})", did),
326
333
AuthFlowState::Authorized { did, code, .. } => {
327
-
write!(f, "authorized ({}, code={}...)", did, &code[..8.min(code.len())])
334
+
write!(
335
+
f,
336
+
"authorized ({}, code={}...)",
337
+
did,
338
+
&code[..8.min(code.len())]
339
+
)
328
340
}
329
341
AuthFlowState::Expired => write!(f, "expired"),
330
342
}
···
334
346
#[derive(Debug, Clone, PartialEq, Eq)]
335
347
pub enum RefreshTokenState {
336
348
Valid,
337
-
Used { at: chrono::DateTime<chrono::Utc> },
338
-
InGracePeriod { rotated_at: chrono::DateTime<chrono::Utc> },
349
+
Used {
350
+
at: chrono::DateTime<chrono::Utc>,
351
+
},
352
+
InGracePeriod {
353
+
rotated_at: chrono::DateTime<chrono::Utc>,
354
+
},
339
355
Expired,
340
356
Revoked,
341
357
}
+3
-1
src/oauth/verify.rs
+3
-1
src/oauth/verify.rs
···
374
374
375
375
async fn try_legacy_auth(pool: &PgPool, token: &str) -> Result<LegacyAuthResult, ()> {
376
376
match crate::auth::validate_bearer_token(pool, token).await {
377
-
Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did.to_string() }),
377
+
Ok(user) if !user.is_oauth => Ok(LegacyAuthResult {
378
+
did: user.did.to_string(),
379
+
}),
378
380
_ => Err(()),
379
381
}
380
382
}
+1
-1
src/sync/commit.rs
+1
-1
src/sync/commit.rs
+2
-4
src/sync/deprecated.rs
+2
-4
src/sync/deprecated.rs
···
57
57
};
58
58
match account.repo_root_cid {
59
59
Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(),
60
-
None => {
61
-
ApiError::RepoNotFound(Some(format!("Could not find root for DID: {}", did)))
62
-
.into_response()
63
-
}
60
+
None => ApiError::RepoNotFound(Some(format!("Could not find root for DID: {}", did)))
61
+
.into_response(),
64
62
}
65
63
}
66
64
+4
-4
src/sync/frame.rs
+4
-4
src/sync/frame.rs
···
122
122
}
123
123
124
124
impl CommitFrameBuilder {
125
+
#[allow(clippy::too_many_arguments)]
125
126
pub fn new(
126
127
seq: i64,
127
128
did: String,
···
134
135
) -> Result<Self, CommitFrameError> {
135
136
let commit_cid = Cid::from_str(commit_cid_str)
136
137
.map_err(|_| CommitFrameError::InvalidCommitCid(commit_cid_str.to_string()))?;
137
-
let prev_cid = prev_cid_str
138
-
.map(|s| Cid::from_str(s))
139
-
.transpose()
140
-
.map_err(|_| CommitFrameError::InvalidCommitCid(prev_cid_str.unwrap_or("").to_string()))?;
138
+
let prev_cid = prev_cid_str.map(Cid::from_str).transpose().map_err(|_| {
139
+
CommitFrameError::InvalidCommitCid(prev_cid_str.unwrap_or("").to_string())
140
+
})?;
141
141
let blob_cids: Vec<Cid> = blob_strs
142
142
.iter()
143
143
.filter_map(|s| Cid::from_str(s).ok())
+3
-2
src/sync/repo.rs
+3
-2
src/sync/repo.rs
···
48
48
{
49
49
Ok(cids) => cids,
50
50
Err(invalid) => {
51
-
return ApiError::InvalidRequest(format!("Invalid CID: {}", invalid)).into_response()
51
+
return ApiError::InvalidRequest(format!("Invalid CID: {}", invalid)).into_response();
52
52
}
53
53
};
54
54
···
67
67
let missing_cids: Vec<String> = blocks
68
68
.iter()
69
69
.zip(&cids)
70
-
.filter_map(|(block_opt, cid)| block_opt.is_none().then(|| cid.to_string()))
70
+
.filter(|(block_opt, _)| block_opt.is_none())
71
+
.map(|(_, cid)| cid.to_string())
71
72
.collect();
72
73
if !missing_cids.is_empty() {
73
74
return ApiError::InvalidRequest(format!(
+4
-1
src/sync/util.rs
+4
-1
src/sync/util.rs
···
67
67
matches!(self, Self::Active)
68
68
}
69
69
70
-
pub fn from_db_fields(takedown_ref: Option<&str>, deactivated_at: Option<chrono::DateTime<chrono::Utc>>) -> Self {
70
+
pub fn from_db_fields(
71
+
takedown_ref: Option<&str>,
72
+
deactivated_at: Option<chrono::DateTime<chrono::Utc>>,
73
+
) -> Self {
71
74
if takedown_ref.is_some() {
72
75
Self::Takendown
73
76
} else if deactivated_at.is_some() {
+19
-5
src/types.rs
+19
-5
src/types.rs
···
916
916
}
917
917
918
918
pub fn now() -> Self {
919
-
Self(chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
919
+
Self(
920
+
chrono::Utc::now()
921
+
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
922
+
.to_string(),
923
+
)
920
924
}
921
925
922
926
pub fn as_str(&self) -> &str {
···
1296
1300
}
1297
1301
1298
1302
pub fn can_access_repo(&self) -> bool {
1299
-
matches!(self, AccountState::Active | AccountState::Deactivated { .. })
1303
+
matches!(
1304
+
self,
1305
+
AccountState::Active | AccountState::Deactivated { .. }
1306
+
)
1300
1307
}
1301
1308
1302
1309
pub fn status_string(&self) -> &'static str {
···
1405
1412
#[derive(Debug, Clone, PartialEq, Eq)]
1406
1413
pub enum TokenSource {
1407
1414
Session,
1408
-
OAuth { client_id: Option<String> },
1409
-
ServiceAuth { lxm: Option<String>, aud: Option<String> },
1415
+
OAuth {
1416
+
client_id: Option<String>,
1417
+
},
1418
+
ServiceAuth {
1419
+
lxm: Option<String>,
1420
+
aud: Option<String>,
1421
+
},
1410
1422
}
1411
1423
1412
1424
impl TokenSource {
···
1642
1654
1643
1655
#[test]
1644
1656
fn test_cidlink_validation() {
1645
-
assert!(CidLink::new("bafyreib74ckyq525l3y6an5txykwwtb3dgex6ofzakml53di77oxwr5pfe").is_ok());
1657
+
assert!(
1658
+
CidLink::new("bafyreib74ckyq525l3y6an5txykwwtb3dgex6ofzakml53di77oxwr5pfe").is_ok()
1659
+
);
1646
1660
assert!(CidLink::new("not-a-cid").is_err());
1647
1661
}
1648
1662
+3
-1
tests/import_verification.rs
+3
-1
tests/import_verification.rs
···
102
102
assert_eq!(import_res.status(), StatusCode::FORBIDDEN);
103
103
let body: serde_json::Value = import_res.json().await.unwrap();
104
104
assert!(
105
-
body["error"] == "InvalidRepo" || body["error"] == "InvalidRequest" || body["error"] == "DidMismatch",
105
+
body["error"] == "InvalidRepo"
106
+
|| body["error"] == "InvalidRequest"
107
+
|| body["error"] == "DidMismatch",
106
108
"Expected InvalidRepo, DidMismatch, or InvalidRequest error, got: {:?}",
107
109
body
108
110
);