wabe 0.6.9 → 0.6.10

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 (158) hide show
  1. package/README.md +138 -32
  2. package/bucket/b.txt +1 -0
  3. package/dev/index.ts +215 -0
  4. package/dist/authentication/Session.d.ts +4 -1
  5. package/dist/authentication/interface.d.ts +16 -0
  6. package/dist/email/interface.d.ts +1 -1
  7. package/dist/graphql/resolvers.d.ts +4 -2
  8. package/dist/hooks/index.d.ts +1 -0
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.js +8713 -8867
  11. package/dist/server/index.d.ts +4 -2
  12. package/dist/utils/crypto.d.ts +7 -0
  13. package/dist/utils/helper.d.ts +4 -1
  14. package/generated/schema.graphql +16 -14
  15. package/generated/wabe.ts +4 -4
  16. package/package.json +15 -15
  17. package/src/authentication/OTP.test.ts +69 -0
  18. package/src/authentication/OTP.ts +66 -0
  19. package/src/authentication/Session.test.ts +665 -0
  20. package/src/authentication/Session.ts +529 -0
  21. package/src/authentication/defaultAuthentication.ts +214 -0
  22. package/src/authentication/index.ts +3 -0
  23. package/src/authentication/interface.ts +157 -0
  24. package/src/authentication/oauth/GitHub.test.ts +105 -0
  25. package/src/authentication/oauth/GitHub.ts +133 -0
  26. package/src/authentication/oauth/Google.test.ts +105 -0
  27. package/src/authentication/oauth/Google.ts +110 -0
  28. package/src/authentication/oauth/Oauth2Client.test.ts +225 -0
  29. package/src/authentication/oauth/Oauth2Client.ts +140 -0
  30. package/src/authentication/oauth/index.ts +2 -0
  31. package/src/authentication/oauth/utils.test.ts +35 -0
  32. package/src/authentication/oauth/utils.ts +28 -0
  33. package/src/authentication/providers/EmailOTP.test.ts +138 -0
  34. package/src/authentication/providers/EmailOTP.ts +93 -0
  35. package/src/authentication/providers/EmailPassword.test.ts +187 -0
  36. package/src/authentication/providers/EmailPassword.ts +130 -0
  37. package/src/authentication/providers/EmailPasswordSRP.test.ts +206 -0
  38. package/src/authentication/providers/EmailPasswordSRP.ts +184 -0
  39. package/src/authentication/providers/GitHub.ts +30 -0
  40. package/src/authentication/providers/Google.ts +30 -0
  41. package/src/authentication/providers/OAuth.test.ts +185 -0
  42. package/src/authentication/providers/OAuth.ts +112 -0
  43. package/src/authentication/providers/PhonePassword.test.ts +187 -0
  44. package/src/authentication/providers/PhonePassword.ts +129 -0
  45. package/src/authentication/providers/QRCodeOTP.test.ts +79 -0
  46. package/src/authentication/providers/QRCodeOTP.ts +65 -0
  47. package/src/authentication/providers/index.ts +6 -0
  48. package/src/authentication/resolvers/refreshResolver.test.ts +37 -0
  49. package/src/authentication/resolvers/refreshResolver.ts +20 -0
  50. package/src/authentication/resolvers/signInWithResolver.inte.test.ts +59 -0
  51. package/src/authentication/resolvers/signInWithResolver.test.ts +307 -0
  52. package/src/authentication/resolvers/signInWithResolver.ts +102 -0
  53. package/src/authentication/resolvers/signOutResolver.test.ts +41 -0
  54. package/src/authentication/resolvers/signOutResolver.ts +22 -0
  55. package/src/authentication/resolvers/signUpWithResolver.test.ts +186 -0
  56. package/src/authentication/resolvers/signUpWithResolver.ts +69 -0
  57. package/src/authentication/resolvers/verifyChallenge.test.ts +136 -0
  58. package/src/authentication/resolvers/verifyChallenge.ts +69 -0
  59. package/src/authentication/roles.test.ts +59 -0
  60. package/src/authentication/roles.ts +40 -0
  61. package/src/authentication/utils.test.ts +99 -0
  62. package/src/authentication/utils.ts +43 -0
  63. package/src/cache/InMemoryCache.test.ts +62 -0
  64. package/src/cache/InMemoryCache.ts +45 -0
  65. package/src/cron/index.test.ts +17 -0
  66. package/src/cron/index.ts +46 -0
  67. package/src/database/DatabaseController.test.ts +625 -0
  68. package/src/database/DatabaseController.ts +983 -0
  69. package/src/database/index.test.ts +1230 -0
  70. package/src/database/index.ts +9 -0
  71. package/src/database/interface.ts +312 -0
  72. package/src/email/DevAdapter.ts +8 -0
  73. package/src/email/EmailController.test.ts +29 -0
  74. package/src/email/EmailController.ts +13 -0
  75. package/src/email/index.ts +2 -0
  76. package/src/email/interface.ts +36 -0
  77. package/src/email/templates/sendOtpCode.ts +120 -0
  78. package/src/file/FileController.ts +28 -0
  79. package/src/file/FileDevAdapter.ts +54 -0
  80. package/src/file/hookDeleteFile.ts +27 -0
  81. package/src/file/hookReadFile.ts +70 -0
  82. package/src/file/hookUploadFile.ts +53 -0
  83. package/src/file/index.test.ts +979 -0
  84. package/src/file/index.ts +2 -0
  85. package/src/file/interface.ts +42 -0
  86. package/src/graphql/GraphQLSchema.test.ts +4399 -0
  87. package/src/graphql/GraphQLSchema.ts +928 -0
  88. package/src/graphql/index.ts +2 -0
  89. package/src/graphql/parseGraphqlSchema.ts +94 -0
  90. package/src/graphql/parser.test.ts +217 -0
  91. package/src/graphql/parser.ts +566 -0
  92. package/src/graphql/pointerAndRelationFunction.ts +200 -0
  93. package/src/graphql/resolvers.ts +467 -0
  94. package/src/graphql/tests/aggregation.test.ts +1123 -0
  95. package/src/graphql/tests/e2e.test.ts +596 -0
  96. package/src/graphql/tests/scalars.test.ts +250 -0
  97. package/src/graphql/types.ts +219 -0
  98. package/src/hooks/HookObject.test.ts +122 -0
  99. package/src/hooks/HookObject.ts +168 -0
  100. package/src/hooks/authentication.ts +76 -0
  101. package/src/hooks/createUser.test.ts +77 -0
  102. package/src/hooks/createUser.ts +10 -0
  103. package/src/hooks/defaultFields.test.ts +187 -0
  104. package/src/hooks/defaultFields.ts +40 -0
  105. package/src/hooks/deleteSession.test.ts +181 -0
  106. package/src/hooks/deleteSession.ts +20 -0
  107. package/src/hooks/hashFieldHook.test.ts +163 -0
  108. package/src/hooks/hashFieldHook.ts +97 -0
  109. package/src/hooks/index.test.ts +207 -0
  110. package/src/hooks/index.ts +430 -0
  111. package/src/hooks/permissions.test.ts +424 -0
  112. package/src/hooks/permissions.ts +113 -0
  113. package/src/hooks/protected.test.ts +551 -0
  114. package/src/hooks/protected.ts +72 -0
  115. package/src/hooks/searchableFields.test.ts +166 -0
  116. package/src/hooks/searchableFields.ts +98 -0
  117. package/src/hooks/session.test.ts +138 -0
  118. package/src/hooks/session.ts +78 -0
  119. package/src/hooks/setEmail.test.ts +216 -0
  120. package/src/hooks/setEmail.ts +35 -0
  121. package/src/hooks/setupAcl.test.ts +589 -0
  122. package/src/hooks/setupAcl.ts +29 -0
  123. package/src/index.ts +9 -0
  124. package/src/schema/Schema.test.ts +484 -0
  125. package/src/schema/Schema.ts +795 -0
  126. package/src/schema/defaultResolvers.ts +94 -0
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/resolvers/meResolver.test.ts +62 -0
  129. package/src/schema/resolvers/meResolver.ts +14 -0
  130. package/src/schema/resolvers/newFile.ts +0 -0
  131. package/src/schema/resolvers/resetPassword.test.ts +345 -0
  132. package/src/schema/resolvers/resetPassword.ts +64 -0
  133. package/src/schema/resolvers/sendEmail.test.ts +118 -0
  134. package/src/schema/resolvers/sendEmail.ts +21 -0
  135. package/src/schema/resolvers/sendOtpCode.test.ts +153 -0
  136. package/src/schema/resolvers/sendOtpCode.ts +52 -0
  137. package/src/security.test.ts +3461 -0
  138. package/src/server/defaultSessionHandler.test.ts +66 -0
  139. package/src/server/defaultSessionHandler.ts +115 -0
  140. package/src/server/generateCodegen.ts +476 -0
  141. package/src/server/index.test.ts +552 -0
  142. package/src/server/index.ts +354 -0
  143. package/src/server/interface.ts +11 -0
  144. package/src/server/routes/authHandler.ts +187 -0
  145. package/src/server/routes/index.ts +40 -0
  146. package/src/utils/crypto.test.ts +41 -0
  147. package/src/utils/crypto.ts +121 -0
  148. package/src/utils/export.ts +13 -0
  149. package/src/utils/helper.ts +195 -0
  150. package/src/utils/index.test.ts +11 -0
  151. package/src/utils/index.ts +201 -0
  152. package/src/utils/preload.ts +8 -0
  153. package/src/utils/testHelper.ts +117 -0
  154. package/tsconfig.json +32 -0
  155. package/bunfig.toml +0 -4
  156. package/dist/ai/index.d.ts +0 -1
  157. package/dist/ai/interface.d.ts +0 -9
  158. /package/dist/server/{defaultHandlers.d.ts → defaultSessionHandler.d.ts} +0 -0
@@ -0,0 +1,665 @@
1
+ import { describe, expect, it, mock, beforeEach } from 'bun:test'
2
+ import { fail } from 'node:assert'
3
+ import crypto from 'node:crypto'
4
+ import jwt, { type JwtPayload } from 'jsonwebtoken'
5
+ import { Session } from './Session'
6
+
7
+ const encryptToken = (token: string, secret: string) => {
8
+ const key = crypto.createHash('sha256').update(secret).digest()
9
+ const iv = crypto
10
+ .createHmac('sha256', key)
11
+ .update(token)
12
+ .digest()
13
+ .subarray(0, 12)
14
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
15
+ const encrypted = Buffer.concat([
16
+ cipher.update(token, 'utf8'),
17
+ cipher.final(),
18
+ ])
19
+ const tag = cipher.getAuthTag()
20
+ return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`
21
+ }
22
+
23
+ describe('Session', () => {
24
+ const mockGetObject = mock(() => Promise.resolve({}) as any)
25
+ const mockGetObjects = mock(() => Promise.resolve([]) as any)
26
+ const mockCreateObject = mock(() => Promise.resolve({ id: 'userId' })) as any
27
+ const mockDeleteObject = mock(() => Promise.resolve()) as any
28
+ const mockUpdateObject = mock(() => Promise.resolve()) as any
29
+
30
+ const controllers = {
31
+ database: {
32
+ getObject: mockGetObject,
33
+ getObjects: mockGetObjects,
34
+ createObject: mockCreateObject,
35
+ deleteObject: mockDeleteObject,
36
+ updateObject: mockUpdateObject,
37
+ },
38
+ }
39
+
40
+ beforeEach(() => {
41
+ mockGetObject.mockClear()
42
+ mockGetObjects.mockClear()
43
+ mockCreateObject.mockClear()
44
+ mockDeleteObject.mockClear()
45
+ mockUpdateObject.mockClear()
46
+ })
47
+
48
+ const context = {
49
+ isRoot: true,
50
+ wabe: {
51
+ controllers,
52
+ config: { authentication: { session: { jwtSecret: 'dev' } } },
53
+ },
54
+ } as any
55
+
56
+ it('should set all data set in the jwtTokenFields on create session', async () => {
57
+ mockGetObject.mockResolvedValueOnce({
58
+ id: 'userId',
59
+ email: 'user@email.com',
60
+ })
61
+
62
+ const session = new Session()
63
+
64
+ const jwtTokenFields = {
65
+ id: true,
66
+ email: true,
67
+ }
68
+
69
+ const { accessToken, refreshToken } = await session.create('userId', {
70
+ isRoot: true,
71
+ wabe: {
72
+ controllers,
73
+ config: {
74
+ authentication: {
75
+ session: {
76
+ jwtSecret: 'dev',
77
+ jwtTokenFields,
78
+ },
79
+ },
80
+ },
81
+ },
82
+ } as any)
83
+
84
+ const decodedAccessToken = jwt.decode(accessToken) as JwtPayload
85
+ const decodedRefreshToken = jwt.decode(refreshToken) as JwtPayload
86
+
87
+ expect(decodedAccessToken.user).toEqual({
88
+ id: 'userId',
89
+ email: 'user@email.com',
90
+ })
91
+
92
+ expect(decodedRefreshToken.user).toEqual({
93
+ id: 'userId',
94
+ email: 'user@email.com',
95
+ })
96
+
97
+ expect(mockCreateObject).toHaveBeenCalledWith({
98
+ className: '_Session',
99
+ context: expect.any(Object),
100
+ data: {
101
+ accessTokenEncrypted: expect.any(String),
102
+ accessTokenExpiresAt: expect.any(Date),
103
+ refreshTokenEncrypted: expect.any(String),
104
+ refreshTokenExpiresAt: expect.any(Date),
105
+ user: 'userId',
106
+ },
107
+ select: { id: true },
108
+ })
109
+ })
110
+
111
+ it('should set all data set in the jwtTokenFields on refresh session', async () => {
112
+ const session = new Session()
113
+
114
+ const { accessToken: oldAccessToken, refreshToken: oldRefreshToken } =
115
+ await session.create('userId', context)
116
+
117
+ mockGetObjects.mockResolvedValue([
118
+ {
119
+ id: 'sessionId',
120
+ refreshTokenEncrypted: encryptToken(oldRefreshToken, 'dev'),
121
+ refreshTokenExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
122
+ user: {
123
+ id: 'userId',
124
+ email: 'userEmail',
125
+ },
126
+ },
127
+ ])
128
+ mockGetObject.mockResolvedValue({
129
+ id: 'userId',
130
+ email: 'user@email.com',
131
+ })
132
+
133
+ const jwtTokenFields = {
134
+ id: true,
135
+ email: true,
136
+ }
137
+
138
+ const { accessToken, refreshToken } = await session.refresh(
139
+ oldAccessToken,
140
+ oldRefreshToken,
141
+ {
142
+ isRoot: true,
143
+ wabe: {
144
+ controllers,
145
+ config: {
146
+ authentication: {
147
+ session: {
148
+ jwtSecret: 'dev',
149
+ jwtTokenFields,
150
+ },
151
+ },
152
+ },
153
+ },
154
+ } as any,
155
+ )
156
+
157
+ if (!accessToken || !refreshToken) fail()
158
+
159
+ const decodedAccessToken = jwt.decode(accessToken) as JwtPayload
160
+ const decodedRefreshToken = jwt.decode(refreshToken) as JwtPayload
161
+
162
+ expect(decodedAccessToken.user).toEqual({
163
+ id: 'userId',
164
+ email: 'user@email.com',
165
+ })
166
+
167
+ expect(decodedRefreshToken.user).toEqual({
168
+ id: 'userId',
169
+ email: 'user@email.com',
170
+ })
171
+ })
172
+
173
+ it('should not set user fields if not jwtTokenFields is set on create session', async () => {
174
+ mockGetObject.mockResolvedValueOnce({
175
+ id: 'userId',
176
+ email: 'user@email.com',
177
+ })
178
+
179
+ const session = new Session()
180
+
181
+ const { accessToken, refreshToken } = await session.create(
182
+ 'userId',
183
+ context,
184
+ )
185
+
186
+ const decodedAccessToken = jwt.decode(accessToken) as JwtPayload
187
+ const decodedRefreshToken = jwt.decode(refreshToken) as JwtPayload
188
+
189
+ expect(decodedAccessToken.user).toBeUndefined()
190
+
191
+ expect(decodedRefreshToken.user).toBeUndefined()
192
+ })
193
+
194
+ it('should not set user fields if not jwtTokenFields is set on refresh session', async () => {
195
+ const session = new Session()
196
+
197
+ const { accessToken: oldAccessToken, refreshToken: oldRefreshToken } =
198
+ await session.create('userId', context)
199
+
200
+ mockGetObjects.mockResolvedValue([
201
+ {
202
+ id: 'sessionId',
203
+ refreshTokenEncrypted: encryptToken(oldRefreshToken, 'dev'),
204
+ refreshTokenExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
205
+ user: {
206
+ id: 'userId',
207
+ email: 'userEmail',
208
+ },
209
+ },
210
+ ])
211
+ mockGetObject.mockResolvedValue({
212
+ id: 'userId',
213
+ email: 'user@email.com',
214
+ })
215
+
216
+ const { accessToken, refreshToken } = await session.refresh(
217
+ oldAccessToken,
218
+ oldRefreshToken,
219
+ context,
220
+ )
221
+
222
+ if (!accessToken || !refreshToken) fail()
223
+
224
+ const decodedAccessToken = jwt.decode(accessToken) as JwtPayload
225
+ const decodedRefreshToken = jwt.decode(refreshToken) as JwtPayload
226
+
227
+ expect(decodedAccessToken.user).toBeUndefined()
228
+
229
+ expect(decodedRefreshToken.user).toBeUndefined()
230
+ })
231
+
232
+ it('should returns null if no user found', async () => {
233
+ mockGetObjects.mockResolvedValue([])
234
+
235
+ const session = new Session()
236
+
237
+ const { accessToken } = await session.create('userId', context)
238
+
239
+ const res = await session.meFromAccessToken(
240
+ { accessToken, csrfToken: '' },
241
+ context,
242
+ )
243
+
244
+ expect(res.user).toBeNull()
245
+ expect(res.sessionId).toBeNull()
246
+
247
+ expect(mockGetObjects).toHaveBeenCalledTimes(1)
248
+ expect(mockGetObjects).toHaveBeenCalledWith({
249
+ className: '_Session',
250
+ where: {
251
+ accessTokenEncrypted: { equalTo: encryptToken(accessToken, 'dev') },
252
+ OR: [
253
+ {
254
+ accessTokenExpiresAt: {
255
+ greaterThanOrEqualTo: expect.any(Date),
256
+ },
257
+ },
258
+ {
259
+ refreshTokenExpiresAt: {
260
+ greaterThanOrEqualTo: expect.any(Date),
261
+ },
262
+ },
263
+ ],
264
+ },
265
+ first: 1,
266
+ select: {
267
+ id: true,
268
+ user: true,
269
+ accessTokenExpiresAt: true,
270
+ refreshTokenExpiresAt: true,
271
+ refreshTokenEncrypted: true,
272
+ },
273
+ context: expect.any(Object),
274
+ })
275
+ })
276
+
277
+ it('should return the user associated with an access token', async () => {
278
+ mockGetObjects.mockResolvedValue([
279
+ {
280
+ id: 'sessionId',
281
+ refreshTokenEncrypted: encryptToken('refreshToken', 'dev'),
282
+ user: {
283
+ id: 'userId',
284
+ email: 'userEmail',
285
+ },
286
+ refreshTokenExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
287
+ },
288
+ ])
289
+
290
+ const session = new Session()
291
+
292
+ const { accessToken } = await session.create('userId', context)
293
+
294
+ const { sessionId, user } = await session.meFromAccessToken(
295
+ { accessToken, csrfToken: '' },
296
+ {
297
+ ...context,
298
+ wabe: {
299
+ ...context.wabe,
300
+ config: {
301
+ ...context.wabe.config,
302
+ security: { disableCSRFProtection: true },
303
+ },
304
+ },
305
+ },
306
+ )
307
+
308
+ expect(mockGetObjects).toHaveBeenCalledTimes(1)
309
+ expect(mockGetObjects).toHaveBeenCalledWith({
310
+ className: '_Session',
311
+ where: {
312
+ accessTokenEncrypted: { equalTo: encryptToken(accessToken, 'dev') },
313
+ OR: [
314
+ {
315
+ accessTokenExpiresAt: {
316
+ greaterThanOrEqualTo: expect.any(Date),
317
+ },
318
+ },
319
+ {
320
+ refreshTokenExpiresAt: {
321
+ greaterThanOrEqualTo: expect.any(Date),
322
+ },
323
+ },
324
+ ],
325
+ },
326
+ first: 1,
327
+ select: {
328
+ id: true,
329
+ user: true,
330
+ accessTokenExpiresAt: true,
331
+ refreshTokenExpiresAt: true,
332
+ refreshTokenEncrypted: true,
333
+ },
334
+ context: expect.any(Object),
335
+ })
336
+
337
+ expect(sessionId).toEqual('sessionId')
338
+ expect(user?.id).toEqual('userId')
339
+ expect(user?.email).toEqual('userEmail')
340
+ })
341
+
342
+ it('should create a new session', async () => {
343
+ const session = new Session()
344
+
345
+ const fifteenMinutes = new Date(Date.now() + 1000 * 60 * 15)
346
+ const sevenDays = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)
347
+
348
+ const { accessToken, refreshToken } = await session.create(
349
+ 'userId',
350
+ context,
351
+ )
352
+
353
+ expect(accessToken).not.toBeUndefined()
354
+ expect(refreshToken).not.toBeUndefined()
355
+
356
+ if (!accessToken || !refreshToken) fail()
357
+
358
+ const decodedAccessToken = jwt.decode(accessToken) as JwtPayload
359
+ const decodedRefreshToken = jwt.decode(refreshToken) as JwtPayload
360
+
361
+ expect(decodedAccessToken).not.toBeNull()
362
+ expect(decodedAccessToken.userId).toEqual('userId')
363
+ expect(decodedAccessToken.exp).toBeGreaterThanOrEqual(
364
+ Math.floor(fifteenMinutes.getTime() / 1000),
365
+ )
366
+ expect(decodedAccessToken.iat).toBeGreaterThanOrEqual(
367
+ Math.floor((Date.now() - 500) / 1000),
368
+ ) // minus 500ms to avoid flaky
369
+
370
+ expect(decodedRefreshToken).not.toBeNull()
371
+ expect(decodedRefreshToken.userId).toEqual('userId')
372
+ expect(decodedRefreshToken.exp).toBeGreaterThanOrEqual(
373
+ Math.floor(sevenDays.getTime() / 1000),
374
+ )
375
+ expect(decodedRefreshToken.iat).toBeGreaterThanOrEqual(
376
+ Math.floor((Date.now() - 500) / 1000),
377
+ ) // minus 500ms to avoid flaky
378
+
379
+ expect(mockCreateObject).toHaveBeenCalledTimes(1)
380
+ expect(mockCreateObject).toHaveBeenCalledWith({
381
+ className: '_Session',
382
+ context: expect.any(Object),
383
+ data: {
384
+ accessTokenEncrypted: expect.any(String),
385
+ accessTokenExpiresAt: expect.any(Date),
386
+ refreshTokenEncrypted: expect.any(String),
387
+ refreshTokenExpiresAt: expect.any(Date),
388
+ user: 'userId',
389
+ },
390
+ select: { id: true },
391
+ })
392
+ })
393
+
394
+ it('should delete a session', async () => {
395
+ const session = new Session()
396
+
397
+ await session.delete({
398
+ sessionId: 'sessionId',
399
+ wabe: {
400
+ controllers,
401
+ },
402
+ } as any)
403
+
404
+ expect(mockDeleteObject).toHaveBeenCalledTimes(1)
405
+ expect(mockDeleteObject).toHaveBeenCalledWith({
406
+ className: '_Session',
407
+ context: {
408
+ sessionId: 'sessionId',
409
+ wabe: { controllers },
410
+ isRoot: true,
411
+ },
412
+ id: 'sessionId',
413
+ select: {},
414
+ })
415
+ })
416
+
417
+ it('should refresh a session', async () => {
418
+ const session = new Session()
419
+
420
+ const { accessToken: oldAccessToken, refreshToken: oldRefreshToken } =
421
+ await session.create('userId', context)
422
+
423
+ mockGetObjects.mockResolvedValue([
424
+ {
425
+ id: 'sessionId',
426
+ refreshTokenEncrypted: encryptToken(oldRefreshToken, 'dev'),
427
+ refreshTokenExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
428
+ user: {
429
+ id: 'userId',
430
+ email: 'userEmail',
431
+ },
432
+ },
433
+ ])
434
+
435
+ const { accessToken, refreshToken } = await session.refresh(
436
+ oldAccessToken,
437
+ oldRefreshToken,
438
+ context,
439
+ )
440
+
441
+ expect(accessToken).not.toBeUndefined()
442
+ expect(refreshToken).not.toBeUndefined()
443
+
444
+ expect(mockGetObjects).toHaveBeenCalledTimes(1)
445
+ expect(mockGetObjects).toHaveBeenCalledWith({
446
+ className: '_Session',
447
+ where: {
448
+ accessTokenEncrypted: { equalTo: encryptToken(oldAccessToken, 'dev') },
449
+ refreshTokenEncrypted: {
450
+ equalTo: encryptToken(oldRefreshToken, 'dev'),
451
+ },
452
+ },
453
+ select: {
454
+ id: true,
455
+ user: {
456
+ id: true,
457
+ role: {
458
+ id: true,
459
+ name: true,
460
+ },
461
+ },
462
+ refreshTokenEncrypted: true,
463
+ refreshTokenExpiresAt: true,
464
+ },
465
+ context: expect.any(Object),
466
+ })
467
+
468
+ expect(mockUpdateObject).toHaveBeenCalledTimes(1)
469
+ expect(mockUpdateObject).toHaveBeenCalledWith({
470
+ className: '_Session',
471
+ context: expect.any(Object),
472
+ id: 'sessionId',
473
+ data: {
474
+ accessTokenEncrypted: expect.any(String),
475
+ accessTokenExpiresAt: expect.any(Date),
476
+ refreshTokenEncrypted: expect.any(String),
477
+ refreshTokenExpiresAt: expect.any(Date),
478
+ },
479
+ select: {},
480
+ })
481
+
482
+ const accessTokenExpiresAt = mockUpdateObject.mock.calls[0][0].data
483
+ .accessTokenExpiresAt as Date
484
+
485
+ const refreshTokenExpiresAt = mockUpdateObject.mock.calls[0][0].data
486
+ .refreshTokenExpiresAt as Date
487
+
488
+ // -1000 to avoid flaky
489
+ expect(accessTokenExpiresAt.getTime()).toBeGreaterThan(
490
+ Date.now() + 1000 * 60 * 15 - 1000,
491
+ )
492
+
493
+ // -1000 to avoid flaky
494
+ expect(refreshTokenExpiresAt.getTime()).toBeGreaterThan(
495
+ Date.now() + 1000 * 60 * 60 * 24 * 7 - 1000,
496
+ )
497
+ })
498
+
499
+ it('should return null if access token is invalid (malformed)', async () => {
500
+ const session = new Session()
501
+
502
+ const res = await session.meFromAccessToken(
503
+ { accessToken: 'not-a-jwt', csrfToken: '' },
504
+ context,
505
+ )
506
+
507
+ expect(res.accessToken).toBeNull()
508
+ expect(res.user).toBeNull()
509
+ expect(res.sessionId).toBeNull()
510
+ })
511
+
512
+ it('should enforce CSRF by default when cookies are used', async () => {
513
+ const session = new Session()
514
+
515
+ mockGetObjects.mockResolvedValue([
516
+ {
517
+ id: 'sessionId',
518
+ refreshTokenEncrypted: encryptToken('refreshToken', 'dev'),
519
+ refreshTokenExpiresAt: new Date(Date.now() + 1000 * 60 * 60),
520
+ accessTokenExpiresAt: new Date(Date.now() + 1000 * 60 * 15),
521
+ user: {
522
+ id: 'userId',
523
+ },
524
+ },
525
+ ])
526
+
527
+ const res = await session.meFromAccessToken(
528
+ { accessToken: 'valid.jwt.token', csrfToken: '' },
529
+ {
530
+ isRoot: true,
531
+ wabe: {
532
+ controllers,
533
+ config: {
534
+ authentication: {
535
+ session: { jwtSecret: 'dev' },
536
+ },
537
+ security: {
538
+ // disableCSRFProtection undefined => protection ON
539
+ },
540
+ },
541
+ },
542
+ } as any,
543
+ )
544
+
545
+ expect(res.accessToken).toBeNull()
546
+ expect(res.user).toBeNull()
547
+ expect(res.sessionId).toBeNull()
548
+ })
549
+
550
+ it('should not refresh session if the access token does not already take 75% of time', () => {
551
+ const session = new Session()
552
+
553
+ // 1 hour
554
+ const refreshTokenAgeInMs = 1000 * 60 * 60
555
+
556
+ // Expires in 1 hour
557
+ const date1 = new Date(Date.now() + 1000 * 60 * 60)
558
+ // Expires in 20 minutes
559
+ const date2 = new Date(Date.now() + 1000 * 60 * 15)
560
+ // Expired since 20 minutes
561
+ const date3 = new Date(Date.now() - 1000 * 60 * 20)
562
+
563
+ expect(session._isRefreshTokenExpired(date1, refreshTokenAgeInMs)).toBe(
564
+ false,
565
+ )
566
+ expect(session._isRefreshTokenExpired(date2, refreshTokenAgeInMs)).toBe(
567
+ true,
568
+ )
569
+ expect(session._isRefreshTokenExpired(date3, refreshTokenAgeInMs)).toBe(
570
+ true,
571
+ )
572
+ })
573
+
574
+ it('should return null on refresh session if session not found', async () => {
575
+ mockGetObjects.mockResolvedValue([])
576
+
577
+ const session = new Session()
578
+
579
+ const { accessToken: oldAccessToken, refreshToken: oldRefreshToken } =
580
+ await session.create('userId', context)
581
+
582
+ const { accessToken, refreshToken } = await session.refresh(
583
+ oldAccessToken,
584
+ oldRefreshToken,
585
+ context,
586
+ )
587
+
588
+ expect(accessToken).toBeNull()
589
+ expect(refreshToken).toBeNull()
590
+
591
+ expect(mockGetObjects).toHaveBeenCalledTimes(1)
592
+ expect(mockGetObjects).toHaveBeenCalledWith({
593
+ className: '_Session',
594
+ where: {
595
+ accessTokenEncrypted: { equalTo: encryptToken(oldAccessToken, 'dev') },
596
+ refreshTokenEncrypted: {
597
+ equalTo: encryptToken(oldRefreshToken, 'dev'),
598
+ },
599
+ },
600
+ select: {
601
+ id: true,
602
+ user: {
603
+ id: true,
604
+ role: {
605
+ id: true,
606
+ name: true,
607
+ },
608
+ },
609
+ refreshTokenEncrypted: true,
610
+ refreshTokenExpiresAt: true,
611
+ },
612
+ context: expect.any(Object),
613
+ })
614
+ })
615
+
616
+ it("should throw an error on refresh session if session's refresh token is expired", async () => {
617
+ mockGetObjects.mockResolvedValue([
618
+ {
619
+ id: 'sessionId',
620
+ refreshTokenEncrypted: encryptToken('refreshToken', 'dev'),
621
+ refreshTokenExpiresAt: new Date(Date.now() - 1000),
622
+ user: {
623
+ id: 'userId',
624
+ email: 'userEmail',
625
+ },
626
+ },
627
+ ])
628
+
629
+ const session = new Session()
630
+
631
+ const { refreshToken, accessToken } = await session.create(
632
+ 'userId',
633
+ context,
634
+ )
635
+
636
+ expect(session.refresh(accessToken, refreshToken, context)).rejects.toThrow(
637
+ 'Refresh token expired',
638
+ )
639
+ })
640
+
641
+ it("should throw an error on refresh session if session's refresh token is not the same as the one in the database", async () => {
642
+ mockGetObjects.mockResolvedValue([
643
+ {
644
+ id: 'sessionId',
645
+ refreshTokenEncrypted: encryptToken('otherRefreshToken', 'dev'),
646
+ refreshTokenExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
647
+ user: {
648
+ id: 'userId',
649
+ email: 'userEmail',
650
+ },
651
+ },
652
+ ])
653
+
654
+ const session = new Session()
655
+
656
+ const { refreshToken, accessToken } = await session.create(
657
+ 'userId',
658
+ context,
659
+ )
660
+
661
+ expect(session.refresh(accessToken, refreshToken, context)).rejects.toThrow(
662
+ 'Invalid refresh token',
663
+ )
664
+ })
665
+ })