@logto/core-kit 2.0.0 → 2.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/LICENSE CHANGED
@@ -1,9 +1,3 @@
1
- Portions of this software are licensed as follows:
2
-
3
- * All content that resides under the "packages/cloud" directory of this repository, if that directory exists, is licensed under the license defined in "packages/cloud/LICENSE" (Elastic-2.0).
4
- * All third party components incorporated into this software are licensed under the original license provided by the owner of the applicable component.
5
- * Content outside of the above mentioned directories or restrictions above is available under the "MPL-2.0" license as defined below.
6
-
7
1
  Mozilla Public License Version 2.0
8
2
  ==================================
9
3
 
package/lib/http.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const httpCodeToMessage: Record<number, string>;
package/lib/http.js ADDED
@@ -0,0 +1,44 @@
1
+ export const httpCodeToMessage = Object.freeze({
2
+ 200: 'OK',
3
+ 201: 'Created',
4
+ 202: 'Accepted',
5
+ 203: 'Non-Authoritative Information',
6
+ 204: 'No Content',
7
+ 205: 'Reset Content',
8
+ 206: 'Partial Content',
9
+ 300: 'Multiple Choices',
10
+ 301: 'Moved Permanently',
11
+ 302: 'Found',
12
+ 303: 'See Other',
13
+ 304: 'Not Modified',
14
+ 305: 'Use Proxy',
15
+ 306: 'Unused',
16
+ 307: 'Temporary Redirect',
17
+ 400: 'Bad Request',
18
+ 401: 'Unauthorized',
19
+ 402: 'Payment Required',
20
+ 403: 'Forbidden',
21
+ 404: 'Not Found',
22
+ 405: 'Method Not Allowed',
23
+ 406: 'Not Acceptable',
24
+ 407: 'Proxy Authentication Required',
25
+ 408: 'Request Timeout',
26
+ 409: 'Conflict',
27
+ 410: 'Gone',
28
+ 411: 'Length Required',
29
+ 412: 'Precondition Required',
30
+ 413: 'Request Entry Too Large',
31
+ 414: 'Request-URI Too Long',
32
+ 415: 'Unsupported Media Type',
33
+ 416: 'Requested Range Not Satisfiable',
34
+ 417: 'Expectation Failed',
35
+ 418: "I'm a teapot",
36
+ 422: 'Unprocessable Content',
37
+ 429: 'Too Many Requests',
38
+ 500: 'Internal Server Error',
39
+ 501: 'Not Implemented',
40
+ 502: 'Bad Gateway',
41
+ 503: 'Service Unavailable',
42
+ 504: 'Gateway Timeout',
43
+ 505: 'HTTP Version Not Supported',
44
+ });
package/lib/index.d.ts CHANGED
@@ -2,3 +2,5 @@ export * from './utils/index.js';
2
2
  export * from './regex.js';
3
3
  export * from './scope.js';
4
4
  export * from './models/index.js';
5
+ export * from './http.js';
6
+ export * from './password-policy.js';
package/lib/index.js CHANGED
@@ -2,3 +2,5 @@ export * from './utils/index.js';
2
2
  export * from './regex.js';
3
3
  export * from './scope.js';
4
4
  export * from './models/index.js';
5
+ export * from './http.js';
6
+ export * from './password-policy.js';
@@ -0,0 +1,226 @@
1
+ import { type DeepPartial } from '@silverhand/essentials';
2
+ import { z } from 'zod';
3
+ /** Password policy configuration type. */
4
+ export type PasswordPolicy = {
5
+ /** Policy about password length. */
6
+ length: {
7
+ /** Minimum password length. */
8
+ min: number;
9
+ /** Maximum password length. */
10
+ max: number;
11
+ };
12
+ /**
13
+ * Policy about password character types. Four types of characters are supported:
14
+ *
15
+ * - Lowercase letters (a-z).
16
+ * - Uppercase letters (A-Z).
17
+ * - Digits (0-9).
18
+ * - Symbols ({@link PasswordPolicyChecker.symbols}).
19
+ */
20
+ characterTypes: {
21
+ /** Minimum number of character types. Range: 1-4. */
22
+ min: number;
23
+ };
24
+ /** Policy about what passwords to reject. */
25
+ rejects: {
26
+ /** Whether to reject passwords that are pwned. */
27
+ pwned: boolean;
28
+ /** Whether to reject passwords that like '123456' or 'aaaaaa'. */
29
+ repetitionAndSequence: boolean;
30
+ /** Whether to reject passwords that include current user information. */
31
+ userInfo: boolean;
32
+ /** Whether to reject passwords that include specific words. */
33
+ words: string[];
34
+ };
35
+ };
36
+ /** Password policy configuration guard. */
37
+ export declare const passwordPolicyGuard: z.ZodObject<{
38
+ length: z.ZodDefault<z.ZodObject<{
39
+ min: z.ZodDefault<z.ZodNumber>;
40
+ max: z.ZodDefault<z.ZodNumber>;
41
+ }, "strip", z.ZodTypeAny, {
42
+ min: number;
43
+ max: number;
44
+ }, {
45
+ min?: number | undefined;
46
+ max?: number | undefined;
47
+ }>>;
48
+ characterTypes: z.ZodDefault<z.ZodObject<{
49
+ min: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
50
+ }, "strip", z.ZodTypeAny, {
51
+ min: number;
52
+ }, {
53
+ min?: number | undefined;
54
+ }>>;
55
+ rejects: z.ZodDefault<z.ZodObject<{
56
+ pwned: z.ZodDefault<z.ZodBoolean>;
57
+ repetitionAndSequence: z.ZodDefault<z.ZodBoolean>;
58
+ userInfo: z.ZodDefault<z.ZodBoolean>;
59
+ words: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
60
+ }, "strip", z.ZodTypeAny, {
61
+ pwned: boolean;
62
+ repetitionAndSequence: boolean;
63
+ userInfo: boolean;
64
+ words: string[];
65
+ }, {
66
+ pwned?: boolean | undefined;
67
+ repetitionAndSequence?: boolean | undefined;
68
+ userInfo?: boolean | undefined;
69
+ words?: string[] | undefined;
70
+ }>>;
71
+ }, "strip", z.ZodTypeAny, {
72
+ length: {
73
+ min: number;
74
+ max: number;
75
+ };
76
+ characterTypes: {
77
+ min: number;
78
+ };
79
+ rejects: {
80
+ pwned: boolean;
81
+ repetitionAndSequence: boolean;
82
+ userInfo: boolean;
83
+ words: string[];
84
+ };
85
+ }, {
86
+ length?: {
87
+ min?: number | undefined;
88
+ max?: number | undefined;
89
+ } | undefined;
90
+ characterTypes?: {
91
+ min?: number | undefined;
92
+ } | undefined;
93
+ rejects?: {
94
+ pwned?: boolean | undefined;
95
+ repetitionAndSequence?: boolean | undefined;
96
+ userInfo?: boolean | undefined;
97
+ words?: string[] | undefined;
98
+ } | undefined;
99
+ }>;
100
+ /** The code of why a password is rejected. */
101
+ export type PasswordRejectionCode = 'too_short' | 'too_long' | 'character_types' | 'unsupported_characters' | 'pwned' | 'restricted.repetition' | 'restricted.sequence' | 'restricted.user_info' | 'restricted.words';
102
+ /** A password issue that does not meet the policy. */
103
+ export type PasswordIssue<Code extends PasswordRejectionCode = PasswordRejectionCode> = {
104
+ /** Issue code. */
105
+ code: `password_rejected.${Code}`;
106
+ /** Interpolation data for the issue message. */
107
+ interpolation?: Record<string, unknown>;
108
+ };
109
+ /** User information to check. */
110
+ export type UserInfo = Partial<{
111
+ name: string;
112
+ username: string;
113
+ email: string;
114
+ phoneNumber: string;
115
+ }>;
116
+ /**
117
+ * The class for checking if a password meets the policy. The policy is defined as
118
+ * {@link PasswordPolicy}.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const checker = new PasswordPolicyChecker({
123
+ * length: { min: 8, max: 256 },
124
+ * characterTypes: { min: 2 },
125
+ * rejects: { pwned: true, repetitionAndSequence: true, words: [] },
126
+ * });
127
+ *
128
+ * const issues = await checker.check('123456');
129
+ * console.log(issues);
130
+ * // [
131
+ * // { code: 'password_rejected.too_short' },
132
+ * // { code: 'password_rejected.character_types', interpolation: { min: 2 } },
133
+ * // { code: 'password_rejected.pwned' },
134
+ * // { code: 'password_rejected.restricted.sequence' },
135
+ * // ]
136
+ * ```
137
+ */
138
+ export declare class PasswordPolicyChecker {
139
+ /** The Web Crypto API to use. By default, the global `crypto.subtle` will be used. */
140
+ protected readonly subtle: SubtleCrypto;
141
+ static symbols: "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ";
142
+ /** A set of characters that are considered as sequential. */
143
+ static sequence: readonly ["0123456789", "abcdefghijklmnopqrstuvwxyz", "qwertyuiop", "asdfghjkl", "zxcvbnm", "1qaz", "2wsx", "3edc", "4rfv", "5tgb", "6yhn", "7ujm", "8ik", "9ol"];
144
+ /** The length threshold for checking repetition and sequence. */
145
+ static repetitionAndSequenceThreshold: 3;
146
+ /**
147
+ * If the password contains more than such number of characters that are not
148
+ * in the restricted phrases, it will be accepted.
149
+ */
150
+ static restrictedPhrasesTolerance: 3;
151
+ /** Get the length threshold for checking restricted phrases. */
152
+ protected static getRestrictedPhraseThreshold(password: string): number;
153
+ readonly policy: PasswordPolicy;
154
+ constructor(policy: DeepPartial<PasswordPolicy>,
155
+ /** The Web Crypto API to use. By default, the global `crypto.subtle` will be used. */
156
+ subtle?: SubtleCrypto);
157
+ /**
158
+ * Check if a password meets all the policy requirements.
159
+ *
160
+ * @param password - Password to check.
161
+ * @param userInfo - User information to check. Required if the policy
162
+ * requires to reject passwords that include user information.
163
+ * @returns An array of issues. If the password meets the policy, an empty array will be returned.
164
+ * @throws TypeError - If the policy requires to reject passwords that include user information
165
+ * but the user information is not provided.
166
+ */
167
+ check(password: string, userInfo?: UserInfo): Promise<PasswordIssue[]>;
168
+ /**
169
+ * Perform a fast check to see if the password passes the basic requirements.
170
+ * Only the length and character types will be checked.
171
+ *
172
+ * This method is used for frontend validation.
173
+ *
174
+ * @param password - Password to check.
175
+ * @returns Whether the password passes the basic requirements.
176
+ */
177
+ fastCheck(password: string): PasswordIssue<PasswordRejectionCode>[];
178
+ /**
179
+ * Check if the given password contains enough character types.
180
+ *
181
+ * @param password - Password to check.
182
+ * @returns Whether the password contains enough character types; or `'unsupported'`
183
+ * if the password contains unsupported characters.
184
+ */
185
+ checkCharTypes(password: string): boolean | 'unsupported';
186
+ /**
187
+ * Check if the given password has been pwned.
188
+ *
189
+ * @param password - Password to check.
190
+ * @returns Whether the password has been pwned.
191
+ */
192
+ hasBeenPwned(password: string): Promise<boolean>;
193
+ /**
194
+ * Get the length of the repetition at the beginning of the given string.
195
+ * For example, `repetitionLength('aaaaa')` will return `5`.
196
+ *
197
+ * If the length is less than {@link PasswordPolicyChecker.repetitionAndSequenceThreshold},
198
+ * `0` will be returned.
199
+ */
200
+ repetitionLength(password: string): number;
201
+ /**
202
+ * Get the length of the user information at the beginning of the given string.
203
+ * For example, `userInfoLength('silverhand', { username: 'silverhand' })` will return `10`.
204
+ *
205
+ * For multiple matches, the longest length will be returned.
206
+ */
207
+ userInfoLength(password: string, userInfo: UserInfo): number;
208
+ /**
209
+ * Get the length of the word that matches the word list at the beginning of the given string.
210
+ *
211
+ * For multiple matches, the longest length will be returned.
212
+ */
213
+ wordLength(password: string): number;
214
+ /**
215
+ * Get the length of the sequence at the beginning of the given string.
216
+ * For example, `sequenceLength('12345')` will return `5`.
217
+ *
218
+ * If the length is less than {@link PasswordPolicyChecker.repetitionAndSequenceThreshold},
219
+ * `0` will be returned.
220
+ */
221
+ sequenceLength(password: string): number;
222
+ /**
223
+ * Check if the given string is sequential by iterating through the {@link PasswordPolicyChecker.sequence}.
224
+ */
225
+ protected isSequential(value: string): boolean;
226
+ }
@@ -0,0 +1,359 @@
1
+ import { z } from 'zod';
2
+ /** Password policy configuration guard. */
3
+ export const passwordPolicyGuard = z.object({
4
+ length: z
5
+ .object({
6
+ min: z.number().int().min(1).default(8),
7
+ max: z.number().int().min(1).default(256),
8
+ })
9
+ .default({}),
10
+ characterTypes: z
11
+ .object({
12
+ min: z.number().int().min(1).max(4).optional().default(1),
13
+ })
14
+ .default({}),
15
+ rejects: z
16
+ .object({
17
+ pwned: z.boolean().default(true),
18
+ repetitionAndSequence: z.boolean().default(true),
19
+ userInfo: z.boolean().default(true),
20
+ words: z.string().array().default([]),
21
+ })
22
+ .default({}),
23
+ });
24
+ /**
25
+ * The class for checking if a password meets the policy. The policy is defined as
26
+ * {@link PasswordPolicy}.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const checker = new PasswordPolicyChecker({
31
+ * length: { min: 8, max: 256 },
32
+ * characterTypes: { min: 2 },
33
+ * rejects: { pwned: true, repetitionAndSequence: true, words: [] },
34
+ * });
35
+ *
36
+ * const issues = await checker.check('123456');
37
+ * console.log(issues);
38
+ * // [
39
+ * // { code: 'password_rejected.too_short' },
40
+ * // { code: 'password_rejected.character_types', interpolation: { min: 2 } },
41
+ * // { code: 'password_rejected.pwned' },
42
+ * // { code: 'password_rejected.restricted.sequence' },
43
+ * // ]
44
+ * ```
45
+ */
46
+ class PasswordPolicyChecker {
47
+ static { this.symbols = Object.freeze('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ '); }
48
+ /** A set of characters that are considered as sequential. */
49
+ static { this.sequence = Object.freeze([
50
+ '0123456789',
51
+ 'abcdefghijklmnopqrstuvwxyz',
52
+ 'qwertyuiop',
53
+ 'asdfghjkl',
54
+ 'zxcvbnm',
55
+ '1qaz',
56
+ '2wsx',
57
+ '3edc',
58
+ '4rfv',
59
+ '5tgb',
60
+ '6yhn',
61
+ '7ujm',
62
+ '8ik',
63
+ '9ol',
64
+ ]); }
65
+ /** The length threshold for checking repetition and sequence. */
66
+ static { this.repetitionAndSequenceThreshold = 3; }
67
+ /**
68
+ * If the password contains more than such number of characters that are not
69
+ * in the restricted phrases, it will be accepted.
70
+ */
71
+ static { this.restrictedPhrasesTolerance = 3; }
72
+ /** Get the length threshold for checking restricted phrases. */
73
+ static getRestrictedPhraseThreshold(password) {
74
+ const { restrictedPhrasesTolerance } = PasswordPolicyChecker;
75
+ return Math.max(1, password.length - restrictedPhrasesTolerance);
76
+ }
77
+ constructor(policy,
78
+ /** The Web Crypto API to use. By default, the global `crypto.subtle` will be used. */
79
+ subtle = crypto.subtle) {
80
+ this.subtle = subtle;
81
+ this.policy = passwordPolicyGuard.parse(policy);
82
+ }
83
+ /**
84
+ * Check if a password meets all the policy requirements.
85
+ *
86
+ * @param password - Password to check.
87
+ * @param userInfo - User information to check. Required if the policy
88
+ * requires to reject passwords that include user information.
89
+ * @returns An array of issues. If the password meets the policy, an empty array will be returned.
90
+ * @throws TypeError - If the policy requires to reject passwords that include user information
91
+ * but the user information is not provided.
92
+ */
93
+ /* eslint-disable @silverhand/fp/no-let, @silverhand/fp/no-mutation, @silverhand/fp/no-mutating-methods */
94
+ async check(password, userInfo) {
95
+ const issues = this.fastCheck(password);
96
+ if (this.policy.rejects.pwned && (await this.hasBeenPwned(password))) {
97
+ issues.push({
98
+ code: 'password_rejected.pwned',
99
+ });
100
+ }
101
+ // `hashArray[i]` indicates whether the `i`th character violates the restriction.
102
+ // We'll gradually set the value to `1` if needed.
103
+ // The algorithm time complexity should be O(n^2), but it's fast enough for a password.
104
+ const hashArray = Array.from({ length: password.length }).fill(0);
105
+ const issueCodes = new Set();
106
+ const { repetitionAndSequence, words, userInfo: rejectUserInfo } = this.policy.rejects;
107
+ const rejectWords = words.length > 0;
108
+ const fillHashArray = (startIndex, length, code) => {
109
+ if (length <= 0) {
110
+ return;
111
+ }
112
+ for (let i = startIndex; i < startIndex + length; i += 1) {
113
+ hashArray[i] = 1;
114
+ }
115
+ issueCodes.add(code);
116
+ };
117
+ for (let i = 0; i < password.length; i += 1) {
118
+ const sliced = password.slice(i);
119
+ if (repetitionAndSequence) {
120
+ fillHashArray(i, this.repetitionLength(sliced), 'restricted.repetition');
121
+ fillHashArray(i, this.sequenceLength(sliced), 'restricted.sequence');
122
+ }
123
+ if (rejectWords) {
124
+ fillHashArray(i, this.wordLength(sliced), 'restricted.words');
125
+ }
126
+ if (rejectUserInfo) {
127
+ if (!userInfo) {
128
+ throw new TypeError('User information data is required to check user information.');
129
+ }
130
+ fillHashArray(i, this.userInfoLength(sliced, userInfo), 'restricted.user_info');
131
+ }
132
+ }
133
+ return hashArray.reduce((total, current) => total + current, 0) >
134
+ PasswordPolicyChecker.getRestrictedPhraseThreshold(password)
135
+ ? [
136
+ ...issues,
137
+ ...[...issueCodes].map((code) => ({ code: `password_rejected.${code}` })),
138
+ ]
139
+ : issues;
140
+ }
141
+ /* eslint-enable @silverhand/fp/no-let, @silverhand/fp/no-mutation, @silverhand/fp/no-mutating-methods */
142
+ /**
143
+ * Perform a fast check to see if the password passes the basic requirements.
144
+ * Only the length and character types will be checked.
145
+ *
146
+ * This method is used for frontend validation.
147
+ *
148
+ * @param password - Password to check.
149
+ * @returns Whether the password passes the basic requirements.
150
+ */
151
+ /* eslint-disable @silverhand/fp/no-mutating-methods */
152
+ fastCheck(password) {
153
+ const issues = [];
154
+ if (password.length < this.policy.length.min) {
155
+ issues.push({
156
+ code: 'password_rejected.too_short',
157
+ interpolation: { min: this.policy.length.min },
158
+ });
159
+ }
160
+ else if (password.length > this.policy.length.max) {
161
+ issues.push({
162
+ code: 'password_rejected.too_long',
163
+ interpolation: { max: this.policy.length.max },
164
+ });
165
+ }
166
+ const characterTypes = this.checkCharTypes(password);
167
+ if (characterTypes === 'unsupported') {
168
+ issues.push({
169
+ code: 'password_rejected.unsupported_characters',
170
+ });
171
+ }
172
+ else if (!characterTypes) {
173
+ issues.push({
174
+ code: 'password_rejected.character_types',
175
+ interpolation: { min: this.policy.characterTypes.min },
176
+ });
177
+ }
178
+ return issues;
179
+ }
180
+ /* eslint-enable @silverhand/fp/no-mutating-methods */
181
+ /**
182
+ * Check if the given password contains enough character types.
183
+ *
184
+ * @param password - Password to check.
185
+ * @returns Whether the password contains enough character types; or `'unsupported'`
186
+ * if the password contains unsupported characters.
187
+ */
188
+ checkCharTypes(password) {
189
+ const characterTypes = new Set();
190
+ for (const char of password) {
191
+ if (char >= 'a' && char <= 'z') {
192
+ characterTypes.add('lowercase');
193
+ }
194
+ else if (char >= 'A' && char <= 'Z') {
195
+ characterTypes.add('uppercase');
196
+ }
197
+ else if (char >= '0' && char <= '9') {
198
+ characterTypes.add('digits');
199
+ }
200
+ else if (PasswordPolicyChecker.symbols.includes(char)) {
201
+ characterTypes.add('symbols');
202
+ }
203
+ else {
204
+ return 'unsupported';
205
+ }
206
+ }
207
+ return characterTypes.size >= this.policy.characterTypes.min;
208
+ }
209
+ /**
210
+ * Check if the given password has been pwned.
211
+ *
212
+ * @param password - Password to check.
213
+ * @returns Whether the password has been pwned.
214
+ */
215
+ async hasBeenPwned(password) {
216
+ const hash = await this.subtle.digest('SHA-1', new TextEncoder().encode(password));
217
+ const hashHex = Array.from(new Uint8Array(hash))
218
+ .map((binary) => binary.toString(16).padStart(2, '0'))
219
+ .join('');
220
+ const hashPrefix = hashHex.slice(0, 5);
221
+ const hashSuffix = hashHex.slice(5);
222
+ const response = await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`);
223
+ const text = await response.text();
224
+ const hashes = text.split('\n');
225
+ const found = hashes.some((hex) => hex.toLowerCase().startsWith(hashSuffix));
226
+ return found;
227
+ }
228
+ /**
229
+ * Get the length of the repetition at the beginning of the given string.
230
+ * For example, `repetitionLength('aaaaa')` will return `5`.
231
+ *
232
+ * If the length is less than {@link PasswordPolicyChecker.repetitionAndSequenceThreshold},
233
+ * `0` will be returned.
234
+ */
235
+ /* eslint-disable @silverhand/fp/no-let, @silverhand/fp/no-mutation */
236
+ repetitionLength(password) {
237
+ const { repetitionAndSequenceThreshold } = PasswordPolicyChecker;
238
+ const firstChar = password[0];
239
+ let length = 0;
240
+ if (firstChar === undefined) {
241
+ return 0;
242
+ }
243
+ for (const char of password) {
244
+ if (char === firstChar) {
245
+ length += 1;
246
+ }
247
+ else {
248
+ break;
249
+ }
250
+ }
251
+ return length >= repetitionAndSequenceThreshold ? length : 0;
252
+ }
253
+ /* eslint-enable @silverhand/fp/no-let, @silverhand/fp/no-mutation */
254
+ /**
255
+ * Get the length of the user information at the beginning of the given string.
256
+ * For example, `userInfoLength('silverhand', { username: 'silverhand' })` will return `10`.
257
+ *
258
+ * For multiple matches, the longest length will be returned.
259
+ */
260
+ // eslint-disable-next-line complexity
261
+ userInfoLength(password, userInfo) {
262
+ const lowercased = password.toLowerCase();
263
+ const { name, username, email, phoneNumber } = userInfo;
264
+ // eslint-disable-next-line @silverhand/fp/no-let
265
+ let length = 0;
266
+ const updateLength = (newLength) => {
267
+ if (newLength > length) {
268
+ // eslint-disable-next-line @silverhand/fp/no-mutation
269
+ length = newLength;
270
+ }
271
+ };
272
+ if (name) {
273
+ const joined = name.replaceAll(/\s+/g, '');
274
+ // The original name should be the longest string, so we check it first.
275
+ if (lowercased.startsWith(name.toLowerCase())) {
276
+ updateLength(name.length);
277
+ }
278
+ else {
279
+ if (lowercased.startsWith(joined.toLowerCase())) {
280
+ updateLength(joined.length);
281
+ }
282
+ for (const word of name.split(' ')) {
283
+ if (lowercased.startsWith(word.toLowerCase())) {
284
+ updateLength(word.length);
285
+ }
286
+ }
287
+ }
288
+ }
289
+ if (username && lowercased.startsWith(username.toLowerCase())) {
290
+ updateLength(username.length);
291
+ }
292
+ if (email) {
293
+ const emailPrefix = email.split('@')[0];
294
+ if (emailPrefix && lowercased.startsWith(emailPrefix.toLowerCase())) {
295
+ updateLength(emailPrefix.length);
296
+ }
297
+ }
298
+ if (phoneNumber && lowercased.startsWith(phoneNumber)) {
299
+ updateLength(phoneNumber.length);
300
+ }
301
+ return length;
302
+ }
303
+ /**
304
+ * Get the length of the word that matches the word list at the beginning of the given string.
305
+ *
306
+ * For multiple matches, the longest length will be returned.
307
+ */
308
+ /* eslint-disable @silverhand/fp/no-let, @silverhand/fp/no-mutation */
309
+ wordLength(password) {
310
+ const sliced = password.toLowerCase();
311
+ let length = 0;
312
+ for (const word of this.policy.rejects.words) {
313
+ if (sliced.startsWith(word.toLowerCase()) && word.length > length) {
314
+ length = word.length;
315
+ }
316
+ }
317
+ return length;
318
+ }
319
+ /* eslint-enable @silverhand/fp/no-let, @silverhand/fp/no-mutation */
320
+ /**
321
+ * Get the length of the sequence at the beginning of the given string.
322
+ * For example, `sequenceLength('12345')` will return `5`.
323
+ *
324
+ * If the length is less than {@link PasswordPolicyChecker.repetitionAndSequenceThreshold},
325
+ * `0` will be returned.
326
+ */
327
+ /* eslint-disable @silverhand/fp/no-let, @silverhand/fp/no-mutation */
328
+ sequenceLength(password) {
329
+ const { repetitionAndSequenceThreshold } = PasswordPolicyChecker;
330
+ let value = '';
331
+ let length = 0;
332
+ for (const char of password) {
333
+ if (!value || this.isSequential(value + char)) {
334
+ value += char;
335
+ length += 1;
336
+ }
337
+ else {
338
+ break;
339
+ }
340
+ }
341
+ return length >= repetitionAndSequenceThreshold ? length : 0;
342
+ }
343
+ /* eslint-enable @silverhand/fp/no-let, @silverhand/fp/no-mutation */
344
+ /**
345
+ * Check if the given string is sequential by iterating through the {@link PasswordPolicyChecker.sequence}.
346
+ */
347
+ isSequential(value) {
348
+ const { sequence } = PasswordPolicyChecker;
349
+ for (const seq of sequence) {
350
+ // eslint-disable-next-line @silverhand/fp/no-mutating-methods -- created a new array before mutating
351
+ const reversedSeq = [...seq].reverse().join('');
352
+ if ([seq, reversedSeq, seq.toUpperCase(), reversedSeq.toUpperCase()].some((item) => item.includes(value))) {
353
+ return true;
354
+ }
355
+ }
356
+ return false;
357
+ }
358
+ }
359
+ export { PasswordPolicyChecker };
package/lib/regex.d.ts CHANGED
@@ -7,4 +7,3 @@ export declare const mobileUriSchemeProtocolRegEx: RegExp;
7
7
  export declare const hexColorRegEx: RegExp;
8
8
  export declare const dateRegex: RegExp;
9
9
  export declare const noSpaceRegEx: RegExp;
10
- export declare const passwordRegEx: RegExp;
package/lib/regex.js CHANGED
@@ -3,12 +3,7 @@ export const phoneRegEx = /^\d+$/;
3
3
  export const phoneInputRegEx = /^\+?[\d-( )]+$/;
4
4
  export const usernameRegEx = /^[A-Z_a-z]\w*$/;
5
5
  export const webRedirectUriProtocolRegEx = /^https?:$/;
6
- export const mobileUriSchemeProtocolRegEx = /^[a-z][\d_a-z]*(\.[\d_a-z]+)+:$/;
6
+ export const mobileUriSchemeProtocolRegEx = /^[a-z][\d+_a-z-]*(\.[\d+_a-z-]+)+:$/;
7
7
  export const hexColorRegEx = /^#[\da-f]{3}([\da-f]{3})?$/i;
8
8
  export const dateRegex = /^\d{4}(-\d{2}){2}/;
9
9
  export const noSpaceRegEx = /^\S+$/;
10
- const atLeastOneDigitAndOneLetters = /(?=.*\d)(?=.*[A-Za-z])/;
11
- const atLeastOneDigitAndOneSpecialChar = /(?=.*\d)(?=.*[!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/;
12
- const atLeastOneLetterAndOneSpecialChar = /(?=.*[A-Za-z])(?=.*[!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/;
13
- const allowedChars = /[\w!"#$%&'()*+,./:;<=>?@[\]^`{|}~-]{8,}/;
14
- export const passwordRegEx = new RegExp(`^(${atLeastOneDigitAndOneLetters.source}|${atLeastOneDigitAndOneSpecialChar.source}|${atLeastOneLetterAndOneSpecialChar.source})${allowedChars.source}$`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/core-kit",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "author": "Silverhand Inc. <contact@silverhand.io>",
5
5
  "homepage": "https://github.com/logto-io/toolkit#readme",
6
6
  "repository": {
@@ -11,9 +11,13 @@
11
11
  "type": "module",
12
12
  "main": "./lib/index.js",
13
13
  "exports": {
14
- "default": "./lib/index.js",
15
- "types": "./lib/index.d.ts",
16
- "import": "./lib/index.js"
14
+ ".": {
15
+ "default": "./lib/index.js",
16
+ "types": "./lib/index.d.ts",
17
+ "import": "./lib/index.js"
18
+ },
19
+ "./declaration": "./declaration/index.ts",
20
+ "./scss/*": "./scss/*.scss"
17
21
  },
18
22
  "types": "./lib/index.d.ts",
19
23
  "files": [
@@ -26,7 +30,7 @@
26
30
  },
27
31
  "dependencies": {
28
32
  "@logto/language-kit": "^1.0.0",
29
- "@logto/shared": "^2.0.0",
33
+ "@logto/shared": "^2.0.1",
30
34
  "color": "^4.2.3"
31
35
  },
32
36
  "optionalDependencies": {
@@ -34,19 +38,19 @@
34
38
  },
35
39
  "devDependencies": {
36
40
  "@jest/types": "^29.0.3",
37
- "@silverhand/eslint-config": "3.0.1",
38
- "@silverhand/essentials": "^2.5.0",
39
- "@silverhand/ts-config": "3.0.0",
40
- "@silverhand/ts-config-react": "3.0.0",
41
+ "@silverhand/eslint-config": "4.0.1",
42
+ "@silverhand/essentials": "^2.8.4",
43
+ "@silverhand/ts-config": "4.0.0",
44
+ "@silverhand/ts-config-react": "4.0.0",
41
45
  "@types/color": "^3.0.3",
42
46
  "@types/jest": "^29.4.0",
43
47
  "@types/node": "^18.11.18",
44
48
  "@types/react": "^18.0.31",
45
- "eslint": "^8.34.0",
49
+ "eslint": "^8.44.0",
46
50
  "jest": "^29.5.0",
47
- "lint-staged": "^13.0.0",
51
+ "lint-staged": "^14.0.0",
48
52
  "postcss": "^8.4.6",
49
- "prettier": "^2.8.2",
53
+ "prettier": "^3.0.0",
50
54
  "stylelint": "^15.0.0",
51
55
  "tslib": "^2.4.1",
52
56
  "typescript": "^5.0.0"
@@ -105,10 +105,15 @@
105
105
  --color-tertiary-container: var(--color-tertiary-90);
106
106
  --color-on-tertiary-container: var(--color-tertiary-10);
107
107
  --color-error: var(--color-error-40);
108
- --color-on-error: var(--color-all-100);
109
- --color-error-container: var(--color-error-90);
110
- --color-on-error-container: var(--color-error-10);
111
- --color-alert-container: var(--color-alert-99);
108
+ --color-error-hover: var(--color-error-50);
109
+ --color-error-container: var(--color-error-95);
110
+ --color-on-error-container: var(--color-error-50);
111
+ --color-alert-container: var(--color-alert-95);
112
+ --color-on-alert-container: var(--color-alert-70);
113
+ --color-success-container: var(--color-success-99);
114
+ --color-on-success-container: var(--color-success-70);
115
+ --color-info-container: var(--color-neutral-variant-90);
116
+ --color-on-info-container: var(--color-neutral-variant-60);
112
117
  --color-background: var(--color-neutral-99);
113
118
  --color-on-background: var(--color-neutral-10);
114
119
  --color-surface: var(--color-neutral-99);
@@ -150,6 +155,9 @@
150
155
  --color-hover-variant: rgba(93, 52, 242, 8%); // 8% Primary-40
151
156
  --color-pressed-variant: rgba(93, 52, 242, 12%); // 12% Primary-40
152
157
  --color-focused-variant: rgba(93, 52, 242, 16%); // 16% Primary-40
158
+ --color-env-tag-development: rgba(93, 52, 242, 15%);
159
+ --color-env-tag-staging: rgba(255, 185, 90, 35%);
160
+ --color-env-tag-production: rgba(131, 218, 133, 35%);
153
161
 
154
162
  // Shadows
155
163
  --shadow-1: 0 4px 8px rgba(0, 0, 0, 8%);
@@ -169,6 +177,21 @@
169
177
  --color-guide-dropdown-background: var(--color-white);
170
178
  --color-guide-dropdown-border: var(--color-border);
171
179
  --color-skeleton-shimmer-rgb: 255, 255, 255; // rgb of Layer-1
180
+ --color-specific-tag-upsell: var(--color-primary-50);
181
+
182
+ // Background
183
+ --color-bg-body-base: var(--color-neutral-95);
184
+ --color-bg-body: var(--color-neutral-100);
185
+ --color-bg-layer-1: var(--color-static-white);
186
+ --color-bg-layer-2: var(--color-neutral-95);
187
+ --color-bg-body-overlay: var(--color-neutral-100);
188
+ --color-bg-float-base: var(--color-neutral-variant-90);
189
+ --color-bg-float: var(--color-neutral-100);
190
+ --color-bg-float-overlay: var(--color-neutral-100);
191
+ --color-bg-mask: rgba(0, 0, 0, 40%); // 4% --color-neutral-0
192
+ --color-bg-toast: var(--color-neutral-20);
193
+ --color-bg-state-unselected: var(--color-neutral-90);
194
+ --color-bg-state-disabled: rgba(25, 28, 29, 8%); // 8% --color-neutral-10
172
195
  }
173
196
 
174
197
  @mixin dark {
@@ -278,10 +301,15 @@
278
301
  --color-tertiary-container: var(--color-tertiary-90);
279
302
  --color-on-tertiary-container: var(--color-tertiary-30);
280
303
  --color-error: var(--color-error-70);
281
- --color-on-error: var(--color-all-0);
282
- --color-error-container: var(--color-error-90);
283
- --color-on-error-container: var(--color-error-30);
304
+ --color-error-hover: var(--color-error-60);
305
+ --color-error-container: var(--color-error-95);
306
+ --color-on-error-container: var(--color-error-70);
284
307
  --color-alert-container: var(--color-alert-90);
308
+ --color-on-alert-container: var(--color-alert-60);
309
+ --color-success-container: var(--color-success-90);
310
+ --color-on-success-container: var(--color-success-60);
311
+ --color-info-container: var(--color-neutral-variant-90);
312
+ --color-on-info-container: var(--color-neutral-variant-70);
285
313
  --color-background: var(--color-neutral-99);
286
314
  --color-on-background: var(--color-neutral-10);
287
315
  --color-surface: var(--color-neutral-99);
@@ -323,6 +351,9 @@
323
351
  --color-hover-variant: rgba(202, 190, 255, 8%); // 8% Primary-40
324
352
  --color-pressed-variant: rgba(202, 190, 255, 12%); // 12% Primary-40
325
353
  --color-focused-variant: rgba(202, 190, 255, 16%); // 16% Primary-40
354
+ --color-env-tag-development: rgba(202, 190, 255, 32%);
355
+ --color-env-tag-staging: rgba(235, 153, 24, 36%);
356
+ --color-env-tag-production: rgba(104, 190, 108, 36%);
326
357
 
327
358
  // Shadows
328
359
  --shadow-1: 0 4px 8px rgba(0, 0, 0, 8%);
@@ -337,9 +368,27 @@
337
368
  --color-danger-focused: rgba(255, 180, 169, 16%); // 16% Error-40
338
369
  --color-tooltip-background: var(--color-surface-4);
339
370
  --color-tooltip-text: var(--color-neutral-10);
340
- --color-overlay: rgba(0, 0, 0, 30%);
371
+ --color-overlay: rgba(0, 0, 0, 70%); // 70% Neutral-100
341
372
  --color-drawer-overlay: rgba(0, 0, 0, 60%);
342
373
  --color-guide-dropdown-background: var(--color-neutral-variant-80);
343
374
  --color-guide-dropdown-border: var(--color-neutral-variant-70);
344
375
  --color-skeleton-shimmer-rgb: 42, 44, 50; // rgb of Layer-1
376
+ --color-specific-tag-upsell: var(--color-primary-70);
377
+
378
+ // Background
379
+ --color-bg-body-base: var(--color-neutral-100);
380
+ --color-bg-body: var(--color-surface);
381
+ --color-bg-body-overlay: var(--color-surface-2);
382
+ --color-bg-layer-1:
383
+ linear-gradient(0deg, rgba(202, 190, 255, 8%), rgba(202, 190, 255, 8%)),
384
+ linear-gradient(0deg, rgba(196, 199, 199, 2%), rgba(196, 199, 199, 2%)),
385
+ #191c1d;
386
+ --color-bg-layer-2: var(--color-surface-4);
387
+ --color-bg-float-base: var(--color-neutral-100);
388
+ --color-bg-float: var(--color-surface-3);
389
+ --color-bg-float-overlay: var(--color-surface-4);
390
+ --color-bg-mask: rgba(0, 0, 0, 60%); // 60% --color-neutral-100;
391
+ --color-bg-toast: var(--color-neutral-80);
392
+ --color-bg-state-unselected: var(--color-neutral-90);
393
+ --color-bg-state-disabled: rgba(247, 248, 248, 8%); // 8% --color-neutral-10
345
394
  }
package/scss/_fonts.scss CHANGED
@@ -12,6 +12,9 @@ $font-family:
12
12
 
13
13
  :root {
14
14
  --font-family: #{$font-family};
15
+ --font-display-1: 700 48/56px #{$font-family};
16
+ --font-display-2: 700 40px/48px #{$font-family};
17
+ --font-display-3: 700 32px/40px #{$font-family};
15
18
  --font-headline-1: 600 32px/40px #{$font-family};
16
19
  --font-headline-2: 600 28px/36px #{$font-family};
17
20
  --font-headline-3: 600 24px/32px #{$font-family};
@@ -21,6 +24,7 @@ $font-family:
21
24
  --font-label-1: 500 16px/24px #{$font-family};
22
25
  --font-label-2: 500 14px/20px #{$font-family};
23
26
  --font-label-3: 500 12px/16px #{$font-family};
27
+ --font-body-0: 400 18px/26px #{$font-family};
24
28
  --font-body-1: 400 16px/24px #{$font-family};
25
29
  --font-body-2: 400 14px/20px #{$font-family};
26
30
  --font-body-3: 400 12px/16px #{$font-family};