@shipfox/api-auth 5.0.0 → 7.0.1

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 +69 -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 +18 -14
  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
package/dist/core/auth.js CHANGED
@@ -1,17 +1,19 @@
1
- import { EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS, emailSchema } from '@shipfox/api-auth-dto';
2
- import { acceptWorkspaceInvitation, InvitationEmailMismatchError, listMembershipsByUser, peekInvitationByRawToken, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError as WorkspacesTokenInvalidError } from '@shipfox/api-workspaces';
1
+ import { emailSchema } from '@shipfox/api-auth-dto';
2
+ import { confirmEmailChallenge, consumeEmailChallengeProof, createEmailChallenge, resendEmailChallenge } from '@shipfox/api-email-challenges';
3
+ import { workspacesInterModuleContract } from '@shipfox/api-workspaces-dto/inter-module';
4
+ import { isInterModuleKnownError } from '@shipfox/inter-module';
5
+ import { userAccessTokenKey } from '@shipfox/node-auth-root-key';
3
6
  import { generateOpaqueToken, hashOpaqueToken } from '@shipfox/node-tokens';
4
7
  import { config } from '#config.js';
5
- import { consumeEmailVerification, createEmailVerification, createResendEmailVerification } from '#db/email-verifications.js';
6
8
  import { consumePasswordReset, createPasswordReset } from '#db/password-resets.js';
7
9
  import { createRefreshToken, findActiveRefreshTokenByHash, findRefreshTokenByHash, revokeRefreshTokenByHash, revokeRefreshTokensForUser, rotateRefreshToken } from '#db/refresh-tokens.js';
8
10
  import { createUser as createDbUser, findUserByEmail, findUserById, markEmailVerified, provisionUser as provisionDbUser, updateUserPassword } from '#db/users.js';
9
11
  import { recordTokenRefreshed } from '#metrics/index.js';
10
- import { AuthDependencyUnavailableError, EmailNotVerifiedError, EmailTakenError, InvalidCredentialsError, TokenInvalidError, UserNotFoundError } from './errors.js';
12
+ import { AuthDependencyUnavailableError, EmailNotVerifiedError, EmailTakenError, InvalidCredentialsError, InvitationEmailMismatchError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError, UserNotFoundError } from './errors.js';
11
13
  import { signUserToken } from './jwt.js';
12
14
  import { hashPassword, verifyPassword } from './password.js';
13
- const VERIFICATION_TTL_HOURS = 24;
14
15
  const RESET_TTL_HOURS = 1;
16
+ const PASSWORD_VERIFICATION_PURPOSE = 'password-verification';
15
17
  let dummyHashCache;
16
18
  async function getDummyHash() {
17
19
  if (!dummyHashCache) {
@@ -30,8 +32,8 @@ function daysFromNow(days) {
30
32
  function recordRefreshOutcome(outcome) {
31
33
  recordTokenRefreshed(outcome);
32
34
  }
33
- async function signAccessToken(user) {
34
- const memberships = await listMembershipsByUser({
35
+ async function signAccessToken(user, workspaces) {
36
+ const memberships = await workspaces.listMembershipsForTokenClaims({
35
37
  userId: user.id
36
38
  }).catch((error)=>{
37
39
  throw new AuthDependencyUnavailableError('workspaces', error);
@@ -40,11 +42,8 @@ async function signAccessToken(user) {
40
42
  userId: user.id,
41
43
  email: user.email,
42
44
  name: user.name,
43
- memberships: memberships.map((m)=>({
44
- workspaceId: m.workspaceId,
45
- role: 'admin'
46
- })),
47
- secret: config.AUTH_JWT_SECRET,
45
+ memberships: memberships.memberships,
46
+ secret: userAccessTokenKey(),
48
47
  expiresIn: config.AUTH_JWT_EXPIRES_IN
49
48
  });
50
49
  }
@@ -62,23 +61,22 @@ async function createRefreshSession(user) {
62
61
  });
63
62
  return refreshToken;
64
63
  }
65
- function verificationLink(rawToken) {
66
- return `${config.CLIENT_BASE_URL}/auth/verify-email?token=${rawToken}`;
67
- }
68
64
  function passwordResetLink(rawToken) {
69
65
  return `${config.CLIENT_BASE_URL}/auth/reset?token=${rawToken}`;
70
66
  }
71
- async function createAndQueueEmailVerification(user) {
72
- const rawToken = generateOpaqueToken('emailVerification');
73
- await createEmailVerification({
74
- userId: user.id,
75
- hashedToken: hashOpaqueToken(rawToken),
76
- expiresAt: hoursFromNow(VERIFICATION_TTL_HOURS),
77
- sendEmail: {
78
- email: user.email,
79
- verifyLink: verificationLink(rawToken)
80
- }
81
- });
67
+ function invitationErrorFromKnownError(error, method) {
68
+ const contractMethod = workspacesInterModuleContract.methods[method];
69
+ if (!isInterModuleKnownError(contractMethod, error)) return error;
70
+ if (error.code === 'invitation-token-invalid') return new TokenInvalidError('Invitation token is invalid');
71
+ if (error.code === 'invitation-token-used') return new TokenAlreadyUsedError();
72
+ if (error.code === 'invitation-token-expired') return new TokenExpiredError();
73
+ return new InvitationEmailMismatchError();
74
+ }
75
+ function invitationErrorMessage(code) {
76
+ if (code === 'invitation-token-invalid') return 'Invitation token is invalid';
77
+ if (code === 'invitation-token-used') return 'Invitation has already been accepted';
78
+ if (code === 'invitation-token-expired') return 'Invitation has expired';
79
+ return 'Signup email does not match the invitation';
82
80
  }
83
81
  /**
84
82
  * Creates a verified, password-less user for an external identity provider.
@@ -94,6 +92,22 @@ export async function signup(params) {
94
92
  email: params.email
95
93
  });
96
94
  if (existing) {
95
+ const canResumeVerification = existing.status === 'active' && existing.emailVerifiedAt === null && existing.hashedPassword !== null && await verifyPassword({
96
+ password: params.password,
97
+ hash: existing.hashedPassword
98
+ });
99
+ if (canResumeVerification) {
100
+ const emailChallenge = await createEmailChallenge({
101
+ email: existing.email,
102
+ purpose: PASSWORD_VERIFICATION_PURPOSE,
103
+ continuation: existing.id,
104
+ sourceIp: params.sourceIp ?? '0.0.0.0'
105
+ });
106
+ return {
107
+ ...existing,
108
+ emailChallenge
109
+ };
110
+ }
97
111
  throw new EmailTakenError(params.email);
98
112
  }
99
113
  const hashedPassword = await hashPassword({
@@ -102,29 +116,33 @@ export async function signup(params) {
102
116
  const user = await createDbUser({
103
117
  email: params.email,
104
118
  hashedPassword,
105
- name: params.name ?? null
119
+ name: params.name ?? null,
120
+ signedUp: {
121
+ viaInvitation: false
122
+ }
106
123
  });
107
- await createAndQueueEmailVerification(user);
108
- return user;
124
+ const emailChallenge = await createEmailChallenge({
125
+ email: user.email,
126
+ purpose: PASSWORD_VERIFICATION_PURPOSE,
127
+ continuation: user.id,
128
+ sourceIp: params.sourceIp ?? '0.0.0.0'
129
+ });
130
+ return {
131
+ ...user,
132
+ emailChallenge
133
+ };
109
134
  }
110
135
  export async function signupWithInvitation(params) {
111
136
  // Step 1: Pre-validate the invitation BEFORE any user write so an invalid
112
137
  // token never produces an orphan user. Re-validation happens again inside
113
138
  // acceptWorkspaceInvitation (race-safe) after the user is created.
114
- const invitation = await peekInvitationByRawToken({
115
- token: params.invitationToken
116
- });
117
- if (!invitation) {
118
- throw new WorkspacesTokenInvalidError('Invitation token is invalid');
119
- }
120
- if (invitation.acceptedAt !== null) {
121
- throw new TokenAlreadyUsedError();
122
- }
123
- if (invitation.expiresAt.getTime() <= Date.now()) {
124
- throw new TokenExpiredError();
125
- }
126
- if (invitation.email !== params.email) {
127
- throw new InvitationEmailMismatchError();
139
+ try {
140
+ await params.workspaces.preflightInvitationAcceptance({
141
+ token: params.invitationToken,
142
+ email: params.email
143
+ });
144
+ } catch (error) {
145
+ throw invitationErrorFromKnownError(error, 'preflightInvitationAcceptance');
128
146
  }
129
147
  // Step 2: Create the user as verified. The invitation email is the proof of
130
148
  // ownership, and this avoids a second auth-table write just to mark verified.
@@ -141,7 +159,10 @@ export async function signupWithInvitation(params) {
141
159
  email: params.email,
142
160
  hashedPassword,
143
161
  name: params.name ?? null,
144
- emailVerifiedAt: new Date()
162
+ emailVerifiedAt: new Date(),
163
+ signedUp: {
164
+ viaInvitation: true
165
+ }
145
166
  });
146
167
  // Step 3: Accept the invitation through the workspaces module boundary.
147
168
  // This is intentionally not one database transaction with auth; treat it like
@@ -150,7 +171,7 @@ export async function signupWithInvitation(params) {
150
171
  let membership = null;
151
172
  let acceptError;
152
173
  try {
153
- const result = await acceptWorkspaceInvitation({
174
+ const result = await params.workspaces.acceptInvitation({
154
175
  token: params.invitationToken,
155
176
  userId: user.id,
156
177
  email: user.email,
@@ -162,10 +183,10 @@ export async function signupWithInvitation(params) {
162
183
  workspaceId: result.membership.workspaceId
163
184
  };
164
185
  } catch (error) {
165
- if (error instanceof WorkspacesTokenInvalidError || error instanceof TokenAlreadyUsedError || error instanceof TokenExpiredError || error instanceof InvitationEmailMismatchError) {
186
+ if (isInterModuleKnownError(workspacesInterModuleContract.methods.acceptInvitation, error)) {
166
187
  acceptError = {
167
- code: error.name,
168
- message: error.message
188
+ code: error.code,
189
+ message: invitationErrorMessage(error.code)
169
190
  };
170
191
  } else {
171
192
  acceptError = {
@@ -176,7 +197,7 @@ export async function signupWithInvitation(params) {
176
197
  }
177
198
  // Step 4: Issue session. signAccessToken reads memberships through the
178
199
  // workspaces module API, so a successful accept is reflected in the JWT.
179
- const token = await signAccessToken(user);
200
+ const token = await signAccessToken(user, params.workspaces);
180
201
  const refreshToken = await createRefreshSession(user);
181
202
  if (acceptError) {
182
203
  return {
@@ -234,7 +255,7 @@ export async function login(params) {
234
255
  if (user.emailVerifiedAt === null) {
235
256
  throw new EmailNotVerifiedError();
236
257
  }
237
- const token = await signAccessToken(user);
258
+ const token = await signAccessToken(user, params.workspaces);
238
259
  const refreshToken = await createRefreshSession(user);
239
260
  return {
240
261
  token,
@@ -246,7 +267,7 @@ export async function createSessionForUser(params) {
246
267
  const user = params.userId ? await findUserById({
247
268
  id: params.userId
248
269
  }) : params.email ? await findUserByEmail({
249
- email: params.email
270
+ email: emailSchema.parse(params.email)
250
271
  }) : undefined;
251
272
  if (!user) {
252
273
  throw new UserNotFoundError(params.userId ?? params.email ?? 'unknown');
@@ -257,7 +278,7 @@ export async function createSessionForUser(params) {
257
278
  if (user.status !== 'active') {
258
279
  throw new InvalidCredentialsError();
259
280
  }
260
- const token = await signAccessToken(user);
281
+ const token = await signAccessToken(user, params.workspaces);
261
282
  const refreshToken = await createRefreshSession(user);
262
283
  return {
263
284
  token,
@@ -288,7 +309,7 @@ export async function refreshAccessToken(params) {
288
309
  // second tab); past it, reuse of a retired token means a compromised session.
289
310
  if (current.rotatedAt) {
290
311
  if (isWithinRotationGrace(current)) {
291
- const token = await signAccessToken(user);
312
+ const token = await signAccessToken(user, params.workspaces);
292
313
  recordRefreshOutcome('grace');
293
314
  return {
294
315
  token,
@@ -319,7 +340,7 @@ export async function refreshAccessToken(params) {
319
340
  recordRefreshOutcome('rejected');
320
341
  throw new TokenInvalidError('Refresh token is invalid or expired');
321
342
  }
322
- const token = await signAccessToken(user);
343
+ const token = await signAccessToken(user, params.workspaces);
323
344
  recordRefreshOutcome('grace');
324
345
  return {
325
346
  token,
@@ -327,7 +348,7 @@ export async function refreshAccessToken(params) {
327
348
  user
328
349
  };
329
350
  }
330
- const token = await signAccessToken(user);
351
+ const token = await signAccessToken(user, params.workspaces);
331
352
  recordRefreshOutcome('rotated');
332
353
  return {
333
354
  token,
@@ -336,36 +357,47 @@ export async function refreshAccessToken(params) {
336
357
  };
337
358
  }
338
359
  export async function confirmEmailVerification(params) {
339
- const consumed = await consumeEmailVerification({
340
- hashedToken: hashOpaqueToken(params.token)
360
+ const user = await findUserByEmail({
361
+ email: emailSchema.parse(params.email)
341
362
  });
342
- if (!consumed) {
343
- throw new TokenInvalidError('Verification token is invalid or expired');
344
- }
345
- const user = await markEmailVerified({
346
- userId: consumed.userId
363
+ if (!user) throw new TokenInvalidError('Verification code is invalid or expired');
364
+ await confirmEmailChallenge({
365
+ id: params.challengeId,
366
+ code: params.code,
367
+ continuation: user.id
347
368
  });
348
- if (user?.status !== 'active') {
349
- throw new TokenInvalidError('Verification token is invalid or expired');
369
+ await consumeEmailChallengeProof({
370
+ id: params.challengeId,
371
+ purpose: PASSWORD_VERIFICATION_PURPOSE,
372
+ continuation: user.id
373
+ });
374
+ const verifiedUser = await markEmailVerified({
375
+ userId: user.id
376
+ });
377
+ if (verifiedUser?.status !== 'active') {
378
+ throw new TokenInvalidError('Verification code is invalid or expired');
350
379
  }
351
- const token = await signAccessToken(user);
352
- const refreshToken = await createRefreshSession(user);
380
+ const token = await signAccessToken(verifiedUser, params.workspaces);
381
+ const refreshToken = await createRefreshSession(verifiedUser);
353
382
  return {
354
383
  token,
355
384
  refreshToken,
356
- user
385
+ user: verifiedUser
357
386
  };
358
387
  }
359
388
  export async function resendEmailVerification(params) {
360
- const rawToken = generateOpaqueToken('emailVerification');
361
- const result = await createResendEmailVerification({
362
- email: params.email,
363
- hashedToken: hashOpaqueToken(rawToken),
364
- expiresAt: hoursFromNow(VERIFICATION_TTL_HOURS),
365
- cooldownSeconds: EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS,
366
- sendEmail: {
367
- verifyLink: verificationLink(rawToken)
368
- }
389
+ const user = await findUserByEmail({
390
+ email: emailSchema.parse(params.email)
391
+ });
392
+ if (!user || user.emailVerifiedAt || user.status !== 'active') {
393
+ return {
394
+ nextResendAvailableAt: new Date()
395
+ };
396
+ }
397
+ const result = await resendEmailChallenge({
398
+ id: params.challengeId,
399
+ continuation: user.id,
400
+ sourceIp: params.sourceIp
369
401
  });
370
402
  return {
371
403
  nextResendAvailableAt: result.nextResendAvailableAt
@@ -416,7 +448,7 @@ export async function confirmPasswordReset(params) {
416
448
  await revokeRefreshTokensForUser({
417
449
  userId: consumed.userId
418
450
  });
419
- const token = await signAccessToken(user);
451
+ const token = await signAccessToken(user, params.workspaces);
420
452
  const refreshToken = await createRefreshSession(user);
421
453
  return {
422
454
  token,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/auth.ts"],"sourcesContent":["import {EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS, emailSchema} from '@shipfox/api-auth-dto';\nimport {\n acceptWorkspaceInvitation,\n InvitationEmailMismatchError,\n listMembershipsByUser,\n peekInvitationByRawToken,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError as WorkspacesTokenInvalidError,\n} from '@shipfox/api-workspaces';\nimport {generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {\n consumeEmailVerification,\n createEmailVerification,\n createResendEmailVerification,\n} from '#db/email-verifications.js';\nimport {consumePasswordReset, createPasswordReset} from '#db/password-resets.js';\nimport {\n createRefreshToken,\n findActiveRefreshTokenByHash,\n findRefreshTokenByHash,\n revokeRefreshTokenByHash,\n revokeRefreshTokensForUser,\n rotateRefreshToken,\n} from '#db/refresh-tokens.js';\nimport {\n createUser as createDbUser,\n findUserByEmail,\n findUserById,\n markEmailVerified,\n provisionUser as provisionDbUser,\n updateUserPassword,\n} from '#db/users.js';\nimport {type AuthTokenRefreshOutcome, recordTokenRefreshed} from '#metrics/index.js';\nimport type {RefreshToken} from './entities/refresh-token.js';\nimport type {User} from './entities/user.js';\nimport {\n AuthDependencyUnavailableError,\n EmailNotVerifiedError,\n EmailTakenError,\n InvalidCredentialsError,\n TokenInvalidError,\n UserNotFoundError,\n} from './errors.js';\nimport {signUserToken} from './jwt.js';\nimport {hashPassword, verifyPassword} from './password.js';\n\nconst VERIFICATION_TTL_HOURS = 24;\nconst RESET_TTL_HOURS = 1;\n\nlet dummyHashCache: string | undefined;\nasync function getDummyHash(): Promise<string> {\n if (!dummyHashCache) {\n dummyHashCache = await hashPassword({password: 'dummy-for-timing-parity'});\n }\n return dummyHashCache;\n}\n\nfunction hoursFromNow(hours: number): Date {\n return new Date(Date.now() + hours * 60 * 60 * 1000);\n}\n\nfunction daysFromNow(days: number): Date {\n return new Date(Date.now() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction recordRefreshOutcome(outcome: AuthTokenRefreshOutcome): void {\n recordTokenRefreshed(outcome);\n}\n\nasync function signAccessToken(user: User): Promise<string> {\n const memberships = await listMembershipsByUser({userId: user.id}).catch((error: unknown) => {\n throw new AuthDependencyUnavailableError('workspaces', error);\n });\n return await signUserToken({\n userId: user.id,\n email: user.email,\n name: user.name,\n memberships: memberships.map((m) => ({workspaceId: m.workspaceId, role: 'admin' as const})),\n secret: config.AUTH_JWT_SECRET,\n expiresIn: config.AUTH_JWT_EXPIRES_IN,\n });\n}\n\nfunction isWithinRotationGrace(refreshToken: RefreshToken): boolean {\n if (!refreshToken.rotatedAt) return false;\n const graceMs = config.AUTH_REFRESH_ROTATION_GRACE_SECONDS * 1000;\n return Date.now() - refreshToken.rotatedAt.getTime() <= graceMs;\n}\n\nasync function createRefreshSession(user: User): Promise<string> {\n const refreshToken = generateOpaqueToken('refreshToken');\n await createRefreshToken({\n userId: user.id,\n hashedToken: hashOpaqueToken(refreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n return refreshToken;\n}\n\nfunction verificationLink(rawToken: string): string {\n return `${config.CLIENT_BASE_URL}/auth/verify-email?token=${rawToken}`;\n}\n\nfunction passwordResetLink(rawToken: string): string {\n return `${config.CLIENT_BASE_URL}/auth/reset?token=${rawToken}`;\n}\n\nasync function createAndQueueEmailVerification(user: User): Promise<void> {\n const rawToken = generateOpaqueToken('emailVerification');\n await createEmailVerification({\n userId: user.id,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: hoursFromNow(VERIFICATION_TTL_HOURS),\n sendEmail: {\n email: user.email,\n verifyLink: verificationLink(rawToken),\n },\n });\n}\n\nexport interface SignupParams {\n email: string;\n password: string;\n name?: string | undefined;\n}\n\nexport interface ProvisionUserParams {\n email: string;\n name?: string | null | undefined;\n}\n\n/**\n * Creates a verified, password-less user for an external identity provider.\n * Existing users are returned unchanged, including their password and profile.\n */\nexport async function provisionUser(params: ProvisionUserParams): Promise<User> {\n return await provisionDbUser({\n email: emailSchema.parse(params.email),\n name: params.name ?? null,\n });\n}\n\nexport async function signup(params: SignupParams): Promise<User> {\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n });\n\n await createAndQueueEmailVerification(user);\n\n return user;\n}\n\nexport interface SignupWithInvitationParams extends SignupParams {\n invitationToken: string;\n}\n\nexport interface SignupWithInvitationResult extends LoginResult {\n membership: {id: string; userId: string; workspaceId: string} | null;\n acceptError?: {code: string; message: string};\n}\n\nexport async function signupWithInvitation(\n params: SignupWithInvitationParams,\n): Promise<SignupWithInvitationResult> {\n // Step 1: Pre-validate the invitation BEFORE any user write so an invalid\n // token never produces an orphan user. Re-validation happens again inside\n // acceptWorkspaceInvitation (race-safe) after the user is created.\n const invitation = await peekInvitationByRawToken({token: params.invitationToken});\n if (!invitation) {\n throw new WorkspacesTokenInvalidError('Invitation token is invalid');\n }\n if (invitation.acceptedAt !== null) {\n throw new TokenAlreadyUsedError();\n }\n if (invitation.expiresAt.getTime() <= Date.now()) {\n throw new TokenExpiredError();\n }\n if (invitation.email !== params.email) {\n throw new InvitationEmailMismatchError();\n }\n\n // Step 2: Create the user as verified. The invitation email is the proof of\n // ownership, and this avoids a second auth-table write just to mark verified.\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: new Date(),\n });\n\n // Step 3: Accept the invitation through the workspaces module boundary.\n // This is intentionally not one database transaction with auth; treat it like\n // calling another service. If it fails after user creation, return a verified\n // signed-in user and an explicit retryable accept error.\n let membership: SignupWithInvitationResult['membership'] = null;\n let acceptError: SignupWithInvitationResult['acceptError'];\n try {\n const result = await acceptWorkspaceInvitation({\n token: params.invitationToken,\n userId: user.id,\n email: user.email,\n name: user.name,\n });\n membership = {\n id: result.membership.id,\n userId: result.membership.userId,\n workspaceId: result.membership.workspaceId,\n };\n } catch (error) {\n if (\n error instanceof WorkspacesTokenInvalidError ||\n error instanceof TokenAlreadyUsedError ||\n error instanceof TokenExpiredError ||\n error instanceof InvitationEmailMismatchError\n ) {\n acceptError = {code: error.name, message: error.message};\n } else {\n acceptError = {\n code: 'AcceptFailed',\n message: 'Could not accept the invitation; please retry from the invite link.',\n };\n }\n }\n\n // Step 4: Issue session. signAccessToken reads memberships through the\n // workspaces module API, so a successful accept is reflected in the JWT.\n const token = await signAccessToken(user);\n const refreshToken = await createRefreshSession(user);\n\n if (acceptError) {\n return {token, refreshToken, user, membership, acceptError};\n }\n return {token, refreshToken, user, membership};\n}\n\nexport interface CreateUserParams extends SignupParams {\n verified: boolean;\n}\n\nexport async function createUser(params: CreateUserParams): Promise<User> {\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: params.verified ? new Date() : null,\n });\n\n return user;\n}\n\nexport interface LoginParams {\n email: string;\n password: string;\n}\n\nexport interface LoginResult {\n token: string;\n refreshToken: string;\n user: User;\n}\n\nexport async function login(params: LoginParams): Promise<LoginResult> {\n const user = await findUserByEmail({email: params.email});\n\n if (!user) {\n await verifyPassword({password: params.password, hash: await getDummyHash()});\n throw new InvalidCredentialsError();\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.password,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok || user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n\n const token = await signAccessToken(user);\n const refreshToken = await createRefreshSession(user);\n\n return {token, refreshToken, user};\n}\n\nexport interface CreateSessionForUserParams {\n userId?: string | undefined;\n email?: string | undefined;\n}\n\nexport interface CreateSessionForUserResult {\n token: string;\n refreshToken: string;\n user: User;\n}\n\nexport type CreateSessionForUserError =\n | AuthDependencyUnavailableError\n | EmailNotVerifiedError\n | InvalidCredentialsError\n | UserNotFoundError;\n\nexport async function createSessionForUser(\n params: CreateSessionForUserParams,\n): Promise<CreateSessionForUserResult> {\n const user = params.userId\n ? await findUserById({id: params.userId})\n : params.email\n ? await findUserByEmail({email: params.email})\n : undefined;\n\n if (!user) {\n throw new UserNotFoundError(params.userId ?? params.email ?? 'unknown');\n }\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n if (user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n const token = await signAccessToken(user);\n const refreshToken = await createRefreshSession(user);\n\n return {token, refreshToken, user};\n}\n\nexport interface RefreshAccessTokenResult {\n token: string;\n /** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */\n refreshToken: string | undefined;\n user: User;\n}\n\nexport async function refreshAccessToken(params: {\n refreshToken: string;\n}): Promise<RefreshAccessTokenResult> {\n const currentHashedToken = hashOpaqueToken(params.refreshToken);\n const current = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!current) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n\n const user = await findUserById({id: current.userId});\n if (user?.status !== 'active') {\n await revokeRefreshTokenByHash({hashedToken: currentHashedToken});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n\n // Within the grace window a rotated token means a concurrent refresh (e.g. a\n // second tab); past it, reuse of a retired token means a compromised session.\n if (current.rotatedAt) {\n if (isWithinRotationGrace(current)) {\n const token = await signAccessToken(user);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user};\n }\n await revokeRefreshTokensForUser({userId: user.id});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token reused after rotation');\n }\n\n // Losing the CAS requires a state check: another request may have rotated,\n // revoked, or expired the token before this refresh could claim it.\n const nextRefreshToken = generateOpaqueToken('refreshToken');\n const rotated = await rotateRefreshToken({\n id: current.id,\n currentHashedToken,\n nextHashedToken: hashOpaqueToken(nextRefreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!rotated) {\n const latest = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!latest || !isWithinRotationGrace(latest)) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const token = await signAccessToken(user);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user};\n }\n\n const token = await signAccessToken(user);\n recordRefreshOutcome('rotated');\n return {token, refreshToken: nextRefreshToken, user};\n}\n\nexport interface ConfirmEmailVerificationResult {\n token: string;\n refreshToken: string;\n user: User;\n}\n\nexport interface ResendEmailVerificationResult {\n nextResendAvailableAt: Date;\n}\n\nexport async function confirmEmailVerification(params: {\n token: string;\n}): Promise<ConfirmEmailVerificationResult> {\n const consumed = await consumeEmailVerification({hashedToken: hashOpaqueToken(params.token)});\n if (!consumed) {\n throw new TokenInvalidError('Verification token is invalid or expired');\n }\n\n const user = await markEmailVerified({userId: consumed.userId});\n if (user?.status !== 'active') {\n throw new TokenInvalidError('Verification token is invalid or expired');\n }\n\n const token = await signAccessToken(user);\n const refreshToken = await createRefreshSession(user);\n\n return {token, refreshToken, user};\n}\n\nexport async function resendEmailVerification(params: {\n email: string;\n}): Promise<ResendEmailVerificationResult> {\n const rawToken = generateOpaqueToken('emailVerification');\n const result = await createResendEmailVerification({\n email: params.email,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: hoursFromNow(VERIFICATION_TTL_HOURS),\n cooldownSeconds: EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS,\n sendEmail: {\n verifyLink: verificationLink(rawToken),\n },\n });\n\n return {nextResendAvailableAt: result.nextResendAvailableAt};\n}\n\nexport async function requestPasswordReset(params: {email: string}): Promise<void> {\n const user = await findUserByEmail({email: params.email});\n if (user?.status !== 'active' || user.hashedPassword === null) {\n return;\n }\n\n const rawToken = generateOpaqueToken('passwordReset');\n await createPasswordReset({\n userId: user.id,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: hoursFromNow(RESET_TTL_HOURS),\n sendEmail: {\n email: user.email,\n resetLink: passwordResetLink(rawToken),\n expiresInHours: RESET_TTL_HOURS,\n },\n });\n}\n\nexport interface ConfirmPasswordResetResult {\n token: string;\n refreshToken: string;\n user: User;\n}\n\nexport async function confirmPasswordReset(params: {\n token: string;\n newPassword: string;\n}): Promise<ConfirmPasswordResetResult> {\n const consumed = await consumePasswordReset({hashedToken: hashOpaqueToken(params.token)});\n if (!consumed) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const existingUser = await findUserById({id: consumed.userId});\n if (existingUser?.status !== 'active' || existingUser.hashedPassword === null) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const user = await updateUserPassword({userId: consumed.userId, hashedPassword});\n if (user?.status !== 'active') {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n await revokeRefreshTokensForUser({userId: consumed.userId});\n\n const token = await signAccessToken(user);\n const refreshToken = await createRefreshSession(user);\n\n return {token, refreshToken, user};\n}\n\nexport async function changePassword(params: {\n userId: string;\n currentPassword: string;\n newPassword: string;\n refreshToken?: string | undefined;\n}): Promise<void> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.currentPassword,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok) {\n throw new InvalidCredentialsError();\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const currentRefreshSession = params.refreshToken\n ? await findActiveRefreshTokenByHash({hashedToken: hashOpaqueToken(params.refreshToken)})\n : undefined;\n const exceptRefreshTokenId =\n currentRefreshSession?.userId === user.id ? currentRefreshSession.id : undefined;\n\n await updateUserPassword({userId: user.id, hashedPassword});\n await revokeRefreshTokensForUser({userId: user.id, exceptRefreshTokenId});\n}\n\nexport async function logout(params: {refreshToken?: string | undefined}): Promise<void> {\n if (!params.refreshToken) return;\n await revokeRefreshTokenByHash({hashedToken: hashOpaqueToken(params.refreshToken)});\n}\n\nexport interface GetCurrentUserResult {\n user: User;\n}\n\nexport async function getCurrentUser(params: {userId: string}): Promise<GetCurrentUserResult> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n return {user};\n}\n"],"names":["EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS","emailSchema","acceptWorkspaceInvitation","InvitationEmailMismatchError","listMembershipsByUser","peekInvitationByRawToken","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","WorkspacesTokenInvalidError","generateOpaqueToken","hashOpaqueToken","config","consumeEmailVerification","createEmailVerification","createResendEmailVerification","consumePasswordReset","createPasswordReset","createRefreshToken","findActiveRefreshTokenByHash","findRefreshTokenByHash","revokeRefreshTokenByHash","revokeRefreshTokensForUser","rotateRefreshToken","createUser","createDbUser","findUserByEmail","findUserById","markEmailVerified","provisionUser","provisionDbUser","updateUserPassword","recordTokenRefreshed","AuthDependencyUnavailableError","EmailNotVerifiedError","EmailTakenError","InvalidCredentialsError","UserNotFoundError","signUserToken","hashPassword","verifyPassword","VERIFICATION_TTL_HOURS","RESET_TTL_HOURS","dummyHashCache","getDummyHash","password","hoursFromNow","hours","Date","now","daysFromNow","days","recordRefreshOutcome","outcome","signAccessToken","user","memberships","userId","id","catch","error","email","name","map","m","workspaceId","role","secret","AUTH_JWT_SECRET","expiresIn","AUTH_JWT_EXPIRES_IN","isWithinRotationGrace","refreshToken","rotatedAt","graceMs","AUTH_REFRESH_ROTATION_GRACE_SECONDS","getTime","createRefreshSession","hashedToken","expiresAt","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","verificationLink","rawToken","CLIENT_BASE_URL","passwordResetLink","createAndQueueEmailVerification","sendEmail","verifyLink","params","parse","signup","existing","hashedPassword","signupWithInvitation","invitation","token","invitationToken","acceptedAt","emailVerifiedAt","membership","acceptError","result","code","message","verified","login","hash","hasPassword","ok","status","createSessionForUser","undefined","refreshAccessToken","currentHashedToken","current","nextRefreshToken","rotated","nextHashedToken","latest","confirmEmailVerification","consumed","resendEmailVerification","cooldownSeconds","nextResendAvailableAt","requestPasswordReset","resetLink","expiresInHours","confirmPasswordReset","existingUser","newPassword","changePassword","currentPassword","currentRefreshSession","exceptRefreshTokenId","logout","getCurrentUser"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/auth.ts"],"sourcesContent":["import {emailSchema} from '@shipfox/api-auth-dto';\nimport {\n confirmEmailChallenge,\n consumeEmailChallengeProof,\n createEmailChallenge,\n resendEmailChallenge,\n} from '@shipfox/api-email-challenges';\nimport {\n type WorkspacesInterModuleClient,\n workspacesInterModuleContract,\n} from '@shipfox/api-workspaces-dto/inter-module';\nimport {isInterModuleKnownError} from '@shipfox/inter-module';\nimport {userAccessTokenKey} from '@shipfox/node-auth-root-key';\nimport {generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {consumePasswordReset, createPasswordReset} from '#db/password-resets.js';\nimport {\n createRefreshToken,\n findActiveRefreshTokenByHash,\n findRefreshTokenByHash,\n revokeRefreshTokenByHash,\n revokeRefreshTokensForUser,\n rotateRefreshToken,\n} from '#db/refresh-tokens.js';\nimport {\n createUser as createDbUser,\n findUserByEmail,\n findUserById,\n markEmailVerified,\n provisionUser as provisionDbUser,\n updateUserPassword,\n} from '#db/users.js';\nimport {type AuthTokenRefreshOutcome, recordTokenRefreshed} from '#metrics/index.js';\nimport type {RefreshToken} from './entities/refresh-token.js';\nimport type {User} from './entities/user.js';\nimport {\n AuthDependencyUnavailableError,\n EmailNotVerifiedError,\n EmailTakenError,\n InvalidCredentialsError,\n InvitationEmailMismatchError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n UserNotFoundError,\n} from './errors.js';\nimport {signUserToken} from './jwt.js';\nimport {hashPassword, verifyPassword} from './password.js';\n\nconst RESET_TTL_HOURS = 1;\nconst PASSWORD_VERIFICATION_PURPOSE = 'password-verification';\n\nlet dummyHashCache: string | undefined;\nasync function getDummyHash(): Promise<string> {\n if (!dummyHashCache) {\n dummyHashCache = await hashPassword({password: 'dummy-for-timing-parity'});\n }\n return dummyHashCache;\n}\n\nfunction hoursFromNow(hours: number): Date {\n return new Date(Date.now() + hours * 60 * 60 * 1000);\n}\n\nfunction daysFromNow(days: number): Date {\n return new Date(Date.now() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction recordRefreshOutcome(outcome: AuthTokenRefreshOutcome): void {\n recordTokenRefreshed(outcome);\n}\n\nasync function signAccessToken(\n user: User,\n workspaces: WorkspacesInterModuleClient,\n): Promise<string> {\n const memberships = await workspaces\n .listMembershipsForTokenClaims({userId: user.id})\n .catch((error: unknown) => {\n throw new AuthDependencyUnavailableError('workspaces', error);\n });\n return await signUserToken({\n userId: user.id,\n email: user.email,\n name: user.name,\n memberships: memberships.memberships,\n secret: userAccessTokenKey(),\n expiresIn: config.AUTH_JWT_EXPIRES_IN,\n });\n}\n\nfunction isWithinRotationGrace(refreshToken: RefreshToken): boolean {\n if (!refreshToken.rotatedAt) return false;\n const graceMs = config.AUTH_REFRESH_ROTATION_GRACE_SECONDS * 1000;\n return Date.now() - refreshToken.rotatedAt.getTime() <= graceMs;\n}\n\nasync function createRefreshSession(user: User): Promise<string> {\n const refreshToken = generateOpaqueToken('refreshToken');\n await createRefreshToken({\n userId: user.id,\n hashedToken: hashOpaqueToken(refreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n return refreshToken;\n}\n\nfunction passwordResetLink(rawToken: string): string {\n return `${config.CLIENT_BASE_URL}/auth/reset?token=${rawToken}`;\n}\n\nfunction invitationErrorFromKnownError(\n error: unknown,\n method: 'preflightInvitationAcceptance' | 'acceptInvitation',\n): Error {\n const contractMethod = workspacesInterModuleContract.methods[method];\n if (!isInterModuleKnownError(contractMethod, error)) return error as Error;\n\n if (error.code === 'invitation-token-invalid')\n return new TokenInvalidError('Invitation token is invalid');\n if (error.code === 'invitation-token-used') return new TokenAlreadyUsedError();\n if (error.code === 'invitation-token-expired') return new TokenExpiredError();\n return new InvitationEmailMismatchError();\n}\n\nfunction invitationErrorMessage(\n code:\n | 'invitation-token-invalid'\n | 'invitation-token-used'\n | 'invitation-token-expired'\n | 'invitation-email-mismatch',\n): string {\n if (code === 'invitation-token-invalid') return 'Invitation token is invalid';\n if (code === 'invitation-token-used') return 'Invitation has already been accepted';\n if (code === 'invitation-token-expired') return 'Invitation has expired';\n return 'Signup email does not match the invitation';\n}\n\nexport interface SignupParams {\n email: string;\n password: string;\n name?: string | undefined;\n}\n\nexport interface ProvisionUserParams {\n email: string;\n name?: string | null | undefined;\n}\n\n/**\n * Creates a verified, password-less user for an external identity provider.\n * Existing users are returned unchanged, including their password and profile.\n */\nexport async function provisionUser(params: ProvisionUserParams): Promise<User> {\n return await provisionDbUser({\n email: emailSchema.parse(params.email),\n name: params.name ?? null,\n });\n}\n\nexport type SignupResult = User & {\n emailChallenge: {id: string; nextResendAvailableAt: Date};\n};\n\nexport async function signup(params: SignupParams & {sourceIp?: string}): Promise<SignupResult> {\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n const canResumeVerification =\n existing.status === 'active' &&\n existing.emailVerifiedAt === null &&\n existing.hashedPassword !== null &&\n (await verifyPassword({password: params.password, hash: existing.hashedPassword}));\n if (canResumeVerification) {\n const emailChallenge = await createEmailChallenge({\n email: existing.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: existing.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n return {...existing, emailChallenge};\n }\n throw new EmailTakenError(params.email);\n }\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n signedUp: {viaInvitation: false},\n });\n\n const emailChallenge = await createEmailChallenge({\n email: user.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n\n return {...user, emailChallenge};\n}\n\nexport interface SignupWithInvitationParams extends SignupParams {\n invitationToken: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface SignupWithInvitationResult extends LoginResult {\n membership: {id: string; userId: string; workspaceId: string} | null;\n acceptError?: {code: string; message: string};\n}\n\nexport async function signupWithInvitation(\n params: SignupWithInvitationParams,\n): Promise<SignupWithInvitationResult> {\n // Step 1: Pre-validate the invitation BEFORE any user write so an invalid\n // token never produces an orphan user. Re-validation happens again inside\n // acceptWorkspaceInvitation (race-safe) after the user is created.\n try {\n await params.workspaces.preflightInvitationAcceptance({\n token: params.invitationToken,\n email: params.email,\n });\n } catch (error) {\n throw invitationErrorFromKnownError(error, 'preflightInvitationAcceptance');\n }\n\n // Step 2: Create the user as verified. The invitation email is the proof of\n // ownership, and this avoids a second auth-table write just to mark verified.\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: new Date(),\n signedUp: {viaInvitation: true},\n });\n\n // Step 3: Accept the invitation through the workspaces module boundary.\n // This is intentionally not one database transaction with auth; treat it like\n // calling another service. If it fails after user creation, return a verified\n // signed-in user and an explicit retryable accept error.\n let membership: SignupWithInvitationResult['membership'] = null;\n let acceptError: SignupWithInvitationResult['acceptError'];\n try {\n const result = await params.workspaces.acceptInvitation({\n token: params.invitationToken,\n userId: user.id,\n email: user.email,\n name: user.name,\n });\n membership = {\n id: result.membership.id,\n userId: result.membership.userId,\n workspaceId: result.membership.workspaceId,\n };\n } catch (error) {\n if (isInterModuleKnownError(workspacesInterModuleContract.methods.acceptInvitation, error)) {\n acceptError = {code: error.code, message: invitationErrorMessage(error.code)};\n } else {\n acceptError = {\n code: 'AcceptFailed',\n message: 'Could not accept the invitation; please retry from the invite link.',\n };\n }\n }\n\n // Step 4: Issue session. signAccessToken reads memberships through the\n // workspaces module API, so a successful accept is reflected in the JWT.\n const token = await signAccessToken(user, params.workspaces);\n const refreshToken = await createRefreshSession(user);\n\n if (acceptError) {\n return {token, refreshToken, user, membership, acceptError};\n }\n return {token, refreshToken, user, membership};\n}\n\nexport interface CreateUserParams extends SignupParams {\n verified: boolean;\n}\n\nexport async function createUser(params: CreateUserParams): Promise<User> {\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: params.verified ? new Date() : null,\n });\n\n return user;\n}\n\nexport interface LoginParams {\n email: string;\n password: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface LoginResult {\n token: string;\n refreshToken: string;\n user: User;\n}\n\nexport async function login(params: LoginParams): Promise<LoginResult> {\n const user = await findUserByEmail({email: params.email});\n\n if (!user) {\n await verifyPassword({password: params.password, hash: await getDummyHash()});\n throw new InvalidCredentialsError();\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.password,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok || user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n\n const token = await signAccessToken(user, params.workspaces);\n const refreshToken = await createRefreshSession(user);\n\n return {token, refreshToken, user};\n}\n\nexport interface CreateSessionForUserParams {\n userId?: string | undefined;\n email?: string | undefined;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface CreateSessionForUserResult {\n token: string;\n refreshToken: string;\n user: User;\n}\n\nexport type CreateSessionForUserError =\n | AuthDependencyUnavailableError\n | EmailNotVerifiedError\n | InvalidCredentialsError\n | UserNotFoundError;\n\nexport async function createSessionForUser(\n params: CreateSessionForUserParams,\n): Promise<CreateSessionForUserResult> {\n const user = params.userId\n ? await findUserById({id: params.userId})\n : params.email\n ? await findUserByEmail({email: emailSchema.parse(params.email)})\n : undefined;\n\n if (!user) {\n throw new UserNotFoundError(params.userId ?? params.email ?? 'unknown');\n }\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n if (user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n const token = await signAccessToken(user, params.workspaces);\n const refreshToken = await createRefreshSession(user);\n\n return {token, refreshToken, user};\n}\n\nexport interface RefreshAccessTokenResult {\n token: string;\n /** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */\n refreshToken: string | undefined;\n user: User;\n}\n\nexport async function refreshAccessToken(params: {\n refreshToken: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<RefreshAccessTokenResult> {\n const currentHashedToken = hashOpaqueToken(params.refreshToken);\n const current = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!current) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n\n const user = await findUserById({id: current.userId});\n if (user?.status !== 'active') {\n await revokeRefreshTokenByHash({hashedToken: currentHashedToken});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n\n // Within the grace window a rotated token means a concurrent refresh (e.g. a\n // second tab); past it, reuse of a retired token means a compromised session.\n if (current.rotatedAt) {\n if (isWithinRotationGrace(current)) {\n const token = await signAccessToken(user, params.workspaces);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user};\n }\n await revokeRefreshTokensForUser({userId: user.id});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token reused after rotation');\n }\n\n // Losing the CAS requires a state check: another request may have rotated,\n // revoked, or expired the token before this refresh could claim it.\n const nextRefreshToken = generateOpaqueToken('refreshToken');\n const rotated = await rotateRefreshToken({\n id: current.id,\n currentHashedToken,\n nextHashedToken: hashOpaqueToken(nextRefreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!rotated) {\n const latest = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!latest || !isWithinRotationGrace(latest)) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const token = await signAccessToken(user, params.workspaces);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user};\n }\n\n const token = await signAccessToken(user, params.workspaces);\n recordRefreshOutcome('rotated');\n return {token, refreshToken: nextRefreshToken, user};\n}\n\nexport interface ConfirmEmailVerificationResult {\n token: string;\n refreshToken: string;\n user: User;\n}\n\nexport interface ResendEmailVerificationResult {\n nextResendAvailableAt: Date;\n}\n\nexport async function confirmEmailVerification(params: {\n email: string;\n challengeId: string;\n code: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user) throw new TokenInvalidError('Verification code is invalid or expired');\n await confirmEmailChallenge({id: params.challengeId, code: params.code, continuation: user.id});\n await consumeEmailChallengeProof({\n id: params.challengeId,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n });\n\n const verifiedUser = await markEmailVerified({userId: user.id});\n if (verifiedUser?.status !== 'active') {\n throw new TokenInvalidError('Verification code is invalid or expired');\n }\n\n const token = await signAccessToken(verifiedUser, params.workspaces);\n const refreshToken = await createRefreshSession(verifiedUser);\n\n return {token, refreshToken, user: verifiedUser};\n}\n\nexport async function resendEmailVerification(params: {\n email: string;\n challengeId: string;\n sourceIp: string;\n}): Promise<ResendEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user || user.emailVerifiedAt || user.status !== 'active') {\n return {nextResendAvailableAt: new Date()};\n }\n const result = await resendEmailChallenge({\n id: params.challengeId,\n continuation: user.id,\n sourceIp: params.sourceIp,\n });\n\n return {nextResendAvailableAt: result.nextResendAvailableAt};\n}\n\nexport async function requestPasswordReset(params: {email: string}): Promise<void> {\n const user = await findUserByEmail({email: params.email});\n if (user?.status !== 'active' || user.hashedPassword === null) {\n return;\n }\n\n const rawToken = generateOpaqueToken('passwordReset');\n await createPasswordReset({\n userId: user.id,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: hoursFromNow(RESET_TTL_HOURS),\n sendEmail: {\n email: user.email,\n resetLink: passwordResetLink(rawToken),\n expiresInHours: RESET_TTL_HOURS,\n },\n });\n}\n\nexport interface ConfirmPasswordResetResult {\n token: string;\n refreshToken: string;\n user: User;\n}\n\nexport async function confirmPasswordReset(params: {\n token: string;\n newPassword: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmPasswordResetResult> {\n const consumed = await consumePasswordReset({hashedToken: hashOpaqueToken(params.token)});\n if (!consumed) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const existingUser = await findUserById({id: consumed.userId});\n if (existingUser?.status !== 'active' || existingUser.hashedPassword === null) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const user = await updateUserPassword({userId: consumed.userId, hashedPassword});\n if (user?.status !== 'active') {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n await revokeRefreshTokensForUser({userId: consumed.userId});\n\n const token = await signAccessToken(user, params.workspaces);\n const refreshToken = await createRefreshSession(user);\n\n return {token, refreshToken, user};\n}\n\nexport async function changePassword(params: {\n userId: string;\n currentPassword: string;\n newPassword: string;\n refreshToken?: string | undefined;\n}): Promise<void> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.currentPassword,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok) {\n throw new InvalidCredentialsError();\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const currentRefreshSession = params.refreshToken\n ? await findActiveRefreshTokenByHash({hashedToken: hashOpaqueToken(params.refreshToken)})\n : undefined;\n const exceptRefreshTokenId =\n currentRefreshSession?.userId === user.id ? currentRefreshSession.id : undefined;\n\n await updateUserPassword({userId: user.id, hashedPassword});\n await revokeRefreshTokensForUser({userId: user.id, exceptRefreshTokenId});\n}\n\nexport async function logout(params: {refreshToken?: string | undefined}): Promise<void> {\n if (!params.refreshToken) return;\n await revokeRefreshTokenByHash({hashedToken: hashOpaqueToken(params.refreshToken)});\n}\n\nexport interface GetCurrentUserResult {\n user: User;\n}\n\nexport async function getCurrentUser(params: {userId: string}): Promise<GetCurrentUserResult> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n return {user};\n}\n"],"names":["emailSchema","confirmEmailChallenge","consumeEmailChallengeProof","createEmailChallenge","resendEmailChallenge","workspacesInterModuleContract","isInterModuleKnownError","userAccessTokenKey","generateOpaqueToken","hashOpaqueToken","config","consumePasswordReset","createPasswordReset","createRefreshToken","findActiveRefreshTokenByHash","findRefreshTokenByHash","revokeRefreshTokenByHash","revokeRefreshTokensForUser","rotateRefreshToken","createUser","createDbUser","findUserByEmail","findUserById","markEmailVerified","provisionUser","provisionDbUser","updateUserPassword","recordTokenRefreshed","AuthDependencyUnavailableError","EmailNotVerifiedError","EmailTakenError","InvalidCredentialsError","InvitationEmailMismatchError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","UserNotFoundError","signUserToken","hashPassword","verifyPassword","RESET_TTL_HOURS","PASSWORD_VERIFICATION_PURPOSE","dummyHashCache","getDummyHash","password","hoursFromNow","hours","Date","now","daysFromNow","days","recordRefreshOutcome","outcome","signAccessToken","user","workspaces","memberships","listMembershipsForTokenClaims","userId","id","catch","error","email","name","secret","expiresIn","AUTH_JWT_EXPIRES_IN","isWithinRotationGrace","refreshToken","rotatedAt","graceMs","AUTH_REFRESH_ROTATION_GRACE_SECONDS","getTime","createRefreshSession","hashedToken","expiresAt","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","passwordResetLink","rawToken","CLIENT_BASE_URL","invitationErrorFromKnownError","method","contractMethod","methods","code","invitationErrorMessage","params","parse","signup","existing","canResumeVerification","status","emailVerifiedAt","hashedPassword","hash","emailChallenge","purpose","continuation","sourceIp","signedUp","viaInvitation","signupWithInvitation","preflightInvitationAcceptance","token","invitationToken","membership","acceptError","result","acceptInvitation","workspaceId","message","verified","login","hasPassword","ok","createSessionForUser","undefined","refreshAccessToken","currentHashedToken","current","nextRefreshToken","rotated","nextHashedToken","latest","confirmEmailVerification","challengeId","verifiedUser","resendEmailVerification","nextResendAvailableAt","requestPasswordReset","sendEmail","resetLink","expiresInHours","confirmPasswordReset","consumed","existingUser","newPassword","changePassword","currentPassword","currentRefreshSession","exceptRefreshTokenId","logout","getCurrentUser"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { User } from './entities/user.js';
2
+ export type EmailOwner = Pick<User, 'id' | 'email' | 'status'>;
3
+ export interface FindUserByEmailParams {
4
+ email: string;
5
+ }
6
+ /**
7
+ * Read-only lookup of the current owner of a normalized email. Performs no
8
+ * user, session, or verification writes; returns undefined when no user owns
9
+ * the address.
10
+ */
11
+ export declare function findUserByEmail(params: FindUserByEmailParams): Promise<EmailOwner | undefined>;
12
+ //# sourceMappingURL=email-owner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-owner.d.ts","sourceRoot":"","sources":["../../src/core/email-owner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,oBAAoB,CAAC;AAE7C,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAKjC"}
@@ -0,0 +1,20 @@
1
+ import { emailSchema } from '@shipfox/api-auth-dto';
2
+ import { findUserByEmail as findUserByEmailInDb } from '#db/users.js';
3
+ /**
4
+ * Read-only lookup of the current owner of a normalized email. Performs no
5
+ * user, session, or verification writes; returns undefined when no user owns
6
+ * the address.
7
+ */ export async function findUserByEmail(params) {
8
+ const email = emailSchema.parse(params.email);
9
+ const user = await findUserByEmailInDb({
10
+ email
11
+ });
12
+ if (!user) return undefined;
13
+ return {
14
+ id: user.id,
15
+ email: user.email,
16
+ status: user.status
17
+ };
18
+ }
19
+
20
+ //# sourceMappingURL=email-owner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/email-owner.ts"],"sourcesContent":["import {emailSchema} from '@shipfox/api-auth-dto';\nimport {findUserByEmail as findUserByEmailInDb} from '#db/users.js';\nimport type {User} from './entities/user.js';\n\nexport type EmailOwner = Pick<User, 'id' | 'email' | 'status'>;\n\nexport interface FindUserByEmailParams {\n email: string;\n}\n\n/**\n * Read-only lookup of the current owner of a normalized email. Performs no\n * user, session, or verification writes; returns undefined when no user owns\n * the address.\n */\nexport async function findUserByEmail(\n params: FindUserByEmailParams,\n): Promise<EmailOwner | undefined> {\n const email = emailSchema.parse(params.email);\n const user = await findUserByEmailInDb({email});\n if (!user) return undefined;\n return {id: user.id, email: user.email, status: user.status};\n}\n"],"names":["emailSchema","findUserByEmail","findUserByEmailInDb","params","email","parse","user","undefined","id","status"],"mappings":"AAAA,SAAQA,WAAW,QAAO,wBAAwB;AAClD,SAAQC,mBAAmBC,mBAAmB,QAAO,eAAe;AASpE;;;;CAIC,GACD,OAAO,eAAeD,gBACpBE,MAA6B;IAE7B,MAAMC,QAAQJ,YAAYK,KAAK,CAACF,OAAOC,KAAK;IAC5C,MAAME,OAAO,MAAMJ,oBAAoB;QAACE;IAAK;IAC7C,IAAI,CAACE,MAAM,OAAOC;IAClB,OAAO;QAACC,IAAIF,KAAKE,EAAE;QAAEJ,OAAOE,KAAKF,KAAK;QAAEK,QAAQH,KAAKG,MAAM;IAAA;AAC7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"job-lease-token.d.ts","sourceRoot":"","sources":["../../src/core/job-lease-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,uBAAuB,CAAC;AAM/B,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,mBAAmB,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;AAExF,KAAK,mBAAmB,GAAG,IAAI,CAC7B,mBAAmB,EACjB,eAAe,GACf,sBAAsB,GACtB,OAAO,GACP,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,iBAAiB,CACpB,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,mBAAmB,EAC3B,SAAS,CAAC,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAC,GAC9D,wBAAwB,CAW1B;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B1F;AAED,qFAAqF;AACrF,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAc5F"}
1
+ {"version":3,"file":"job-lease-token.d.ts","sourceRoot":"","sources":["../../src/core/job-lease-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,mBAAmB,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;AAExF,KAAK,mBAAmB,GAAG,IAAI,CAC7B,mBAAmB,EACjB,eAAe,GACf,sBAAsB,GACtB,OAAO,GACP,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,iBAAiB,CACpB,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,mBAAmB,EAC3B,SAAS,CAAC,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAC,GAC9D,wBAAwB,CAW1B;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B1F;AAED,qFAAqF;AACrF,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAc5F"}
@@ -1,4 +1,5 @@
1
1
  import { JOB_LEASE_TOKEN_AUDIENCE, jobLeaseTokenClaimsSchema } from '@shipfox/api-auth-dto';
2
+ import { jobLeaseTokenKey } from '@shipfox/node-auth-root-key';
2
3
  import { signHs256, verifyHs256 } from '@shipfox/node-jwt';
3
4
  import { config } from '#config.js';
4
5
  import { recordTokenIssued, recordTokenVerified } from '#metrics/index.js';
@@ -32,7 +33,7 @@ export async function issueJobLeaseToken(claims) {
32
33
  currentStepAttempt: claims.currentStepAttempt
33
34
  } : {}
34
35
  },
35
- secret: config.AUTH_JOB_LEASE_TOKEN_SECRET,
36
+ secret: jobLeaseTokenKey(),
36
37
  expiresIn: config.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN,
37
38
  audience: JOB_LEASE_TOKEN_AUDIENCE
38
39
  });
@@ -43,7 +44,7 @@ export async function issueJobLeaseToken(claims) {
43
44
  try {
44
45
  const claims = await verifyHs256({
45
46
  token,
46
- secret: config.AUTH_JOB_LEASE_TOKEN_SECRET,
47
+ secret: jobLeaseTokenKey(),
47
48
  schema: jobLeaseTokenClaimsSchema,
48
49
  audience: JOB_LEASE_TOKEN_AUDIENCE
49
50
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/job-lease-token.ts"],"sourcesContent":["import {\n JOB_LEASE_TOKEN_AUDIENCE,\n type JobLeaseTokenClaims,\n jobLeaseTokenClaimsSchema,\n} from '@shipfox/api-auth-dto';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {config} from '#config.js';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\n// `aud`, `iat` and `exp` are set by the codec (jose); callers supply the business ids only.\nexport type IssueJobLeaseTokenParams = Omit<JobLeaseTokenClaims, 'aud' | 'iat' | 'exp'>;\n\ntype JobLeaseParamSource = Pick<\n JobLeaseTokenClaims,\n | 'workflowRunId'\n | 'workflowRunAttemptId'\n | 'jobId'\n | 'jobExecutionId'\n | 'projectId'\n | 'workspaceId'\n | 'runnerSessionId'\n>;\n\nexport function jobLeaseParamsFrom(\n source: JobLeaseParamSource,\n stepScope?: {currentStepId: string; currentStepAttempt: number},\n): IssueJobLeaseTokenParams {\n return {\n workflowRunId: source.workflowRunId,\n workflowRunAttemptId: source.workflowRunAttemptId,\n jobId: source.jobId,\n jobExecutionId: source.jobExecutionId,\n projectId: source.projectId,\n workspaceId: source.workspaceId,\n runnerSessionId: source.runnerSessionId,\n ...(stepScope ? stepScope : {}),\n };\n}\n\nexport async function issueJobLeaseToken(claims: IssueJobLeaseTokenParams): Promise<string> {\n const token = await signHs256({\n payload: {\n workflowRunId: claims.workflowRunId,\n ...(claims.workflowRunAttempt === undefined\n ? {}\n : {workflowRunAttempt: claims.workflowRunAttempt}),\n workflowRunAttemptId: claims.workflowRunAttemptId,\n jobId: claims.jobId,\n jobExecutionId: claims.jobExecutionId,\n projectId: claims.projectId,\n workspaceId: claims.workspaceId,\n runnerSessionId: claims.runnerSessionId,\n ...(claims.currentStepId && claims.currentStepAttempt !== undefined\n ? {\n currentStepId: claims.currentStepId,\n currentStepAttempt: claims.currentStepAttempt,\n }\n : {}),\n },\n secret: config.AUTH_JOB_LEASE_TOKEN_SECRET,\n expiresIn: config.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN,\n audience: JOB_LEASE_TOKEN_AUDIENCE,\n });\n recordTokenIssued('job_lease');\n return token;\n}\n\n/** Returns the claims on success, or `null` for any invalid input — never throws. */\nexport async function verifyJobLeaseToken(token: string): Promise<JobLeaseTokenClaims | null> {\n try {\n const claims = await verifyHs256({\n token,\n secret: config.AUTH_JOB_LEASE_TOKEN_SECRET,\n schema: jobLeaseTokenClaimsSchema,\n audience: JOB_LEASE_TOKEN_AUDIENCE,\n });\n recordTokenVerified('job_lease', 'ok');\n return claims;\n } catch {\n recordTokenVerified('job_lease', 'rejected');\n return null;\n }\n}\n"],"names":["JOB_LEASE_TOKEN_AUDIENCE","jobLeaseTokenClaimsSchema","signHs256","verifyHs256","config","recordTokenIssued","recordTokenVerified","jobLeaseParamsFrom","source","stepScope","workflowRunId","workflowRunAttemptId","jobId","jobExecutionId","projectId","workspaceId","runnerSessionId","issueJobLeaseToken","claims","token","payload","workflowRunAttempt","undefined","currentStepId","currentStepAttempt","secret","AUTH_JOB_LEASE_TOKEN_SECRET","expiresIn","AUTH_JOB_LEASE_TOKEN_EXPIRES_IN","audience","verifyJobLeaseToken","schema"],"mappings":"AAAA,SACEA,wBAAwB,EAExBC,yBAAyB,QACpB,wBAAwB;AAC/B,SAAQC,SAAS,EAAEC,WAAW,QAAO,oBAAoB;AACzD,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,iBAAiB,EAAEC,mBAAmB,QAAO,oBAAoB;AAgBzE,OAAO,SAASC,mBACdC,MAA2B,EAC3BC,SAA+D;IAE/D,OAAO;QACLC,eAAeF,OAAOE,aAAa;QACnCC,sBAAsBH,OAAOG,oBAAoB;QACjDC,OAAOJ,OAAOI,KAAK;QACnBC,gBAAgBL,OAAOK,cAAc;QACrCC,WAAWN,OAAOM,SAAS;QAC3BC,aAAaP,OAAOO,WAAW;QAC/BC,iBAAiBR,OAAOQ,eAAe;QACvC,GAAIP,YAAYA,YAAY,CAAC,CAAC;IAChC;AACF;AAEA,OAAO,eAAeQ,mBAAmBC,MAAgC;IACvE,MAAMC,QAAQ,MAAMjB,UAAU;QAC5BkB,SAAS;YACPV,eAAeQ,OAAOR,aAAa;YACnC,GAAIQ,OAAOG,kBAAkB,KAAKC,YAC9B,CAAC,IACD;gBAACD,oBAAoBH,OAAOG,kBAAkB;YAAA,CAAC;YACnDV,sBAAsBO,OAAOP,oBAAoB;YACjDC,OAAOM,OAAON,KAAK;YACnBC,gBAAgBK,OAAOL,cAAc;YACrCC,WAAWI,OAAOJ,SAAS;YAC3BC,aAAaG,OAAOH,WAAW;YAC/BC,iBAAiBE,OAAOF,eAAe;YACvC,GAAIE,OAAOK,aAAa,IAAIL,OAAOM,kBAAkB,KAAKF,YACtD;gBACEC,eAAeL,OAAOK,aAAa;gBACnCC,oBAAoBN,OAAOM,kBAAkB;YAC/C,IACA,CAAC,CAAC;QACR;QACAC,QAAQrB,OAAOsB,2BAA2B;QAC1CC,WAAWvB,OAAOwB,+BAA+B;QACjDC,UAAU7B;IACZ;IACAK,kBAAkB;IAClB,OAAOc;AACT;AAEA,mFAAmF,GACnF,OAAO,eAAeW,oBAAoBX,KAAa;IACrD,IAAI;QACF,MAAMD,SAAS,MAAMf,YAAY;YAC/BgB;YACAM,QAAQrB,OAAOsB,2BAA2B;YAC1CK,QAAQ9B;YACR4B,UAAU7B;QACZ;QACAM,oBAAoB,aAAa;QACjC,OAAOY;IACT,EAAE,OAAM;QACNZ,oBAAoB,aAAa;QACjC,OAAO;IACT;AACF"}
1
+ {"version":3,"sources":["../../src/core/job-lease-token.ts"],"sourcesContent":["import {\n JOB_LEASE_TOKEN_AUDIENCE,\n type JobLeaseTokenClaims,\n jobLeaseTokenClaimsSchema,\n} from '@shipfox/api-auth-dto';\nimport {jobLeaseTokenKey} from '@shipfox/node-auth-root-key';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {config} from '#config.js';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\n// `aud`, `iat` and `exp` are set by the codec (jose); callers supply the business ids only.\nexport type IssueJobLeaseTokenParams = Omit<JobLeaseTokenClaims, 'aud' | 'iat' | 'exp'>;\n\ntype JobLeaseParamSource = Pick<\n JobLeaseTokenClaims,\n | 'workflowRunId'\n | 'workflowRunAttemptId'\n | 'jobId'\n | 'jobExecutionId'\n | 'projectId'\n | 'workspaceId'\n | 'runnerSessionId'\n>;\n\nexport function jobLeaseParamsFrom(\n source: JobLeaseParamSource,\n stepScope?: {currentStepId: string; currentStepAttempt: number},\n): IssueJobLeaseTokenParams {\n return {\n workflowRunId: source.workflowRunId,\n workflowRunAttemptId: source.workflowRunAttemptId,\n jobId: source.jobId,\n jobExecutionId: source.jobExecutionId,\n projectId: source.projectId,\n workspaceId: source.workspaceId,\n runnerSessionId: source.runnerSessionId,\n ...(stepScope ? stepScope : {}),\n };\n}\n\nexport async function issueJobLeaseToken(claims: IssueJobLeaseTokenParams): Promise<string> {\n const token = await signHs256({\n payload: {\n workflowRunId: claims.workflowRunId,\n ...(claims.workflowRunAttempt === undefined\n ? {}\n : {workflowRunAttempt: claims.workflowRunAttempt}),\n workflowRunAttemptId: claims.workflowRunAttemptId,\n jobId: claims.jobId,\n jobExecutionId: claims.jobExecutionId,\n projectId: claims.projectId,\n workspaceId: claims.workspaceId,\n runnerSessionId: claims.runnerSessionId,\n ...(claims.currentStepId && claims.currentStepAttempt !== undefined\n ? {\n currentStepId: claims.currentStepId,\n currentStepAttempt: claims.currentStepAttempt,\n }\n : {}),\n },\n secret: jobLeaseTokenKey(),\n expiresIn: config.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN,\n audience: JOB_LEASE_TOKEN_AUDIENCE,\n });\n recordTokenIssued('job_lease');\n return token;\n}\n\n/** Returns the claims on success, or `null` for any invalid input — never throws. */\nexport async function verifyJobLeaseToken(token: string): Promise<JobLeaseTokenClaims | null> {\n try {\n const claims = await verifyHs256({\n token,\n secret: jobLeaseTokenKey(),\n schema: jobLeaseTokenClaimsSchema,\n audience: JOB_LEASE_TOKEN_AUDIENCE,\n });\n recordTokenVerified('job_lease', 'ok');\n return claims;\n } catch {\n recordTokenVerified('job_lease', 'rejected');\n return null;\n }\n}\n"],"names":["JOB_LEASE_TOKEN_AUDIENCE","jobLeaseTokenClaimsSchema","jobLeaseTokenKey","signHs256","verifyHs256","config","recordTokenIssued","recordTokenVerified","jobLeaseParamsFrom","source","stepScope","workflowRunId","workflowRunAttemptId","jobId","jobExecutionId","projectId","workspaceId","runnerSessionId","issueJobLeaseToken","claims","token","payload","workflowRunAttempt","undefined","currentStepId","currentStepAttempt","secret","expiresIn","AUTH_JOB_LEASE_TOKEN_EXPIRES_IN","audience","verifyJobLeaseToken","schema"],"mappings":"AAAA,SACEA,wBAAwB,EAExBC,yBAAyB,QACpB,wBAAwB;AAC/B,SAAQC,gBAAgB,QAAO,8BAA8B;AAC7D,SAAQC,SAAS,EAAEC,WAAW,QAAO,oBAAoB;AACzD,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,iBAAiB,EAAEC,mBAAmB,QAAO,oBAAoB;AAgBzE,OAAO,SAASC,mBACdC,MAA2B,EAC3BC,SAA+D;IAE/D,OAAO;QACLC,eAAeF,OAAOE,aAAa;QACnCC,sBAAsBH,OAAOG,oBAAoB;QACjDC,OAAOJ,OAAOI,KAAK;QACnBC,gBAAgBL,OAAOK,cAAc;QACrCC,WAAWN,OAAOM,SAAS;QAC3BC,aAAaP,OAAOO,WAAW;QAC/BC,iBAAiBR,OAAOQ,eAAe;QACvC,GAAIP,YAAYA,YAAY,CAAC,CAAC;IAChC;AACF;AAEA,OAAO,eAAeQ,mBAAmBC,MAAgC;IACvE,MAAMC,QAAQ,MAAMjB,UAAU;QAC5BkB,SAAS;YACPV,eAAeQ,OAAOR,aAAa;YACnC,GAAIQ,OAAOG,kBAAkB,KAAKC,YAC9B,CAAC,IACD;gBAACD,oBAAoBH,OAAOG,kBAAkB;YAAA,CAAC;YACnDV,sBAAsBO,OAAOP,oBAAoB;YACjDC,OAAOM,OAAON,KAAK;YACnBC,gBAAgBK,OAAOL,cAAc;YACrCC,WAAWI,OAAOJ,SAAS;YAC3BC,aAAaG,OAAOH,WAAW;YAC/BC,iBAAiBE,OAAOF,eAAe;YACvC,GAAIE,OAAOK,aAAa,IAAIL,OAAOM,kBAAkB,KAAKF,YACtD;gBACEC,eAAeL,OAAOK,aAAa;gBACnCC,oBAAoBN,OAAOM,kBAAkB;YAC/C,IACA,CAAC,CAAC;QACR;QACAC,QAAQxB;QACRyB,WAAWtB,OAAOuB,+BAA+B;QACjDC,UAAU7B;IACZ;IACAM,kBAAkB;IAClB,OAAOc;AACT;AAEA,mFAAmF,GACnF,OAAO,eAAeU,oBAAoBV,KAAa;IACrD,IAAI;QACF,MAAMD,SAAS,MAAMf,YAAY;YAC/BgB;YACAM,QAAQxB;YACR6B,QAAQ9B;YACR4B,UAAU7B;QACZ;QACAO,oBAAoB,aAAa;QACjC,OAAOY;IACT,EAAE,OAAM;QACNZ,oBAAoB,aAAa;QACjC,OAAO;IACT;AACF"}
@@ -25,12 +25,12 @@ export interface SignUserTokenParams {
25
25
  email: string;
26
26
  name?: string | null | undefined;
27
27
  memberships: TokenMembership[];
28
- secret: string;
28
+ secret: string | Uint8Array;
29
29
  expiresIn: string;
30
30
  }
31
31
  export interface VerifyUserTokenParams {
32
32
  token: string;
33
- secret: string;
33
+ secret: string | Uint8Array;
34
34
  }
35
35
  export declare function signUserToken(params: SignUserTokenParams): Promise<string>;
36
36
  export declare function verifyUserToken(params: VerifyUserTokenParams): Promise<UserTokenClaims>;
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/core/jwt.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,eAAO,MAAM,qBAAqB;;;;;iBAGhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;iBAOhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAahF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAa7F"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/core/jwt.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,eAAO,MAAM,qBAAqB;;;;;iBAGhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;iBAOhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;CAC7B;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAahF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAa7F"}