@shipfox/api-auth 10.2.0 → 12.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 (58) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +48 -32
  4. package/dist/core/auth.d.ts.map +1 -1
  5. package/dist/core/auth.js +16 -8
  6. package/dist/core/auth.js.map +1 -1
  7. package/dist/core/job-lease-token.d.ts +1 -1
  8. package/dist/core/job-lease-token.d.ts.map +1 -1
  9. package/dist/core/job-lease-token.js +1 -1
  10. package/dist/core/job-lease-token.js.map +1 -1
  11. package/dist/core/jwt.d.ts +10 -0
  12. package/dist/core/jwt.d.ts.map +1 -1
  13. package/dist/core/jwt.js +3 -2
  14. package/dist/core/jwt.js.map +1 -1
  15. package/dist/core/runner-session-token.d.ts +1 -1
  16. package/dist/core/runner-session-token.d.ts.map +1 -1
  17. package/dist/core/runner-session-token.js +1 -1
  18. package/dist/core/runner-session-token.js.map +1 -1
  19. package/dist/db/rate-limits.d.ts +6 -15
  20. package/dist/db/rate-limits.d.ts.map +1 -1
  21. package/dist/db/rate-limits.js +17 -18
  22. package/dist/db/rate-limits.js.map +1 -1
  23. package/dist/db/refresh-tokens.d.ts +3 -7
  24. package/dist/db/refresh-tokens.d.ts.map +1 -1
  25. package/dist/db/refresh-tokens.js +3 -9
  26. package/dist/db/refresh-tokens.js.map +1 -1
  27. package/dist/presentation/auth/jwt-auth.d.ts +1 -4
  28. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
  29. package/dist/presentation/auth/jwt-auth.js +5 -29
  30. package/dist/presentation/auth/jwt-auth.js.map +1 -1
  31. package/dist/presentation/auth/lease-token-auth.d.ts +1 -1
  32. package/dist/presentation/auth/lease-token-auth.js +1 -1
  33. package/dist/presentation/auth/lease-token-auth.js.map +1 -1
  34. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  35. package/dist/presentation/routes/rate-limit.js +32 -53
  36. package/dist/presentation/routes/rate-limit.js.map +1 -1
  37. package/dist/tsconfig.test.tsbuildinfo +1 -1
  38. package/package.json +13 -13
  39. package/src/core/auth.test.ts +29 -10
  40. package/src/core/auth.ts +27 -12
  41. package/src/core/job-lease-token.ts +1 -1
  42. package/src/core/jwt.test.ts +20 -4
  43. package/src/core/jwt.ts +2 -1
  44. package/src/core/runner-session-token.ts +1 -1
  45. package/src/db/rate-limits.ts +28 -42
  46. package/src/db/refresh-tokens.test.ts +0 -19
  47. package/src/db/refresh-tokens.ts +3 -26
  48. package/src/presentation/auth/jwt-auth.test.ts +33 -8
  49. package/src/presentation/auth/jwt-auth.ts +3 -26
  50. package/src/presentation/auth/lease-token-auth.ts +1 -1
  51. package/src/presentation/routes/administration.test.ts +9 -1
  52. package/src/presentation/routes/password/change-password.test.ts +6 -0
  53. package/src/presentation/routes/password/password-reset-confirm.test.ts +14 -0
  54. package/src/presentation/routes/rate-limit.ts +31 -64
  55. package/src/presentation/routes/registration/signup.test.ts +2 -2
  56. package/src/presentation/routes/session/logout.test.ts +6 -0
  57. package/src/presentation/routes/session/refresh.test.ts +11 -0
  58. package/tsconfig.build.tsbuildinfo +1 -1
@@ -723,7 +723,8 @@ describe('Auth administration routes', () => {
723
723
  headers: {authorization: `Bearer ${target.token}`},
724
724
  });
725
725
  const suspendedLogin = await login(app, {email: target.email, password: target.password});
726
- expect(suspendedMe.statusCode).toBe(401);
726
+ expect(suspendedMe.statusCode).toBe(200);
727
+ expect(suspendedMe.json().user).toMatchObject({id: target.userId, status: 'suspended'});
727
728
  expect(suspendedLogin.statusCode).toBe(401);
728
729
 
729
730
  const suspendedDbUser = (
@@ -841,6 +842,13 @@ describe('Auth administration routes', () => {
841
842
  });
842
843
  expect(secondRefresh.statusCode).toBe(401);
843
844
 
845
+ const revokedAccess = await app.inject({
846
+ method: 'GET',
847
+ url: '/auth/me',
848
+ headers: {authorization: `Bearer ${newLogin.json().token}`},
849
+ });
850
+ expect(revokedAccess.statusCode).toBe(200);
851
+
844
852
  const repeatedRevoke = await app.inject({
845
853
  method: 'POST',
846
854
  url: `/admin/auth/users/${target.userId}/revoke-sessions`,
@@ -48,6 +48,11 @@ describe('POST /auth/change-password', () => {
48
48
  headers: {cookie: cookieHeader(account.refreshCookie)},
49
49
  payload: {},
50
50
  });
51
+ const access = await app.inject({
52
+ method: 'GET',
53
+ url: '/auth/me',
54
+ headers: {authorization: `Bearer ${account.token}`},
55
+ });
51
56
  const oldPasswordValid = await verifyPassword({
52
57
  password: account.password,
53
58
  hash: updatedUser?.hashedPassword ?? '',
@@ -62,6 +67,7 @@ describe('POST /auth/change-password', () => {
62
67
  expect(oldPasswordValid).toBe(false);
63
68
  expect(newPasswordValid).toBe(true);
64
69
  expect(refresh.statusCode).toBe(200);
70
+ expect(access.statusCode).toBe(200);
65
71
  });
66
72
 
67
73
  test('transforms invalid current password into 401', async () => {
@@ -1,6 +1,7 @@
1
1
  import {AUTH_PASSWORD_RESET_SEND_REQUESTED} from '@shipfox/api-auth-dto';
2
2
  import type {FastifyInstance} from 'fastify';
3
3
  import {
4
+ cookieHeader,
4
5
  createAuthTestApp,
5
6
  extractToken,
6
7
  getSetCookie,
@@ -42,12 +43,25 @@ describe('POST /auth/password-reset/confirm', () => {
42
43
  payload: {token: resetToken, new_password: 'reset password is long'},
43
44
  });
44
45
  const loginRes = await login(app, {email: account.email, password: 'reset password is long'});
46
+ const oldRefresh = await app.inject({
47
+ method: 'POST',
48
+ url: '/auth/refresh',
49
+ headers: {cookie: cookieHeader(account.refreshCookie)},
50
+ payload: {},
51
+ });
52
+ const oldAccess = await app.inject({
53
+ method: 'GET',
54
+ url: '/auth/me',
55
+ headers: {authorization: `Bearer ${account.token}`},
56
+ });
45
57
 
46
58
  expect(confirm.statusCode).toBe(200);
47
59
  expect(confirm.json().token).toBeDefined();
48
60
  expect(confirm.json().user.email).toBe(account.email);
49
61
  expect(getSetCookie(confirm)).toContain('shipfox_refresh_token=');
50
62
  expect(loginRes.statusCode).toBe(200);
63
+ expect(oldRefresh.statusCode).toBe(401);
64
+ expect(oldAccess.statusCode).toBe(200);
51
65
  });
52
66
 
53
67
  test('transforms invalid token into 410', async () => {
@@ -1,10 +1,9 @@
1
1
  import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';
2
+ import {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';
2
3
  import {
3
4
  type AuthRateLimitAction,
4
- AuthRateLimitExceededError,
5
5
  type AuthRateLimitPolicy,
6
6
  type AuthRateLimitScope,
7
- AuthRateLimitUnavailableError,
8
7
  checkAuthRateLimit,
9
8
  } from '#core/rate-limit.js';
10
9
 
@@ -49,68 +48,36 @@ async function enforceRateLimit(params: {
49
48
  const policy = policies[params.action][params.scope];
50
49
  if (!policy) return;
51
50
 
52
- try {
53
- await checkAuthRateLimit({
54
- action: params.action,
55
- scope: params.scope,
56
- identifier: params.identifier,
57
- ...policy,
58
- });
59
- } catch (error) {
60
- if (error instanceof AuthRateLimitExceededError) {
61
- params.request.log.warn(
62
- {
63
- action: error.action,
64
- scope: error.scope,
65
- route: routeName(params.request),
66
- retryAfterSeconds: error.retryAfterSeconds,
67
- identifierHmacPrefix: error.identifierHmacPrefix,
68
- },
69
- 'Auth rate limit blocked request',
70
- );
71
- params.reply.header('Retry-After', String(error.retryAfterSeconds));
72
- throw new ClientError('Rate limit exceeded', 'rate-limited', {
73
- status: 429,
74
- details: {retry_after_seconds: error.retryAfterSeconds},
75
- data: {
76
- action: error.action,
77
- scope: error.scope,
78
- route: routeName(params.request),
79
- identifierHmacPrefix: error.identifierHmacPrefix,
80
- },
81
- cause: error,
82
- });
83
- }
84
-
85
- if (error instanceof AuthRateLimitUnavailableError) {
86
- params.request.log.error(
87
- {
88
- action: error.action,
89
- scope: error.scope,
90
- route: routeName(params.request),
91
- identifierHmacPrefix: error.identifierHmacPrefix,
92
- err: error,
93
- },
94
- 'Auth rate limiter unavailable',
95
- );
96
- throw new ClientError(
97
- 'Authentication rate limiter unavailable',
98
- 'auth-rate-limit-unavailable',
99
- {
100
- status: 503,
101
- data: {
102
- action: error.action,
103
- scope: error.scope,
104
- route: routeName(params.request),
105
- identifierHmacPrefix: error.identifierHmacPrefix,
106
- },
107
- cause: error,
108
- },
109
- );
110
- }
111
-
112
- throw error;
113
- }
51
+ await enforceSharedRateLimit({
52
+ request: params.request,
53
+ reply: params.reply,
54
+ check: () =>
55
+ checkAuthRateLimit({
56
+ action: params.action,
57
+ scope: params.scope,
58
+ identifier: params.identifier,
59
+ ...policy,
60
+ }),
61
+ route: routeName(params.request),
62
+ unavailableCode: 'auth-rate-limit-unavailable',
63
+ unavailableMessage: 'Authentication rate limiter unavailable',
64
+ setRetryAfter: (reply, retryAfterSeconds) => {
65
+ reply.header('Retry-After', String(retryAfterSeconds));
66
+ },
67
+ logWarn: (request, context) => {
68
+ request.log.warn(context, 'Auth rate limit blocked request');
69
+ },
70
+ logError: (request, context) => {
71
+ request.log.error(context, 'Auth rate limiter unavailable');
72
+ },
73
+ createClientError: (presentation, cause) =>
74
+ new ClientError(presentation.message, presentation.code, {
75
+ status: presentation.status,
76
+ ...(presentation.details ? {details: presentation.details} : {}),
77
+ data: presentation.data,
78
+ cause,
79
+ }),
80
+ });
114
81
  }
115
82
 
116
83
  export function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {
@@ -165,7 +165,7 @@ describe('POST /auth/signup', () => {
165
165
  alreadyMember: false,
166
166
  }));
167
167
  listMembershipsByUserMock.mockResolvedValueOnce({
168
- memberships: [{workspaceId, role: 'admin' as const}],
168
+ memberships: [{workspaceId, role: 'admin' as const, workspaceStatus: 'active'}],
169
169
  });
170
170
  const res = await app.inject({
171
171
  method: 'POST',
@@ -190,7 +190,7 @@ describe('POST /auth/signup', () => {
190
190
  workspace_id: workspaceId,
191
191
  });
192
192
  expect(body.accept_error).toBeUndefined();
193
- expect(claims.memberships).toEqual([{workspaceId, role: 'admin'}]);
193
+ expect(claims.memberships).toEqual([{workspaceId, role: 'admin', workspaceStatus: 'active'}]);
194
194
  expect(capturedMail()).toHaveLength(0);
195
195
  });
196
196
 
@@ -36,10 +36,16 @@ describe('POST /auth/logout', () => {
36
36
  headers: {cookie: cookieHeader(account.refreshCookie)},
37
37
  payload: {},
38
38
  });
39
+ const access = await app.inject({
40
+ method: 'GET',
41
+ url: '/auth/me',
42
+ headers: {authorization: `Bearer ${account.token}`},
43
+ });
39
44
 
40
45
  expect(res.statusCode).toBe(204);
41
46
  expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=;');
42
47
  expect(refresh.statusCode).toBe(401);
48
+ expect(access.statusCode).toBe(200);
43
49
  });
44
50
 
45
51
  test('returns 204 when the refresh cookie is missing', async () => {
@@ -85,5 +85,16 @@ describe('POST /auth/refresh', () => {
85
85
 
86
86
  expect(res.statusCode).toBe(503);
87
87
  expect(res.json().code).toBe('auth-dependency-unavailable');
88
+ expect(res.headers['set-cookie']).toBeUndefined();
89
+
90
+ const retry = await app.inject({
91
+ method: 'POST',
92
+ url: '/auth/refresh',
93
+ headers: {cookie: cookieHeader(account.refreshCookie)},
94
+ });
95
+
96
+ expect(retry.statusCode).toBe(200);
97
+ expect(retry.json().token).toBeDefined();
98
+ expect(retry.headers['set-cookie']).toContain('shipfox_refresh_token=');
88
99
  });
89
100
  });