@logto/core-kit 2.10.0 → 2.11.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/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/username-policy.d.ts +111 -0
- package/lib/username-policy.js +85 -0
- package/package.json +4 -4
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Per-tenant username policy: case sensitivity, length bounds, and allowed character classes. */
|
|
3
|
+
export type UsernamePolicy = {
|
|
4
|
+
caseSensitive: boolean;
|
|
5
|
+
/** Integer in [1, 128], inclusive. */
|
|
6
|
+
minLength: number;
|
|
7
|
+
/** Integer in [1, 128], inclusive. Must be >= minLength. */
|
|
8
|
+
maxLength: number;
|
|
9
|
+
/** At least one of lowercase, uppercase, or underscore must be enabled (see usernamePolicyGuard). */
|
|
10
|
+
allowedChars: {
|
|
11
|
+
lowercase: boolean;
|
|
12
|
+
uppercase: boolean;
|
|
13
|
+
numbers: boolean;
|
|
14
|
+
underscore: boolean;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare const usernamePolicyGuard: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
18
|
+
caseSensitive: z.ZodBoolean;
|
|
19
|
+
minLength: z.ZodNumber;
|
|
20
|
+
maxLength: z.ZodNumber;
|
|
21
|
+
allowedChars: z.ZodObject<{
|
|
22
|
+
lowercase: z.ZodBoolean;
|
|
23
|
+
uppercase: z.ZodBoolean;
|
|
24
|
+
numbers: z.ZodBoolean;
|
|
25
|
+
underscore: z.ZodBoolean;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
lowercase: boolean;
|
|
28
|
+
uppercase: boolean;
|
|
29
|
+
numbers: boolean;
|
|
30
|
+
underscore: boolean;
|
|
31
|
+
}, {
|
|
32
|
+
lowercase: boolean;
|
|
33
|
+
uppercase: boolean;
|
|
34
|
+
numbers: boolean;
|
|
35
|
+
underscore: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
}, "strip", z.ZodTypeAny, {
|
|
38
|
+
caseSensitive: boolean;
|
|
39
|
+
minLength: number;
|
|
40
|
+
maxLength: number;
|
|
41
|
+
allowedChars: {
|
|
42
|
+
lowercase: boolean;
|
|
43
|
+
uppercase: boolean;
|
|
44
|
+
numbers: boolean;
|
|
45
|
+
underscore: boolean;
|
|
46
|
+
};
|
|
47
|
+
}, {
|
|
48
|
+
caseSensitive: boolean;
|
|
49
|
+
minLength: number;
|
|
50
|
+
maxLength: number;
|
|
51
|
+
allowedChars: {
|
|
52
|
+
lowercase: boolean;
|
|
53
|
+
uppercase: boolean;
|
|
54
|
+
numbers: boolean;
|
|
55
|
+
underscore: boolean;
|
|
56
|
+
};
|
|
57
|
+
}>, {
|
|
58
|
+
caseSensitive: boolean;
|
|
59
|
+
minLength: number;
|
|
60
|
+
maxLength: number;
|
|
61
|
+
allowedChars: {
|
|
62
|
+
lowercase: boolean;
|
|
63
|
+
uppercase: boolean;
|
|
64
|
+
numbers: boolean;
|
|
65
|
+
underscore: boolean;
|
|
66
|
+
};
|
|
67
|
+
}, {
|
|
68
|
+
caseSensitive: boolean;
|
|
69
|
+
minLength: number;
|
|
70
|
+
maxLength: number;
|
|
71
|
+
allowedChars: {
|
|
72
|
+
lowercase: boolean;
|
|
73
|
+
uppercase: boolean;
|
|
74
|
+
numbers: boolean;
|
|
75
|
+
underscore: boolean;
|
|
76
|
+
};
|
|
77
|
+
}>, {
|
|
78
|
+
caseSensitive: boolean;
|
|
79
|
+
minLength: number;
|
|
80
|
+
maxLength: number;
|
|
81
|
+
allowedChars: {
|
|
82
|
+
lowercase: boolean;
|
|
83
|
+
uppercase: boolean;
|
|
84
|
+
numbers: boolean;
|
|
85
|
+
underscore: boolean;
|
|
86
|
+
};
|
|
87
|
+
}, {
|
|
88
|
+
caseSensitive: boolean;
|
|
89
|
+
minLength: number;
|
|
90
|
+
maxLength: number;
|
|
91
|
+
allowedChars: {
|
|
92
|
+
lowercase: boolean;
|
|
93
|
+
uppercase: boolean;
|
|
94
|
+
numbers: boolean;
|
|
95
|
+
underscore: boolean;
|
|
96
|
+
};
|
|
97
|
+
}>;
|
|
98
|
+
/**
|
|
99
|
+
* Mirrors current username behavior so the policy is a no-op when unset: `usernameRegEx` charset
|
|
100
|
+
* (all classes), length 1-128 (`z.string().min(1)` + `users.username varchar(128)`), case-sensitive
|
|
101
|
+
* (the `CASE_SENSITIVE_USERNAME` env default).
|
|
102
|
+
*/
|
|
103
|
+
export declare const defaultUsernamePolicy: UsernamePolicy;
|
|
104
|
+
export type UsernameViolation = 'required' | 'starts_with_number' | 'invalid_charset_hard' | 'too_short' | 'too_long' | 'uppercase_not_allowed' | 'lowercase_not_allowed' | 'numbers_not_allowed' | 'underscore_not_allowed';
|
|
105
|
+
/**
|
|
106
|
+
* The always-on baseline, independent of any per-tenant policy: non-empty, no leading digit, and
|
|
107
|
+
* matching the existing `usernameRegEx` charset. Applies to admin writes too.
|
|
108
|
+
*/
|
|
109
|
+
export declare const validateUsernameHardFloor: (username: string) => UsernameViolation | undefined;
|
|
110
|
+
/** Returns the first violation against the hard floor then the per-tenant policy, or undefined. */
|
|
111
|
+
export declare const validateUsernameAgainstPolicy: (username: string, policy: UsernamePolicy) => UsernameViolation | undefined;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { usernameRegEx } from './regex.js';
|
|
3
|
+
export const usernamePolicyGuard = z
|
|
4
|
+
.object({
|
|
5
|
+
caseSensitive: z.boolean(),
|
|
6
|
+
minLength: z.number().int().min(1).max(128),
|
|
7
|
+
maxLength: z.number().int().min(1).max(128),
|
|
8
|
+
allowedChars: z.object({
|
|
9
|
+
lowercase: z.boolean(),
|
|
10
|
+
uppercase: z.boolean(),
|
|
11
|
+
numbers: z.boolean(),
|
|
12
|
+
underscore: z.boolean(),
|
|
13
|
+
}),
|
|
14
|
+
})
|
|
15
|
+
.refine((policy) => policy.minLength <= policy.maxLength, {
|
|
16
|
+
message: 'Minimum length cannot exceed maximum length.',
|
|
17
|
+
path: ['maxLength'],
|
|
18
|
+
})
|
|
19
|
+
// Messages are full sentences on purpose: koaGuard returns them verbatim in the 400 response,
|
|
20
|
+
// so a Management API caller posting an invalid policy reads exactly why.
|
|
21
|
+
.refine((policy) => policy.allowedChars.lowercase ||
|
|
22
|
+
policy.allowedChars.uppercase ||
|
|
23
|
+
policy.allowedChars.underscore, {
|
|
24
|
+
message: 'At least one of lowercase, uppercase, or underscore must be enabled. Usernames cannot start with a number, so numbers alone are not allowed.',
|
|
25
|
+
path: ['allowedChars'],
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Mirrors current username behavior so the policy is a no-op when unset: `usernameRegEx` charset
|
|
29
|
+
* (all classes), length 1-128 (`z.string().min(1)` + `users.username varchar(128)`), case-sensitive
|
|
30
|
+
* (the `CASE_SENSITIVE_USERNAME` env default).
|
|
31
|
+
*/
|
|
32
|
+
export const defaultUsernamePolicy = Object.freeze({
|
|
33
|
+
caseSensitive: true,
|
|
34
|
+
minLength: 1,
|
|
35
|
+
maxLength: 128,
|
|
36
|
+
allowedChars: Object.freeze({
|
|
37
|
+
lowercase: true,
|
|
38
|
+
uppercase: true,
|
|
39
|
+
numbers: true,
|
|
40
|
+
underscore: true,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* The always-on baseline, independent of any per-tenant policy: non-empty, no leading digit, and
|
|
45
|
+
* matching the existing `usernameRegEx` charset. Applies to admin writes too.
|
|
46
|
+
*/
|
|
47
|
+
export const validateUsernameHardFloor = (username) => {
|
|
48
|
+
if (username.length === 0) {
|
|
49
|
+
return 'required';
|
|
50
|
+
}
|
|
51
|
+
if (/^\d/.test(username)) {
|
|
52
|
+
return 'starts_with_number';
|
|
53
|
+
}
|
|
54
|
+
if (!usernameRegEx.test(username)) {
|
|
55
|
+
return 'invalid_charset_hard';
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const checkAllowedChars = (username, allowedChars) => {
|
|
59
|
+
if (!allowedChars.uppercase && /[A-Z]/.test(username)) {
|
|
60
|
+
return 'uppercase_not_allowed';
|
|
61
|
+
}
|
|
62
|
+
if (!allowedChars.lowercase && /[a-z]/.test(username)) {
|
|
63
|
+
return 'lowercase_not_allowed';
|
|
64
|
+
}
|
|
65
|
+
if (!allowedChars.numbers && /\d/.test(username)) {
|
|
66
|
+
return 'numbers_not_allowed';
|
|
67
|
+
}
|
|
68
|
+
if (!allowedChars.underscore && username.includes('_')) {
|
|
69
|
+
return 'underscore_not_allowed';
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/** Returns the first violation against the hard floor then the per-tenant policy, or undefined. */
|
|
73
|
+
export const validateUsernameAgainstPolicy = (username, policy) => {
|
|
74
|
+
const hardFloor = validateUsernameHardFloor(username);
|
|
75
|
+
if (hardFloor) {
|
|
76
|
+
return hardFloor;
|
|
77
|
+
}
|
|
78
|
+
if (username.length < policy.minLength) {
|
|
79
|
+
return 'too_short';
|
|
80
|
+
}
|
|
81
|
+
if (username.length > policy.maxLength) {
|
|
82
|
+
return 'too_long';
|
|
83
|
+
}
|
|
84
|
+
return checkAllowedChars(username, policy.allowedChars);
|
|
85
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/core-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
5
5
|
"homepage": "https://github.com/logto-io/toolkit#readme",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@silverhand/essentials": "^2.9.1",
|
|
37
37
|
"color": "^4.2.3",
|
|
38
38
|
"@logto/language-kit": "^1.3.0",
|
|
39
|
-
"@logto/shared": "^3.4.
|
|
39
|
+
"@logto/shared": "^3.4.1"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
42
|
"zod": "3.24.3"
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
"@types/color": "^4.0.0",
|
|
50
50
|
"@types/node": "^22.14.0",
|
|
51
51
|
"@types/react": "^18.3.3",
|
|
52
|
-
"@vitest/coverage-v8": "^
|
|
52
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
53
53
|
"eslint": "^8.56.0",
|
|
54
54
|
"lint-staged": "^15.0.0",
|
|
55
55
|
"postcss": "^8.4.31",
|
|
56
56
|
"prettier": "^3.5.3",
|
|
57
57
|
"stylelint": "^15.0.0",
|
|
58
58
|
"typescript": "^5.5.3",
|
|
59
|
-
"vitest": "^
|
|
59
|
+
"vitest": "^4.1.8"
|
|
60
60
|
},
|
|
61
61
|
"eslintConfig": {
|
|
62
62
|
"extends": "@silverhand"
|