@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +15 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +1 -1
- package/dist/core/auth.js.map +1 -1
- package/dist/core/errors.d.ts +3 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +2 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/jwt.d.ts +2 -2
- package/dist/core/ports.d.ts +3 -0
- package/dist/core/ports.d.ts.map +1 -1
- package/dist/core/ports.js +1 -1
- package/dist/core/ports.js.map +1 -1
- package/dist/core/rate-limit.d.ts.map +1 -1
- package/dist/core/signup-policy.d.ts.map +1 -1
- package/dist/core/signup-policy.js +2 -1
- package/dist/core/signup-policy.js.map +1 -1
- package/dist/db/admin-user-moderation.d.ts.map +1 -1
- package/dist/db/db.d.ts +16 -16
- package/dist/db/schema/admin-command-results.d.ts +1 -1
- package/dist/db/schema/admin-grants.d.ts +1 -1
- package/dist/db/schema/outbox.d.ts +1 -1
- package/dist/db/schema/password-resets.d.ts +1 -1
- package/dist/db/schema/rate-limits.d.ts +1 -1
- package/dist/db/schema/refresh-tokens.d.ts +1 -1
- package/dist/db/schema/users.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/registration/signup.d.ts +1 -1
- package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
- package/dist/presentation/routes/registration/signup.js +6 -3
- package/dist/presentation/routes/registration/signup.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/config.test.ts +12 -0
- package/src/config.ts +11 -1
- package/src/core/auth-default-signup-policy.test.ts +2 -0
- package/src/core/auth.test.ts +1 -0
- package/src/core/auth.ts +4 -1
- package/src/core/errors.ts +6 -1
- package/src/core/ports.ts +6 -1
- package/src/core/signup-policy.test.ts +10 -2
- package/src/core/signup-policy.ts +1 -0
- package/src/index.test.ts +1 -0
- package/src/index.ts +1 -1
- package/src/presentation/routes/registration/signup.test.ts +21 -0
- package/src/presentation/routes/registration/signup.ts +3 -5
- package/test/routes.ts +6 -2
- 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": "
|
|
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.
|
|
30
|
+
"@shipfox/api-auth-context": "12.2.0",
|
|
31
31
|
"@shipfox/api-auth-dto": "12.0.0",
|
|
32
|
-
"@shipfox/api-email-challenges": "1.1.
|
|
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.
|
|
38
|
+
"@shipfox/node-fastify": "0.4.2",
|
|
39
39
|
"@shipfox/node-jwt": "0.4.0",
|
|
40
|
-
"@shipfox/node-email": "0.3.
|
|
41
|
-
"@shipfox/node-mailer": "0.2.
|
|
42
|
-
"@shipfox/node-module": "1.0.
|
|
43
|
-
"@shipfox/node-opentelemetry": "0.6.
|
|
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",
|
package/src/config.test.ts
CHANGED
|
@@ -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: '
|
|
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
|
|
package/src/core/auth.test.ts
CHANGED
|
@@ -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(
|
|
74
|
+
throw new SignupNotAllowedError(
|
|
75
|
+
result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE,
|
|
76
|
+
result.format,
|
|
77
|
+
);
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
|
package/src/core/errors.ts
CHANGED
|
@@ -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(
|
|
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<
|
|
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({
|
|
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({
|
|
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 () => {
|
package/src/index.test.ts
CHANGED
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
|
|
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,
|
|
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(
|
|
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',
|