@atproto/lex-password-session 0.0.4 → 0.0.6
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 +20 -0
- package/README.md +187 -167
- package/dist/error.d.ts +47 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +47 -0
- package/dist/error.js.map +1 -1
- package/dist/lexicons/com/atproto/server/createAccount.defs.d.ts +33 -33
- package/dist/lexicons/com/atproto/server/createAccount.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/server/createAccount.defs.js +2 -2
- package/dist/lexicons/com/atproto/server/createAccount.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/server/createSession.defs.d.ts +37 -33
- package/dist/lexicons/com/atproto/server/createSession.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/server/createSession.defs.js +3 -2
- package/dist/lexicons/com/atproto/server/createSession.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/server/deleteSession.defs.d.ts +5 -5
- package/dist/lexicons/com/atproto/server/deleteSession.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/server/deleteSession.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/server/getSession.defs.d.ts +23 -19
- package/dist/lexicons/com/atproto/server/getSession.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/server/getSession.defs.js +3 -2
- package/dist/lexicons/com/atproto/server/getSession.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/server/refreshSession.defs.d.ts +29 -25
- package/dist/lexicons/com/atproto/server/refreshSession.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/server/refreshSession.defs.js +3 -2
- package/dist/lexicons/com/atproto/server/refreshSession.defs.js.map +1 -1
- package/dist/password-session.d.ts +191 -16
- package/dist/password-session.d.ts.map +1 -1
- package/dist/password-session.js +168 -14
- package/dist/password-session.js.map +1 -1
- package/package.json +6 -6
- package/src/error.ts +47 -0
- package/src/lexicons/com/atproto/server/createAccount.defs.ts +13 -7
- package/src/lexicons/com/atproto/server/createSession.defs.ts +17 -7
- package/src/lexicons/com/atproto/server/deleteSession.defs.ts +11 -5
- package/src/lexicons/com/atproto/server/getSession.defs.ts +12 -5
- package/src/lexicons/com/atproto/server/refreshSession.defs.ts +17 -7
- package/src/password-session.test.ts +3 -3
- package/src/password-session.ts +191 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/lex-password-session",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Password based client authentication for AT Lexicons",
|
|
6
6
|
"keywords": [
|
|
@@ -31,18 +31,18 @@
|
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
32
|
"browser": "./dist/index.js",
|
|
33
33
|
"import": "./dist/index.js",
|
|
34
|
-
"
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"tslib": "^2.8.1",
|
|
39
|
-
"@atproto/lex-
|
|
40
|
-
"@atproto/lex-
|
|
39
|
+
"@atproto/lex-schema": "^0.0.13",
|
|
40
|
+
"@atproto/lex-client": "^0.0.13"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"vitest": "^4.0.16",
|
|
44
|
-
"@atproto/lex-builder": "^0.0.
|
|
45
|
-
"@atproto/lex-server": "^0.0.
|
|
44
|
+
"@atproto/lex-builder": "^0.0.16",
|
|
45
|
+
"@atproto/lex-server": "^0.0.10"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"prebuild": "node ./scripts/lex-build.mjs",
|
package/src/error.ts
CHANGED
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
import { LexError, XrpcFailure } from '@atproto/lex-client'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when two-factor authentication (2FA) is required.
|
|
5
|
+
*
|
|
6
|
+
* This error is thrown by {@link PasswordSession.login} when the server
|
|
7
|
+
* requires an additional authentication factor (e.g., email code). Catch this
|
|
8
|
+
* error to prompt the user for their 2FA code and retry the login with the
|
|
9
|
+
* `authFactorToken` parameter.
|
|
10
|
+
*
|
|
11
|
+
* @example Handling 2FA requirement
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { PasswordSession, LexAuthFactorError } from '@atproto/lex-password-session'
|
|
14
|
+
*
|
|
15
|
+
* try {
|
|
16
|
+
* const session = await PasswordSession.login({
|
|
17
|
+
* service: 'https://bsky.social',
|
|
18
|
+
* identifier: 'alice.bsky.social',
|
|
19
|
+
* password: 'xxxx-xxxx-xxxx-xxxx',
|
|
20
|
+
* })
|
|
21
|
+
* } catch (err) {
|
|
22
|
+
* if (err instanceof LexAuthFactorError) {
|
|
23
|
+
* // Prompt user for 2FA code
|
|
24
|
+
* const token = await promptUser('Enter 2FA code from email:')
|
|
25
|
+
*
|
|
26
|
+
* // Retry with the 2FA token
|
|
27
|
+
* const session = await PasswordSession.login({
|
|
28
|
+
* service: 'https://bsky.social',
|
|
29
|
+
* identifier: 'alice.bsky.social',
|
|
30
|
+
* password: 'xxxx-xxxx-xxxx-xxxx',
|
|
31
|
+
* authFactorToken: token,
|
|
32
|
+
* })
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @extends LexError
|
|
38
|
+
*/
|
|
3
39
|
export class LexAuthFactorError extends LexError {
|
|
4
40
|
name = 'LexAuthFactorError'
|
|
5
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Creates a new LexAuthFactorError.
|
|
44
|
+
*
|
|
45
|
+
* @param cause - The underlying XRPC failure response from the server
|
|
46
|
+
*/
|
|
6
47
|
constructor(readonly cause: XrpcFailure) {
|
|
7
48
|
super(cause.error, cause.message ?? 'Auth factor token required', { cause })
|
|
8
49
|
}
|
|
9
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Converts this error to an HTTP Response.
|
|
53
|
+
*
|
|
54
|
+
* @returns A 500 Internal Server Error response (2FA errors should not be
|
|
55
|
+
* exposed to end users in server contexts)
|
|
56
|
+
*/
|
|
10
57
|
override toResponse(): Response {
|
|
11
58
|
return Response.json({ error: 'InternalServerError' }, { status: 500 })
|
|
12
59
|
}
|
|
@@ -23,14 +23,14 @@ const main =
|
|
|
23
23
|
verificationPhone: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()),
|
|
24
24
|
password: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()),
|
|
25
25
|
recoveryKey: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()),
|
|
26
|
-
plcOp: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.
|
|
26
|
+
plcOp: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.lexMap()),
|
|
27
27
|
}),
|
|
28
28
|
/*#__PURE__*/ l.jsonPayload({
|
|
29
29
|
accessJwt: /*#__PURE__*/ l.string(),
|
|
30
30
|
refreshJwt: /*#__PURE__*/ l.string(),
|
|
31
31
|
handle: /*#__PURE__*/ l.string({ format: 'handle' }),
|
|
32
32
|
did: /*#__PURE__*/ l.string({ format: 'did' }),
|
|
33
|
-
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.
|
|
33
|
+
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.lexMap()),
|
|
34
34
|
}),
|
|
35
35
|
[
|
|
36
36
|
'InvalidHandle',
|
|
@@ -44,11 +44,17 @@ const main =
|
|
|
44
44
|
)
|
|
45
45
|
export { main }
|
|
46
46
|
|
|
47
|
-
export type Params = l.InferMethodParams<typeof main>
|
|
48
|
-
export type Input = l.InferMethodInput<typeof main>
|
|
49
|
-
export type InputBody = l.InferMethodInputBody<
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
export type $Params = l.InferMethodParams<typeof main>
|
|
48
|
+
export type $Input<B = l.BinaryData> = l.InferMethodInput<typeof main, B>
|
|
49
|
+
export type $InputBody<B = l.BinaryData> = l.InferMethodInputBody<
|
|
50
|
+
typeof main,
|
|
51
|
+
B
|
|
52
|
+
>
|
|
53
|
+
export type $Output<B = l.BinaryData> = l.InferMethodOutput<typeof main, B>
|
|
54
|
+
export type $OutputBody<B = l.BinaryData> = l.InferMethodOutputBody<
|
|
55
|
+
typeof main,
|
|
56
|
+
B
|
|
57
|
+
>
|
|
52
58
|
|
|
53
59
|
export const $lxm = /*#__PURE__*/ main.nsid,
|
|
54
60
|
$params = /*#__PURE__*/ main.parameters,
|
|
@@ -25,22 +25,32 @@ const main =
|
|
|
25
25
|
refreshJwt: /*#__PURE__*/ l.string(),
|
|
26
26
|
handle: /*#__PURE__*/ l.string({ format: 'handle' }),
|
|
27
27
|
did: /*#__PURE__*/ l.string({ format: 'did' }),
|
|
28
|
-
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.
|
|
28
|
+
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.lexMap()),
|
|
29
29
|
email: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()),
|
|
30
30
|
emailConfirmed: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
31
31
|
emailAuthFactor: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
32
32
|
active: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
33
|
-
status: /*#__PURE__*/ l.optional(
|
|
33
|
+
status: /*#__PURE__*/ l.optional(
|
|
34
|
+
/*#__PURE__*/ l.string<{
|
|
35
|
+
knownValues: ['takendown', 'suspended', 'deactivated']
|
|
36
|
+
}>(),
|
|
37
|
+
),
|
|
34
38
|
}),
|
|
35
39
|
['AccountTakedown', 'AuthFactorTokenRequired'],
|
|
36
40
|
)
|
|
37
41
|
export { main }
|
|
38
42
|
|
|
39
|
-
export type Params = l.InferMethodParams<typeof main>
|
|
40
|
-
export type Input = l.InferMethodInput<typeof main>
|
|
41
|
-
export type InputBody = l.InferMethodInputBody<
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
export type $Params = l.InferMethodParams<typeof main>
|
|
44
|
+
export type $Input<B = l.BinaryData> = l.InferMethodInput<typeof main, B>
|
|
45
|
+
export type $InputBody<B = l.BinaryData> = l.InferMethodInputBody<
|
|
46
|
+
typeof main,
|
|
47
|
+
B
|
|
48
|
+
>
|
|
49
|
+
export type $Output<B = l.BinaryData> = l.InferMethodOutput<typeof main, B>
|
|
50
|
+
export type $OutputBody<B = l.BinaryData> = l.InferMethodOutputBody<
|
|
51
|
+
typeof main,
|
|
52
|
+
B
|
|
53
|
+
>
|
|
44
54
|
|
|
45
55
|
export const $lxm = /*#__PURE__*/ main.nsid,
|
|
46
56
|
$params = /*#__PURE__*/ main.parameters,
|
|
@@ -20,11 +20,17 @@ const main =
|
|
|
20
20
|
)
|
|
21
21
|
export { main }
|
|
22
22
|
|
|
23
|
-
export type Params = l.InferMethodParams<typeof main>
|
|
24
|
-
export type Input = l.InferMethodInput<typeof main>
|
|
25
|
-
export type InputBody = l.InferMethodInputBody<
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
export type $Params = l.InferMethodParams<typeof main>
|
|
24
|
+
export type $Input<B = l.BinaryData> = l.InferMethodInput<typeof main, B>
|
|
25
|
+
export type $InputBody<B = l.BinaryData> = l.InferMethodInputBody<
|
|
26
|
+
typeof main,
|
|
27
|
+
B
|
|
28
|
+
>
|
|
29
|
+
export type $Output<B = l.BinaryData> = l.InferMethodOutput<typeof main, B>
|
|
30
|
+
export type $OutputBody<B = l.BinaryData> = l.InferMethodOutputBody<
|
|
31
|
+
typeof main,
|
|
32
|
+
B
|
|
33
|
+
>
|
|
28
34
|
|
|
29
35
|
export const $lxm = /*#__PURE__*/ main.nsid,
|
|
30
36
|
$params = /*#__PURE__*/ main.parameters,
|
|
@@ -17,19 +17,26 @@ const main =
|
|
|
17
17
|
/*#__PURE__*/ l.jsonPayload({
|
|
18
18
|
handle: /*#__PURE__*/ l.string({ format: 'handle' }),
|
|
19
19
|
did: /*#__PURE__*/ l.string({ format: 'did' }),
|
|
20
|
-
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.
|
|
20
|
+
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.lexMap()),
|
|
21
21
|
email: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()),
|
|
22
22
|
emailConfirmed: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
23
23
|
emailAuthFactor: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
24
24
|
active: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
25
|
-
status: /*#__PURE__*/ l.optional(
|
|
25
|
+
status: /*#__PURE__*/ l.optional(
|
|
26
|
+
/*#__PURE__*/ l.string<{
|
|
27
|
+
knownValues: ['takendown', 'suspended', 'deactivated']
|
|
28
|
+
}>(),
|
|
29
|
+
),
|
|
26
30
|
}),
|
|
27
31
|
)
|
|
28
32
|
export { main }
|
|
29
33
|
|
|
30
|
-
export type Params = l.InferMethodParams<typeof main>
|
|
31
|
-
export type Output = l.InferMethodOutput<typeof main>
|
|
32
|
-
export type OutputBody = l.InferMethodOutputBody<
|
|
34
|
+
export type $Params = l.InferMethodParams<typeof main>
|
|
35
|
+
export type $Output<B = l.BinaryData> = l.InferMethodOutput<typeof main, B>
|
|
36
|
+
export type $OutputBody<B = l.BinaryData> = l.InferMethodOutputBody<
|
|
37
|
+
typeof main,
|
|
38
|
+
B
|
|
39
|
+
>
|
|
33
40
|
|
|
34
41
|
export const $lxm = /*#__PURE__*/ main.nsid,
|
|
35
42
|
$params = main.parameters,
|
|
@@ -20,22 +20,32 @@ const main =
|
|
|
20
20
|
refreshJwt: /*#__PURE__*/ l.string(),
|
|
21
21
|
handle: /*#__PURE__*/ l.string({ format: 'handle' }),
|
|
22
22
|
did: /*#__PURE__*/ l.string({ format: 'did' }),
|
|
23
|
-
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.
|
|
23
|
+
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.lexMap()),
|
|
24
24
|
email: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()),
|
|
25
25
|
emailConfirmed: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
26
26
|
emailAuthFactor: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
27
27
|
active: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
28
|
-
status: /*#__PURE__*/ l.optional(
|
|
28
|
+
status: /*#__PURE__*/ l.optional(
|
|
29
|
+
/*#__PURE__*/ l.string<{
|
|
30
|
+
knownValues: ['takendown', 'suspended', 'deactivated']
|
|
31
|
+
}>(),
|
|
32
|
+
),
|
|
29
33
|
}),
|
|
30
34
|
['AccountTakedown', 'InvalidToken', 'ExpiredToken'],
|
|
31
35
|
)
|
|
32
36
|
export { main }
|
|
33
37
|
|
|
34
|
-
export type Params = l.InferMethodParams<typeof main>
|
|
35
|
-
export type Input = l.InferMethodInput<typeof main>
|
|
36
|
-
export type InputBody = l.InferMethodInputBody<
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
export type $Params = l.InferMethodParams<typeof main>
|
|
39
|
+
export type $Input<B = l.BinaryData> = l.InferMethodInput<typeof main, B>
|
|
40
|
+
export type $InputBody<B = l.BinaryData> = l.InferMethodInputBody<
|
|
41
|
+
typeof main,
|
|
42
|
+
B
|
|
43
|
+
>
|
|
44
|
+
export type $Output<B = l.BinaryData> = l.InferMethodOutput<typeof main, B>
|
|
45
|
+
export type $OutputBody<B = l.BinaryData> = l.InferMethodOutputBody<
|
|
46
|
+
typeof main,
|
|
47
|
+
B
|
|
48
|
+
>
|
|
39
49
|
|
|
40
50
|
export const $lxm = /*#__PURE__*/ main.nsid,
|
|
41
51
|
$params = /*#__PURE__*/ main.parameters,
|
|
@@ -62,7 +62,7 @@ describe(PasswordSession, () => {
|
|
|
62
62
|
.add(com.atproto.server.createSession, async ({ input }) => {
|
|
63
63
|
const session = await authVerifier.create(input.body)
|
|
64
64
|
|
|
65
|
-
const body: com.atproto.server.createSession
|
|
65
|
+
const body: com.atproto.server.createSession.$OutputBody = {
|
|
66
66
|
accessJwt: session.accessJwt,
|
|
67
67
|
refreshJwt: session.refreshJwt,
|
|
68
68
|
|
|
@@ -86,7 +86,7 @@ describe(PasswordSession, () => {
|
|
|
86
86
|
.add(com.atproto.server.getSession, {
|
|
87
87
|
auth: authVerifier.accessStrategy,
|
|
88
88
|
handler: async ({ credentials: { session } }) => {
|
|
89
|
-
const body: com.atproto.server.getSession
|
|
89
|
+
const body: com.atproto.server.getSession.$OutputBody = {
|
|
90
90
|
did: session.did,
|
|
91
91
|
didDoc: {
|
|
92
92
|
'@context': 'https://w3.org/ns/did/v1',
|
|
@@ -117,7 +117,7 @@ describe(PasswordSession, () => {
|
|
|
117
117
|
|
|
118
118
|
// Note, we omit email and didDoc here to test that they are properly
|
|
119
119
|
// fetched via getSession in the agent
|
|
120
|
-
const body: com.atproto.server.refreshSession
|
|
120
|
+
const body: com.atproto.server.refreshSession.$OutputBody = {
|
|
121
121
|
accessJwt: session.accessJwt,
|
|
122
122
|
refreshJwt: session.refreshJwt,
|
|
123
123
|
|
package/src/password-session.ts
CHANGED
|
@@ -9,15 +9,36 @@ 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
|
|
|
20
|
-
|
|
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 & {
|
|
21
42
|
service: string
|
|
22
43
|
}
|
|
23
44
|
|
|
@@ -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,8 +415,34 @@ 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
|
-
body: com.atproto.server.createAccount
|
|
445
|
+
body: com.atproto.server.createAccount.$InputBody,
|
|
295
446
|
{
|
|
296
447
|
service,
|
|
297
448
|
headers,
|
|
@@ -318,29 +469,53 @@ export class PasswordSession implements Agent {
|
|
|
318
469
|
}
|
|
319
470
|
|
|
320
471
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
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
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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
|
|
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://
|
|
338
|
-
* identifier: 'alice',
|
|
339
|
-
* password: '
|
|
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
|
|
343
|
-
*
|
|
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
|
* ```
|