@atproto/lex-password-session 0.1.2 → 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.
- package/CHANGELOG.md +19 -0
- package/dist/error.d.ts +2 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js.map +1 -1
- package/dist/password-session.d.ts +1 -1
- package/dist/password-session.d.ts.map +1 -1
- package/dist/password-session.js +1 -1
- package/dist/password-session.js.map +1 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js.map +1 -1
- package/package.json +10 -13
- package/src/error.ts +0 -50
- package/src/index.ts +0 -2
- package/src/lexicons/com/atproto/server/createAccount.defs.ts +0 -57
- package/src/lexicons/com/atproto/server/createAccount.ts +0 -5
- package/src/lexicons/com/atproto/server/createSession.defs.ts +0 -56
- package/src/lexicons/com/atproto/server/createSession.ts +0 -5
- package/src/lexicons/com/atproto/server/deleteSession.defs.ts +0 -36
- package/src/lexicons/com/atproto/server/deleteSession.ts +0 -5
- package/src/lexicons/com/atproto/server/getSession.defs.ts +0 -41
- package/src/lexicons/com/atproto/server/getSession.ts +0 -5
- package/src/lexicons/com/atproto/server/refreshSession.defs.ts +0 -52
- package/src/lexicons/com/atproto/server/refreshSession.ts +0 -5
- package/src/lexicons/com/atproto/server.ts +0 -9
- package/src/lexicons/com/atproto.ts +0 -5
- package/src/lexicons/com.ts +0 -5
- package/src/lexicons/index.ts +0 -5
- package/src/password-session-utils.test.ts +0 -177
- package/src/password-session.test.ts +0 -417
- package/src/password-session.ts +0 -655
- package/src/util.ts +0 -59
- package/tsconfig.build.json +0 -12
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { l } from '@atproto/lex-schema'
|
|
6
|
-
|
|
7
|
-
const $nsid = 'com.atproto.server.refreshSession'
|
|
8
|
-
|
|
9
|
-
export { $nsid }
|
|
10
|
-
|
|
11
|
-
export const $params = /*#__PURE__*/ l.params()
|
|
12
|
-
|
|
13
|
-
export type $Params = l.InferOutput<typeof $params>
|
|
14
|
-
|
|
15
|
-
export const $input = /*#__PURE__*/ l.payload()
|
|
16
|
-
|
|
17
|
-
export type $Input<B = l.BinaryData> = l.InferPayload<typeof $input, B>
|
|
18
|
-
export type $InputBody<B = l.BinaryData> = l.InferPayloadBody<typeof $input, B>
|
|
19
|
-
|
|
20
|
-
export const $output = /*#__PURE__*/ l.jsonPayload({
|
|
21
|
-
accessJwt: /*#__PURE__*/ l.string(),
|
|
22
|
-
refreshJwt: /*#__PURE__*/ l.string(),
|
|
23
|
-
handle: /*#__PURE__*/ l.string({ format: 'handle' }),
|
|
24
|
-
did: /*#__PURE__*/ l.string({ format: 'did' }),
|
|
25
|
-
didDoc: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.lexMap()),
|
|
26
|
-
email: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()),
|
|
27
|
-
emailConfirmed: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
28
|
-
emailAuthFactor: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
29
|
-
active: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
|
30
|
-
status: /*#__PURE__*/ l.optional(
|
|
31
|
-
/*#__PURE__*/ l.string<{
|
|
32
|
-
knownValues: ['takendown', 'suspended', 'deactivated']
|
|
33
|
-
}>(),
|
|
34
|
-
),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
export type $Output<B = l.BinaryData> = l.InferPayload<typeof $output, B>
|
|
38
|
-
export type $OutputBody<B = l.BinaryData> = l.InferPayloadBody<
|
|
39
|
-
typeof $output,
|
|
40
|
-
B
|
|
41
|
-
>
|
|
42
|
-
|
|
43
|
-
/** Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt'). */
|
|
44
|
-
const main = /*#__PURE__*/ l.procedure($nsid, $params, $input, $output, [
|
|
45
|
-
'AccountTakedown',
|
|
46
|
-
'InvalidToken',
|
|
47
|
-
'ExpiredToken',
|
|
48
|
-
])
|
|
49
|
-
|
|
50
|
-
export { main }
|
|
51
|
-
|
|
52
|
-
export const $lxm = $nsid
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export * as createAccount from './server/createAccount.js'
|
|
6
|
-
export * as createSession from './server/createSession.js'
|
|
7
|
-
export * as deleteSession from './server/deleteSession.js'
|
|
8
|
-
export * as getSession from './server/getSession.js'
|
|
9
|
-
export * as refreshSession from './server/refreshSession.js'
|
package/src/lexicons/com.ts
DELETED
package/src/lexicons/index.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-namespace */
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
import { DidString, HandleString } from '@atproto/lex-schema'
|
|
5
|
-
import { LexServerAuthError } from '@atproto/lex-server'
|
|
6
|
-
|
|
7
|
-
const randomString = () =>
|
|
8
|
-
Math.random().toString(36).substring(2, 10) +
|
|
9
|
-
Math.random().toString(36).substring(2, 10)
|
|
10
|
-
|
|
11
|
-
export class Session {
|
|
12
|
-
active = true
|
|
13
|
-
accessJwt = randomString()
|
|
14
|
-
refreshJwt = randomString()
|
|
15
|
-
constructor(readonly identifier: string) {}
|
|
16
|
-
get did(): DidString {
|
|
17
|
-
return `did:example:${this.identifier}`
|
|
18
|
-
}
|
|
19
|
-
get handle(): HandleString {
|
|
20
|
-
return `${this.identifier}.example`
|
|
21
|
-
}
|
|
22
|
-
get email(): string {
|
|
23
|
-
return `${this.identifier}@example.com`
|
|
24
|
-
}
|
|
25
|
-
rotate() {
|
|
26
|
-
this.accessJwt = randomString()
|
|
27
|
-
this.refreshJwt = randomString()
|
|
28
|
-
return this
|
|
29
|
-
}
|
|
30
|
-
destroy() {
|
|
31
|
-
this.active = false
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('Session', () => {
|
|
36
|
-
it('generates DID and handle from identifier', async () => {
|
|
37
|
-
const session = new Session('alice')
|
|
38
|
-
expect(session.did).toBe('did:example:alice')
|
|
39
|
-
expect(session.handle).toBe('alice.example')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('rotates tokens', async () => {
|
|
43
|
-
const session = new Session('alice')
|
|
44
|
-
const oldAccess = session.accessJwt
|
|
45
|
-
const oldRefresh = session.refreshJwt
|
|
46
|
-
session.rotate()
|
|
47
|
-
expect(session.accessJwt).not.toBe(oldAccess)
|
|
48
|
-
expect(session.refreshJwt).not.toBe(oldRefresh)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('destroys session', async () => {
|
|
52
|
-
const session = new Session('alice')
|
|
53
|
-
expect(session.active).toBe(true)
|
|
54
|
-
session.destroy()
|
|
55
|
-
expect(session.active).toBe(false)
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
export class AuthVerifier {
|
|
60
|
-
sessions: Session[] = []
|
|
61
|
-
|
|
62
|
-
async create(credentials: {
|
|
63
|
-
identifier: string
|
|
64
|
-
password: string
|
|
65
|
-
authFactorToken?: string
|
|
66
|
-
}) {
|
|
67
|
-
if (!credentials.identifier || credentials.password !== 'password123') {
|
|
68
|
-
throw new LexServerAuthError(
|
|
69
|
-
'AuthenticationRequired',
|
|
70
|
-
'Invalid identifier',
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
if (credentials.authFactorToken !== '2fa-token') {
|
|
74
|
-
throw new LexServerAuthError(
|
|
75
|
-
'AuthFactorTokenRequired',
|
|
76
|
-
'2FA token is required',
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
const session = new Session(credentials.identifier)
|
|
80
|
-
this.sessions.push(session)
|
|
81
|
-
return session
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async findBy(predicate: (s: Session) => boolean) {
|
|
85
|
-
return this.sessions.find((s) => s.active && predicate(s))
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
accessStrategy = async ({ request }: { request: Request }) => {
|
|
89
|
-
const auth = request.headers.get('authorization')
|
|
90
|
-
const token = auth?.startsWith('Bearer ') && auth.slice(7)
|
|
91
|
-
const session = await this.findBy((s) => s.accessJwt === token)
|
|
92
|
-
if (!session) {
|
|
93
|
-
throw new LexServerAuthError('AuthenticationRequired', 'Invalid token', {
|
|
94
|
-
Bearer: { realm: 'access token' },
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
return { session }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
refreshStrategy = async ({ request }: { request: Request }) => {
|
|
101
|
-
const auth = request.headers.get('authorization')
|
|
102
|
-
const token = auth?.startsWith('Bearer ') && auth.slice(7)
|
|
103
|
-
const session = await this.findBy((s) => s.refreshJwt === token)
|
|
104
|
-
if (!session) {
|
|
105
|
-
throw new LexServerAuthError('ExpiredToken', 'Invalid token', {
|
|
106
|
-
Bearer: { realm: 'refresh token' },
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
return { session }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
describe('AuthVerifier', () => {
|
|
114
|
-
it('creates session with valid credentials', async () => {
|
|
115
|
-
const verifier = new AuthVerifier()
|
|
116
|
-
const session = await verifier.create({
|
|
117
|
-
identifier: 'alice',
|
|
118
|
-
password: 'password123',
|
|
119
|
-
authFactorToken: '2fa-token',
|
|
120
|
-
})
|
|
121
|
-
expect(session.identifier).toBe('alice')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('rejects invalid credentials', async () => {
|
|
125
|
-
const verifier = new AuthVerifier()
|
|
126
|
-
await expect(
|
|
127
|
-
verifier.create({
|
|
128
|
-
identifier: 'alice',
|
|
129
|
-
password: 'wrong-password',
|
|
130
|
-
}),
|
|
131
|
-
).rejects.toMatchObject({
|
|
132
|
-
error: 'AuthenticationRequired',
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('rejects missing 2fa token', async () => {
|
|
137
|
-
const verifier = new AuthVerifier()
|
|
138
|
-
await expect(
|
|
139
|
-
verifier.create({
|
|
140
|
-
identifier: 'alice',
|
|
141
|
-
password: 'password123',
|
|
142
|
-
}),
|
|
143
|
-
).rejects.toMatchObject({
|
|
144
|
-
error: 'AuthFactorTokenRequired',
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('finds session by access token', async () => {
|
|
149
|
-
const verifier = new AuthVerifier()
|
|
150
|
-
const session = await verifier.create({
|
|
151
|
-
identifier: 'alice',
|
|
152
|
-
password: 'password123',
|
|
153
|
-
authFactorToken: '2fa-token',
|
|
154
|
-
})
|
|
155
|
-
const found = await verifier.accessStrategy({
|
|
156
|
-
request: new Request('http://example.com', {
|
|
157
|
-
headers: { authorization: `Bearer ${session.accessJwt}` },
|
|
158
|
-
}),
|
|
159
|
-
})
|
|
160
|
-
expect(found.session).toBe(session)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('finds session by refresh token', async () => {
|
|
164
|
-
const verifier = new AuthVerifier()
|
|
165
|
-
const session = await verifier.create({
|
|
166
|
-
identifier: 'alice',
|
|
167
|
-
password: 'password123',
|
|
168
|
-
authFactorToken: '2fa-token',
|
|
169
|
-
})
|
|
170
|
-
const found = await verifier.refreshStrategy({
|
|
171
|
-
request: new Request('http://example.com', {
|
|
172
|
-
headers: { authorization: `Bearer ${session.refreshJwt}` },
|
|
173
|
-
}),
|
|
174
|
-
})
|
|
175
|
-
expect(found.session).toBe(session)
|
|
176
|
-
})
|
|
177
|
-
})
|
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-namespace */
|
|
2
|
-
|
|
3
|
-
import { afterAll, assert, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
4
|
-
import { Client, XrpcAuthenticationError } from '@atproto/lex-client'
|
|
5
|
-
import { l } from '@atproto/lex-schema'
|
|
6
|
-
import { LexRouter, LexServerAuthError } from '@atproto/lex-server'
|
|
7
|
-
import { Server, serve } from '@atproto/lex-server/nodejs'
|
|
8
|
-
import { LexAuthFactorError } from './error.js'
|
|
9
|
-
import { com } from './lexicons/index.js'
|
|
10
|
-
import { AuthVerifier } from './password-session-utils.test.js'
|
|
11
|
-
import {
|
|
12
|
-
PasswordSession,
|
|
13
|
-
PasswordSessionOptions,
|
|
14
|
-
SessionData,
|
|
15
|
-
} from './password-session.js'
|
|
16
|
-
|
|
17
|
-
const defaultOptions: Partial<PasswordSessionOptions> = {
|
|
18
|
-
onUpdateFailure: async (session, cause) => {
|
|
19
|
-
throw new Error('Should not fail to refresh session', { cause })
|
|
20
|
-
},
|
|
21
|
-
onDeleteFailure: async (session, cause) => {
|
|
22
|
-
throw new Error('Should not fail to delete session', { cause })
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Example app lexicon
|
|
27
|
-
namespace app {
|
|
28
|
-
export namespace example {
|
|
29
|
-
export namespace customMethod {
|
|
30
|
-
export const main = l.procedure(
|
|
31
|
-
'app.example.customMethod',
|
|
32
|
-
l.params(),
|
|
33
|
-
l.jsonPayload({ message: l.string() }),
|
|
34
|
-
l.jsonPayload({
|
|
35
|
-
message: l.string(),
|
|
36
|
-
did: l.string({ format: 'did' }),
|
|
37
|
-
}),
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export namespace expiredToken {
|
|
42
|
-
export const main = l.query(
|
|
43
|
-
'app.example.expiredToken',
|
|
44
|
-
l.params(),
|
|
45
|
-
l.payload(),
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe(PasswordSession, () => {
|
|
52
|
-
let entrywayServer: Server
|
|
53
|
-
let entrywayOrigin: string
|
|
54
|
-
|
|
55
|
-
let pdsServer: Server
|
|
56
|
-
let pdsOrigin: string
|
|
57
|
-
|
|
58
|
-
beforeAll(async () => {
|
|
59
|
-
const authVerifier = new AuthVerifier()
|
|
60
|
-
|
|
61
|
-
const entrywayRouter = new LexRouter()
|
|
62
|
-
.add(com.atproto.server.createSession, async ({ input }) => {
|
|
63
|
-
const session = await authVerifier.create(input.body)
|
|
64
|
-
|
|
65
|
-
const body: com.atproto.server.createSession.$OutputBody = {
|
|
66
|
-
accessJwt: session.accessJwt,
|
|
67
|
-
refreshJwt: session.refreshJwt,
|
|
68
|
-
|
|
69
|
-
did: session.did,
|
|
70
|
-
didDoc: {
|
|
71
|
-
'@context': 'https://w3.org/ns/did/v1',
|
|
72
|
-
id: session.did,
|
|
73
|
-
service: [
|
|
74
|
-
{
|
|
75
|
-
id: `${session.did}#atproto_pds`,
|
|
76
|
-
type: 'AtprotoPersonalDataServer',
|
|
77
|
-
serviceEndpoint: pdsUrl,
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
handle: session.handle,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { body }
|
|
85
|
-
})
|
|
86
|
-
.add(com.atproto.server.getSession, {
|
|
87
|
-
auth: authVerifier.accessStrategy,
|
|
88
|
-
handler: async ({ credentials: { session } }) => {
|
|
89
|
-
const body: com.atproto.server.getSession.$OutputBody = {
|
|
90
|
-
did: session.did,
|
|
91
|
-
didDoc: {
|
|
92
|
-
'@context': 'https://w3.org/ns/did/v1',
|
|
93
|
-
id: session.did,
|
|
94
|
-
service: [
|
|
95
|
-
{
|
|
96
|
-
id: `${session.did}#atproto_pds`,
|
|
97
|
-
type: 'AtprotoPersonalDataServer',
|
|
98
|
-
serviceEndpoint: pdsOrigin,
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
handle: session.handle,
|
|
103
|
-
email: session.email,
|
|
104
|
-
emailConfirmed: true,
|
|
105
|
-
emailAuthFactor: false,
|
|
106
|
-
active: true,
|
|
107
|
-
status: 'active',
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { body }
|
|
111
|
-
},
|
|
112
|
-
})
|
|
113
|
-
.add(com.atproto.server.refreshSession, {
|
|
114
|
-
auth: authVerifier.refreshStrategy,
|
|
115
|
-
handler: async ({ credentials: { session } }) => {
|
|
116
|
-
await session.rotate()
|
|
117
|
-
|
|
118
|
-
// Note, we omit email and didDoc here to test that they are properly
|
|
119
|
-
// fetched via getSession in the agent
|
|
120
|
-
const body: com.atproto.server.refreshSession.$OutputBody = {
|
|
121
|
-
accessJwt: session.accessJwt,
|
|
122
|
-
refreshJwt: session.refreshJwt,
|
|
123
|
-
|
|
124
|
-
did: session.did,
|
|
125
|
-
didDoc: undefined,
|
|
126
|
-
handle: session.handle,
|
|
127
|
-
|
|
128
|
-
email: undefined,
|
|
129
|
-
emailConfirmed: undefined,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return { body }
|
|
133
|
-
},
|
|
134
|
-
})
|
|
135
|
-
.add(com.atproto.server.deleteSession, {
|
|
136
|
-
auth: authVerifier.refreshStrategy,
|
|
137
|
-
handler: async ({ credentials: { session } }) => {
|
|
138
|
-
await session.destroy()
|
|
139
|
-
return {}
|
|
140
|
-
},
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
entrywayServer = await serve(entrywayRouter)
|
|
144
|
-
const { port } = entrywayServer.address() as { port: number }
|
|
145
|
-
entrywayOrigin = `http://localhost:${port}`
|
|
146
|
-
|
|
147
|
-
const pdsRouter = new LexRouter()
|
|
148
|
-
.add(app.example.customMethod, {
|
|
149
|
-
auth: authVerifier.accessStrategy,
|
|
150
|
-
handler: async ({ input, credentials: { session } }) => {
|
|
151
|
-
return { body: { message: input.body.message, did: session.did } }
|
|
152
|
-
},
|
|
153
|
-
})
|
|
154
|
-
.add(app.example.expiredToken, async () => {
|
|
155
|
-
throw new LexServerAuthError('ExpiredToken', 'Token expired')
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
pdsServer = await serve(pdsRouter)
|
|
159
|
-
const { port: pdsPort } = pdsServer.address() as { port: number }
|
|
160
|
-
pdsOrigin = `http://localhost:${pdsPort}`
|
|
161
|
-
const pdsUrl = pdsOrigin
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
afterAll(async () => {
|
|
165
|
-
entrywayServer.close()
|
|
166
|
-
pdsServer.close()
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('fails with invalid credentials', async () => {
|
|
170
|
-
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
171
|
-
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
172
|
-
|
|
173
|
-
await expect(
|
|
174
|
-
PasswordSession.login({
|
|
175
|
-
...defaultOptions,
|
|
176
|
-
service: entrywayOrigin,
|
|
177
|
-
identifier: 'alice',
|
|
178
|
-
password: 'wrong-password',
|
|
179
|
-
onDeleted,
|
|
180
|
-
onUpdated,
|
|
181
|
-
}),
|
|
182
|
-
).rejects.toMatchObject({
|
|
183
|
-
success: false,
|
|
184
|
-
error: 'AuthenticationRequired',
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
expect(onDeleted).not.toHaveBeenCalled()
|
|
188
|
-
expect(onUpdated).not.toHaveBeenCalled()
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('requires 2fa', async () => {
|
|
192
|
-
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
193
|
-
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
194
|
-
|
|
195
|
-
const result = await PasswordSession.login({
|
|
196
|
-
...defaultOptions,
|
|
197
|
-
service: entrywayOrigin,
|
|
198
|
-
identifier: 'alice',
|
|
199
|
-
password: 'password123',
|
|
200
|
-
onDeleted,
|
|
201
|
-
onUpdated,
|
|
202
|
-
}).then(
|
|
203
|
-
() => {
|
|
204
|
-
throw new Error('Expected to fail')
|
|
205
|
-
},
|
|
206
|
-
(err: unknown) => err,
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
assert(result instanceof LexAuthFactorError)
|
|
210
|
-
expect(result.error).toBe('AuthFactorTokenRequired')
|
|
211
|
-
expect(onDeleted).not.toHaveBeenCalled()
|
|
212
|
-
expect(onUpdated).not.toHaveBeenCalled()
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
it('logs in', async () => {
|
|
216
|
-
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
217
|
-
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
218
|
-
|
|
219
|
-
const session = await PasswordSession.login({
|
|
220
|
-
...defaultOptions,
|
|
221
|
-
service: entrywayOrigin,
|
|
222
|
-
identifier: 'alice',
|
|
223
|
-
password: 'password123',
|
|
224
|
-
authFactorToken: '2fa-token',
|
|
225
|
-
onDeleted,
|
|
226
|
-
onUpdated,
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
expect(onUpdated).toHaveBeenCalledTimes(1)
|
|
230
|
-
|
|
231
|
-
const client = new Client(session)
|
|
232
|
-
|
|
233
|
-
await expect(
|
|
234
|
-
client.call(app.example.customMethod, { message: 'hello' }),
|
|
235
|
-
).resolves.toMatchObject({
|
|
236
|
-
message: 'hello',
|
|
237
|
-
did: 'did:example:alice',
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
await expect(
|
|
241
|
-
client.call(app.example.customMethod, { message: 'world' }),
|
|
242
|
-
).resolves.toMatchObject({
|
|
243
|
-
message: 'world',
|
|
244
|
-
did: 'did:example:alice',
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
expect(onDeleted).not.toHaveBeenCalled()
|
|
248
|
-
|
|
249
|
-
await session.logout()
|
|
250
|
-
|
|
251
|
-
expect(onDeleted).toHaveBeenCalled()
|
|
252
|
-
|
|
253
|
-
await expect(
|
|
254
|
-
client.call(app.example.customMethod, { message: 'hello' }),
|
|
255
|
-
).rejects.toMatchObject({
|
|
256
|
-
message: 'Unable to fulfill XRPC request',
|
|
257
|
-
cause: expect.objectContaining({
|
|
258
|
-
message: 'Logged out',
|
|
259
|
-
}),
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('fails to perform unauthenticated call', async () => {
|
|
264
|
-
const client = new Client(pdsOrigin)
|
|
265
|
-
const result = await client.xrpcSafe(app.example.customMethod, {
|
|
266
|
-
body: { message: 'hello' },
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
assert(result.success === false)
|
|
270
|
-
assert(result instanceof XrpcAuthenticationError)
|
|
271
|
-
expect(result).toMatchObject({
|
|
272
|
-
success: false,
|
|
273
|
-
error: 'AuthenticationRequired',
|
|
274
|
-
})
|
|
275
|
-
expect(result.wwwAuthenticate).toEqual({
|
|
276
|
-
Bearer: { realm: 'access token' },
|
|
277
|
-
})
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
it('refreshes expired token', async () => {
|
|
281
|
-
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
282
|
-
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
283
|
-
|
|
284
|
-
const session = await PasswordSession.login({
|
|
285
|
-
...defaultOptions,
|
|
286
|
-
service: entrywayOrigin,
|
|
287
|
-
identifier: 'bob',
|
|
288
|
-
password: 'password123',
|
|
289
|
-
authFactorToken: '2fa-token',
|
|
290
|
-
onUpdated,
|
|
291
|
-
onDeleted,
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
const client = new Client(session)
|
|
295
|
-
|
|
296
|
-
await expect(
|
|
297
|
-
client.call(app.example.customMethod, { message: 'before' }),
|
|
298
|
-
).resolves.toMatchObject({
|
|
299
|
-
message: 'before',
|
|
300
|
-
did: 'did:example:bob',
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
expect(onUpdated).toHaveBeenCalledTimes(1)
|
|
304
|
-
|
|
305
|
-
await expect(client.call(app.example.expiredToken)).rejects.toThrow(
|
|
306
|
-
'Token expired',
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
expect(onUpdated).toHaveBeenCalledTimes(2)
|
|
310
|
-
expect(onUpdated).toHaveBeenLastCalledWith(
|
|
311
|
-
expect.objectContaining({
|
|
312
|
-
service: entrywayOrigin,
|
|
313
|
-
|
|
314
|
-
accessJwt: expect.any(String),
|
|
315
|
-
refreshJwt: expect.any(String),
|
|
316
|
-
|
|
317
|
-
email: expect.stringContaining('@'),
|
|
318
|
-
emailConfirmed: true,
|
|
319
|
-
emailAuthFactor: false,
|
|
320
|
-
handle: 'bob.example',
|
|
321
|
-
did: 'did:example:bob',
|
|
322
|
-
didDoc: expect.objectContaining({ id: 'did:example:bob' }),
|
|
323
|
-
}),
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
await expect(
|
|
327
|
-
client.call(app.example.customMethod, { message: 'after' }),
|
|
328
|
-
).resolves.toMatchObject({
|
|
329
|
-
message: 'after',
|
|
330
|
-
did: 'did:example:bob',
|
|
331
|
-
})
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
it('restores session from storage', async () => {
|
|
335
|
-
const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()
|
|
336
|
-
const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()
|
|
337
|
-
|
|
338
|
-
const initialAgent = await PasswordSession.login({
|
|
339
|
-
...defaultOptions,
|
|
340
|
-
service: entrywayOrigin,
|
|
341
|
-
identifier: 'carla',
|
|
342
|
-
password: 'password123',
|
|
343
|
-
authFactorToken: '2fa-token',
|
|
344
|
-
onUpdated,
|
|
345
|
-
onDeleted,
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
expect(initialAgent.did).toEqual('did:example:carla')
|
|
349
|
-
expect(onDeleted).toHaveBeenCalledTimes(0)
|
|
350
|
-
expect(onUpdated).toHaveBeenCalledTimes(1)
|
|
351
|
-
expect(onUpdated).toHaveBeenCalledWith(
|
|
352
|
-
expect.objectContaining({
|
|
353
|
-
accessJwt: expect.any(String),
|
|
354
|
-
refreshJwt: expect.any(String),
|
|
355
|
-
}),
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
const sessionData = initialAgent.session
|
|
359
|
-
|
|
360
|
-
const resumedAgent = await PasswordSession.resume(sessionData, {
|
|
361
|
-
...defaultOptions,
|
|
362
|
-
onUpdated,
|
|
363
|
-
onDeleted,
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
expect(resumedAgent.did).toEqual('did:example:carla')
|
|
367
|
-
expect(onDeleted).toHaveBeenCalledTimes(0)
|
|
368
|
-
expect(onUpdated).toHaveBeenCalledTimes(2)
|
|
369
|
-
|
|
370
|
-
// The initial session was refreshed. The data it contains is now invalid.
|
|
371
|
-
await expect(initialAgent.refresh()).rejects.toMatchObject({
|
|
372
|
-
success: false,
|
|
373
|
-
error: 'ExpiredToken',
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
expect(onDeleted).toHaveBeenCalledTimes(1)
|
|
377
|
-
|
|
378
|
-
const client = new Client(resumedAgent)
|
|
379
|
-
await expect(
|
|
380
|
-
client.call(app.example.customMethod, { message: 'resume' }),
|
|
381
|
-
).resolves.toMatchObject({
|
|
382
|
-
message: 'resume',
|
|
383
|
-
did: 'did:example:carla',
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
expect(onDeleted).toHaveBeenCalledTimes(1)
|
|
387
|
-
expect(onUpdated).toHaveBeenCalledTimes(2)
|
|
388
|
-
|
|
389
|
-
await resumedAgent.logout()
|
|
390
|
-
|
|
391
|
-
expect(onDeleted).toHaveBeenCalledTimes(2)
|
|
392
|
-
expect(onUpdated).toHaveBeenCalledTimes(2)
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
it('silently ignores expected logout errors', async () => {
|
|
396
|
-
let sessionData: SessionData | null = null
|
|
397
|
-
|
|
398
|
-
const session = await PasswordSession.login({
|
|
399
|
-
...defaultOptions,
|
|
400
|
-
service: entrywayOrigin,
|
|
401
|
-
identifier: 'dave',
|
|
402
|
-
password: 'password123',
|
|
403
|
-
authFactorToken: '2fa-token',
|
|
404
|
-
onUpdated: (data) => {
|
|
405
|
-
sessionData = structuredClone(data)
|
|
406
|
-
},
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
assert(sessionData)
|
|
410
|
-
|
|
411
|
-
await session.logout()
|
|
412
|
-
await session.logout()
|
|
413
|
-
|
|
414
|
-
await PasswordSession.delete(sessionData)
|
|
415
|
-
await PasswordSession.delete(sessionData)
|
|
416
|
-
})
|
|
417
|
-
})
|