@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/error.d.ts +2 -1
  3. package/dist/error.d.ts.map +1 -1
  4. package/dist/error.js.map +1 -1
  5. package/dist/password-session.d.ts +1 -1
  6. package/dist/password-session.d.ts.map +1 -1
  7. package/dist/password-session.js +1 -1
  8. package/dist/password-session.js.map +1 -1
  9. package/dist/util.d.ts +1 -1
  10. package/dist/util.d.ts.map +1 -1
  11. package/dist/util.js.map +1 -1
  12. package/package.json +10 -13
  13. package/src/error.ts +0 -50
  14. package/src/index.ts +0 -2
  15. package/src/lexicons/com/atproto/server/createAccount.defs.ts +0 -57
  16. package/src/lexicons/com/atproto/server/createAccount.ts +0 -5
  17. package/src/lexicons/com/atproto/server/createSession.defs.ts +0 -56
  18. package/src/lexicons/com/atproto/server/createSession.ts +0 -5
  19. package/src/lexicons/com/atproto/server/deleteSession.defs.ts +0 -36
  20. package/src/lexicons/com/atproto/server/deleteSession.ts +0 -5
  21. package/src/lexicons/com/atproto/server/getSession.defs.ts +0 -41
  22. package/src/lexicons/com/atproto/server/getSession.ts +0 -5
  23. package/src/lexicons/com/atproto/server/refreshSession.defs.ts +0 -52
  24. package/src/lexicons/com/atproto/server/refreshSession.ts +0 -5
  25. package/src/lexicons/com/atproto/server.ts +0 -9
  26. package/src/lexicons/com/atproto.ts +0 -5
  27. package/src/lexicons/com.ts +0 -5
  28. package/src/lexicons/index.ts +0 -5
  29. package/src/password-session-utils.test.ts +0 -177
  30. package/src/password-session.test.ts +0 -417
  31. package/src/password-session.ts +0 -655
  32. package/src/util.ts +0 -59
  33. package/tsconfig.build.json +0 -12
  34. package/tsconfig.json +0 -7
  35. package/tsconfig.tests.json +0 -8
@@ -1,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,5 +0,0 @@
1
- /*
2
- * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
3
- */
4
-
5
- export * from './refreshSession.defs.js'
@@ -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'
@@ -1,5 +0,0 @@
1
- /*
2
- * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
3
- */
4
-
5
- export * as server from './atproto/server.js'
@@ -1,5 +0,0 @@
1
- /*
2
- * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
3
- */
4
-
5
- export * as atproto from './com/atproto.js'
@@ -1,5 +0,0 @@
1
- /*
2
- * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
3
- */
4
-
5
- export * as com from './com.js'
@@ -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
- })