@shipfox/api-auth 12.0.0 → 13.1.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 (56) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +15 -2
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +5 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/auth.d.ts.map +1 -1
  8. package/dist/core/auth.js +1 -1
  9. package/dist/core/auth.js.map +1 -1
  10. package/dist/core/errors.d.ts +3 -1
  11. package/dist/core/errors.d.ts.map +1 -1
  12. package/dist/core/errors.js +2 -2
  13. package/dist/core/errors.js.map +1 -1
  14. package/dist/core/jwt.d.ts +2 -2
  15. package/dist/core/ports.d.ts +3 -0
  16. package/dist/core/ports.d.ts.map +1 -1
  17. package/dist/core/ports.js +1 -1
  18. package/dist/core/ports.js.map +1 -1
  19. package/dist/core/rate-limit.d.ts.map +1 -1
  20. package/dist/core/signup-policy.d.ts.map +1 -1
  21. package/dist/core/signup-policy.js +2 -1
  22. package/dist/core/signup-policy.js.map +1 -1
  23. package/dist/db/admin-user-moderation.d.ts.map +1 -1
  24. package/dist/db/db.d.ts +16 -16
  25. package/dist/db/schema/admin-command-results.d.ts +1 -1
  26. package/dist/db/schema/admin-grants.d.ts +1 -1
  27. package/dist/db/schema/outbox.d.ts +1 -1
  28. package/dist/db/schema/password-resets.d.ts +1 -1
  29. package/dist/db/schema/rate-limits.d.ts +1 -1
  30. package/dist/db/schema/refresh-tokens.d.ts +1 -1
  31. package/dist/db/schema/users.d.ts +2 -2
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  36. package/dist/presentation/routes/registration/signup.d.ts +1 -1
  37. package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
  38. package/dist/presentation/routes/registration/signup.js +6 -3
  39. package/dist/presentation/routes/registration/signup.js.map +1 -1
  40. package/dist/tsconfig.test.tsbuildinfo +1 -1
  41. package/package.json +8 -8
  42. package/src/config.test.ts +12 -0
  43. package/src/config.ts +11 -1
  44. package/src/core/auth-default-signup-policy.test.ts +2 -0
  45. package/src/core/auth.test.ts +1 -0
  46. package/src/core/auth.ts +4 -1
  47. package/src/core/errors.ts +6 -1
  48. package/src/core/ports.ts +6 -1
  49. package/src/core/signup-policy.test.ts +10 -2
  50. package/src/core/signup-policy.ts +1 -0
  51. package/src/index.test.ts +1 -0
  52. package/src/index.ts +1 -1
  53. package/src/presentation/routes/registration/signup.test.ts +21 -0
  54. package/src/presentation/routes/registration/signup.ts +3 -5
  55. package/test/routes.ts +6 -2
  56. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-auth",
3
3
  "license": "MIT",
4
- "version": "12.0.0",
4
+ "version": "13.1.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -27,20 +27,20 @@
27
27
  "drizzle-orm": "^0.45.2",
28
28
  "zod": "^4.4.3",
29
29
  "@shipfox/api-common-dto": "12.0.0",
30
- "@shipfox/api-auth-context": "12.0.0",
30
+ "@shipfox/api-auth-context": "12.2.0",
31
31
  "@shipfox/api-auth-dto": "12.0.0",
32
- "@shipfox/api-email-challenges": "1.1.7",
32
+ "@shipfox/api-email-challenges": "1.1.9",
33
33
  "@shipfox/api-workspaces-dto": "12.0.0",
34
34
  "@shipfox/inter-module": "0.2.3",
35
35
  "@shipfox/config": "1.2.4",
36
36
  "@shipfox/node-drizzle": "0.3.5",
37
37
  "@shipfox/node-auth-root-key": "0.2.3",
38
- "@shipfox/node-fastify": "0.4.1",
38
+ "@shipfox/node-fastify": "0.4.2",
39
39
  "@shipfox/node-jwt": "0.4.0",
40
- "@shipfox/node-email": "0.3.4",
41
- "@shipfox/node-mailer": "0.2.4",
42
- "@shipfox/node-module": "1.0.5",
43
- "@shipfox/node-opentelemetry": "0.6.3",
40
+ "@shipfox/node-email": "0.3.5",
41
+ "@shipfox/node-mailer": "0.2.5",
42
+ "@shipfox/node-module": "1.0.6",
43
+ "@shipfox/node-opentelemetry": "0.6.4",
44
44
  "@shipfox/node-outbox": "0.2.6",
45
45
  "@shipfox/node-postgres": "0.5.0",
46
46
  "@shipfox/node-rate-limit": "0.4.0",
@@ -36,4 +36,16 @@ describe('signup gate configuration', () => {
36
36
  expect(config.AUTH_SIGNUP_GATE_ENABLED).toBe(true);
37
37
  expect(config.AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS).toBe('shipfox.io');
38
38
  });
39
+
40
+ test('rejects a Markdown denial message that exceeds the response limit', async () => {
41
+ vi.stubEnv(
42
+ 'AUTH_SIGNUP_NOT_ALLOWED_MESSAGE',
43
+ `${'x'.repeat(499)}[Request access](https://example.test/access)`,
44
+ );
45
+ vi.resetModules();
46
+
47
+ await expect(import('#config.js')).rejects.toThrow(
48
+ 'AUTH_SIGNUP_NOT_ALLOWED_MESSAGE must contain at most 500 characters',
49
+ );
50
+ });
39
51
  });
package/src/config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {bool, createConfig, num, str} from '@shipfox/config';
2
+ import {SIGNUP_DENIAL_MESSAGE_MAX_LENGTH} from '#core/ports.js';
2
3
 
3
4
  export const config = createConfig({
4
5
  ADMIN_BOOTSTRAP_TOKEN: str({
@@ -46,7 +47,7 @@ export const config = createConfig({
46
47
  default: '',
47
48
  }),
48
49
  AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: str({
49
- desc: 'Description returned when the signup gate blocks an account. Optional. The default is This Shipfox deployment does not accept new accounts right now.',
50
+ desc: 'Markdown message returned when the signup gate blocks an account. Use at most 500 characters. Optional. The default is This Shipfox deployment does not accept new accounts right now.',
50
51
  default: undefined,
51
52
  }),
52
53
  CLIENT_BASE_URL: str({
@@ -65,6 +66,15 @@ if (
65
66
  );
66
67
  }
67
68
 
69
+ if (
70
+ config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE !== undefined &&
71
+ config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE.length > SIGNUP_DENIAL_MESSAGE_MAX_LENGTH
72
+ ) {
73
+ throw new Error(
74
+ `AUTH_SIGNUP_NOT_ALLOWED_MESSAGE must contain at most ${SIGNUP_DENIAL_MESSAGE_MAX_LENGTH} characters.`,
75
+ );
76
+ }
77
+
68
78
  function hasSignupAllowlistEntry(value: string): boolean {
69
79
  return value.split(',').some((entry) => entry.trim().length > 0);
70
80
  }
@@ -28,6 +28,7 @@ describe('low-level account creation defaults', () => {
28
28
 
29
29
  await expect(signup({email, password: 'correct horse battery staple'})).rejects.toMatchObject({
30
30
  name: 'SignupNotAllowedError',
31
+ format: 'markdown',
31
32
  });
32
33
  });
33
34
 
@@ -44,6 +45,7 @@ describe('low-level account creation defaults', () => {
44
45
 
45
46
  await expect(provisionUser({email})).rejects.toMatchObject({
46
47
  name: 'SignupNotAllowedError',
48
+ format: 'markdown',
47
49
  });
48
50
  });
49
51
 
@@ -202,6 +202,7 @@ describe('auth core', () => {
202
202
  expect.objectContaining({
203
203
  name: 'SignupNotAllowedError',
204
204
  message: 'This Shipfox deployment does not accept new accounts right now.',
205
+ format: undefined,
205
206
  }),
206
207
  );
207
208
  expect(await findUserByEmail({email})).toBeUndefined();
package/src/core/auth.ts CHANGED
@@ -71,7 +71,10 @@ async function assertSignupAllowed(params: {
71
71
  });
72
72
 
73
73
  if (!result.allowed) {
74
- throw new SignupNotAllowedError(result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE);
74
+ throw new SignupNotAllowedError(
75
+ result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE,
76
+ result.format,
77
+ );
75
78
  }
76
79
  }
77
80
 
@@ -1,3 +1,5 @@
1
+ import type {SignupDenialMessageFormat} from './ports.js';
2
+
1
3
  export class WorkspaceNotFoundError extends Error {
2
4
  constructor(workspaceId: string) {
3
5
  super(`Workspace not found: ${workspaceId}`);
@@ -48,7 +50,10 @@ export class EmailTakenError extends Error {
48
50
  }
49
51
 
50
52
  export class SignupNotAllowedError extends Error {
51
- constructor(message: string) {
53
+ constructor(
54
+ message: string,
55
+ readonly format?: SignupDenialMessageFormat,
56
+ ) {
52
57
  super(message);
53
58
  this.name = 'SignupNotAllowedError';
54
59
  }
package/src/core/ports.ts CHANGED
@@ -1,7 +1,12 @@
1
+ export const SIGNUP_DENIAL_MESSAGE_MAX_LENGTH = 500;
2
+ export type SignupDenialMessageFormat = 'markdown';
3
+
1
4
  export interface SignupPolicy {
2
5
  isSignupAllowed(params: {
3
6
  email: string;
4
7
  emailVerified: boolean;
5
8
  source: string;
6
- }): Promise<{allowed: true} | {allowed: false; message?: string}>;
9
+ }): Promise<
10
+ {allowed: true} | {allowed: false; message?: string; format?: SignupDenialMessageFormat}
11
+ >;
7
12
  }
@@ -47,7 +47,11 @@ describe('createEnvironmentSignupPolicy', () => {
47
47
  emailVerified: true,
48
48
  source: 'oauth-google',
49
49
  }),
50
- ).resolves.toEqual({allowed: false, message: DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE});
50
+ ).resolves.toEqual({
51
+ allowed: false,
52
+ message: DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE,
53
+ format: 'markdown',
54
+ });
51
55
  });
52
56
 
53
57
  test('returns the configured denial message for an unmatched address', async () => {
@@ -60,7 +64,11 @@ describe('createEnvironmentSignupPolicy', () => {
60
64
  emailVerified: true,
61
65
  source: 'oauth-google',
62
66
  }),
63
- ).resolves.toEqual({allowed: false, message: 'Ask your administrator for access.'});
67
+ ).resolves.toEqual({
68
+ allowed: false,
69
+ message: 'Ask your administrator for access.',
70
+ format: 'markdown',
71
+ });
64
72
  });
65
73
 
66
74
  test('allows every address when the gate is disabled', async () => {
@@ -28,6 +28,7 @@ export function createEnvironmentSignupPolicy(): SignupPolicy {
28
28
  return Promise.resolve({
29
29
  allowed: false,
30
30
  message: config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE,
31
+ format: 'markdown',
31
32
  });
32
33
  },
33
34
  };
package/src/index.test.ts CHANGED
@@ -94,6 +94,7 @@ describe('authModule', () => {
94
94
  ).resolves.toEqual({
95
95
  allowed: false,
96
96
  message: 'This Shipfox deployment does not accept new accounts right now.',
97
+ format: 'markdown',
97
98
  });
98
99
  });
99
100
 
package/src/index.ts CHANGED
@@ -75,7 +75,7 @@ export {
75
75
  jobLeaseParamsFrom,
76
76
  verifyJobLeaseToken,
77
77
  } from '#core/job-lease-token.js';
78
- export type {SignupPolicy} from '#core/ports.js';
78
+ export type {SignupDenialMessageFormat, SignupPolicy} from '#core/ports.js';
79
79
  export {
80
80
  issueRunnerSessionToken,
81
81
  verifyRunnerSessionToken,
@@ -53,6 +53,27 @@ describe('POST /auth/signup', () => {
53
53
  });
54
54
  });
55
55
 
56
+ test('declares Markdown denial messages in the client error details', () => {
57
+ const errorHandler = createSignupRoute({} as WorkspacesInterModuleClient).errorHandler as (
58
+ error: unknown,
59
+ ) => never;
60
+ const message = 'Read the [access policy](https://example.test/access).';
61
+ let mapped: unknown;
62
+
63
+ try {
64
+ errorHandler(new SignupNotAllowedError(message, 'markdown'));
65
+ } catch (error) {
66
+ mapped = error;
67
+ }
68
+
69
+ expect(mapped).toMatchObject({
70
+ message,
71
+ status: 403,
72
+ code: 'signup-not-allowed',
73
+ details: {message, format: 'markdown'},
74
+ });
75
+ });
76
+
56
77
  afterAll(async () => {
57
78
  await app.close();
58
79
  });
@@ -11,12 +11,10 @@ import {
11
11
  TokenExpiredError,
12
12
  TokenInvalidError,
13
13
  } from '#core/errors.js';
14
- import type {SignupPolicy} from '#core/ports.js';
14
+ import {SIGNUP_DENIAL_MESSAGE_MAX_LENGTH, type SignupPolicy} from '#core/ports.js';
15
15
  import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
16
16
  import {toUserDto} from '#presentation/dto/user.js';
17
17
 
18
- const SIGNUP_NOT_ALLOWED_MESSAGE_MAX_LENGTH = 500;
19
-
20
18
  export function createSignupRoute(
21
19
  workspaces: WorkspacesInterModuleClient,
22
20
  signupPolicy?: SignupPolicy,
@@ -43,10 +41,10 @@ export function createSignupRoute(
43
41
  throw new ClientError('Email already registered', 'email-taken', {status: 409});
44
42
  }
45
43
  if (error instanceof SignupNotAllowedError) {
46
- const message = error.message.slice(0, SIGNUP_NOT_ALLOWED_MESSAGE_MAX_LENGTH);
44
+ const message = error.message.slice(0, SIGNUP_DENIAL_MESSAGE_MAX_LENGTH);
47
45
  throw new ClientError(message, 'signup-not-allowed', {
48
46
  status: 403,
49
- details: {message},
47
+ details: {message, ...(error.format ? {format: error.format} : {})},
50
48
  });
51
49
  }
52
50
  if (error instanceof TokenInvalidError) {
package/test/routes.ts CHANGED
@@ -73,6 +73,7 @@ vi.mock('@shipfox/node-mailer', () => ({mailer: testConfig.mailer}));
73
73
  const CODE_RE = /\b\d{8}\b/u;
74
74
  const TOKEN_RE = /token=([\w\-_=]+)/;
75
75
  type AuthEmailEventType = typeof AUTH_PASSWORD_RESET_SEND_REQUESTED;
76
+ type InjectResponse = Awaited<ReturnType<FastifyInstance['inject']>>;
76
77
 
77
78
  export const ROUTE_TEST_SECRET = userAccessTokenKey();
78
79
  export const acceptWorkspaceInvitationMock: ReturnType<typeof vi.fn> =
@@ -172,7 +173,7 @@ export async function createAuthTestApp(params?: {
172
173
  export async function signup(
173
174
  app: FastifyInstance,
174
175
  params: {email: string; password: string; name?: string},
175
- ) {
176
+ ): Promise<InjectResponse> {
176
177
  const result = await app.inject({
177
178
  method: 'POST',
178
179
  url: '/auth/signup',
@@ -198,7 +199,10 @@ export async function verifyEmail(
198
199
  expect(res.statusCode).toBe(200);
199
200
  }
200
201
 
201
- export async function login(app: FastifyInstance, params: {email: string; password: string}) {
202
+ export async function login(
203
+ app: FastifyInstance,
204
+ params: {email: string; password: string},
205
+ ): Promise<InjectResponse> {
202
206
  return await app.inject({
203
207
  method: 'POST',
204
208
  url: '/auth/login',