@byline/core 5.0.0 → 5.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/dist/@types/site-config.d.ts +3 -1
- package/dist/core.js +5 -1
- package/dist/scheduler/scheduler-boot.test.node.js +14 -0
- package/dist/scheduler/validate-scheduler-config.d.ts +2 -0
- package/dist/scheduler/validate-scheduler-config.js +5 -0
- package/dist/scheduler/validate-scheduler-config.test.node.js +16 -0
- package/dist/validation/password-sign-in.test.node.d.ts +8 -0
- package/dist/validation/password-sign-in.test.node.js +21 -0
- package/dist/validation/shared.d.ts +5 -11
- package/dist/validation/shared.js +7 -1
- package/package.json +8 -8
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type { SessionProvider } from '@byline/auth';
|
|
8
|
+
import type { PasswordSignInProtection, SessionProvider } from '@byline/auth';
|
|
9
9
|
import type { RecurringTaskDefinition } from '../scheduler/types.js';
|
|
10
10
|
import type { SlugifierFn } from '../utils/slugify.js';
|
|
11
11
|
import type { FilenameSlugifierFn } from '../utils/slugify-filename.js';
|
|
@@ -351,6 +351,8 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
|
|
|
351
351
|
* ```
|
|
352
352
|
*/
|
|
353
353
|
sessionProvider?: SessionProvider;
|
|
354
|
+
/** Required by the host password sign-in endpoint; shared limiter and trusted IP resolver. */
|
|
355
|
+
passwordSignIn?: PasswordSignInProtection;
|
|
354
356
|
/**
|
|
355
357
|
* Adapter-built bundle of admin repositories (users / roles /
|
|
356
358
|
* permissions / refresh tokens). Typically constructed via the
|
package/dist/core.js
CHANGED
|
@@ -83,7 +83,11 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
83
83
|
},
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
|
-
validateSchedulerConfig({
|
|
86
|
+
validateSchedulerConfig({
|
|
87
|
+
tasks: configuredTasks,
|
|
88
|
+
adapter: composed.db,
|
|
89
|
+
passwordSignIn: resolvedConfig.passwordSignIn,
|
|
90
|
+
});
|
|
87
91
|
// Freeze a snapshot of the validated task set: a new array of new, frozen
|
|
88
92
|
// objects, then freeze the array itself. Without this, `core.recurringTasks`
|
|
89
93
|
// would hold the caller's own array and definition objects, so a caller
|
|
@@ -26,6 +26,20 @@ const task = defineRecurringTask({
|
|
|
26
26
|
run: async () => { },
|
|
27
27
|
});
|
|
28
28
|
describe('initBylineCore scheduler wiring', () => {
|
|
29
|
+
it('rejects missing password-sign-in cleanup registration before database initialization', async () => {
|
|
30
|
+
const config = serverConfig(testAdapter({}));
|
|
31
|
+
config.passwordSignIn = {
|
|
32
|
+
resolveClientIp: () => '192.0.2.1',
|
|
33
|
+
limiter: {
|
|
34
|
+
requiredCleanupTask: 'auth.sign-in-counters.cleanup',
|
|
35
|
+
acquire: async () => () => { },
|
|
36
|
+
consume: async () => ({ allowed: true, retryAfterSeconds: 0 }),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
await expect(initBylineCore(config, {})).rejects.toThrow(/Register the limiter's cleanupTask/);
|
|
40
|
+
config.recurringTasks = [{ ...task, name: 'auth.sign-in-counters.cleanup' }];
|
|
41
|
+
await expect(initBylineCore(config, {})).rejects.toThrow(/scheduler capability/);
|
|
42
|
+
});
|
|
29
43
|
it('rejects recurring tasks registered against an adapter without the scheduler capability', async () => {
|
|
30
44
|
const config = serverConfig(testAdapter({}));
|
|
31
45
|
config.recurringTasks = [task];
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import type { PasswordSignInProtection } from '@byline/auth';
|
|
8
9
|
import type { ISchedulerStore, RecurringTaskDefinition } from './types.js';
|
|
9
10
|
/**
|
|
10
11
|
* Boot-time gate. Recurring tasks registered against an adapter that does not
|
|
@@ -12,6 +13,7 @@ import type { ISchedulerStore, RecurringTaskDefinition } from './types.js';
|
|
|
12
13
|
* fails loudly at `initBylineCore()` instead.
|
|
13
14
|
*/
|
|
14
15
|
export declare function validateSchedulerConfig(params: {
|
|
16
|
+
passwordSignIn?: PasswordSignInProtection;
|
|
15
17
|
tasks?: readonly RecurringTaskDefinition[];
|
|
16
18
|
adapter: {
|
|
17
19
|
scheduler?: ISchedulerStore;
|
|
@@ -13,6 +13,11 @@ import { validateRecurringTasks } from './validate-tasks.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export function validateSchedulerConfig(params) {
|
|
15
15
|
const tasks = params.tasks ?? [];
|
|
16
|
+
const cleanup = params.passwordSignIn?.limiter.requiredCleanupTask;
|
|
17
|
+
if (cleanup && !tasks.some((task) => task.name === cleanup)) {
|
|
18
|
+
throw new Error(`Password sign-in requires recurring task "${cleanup}". Register the limiter's cleanupTask ` +
|
|
19
|
+
'in recurringTasks and start the host scheduler (or invoke runDueTasks externally).');
|
|
20
|
+
}
|
|
16
21
|
if (tasks.length === 0)
|
|
17
22
|
return;
|
|
18
23
|
validateRecurringTasks(tasks);
|
|
@@ -16,6 +16,22 @@ const task = defineRecurringTask({
|
|
|
16
16
|
});
|
|
17
17
|
const store = {};
|
|
18
18
|
describe('validateSchedulerConfig', () => {
|
|
19
|
+
it('accepts matching cleanup registration and permits self-expiring custom limiters', () => {
|
|
20
|
+
const passwordSignIn = {
|
|
21
|
+
resolveClientIp: () => '192.0.2.1',
|
|
22
|
+
limiter: {
|
|
23
|
+
requiredCleanupTask: task.name,
|
|
24
|
+
acquire: async () => () => { },
|
|
25
|
+
consume: async () => ({ allowed: true, retryAfterSeconds: 0 }),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
expect(() => validateSchedulerConfig({ passwordSignIn, tasks: [task], adapter: { scheduler: store } })).not.toThrow();
|
|
29
|
+
const { requiredCleanupTask: _, ...ttlLimiter } = passwordSignIn.limiter;
|
|
30
|
+
expect(() => validateSchedulerConfig({
|
|
31
|
+
passwordSignIn: { ...passwordSignIn, limiter: ttlLimiter },
|
|
32
|
+
adapter: {},
|
|
33
|
+
})).not.toThrow();
|
|
34
|
+
});
|
|
19
35
|
it('passes when tasks are registered against a scheduler-capable adapter', () => {
|
|
20
36
|
expect(() => validateSchedulerConfig({ tasks: [task], adapter: { scheduler: store } })).not.toThrow();
|
|
21
37
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { passwordSchema, passwordSignInSchema } from './shared.js';
|
|
10
|
+
describe('password sign-in validation', () => {
|
|
11
|
+
it('accepts legacy credentials without imposing enrollment policy', () => {
|
|
12
|
+
expect(passwordSchema.safeParse('legacy').success).toBe(false);
|
|
13
|
+
expect(passwordSignInSchema.parse({ email: ' Admin@Example.com ', password: 'legacy' })).toEqual({ email: 'admin@example.com', password: 'legacy' });
|
|
14
|
+
});
|
|
15
|
+
it('shares the enrollment maximum without trimming passwords', () => {
|
|
16
|
+
expect(passwordSignInSchema.parse({ email: 'a', password: ' '.repeat(128) }).password).toHaveLength(128);
|
|
17
|
+
for (const password of ['', 'a'.repeat(129)])
|
|
18
|
+
expect(passwordSignInSchema.safeParse({ email: 'a', password }).success).toBe(false);
|
|
19
|
+
expect(passwordSignInSchema.safeParse({ email: 'a'.repeat(255), password: 'valid' }).success).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -5,17 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
* Generic Zod helpers shared across packages.
|
|
10
|
-
*
|
|
11
|
-
* Kept separate from `@byline/core/zod-schemas` (which hosts the
|
|
12
|
-
* collection-builder Zod machinery) so the purpose of each subpath
|
|
13
|
-
* stays clear. Ported from `@infonomic/shared` — the organisation's
|
|
14
|
-
* standard schema set, re-declared here so Byline has no external
|
|
15
|
-
* dependency on that package.
|
|
16
|
-
*
|
|
17
|
-
* Pure Zod — no runtime side effects, client-safe.
|
|
18
|
-
*/
|
|
19
8
|
import { z } from 'zod';
|
|
20
9
|
/**
|
|
21
10
|
* Stable error codes emitted by `passwordSchema`. The schema yields
|
|
@@ -53,3 +42,8 @@ export declare const passwordSchema: z.ZodString;
|
|
|
53
42
|
* directly.
|
|
54
43
|
*/
|
|
55
44
|
export declare const uuidSchema: z.ZodUUID;
|
|
45
|
+
/** Login accepts existing passwords regardless of current enrollment complexity policy. */
|
|
46
|
+
export declare const passwordSignInSchema: z.ZodObject<{
|
|
47
|
+
email: z.ZodString;
|
|
48
|
+
password: z.ZodString;
|
|
49
|
+
}, z.core.$strip>;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Pure Zod — no runtime side effects, client-safe.
|
|
18
18
|
*/
|
|
19
|
+
import { MAX_PASSWORD_LENGTH, MAX_SIGN_IN_EMAIL_LENGTH } from '@byline/auth';
|
|
19
20
|
import { z } from 'zod';
|
|
20
21
|
/**
|
|
21
22
|
* Stable error codes emitted by `passwordSchema`. The schema yields
|
|
@@ -49,7 +50,7 @@ export const PASSWORD_ERROR_CODES = {
|
|
|
49
50
|
export const passwordSchema = z
|
|
50
51
|
.string()
|
|
51
52
|
.min(8, PASSWORD_ERROR_CODES.TOO_SHORT)
|
|
52
|
-
.max(
|
|
53
|
+
.max(MAX_PASSWORD_LENGTH, PASSWORD_ERROR_CODES.TOO_LONG)
|
|
53
54
|
.regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/, PASSWORD_ERROR_CODES.COMPLEXITY);
|
|
54
55
|
/**
|
|
55
56
|
* UUID with a descriptive error message. Equivalent to `z.uuid()` but
|
|
@@ -59,3 +60,8 @@ export const passwordSchema = z
|
|
|
59
60
|
export const uuidSchema = z.uuid({
|
|
60
61
|
message: 'Invalid UUID format. Must be a 36-character hex string with hyphens (e.g., 123e4567-e89b-12d3-a456-426614174000)',
|
|
61
62
|
});
|
|
63
|
+
/** Login accepts existing passwords regardless of current enrollment complexity policy. */
|
|
64
|
+
export const passwordSignInSchema = z.object({
|
|
65
|
+
email: z.string().max(MAX_SIGN_IN_EMAIL_LENGTH).trim().min(1).toLowerCase(),
|
|
66
|
+
password: z.string().min(1).max(MAX_PASSWORD_LENGTH),
|
|
67
|
+
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.1.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -86,19 +86,19 @@
|
|
|
86
86
|
"pino": "^10.3.1",
|
|
87
87
|
"sharp": "^0.35.4",
|
|
88
88
|
"uuid": "^14.0.2",
|
|
89
|
-
"zod": "^4.4
|
|
90
|
-
"@byline/auth": "5.
|
|
89
|
+
"zod": "^4.5.4",
|
|
90
|
+
"@byline/auth": "5.1.0"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
|
-
"@biomejs/biome": "2.5.
|
|
94
|
-
"@types/node": "^26.
|
|
93
|
+
"@biomejs/biome": "2.5.12",
|
|
94
|
+
"@types/node": "^26.5.0",
|
|
95
95
|
"chokidar": "^5.0.0",
|
|
96
96
|
"chokidar-cli": "^3.0.0",
|
|
97
97
|
"npm-run-all": "^4.1.5",
|
|
98
|
-
"tsc-alias": "^1.9.
|
|
99
|
-
"tsx": "^4.23.
|
|
98
|
+
"tsc-alias": "^1.9.4",
|
|
99
|
+
"tsx": "^4.23.13",
|
|
100
100
|
"typescript": "^7.0.2",
|
|
101
|
-
"vitest": "^
|
|
101
|
+
"vitest": "^5.0.0"
|
|
102
102
|
},
|
|
103
103
|
"publishConfig": {
|
|
104
104
|
"access": "public",
|