@atproto/lex-password-session 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/error.d.ts +2 -1
  3. package/dist/error.d.ts.map +1 -1
  4. package/dist/error.js.map +1 -1
  5. package/dist/password-session.d.ts +1 -1
  6. package/dist/password-session.d.ts.map +1 -1
  7. package/dist/password-session.js +1 -1
  8. package/dist/password-session.js.map +1 -1
  9. package/dist/util.d.ts +1 -1
  10. package/dist/util.d.ts.map +1 -1
  11. package/dist/util.js.map +1 -1
  12. package/package.json +10 -13
  13. package/src/error.ts +0 -50
  14. package/src/index.ts +0 -2
  15. package/src/lexicons/com/atproto/server/createAccount.defs.ts +0 -57
  16. package/src/lexicons/com/atproto/server/createAccount.ts +0 -5
  17. package/src/lexicons/com/atproto/server/createSession.defs.ts +0 -56
  18. package/src/lexicons/com/atproto/server/createSession.ts +0 -5
  19. package/src/lexicons/com/atproto/server/deleteSession.defs.ts +0 -36
  20. package/src/lexicons/com/atproto/server/deleteSession.ts +0 -5
  21. package/src/lexicons/com/atproto/server/getSession.defs.ts +0 -41
  22. package/src/lexicons/com/atproto/server/getSession.ts +0 -5
  23. package/src/lexicons/com/atproto/server/refreshSession.defs.ts +0 -52
  24. package/src/lexicons/com/atproto/server/refreshSession.ts +0 -5
  25. package/src/lexicons/com/atproto/server.ts +0 -9
  26. package/src/lexicons/com/atproto.ts +0 -5
  27. package/src/lexicons/com.ts +0 -5
  28. package/src/lexicons/index.ts +0 -5
  29. package/src/password-session-utils.test.ts +0 -177
  30. package/src/password-session.test.ts +0 -417
  31. package/src/password-session.ts +0 -655
  32. package/src/util.ts +0 -59
  33. package/tsconfig.build.json +0 -12
  34. package/tsconfig.json +0 -7
  35. package/tsconfig.tests.json +0 -8
@@ -1,655 +0,0 @@
1
- import {
2
- Agent,
3
- XrpcFailure,
4
- buildAgent,
5
- xrpc,
6
- xrpcSafe,
7
- } from '@atproto/lex-client'
8
- import { LexAuthFactorError } from './error.js'
9
- import { com } from './lexicons/index.js'
10
- import { extractPdsUrl, extractXrpcErrorCode } from './util.js'
11
-
12
- /**
13
- * Represents a failure response when refreshing a session.
14
- *
15
- * This type captures the possible error responses from
16
- * `com.atproto.server.refreshSession`, including both expected errors
17
- * (e.g., invalid/expired refresh token) and unexpected errors (e.g., network issues).
18
- */
19
- export type RefreshFailure = XrpcFailure<
20
- typeof com.atproto.server.refreshSession.main
21
- >
22
-
23
- /**
24
- * Represents a failure response when deleting a session.
25
- *
26
- * This type captures the possible error responses from
27
- * `com.atproto.server.deleteSession`, including both expected errors
28
- * and unexpected errors (e.g., network issues, server unavailability).
29
- */
30
- export type DeleteFailure = XrpcFailure<
31
- typeof com.atproto.server.deleteSession.main
32
- >
33
-
34
- /**
35
- * Persisted session data containing authentication credentials and service information.
36
- *
37
- * This type extends the response from `com.atproto.server.createSession` with the
38
- * service URL used for authentication. Store this data securely to resume sessions
39
- * later without re-authenticating.
40
- */
41
- export type SessionData = com.atproto.server.createSession.$OutputBody & {
42
- service: string
43
- }
44
-
45
- export type LoginOptions = PasswordSessionOptions & {
46
- service: string | URL
47
- identifier: string
48
- password: string
49
- allowTakendown?: boolean
50
- authFactorToken?: string
51
- }
52
-
53
- export type PasswordSessionOptions = {
54
- /**
55
- * Custom fetch implementation to use for network requests
56
- */
57
- fetch?: typeof globalThis.fetch
58
-
59
- /**
60
- * Called whenever the session is successfully created/refreshed, and new
61
- * credentials have been obtained. Use this hook to persist the updated
62
- * session information.
63
- *
64
- * If this callback returns a promise, this function will never be called
65
- * again (on the same process) until the promise resolves.
66
- *
67
- * @note this function **must** not throw
68
- */
69
- onUpdated?: (this: PasswordSession, data: SessionData) => void | Promise<void>
70
-
71
- /**
72
- * Called whenever the session update fails due to an expected error, such as
73
- * a network issue or server unavailability. This function can be used to log
74
- * the error or notify the user, but should not assume that the session is
75
- * invalid.
76
- *
77
- * @note this function **must** not throw
78
- */
79
- onUpdateFailure?: (
80
- this: PasswordSession,
81
- data: SessionData,
82
- err: RefreshFailure,
83
- ) => void | Promise<void>
84
-
85
- /**
86
- * Called whenever the session is deleted, either due to an explicit logout or
87
- * because the refresh operation indicated that the session is no longer
88
- * valid. Use this hook to clean up any persisted session information and
89
- * update the application state accordingly.
90
- *
91
- * @note this function **must** not throw
92
- */
93
- onDeleted?: (this: PasswordSession, data: SessionData) => void | Promise<void>
94
-
95
- /**
96
- * Called whenever a session deletion fails due to an unexpected error, such
97
- * as a network issue or server unavailability. This function can be used to
98
- * log the error or notify the user. When this function is called, the session
99
- * might still be valid on the server. It is up to the implementation to
100
- * decide whether to retry the deletion or keep the session active. Ignoring
101
- * these errors is not recommended as it can lead to orphaned sessions on the
102
- * server, or security issues if the user believes they have logged out when a
103
- * bad actor is still using the session. The implementation should consider
104
- * keeping track of failed deletions and retrying them later, until they
105
- * succeed.
106
- *
107
- * @note this function **must** not throw
108
- */
109
- onDeleteFailure?: (
110
- this: PasswordSession,
111
- data: SessionData,
112
- err: DeleteFailure,
113
- ) => void | Promise<void>
114
- }
115
-
116
- /**
117
- * Password-based authentication session for AT Protocol services.
118
- *
119
- * This class provides session management for CLI tools, scripts, and bots that
120
- * need to authenticate with AT Protocol services using password credentials.
121
- * It implements the {@link Agent} interface, allowing it to be used directly
122
- * with AT Protocol clients.
123
- *
124
- * **Security Warning:** It is strongly recommended to use app passwords instead
125
- * of main account credentials. App passwords provide limited access and can be
126
- * revoked independently without compromising your main account. For browser-based
127
- * applications, use OAuth-based authentication instead.
128
- *
129
- * @example Basic usage with app password
130
- * ```ts
131
- * const session = await PasswordSession.login({
132
- * service: 'https://bsky.social',
133
- * identifier: 'alice.bsky.social',
134
- * password: 'xxxx-xxxx-xxxx-xxxx', // App password
135
- * onUpdated: (data) => saveToStorage(data),
136
- * onDeleted: (data) => clearStorage(data.did),
137
- * })
138
- *
139
- * const client = new Client(session)
140
- * // Use client to make authenticated requests
141
- * ```
142
- *
143
- * @example Resuming a persisted session
144
- * ```ts
145
- * const savedData = JSON.parse(fs.readFileSync('session.json', 'utf8'))
146
- * const session = await PasswordSession.resume(savedData, {
147
- * onUpdated: (data) => saveToStorage(data),
148
- * onDeleted: (data) => clearStorage(data.did),
149
- * })
150
- * ```
151
- *
152
- * @implements {Agent}
153
- */
154
- export class PasswordSession implements Agent, AsyncDisposable {
155
- /**
156
- * Internal {@link Agent} used for session management towards the
157
- * authentication service only.
158
- */
159
- #serviceAgent: Agent
160
-
161
- #sessionData: null | SessionData
162
- #sessionPromise: Promise<SessionData>
163
-
164
- constructor(
165
- sessionData: SessionData,
166
- protected readonly options: PasswordSessionOptions = {},
167
- ) {
168
- this.#serviceAgent = buildAgent({
169
- service: sessionData.service,
170
- fetch: options.fetch,
171
- })
172
-
173
- this.#sessionData = sessionData
174
- this.#sessionPromise = Promise.resolve(this.#sessionData)
175
- }
176
-
177
- /**
178
- * The DID (Decentralized Identifier) of the authenticated account.
179
- *
180
- * @throws {Error} If the session has been destroyed (logged out).
181
- */
182
- get did() {
183
- return this.session.did
184
- }
185
-
186
- /**
187
- * The handle (username) of the authenticated account.
188
- *
189
- * @throws {Error} If the session has been destroyed (logged out).
190
- */
191
- get handle() {
192
- return this.session.handle
193
- }
194
-
195
- /**
196
- * The current session data containing authentication credentials.
197
- *
198
- * @throws {Error} If the session has been destroyed (logged out).
199
- */
200
- get session() {
201
- if (this.#sessionData) return this.#sessionData
202
- throw new Error('Logged out')
203
- }
204
-
205
- /**
206
- * Whether this session has been destroyed (logged out).
207
- *
208
- * Once destroyed, this session instance can no longer be used for
209
- * authenticated requests. Create a new session via {@link PasswordSession.login}
210
- * or {@link PasswordSession.resume}.
211
- */
212
- get destroyed(): boolean {
213
- return this.#sessionData === null
214
- }
215
-
216
- /**
217
- * Handles authenticated fetch requests to the user's PDS.
218
- *
219
- * This method implements the {@link Agent} interface and is called by
220
- * AT Protocol clients to make authenticated requests. It automatically:
221
- * - Adds the access token to request headers
222
- * - Detects expired tokens and triggers refresh
223
- * - Retries requests after successful token refresh
224
- *
225
- * @param path - The request path (will be resolved against the PDS URL)
226
- * @param init - Standard fetch RequestInit options (headers, body, etc.)
227
- * @returns The fetch Response from the PDS
228
- * @throws {TypeError} If an 'authorization' header is already set in init
229
- */
230
- async fetchHandler(path: string, init: RequestInit): Promise<Response> {
231
- const headers = new Headers(init.headers)
232
- if (headers.has('authorization')) {
233
- throw new TypeError("Unexpected 'authorization' header set")
234
- }
235
-
236
- const sessionPromise = this.#sessionPromise
237
- const sessionData = await sessionPromise
238
-
239
- const fetch = this.options.fetch ?? globalThis.fetch
240
-
241
- headers.set('authorization', `Bearer ${sessionData.accessJwt}`)
242
- const initialRes = await fetch(fetchUrl(sessionData, path), {
243
- ...init,
244
- headers,
245
- })
246
-
247
- const refreshNeeded =
248
- initialRes.status === 401 ||
249
- (initialRes.status === 400 &&
250
- (await extractXrpcErrorCode(initialRes)) === 'ExpiredToken')
251
-
252
- if (!refreshNeeded) {
253
- return initialRes
254
- }
255
-
256
- // Refresh session (unless it was already refreshed in the meantime)
257
- const newSessionPromise =
258
- this.#sessionPromise === sessionPromise
259
- ? this.refresh()
260
- : this.#sessionPromise
261
-
262
- // Error should have been propagated through hooks
263
- const newSessionData = await newSessionPromise.catch((_err) => null)
264
- if (!newSessionData) {
265
- return initialRes
266
- }
267
-
268
- // refresh silently failed, no point in retrying.
269
- if (newSessionData.accessJwt === sessionData.accessJwt) {
270
- return initialRes
271
- }
272
-
273
- if (init?.signal?.aborted) {
274
- return initialRes
275
- }
276
-
277
- // The stream was already consumed. We cannot retry the request. A solution
278
- // would be to tee() the input stream but that would bufferize the entire
279
- // stream in memory which can lead to memory starvation. Instead, we will
280
- // return the original response and let the calling code handle retries.
281
- if (ReadableStream && init?.body instanceof ReadableStream) {
282
- return initialRes
283
- }
284
-
285
- // Make sure the initial request is cancelled to avoid leaking resources
286
- // (NodeJS 👀): https://undici.nodejs.org/#/?id=garbage-collection
287
- if (!initialRes.bodyUsed) {
288
- await initialRes.body?.cancel()
289
- }
290
-
291
- // Finally, retry the request with the new access token
292
- headers.set('authorization', `Bearer ${newSessionData.accessJwt}`)
293
- return fetch(fetchUrl(newSessionData, path), { ...init, headers })
294
- }
295
-
296
- /**
297
- * Refreshes the session by obtaining new access and refresh tokens.
298
- *
299
- * This method is automatically called by {@link fetchHandler} when the access
300
- * token expires. You can also call it manually to proactively refresh tokens.
301
- *
302
- * On success, the {@link PasswordSessionOptions.onUpdated} callback is invoked
303
- * with the new session data. On expected failures (invalid session), the
304
- * {@link PasswordSessionOptions.onDeleted} callback is invoked. On unexpected
305
- * failures (network issues), the {@link PasswordSessionOptions.onUpdateFailure}
306
- * callback is invoked and the existing session data is preserved.
307
- *
308
- * @returns The refreshed session data
309
- * @throws {RefreshFailure} If the session is no longer valid (triggers onDeleted)
310
- */
311
- async refresh(): Promise<SessionData> {
312
- this.#sessionPromise = this.#sessionPromise.then(async (sessionData) => {
313
- const response = await xrpcSafe(
314
- this.#serviceAgent,
315
- com.atproto.server.refreshSession.main,
316
- { headers: { Authorization: `Bearer ${sessionData.refreshJwt}` } },
317
- )
318
-
319
- if (!response.success && response.matchesSchemaErrors()) {
320
- // Expected errors that indicate the session is no longer valid
321
- await this.options.onDeleted?.call(this, sessionData)
322
-
323
- // Update the session promise to a rejected state
324
- this.#sessionData = null
325
- throw response
326
- }
327
-
328
- if (!response.success) {
329
- // We failed to refresh the token, assume the session might still be
330
- // valid by returning the existing session.
331
- await this.options.onUpdateFailure?.call(this, sessionData, response)
332
-
333
- return sessionData
334
- }
335
-
336
- const data = response.body
337
-
338
- // Historically, refreshSession did not return all the fields from
339
- // getSession. In particular, emailConfirmed and didDoc were missing.
340
- // Similarly, some servers might not return the didDoc in refreshSession.
341
- // We fetch them via getSession if missing, allowing to ensure that we are
342
- // always talking with the right PDS.
343
- if (data.emailConfirmed == null || data.didDoc == null) {
344
- const extraData = await xrpcSafe(
345
- this.#serviceAgent,
346
- com.atproto.server.getSession.main,
347
- { headers: { Authorization: `Bearer ${data.accessJwt}` } },
348
- )
349
- if (extraData.success && extraData.body.did === data.did) {
350
- Object.assign(data, extraData.body)
351
- }
352
- }
353
-
354
- const newSession: SessionData = {
355
- ...data,
356
- service: sessionData.service,
357
- }
358
-
359
- await this.options.onUpdated?.call(this, newSession)
360
-
361
- return (this.#sessionData = newSession)
362
- })
363
-
364
- return this.#sessionPromise
365
- }
366
-
367
- /**
368
- * Logs out by deleting the session on the server.
369
- *
370
- * This method invalidates both the access and refresh tokens on the server,
371
- * preventing any further use of this session. After successful logout, the
372
- * session is marked as destroyed and the {@link PasswordSessionOptions.onDeleted}
373
- * callback is invoked.
374
- *
375
- * If the logout request fails due to network issues or server unavailability,
376
- * the {@link PasswordSessionOptions.onDeleteFailure} callback is invoked and
377
- * the session remains active locally. In this case, you should retry the
378
- * logout later to ensure the session is properly invalidated on the server.
379
- *
380
- * @throws {DeleteFailure} If the logout request fails due to unexpected errors
381
- */
382
- async logout(): Promise<void> {
383
- let reason: DeleteFailure | null = null
384
-
385
- this.#sessionPromise = this.#sessionPromise.then(async (sessionData) => {
386
- const result = await xrpcSafe(
387
- this.#serviceAgent,
388
- com.atproto.server.deleteSession.main,
389
- { headers: { Authorization: `Bearer ${sessionData.refreshJwt}` } },
390
- )
391
-
392
- if (result.success || result.matchesSchemaErrors()) {
393
- await this.options.onDeleted?.call(this, sessionData)
394
-
395
- // Update the session promise to a rejected state
396
- this.#sessionData = null
397
- throw new Error('Logged out')
398
- } else {
399
- // Capture the reason for the failure to re-throw in the outer promise
400
- reason = result
401
-
402
- // An unknown/unexpected error occurred (network, server down, etc)
403
- await this.options.onDeleteFailure?.call(this, sessionData, result)
404
-
405
- // Keep the session in an active state
406
- return sessionData
407
- }
408
- })
409
-
410
- return this.#sessionPromise.then(
411
- (_session) => {
412
- // If the promise above resolved, then logout failed. Re-throw the
413
- // reason captured earlier.
414
- throw reason!
415
- },
416
- (_err) => {
417
- // Successful logout
418
- },
419
- )
420
- }
421
-
422
- async [Symbol.asyncDispose]() {
423
- await this.logout()
424
- }
425
-
426
- /**
427
- * Creates a new account and returns an authenticated session.
428
- *
429
- * This static method registers a new account on the specified service and
430
- * automatically creates an authenticated session for it.
431
- *
432
- * @param body - Account creation parameters (handle, email, password, etc.)
433
- * @param options - Session options including the service URL
434
- * @returns A new PasswordSession for the created account
435
- * @throws If account creation fails (e.g., handle taken, invalid invite code)
436
- *
437
- * @example
438
- * ```ts
439
- * const session = await PasswordSession.createAccount(
440
- * {
441
- * handle: 'alice.bsky.social',
442
- * email: 'alice@example.com',
443
- * password: 'secure-password',
444
- * },
445
- * {
446
- * service: 'https://bsky.social',
447
- * onUpdated: (data) => saveToStorage(data),
448
- * }
449
- * )
450
- * ```
451
- */
452
- static async createAccount(
453
- body: com.atproto.server.createAccount.$InputBody,
454
- {
455
- service,
456
- headers,
457
- ...options
458
- }: PasswordSessionOptions & {
459
- headers?: HeadersInit
460
- service: string | URL
461
- },
462
- ): Promise<PasswordSession> {
463
- const response = await xrpc(
464
- buildAgent({ service, headers, fetch: options.fetch }),
465
- com.atproto.server.createAccount.main,
466
- { body },
467
- )
468
-
469
- const data: SessionData = {
470
- ...response.body,
471
- service: String(service),
472
- }
473
-
474
- const agent = new PasswordSession(data, options)
475
- await options.onUpdated?.call(agent, data)
476
- return agent
477
- }
478
-
479
- /**
480
- * Creates a new authenticated session using password credentials.
481
- *
482
- * This static method authenticates with the specified service and returns
483
- * a new PasswordSession instance that can be used for authenticated requests.
484
- *
485
- * **Security Warning:** It is strongly recommended to use app passwords instead
486
- * of main account credentials. App passwords can be created in your account
487
- * settings and provide limited access that can be revoked independently. For
488
- * browser-based applications, use OAuth-based authentication instead.
489
- *
490
- * @param options - Login options including service URL, identifier, and password
491
- * @param options.service - The AT Protocol service URL (e.g., 'https://bsky.social')
492
- * @param options.identifier - The user's handle or DID
493
- * @param options.password - The user's password or app password
494
- * @param options.allowTakendown - If true, allow login to takendown accounts
495
- * @param options.authFactorToken - 2FA token if required by the server
496
- * @returns A new authenticated PasswordSession
497
- * @throws {LexAuthFactorError} If the server requires a 2FA token
498
- * @throws If authentication fails (invalid credentials, etc.)
499
- *
500
- * **Basic login with app password in script**
501
- * @example
502
- * ```ts
503
- * // .env
504
- * // APP_PASSWORD_CREDENTIALS="https://<handle>:<app-password>@<pds-hosting-provider>"
505
- *
506
- * // Make sure to dispose (or logout) the session when done to avoid leaking
507
- * // resources and leaving orphaned sessions on the server
508
- * await using session = await PasswordSession.login(process.env.APP_PASSWORD_CREDENTIALS)
509
- *
510
- * // Use session to make authenticated requests
511
- * ```
512
- *
513
- * **Basic login with user password (not recommended!!!)**
514
- * @example
515
- * ```ts
516
- * const session = await PasswordSession.login({
517
- * service: 'https://bsky.social',
518
- * identifier: 'alice.bsky.social',
519
- * password: 'xxxx',
520
- * onUpdated: (data) => saveToStorage(data),
521
- * onDeleted: (data) => clearStorage(data.did),
522
- * })
523
- *
524
- * // Next time, use resume with the persisted session data to avoid storing
525
- * // user credentials.
526
- * ```
527
- *
528
- * **Handling 2FA requirement**
529
- * @example
530
- * ```ts
531
- * try {
532
- * const session = await PasswordSession.login({
533
- * service: 'https://bsky.social',
534
- * identifier: 'alice.bsky.social',
535
- * password: 'xxxx',
536
- * })
537
- * } catch (err) {
538
- * if (err instanceof LexAuthFactorError) {
539
- * const token = await promptUser('Enter 2FA code:')
540
- * const session = await PasswordSession.login({
541
- * service: 'https://bsky.social',
542
- * identifier: 'alice.bsky.social',
543
- * password: 'xxxx',
544
- * authFactorToken: token,
545
- * })
546
- * }
547
- * }
548
- * ```
549
- */
550
- static async login(
551
- input: string | URL | LoginOptions,
552
- ): Promise<PasswordSession> {
553
- const {
554
- service,
555
- identifier,
556
- password,
557
- allowTakendown,
558
- authFactorToken,
559
- ...options
560
- } =
561
- typeof input === 'string' || input instanceof URL
562
- ? parseLoginUrl(input)
563
- : input
564
-
565
- const xrpcAgent = buildAgent({
566
- service,
567
- fetch: options.fetch,
568
- })
569
-
570
- const response = await xrpcSafe(
571
- xrpcAgent,
572
- com.atproto.server.createSession.main,
573
- { body: { identifier, password, allowTakendown, authFactorToken } },
574
- )
575
-
576
- if (!response.success) {
577
- if (response.error === 'AuthFactorTokenRequired') {
578
- throw new LexAuthFactorError(response)
579
- }
580
- throw response.reason
581
- }
582
-
583
- const data: SessionData = {
584
- ...response.body,
585
- service: String(service),
586
- }
587
-
588
- const agent = new PasswordSession(data, options)
589
- await options.onUpdated?.call(agent, data)
590
- return agent
591
- }
592
-
593
- /**
594
- * Resume an existing session, ensuring it is still valid by refreshing it.
595
- * Any error thrown here indicates that the session is definitely no longer
596
- * valid. Network errors will be propagated through the
597
- * {@link PasswordSessionOptions.onUpdateFailure} hook, and not re-thrown
598
- * here. This means that a resolved promise does not necessarily indicate a
599
- * valid session, only that it's refresh did not definitively fail.
600
- *
601
- * This is the same as calling {@link PasswordSession.refresh} after
602
- * constructing the {@link PasswordSession} manually.
603
- *
604
- * @throws If, and only if, the session is definitely no longer valid.
605
- */
606
- static async resume(
607
- data: SessionData,
608
- options: PasswordSessionOptions,
609
- ): Promise<PasswordSession> {
610
- const agent = new PasswordSession(data, options)
611
- await agent.refresh()
612
- return agent
613
- }
614
-
615
- /**
616
- * Delete a session without having to {@link resume resume()} it first, or
617
- * provide hooks.
618
- *
619
- * @throws In case of unexpected error (network issue, server down, etc)
620
- * meaning that the session may still be valid.
621
- */
622
- static async delete(
623
- data: SessionData,
624
- options?: PasswordSessionOptions,
625
- ): Promise<void> {
626
- const agent = new PasswordSession(data, options)
627
- await agent.logout()
628
- }
629
- }
630
-
631
- function fetchUrl(sessionData: SessionData, path: string): URL {
632
- const pdsUrl = extractPdsUrl(sessionData.didDoc)
633
- return new URL(path, pdsUrl ?? sessionData.service)
634
- }
635
-
636
- function parseLoginUrl(input: string | URL): LoginOptions {
637
- const url = typeof input === 'string' ? new URL(input) : input
638
- if (url.pathname !== '/') {
639
- throw new TypeError('Invalid login URL: unexpected pathname')
640
- }
641
- if (url.hash) {
642
- throw new TypeError('Invalid login URL: unexpected hash')
643
- }
644
- if (url.search) {
645
- throw new TypeError('Invalid login URL: unexpected search parameters')
646
- }
647
- if (!url.username || !url.password) {
648
- throw new TypeError('Invalid login URL: missing identifier or password')
649
- }
650
- return {
651
- service: url.origin,
652
- identifier: url.username,
653
- password: url.password,
654
- }
655
- }
package/src/util.ts DELETED
@@ -1,59 +0,0 @@
1
- import { LexMap, LexValue } from '@atproto/lex-client'
2
- import { lexErrorDataSchema } from '@atproto/lex-schema'
3
-
4
- export async function extractXrpcErrorCode(
5
- response: Response,
6
- ): Promise<string | null> {
7
- const json = await peekJson(response, 10 * 1024) // Avoid reading large bodies
8
- if (json === undefined) return null
9
- if (!lexErrorDataSchema.matches(json)) return null
10
- return json.error
11
- }
12
-
13
- async function peekJson(
14
- response: Response,
15
- maxSize = Infinity,
16
- ): Promise<undefined | LexValue> {
17
- const type = extractType(response)
18
- if (type !== 'application/json') return undefined
19
- const length = extractLength(response)
20
- if (length != null && length > maxSize) return undefined
21
-
22
- try {
23
- return (await response.clone().json()) as Promise<LexValue>
24
- } catch {
25
- return undefined
26
- }
27
- }
28
-
29
- function extractLength({ headers }: Response) {
30
- return headers.get('Content-Length')
31
- ? Number(headers.get('Content-Length'))
32
- : undefined
33
- }
34
-
35
- function extractType({ headers }: Response) {
36
- return headers.get('Content-Type')?.split(';')[0]?.trim().toLowerCase()
37
- }
38
-
39
- export function extractPdsUrl(didDoc?: LexMap): string | null {
40
- const pdsService = ifArray(didDoc?.service)?.find((service) =>
41
- ifString((service as any)?.id)?.endsWith('#atproto_pds'),
42
- )
43
- const pdsEndpoint = ifString((pdsService as any)?.serviceEndpoint)
44
- return pdsEndpoint && URL.canParse(pdsEndpoint) ? pdsEndpoint : null
45
- }
46
-
47
- const ifString = <T>(v: T) =>
48
- (typeof v === 'string' ? v : undefined) as unknown extends T
49
- ? undefined | string
50
- : T extends string
51
- ? string
52
- : undefined
53
-
54
- const ifArray = <T>(v: T) =>
55
- (Array.isArray(v) ? v : undefined) as unknown extends T
56
- ? undefined | unknown[]
57
- : T extends unknown[]
58
- ? Extract<T, unknown[]>
59
- : undefined