@atproto/lex-password-session 0.0.4 → 0.0.5

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.
@@ -9,14 +9,35 @@ import { LexAuthFactorError } from './error.js'
9
9
  import { com } from './lexicons/index.js'
10
10
  import { extractPdsUrl, extractXrpcErrorCode } from './util.js'
11
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
+ */
12
19
  export type RefreshFailure = XrpcFailure<
13
20
  typeof com.atproto.server.refreshSession.main
14
21
  >
15
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
+ */
16
30
  export type DeleteFailure = XrpcFailure<
17
31
  typeof com.atproto.server.deleteSession.main
18
32
  >
19
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
+ */
20
41
  export type SessionData = com.atproto.server.createSession.OutputBody & {
21
42
  service: string
22
43
  }
@@ -84,6 +105,44 @@ export type PasswordSessionOptions = {
84
105
  ) => void | Promise<void>
85
106
  }
86
107
 
108
+ /**
109
+ * Password-based authentication session for AT Protocol services.
110
+ *
111
+ * This class provides session management for CLI tools, scripts, and bots that
112
+ * need to authenticate with AT Protocol services using password credentials.
113
+ * It implements the {@link Agent} interface, allowing it to be used directly
114
+ * with AT Protocol clients.
115
+ *
116
+ * **Security Warning:** It is strongly recommended to use app passwords instead
117
+ * of main account credentials. App passwords provide limited access and can be
118
+ * revoked independently without compromising your main account. For browser-based
119
+ * applications, use OAuth-based authentication instead.
120
+ *
121
+ * @example Basic usage with app password
122
+ * ```ts
123
+ * const session = await PasswordSession.login({
124
+ * service: 'https://bsky.social',
125
+ * identifier: 'alice.bsky.social',
126
+ * password: 'xxxx-xxxx-xxxx-xxxx', // App password
127
+ * onUpdated: (data) => saveToStorage(data),
128
+ * onDeleted: (data) => clearStorage(data.did),
129
+ * })
130
+ *
131
+ * const client = new Client(session)
132
+ * // Use client to make authenticated requests
133
+ * ```
134
+ *
135
+ * @example Resuming a persisted session
136
+ * ```ts
137
+ * const savedData = JSON.parse(fs.readFileSync('session.json', 'utf8'))
138
+ * const session = await PasswordSession.resume(savedData, {
139
+ * onUpdated: (data) => saveToStorage(data),
140
+ * onDeleted: (data) => clearStorage(data.did),
141
+ * })
142
+ * ```
143
+ *
144
+ * @implements {Agent}
145
+ */
87
146
  export class PasswordSession implements Agent {
88
147
  /**
89
148
  * Internal {@link Agent} used for session management towards the
@@ -107,23 +166,59 @@ export class PasswordSession implements Agent {
107
166
  this.#sessionPromise = Promise.resolve(this.#sessionData)
108
167
  }
109
168
 
169
+ /**
170
+ * The DID (Decentralized Identifier) of the authenticated account.
171
+ *
172
+ * @throws {Error} If the session has been destroyed (logged out).
173
+ */
110
174
  get did() {
111
175
  return this.session.did
112
176
  }
113
177
 
178
+ /**
179
+ * The handle (username) of the authenticated account.
180
+ *
181
+ * @throws {Error} If the session has been destroyed (logged out).
182
+ */
114
183
  get handle() {
115
184
  return this.session.handle
116
185
  }
117
186
 
187
+ /**
188
+ * The current session data containing authentication credentials.
189
+ *
190
+ * @throws {Error} If the session has been destroyed (logged out).
191
+ */
118
192
  get session() {
119
193
  if (this.#sessionData) return this.#sessionData
120
194
  throw new Error('Logged out')
121
195
  }
122
196
 
197
+ /**
198
+ * Whether this session has been destroyed (logged out).
199
+ *
200
+ * Once destroyed, this session instance can no longer be used for
201
+ * authenticated requests. Create a new session via {@link PasswordSession.login}
202
+ * or {@link PasswordSession.resume}.
203
+ */
123
204
  get destroyed(): boolean {
124
205
  return this.#sessionData === null
125
206
  }
126
207
 
208
+ /**
209
+ * Handles authenticated fetch requests to the user's PDS.
210
+ *
211
+ * This method implements the {@link Agent} interface and is called by
212
+ * AT Protocol clients to make authenticated requests. It automatically:
213
+ * - Adds the access token to request headers
214
+ * - Detects expired tokens and triggers refresh
215
+ * - Retries requests after successful token refresh
216
+ *
217
+ * @param path - The request path (will be resolved against the PDS URL)
218
+ * @param init - Standard fetch RequestInit options (headers, body, etc.)
219
+ * @returns The fetch Response from the PDS
220
+ * @throws {TypeError} If an 'authorization' header is already set in init
221
+ */
127
222
  async fetchHandler(path: string, init: RequestInit): Promise<Response> {
128
223
  const headers = new Headers(init.headers)
129
224
  if (headers.has('authorization')) {
@@ -190,6 +285,21 @@ export class PasswordSession implements Agent {
190
285
  return fetch(fetchUrl(newSessionData, path), { ...init, headers })
191
286
  }
192
287
 
288
+ /**
289
+ * Refreshes the session by obtaining new access and refresh tokens.
290
+ *
291
+ * This method is automatically called by {@link fetchHandler} when the access
292
+ * token expires. You can also call it manually to proactively refresh tokens.
293
+ *
294
+ * On success, the {@link PasswordSessionOptions.onUpdated} callback is invoked
295
+ * with the new session data. On expected failures (invalid session), the
296
+ * {@link PasswordSessionOptions.onDeleted} callback is invoked. On unexpected
297
+ * failures (network issues), the {@link PasswordSessionOptions.onUpdateFailure}
298
+ * callback is invoked and the existing session data is preserved.
299
+ *
300
+ * @returns The refreshed session data
301
+ * @throws {RefreshFailure} If the session is no longer valid (triggers onDeleted)
302
+ */
193
303
  async refresh(): Promise<SessionData> {
194
304
  this.#sessionPromise = this.#sessionPromise.then(async (sessionData) => {
195
305
  const response = await xrpcSafe(
@@ -250,6 +360,21 @@ export class PasswordSession implements Agent {
250
360
  return this.#sessionPromise
251
361
  }
252
362
 
363
+ /**
364
+ * Logs out by deleting the session on the server.
365
+ *
366
+ * This method invalidates both the access and refresh tokens on the server,
367
+ * preventing any further use of this session. After successful logout, the
368
+ * session is marked as destroyed and the {@link PasswordSessionOptions.onDeleted}
369
+ * callback is invoked.
370
+ *
371
+ * If the logout request fails due to network issues or server unavailability,
372
+ * the {@link PasswordSessionOptions.onDeleteFailure} callback is invoked and
373
+ * the session remains active locally. In this case, you should retry the
374
+ * logout later to ensure the session is properly invalidated on the server.
375
+ *
376
+ * @throws {DeleteFailure} If the logout request fails due to unexpected errors
377
+ */
253
378
  async logout(): Promise<void> {
254
379
  let reason: DeleteFailure | null = null
255
380
 
@@ -290,6 +415,32 @@ export class PasswordSession implements Agent {
290
415
  )
291
416
  }
292
417
 
418
+ /**
419
+ * Creates a new account and returns an authenticated session.
420
+ *
421
+ * This static method registers a new account on the specified service and
422
+ * automatically creates an authenticated session for it.
423
+ *
424
+ * @param body - Account creation parameters (handle, email, password, etc.)
425
+ * @param options - Session options including the service URL
426
+ * @returns A new PasswordSession for the created account
427
+ * @throws If account creation fails (e.g., handle taken, invalid invite code)
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * const session = await PasswordSession.createAccount(
432
+ * {
433
+ * handle: 'alice.bsky.social',
434
+ * email: 'alice@example.com',
435
+ * password: 'secure-password',
436
+ * },
437
+ * {
438
+ * service: 'https://bsky.social',
439
+ * onUpdated: (data) => saveToStorage(data),
440
+ * }
441
+ * )
442
+ * ```
443
+ */
293
444
  static async createAccount(
294
445
  body: com.atproto.server.createAccount.InputBody,
295
446
  {
@@ -318,29 +469,53 @@ export class PasswordSession implements Agent {
318
469
  }
319
470
 
320
471
  /**
321
- * @note It is **not** recommended to use {@link PasswordSession} with main
322
- * account credentials. Instead, it is strongly advised to use OAuth based
323
- * authentication for main username/password credentials and use
324
- * {@link PasswordSession} with an app-password, for bots, scripts, or similar
325
- * use-cases.
472
+ * Creates a new authenticated session using password credentials.
473
+ *
474
+ * This static method authenticates with the specified service and returns
475
+ * a new PasswordSession instance that can be used for authenticated requests.
326
476
  *
327
- * @throws If unable to create a session. In particular, if the server
328
- * requires a 2FA token, a {@link XrpcResponseError} with the
329
- * `AuthFactorTokenRequired` error code will be thrown.
477
+ * **Security Warning:** It is strongly recommended to use app passwords instead
478
+ * of main account credentials. App passwords can be created in your account
479
+ * settings and provide limited access that can be revoked independently. For
480
+ * browser-based applications, use OAuth-based authentication instead.
330
481
  *
482
+ * @param options - Login options including service URL, identifier, and password
483
+ * @param options.service - The AT Protocol service URL (e.g., 'https://bsky.social')
484
+ * @param options.identifier - The user's handle or DID
485
+ * @param options.password - The user's password or app password
486
+ * @param options.allowTakendown - If true, allow login to takendown accounts
487
+ * @param options.authFactorToken - 2FA token if required by the server
488
+ * @returns A new authenticated PasswordSession
489
+ * @throws {LexAuthFactorError} If the server requires a 2FA token
490
+ * @throws If authentication fails (invalid credentials, etc.)
331
491
  *
332
- * @example Handling 2FA errors
492
+ * @example Basic login with app password
493
+ * ```ts
494
+ * const session = await PasswordSession.login({
495
+ * service: 'https://bsky.social',
496
+ * identifier: 'alice.bsky.social',
497
+ * password: 'xxxx-xxxx-xxxx-xxxx', // App password
498
+ * onUpdated: (data) => saveToStorage(data),
499
+ * })
500
+ * ```
333
501
  *
502
+ * @example Handling 2FA requirement
334
503
  * ```ts
335
504
  * try {
336
505
  * const session = await PasswordSession.login({
337
- * service: 'https://example.com',
338
- * identifier: 'alice',
339
- * password: 'correct horse battery staple',
506
+ * service: 'https://bsky.social',
507
+ * identifier: 'alice.bsky.social',
508
+ * password: 'xxxx-xxxx-xxxx-xxxx',
340
509
  * })
341
510
  * } catch (err) {
342
- * if (err instanceof XrpcResponseError && err.error === 'AuthFactorTokenRequired') {
343
- * // Prompt user for 2FA token and re-attempt session creation
511
+ * if (err instanceof LexAuthFactorError) {
512
+ * const token = await promptUser('Enter 2FA code:')
513
+ * const session = await PasswordSession.login({
514
+ * service: 'https://bsky.social',
515
+ * identifier: 'alice.bsky.social',
516
+ * password: 'xxxx-xxxx-xxxx-xxxx',
517
+ * authFactorToken: token,
518
+ * })
344
519
  * }
345
520
  * }
346
521
  * ```