@logto/core-kit 2.6.0 → 2.7.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/regex.d.ts +17 -1
- package/lib/regex.js +19 -1
- package/lib/utils/url.js +82 -0
- package/package.json +9 -9
package/lib/regex.d.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
export declare const emailRegEx: RegExp;
|
|
2
|
+
/** Validates full email address or email domain. */
|
|
3
|
+
export declare const emailOrEmailDomainRegEx: RegExp;
|
|
2
4
|
export declare const phoneRegEx: RegExp;
|
|
3
5
|
export declare const phoneInputRegEx: RegExp;
|
|
4
6
|
export declare const usernameRegEx: RegExp;
|
|
5
7
|
export declare const webRedirectUriProtocolRegEx: RegExp;
|
|
6
8
|
export declare const mobileUriSchemeProtocolRegEx: RegExp;
|
|
7
9
|
export declare const hexColorRegEx: RegExp;
|
|
8
|
-
export declare const
|
|
10
|
+
export declare const dateRegEx: RegExp;
|
|
9
11
|
export declare const noSpaceRegEx: RegExp;
|
|
10
12
|
/** Full domain that consists of at least 3 parts, e.g. foo.bar.com or example-foo.bar.com */
|
|
11
13
|
export declare const domainRegEx: RegExp;
|
|
14
|
+
export declare const numberAndAlphabetRegEx: RegExp;
|
|
15
|
+
/**
|
|
16
|
+
* Custom tenant ID validation rules.
|
|
17
|
+
* Used when creating tenants with custom IDs in private cloud regions.
|
|
18
|
+
*/
|
|
19
|
+
/** Maximum length for custom tenant ID */
|
|
20
|
+
export declare const customTenantIdMaxLength = 21;
|
|
21
|
+
/** Pattern for custom tenant ID: lowercase letters, numbers, and hyphens only */
|
|
22
|
+
export declare const customTenantIdRegEx: RegExp;
|
|
23
|
+
/**
|
|
24
|
+
* Validates a custom tenant ID format.
|
|
25
|
+
* @returns `true` if valid, `false` otherwise
|
|
26
|
+
*/
|
|
27
|
+
export declare const isValidCustomTenantId: (id: string) => boolean;
|
package/lib/regex.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
export const emailRegEx = /^\S+@\S+\.\S+$/;
|
|
2
|
+
/** Validates full email address or email domain. */
|
|
3
|
+
export const emailOrEmailDomainRegEx = /^\S+@\S+\.\S+|^@\S+\.\S+$/;
|
|
2
4
|
export const phoneRegEx = /^\d+$/;
|
|
3
5
|
export const phoneInputRegEx = /^\+?[\d-( )]+$/;
|
|
4
6
|
export const usernameRegEx = /^[A-Z_a-z]\w*$/;
|
|
5
7
|
export const webRedirectUriProtocolRegEx = /^https?:$/;
|
|
6
8
|
export const mobileUriSchemeProtocolRegEx = /^(?!http(s)?:)[a-z][\d+_a-z-]*(\.[\d+_a-z-]+)*:$/;
|
|
7
9
|
export const hexColorRegEx = /^#[\da-f]{3}([\da-f]{3})?$/i;
|
|
8
|
-
export const
|
|
10
|
+
export const dateRegEx = /^\d{4}(-\d{2}){2}/;
|
|
9
11
|
export const noSpaceRegEx = /^\S+$/;
|
|
10
12
|
/** Full domain that consists of at least 3 parts, e.g. foo.bar.com or example-foo.bar.com */
|
|
11
13
|
export const domainRegEx = /^[\dA-Za-z](?:[\dA-Za-z-]*[\dA-Za-z])?(?:\.[\dA-Za-z](?:[\dA-Za-z-]*[\dA-Za-z])?){2,}$/;
|
|
14
|
+
export const numberAndAlphabetRegEx = /^[\dA-Za-z]+$/;
|
|
15
|
+
/**
|
|
16
|
+
* Custom tenant ID validation rules.
|
|
17
|
+
* Used when creating tenants with custom IDs in private cloud regions.
|
|
18
|
+
*/
|
|
19
|
+
/** Maximum length for custom tenant ID */
|
|
20
|
+
export const customTenantIdMaxLength = 21;
|
|
21
|
+
/** Pattern for custom tenant ID: lowercase letters, numbers, and hyphens only */
|
|
22
|
+
export const customTenantIdRegEx = /^[\da-z-]+$/;
|
|
23
|
+
/**
|
|
24
|
+
* Validates a custom tenant ID format.
|
|
25
|
+
* @returns `true` if valid, `false` otherwise
|
|
26
|
+
*/
|
|
27
|
+
export const isValidCustomTenantId = (id) => {
|
|
28
|
+
return id.length <= customTenantIdMaxLength && customTenantIdRegEx.test(id);
|
|
29
|
+
};
|
package/lib/utils/url.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { mobileUriSchemeProtocolRegEx, webRedirectUriProtocolRegEx } from '../regex.js';
|
|
2
2
|
export const validateRedirectUrl = (url, type) => {
|
|
3
|
+
if (type === 'web' && url.includes('*')) {
|
|
4
|
+
return validateWildcardWebRedirectUrl(url);
|
|
5
|
+
}
|
|
6
|
+
if (type === 'web' && hasDotSegmentsInAbsoluteUrlPath(url)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
3
9
|
try {
|
|
4
10
|
const { protocol } = new URL(url);
|
|
5
11
|
const protocolRegEx = type === 'mobile' ? mobileUriSchemeProtocolRegEx : webRedirectUriProtocolRegEx;
|
|
@@ -9,6 +15,82 @@ export const validateRedirectUrl = (url, type) => {
|
|
|
9
15
|
return false;
|
|
10
16
|
}
|
|
11
17
|
};
|
|
18
|
+
const validateWildcardWebRedirectUrl = (url) => {
|
|
19
|
+
const schemeSeparatorIndex = url.indexOf('://');
|
|
20
|
+
if (schemeSeparatorIndex <= 0) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (hasWildcardInScheme(url, schemeSeparatorIndex)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (hasWildcardInQueryOrHash(url)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const authority = getAuthorityFromUrl(url, schemeSeparatorIndex);
|
|
30
|
+
if (!isAuthorityAllowedForWildcardWebRedirect(authority)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (hasDotSegmentsInAbsoluteUrlPath(url)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return isUrlProtocolAllowedAfterWildcardReplacement(url);
|
|
37
|
+
};
|
|
38
|
+
const hasWildcardInScheme = (url, schemeSeparatorIndex) => url.slice(0, schemeSeparatorIndex).includes('*');
|
|
39
|
+
const hasWildcardInQueryOrHash = (url) => {
|
|
40
|
+
// Disallow wildcards in query/hash to keep matching deterministic and safer.
|
|
41
|
+
const queryIndex = url.indexOf('?');
|
|
42
|
+
if (queryIndex >= 0 && url.slice(queryIndex).includes('*')) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const hashIndex = url.indexOf('#');
|
|
46
|
+
return hashIndex >= 0 && url.slice(hashIndex).includes('*');
|
|
47
|
+
};
|
|
48
|
+
const getAuthorityFromUrl = (url, schemeSeparatorIndex) => url.slice(schemeSeparatorIndex + 3).split(/[#/?]/)[0] ?? '';
|
|
49
|
+
const hasDotSegmentsInAbsoluteUrlPath = (url) => {
|
|
50
|
+
const schemeSeparatorIndex = url.indexOf('://');
|
|
51
|
+
if (schemeSeparatorIndex <= 0) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const authority = getAuthorityFromUrl(url, schemeSeparatorIndex);
|
|
55
|
+
const afterAuthorityIndex = schemeSeparatorIndex + 3 + authority.length;
|
|
56
|
+
const rest = url.slice(afterAuthorityIndex);
|
|
57
|
+
const path = rest.split(/[#?]/)[0] ?? '';
|
|
58
|
+
if (!path) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const segments = path.split('/');
|
|
62
|
+
return segments.some((segment) => {
|
|
63
|
+
const normalized = segment.toLowerCase();
|
|
64
|
+
return segment === '.' || segment === '..' || normalized === '%2e' || normalized === '%2e%2e';
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const isAuthorityAllowedForWildcardWebRedirect = (authority) => {
|
|
68
|
+
// Disallow credentials in authority part.
|
|
69
|
+
if (authority.includes('@')) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (authority.startsWith('[')) {
|
|
73
|
+
// IPv6 literals are not a typical use-case for wildcard redirect URIs; reject for simplicity.
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const lastColonIndex = authority.lastIndexOf(':');
|
|
77
|
+
const hasPort = lastColonIndex > -1 && authority.indexOf(':') === lastColonIndex;
|
|
78
|
+
const hostname = hasPort ? authority.slice(0, lastColonIndex) : authority;
|
|
79
|
+
// When wildcard is used in hostname, require at least one dot to avoid overly broad patterns.
|
|
80
|
+
if (hostname.includes('*') && !hostname.includes('.')) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return !(hasPort && authority.slice(lastColonIndex + 1).includes('*'));
|
|
84
|
+
};
|
|
85
|
+
const isUrlProtocolAllowedAfterWildcardReplacement = (url) => {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = new URL(url.replaceAll('*', 'wildcard'));
|
|
88
|
+
return webRedirectUriProtocolRegEx.test(parsed.protocol);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
12
94
|
export const validateUriOrigin = (url) => {
|
|
13
95
|
try {
|
|
14
96
|
return new URL(url).origin === url;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/core-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
5
5
|
"homepage": "https://github.com/logto-io/toolkit#readme",
|
|
6
6
|
"repository": {
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
"main": "./lib/index.js",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
|
-
"default": "./lib/index.js",
|
|
16
15
|
"types": "./lib/index.d.ts",
|
|
17
|
-
"import": "./lib/index.js"
|
|
16
|
+
"import": "./lib/index.js",
|
|
17
|
+
"default": "./lib/index.js"
|
|
18
18
|
},
|
|
19
19
|
"./declaration": "./declaration/index.ts",
|
|
20
20
|
"./scss/*": "./scss/*.scss",
|
|
21
21
|
"./custom-jwt": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"types": "./lib/custom-jwt/index.d.ts",
|
|
23
|
+
"node": "./lib/custom-jwt/index.js"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"types": "./lib/index.d.ts",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"node": "^22.14.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@logto/language-kit": "^1.2.0",
|
|
37
|
-
"@logto/shared": "^3.2.0",
|
|
38
36
|
"@silverhand/essentials": "^2.9.1",
|
|
39
|
-
"color": "^4.2.3"
|
|
37
|
+
"color": "^4.2.3",
|
|
38
|
+
"@logto/language-kit": "^1.2.0",
|
|
39
|
+
"@logto/shared": "^3.3.1"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"zod": "
|
|
42
|
+
"zod": "3.24.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@silverhand/eslint-config": "6.0.1",
|