@atproto/lex-password-session 0.0.3 → 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.
- package/CHANGELOG.md +26 -0
- package/README.md +187 -167
- package/dist/error.d.ts +51 -4
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +54 -4
- package/dist/error.js.map +1 -1
- package/dist/lexicons/com/atproto/server/createAccount.defs.d.ts +28 -28
- package/dist/lexicons/com/atproto/server/createSession.defs.d.ts +28 -28
- package/dist/lexicons/com/atproto/server/getSession.defs.d.ts +16 -16
- package/dist/lexicons/com/atproto/server/refreshSession.defs.d.ts +20 -20
- package/dist/password-session.d.ts +199 -20
- package/dist/password-session.d.ts.map +1 -1
- package/dist/password-session.js +192 -28
- package/dist/password-session.js.map +1 -1
- package/dist/util.d.ts +0 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +1 -4
- package/dist/util.js.map +1 -1
- package/package.json +6 -6
- package/src/error.ts +54 -8
- package/src/password-session.test.ts +17 -16
- package/src/password-session.ts +236 -34
- package/src/util.ts +2 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-namespace */
|
|
2
2
|
|
|
3
3
|
import { afterAll, assert, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
4
|
-
import { Client,
|
|
4
|
+
import { Client, XrpcAuthenticationError } from '@atproto/lex-client'
|
|
5
5
|
import { l } from '@atproto/lex-schema'
|
|
6
6
|
import { LexRouter, LexServerAuthError } from '@atproto/lex-server'
|
|
7
7
|
import { Server, serve } from '@atproto/lex-server/nodejs'
|
|
@@ -171,7 +171,7 @@ describe(PasswordSession, () => {
|
|
|
171
171
|
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
172
172
|
|
|
173
173
|
await expect(
|
|
174
|
-
PasswordSession.
|
|
174
|
+
PasswordSession.login({
|
|
175
175
|
...defaultOptions,
|
|
176
176
|
service: entrywayOrigin,
|
|
177
177
|
identifier: 'alice',
|
|
@@ -181,7 +181,6 @@ describe(PasswordSession, () => {
|
|
|
181
181
|
}),
|
|
182
182
|
).rejects.toMatchObject({
|
|
183
183
|
success: false,
|
|
184
|
-
status: 401,
|
|
185
184
|
error: 'AuthenticationRequired',
|
|
186
185
|
})
|
|
187
186
|
|
|
@@ -193,7 +192,7 @@ describe(PasswordSession, () => {
|
|
|
193
192
|
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
194
193
|
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
195
194
|
|
|
196
|
-
const result = await PasswordSession.
|
|
195
|
+
const result = await PasswordSession.login({
|
|
197
196
|
...defaultOptions,
|
|
198
197
|
service: entrywayOrigin,
|
|
199
198
|
identifier: 'alice',
|
|
@@ -217,7 +216,7 @@ describe(PasswordSession, () => {
|
|
|
217
216
|
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
218
217
|
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
219
218
|
|
|
220
|
-
const session = await PasswordSession.
|
|
219
|
+
const session = await PasswordSession.login({
|
|
221
220
|
...defaultOptions,
|
|
222
221
|
service: entrywayOrigin,
|
|
223
222
|
identifier: 'alice',
|
|
@@ -253,7 +252,12 @@ describe(PasswordSession, () => {
|
|
|
253
252
|
|
|
254
253
|
await expect(
|
|
255
254
|
client.call(app.example.customMethod, { message: 'hello' }),
|
|
256
|
-
).rejects.
|
|
255
|
+
).rejects.toMatchObject({
|
|
256
|
+
message: 'Unable to fulfill XRPC request',
|
|
257
|
+
cause: expect.objectContaining({
|
|
258
|
+
message: 'Logged out',
|
|
259
|
+
}),
|
|
260
|
+
})
|
|
257
261
|
})
|
|
258
262
|
|
|
259
263
|
it('fails to perform unauthenticated call', async () => {
|
|
@@ -263,22 +267,21 @@ describe(PasswordSession, () => {
|
|
|
263
267
|
})
|
|
264
268
|
|
|
265
269
|
assert(result.success === false)
|
|
266
|
-
assert(result instanceof
|
|
270
|
+
assert(result instanceof XrpcAuthenticationError)
|
|
267
271
|
expect(result).toMatchObject({
|
|
268
272
|
success: false,
|
|
269
|
-
status: 401,
|
|
270
273
|
error: 'AuthenticationRequired',
|
|
271
274
|
})
|
|
272
|
-
expect(result.
|
|
273
|
-
|
|
274
|
-
)
|
|
275
|
+
expect(result.wwwAuthenticate).toEqual({
|
|
276
|
+
Bearer: { realm: 'access token' },
|
|
277
|
+
})
|
|
275
278
|
})
|
|
276
279
|
|
|
277
280
|
it('refreshes expired token', async () => {
|
|
278
281
|
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
279
282
|
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
280
283
|
|
|
281
|
-
const session = await PasswordSession.
|
|
284
|
+
const session = await PasswordSession.login({
|
|
282
285
|
...defaultOptions,
|
|
283
286
|
service: entrywayOrigin,
|
|
284
287
|
identifier: 'bob',
|
|
@@ -332,7 +335,7 @@ describe(PasswordSession, () => {
|
|
|
332
335
|
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
333
336
|
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
334
337
|
|
|
335
|
-
const initialAgent = await PasswordSession.
|
|
338
|
+
const initialAgent = await PasswordSession.login({
|
|
336
339
|
...defaultOptions,
|
|
337
340
|
service: entrywayOrigin,
|
|
338
341
|
identifier: 'carla',
|
|
@@ -368,7 +371,6 @@ describe(PasswordSession, () => {
|
|
|
368
371
|
await expect(initialAgent.refresh()).rejects.toMatchObject({
|
|
369
372
|
success: false,
|
|
370
373
|
error: 'ExpiredToken',
|
|
371
|
-
status: 401,
|
|
372
374
|
})
|
|
373
375
|
|
|
374
376
|
expect(onDeleted).toHaveBeenCalledTimes(1)
|
|
@@ -393,7 +395,7 @@ describe(PasswordSession, () => {
|
|
|
393
395
|
it('silently ignores expected logout errors', async () => {
|
|
394
396
|
let sessionData: SessionData | null = null
|
|
395
397
|
|
|
396
|
-
const session = await PasswordSession.
|
|
398
|
+
const session = await PasswordSession.login({
|
|
397
399
|
...defaultOptions,
|
|
398
400
|
service: entrywayOrigin,
|
|
399
401
|
identifier: 'dave',
|
|
@@ -402,7 +404,6 @@ describe(PasswordSession, () => {
|
|
|
402
404
|
onUpdated: (data) => {
|
|
403
405
|
sessionData = structuredClone(data)
|
|
404
406
|
},
|
|
405
|
-
onDeleted: () => {},
|
|
406
407
|
})
|
|
407
408
|
|
|
408
409
|
assert(sessionData)
|
package/src/password-session.ts
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Agent,
|
|
3
|
-
XrpcError,
|
|
4
3
|
XrpcFailure,
|
|
5
4
|
buildAgent,
|
|
5
|
+
xrpc,
|
|
6
6
|
xrpcSafe,
|
|
7
7
|
} from '@atproto/lex-client'
|
|
8
8
|
import { LexAuthFactorError } from './error.js'
|
|
9
9
|
import { com } from './lexicons/index.js'
|
|
10
|
-
import { extractPdsUrl, extractXrpcErrorCode
|
|
11
|
-
|
|
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
|
+
*/
|
|
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
|
}
|
|
@@ -37,7 +58,7 @@ export type PasswordSessionOptions = {
|
|
|
37
58
|
*
|
|
38
59
|
* @note this function **must** not throw
|
|
39
60
|
*/
|
|
40
|
-
onUpdated
|
|
61
|
+
onUpdated?: (this: PasswordSession, data: SessionData) => void | Promise<void>
|
|
41
62
|
|
|
42
63
|
/**
|
|
43
64
|
* Called whenever the session update fails due to an expected error, such as
|
|
@@ -61,7 +82,7 @@ export type PasswordSessionOptions = {
|
|
|
61
82
|
*
|
|
62
83
|
* @note this function **must** not throw
|
|
63
84
|
*/
|
|
64
|
-
onDeleted
|
|
85
|
+
onDeleted?: (this: PasswordSession, data: SessionData) => void | Promise<void>
|
|
65
86
|
|
|
66
87
|
/**
|
|
67
88
|
* Called whenever a session deletion fails due to an unexpected error, such
|
|
@@ -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
|
|
@@ -96,7 +155,7 @@ export class PasswordSession implements Agent {
|
|
|
96
155
|
|
|
97
156
|
constructor(
|
|
98
157
|
sessionData: SessionData,
|
|
99
|
-
protected readonly options: PasswordSessionOptions,
|
|
158
|
+
protected readonly options: PasswordSessionOptions = {},
|
|
100
159
|
) {
|
|
101
160
|
this.#serviceAgent = buildAgent({
|
|
102
161
|
service: sessionData.service,
|
|
@@ -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
|
-
throw new
|
|
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(
|
|
@@ -200,7 +310,7 @@ export class PasswordSession implements Agent {
|
|
|
200
310
|
|
|
201
311
|
if (!response.success && response.matchesSchema()) {
|
|
202
312
|
// Expected errors that indicate the session is no longer valid
|
|
203
|
-
await this.options.onDeleted
|
|
313
|
+
await this.options.onDeleted?.call(this, sessionData)
|
|
204
314
|
|
|
205
315
|
// Update the session promise to a rejected state
|
|
206
316
|
this.#sessionData = null
|
|
@@ -208,6 +318,10 @@ export class PasswordSession implements Agent {
|
|
|
208
318
|
}
|
|
209
319
|
|
|
210
320
|
if (!response.success) {
|
|
321
|
+
response.error
|
|
322
|
+
if (response.matchesSchema()) {
|
|
323
|
+
response.error
|
|
324
|
+
}
|
|
211
325
|
// We failed to refresh the token, assume the session might still be
|
|
212
326
|
// valid by returning the existing session.
|
|
213
327
|
await this.options.onUpdateFailure?.call(this, sessionData, response)
|
|
@@ -238,7 +352,7 @@ export class PasswordSession implements Agent {
|
|
|
238
352
|
service: sessionData.service,
|
|
239
353
|
}
|
|
240
354
|
|
|
241
|
-
await this.options.onUpdated
|
|
355
|
+
await this.options.onUpdated?.call(this, newSession)
|
|
242
356
|
|
|
243
357
|
return (this.#sessionData = newSession)
|
|
244
358
|
})
|
|
@@ -246,6 +360,21 @@ export class PasswordSession implements Agent {
|
|
|
246
360
|
return this.#sessionPromise
|
|
247
361
|
}
|
|
248
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
|
+
*/
|
|
249
378
|
async logout(): Promise<void> {
|
|
250
379
|
let reason: DeleteFailure | null = null
|
|
251
380
|
|
|
@@ -257,11 +386,11 @@ export class PasswordSession implements Agent {
|
|
|
257
386
|
)
|
|
258
387
|
|
|
259
388
|
if (result.success || result.matchesSchema()) {
|
|
260
|
-
await this.options.onDeleted
|
|
389
|
+
await this.options.onDeleted?.call(this, sessionData)
|
|
261
390
|
|
|
262
391
|
// Update the session promise to a rejected state
|
|
263
392
|
this.#sessionData = null
|
|
264
|
-
throw new
|
|
393
|
+
throw new Error('Logged out')
|
|
265
394
|
} else {
|
|
266
395
|
// Capture the reason for the failure to re-throw in the outer promise
|
|
267
396
|
reason = result
|
|
@@ -287,34 +416,111 @@ export class PasswordSession implements Agent {
|
|
|
287
416
|
}
|
|
288
417
|
|
|
289
418
|
/**
|
|
290
|
-
*
|
|
291
|
-
* account credentials. Instead, it is strongly advised to use OAuth based
|
|
292
|
-
* authentication for main username/password credentials and use
|
|
293
|
-
* {@link PasswordSession} with an app-password, for bots, scripts, or similar
|
|
294
|
-
* use-cases.
|
|
419
|
+
* Creates a new account and returns an authenticated session.
|
|
295
420
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* `AuthFactorTokenRequired` error code will be thrown.
|
|
421
|
+
* This static method registers a new account on the specified service and
|
|
422
|
+
* automatically creates an authenticated session for it.
|
|
299
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)
|
|
300
428
|
*
|
|
301
|
-
* @example
|
|
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
|
+
*/
|
|
444
|
+
static async createAccount(
|
|
445
|
+
body: com.atproto.server.createAccount.InputBody,
|
|
446
|
+
{
|
|
447
|
+
service,
|
|
448
|
+
headers,
|
|
449
|
+
...options
|
|
450
|
+
}: PasswordSessionOptions & {
|
|
451
|
+
headers?: HeadersInit
|
|
452
|
+
service: string | URL
|
|
453
|
+
},
|
|
454
|
+
): Promise<PasswordSession> {
|
|
455
|
+
const response = await xrpc(
|
|
456
|
+
buildAgent({ service, headers, fetch: options.fetch }),
|
|
457
|
+
com.atproto.server.createAccount.main,
|
|
458
|
+
{ body },
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
const data: SessionData = {
|
|
462
|
+
...response.body,
|
|
463
|
+
service: String(service),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const agent = new PasswordSession(data, options)
|
|
467
|
+
await options.onUpdated?.call(agent, data)
|
|
468
|
+
return agent
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
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.
|
|
476
|
+
*
|
|
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.
|
|
302
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.)
|
|
491
|
+
*
|
|
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
|
+
* ```
|
|
501
|
+
*
|
|
502
|
+
* @example Handling 2FA requirement
|
|
303
503
|
* ```ts
|
|
304
504
|
* try {
|
|
305
|
-
* const session = await PasswordSession.
|
|
306
|
-
* service: 'https://
|
|
307
|
-
* identifier: 'alice',
|
|
308
|
-
* password: '
|
|
505
|
+
* const session = await PasswordSession.login({
|
|
506
|
+
* service: 'https://bsky.social',
|
|
507
|
+
* identifier: 'alice.bsky.social',
|
|
508
|
+
* password: 'xxxx-xxxx-xxxx-xxxx',
|
|
309
509
|
* })
|
|
310
510
|
* } catch (err) {
|
|
311
|
-
* if (err instanceof
|
|
312
|
-
*
|
|
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
|
+
* })
|
|
313
519
|
* }
|
|
314
520
|
* }
|
|
315
521
|
* ```
|
|
316
522
|
*/
|
|
317
|
-
static async
|
|
523
|
+
static async login({
|
|
318
524
|
service,
|
|
319
525
|
identifier,
|
|
320
526
|
password,
|
|
@@ -352,7 +558,7 @@ export class PasswordSession implements Agent {
|
|
|
352
558
|
}
|
|
353
559
|
|
|
354
560
|
const agent = new PasswordSession(data, options)
|
|
355
|
-
await options.onUpdated
|
|
561
|
+
await options.onUpdated?.call(agent, data)
|
|
356
562
|
return agent
|
|
357
563
|
}
|
|
358
564
|
|
|
@@ -387,13 +593,9 @@ export class PasswordSession implements Agent {
|
|
|
387
593
|
*/
|
|
388
594
|
static async delete(
|
|
389
595
|
data: SessionData,
|
|
390
|
-
options?:
|
|
596
|
+
options?: PasswordSessionOptions,
|
|
391
597
|
): Promise<void> {
|
|
392
|
-
const agent = new PasswordSession(data,
|
|
393
|
-
...options,
|
|
394
|
-
onUpdated: options?.onUpdated ?? noop,
|
|
395
|
-
onDeleted: options?.onDeleted ?? noop,
|
|
396
|
-
})
|
|
598
|
+
const agent = new PasswordSession(data, options)
|
|
397
599
|
await agent.logout()
|
|
398
600
|
}
|
|
399
601
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { LexMap, LexValue } from '@atproto/lex-client'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export const noop = () => {}
|
|
2
|
+
import { lexErrorDataSchema } from '@atproto/lex-schema'
|
|
5
3
|
|
|
6
4
|
export async function extractXrpcErrorCode(
|
|
7
5
|
response: Response,
|
|
8
6
|
): Promise<string | null> {
|
|
9
7
|
const json = await peekJson(response, 10 * 1024) // Avoid reading large bodies
|
|
10
8
|
if (json === undefined) return null
|
|
11
|
-
if (!
|
|
9
|
+
if (!lexErrorDataSchema.matches(json)) return null
|
|
12
10
|
return json.error
|
|
13
11
|
}
|
|
14
12
|
|