PDS software with bells & whistles you didn’t even know you needed. will move this to its own account when ready. tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

Typechecks and linting

Changed files
+3107 -2322
frontend
src
tests
-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 );