Our Personal Data Server from scratch! tranquil.farm
pds rust database fun oauth atproto
238
fork

Configure Feed

Select the types of activity you want to include in your feed.

Typechecks and linting

+3107 -2322
-1
Cargo.toml
··· 3 3 version = "0.1.0" 4 4 edition = "2024" 5 5 license = "AGPL-3.0-or-later" 6 - license-file = "LICENSE-AGPL-3.0-or-later" 7 6 [dependencies] 8 7 anyhow = "1.0.100" 9 8 async-trait = "0.1.89"
+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
··· 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
··· 28 28 "@testing-library/user-event": "^14.6.1", 29 29 "jsdom": "^25.0.1", 30 30 "svelte": "^5.46.1", 31 + "svelte-check": "^4.3.5", 32 + "typescript": "^5.9.3", 31 33 "vite": "^7.3.0", 32 34 "vitest": "^4.0.16" 33 35 }
+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
··· 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
··· 81 81 }, 3000) 82 82 return () => clearInterval(interval) 83 83 } 84 + return undefined 84 85 }) 85 86 86 87 async function loadServerInfo() {
+1
frontend/src/components/migration/OfflineInboundWizard.svelte
··· 62 62 }, 3000) 63 63 return () => clearInterval(interval) 64 64 } 65 + return undefined 65 66 }) 66 67 67 68 async function loadServerInfo() {
+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
··· 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
··· 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
··· 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
··· 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
··· 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 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 17 17 18 18 type PageState = 19 19 | { kind: 'login' } 20 - | { kind: 'verification'; did: string } 20 + | { kind: 'verification'; did: Did } 21 21 22 22 let pageState = $state<PageState>({ kind: 'login' }) 23 23 let submitting = $state(false)
+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
··· 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
··· 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 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 88 88 . ~/.deno/env && cd frontend && deno task dev 89 89 frontend-build: 90 90 . ~/.deno/env && cd frontend && deno task build 91 + frontend-check: 92 + . ~/.deno/env && cd frontend && deno task check 91 93 frontend-clean: 92 94 rm -rf frontend/dist frontend/node_modules 93 95
+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
··· 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
··· 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
··· 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
··· 119 119 let did = match &params.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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 - use crate::api::error::ApiError; 2 1 use crate::api::EmptyResponse; 2 + use crate::api::error::ApiError; 3 3 use crate::auth::BearerAuth; 4 4 use crate::delegation::{self, DelegationActionType}; 5 5 use crate::state::{AppState, RateLimitKind};
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 33 33 34 34 Ok(Json(VerifyMigrationEmailOutput { 35 35 success: result.success, 36 - did: result.did.clone().into(), 36 + did: result.did.clone(), 37 37 })) 38 38 } 39 39
+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
··· 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 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 196 196 Ok(Some(a)) => a, 197 197 Ok(None) => { 198 198 return ApiError::RepoNotFound(Some(format!("Could not find repo for DID: {}", did))) 199 - .into_response() 199 + .into_response(); 200 200 } 201 201 Err(e) => { 202 202 error!("DB error in get_repo_status: {:?}", e);
+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
··· 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
··· 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
··· 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
··· 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
··· 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 );