@logto/core-kit 2.11.0 → 2.13.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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Validates an email blocklist item.
3
+ *
4
+ * An item can be either a full email address (`foo@example.com`) or a domain entry
5
+ * prefixed with `@` (`@example.com`). `*` can be used inside the local part or
6
+ * domain while keeping the same email/domain shapes.
7
+ */
8
+ export declare const isEmailBlocklistItem: (value: string) => boolean;
9
+ /**
10
+ * Checks whether an email address matches an email blocklist item.
11
+ *
12
+ * Matching is case-insensitive. Domain items (`@example.com`) match only the email
13
+ * domain, while full email items match the complete email address. `gmail.com` and
14
+ * `googlemail.com` are treated as the same domain, and dots in their local parts are
15
+ * ignored because Gmail treats those variants as the same mailbox.
16
+ */
17
+ export declare const matchesEmailBlocklistItem: (item: string, email: string) => boolean;
@@ -0,0 +1,85 @@
1
+ import { emailOrEmailDomainRegEx } from './regex.js';
2
+ const wildcard = '*';
3
+ const emailSeparator = '@';
4
+ const domainSeparator = '.';
5
+ const gmailDomains = new Set(['gmail.com', 'googlemail.com']);
6
+ const whitespaceRegEx = /\s/u;
7
+ const wildcardOnlyDomainRegEx = /^[*.]+$/u;
8
+ const hasWildcard = (value) => value.includes(wildcard);
9
+ const escapeRegExp = (value) => value.replaceAll(/[.+?^${}()|[\]\\]/gu, '\\$&');
10
+ const buildWildcardRegExp = (pattern) => new RegExp(`^${escapeRegExp(pattern).replaceAll(wildcard, '.*')}$`, 'u');
11
+ const matchesPattern = (pattern, value) => hasWildcard(pattern) ? buildWildcardRegExp(pattern).test(value) : pattern === value;
12
+ const removeDotsFromLocalPart = (value) => {
13
+ const separatorIndex = value.indexOf(emailSeparator);
14
+ if (separatorIndex === -1) {
15
+ return value;
16
+ }
17
+ return `${value.slice(0, separatorIndex).replaceAll(domainSeparator, '')}${value.slice(separatorIndex)}`;
18
+ };
19
+ const buildGmailAddressVariants = (email) => {
20
+ const emailWithoutLocalPartDots = removeDotsFromLocalPart(email);
21
+ const separatorIndex = emailWithoutLocalPartDots.indexOf(emailSeparator);
22
+ const localPart = emailWithoutLocalPartDots.slice(0, separatorIndex);
23
+ return Array.from(gmailDomains, (domain) => `${localPart}${emailSeparator}${domain}`);
24
+ };
25
+ const isValidWildcardLocalPart = (localPart) => localPart.length > 0 && !localPart.includes(emailSeparator) && !whitespaceRegEx.test(localPart);
26
+ const isValidWildcardDomain = (domain) => domain.length > 0 &&
27
+ domain.includes(domainSeparator) &&
28
+ !domain.includes(emailSeparator) &&
29
+ !domain.includes(`${domainSeparator}${domainSeparator}`) &&
30
+ !domain.startsWith(domainSeparator) &&
31
+ !domain.endsWith(domainSeparator) &&
32
+ !whitespaceRegEx.test(domain) &&
33
+ !wildcardOnlyDomainRegEx.test(domain);
34
+ /**
35
+ * Validates an email blocklist item.
36
+ *
37
+ * An item can be either a full email address (`foo@example.com`) or a domain entry
38
+ * prefixed with `@` (`@example.com`). `*` can be used inside the local part or
39
+ * domain while keeping the same email/domain shapes.
40
+ */
41
+ export const isEmailBlocklistItem = (value) => {
42
+ if (!hasWildcard(value)) {
43
+ return emailOrEmailDomainRegEx.test(value);
44
+ }
45
+ if (whitespaceRegEx.test(value)) {
46
+ return false;
47
+ }
48
+ if (value.startsWith(emailSeparator)) {
49
+ return isValidWildcardDomain(value.slice(1));
50
+ }
51
+ const emailParts = value.split(emailSeparator);
52
+ if (emailParts.length !== 2) {
53
+ return false;
54
+ }
55
+ const [localPart, domain] = emailParts;
56
+ return (localPart !== undefined &&
57
+ domain !== undefined &&
58
+ isValidWildcardLocalPart(localPart) &&
59
+ isValidWildcardDomain(domain));
60
+ };
61
+ /**
62
+ * Checks whether an email address matches an email blocklist item.
63
+ *
64
+ * Matching is case-insensitive. Domain items (`@example.com`) match only the email
65
+ * domain, while full email items match the complete email address. `gmail.com` and
66
+ * `googlemail.com` are treated as the same domain, and dots in their local parts are
67
+ * ignored because Gmail treats those variants as the same mailbox.
68
+ */
69
+ export const matchesEmailBlocklistItem = (item, email) => {
70
+ const normalizedItem = item.toLowerCase();
71
+ const normalizedEmail = email.toLowerCase();
72
+ const domain = normalizedEmail.split(emailSeparator)[1];
73
+ if (normalizedItem.startsWith(emailSeparator)) {
74
+ if (!domain) {
75
+ return false;
76
+ }
77
+ const comparableDomains = gmailDomains.has(domain) ? gmailDomains : [domain];
78
+ return Array.from(comparableDomains).some((comparableDomain) => matchesPattern(normalizedItem.slice(1), comparableDomain));
79
+ }
80
+ const comparableItem = domain && gmailDomains.has(domain) ? removeDotsFromLocalPart(normalizedItem) : normalizedItem;
81
+ const comparableEmails = domain && gmailDomains.has(domain)
82
+ ? buildGmailAddressVariants(normalizedEmail)
83
+ : [normalizedEmail];
84
+ return comparableEmails.some((comparableEmail) => matchesPattern(comparableItem, comparableEmail));
85
+ };
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './utils/index.js';
2
2
  export * from './custom-ui-csp.js';
3
+ export * from './email-blocklist.js';
3
4
  export * from './regex.js';
4
5
  export * from './openid.js';
5
6
  export * from './models/index.js';
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './utils/index.js';
2
2
  export * from './custom-ui-csp.js';
3
+ export * from './email-blocklist.js';
3
4
  export * from './regex.js';
4
5
  export * from './openid.js';
5
6
  export * from './models/index.js';
package/lib/openid.d.ts CHANGED
@@ -102,7 +102,14 @@ export declare enum UserScope {
102
102
  * Only used for session management via account API.
103
103
  * Not included in user claims, even when the scope is requested, as it's not meant for ID token or userinfo endpoint.
104
104
  */
105
- Sessions = "urn:logto:scope:sessions"
105
+ Sessions = "urn:logto:scope:sessions",
106
+ /**
107
+ * Scope for user's trusted devices.
108
+ *
109
+ * Only used for trusted-device management via account API.
110
+ * Not included in user claims, even when the scope is requested, as it's not meant for ID token or userinfo endpoint.
111
+ */
112
+ TrustedDevices = "urn:logto:scope:trusted_devices"
106
113
  }
107
114
  /**
108
115
  * Mapped claims that ID Token includes.
package/lib/openid.js CHANGED
@@ -137,6 +137,13 @@ export var UserScope;
137
137
  * Not included in user claims, even when the scope is requested, as it's not meant for ID token or userinfo endpoint.
138
138
  */
139
139
  UserScope["Sessions"] = "urn:logto:scope:sessions";
140
+ /**
141
+ * Scope for user's trusted devices.
142
+ *
143
+ * Only used for trusted-device management via account API.
144
+ * Not included in user claims, even when the scope is requested, as it's not meant for ID token or userinfo endpoint.
145
+ */
146
+ UserScope["TrustedDevices"] = "urn:logto:scope:trusted_devices";
140
147
  })(UserScope || (UserScope = {}));
141
148
  /**
142
149
  * Mapped claims that ID Token includes.
@@ -178,6 +185,7 @@ export const idTokenClaims = Object.freeze({
178
185
  [UserScope.CustomData]: [],
179
186
  [UserScope.Identities]: [],
180
187
  [UserScope.Sessions]: [],
188
+ [UserScope.TrustedDevices]: [],
181
189
  });
182
190
  /**
183
191
  * Extended claims for ID token grouped by scope, controlled by tenant configuration.
package/lib/regex.js CHANGED
@@ -1,13 +1,13 @@
1
1
  export const emailRegEx = /^\S+@\S+\.\S+$/;
2
2
  /** Validates full email address or email domain. */
3
- export const emailOrEmailDomainRegEx = /^\S+@\S+\.\S+|^@\S+\.\S+$/;
3
+ export const emailOrEmailDomainRegEx = /^(?:[^\s@]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+|@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+)$(?![\s\S])/u;
4
4
  export const phoneRegEx = /^\d+$/;
5
5
  export const phoneInputRegEx = /^\+?[\d-( )]+$/;
6
6
  export const usernameRegEx = /^[A-Z_a-z]\w*$/;
7
7
  export const webRedirectUriProtocolRegEx = /^https?:$/;
8
8
  export const mobileUriSchemeProtocolRegEx = /^(?!http(s)?:)[a-z][\d+_a-z-]*(\.[\d+_a-z-]+)*:$/;
9
9
  export const hexColorRegEx = /^#[\da-f]{3}([\da-f]{3})?$/i;
10
- export const dateRegEx = /^\d{4}(-\d{2}){2}/;
10
+ export const dateRegEx = /^\d{4}(-\d{2}){2}$/;
11
11
  export const noSpaceRegEx = /^\S+$/;
12
12
  /** Full domain that consists of at least 3 parts, e.g. foo.bar.com or example-foo.bar.com */
13
13
  export const domainRegEx = /^[\dA-Za-z](?:[\dA-Za-z-]*[\dA-Za-z])?(?:\.[\dA-Za-z](?:[\dA-Za-z-]*[\dA-Za-z])?){2,}$/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/core-kit",
3
- "version": "2.11.0",
3
+ "version": "2.13.0",
4
4
  "author": "Silverhand Inc. <contact@silverhand.io>",
5
5
  "homepage": "https://github.com/logto-io/toolkit#readme",
6
6
  "repository": {
@@ -35,8 +35,8 @@
35
35
  "dependencies": {
36
36
  "@silverhand/essentials": "^2.9.1",
37
37
  "color": "^4.2.3",
38
- "@logto/language-kit": "^1.3.0",
39
- "@logto/shared": "^3.4.1"
38
+ "@logto/language-kit": "^1.4.0",
39
+ "@logto/shared": "^3.4.3"
40
40
  },
41
41
  "optionalDependencies": {
42
42
  "zod": "3.24.3"
@@ -52,7 +52,7 @@
52
52
  "@vitest/coverage-v8": "^4.1.8",
53
53
  "eslint": "^8.56.0",
54
54
  "lint-staged": "^15.0.0",
55
- "postcss": "^8.4.31",
55
+ "postcss": "^8.5.18",
56
56
  "prettier": "^3.5.3",
57
57
  "stylelint": "^15.0.0",
58
58
  "typescript": "^5.5.3",