@shipfox/api-auth 4.0.0 → 6.0.0

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 (154) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +75 -0
  3. package/README.md +71 -37
  4. package/dist/config.d.ts +0 -12
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +0 -62
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/auth.d.ts +21 -2
  9. package/dist/core/auth.d.ts.map +1 -1
  10. package/dist/core/auth.js +109 -77
  11. package/dist/core/auth.js.map +1 -1
  12. package/dist/core/email-owner.d.ts +12 -0
  13. package/dist/core/email-owner.d.ts.map +1 -0
  14. package/dist/core/email-owner.js +20 -0
  15. package/dist/core/email-owner.js.map +1 -0
  16. package/dist/core/job-lease-token.d.ts.map +1 -1
  17. package/dist/core/job-lease-token.js +3 -2
  18. package/dist/core/job-lease-token.js.map +1 -1
  19. package/dist/core/jwt.d.ts +2 -2
  20. package/dist/core/jwt.d.ts.map +1 -1
  21. package/dist/core/jwt.js.map +1 -1
  22. package/dist/core/rate-limit.d.ts.map +1 -1
  23. package/dist/core/rate-limit.js +2 -7
  24. package/dist/core/rate-limit.js.map +1 -1
  25. package/dist/core/runner-session-token.d.ts.map +1 -1
  26. package/dist/core/runner-session-token.js +3 -2
  27. package/dist/core/runner-session-token.js.map +1 -1
  28. package/dist/db/db.d.ts +0 -218
  29. package/dist/db/db.d.ts.map +1 -1
  30. package/dist/db/db.js +0 -2
  31. package/dist/db/db.js.map +1 -1
  32. package/dist/db/users.d.ts +3 -0
  33. package/dist/db/users.d.ts.map +1 -1
  34. package/dist/db/users.js +28 -10
  35. package/dist/db/users.js.map +1 -1
  36. package/dist/index.d.ts +5 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +40 -34
  39. package/dist/index.js.map +1 -1
  40. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
  41. package/dist/presentation/auth/jwt-auth.js +2 -2
  42. package/dist/presentation/auth/jwt-auth.js.map +1 -1
  43. package/dist/presentation/e2eRoutes/create-session.d.ts +2 -1
  44. package/dist/presentation/e2eRoutes/create-session.d.ts.map +1 -1
  45. package/dist/presentation/e2eRoutes/create-session.js +42 -39
  46. package/dist/presentation/e2eRoutes/create-session.js.map +1 -1
  47. package/dist/presentation/e2eRoutes/index.d.ts +2 -1
  48. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
  49. package/dist/presentation/e2eRoutes/index.js +12 -10
  50. package/dist/presentation/e2eRoutes/index.js.map +1 -1
  51. package/dist/presentation/inter-module.d.ts +4 -0
  52. package/dist/presentation/inter-module.d.ts.map +1 -0
  53. package/dist/presentation/inter-module.js +16 -0
  54. package/dist/presentation/inter-module.js.map +1 -0
  55. package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts +2 -1
  56. package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts.map +1 -1
  57. package/dist/presentation/routes/email-verification/verify-email-confirm.js +36 -25
  58. package/dist/presentation/routes/email-verification/verify-email-confirm.js.map +1 -1
  59. package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -1
  60. package/dist/presentation/routes/email-verification/verify-email-resend.js +20 -4
  61. package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -1
  62. package/dist/presentation/routes/index.d.ts +2 -2
  63. package/dist/presentation/routes/index.d.ts.map +1 -1
  64. package/dist/presentation/routes/index.js +11 -13
  65. package/dist/presentation/routes/index.js.map +1 -1
  66. package/dist/presentation/routes/password/password-reset-confirm.d.ts +2 -1
  67. package/dist/presentation/routes/password/password-reset-confirm.d.ts.map +1 -1
  68. package/dist/presentation/routes/password/password-reset-confirm.js +29 -26
  69. package/dist/presentation/routes/password/password-reset-confirm.js.map +1 -1
  70. package/dist/presentation/routes/registration/signup.d.ts +2 -1
  71. package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
  72. package/dist/presentation/routes/registration/signup.js +86 -68
  73. package/dist/presentation/routes/registration/signup.js.map +1 -1
  74. package/dist/presentation/routes/session/login.d.ts +2 -1
  75. package/dist/presentation/routes/session/login.d.ts.map +1 -1
  76. package/dist/presentation/routes/session/login.js +40 -37
  77. package/dist/presentation/routes/session/login.js.map +1 -1
  78. package/dist/presentation/routes/session/refresh.d.ts +2 -1
  79. package/dist/presentation/routes/session/refresh.d.ts.map +1 -1
  80. package/dist/presentation/routes/session/refresh.js +42 -39
  81. package/dist/presentation/routes/session/refresh.js.map +1 -1
  82. package/dist/presentation/subscribers/index.d.ts +0 -1
  83. package/dist/presentation/subscribers/index.d.ts.map +1 -1
  84. package/dist/presentation/subscribers/index.js +0 -1
  85. package/dist/presentation/subscribers/index.js.map +1 -1
  86. package/dist/presentation/subscribers/on-password-reset-send-requested.js +1 -1
  87. package/dist/presentation/subscribers/on-password-reset-send-requested.js.map +1 -1
  88. package/dist/tsconfig.test.tsbuildinfo +1 -1
  89. package/drizzle/0000_initial.sql +1 -13
  90. package/drizzle/meta/0000_snapshot.json +0 -92
  91. package/package.json +30 -42
  92. package/src/config.ts +0 -56
  93. package/src/core/auth.test.ts +106 -187
  94. package/src/core/auth.ts +131 -87
  95. package/src/core/email-owner.test.ts +86 -0
  96. package/src/core/email-owner.ts +23 -0
  97. package/src/core/job-lease-token.test.ts +2 -1
  98. package/src/core/job-lease-token.ts +3 -2
  99. package/src/core/jwt.ts +2 -2
  100. package/src/core/rate-limit.test.ts +11 -28
  101. package/src/core/rate-limit.ts +3 -11
  102. package/src/core/runner-session-token.test.ts +2 -1
  103. package/src/core/runner-session-token.ts +3 -2
  104. package/src/db/db.ts +0 -2
  105. package/src/db/users.ts +32 -14
  106. package/src/index.test.ts +15 -11
  107. package/src/index.ts +23 -20
  108. package/src/presentation/auth/jwt-auth.test.ts +2 -1
  109. package/src/presentation/auth/jwt-auth.ts +2 -2
  110. package/src/presentation/e2eRoutes/create-session.ts +34 -30
  111. package/src/presentation/e2eRoutes/index.test.ts +9 -6
  112. package/src/presentation/e2eRoutes/index.ts +8 -5
  113. package/src/presentation/inter-module.ts +13 -0
  114. package/src/presentation/routes/email-verification/verify-email-confirm.ts +42 -24
  115. package/src/presentation/routes/email-verification/verify-email-resend.ts +18 -4
  116. package/src/presentation/routes/index.test.ts +10 -3
  117. package/src/presentation/routes/index.ts +22 -12
  118. package/src/presentation/routes/password/password-reset-confirm.ts +25 -22
  119. package/src/presentation/routes/password/password-reset-request.test.ts +1 -0
  120. package/src/presentation/routes/rate-limit.test.ts +2 -2
  121. package/src/presentation/routes/registration/signup.test.ts +39 -74
  122. package/src/presentation/routes/registration/signup.ts +89 -70
  123. package/src/presentation/routes/session/login.ts +38 -35
  124. package/src/presentation/routes/session/refresh.ts +43 -40
  125. package/src/presentation/subscribers/email-send-requested.test.ts +2 -30
  126. package/src/presentation/subscribers/index.ts +0 -1
  127. package/src/presentation/subscribers/on-password-reset-send-requested.ts +1 -1
  128. package/test/env.ts +1 -3
  129. package/test/globalSetup.ts +10 -1
  130. package/test/routes.ts +51 -84
  131. package/tsconfig.build.tsbuildinfo +1 -1
  132. package/dist/core/entities/email-verification.d.ts +0 -9
  133. package/dist/core/entities/email-verification.d.ts.map +0 -1
  134. package/dist/core/entities/email-verification.js +0 -3
  135. package/dist/core/entities/email-verification.js.map +0 -1
  136. package/dist/db/email-verifications.d.ts +0 -39
  137. package/dist/db/email-verifications.d.ts.map +0 -1
  138. package/dist/db/email-verifications.js +0 -95
  139. package/dist/db/email-verifications.js.map +0 -1
  140. package/dist/db/schema/email-verifications.d.ts +0 -114
  141. package/dist/db/schema/email-verifications.d.ts.map +0 -1
  142. package/dist/db/schema/email-verifications.js +0 -35
  143. package/dist/db/schema/email-verifications.js.map +0 -1
  144. package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts +0 -3
  145. package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts.map +0 -1
  146. package/dist/presentation/subscribers/on-email-verification-send-requested.js +0 -13
  147. package/dist/presentation/subscribers/on-email-verification-send-requested.js.map +0 -1
  148. package/src/core/entities/email-verification.ts +0 -8
  149. package/src/db/email-verifications.test.ts +0 -61
  150. package/src/db/email-verifications.ts +0 -160
  151. package/src/db/schema/email-verifications.ts +0 -37
  152. package/src/presentation/routes/email-verification/verify-email-confirm.test.ts +0 -66
  153. package/src/presentation/routes/email-verification/verify-email-resend.test.ts +0 -100
  154. package/src/presentation/subscribers/on-email-verification-send-requested.ts +0 -10
@@ -1,24 +1,20 @@
1
- import {
2
- AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
3
- AUTH_PASSWORD_RESET_SEND_REQUESTED,
4
- EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS,
5
- } from '@shipfox/api-auth-dto';
1
+ import {AUTH_PASSWORD_RESET_SEND_REQUESTED, AUTH_USER_SIGNED_UP} from '@shipfox/api-auth-dto';
2
+ import {userAccessTokenKey} from '@shipfox/node-auth-root-key';
6
3
  import type {Mailer, MailMessage} from '@shipfox/node-mailer';
7
4
  import {hashOpaqueToken} from '@shipfox/node-tokens';
8
5
  import {and, desc, eq, sql} from 'drizzle-orm';
9
6
  import {
10
7
  changePassword,
11
- confirmEmailVerification,
12
- confirmPasswordReset,
13
- createSessionForUser,
8
+ confirmPasswordReset as coreConfirmPasswordReset,
9
+ createSessionForUser as coreCreateSessionForUser,
10
+ login as coreLogin,
11
+ refreshAccessToken as coreRefreshAccessToken,
14
12
  getCurrentUser,
15
- login,
16
13
  logout,
17
14
  provisionUser,
18
- refreshAccessToken,
19
15
  requestPasswordReset,
20
- resendEmailVerification,
21
16
  signup,
17
+ signupWithInvitation,
22
18
  } from '#core/auth.js';
23
19
  import {
24
20
  AuthDependencyUnavailableError,
@@ -31,7 +27,6 @@ import {
31
27
  import {verifyUserToken} from '#core/jwt.js';
32
28
  import {db} from '#db/db.js';
33
29
  import * as refreshTokenDb from '#db/refresh-tokens.js';
34
- import {emailVerifications} from '#db/schema/email-verifications.js';
35
30
  import {authOutbox} from '#db/schema/outbox.js';
36
31
  import {passwordResets} from '#db/schema/password-resets.js';
37
32
  import {refreshTokens} from '#db/schema/refresh-tokens.js';
@@ -62,7 +57,6 @@ const testConfig = vi.hoisted(
62
57
 
63
58
  vi.mock('#config.js', () => ({
64
59
  config: {
65
- AUTH_JWT_SECRET: 'auth-core-test-secret',
66
60
  AUTH_JWT_EXPIRES_IN: '15m',
67
61
  AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
68
62
  AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
@@ -72,12 +66,25 @@ vi.mock('#config.js', () => ({
72
66
  mailer: testConfig.mailer,
73
67
  }));
74
68
 
75
- vi.mock('@shipfox/api-workspaces', () => ({
76
- listMembershipsByUser: vi.fn(() => Promise.resolve([])),
77
- }));
69
+ vi.mock('@shipfox/node-mailer', () => ({mailer: testConfig.mailer}));
78
70
 
79
- const {listMembershipsByUser} = await import('@shipfox/api-workspaces');
80
- const listMembershipsByUserMock = vi.mocked(listMembershipsByUser);
71
+ const listMembershipsByUserMock = vi.fn(() =>
72
+ Promise.resolve({memberships: [] as Array<{workspaceId: string; role: 'admin'}>}),
73
+ );
74
+ const workspaces = {
75
+ listMembershipsForTokenClaims: listMembershipsByUserMock,
76
+ preflightInvitationAcceptance: vi.fn(),
77
+ acceptInvitation: vi.fn(),
78
+ requireActiveMembership: vi.fn(),
79
+ };
80
+
81
+ const login = (params: {email: string; password: string}) => coreLogin({...params, workspaces});
82
+ const createSessionForUser = (params: {userId?: string; email?: string}) =>
83
+ coreCreateSessionForUser({...params, workspaces});
84
+ const refreshAccessToken = (params: {refreshToken: string}) =>
85
+ coreRefreshAccessToken({...params, workspaces});
86
+ const confirmPasswordReset = (params: {token: string; newPassword: string}) =>
87
+ coreConfirmPasswordReset({...params, workspaces});
81
88
 
82
89
  const TOKEN_RE = /token=([\w\-_=]+)/;
83
90
 
@@ -101,9 +108,8 @@ async function outboxEventsTo(email: string, eventType: string) {
101
108
 
102
109
  async function latestEmailLinkTo(email: string, eventType: string): Promise<string> {
103
110
  const event = (await outboxEventsTo(email, eventType))[0];
104
- const payload = event?.payload as {verifyLink?: string; resetLink?: string} | undefined;
105
- const link =
106
- eventType === AUTH_EMAIL_VERIFICATION_SEND_REQUESTED ? payload?.verifyLink : payload?.resetLink;
111
+ const payload = event?.payload as {resetLink?: string} | undefined;
112
+ const link = payload?.resetLink;
107
113
  if (!link) throw new Error(`No ${eventType} outbox link for ${email}`);
108
114
  return link;
109
115
  }
@@ -128,14 +134,22 @@ describe('auth core', () => {
128
134
  expect(user.email).toBe(email);
129
135
  expect(user.name).toBe('Sign Up');
130
136
  expect(user.emailVerifiedAt).toBeNull();
131
- expect(captured).toHaveLength(0);
132
- expect(await latestEmailLinkTo(email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toContain(
133
- `${testConfig.clientBaseUrl}/auth/verify-email?token=`,
134
- );
137
+ expect(user.emailChallenge.id).toEqual(expect.any(String));
138
+ expect(user.emailChallenge.nextResendAvailableAt).toBeInstanceOf(Date);
139
+ expect(captured).toHaveLength(1);
140
+
141
+ const events = await outboxEventsTo(email, AUTH_USER_SIGNED_UP);
142
+ expect(events).toHaveLength(1);
143
+ expect(events[0]?.payload).toEqual({
144
+ userId: user.id,
145
+ email,
146
+ name: 'Sign Up',
147
+ viaInvitation: false,
148
+ });
135
149
  });
136
150
 
137
151
  test('signup rejects duplicate email with a business error', async () => {
138
- const existing = await userFactory.create();
152
+ const existing = await userFactory.create({emailVerifiedAt: new Date()});
139
153
 
140
154
  const promise = signup({
141
155
  email: existing.email,
@@ -145,6 +159,33 @@ describe('auth core', () => {
145
159
  await expect(promise).rejects.toBeInstanceOf(EmailTakenError);
146
160
  });
147
161
 
162
+ test('signup with an invitation writes the signed-up event with its user insert', async () => {
163
+ const email = `signup-invitation-${crypto.randomUUID()}@example.com`;
164
+ const userId = crypto.randomUUID();
165
+ const workspaceId = crypto.randomUUID();
166
+ workspaces.preflightInvitationAcceptance.mockResolvedValueOnce(undefined);
167
+ workspaces.acceptInvitation.mockResolvedValueOnce({
168
+ membership: {id: crypto.randomUUID(), userId, workspaceId},
169
+ });
170
+
171
+ const result = await signupWithInvitation({
172
+ email,
173
+ password: 'correct horse battery staple',
174
+ name: 'Invited User',
175
+ invitationToken: `invite-${crypto.randomUUID()}`,
176
+ workspaces,
177
+ });
178
+
179
+ const events = await outboxEventsTo(email, AUTH_USER_SIGNED_UP);
180
+ expect(events).toHaveLength(1);
181
+ expect(events[0]?.payload).toEqual({
182
+ userId: result.user.id,
183
+ email,
184
+ name: 'Invited User',
185
+ viaInvitation: true,
186
+ });
187
+ });
188
+
148
189
  test('provisionUser creates a verified, password-less user with a normalized email', async () => {
149
190
  const email = `Provision-${crypto.randomUUID()}@EXAMPLE.COM`;
150
191
 
@@ -165,11 +206,11 @@ describe('auth core', () => {
165
206
  const storedSuspended = await findUserById({id: suspended.id});
166
207
 
167
208
  const existingUnverified = await provisionUser({
168
- email: unverified.email.toUpperCase(),
209
+ email: ` ${unverified.email.toUpperCase()} `,
169
210
  name: 'Replacement Name',
170
211
  });
171
212
  const existingSuspended = await provisionUser({
172
- email: suspended.email.toUpperCase(),
213
+ email: ` ${suspended.email.toUpperCase()} `,
173
214
  name: 'Replacement Name',
174
215
  });
175
216
 
@@ -257,6 +298,30 @@ describe('auth core', () => {
257
298
  );
258
299
  });
259
300
 
301
+ test('createSessionForUser({email}) resolves surrounding whitespace and mixed case', async () => {
302
+ const user = await userFactory.create({emailVerifiedAt: new Date()});
303
+
304
+ const result = await createSessionForUser({email: ` ${user.email.toUpperCase()} `});
305
+
306
+ expect(result.user.id).toBe(user.id);
307
+ expect(result.token).toEqual(expect.any(String));
308
+ });
309
+
310
+ test('createSessionForUser({email}) retains eligibility checks', async () => {
311
+ const unverified = await userFactory.create();
312
+ const suspended = await userFactory.create({emailVerifiedAt: new Date()});
313
+ await db().update(users).set({status: 'suspended'}).where(eq(users.id, suspended.id));
314
+
315
+ const unverifiedExpectation = expect(
316
+ createSessionForUser({email: unverified.email}),
317
+ ).rejects.toBeInstanceOf(EmailNotVerifiedError);
318
+ const suspendedExpectation = expect(
319
+ createSessionForUser({email: suspended.email}),
320
+ ).rejects.toBeInstanceOf(InvalidCredentialsError);
321
+
322
+ await Promise.all([unverifiedExpectation, suspendedExpectation]);
323
+ });
324
+
260
325
  test('refreshAccessToken rotates the refresh token', async () => {
261
326
  const user = await userFactory.create({emailVerifiedAt: new Date()});
262
327
  const loginResult = await login({email: user.email, password: user.plainPassword});
@@ -329,127 +394,6 @@ describe('auth core', () => {
329
394
  await expect(promise).rejects.toBeInstanceOf(TokenInvalidError);
330
395
  });
331
396
 
332
- test('confirmEmailVerification marks the user verified, creates a session, and rejects reused tokens', async () => {
333
- const user = await signup({
334
- email: `verify-${crypto.randomUUID()}@example.com`,
335
- password: 'correct horse battery staple',
336
- });
337
- const token = extractToken(
338
- await latestEmailLinkTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
339
- );
340
-
341
- const result = await confirmEmailVerification({token});
342
- const verified = await findUserById({id: user.id});
343
- const refreshSession = await refreshTokenDb.findActiveRefreshTokenByHash({
344
- hashedToken: hashOpaqueToken(result.refreshToken),
345
- });
346
- const reused = confirmEmailVerification({token});
347
-
348
- expect(verified?.emailVerifiedAt).toBeInstanceOf(Date);
349
- expect(result.token).toEqual(expect.any(String));
350
- expect(result.user.id).toBe(user.id);
351
- expect(refreshSession?.userId).toBe(user.id);
352
- await expect(reused).rejects.toBeInstanceOf(TokenInvalidError);
353
- });
354
-
355
- test('resendEmailVerification only sends for active unverified users', async () => {
356
- const unverified = await userFactory.create();
357
- const verified = await userFactory.create({emailVerifiedAt: new Date()});
358
- const inactive = await userFactory.create();
359
- await db().update(users).set({status: 'suspended'}).where(eq(users.id, inactive.id));
360
-
361
- const sent = await resendEmailVerification({email: unverified.email});
362
- const verifiedResult = await resendEmailVerification({email: verified.email});
363
- const inactiveResult = await resendEmailVerification({email: inactive.email});
364
- const missingResult = await resendEmailVerification({
365
- email: `missing-${crypto.randomUUID()}@example.com`,
366
- });
367
-
368
- expect(captured).toHaveLength(0);
369
- expect(
370
- await outboxEventsTo(unverified.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
371
- ).toHaveLength(1);
372
- expect(
373
- await outboxEventsTo(verified.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
374
- ).toHaveLength(0);
375
- expect(
376
- await outboxEventsTo(inactive.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
377
- ).toHaveLength(0);
378
- expect(sent.nextResendAvailableAt).toBeInstanceOf(Date);
379
- expect(verifiedResult.nextResendAvailableAt).toBeInstanceOf(Date);
380
- expect(inactiveResult.nextResendAvailableAt).toBeInstanceOf(Date);
381
- expect(missingResult.nextResendAvailableAt).toBeInstanceOf(Date);
382
- });
383
-
384
- test('resendEmailVerification respects cooldown without invalidating the current token', async () => {
385
- const user = await signup({
386
- email: `resend-cooldown-${crypto.randomUUID()}@example.com`,
387
- password: 'correct horse battery staple',
388
- });
389
- const token = extractToken(
390
- await latestEmailLinkTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
391
- );
392
- const existingCreatedAt = new Date(
393
- Date.now() - (EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS / 2) * 1000,
394
- );
395
- await db()
396
- .update(emailVerifications)
397
- .set({createdAt: existingCreatedAt})
398
- .where(eq(emailVerifications.userId, user.id));
399
-
400
- const beforeCall = Date.now();
401
- const result = await resendEmailVerification({email: user.email});
402
- const verified = await confirmEmailVerification({token});
403
-
404
- // The cooldown branch must return the generic `now + cooldown` estimate, not
405
- // `latest.createdAt + cooldown`, so the response cannot leak prior timing.
406
- expect(result.nextResendAvailableAt.getTime()).toBeGreaterThanOrEqual(
407
- beforeCall + EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS * 1000,
408
- );
409
- expect(captured).toHaveLength(0);
410
- expect(await outboxEventsTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(
411
- 1,
412
- );
413
- expect(verified.user.id).toBe(user.id);
414
- });
415
-
416
- test('resendEmailVerification sends again after cooldown', async () => {
417
- const user = await signup({
418
- email: `resend-after-cooldown-${crypto.randomUUID()}@example.com`,
419
- password: 'correct horse battery staple',
420
- });
421
- const staleCreatedAt = new Date(
422
- Date.now() - (EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS + 1) * 1000,
423
- );
424
- await db()
425
- .update(emailVerifications)
426
- .set({createdAt: staleCreatedAt})
427
- .where(eq(emailVerifications.userId, user.id));
428
-
429
- const result = await resendEmailVerification({email: user.email});
430
-
431
- expect(result.nextResendAvailableAt).toBeInstanceOf(Date);
432
- expect(captured).toHaveLength(0);
433
- expect(await outboxEventsTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(
434
- 2,
435
- );
436
- });
437
-
438
- test('resendEmailVerification serializes duplicate requests for one user', async () => {
439
- const user = await userFactory.create();
440
-
441
- const results = await Promise.all([
442
- resendEmailVerification({email: user.email}),
443
- resendEmailVerification({email: user.email}),
444
- ]);
445
-
446
- expect(results).toHaveLength(2);
447
- expect(captured).toHaveLength(0);
448
- expect(await outboxEventsTo(user.email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(
449
- 1,
450
- );
451
- });
452
-
453
397
  test('password reset request and confirm update the password and invalidate the token', async () => {
454
398
  const user = await userFactory.create({emailVerifiedAt: new Date()});
455
399
  const loginResult = await login({email: user.email, password: user.plainPassword});
@@ -593,32 +537,16 @@ describe('auth core', () => {
593
537
  const user = await userFactory.create({emailVerifiedAt: new Date()});
594
538
  const workspaceA = crypto.randomUUID();
595
539
  const workspaceB = crypto.randomUUID();
596
- listMembershipsByUserMock.mockResolvedValueOnce([
597
- {
598
- id: crypto.randomUUID(),
599
- userId: user.id,
600
- userEmail: user.email,
601
- userName: null,
602
- workspaceId: workspaceA,
603
- workspaceName: 'Workspace A',
604
- createdAt: new Date(),
605
- updatedAt: new Date(),
606
- },
607
- {
608
- id: crypto.randomUUID(),
609
- userId: user.id,
610
- userEmail: user.email,
611
- userName: null,
612
- workspaceId: workspaceB,
613
- workspaceName: 'Workspace B',
614
- createdAt: new Date(),
615
- updatedAt: new Date(),
616
- },
617
- ]);
540
+ listMembershipsByUserMock.mockResolvedValueOnce({
541
+ memberships: [
542
+ {workspaceId: workspaceA, role: 'admin'},
543
+ {workspaceId: workspaceB, role: 'admin'},
544
+ ],
545
+ });
618
546
 
619
547
  const result = await login({email: user.email, password: user.plainPassword});
620
548
 
621
- const claims = await verifyUserToken({token: result.token, secret: 'auth-core-test-secret'});
549
+ const claims = await verifyUserToken({token: result.token, secret: userAccessTokenKey()});
622
550
  expect(claims.name).toBe(user.name);
623
551
  expect(claims.memberships).toEqual([
624
552
  {workspaceId: workspaceA, role: 'admin'},
@@ -628,27 +556,18 @@ describe('auth core', () => {
628
556
 
629
557
  test('refresh re-fetches memberships so newly-added workspaces appear in the next token', async () => {
630
558
  const user = await userFactory.create({emailVerifiedAt: new Date()});
631
- listMembershipsByUserMock.mockResolvedValueOnce([]);
559
+ listMembershipsByUserMock.mockResolvedValueOnce({memberships: []});
632
560
  const loginResult = await login({email: user.email, password: user.plainPassword});
633
561
 
634
562
  const newWorkspaceId = crypto.randomUUID();
635
- listMembershipsByUserMock.mockResolvedValueOnce([
636
- {
637
- id: crypto.randomUUID(),
638
- userId: user.id,
639
- userEmail: user.email,
640
- userName: null,
641
- workspaceId: newWorkspaceId,
642
- workspaceName: 'New Workspace',
643
- createdAt: new Date(),
644
- updatedAt: new Date(),
645
- },
646
- ]);
563
+ listMembershipsByUserMock.mockResolvedValueOnce({
564
+ memberships: [{workspaceId: newWorkspaceId, role: 'admin'}],
565
+ });
647
566
  const refreshed = await refreshAccessToken({refreshToken: loginResult.refreshToken});
648
567
 
649
568
  const refreshedClaims = await verifyUserToken({
650
569
  token: refreshed.token,
651
- secret: 'auth-core-test-secret',
570
+ secret: userAccessTokenKey(),
652
571
  });
653
572
  expect(refreshedClaims.memberships).toEqual([{workspaceId: newWorkspaceId, role: 'admin'}]);
654
573
  });
@@ -664,7 +583,7 @@ describe('auth core', () => {
664
583
 
665
584
  test('refresh fails closed when listMembershipsByUser throws', async () => {
666
585
  const user = await userFactory.create({emailVerifiedAt: new Date()});
667
- listMembershipsByUserMock.mockResolvedValueOnce([]);
586
+ listMembershipsByUserMock.mockResolvedValueOnce({memberships: []});
668
587
  const loginResult = await login({email: user.email, password: user.plainPassword});
669
588
 
670
589
  listMembershipsByUserMock.mockRejectedValueOnce(new Error('workspaces DB down'));