@logto/core-kit 2.3.0 → 2.5.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,7 @@
1
+ export declare const buildErrorResponse: (error: unknown) => {
2
+ message: string;
3
+ stack: string | undefined;
4
+ } | {
5
+ message: string;
6
+ stack?: undefined;
7
+ };
@@ -0,0 +1,9 @@
1
+ import { types } from 'node:util';
2
+ export const buildErrorResponse = (error) =>
3
+ /**
4
+ * Use `isNativeError` to check if the error is an instance of `Error`.
5
+ * If the error comes from `node:vm` module, then it will not be an instance of `Error` but can be captured by `isNativeError`.
6
+ */
7
+ types.isNativeError(error)
8
+ ? { message: error.message, stack: error.stack }
9
+ : { message: String(error) };
@@ -0,0 +1,2 @@
1
+ export * from './script-execution.js';
2
+ export * from './error-handling.js';
@@ -0,0 +1,2 @@
1
+ export * from './script-execution.js';
2
+ export * from './error-handling.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * This function is used to execute a named function in a customized code script in a local
3
+ * virtual machine with the given payload as input.
4
+ *
5
+ * @param script Custom code snippet.
6
+ * @param functionName The name of the function to be executed.
7
+ * @param payload The input payload for the function.
8
+ * @returns The result of the function execution.
9
+ */
10
+ export declare const runScriptFunctionInLocalVm: (script: string, functionName: string, payload: unknown) => Promise<unknown>;
@@ -0,0 +1,30 @@
1
+ import { runInNewContext } from 'node:vm';
2
+ /**
3
+ * This function is used to execute a named function in a customized code script in a local
4
+ * virtual machine with the given payload as input.
5
+ *
6
+ * @param script Custom code snippet.
7
+ * @param functionName The name of the function to be executed.
8
+ * @param payload The input payload for the function.
9
+ * @returns The result of the function execution.
10
+ */
11
+ export const runScriptFunctionInLocalVm = async (script, functionName, payload) => {
12
+ const globalContext = Object.freeze({
13
+ fetch: async (...args) => fetch(...args),
14
+ });
15
+ const customFunction = runInNewContext(script + `;${functionName};`, globalContext);
16
+ if (typeof customFunction !== 'function') {
17
+ throw new TypeError(`The script does not have a function named \`${functionName}\``);
18
+ }
19
+ /**
20
+ * We can not use top-level await in `vm`, use the following implementation instead.
21
+ *
22
+ * Ref:
23
+ * 1. https://github.com/nodejs/node/issues/40898
24
+ * 2. https://github.com/n-riesco/ijavascript/issues/173#issuecomment-693924098
25
+ */
26
+ const result = await runInNewContext('(async () => customFunction(payload))();', Object.freeze({ customFunction, payload }),
27
+ // Limit the execution time to 3 seconds, throws error if the script takes too long to execute.
28
+ { timeout: 3000 });
29
+ return result;
30
+ };
@@ -1,7 +1,7 @@
1
- export type TenantMetadata = {
1
+ export type TenantDatabaseMetadata = {
2
2
  id: string;
3
3
  parentRole: string;
4
4
  role: string;
5
5
  password: string;
6
6
  };
7
- export declare const createTenantMetadata: (databaseName: string, tenantId?: string) => TenantMetadata;
7
+ export declare const createTenantDatabaseMetadata: (databaseName: string, tenantId?: string) => TenantDatabaseMetadata;
@@ -1,7 +1,7 @@
1
1
  import { generateStandardId } from '@logto/shared/universal';
2
2
  // Use lowercase letters for tenant IDs to improve compatibility
3
3
  const generateTenantId = () => generateStandardId(6);
4
- export const createTenantMetadata = (databaseName, tenantId = generateTenantId()) => {
4
+ export const createTenantDatabaseMetadata = (databaseName, tenantId = generateTenantId()) => {
5
5
  const parentRole = `logto_tenant_${databaseName}`;
6
6
  const role = `logto_tenant_${databaseName}_${tenantId}`;
7
7
  const password = generateStandardId(32);
package/lib/openid.d.ts CHANGED
@@ -12,7 +12,7 @@ export declare enum ReservedResource {
12
12
  */
13
13
  Organization = "urn:logto:resource:organizations"
14
14
  }
15
- export type UserClaim = 'name' | 'picture' | 'username' | 'email' | 'email_verified' | 'phone_number' | 'phone_number_verified' | 'roles' | 'organizations' | 'organization_data' | 'organization_roles' | 'custom_data' | 'identities';
15
+ export type UserClaim = 'name' | 'given_name' | 'family_name' | 'middle_name' | 'nickname' | 'preferred_username' | 'profile' | 'picture' | 'website' | 'email' | 'email_verified' | 'gender' | 'birthdate' | 'zoneinfo' | 'locale' | 'phone_number' | 'phone_number_verified' | 'address' | 'updated_at' | 'username' | 'roles' | 'organizations' | 'organization_data' | 'organization_roles' | 'custom_data' | 'identities' | 'sso_identities' | 'created_at';
16
16
  /**
17
17
  * Scopes for ID Token and Userinfo Endpoint.
18
18
  */
@@ -35,6 +35,12 @@ export declare enum UserScope {
35
35
  * See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
36
36
  */
37
37
  Phone = "phone",
38
+ /**
39
+ * Scope for user address.
40
+ *
41
+ * See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
42
+ */
43
+ Address = "address",
38
44
  /**
39
45
  * Scope for user's custom data.
40
46
  *
@@ -42,7 +48,7 @@ export declare enum UserScope {
42
48
  */
43
49
  CustomData = "custom_data",
44
50
  /**
45
- * Scope for user's social identity details.
51
+ * Scope for user's social and SSO identity details.
46
52
  *
47
53
  * See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
48
54
  */
@@ -68,6 +74,8 @@ export declare enum UserScope {
68
74
  }
69
75
  /**
70
76
  * Mapped claims that ID Token includes.
77
+ *
78
+ * @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0} for standard scope - claim mapping.
71
79
  */
72
80
  export declare const idTokenClaims: Readonly<Record<UserScope, UserClaim[]>>;
73
81
  /**
package/lib/openid.js CHANGED
@@ -37,6 +37,12 @@ export var UserScope;
37
37
  * See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
38
38
  */
39
39
  UserScope["Phone"] = "phone";
40
+ /**
41
+ * Scope for user address.
42
+ *
43
+ * See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
44
+ */
45
+ UserScope["Address"] = "address";
40
46
  /**
41
47
  * Scope for user's custom data.
42
48
  *
@@ -44,7 +50,7 @@ export var UserScope;
44
50
  */
45
51
  UserScope["CustomData"] = "custom_data";
46
52
  /**
47
- * Scope for user's social identity details.
53
+ * Scope for user's social and SSO identity details.
48
54
  *
49
55
  * See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
50
56
  */
@@ -70,11 +76,33 @@ export var UserScope;
70
76
  })(UserScope || (UserScope = {}));
71
77
  /**
72
78
  * Mapped claims that ID Token includes.
79
+ *
80
+ * @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0} for standard scope - claim mapping.
73
81
  */
74
82
  export const idTokenClaims = Object.freeze({
75
- [UserScope.Profile]: ['name', 'picture', 'username'],
83
+ [UserScope.Profile]: [
84
+ // Standard claims
85
+ 'name',
86
+ 'family_name',
87
+ 'given_name',
88
+ 'middle_name',
89
+ 'nickname',
90
+ 'preferred_username',
91
+ 'profile',
92
+ 'picture',
93
+ 'website',
94
+ 'gender',
95
+ 'birthdate',
96
+ 'zoneinfo',
97
+ 'locale',
98
+ 'updated_at',
99
+ // Custom claims
100
+ 'username',
101
+ 'created_at',
102
+ ],
76
103
  [UserScope.Email]: ['email', 'email_verified'],
77
104
  [UserScope.Phone]: ['phone_number', 'phone_number_verified'],
105
+ [UserScope.Address]: ['address'],
78
106
  [UserScope.Roles]: ['roles'],
79
107
  [UserScope.Organizations]: ['organizations'],
80
108
  [UserScope.OrganizationRoles]: ['organization_roles'],
@@ -88,11 +116,12 @@ export const userinfoClaims = Object.freeze({
88
116
  [UserScope.Profile]: [],
89
117
  [UserScope.Email]: [],
90
118
  [UserScope.Phone]: [],
119
+ [UserScope.Address]: [],
91
120
  [UserScope.Roles]: [],
92
121
  [UserScope.Organizations]: ['organization_data'],
93
122
  [UserScope.OrganizationRoles]: [],
94
123
  [UserScope.CustomData]: ['custom_data'],
95
- [UserScope.Identities]: ['identities'],
124
+ [UserScope.Identities]: ['identities', 'sso_identities'],
96
125
  });
97
126
  export const userClaims = Object.freeze(
98
127
  // Hard to infer type directly, use `as` for a workaround.
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { getPwnPasswordsForTest, isIntegrationTest } from './utils/integration-test.js';
2
3
  /** Password policy configuration guard. */
3
4
  export const passwordPolicyGuard = z.object({
4
5
  length: z
@@ -213,6 +214,9 @@ export class PasswordPolicyChecker {
213
214
  * @returns Whether the password has been pwned.
214
215
  */
215
216
  async hasBeenPwned(password) {
217
+ if (isIntegrationTest()) {
218
+ return getPwnPasswordsForTest().includes(password);
219
+ }
216
220
  const hash = await this.subtle.digest('SHA-1', new TextEncoder().encode(password));
217
221
  const hashHex = Array.from(new Uint8Array(hash))
218
222
  .map((binary) => binary.toString(16).padStart(2, '0'))
package/lib/regex.d.ts CHANGED
@@ -7,5 +7,5 @@ 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
- /** Full domain that consists of at least 3 parts, e.g. foo.bar.com */
10
+ /** Full domain that consists of at least 3 parts, e.g. foo.bar.com or example-foo.bar.com */
11
11
  export declare const domainRegEx: RegExp;
package/lib/regex.js CHANGED
@@ -7,5 +7,5 @@ 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
- /** Full domain that consists of at least 3 parts, e.g. foo.bar.com */
11
- export const domainRegEx = /^[\dA-Za-z]+(\.[\dA-Za-z]+){2,}$/;
10
+ /** Full domain that consists of at least 3 parts, e.g. foo.bar.com or example-foo.bar.com */
11
+ export const domainRegEx = /^[\dA-Za-z](?:[\dA-Za-z-]*[\dA-Za-z])?(?:\.[\dA-Za-z](?:[\dA-Za-z-]*[\dA-Za-z])?){2,}$/;
@@ -0,0 +1,2 @@
1
+ export declare const isIntegrationTest: () => boolean;
2
+ export declare const getPwnPasswordsForTest: () => readonly string[];
@@ -0,0 +1,8 @@
1
+ import { yes } from '@silverhand/essentials';
2
+ export const isIntegrationTest = () => yes(process.env.INTEGRATION_TEST);
3
+ export const getPwnPasswordsForTest = () => {
4
+ if (!isIntegrationTest()) {
5
+ throw new Error('This function should only be called in integration tests');
6
+ }
7
+ return Object.freeze(['123456aA', 'test_password']);
8
+ };
@@ -1,3 +1,7 @@
1
1
  export declare const validateRedirectUrl: (url: string, type: 'web' | 'mobile') => boolean;
2
2
  export declare const validateUriOrigin: (url: string) => boolean;
3
3
  export declare const isValidUrl: (url?: string) => boolean;
4
+ /**
5
+ * Check if the given URL is localhost
6
+ */
7
+ export declare const isLocalhost: (url: string) => boolean;
package/lib/utils/url.js CHANGED
@@ -25,3 +25,10 @@ export const isValidUrl = (url) => {
25
25
  return false;
26
26
  }
27
27
  };
28
+ /**
29
+ * Check if the given URL is localhost
30
+ */
31
+ export const isLocalhost = (url) => {
32
+ const parsedUrl = new URL(url);
33
+ return ['localhost', '127.0.0.1', '::1'].includes(parsedUrl.hostname);
34
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/core-kit",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "author": "Silverhand Inc. <contact@silverhand.io>",
5
5
  "homepage": "https://github.com/logto-io/toolkit#readme",
6
6
  "repository": {
@@ -17,7 +17,11 @@
17
17
  "import": "./lib/index.js"
18
18
  },
19
19
  "./declaration": "./declaration/index.ts",
20
- "./scss/*": "./scss/*.scss"
20
+ "./scss/*": "./scss/*.scss",
21
+ "./custom-jwt": {
22
+ "node": "./lib/custom-jwt/index.js",
23
+ "types": "./lib/custom-jwt/index.d.ts"
24
+ }
21
25
  },
22
26
  "types": "./lib/index.d.ts",
23
27
  "files": [
@@ -31,29 +35,28 @@
31
35
  "dependencies": {
32
36
  "@logto/language-kit": "^1.1.0",
33
37
  "@logto/shared": "^3.1.0",
38
+ "@silverhand/essentials": "^2.9.1",
34
39
  "color": "^4.2.3"
35
40
  },
36
41
  "optionalDependencies": {
37
42
  "zod": "^3.22.4"
38
43
  },
39
44
  "devDependencies": {
40
- "@jest/types": "^29.0.3",
41
- "@silverhand/eslint-config": "5.0.0",
42
- "@silverhand/essentials": "^2.9.0",
43
- "@silverhand/ts-config": "5.0.0",
44
- "@silverhand/ts-config-react": "5.0.0",
45
+ "@silverhand/eslint-config": "6.0.1",
46
+ "@silverhand/eslint-config-react": "6.0.2",
47
+ "@silverhand/ts-config": "6.0.0",
48
+ "@silverhand/ts-config-react": "6.0.0",
45
49
  "@types/color": "^3.0.3",
46
- "@types/jest": "^29.4.0",
47
50
  "@types/node": "^20.9.5",
48
51
  "@types/react": "^18.0.31",
49
- "eslint": "^8.44.0",
50
- "jest": "^29.7.0",
52
+ "@vitest/coverage-v8": "^1.4.0",
53
+ "eslint": "^8.56.0",
51
54
  "lint-staged": "^15.0.0",
52
55
  "postcss": "^8.4.31",
53
56
  "prettier": "^3.0.0",
54
57
  "stylelint": "^15.0.0",
55
- "tslib": "^2.4.1",
56
- "typescript": "^5.3.3"
58
+ "typescript": "^5.3.3",
59
+ "vitest": "^1.4.0"
57
60
  },
58
61
  "eslintConfig": {
59
62
  "extends": "@silverhand"
@@ -69,13 +72,11 @@
69
72
  "precommit": "lint-staged",
70
73
  "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
71
74
  "build": "rm -rf lib/ && tsc -p tsconfig.build.json",
72
- "build:test": "pnpm build -p tsconfig.test.json --sourcemap",
75
+ "build:test": "pnpm build",
73
76
  "lint": "eslint --ext .ts src",
74
77
  "lint:report": "pnpm lint --format json --output-file report.json",
75
78
  "stylelint": "stylelint \"scss/**/*.scss\"",
76
- "test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
77
- "test": "pnpm build:test && pnpm test:only",
78
- "test:ci": "pnpm test:only",
79
- "test:coverage": "pnpm test:only --silent --coverage"
79
+ "test": "vitest src",
80
+ "test:ci": "pnpm run test --silent --coverage"
80
81
  }
81
82
  }
@@ -162,7 +162,17 @@
162
162
  --color-specific-icon-bg: #f3effa;
163
163
  --color-specific-toggle-off-enable: var(--color-neutral-90);
164
164
  --color-specific-unselected-disabled: var(--color-hover); // 8% Neutral-10
165
+ --color-specific-selected-disabled: var(--color-primary-80);
166
+ --color-specific-focused-inside: var(--color-primary-30);
167
+ --color-specific-focused-outside: var(--color-primary-40);
168
+ --color-specific-button-icon: rgba(255, 255, 255, 70%); // 70% static white
169
+ --color-overlay-primary-hover: rgba(93, 52, 242, 8%); // 8% Primary-40
170
+ --color-overlay-primary-pressed: rgba(93, 52, 242, 12%); // 12% Primary-40
165
171
  --color-function-n-overlay-primary-focused: rgba(93, 52, 242, 16%); // 16% Primary-40
172
+ --color-overlay-danger-hover: rgba(186, 27, 27, 8%); // 8% Error-40
173
+ --color-overlay-dark-bg-hover: rgba(255, 255, 255, 12%); // 12% static white
174
+ --color-overlay-dark-bg-pressed: rgba(255, 255, 255, 18%); // 18% static white
175
+ --color-overlay-dark-bg-focused: rgba(255, 255, 255, 24%); // 24% static white
166
176
 
167
177
  // Shadows
168
178
  --shadow-1: 0 4px 8px rgba(0, 0, 0, 8%);
@@ -201,6 +211,15 @@
201
211
  --color-bg-state-unselected: var(--color-neutral-90);
202
212
  --color-bg-state-disabled: rgba(25, 28, 29, 8%); // 8% --color-neutral-10
203
213
  --color-bg-info-tag: rgba(229, 225, 236, 80%); // 80% --color-neutral-variant-90
214
+
215
+ // code editor
216
+ --color-code-bg: #181133;
217
+ --color-code-bg-float: #30314e;
218
+ --color-code-white: #f7f8f8;
219
+ --color-code-grey: #adaab4;
220
+ --color-code-dark-bg-focused: rgba(255, 255, 255, 24%);
221
+ --color-code-dark-bg-hover: rgba(255, 255, 255, 12%);
222
+ --color-code-dark-bg-pressed: rgba(255, 255, 255, 18%);
204
223
  }
205
224
 
206
225
  @mixin dark {
@@ -367,7 +386,17 @@
367
386
  --color-specific-icon-bg: rgba(247, 248, 248, 12%);
368
387
  --color-specific-toggle-off-enable: var(--color-neutral-90);
369
388
  --color-specific-unselected-disabled: var(--color-hover); // 8% Neutral-10
389
+ --color-specific-selected-disabled: var(--color-primary-80);
390
+ --color-specific-focused-inside: var(--color-primary-40);
391
+ --color-specific-focused-outside: rgba(#cabeff, 32%); // 32% Primary-40
392
+ --color-specific-button-icon: rgba(255, 255, 255, 60%); // 60% static white
393
+ --color-overlay-primary-hover: rgba(202, 190, 255, 8%); // 8% Primary-40
394
+ --color-overlay-primary-pressed: rgba(202, 190, 255, 12%); // 12% Primary-40
370
395
  --color-function-n-overlay-primary-focused: rgba(202, 190, 255, 16%); // 16% Primary-40
396
+ --color-overlay-danger-hover: rgba(186, 27, 27, 8%); // 8% Error-40
397
+ --color-overlay-dark-bg-hover: rgba(255, 255, 255, 12%); // 12% static white
398
+ --color-overlay-dark-bg-pressed: rgba(255, 255, 255, 18%); // 18% static white
399
+ --color-overlay-dark-bg-focused: rgba(255, 255, 255, 24%); // 24% static white
371
400
 
372
401
  // Shadows
373
402
  --shadow-1: 0 4px 8px rgba(0, 0, 0, 8%);
@@ -409,4 +438,11 @@
409
438
  --color-bg-state-unselected: var(--color-neutral-90);
410
439
  --color-bg-state-disabled: rgba(247, 248, 248, 8%); // 8% --color-neutral-10
411
440
  --color-bg-info-tag: var(--color-neutral-variant-90);
441
+
442
+ // code editor
443
+ --color-code-bg: #090613;
444
+ --color-code-bg-float: #232439;
445
+ --color-code-white: #f7f8f8;
446
+ --color-code-grey: #adaab4;
447
+ --color-code-dark-bg-focused: rgba(255, 255, 255, 24%);
412
448
  }
package/scss/_fonts.scss CHANGED
@@ -30,4 +30,5 @@ $font-family:
30
30
  --font-body-3: 400 12px/16px #{$font-family};
31
31
  --font-section-head-1: 700 12px/16px #{$font-family};
32
32
  --font-section-head-2: 700 10px/16px #{$font-family};
33
+ --font-code: 500 14px/20px 'Roboto Mono', monospace;
33
34
  }