@shipfox/api-auth 5.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 +52 -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 +17 -13
  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,10 +1,11 @@
1
1
  import {JOB_LEASE_TOKEN_AUDIENCE, RUNNER_SESSION_TOKEN_AUDIENCE} from '@shipfox/api-auth-dto';
2
+ import {runnerSessionTokenKey} from '@shipfox/node-auth-root-key';
2
3
  import {signHs256} from '@shipfox/node-jwt';
3
4
  import {generateKeyPair, SignJWT} from 'jose';
4
5
  import {issueJobLeaseToken, verifyJobLeaseToken} from './job-lease-token.js';
5
6
  import {issueRunnerSessionToken, verifyRunnerSessionToken} from './runner-session-token.js';
6
7
 
7
- const SECRET = process.env.AUTH_RUNNER_SESSION_TOKEN_SECRET ?? 'test-runner-session-secret';
8
+ const SECRET = runnerSessionTokenKey();
8
9
 
9
10
  function claims() {
10
11
  return {
@@ -3,6 +3,7 @@ import {
3
3
  type RunnerSessionTokenClaims,
4
4
  runnerSessionTokenClaimsSchema,
5
5
  } from '@shipfox/api-auth-dto';
6
+ import {runnerSessionTokenKey} from '@shipfox/node-auth-root-key';
6
7
  import {signHs256, verifyHs256} from '@shipfox/node-jwt';
7
8
  import {config} from '#config.js';
8
9
  import {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';
@@ -21,7 +22,7 @@ export async function issueRunnerSessionToken(
21
22
  labels: claims.labels,
22
23
  maxClaims: claims.maxClaims,
23
24
  },
24
- secret: config.AUTH_RUNNER_SESSION_TOKEN_SECRET,
25
+ secret: runnerSessionTokenKey(),
25
26
  expiresIn: config.AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN,
26
27
  audience: RUNNER_SESSION_TOKEN_AUDIENCE,
27
28
  });
@@ -36,7 +37,7 @@ export async function verifyRunnerSessionToken(
36
37
  try {
37
38
  const claims = await verifyHs256({
38
39
  token,
39
- secret: config.AUTH_RUNNER_SESSION_TOKEN_SECRET,
40
+ secret: runnerSessionTokenKey(),
40
41
  schema: runnerSessionTokenClaimsSchema,
41
42
  audience: RUNNER_SESSION_TOKEN_AUDIENCE,
42
43
  });
package/src/db/db.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
2
2
  import {pgClient} from '@shipfox/node-postgres';
3
- import {emailVerifications} from './schema/email-verifications.js';
4
3
  import {authOutbox} from './schema/outbox.js';
5
4
  import {passwordResets} from './schema/password-resets.js';
6
5
  import {authRateLimits} from './schema/rate-limits.js';
@@ -11,7 +10,6 @@ export const schema = {
11
10
  users,
12
11
  passwordResets,
13
12
  refreshTokens,
14
- emailVerifications,
15
13
  authOutbox,
16
14
  authRateLimits,
17
15
  };
package/src/db/users.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import {AUTH_USER_SIGNED_UP, type AuthEventMap} from '@shipfox/api-auth-dto';
2
+ import {writeOutboxEvent} from '@shipfox/node-outbox';
1
3
  import {eq} from 'drizzle-orm';
2
4
  import type {User} from '#core/entities/user.js';
3
5
  import {EmailTakenError} from '#core/errors.js';
4
6
  import {db} from './db.js';
7
+ import {authOutbox} from './schema/outbox.js';
5
8
  import {toUser, type UserDb, users} from './schema/users.js';
6
9
 
7
10
  export interface CreateUserParams {
@@ -9,6 +12,7 @@ export interface CreateUserParams {
9
12
  hashedPassword: string | null;
10
13
  name?: string | null;
11
14
  emailVerifiedAt?: Date | null;
15
+ signedUp?: {viaInvitation: boolean};
12
16
  }
13
17
 
14
18
  export interface ProvisionUserParams {
@@ -29,27 +33,41 @@ function isAuthUsersEmailUniqueViolation(error: unknown): boolean {
29
33
  }
30
34
 
31
35
  export async function createUser(params: CreateUserParams): Promise<User> {
32
- let rows: UserDb[];
33
36
  try {
34
- rows = await db()
35
- .insert(users)
36
- .values({
37
- email: params.email,
38
- hashedPassword: params.hashedPassword,
39
- name: params.name ?? null,
40
- emailVerifiedAt: params.emailVerifiedAt ?? null,
41
- })
42
- .returning();
37
+ return await db().transaction(async (tx) => {
38
+ const rows: UserDb[] = await tx
39
+ .insert(users)
40
+ .values({
41
+ email: params.email,
42
+ hashedPassword: params.hashedPassword,
43
+ name: params.name ?? null,
44
+ emailVerifiedAt: params.emailVerifiedAt ?? null,
45
+ })
46
+ .returning();
47
+
48
+ const row = rows[0];
49
+ if (!row) throw new Error('Insert returned no rows');
50
+
51
+ const user = toUser(row);
52
+ if (params.signedUp) {
53
+ await writeOutboxEvent<AuthEventMap>(tx, authOutbox, {
54
+ type: AUTH_USER_SIGNED_UP,
55
+ payload: {
56
+ userId: user.id,
57
+ email: user.email,
58
+ ...(user.name ? {name: user.name} : {}),
59
+ viaInvitation: params.signedUp.viaInvitation,
60
+ },
61
+ });
62
+ }
63
+ return user;
64
+ });
43
65
  } catch (error) {
44
66
  if (isAuthUsersEmailUniqueViolation(error)) {
45
67
  throw new EmailTakenError(params.email);
46
68
  }
47
69
  throw error;
48
70
  }
49
-
50
- const row = rows[0];
51
- if (!row) throw new Error('Insert returned no rows');
52
- return toUser(row);
53
71
  }
54
72
 
55
73
  export async function provisionUser(params: ProvisionUserParams): Promise<User> {
package/src/index.test.ts CHANGED
@@ -1,16 +1,14 @@
1
1
  import {
2
- AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
3
2
  AUTH_PASSWORD_RESET_SEND_REQUESTED,
3
+ AUTH_USER_SIGNED_UP,
4
4
  authEventSchemas,
5
5
  } from '@shipfox/api-auth-dto';
6
- import {authModule} from './index.js';
6
+ import {createAuthModule} from './index.js';
7
7
  import {passwordLoginMethods} from './login-methods.js';
8
8
 
9
9
  vi.mock('#config.js', () => ({
10
10
  config: {
11
- AUTH_JWT_SECRET: 'auth-module-test-secret',
12
11
  AUTH_JWT_EXPIRES_IN: '15m',
13
- AUTH_JOB_LEASE_TOKEN_SECRET: 'auth-module-test-job-secret',
14
12
  AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: '90m',
15
13
  AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
16
14
  AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
@@ -18,10 +16,21 @@ vi.mock('#config.js', () => ({
18
16
  AUTH_PASSWORD_ENABLED: true,
19
17
  CLIENT_BASE_URL: 'https://app.example.test',
20
18
  },
19
+ }));
20
+
21
+ vi.mock('@shipfox/node-mailer', () => ({
21
22
  mailer: {send: vi.fn()},
22
23
  }));
23
24
 
24
25
  describe('authModule', () => {
26
+ const authModule = createAuthModule({
27
+ workspaces: {
28
+ listMembershipsForTokenClaims: vi.fn(),
29
+ preflightInvitationAcceptance: vi.fn(),
30
+ acceptInvitation: vi.fn(),
31
+ requireActiveMembership: vi.fn(),
32
+ },
33
+ });
25
34
  test('declares password login only when password login is enabled', () => {
26
35
  expect(passwordLoginMethods(true)).toEqual([{id: 'password'}]);
27
36
  expect(passwordLoginMethods(false)).toEqual([]);
@@ -34,14 +43,9 @@ describe('authModule', () => {
34
43
 
35
44
  expect(publisher?.eventSchemas).toBe(authEventSchemas);
36
45
  expect(Object.keys(publisher?.eventSchemas ?? {}).sort()).toEqual([
37
- AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
38
46
  AUTH_PASSWORD_RESET_SEND_REQUESTED,
47
+ AUTH_USER_SIGNED_UP,
39
48
  ]);
40
- expect(events).toEqual(
41
- expect.arrayContaining([
42
- AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
43
- AUTH_PASSWORD_RESET_SEND_REQUESTED,
44
- ]),
45
- );
49
+ expect(events).toEqual(expect.arrayContaining([AUTH_PASSWORD_RESET_SEND_REQUESTED]));
46
50
  });
47
51
  });
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
3
2
  AUTH_PASSWORD_RESET_SEND_REQUESTED,
4
3
  type AuthEventMap,
5
4
  authEventSchemas,
@@ -13,12 +12,10 @@ import {authOutbox} from '#db/schema/outbox.js';
13
12
  import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
14
13
  import {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js';
15
14
  import {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';
16
- import {authE2eRoutes} from '#presentation/e2eRoutes/index.js';
17
- import {authRoutes} from '#presentation/routes/index.js';
18
- import {
19
- onEmailVerificationSendRequested,
20
- onPasswordResetSendRequested,
21
- } from '#presentation/subscribers/index.js';
15
+ import {createAuthE2eRoutes} from '#presentation/e2eRoutes/index.js';
16
+ import {createAuthInterModulePresentation} from '#presentation/inter-module.js';
17
+ import {buildAuthRoutes} from '#presentation/routes/index.js';
18
+ import {onPasswordResetSendRequested} from '#presentation/subscribers/index.js';
22
19
  import {passwordLoginMethods} from './login-methods.js';
23
20
 
24
21
  export type {JobLeaseTokenClaims, RunnerSessionTokenClaims} from '@shipfox/api-auth-dto';
@@ -29,6 +26,8 @@ export type {
29
26
  ProvisionUserParams,
30
27
  } from '#core/auth.js';
31
28
  export {createSessionForUser, provisionUser} from '#core/auth.js';
29
+ export type {EmailOwner, FindUserByEmailParams} from '#core/email-owner.js';
30
+ export {findUserByEmail} from '#core/email-owner.js';
32
31
  export type {User, UserStatus} from '#core/entities/user.js';
33
32
  export {
34
33
  AuthDependencyUnavailableError,
@@ -57,16 +56,20 @@ export {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-a
57
56
 
58
57
  const subscriber = subscriberFactory<AuthEventMap>();
59
58
 
60
- export const authModule: ShipfoxModule = {
61
- name: 'auth',
62
- database: {db, migrationsPath},
63
- auth: [createJwtAuthMethod(), createLeaseTokenAuthMethod(), createRunnerSessionAuthMethod()],
64
- loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),
65
- routes: [authRoutes],
66
- e2eRoutes: [authE2eRoutes],
67
- publishers: [{name: 'auth', table: authOutbox, db, eventSchemas: authEventSchemas}],
68
- subscribers: [
69
- subscriber(AUTH_EMAIL_VERIFICATION_SEND_REQUESTED, onEmailVerificationSendRequested),
70
- subscriber(AUTH_PASSWORD_RESET_SEND_REQUESTED, onPasswordResetSendRequested),
71
- ],
72
- };
59
+ export function createAuthModule({
60
+ workspaces,
61
+ }: {
62
+ workspaces: import('@shipfox/api-workspaces-dto/inter-module').WorkspacesInterModuleClient;
63
+ }): ShipfoxModule {
64
+ return {
65
+ name: 'auth',
66
+ database: {db, migrationsPath},
67
+ auth: [createJwtAuthMethod(), createLeaseTokenAuthMethod(), createRunnerSessionAuthMethod()],
68
+ loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),
69
+ routes: [buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces)],
70
+ e2eRoutes: [createAuthE2eRoutes(workspaces)],
71
+ publishers: [{name: 'auth', table: authOutbox, db, eventSchemas: authEventSchemas}],
72
+ subscribers: [subscriber(AUTH_PASSWORD_RESET_SEND_REQUESTED, onPasswordResetSendRequested)],
73
+ interModulePresentations: [createAuthInterModulePresentation()],
74
+ };
75
+ }
@@ -1,3 +1,4 @@
1
+ import {userAccessTokenKey} from '@shipfox/node-auth-root-key';
1
2
  import type {FastifyInstance} from 'fastify';
2
3
  import Fastify from 'fastify';
3
4
  import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
@@ -5,7 +6,7 @@ import {signUserToken} from '#core/jwt.js';
5
6
  import {createUser, findUserById} from '#db/users.js';
6
7
  import {createJwtAuthMethod, getClientContext} from './jwt-auth.js';
7
8
 
8
- const SECRET = 'test-secret';
9
+ const SECRET = userAccessTokenKey();
9
10
 
10
11
  function emailFor(suffix: string): string {
11
12
  return `${suffix}-${crypto.randomUUID()}@example.com`;
@@ -5,9 +5,9 @@ import {
5
5
  setUserContext,
6
6
  type UserContext,
7
7
  } from '@shipfox/api-auth-context';
8
+ import {userAccessTokenKey} from '@shipfox/node-auth-root-key';
8
9
  import type {AuthMethod} from '@shipfox/node-fastify';
9
10
  import type {FastifyRequest} from 'fastify';
10
- import {config} from '#config.js';
11
11
  import {verifyUserToken} from '#core/jwt.js';
12
12
  import {createBearerTokenAuthMethod} from './bearer-token-auth.js';
13
13
 
@@ -24,7 +24,7 @@ export function getClientContext(request: FastifyRequest): ClientContext | null
24
24
  export function createJwtAuthMethod(): AuthMethod {
25
25
  return createBearerTokenAuthMethod({
26
26
  name: AUTH_USER,
27
- verifyToken: (token) => verifyUserToken({token, secret: config.AUTH_JWT_SECRET}),
27
+ verifyToken: (token) => verifyUserToken({token, secret: userAccessTokenKey()}),
28
28
  invalidTokenError: {message: 'Invalid or expired token', code: 'unauthorized'},
29
29
  setContext: (request, claims) => {
30
30
  const clientContext: ClientContext = buildUserContext({
@@ -1,39 +1,43 @@
1
1
  import {e2eCreateSessionBodySchema, e2eCreateSessionResponseSchema} from '@shipfox/api-auth-dto';
2
+ import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
2
3
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
3
4
  import {createSessionForUser} from '#core/auth.js';
4
5
  import {EmailNotVerifiedError, InvalidCredentialsError, UserNotFoundError} from '#core/errors.js';
5
6
  import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
6
7
  import {toAuthSessionDto} from '#presentation/dto/user.js';
7
8
 
8
- export const createE2eSessionRoute = defineRoute({
9
- method: 'POST',
10
- path: '/sessions',
11
- description: 'Create an authenticated browser session for E2E tests.',
12
- schema: {
13
- body: e2eCreateSessionBodySchema,
14
- response: {
15
- 200: e2eCreateSessionResponseSchema,
9
+ export function createE2eSessionRoute(workspaces: WorkspacesInterModuleClient) {
10
+ return defineRoute({
11
+ method: 'POST',
12
+ path: '/sessions',
13
+ description: 'Create an authenticated browser session for E2E tests.',
14
+ schema: {
15
+ body: e2eCreateSessionBodySchema,
16
+ response: {
17
+ 200: e2eCreateSessionResponseSchema,
18
+ },
16
19
  },
17
- },
18
- errorHandler: (error) => {
19
- if (error instanceof UserNotFoundError) {
20
- throw new ClientError('User not found', 'user-not-found', {status: 404});
21
- }
22
- if (error instanceof EmailNotVerifiedError) {
23
- throw new ClientError('Email not verified', 'email-not-verified', {status: 403});
24
- }
25
- if (error instanceof InvalidCredentialsError) {
26
- throw new ClientError('User is not active', 'user-inactive', {status: 409});
27
- }
28
- throw error;
29
- },
30
- handler: async (request, reply) => {
31
- const result = await createSessionForUser({
32
- ...(request.body.user_id ? {userId: request.body.user_id} : {}),
33
- ...(request.body.email ? {email: request.body.email} : {}),
34
- });
20
+ errorHandler: (error) => {
21
+ if (error instanceof UserNotFoundError) {
22
+ throw new ClientError('User not found', 'user-not-found', {status: 404});
23
+ }
24
+ if (error instanceof EmailNotVerifiedError) {
25
+ throw new ClientError('Email not verified', 'email-not-verified', {status: 403});
26
+ }
27
+ if (error instanceof InvalidCredentialsError) {
28
+ throw new ClientError('User is not active', 'user-inactive', {status: 409});
29
+ }
30
+ throw error;
31
+ },
32
+ handler: async (request, reply) => {
33
+ const result = await createSessionForUser({
34
+ ...(request.body.user_id ? {userId: request.body.user_id} : {}),
35
+ ...(request.body.email ? {email: request.body.email} : {}),
36
+ workspaces,
37
+ });
35
38
 
36
- setRefreshTokenCookie(reply, result.refreshToken);
37
- return toAuthSessionDto(result);
38
- },
39
- });
39
+ setRefreshTokenCookie(reply, result.refreshToken);
40
+ return toAuthSessionDto(result);
41
+ },
42
+ });
43
+ }
@@ -1,17 +1,20 @@
1
1
  import {createApp} from '@shipfox/node-fastify';
2
2
  import type {FastifyInstance} from 'fastify';
3
- import {authE2eRoutes} from './index.js';
4
-
5
- vi.mock('@shipfox/api-workspaces', () => ({
6
- listMembershipsByUser: vi.fn(() => Promise.resolve([])),
7
- }));
3
+ import {createAuthE2eRoutes} from './index.js';
8
4
 
9
5
  describe('auth E2E routes', () => {
10
6
  let app: FastifyInstance;
11
7
 
12
8
  beforeAll(async () => {
13
9
  app = await createApp({
14
- routes: [authE2eRoutes],
10
+ routes: [
11
+ createAuthE2eRoutes({
12
+ listMembershipsForTokenClaims: vi.fn(() => Promise.resolve({memberships: []})),
13
+ preflightInvitationAcceptance: vi.fn(),
14
+ acceptInvitation: vi.fn(),
15
+ requireActiveMembership: vi.fn(),
16
+ }),
17
+ ],
15
18
  swagger: false,
16
19
  });
17
20
  });
@@ -1,10 +1,13 @@
1
1
  import cookie from '@fastify/cookie';
2
+ import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
2
3
  import type {RouteGroup} from '@shipfox/node-fastify';
3
4
  import {createE2eSessionRoute} from './create-session.js';
4
5
  import {createE2eUserRoute} from './create-user.js';
5
6
 
6
- export const authE2eRoutes: RouteGroup = {
7
- prefix: '/auth',
8
- plugins: [cookie],
9
- routes: [createE2eUserRoute, createE2eSessionRoute],
10
- };
7
+ export function createAuthE2eRoutes(workspaces: WorkspacesInterModuleClient): RouteGroup {
8
+ return {
9
+ prefix: '/auth',
10
+ plugins: [cookie],
11
+ routes: [createE2eUserRoute, createE2eSessionRoute(workspaces)],
12
+ };
13
+ }
@@ -0,0 +1,13 @@
1
+ import {authInterModuleContract} from '@shipfox/api-auth-dto/inter-module';
2
+ import {defineInterModulePresentation, type InterModulePresentation} from '@shipfox/inter-module';
3
+ import {issueJobLeaseToken} from '#core/job-lease-token.js';
4
+ import {issueRunnerSessionToken} from '#core/runner-session-token.js';
5
+
6
+ export function createAuthInterModulePresentation(): InterModulePresentation<
7
+ typeof authInterModuleContract
8
+ > {
9
+ return defineInterModulePresentation(authInterModuleContract, {
10
+ mintRunnerSessionToken: async (claims) => ({token: await issueRunnerSessionToken(claims)}),
11
+ mintJobLeaseToken: async (claims) => ({token: await issueJobLeaseToken(claims)}),
12
+ });
13
+ }
@@ -2,36 +2,54 @@ import {
2
2
  verifyEmailConfirmBodySchema,
3
3
  verifyEmailConfirmResponseSchema,
4
4
  } from '@shipfox/api-auth-dto';
5
+ import {EmailChallengeError} from '@shipfox/api-email-challenges';
6
+ import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
5
7
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
6
8
  import {confirmEmailVerification} from '#core/auth.js';
7
9
  import {TokenInvalidError} from '#core/errors.js';
8
10
  import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
9
11
  import {toAuthSessionDto} from '#presentation/dto/user.js';
10
12
 
11
- export const verifyEmailConfirmRoute = defineRoute({
12
- method: 'POST',
13
- path: '/verify-email/confirm',
14
- description: 'Verify a user email address with the link sent by email.',
15
- schema: {
16
- body: verifyEmailConfirmBodySchema,
17
- response: {
18
- 200: verifyEmailConfirmResponseSchema,
13
+ export function createVerifyEmailConfirmRoute(workspaces: WorkspacesInterModuleClient) {
14
+ return defineRoute({
15
+ method: 'POST',
16
+ path: '/verify-email/confirm',
17
+ description: 'Verify a user email address with the code sent by email.',
18
+ schema: {
19
+ body: verifyEmailConfirmBodySchema,
20
+ response: {
21
+ 200: verifyEmailConfirmResponseSchema,
22
+ },
19
23
  },
20
- },
21
- errorHandler: (error) => {
22
- if (error instanceof TokenInvalidError) {
23
- throw new ClientError('Verification token is invalid or expired', 'token-invalid', {
24
- status: 410,
25
- });
26
- }
27
- throw error;
28
- },
29
- handler: async (request, reply) => {
30
- const {token} = request.body;
24
+ errorHandler: (error) => {
25
+ if (error instanceof TokenInvalidError) {
26
+ throw new ClientError(
27
+ 'Verification code is invalid or expired',
28
+ 'email-challenge-invalid',
29
+ {
30
+ status: 410,
31
+ },
32
+ );
33
+ }
34
+ if (error instanceof EmailChallengeError) {
35
+ throw new ClientError(error.message, `email-challenge-${error.code}`, {
36
+ status: error.code === 'expired' || error.code === 'exhausted' ? 410 : 400,
37
+ });
38
+ }
39
+ throw error;
40
+ },
41
+ handler: async (request, reply) => {
42
+ const {email, challenge_id, code} = request.body;
31
43
 
32
- const result = await confirmEmailVerification({token});
33
- setRefreshTokenCookie(reply, result.refreshToken);
44
+ const result = await confirmEmailVerification({
45
+ email,
46
+ challengeId: challenge_id,
47
+ code,
48
+ workspaces,
49
+ });
50
+ setRefreshTokenCookie(reply, result.refreshToken);
34
51
 
35
- return toAuthSessionDto(result);
36
- },
37
- });
52
+ return toAuthSessionDto(result);
53
+ },
54
+ });
55
+ }
@@ -1,12 +1,13 @@
1
1
  import {verifyEmailResendBodySchema, verifyEmailResendResponseSchema} from '@shipfox/api-auth-dto';
2
- import {defineRoute} from '@shipfox/node-fastify';
2
+ import {EmailChallengeError} from '@shipfox/api-email-challenges';
3
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
3
4
  import {resendEmailVerification} from '#core/auth.js';
4
5
  import {createAuthRateLimitPreHandler} from '#presentation/routes/rate-limit.js';
5
6
 
6
7
  export const verifyEmailResendRoute = defineRoute({
7
8
  method: 'POST',
8
9
  path: '/verify-email/resend',
9
- description: 'Send a new email verification link if the account can be verified.',
10
+ description: 'Send a new email verification code if the account can be verified.',
10
11
  schema: {
11
12
  body: verifyEmailResendBodySchema,
12
13
  response: {
@@ -14,10 +15,23 @@ export const verifyEmailResendRoute = defineRoute({
14
15
  },
15
16
  },
16
17
  preHandler: createAuthRateLimitPreHandler('email-send'),
18
+ errorHandler: (error) => {
19
+ if (error instanceof EmailChallengeError) {
20
+ throw new ClientError(error.message, `email-challenge-${error.code}`, {
21
+ status: error.code === 'limited' ? 429 : error.code === 'expired' ? 410 : 400,
22
+ ...(error.retryAt ? {details: {retry_at: error.retryAt.toISOString()}} : {}),
23
+ });
24
+ }
25
+ throw error;
26
+ },
17
27
  handler: async (request, reply) => {
18
- const {email} = request.body;
28
+ const {email, challenge_id} = request.body;
19
29
 
20
- const result = await resendEmailVerification({email});
30
+ const result = await resendEmailVerification({
31
+ email,
32
+ challengeId: challenge_id,
33
+ sourceIp: request.ip,
34
+ });
21
35
 
22
36
  reply.code(200).send({
23
37
  next_resend_available_at: result.nextResendAvailableAt.toISOString(),
@@ -2,6 +2,13 @@ import {createApp} from '@shipfox/node-fastify';
2
2
  import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
3
3
  import {buildAuthRoutes} from './index.js';
4
4
 
5
+ const workspaces = {
6
+ listMembershipsForTokenClaims: vi.fn(() => Promise.resolve({memberships: []})),
7
+ preflightInvitationAcceptance: vi.fn(),
8
+ acceptInvitation: vi.fn(),
9
+ requireActiveMembership: vi.fn(),
10
+ };
11
+
5
12
  const disabledPasswordPaths = [
6
13
  '/auth/signup',
7
14
  '/auth/verify-email/confirm',
@@ -14,7 +21,7 @@ const disabledPasswordPaths = [
14
21
 
15
22
  describe('buildAuthRoutes', () => {
16
23
  test('preserves the default route table when password login is enabled', () => {
17
- const routes = buildAuthRoutes(true);
24
+ const routes = buildAuthRoutes(true, workspaces);
18
25
 
19
26
  expect(routes.routes.map((route) => ('path' in route ? route.path : route.prefix))).toEqual([
20
27
  '/signup',
@@ -31,7 +38,7 @@ describe('buildAuthRoutes', () => {
31
38
  });
32
39
 
33
40
  test('keeps only session lifecycle routes when password login is disabled', () => {
34
- const routes = buildAuthRoutes(false);
41
+ const routes = buildAuthRoutes(false, workspaces);
35
42
 
36
43
  expect(routes.routes.map((route) => ('path' in route ? route.path : route.prefix))).toEqual([
37
44
  '/refresh',
@@ -43,7 +50,7 @@ describe('buildAuthRoutes', () => {
43
50
  test('does not register password or email-verification routes when password login is disabled', async () => {
44
51
  const app = await createApp({
45
52
  auth: [createJwtAuthMethod()],
46
- routes: [buildAuthRoutes(false)],
53
+ routes: [buildAuthRoutes(false, workspaces)],
47
54
  swagger: false,
48
55
  });
49
56
 
@@ -1,20 +1,28 @@
1
+ import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
1
2
  import type {RouteGroup} from '@shipfox/node-fastify';
2
- import {config} from '#config.js';
3
3
  import {authCookiePlugin} from '#presentation/auth/refresh-cookie.js';
4
- import {verifyEmailConfirmRoute} from './email-verification/verify-email-confirm.js';
4
+ import {createVerifyEmailConfirmRoute} from './email-verification/verify-email-confirm.js';
5
5
  import {verifyEmailResendRoute} from './email-verification/verify-email-resend.js';
6
6
  import {changePasswordRoute} from './password/change-password.js';
7
- import {passwordResetConfirmRoute} from './password/password-reset-confirm.js';
7
+ import {createPasswordResetConfirmRoute} from './password/password-reset-confirm.js';
8
8
  import {passwordResetRequestRoute} from './password/password-reset-request.js';
9
- import {signupRoute} from './registration/signup.js';
10
- import {loginRoute} from './session/login.js';
9
+ import {createSignupRoute} from './registration/signup.js';
10
+ import {createLoginRoute} from './session/login.js';
11
11
  import {logoutRoute} from './session/logout.js';
12
12
  import {meRoute} from './session/me.js';
13
- import {refreshRoute} from './session/refresh.js';
13
+ import {createRefreshRoute} from './session/refresh.js';
14
14
 
15
- export function buildAuthRoutes(passwordEnabled: boolean): RouteGroup {
15
+ export function buildAuthRoutes(
16
+ passwordEnabled: boolean,
17
+ workspaces: WorkspacesInterModuleClient,
18
+ ): RouteGroup {
16
19
  const passwordRoutes = passwordEnabled
17
- ? [signupRoute, verifyEmailConfirmRoute, verifyEmailResendRoute, loginRoute]
20
+ ? [
21
+ createSignupRoute(workspaces),
22
+ createVerifyEmailConfirmRoute(workspaces),
23
+ verifyEmailResendRoute,
24
+ createLoginRoute(workspaces),
25
+ ]
18
26
  : [];
19
27
 
20
28
  return {
@@ -22,14 +30,16 @@ export function buildAuthRoutes(passwordEnabled: boolean): RouteGroup {
22
30
  plugins: [authCookiePlugin],
23
31
  routes: [
24
32
  ...passwordRoutes,
25
- refreshRoute,
33
+ createRefreshRoute(workspaces),
26
34
  logoutRoute,
27
35
  meRoute,
28
36
  ...(passwordEnabled
29
- ? [changePasswordRoute, passwordResetRequestRoute, passwordResetConfirmRoute]
37
+ ? [
38
+ changePasswordRoute,
39
+ passwordResetRequestRoute,
40
+ createPasswordResetConfirmRoute(workspaces),
41
+ ]
30
42
  : []),
31
43
  ],
32
44
  };
33
45
  }
34
-
35
- export const authRoutes = buildAuthRoutes(config.AUTH_PASSWORD_ENABLED);