@logto/cli 1.38.0 → 1.39.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/commands/database/alteration/index.js +6 -0
- package/lib/commands/database/config-rotation.d.ts +25 -0
- package/lib/commands/database/config-rotation.js +111 -0
- package/lib/commands/database/config-rotation.test.d.ts +1 -0
- package/lib/commands/database/config-rotation.test.js +36 -0
- package/lib/commands/database/config.js +39 -37
- package/lib/commands/database/seed/oidc-config.js +4 -4
- package/lib/package-json.js +1 -1
- package/package.json +4 -4
|
@@ -27,6 +27,12 @@ export const getAvailableAlterations = async (pool, compareMode = 'gt') => {
|
|
|
27
27
|
? getTimestampFromFilename(filename) > databaseTimestamp
|
|
28
28
|
: getTimestampFromFilename(filename) <= databaseTimestamp);
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* IMPORTANT: Logto Cloud has a parallel implementation of this logic in `CoreAlterationClient`
|
|
32
|
+
* (logto-cloud repo: `packages/azure-functions/src/utils/core-alteration-client.ts`).
|
|
33
|
+
* Any changes to the alteration execution behavior here (e.g., new hooks, execution order)
|
|
34
|
+
* must be synchronized with the Cloud implementation.
|
|
35
|
+
*/
|
|
30
36
|
const deployAlteration = async (pool, { path: filePath, filename }, action = 'up') => {
|
|
31
37
|
const { up, down, beforeUp, beforeDown } = await importAlterationScript(filePath);
|
|
32
38
|
const timestamp = getTimestampFromFilename(filename);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LogtoOidcConfigKey, type SupportedSigningKeyAlgorithm, type OidcConfigKey, type OidcPrivateKey } from '@logto/schemas';
|
|
2
|
+
import type { CommonQueryMethods } from '@silverhand/slonik';
|
|
3
|
+
type RotateConfigResult = {
|
|
4
|
+
keys: OidcPrivateKey[] | OidcConfigKey[];
|
|
5
|
+
};
|
|
6
|
+
export declare const parseRotationGracePeriod: (value?: string) => number | undefined;
|
|
7
|
+
export declare const getEffectiveRotationGracePeriod: ({ key, gracePeriod, envGracePeriod, }: {
|
|
8
|
+
key: LogtoOidcConfigKey.PrivateKeys | LogtoOidcConfigKey.CookieKeys;
|
|
9
|
+
gracePeriod?: number;
|
|
10
|
+
envGracePeriod?: string;
|
|
11
|
+
}) => number;
|
|
12
|
+
export declare const rotateConfigKey: ({ connection, tenantId, key, privateKeyType, gracePeriod, }: {
|
|
13
|
+
connection: CommonQueryMethods;
|
|
14
|
+
tenantId: string;
|
|
15
|
+
key: LogtoOidcConfigKey.PrivateKeys | LogtoOidcConfigKey.CookieKeys;
|
|
16
|
+
privateKeyType: SupportedSigningKeyAlgorithm;
|
|
17
|
+
gracePeriod: number;
|
|
18
|
+
}) => Promise<RotateConfigResult>;
|
|
19
|
+
export declare const trimConfigKey: ({ connection, tenantId, key, length, }: {
|
|
20
|
+
connection: CommonQueryMethods;
|
|
21
|
+
tenantId: string;
|
|
22
|
+
key: LogtoOidcConfigKey.PrivateKeys | LogtoOidcConfigKey.CookieKeys;
|
|
23
|
+
length: number;
|
|
24
|
+
}) => Promise<RotateConfigResult>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getSeededOidcPrivateKeys, getImmediatelyRotatedOidcPrivateKeys, getRotationStateForCacheInvalidation, getRotationStateForStagedRotation, getStagedRotatedOidcPrivateKeys, getTrimmedOidcPrivateKeys, LogtoOidcConfigKey, LogtoTenantConfigKey, normalizeOidcPrivateKeys, logtoConfigGuards, } from '@logto/schemas';
|
|
2
|
+
import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config.js';
|
|
3
|
+
import { consoleLog } from '../../utils.js';
|
|
4
|
+
import { generateOidcCookieKey, generateOidcPrivateKey } from './utils.js';
|
|
5
|
+
export const parseRotationGracePeriod = (value) => {
|
|
6
|
+
if (value === undefined || value.trim() === '') {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
11
|
+
throw new TypeError('Invalid PRIVATE_KEY_ROTATION_GRACE_PERIOD env value');
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
};
|
|
15
|
+
export const getEffectiveRotationGracePeriod = ({ key, gracePeriod, envGracePeriod, }) => {
|
|
16
|
+
if (key === LogtoOidcConfigKey.CookieKeys) {
|
|
17
|
+
if (gracePeriod !== undefined) {
|
|
18
|
+
throw new TypeError(`${LogtoOidcConfigKey.CookieKeys} does not support grace period`);
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
if (gracePeriod !== undefined) {
|
|
23
|
+
if (!Number.isInteger(gracePeriod) || gracePeriod < 0) {
|
|
24
|
+
throw new TypeError('Invalid grace period provided');
|
|
25
|
+
}
|
|
26
|
+
return gracePeriod;
|
|
27
|
+
}
|
|
28
|
+
return parseRotationGracePeriod(envGracePeriod) ?? 0;
|
|
29
|
+
};
|
|
30
|
+
const getRotationStateRow = (rows) => {
|
|
31
|
+
const row = rows.find(({ key }) => key === LogtoTenantConfigKey.SigningKeyRotationState);
|
|
32
|
+
if (!row) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const parsed = logtoConfigGuards[LogtoTenantConfigKey.SigningKeyRotationState].safeParse(row.value);
|
|
36
|
+
return parsed.success ? parsed.data : undefined;
|
|
37
|
+
};
|
|
38
|
+
export const rotateConfigKey = async ({ connection, tenantId, key, privateKeyType, gracePeriod, }) => {
|
|
39
|
+
const { rows } = await getRowsByKeys(connection, tenantId, [
|
|
40
|
+
key,
|
|
41
|
+
LogtoTenantConfigKey.SigningKeyRotationState,
|
|
42
|
+
]);
|
|
43
|
+
const configRow = rows.find((row) => row.key === key);
|
|
44
|
+
const currentRotationState = getRotationStateRow(rows);
|
|
45
|
+
if (!configRow) {
|
|
46
|
+
consoleLog.warn('No key found, create a new one');
|
|
47
|
+
}
|
|
48
|
+
switch (key) {
|
|
49
|
+
case LogtoOidcConfigKey.PrivateKeys: {
|
|
50
|
+
const parsed = logtoConfigGuards[key].safeParse(configRow?.value);
|
|
51
|
+
const original = parsed.success ? parsed.data : [];
|
|
52
|
+
const hasExistingPrivateKeys = original.length > 0;
|
|
53
|
+
const newPrivateKey = await generateOidcPrivateKey(privateKeyType);
|
|
54
|
+
const rotatedPrivateKeys = hasExistingPrivateKeys
|
|
55
|
+
? gracePeriod === 0
|
|
56
|
+
? getImmediatelyRotatedOidcPrivateKeys(original, newPrivateKey)
|
|
57
|
+
: getStagedRotatedOidcPrivateKeys(original, newPrivateKey)
|
|
58
|
+
: getSeededOidcPrivateKeys([newPrivateKey]);
|
|
59
|
+
const rotationState = !hasExistingPrivateKeys || gracePeriod === 0
|
|
60
|
+
? getRotationStateForCacheInvalidation(currentRotationState)
|
|
61
|
+
: getRotationStateForStagedRotation(currentRotationState, gracePeriod);
|
|
62
|
+
await updateValueByKey(connection, tenantId, key, rotatedPrivateKeys);
|
|
63
|
+
await updateValueByKey(connection, tenantId, LogtoTenantConfigKey.SigningKeyRotationState, rotationState);
|
|
64
|
+
return { keys: rotatedPrivateKeys };
|
|
65
|
+
}
|
|
66
|
+
case LogtoOidcConfigKey.CookieKeys: {
|
|
67
|
+
const parsed = logtoConfigGuards[key].safeParse(configRow?.value);
|
|
68
|
+
const original = parsed.success ? parsed.data : [];
|
|
69
|
+
const rotatedCookieKeys = [generateOidcCookieKey(), ...original];
|
|
70
|
+
const rotationState = getRotationStateForCacheInvalidation(currentRotationState);
|
|
71
|
+
await updateValueByKey(connection, tenantId, key, rotatedCookieKeys);
|
|
72
|
+
await updateValueByKey(connection, tenantId, LogtoTenantConfigKey.SigningKeyRotationState, rotationState);
|
|
73
|
+
return { keys: rotatedCookieKeys };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
export const trimConfigKey = async ({ connection, tenantId, key, length, }) => {
|
|
78
|
+
const { rows } = await getRowsByKeys(connection, tenantId, [
|
|
79
|
+
key,
|
|
80
|
+
LogtoTenantConfigKey.SigningKeyRotationState,
|
|
81
|
+
]);
|
|
82
|
+
const configRow = rows.find((row) => row.key === key);
|
|
83
|
+
const currentRotationState = getRotationStateRow(rows);
|
|
84
|
+
if (!configRow) {
|
|
85
|
+
consoleLog.fatal('No key found');
|
|
86
|
+
}
|
|
87
|
+
switch (key) {
|
|
88
|
+
case LogtoOidcConfigKey.PrivateKeys: {
|
|
89
|
+
const value = normalizeOidcPrivateKeys(logtoConfigGuards[key].parse(configRow.value));
|
|
90
|
+
const trimmedPrivateKeys = getTrimmedOidcPrivateKeys(value, length);
|
|
91
|
+
const rotationState = getRotationStateForCacheInvalidation(currentRotationState);
|
|
92
|
+
if (trimmedPrivateKeys.length === 0) {
|
|
93
|
+
consoleLog.fatal(`You should keep at least one key in the array, current length=${value.length}`);
|
|
94
|
+
}
|
|
95
|
+
await updateValueByKey(connection, tenantId, key, trimmedPrivateKeys);
|
|
96
|
+
await updateValueByKey(connection, tenantId, LogtoTenantConfigKey.SigningKeyRotationState, rotationState);
|
|
97
|
+
return { keys: trimmedPrivateKeys };
|
|
98
|
+
}
|
|
99
|
+
case LogtoOidcConfigKey.CookieKeys: {
|
|
100
|
+
const value = logtoConfigGuards[key].parse(configRow.value);
|
|
101
|
+
const rotationState = getRotationStateForCacheInvalidation(currentRotationState);
|
|
102
|
+
if (value.length - length < 1) {
|
|
103
|
+
consoleLog.fatal(`You should keep at least one key in the array, current length=${value.length}`);
|
|
104
|
+
}
|
|
105
|
+
const trimmedCookieKeys = value.slice(0, -length);
|
|
106
|
+
await updateValueByKey(connection, tenantId, key, trimmedCookieKeys);
|
|
107
|
+
await updateValueByKey(connection, tenantId, LogtoTenantConfigKey.SigningKeyRotationState, rotationState);
|
|
108
|
+
return { keys: trimmedCookieKeys };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
const { getEffectiveRotationGracePeriod, parseRotationGracePeriod } = await import('./config-rotation.js');
|
|
3
|
+
const { LogtoOidcConfigKey } = await import('@logto/schemas');
|
|
4
|
+
describe('parseRotationGracePeriod()', () => {
|
|
5
|
+
it('returns undefined when env is not set', () => {
|
|
6
|
+
expect(parseRotationGracePeriod()).toBeUndefined();
|
|
7
|
+
expect(parseRotationGracePeriod(' ')).toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
it('parses a valid non-negative integer', () => {
|
|
10
|
+
expect(parseRotationGracePeriod('14400')).toBe(14_400);
|
|
11
|
+
});
|
|
12
|
+
it('throws on malformed input instead of silently falling back to 0', () => {
|
|
13
|
+
expect(() => parseRotationGracePeriod('4h')).toThrow('Invalid PRIVATE_KEY_ROTATION_GRACE_PERIOD env value');
|
|
14
|
+
expect(() => parseRotationGracePeriod('-1')).toThrow('Invalid PRIVATE_KEY_ROTATION_GRACE_PERIOD env value');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('getEffectiveRotationGracePeriod()', () => {
|
|
18
|
+
it('uses the explicit private-key grace period when provided', () => {
|
|
19
|
+
expect(getEffectiveRotationGracePeriod({
|
|
20
|
+
key: LogtoOidcConfigKey.PrivateKeys,
|
|
21
|
+
gracePeriod: 60,
|
|
22
|
+
})).toBe(60);
|
|
23
|
+
});
|
|
24
|
+
it('uses the env default for private keys when no explicit grace period is provided', () => {
|
|
25
|
+
expect(getEffectiveRotationGracePeriod({
|
|
26
|
+
key: LogtoOidcConfigKey.PrivateKeys,
|
|
27
|
+
envGracePeriod: '120',
|
|
28
|
+
})).toBe(120);
|
|
29
|
+
});
|
|
30
|
+
it('rejects cookie-key grace period even when the explicit value is 0', () => {
|
|
31
|
+
expect(() => getEffectiveRotationGracePeriod({
|
|
32
|
+
key: LogtoOidcConfigKey.CookieKeys,
|
|
33
|
+
gracePeriod: 0,
|
|
34
|
+
})).toThrow(`${LogtoOidcConfigKey.CookieKeys} does not support grace period`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { createPoolFromConfig } from '../../database.js';
|
|
5
5
|
import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config.js';
|
|
6
6
|
import { consoleLog } from '../../utils.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getEffectiveRotationGracePeriod, rotateConfigKey, trimConfigKey, } from './config-rotation.js';
|
|
8
8
|
const validKeysDisplay = chalk.green(logtoConfigKeys.join(', '));
|
|
9
9
|
const validateKeys = (keys) => {
|
|
10
10
|
const invalidKey = (Array.isArray(keys) ? keys : [keys]).find(
|
|
@@ -37,6 +37,10 @@ const validatePrivateKeyType = (key) => {
|
|
|
37
37
|
consoleLog.fatal(`Invalid private key type ${chalk.red(key)} found, expected one of ${validPrivateKeyTypes.join(', ')}`);
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
|
+
const getValidatedPrivateKeyType = (key) => {
|
|
41
|
+
validatePrivateKeyType(key);
|
|
42
|
+
return key;
|
|
43
|
+
};
|
|
40
44
|
const getConfig = {
|
|
41
45
|
command: 'get <key> [keys...]',
|
|
42
46
|
describe: 'Get config value(s) of the given key(s) in Logto database',
|
|
@@ -103,7 +107,7 @@ const setConfig = {
|
|
|
103
107
|
};
|
|
104
108
|
const rotateConfig = {
|
|
105
109
|
command: 'rotate <key>',
|
|
106
|
-
describe: 'Generate a new private or secret key for the given config key and
|
|
110
|
+
describe: 'Generate a new private or secret key for the given config key and update the key state',
|
|
107
111
|
builder: (yargs) => yargs
|
|
108
112
|
.positional('key', {
|
|
109
113
|
describe: `The key to rotate, one of ${chalk.green(validRotateKeys.join(', '))}`,
|
|
@@ -119,31 +123,37 @@ const rotateConfig = {
|
|
|
119
123
|
describe: `The key type for ${LogtoOidcConfigKey.PrivateKeys}, one of ${validPrivateKeyTypes.join(', ')}`,
|
|
120
124
|
type: 'string',
|
|
121
125
|
default: 'ec',
|
|
126
|
+
})
|
|
127
|
+
.option('gracePeriod', {
|
|
128
|
+
describe: `Grace period in seconds for ${LogtoOidcConfigKey.PrivateKeys} staged rotation`,
|
|
129
|
+
type: 'number',
|
|
122
130
|
}),
|
|
123
|
-
handler: async ({ key, tenantId, type }) => {
|
|
131
|
+
handler: async ({ key, tenantId, type, gracePeriod }) => {
|
|
124
132
|
const keyType = type.toUpperCase();
|
|
133
|
+
const privateKeyType = key === LogtoOidcConfigKey.PrivateKeys
|
|
134
|
+
? getValidatedPrivateKeyType(keyType)
|
|
135
|
+
: SupportedSigningKeyAlgorithm.EC;
|
|
125
136
|
validateRotateKey(key);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const parsed = logtoConfigGuards[key].safeParse(rows[0]?.value);
|
|
134
|
-
const original = parsed.success ? parsed.data : [];
|
|
135
|
-
// No need for default. It's already exhaustive
|
|
136
|
-
switch (key) {
|
|
137
|
-
case LogtoOidcConfigKey.PrivateKeys: {
|
|
138
|
-
return [await generateOidcPrivateKey(keyType), ...original];
|
|
139
|
-
}
|
|
140
|
-
case LogtoOidcConfigKey.CookieKeys: {
|
|
141
|
-
return [generateOidcCookieKey(), ...original];
|
|
142
|
-
}
|
|
137
|
+
const effectiveGracePeriod = (() => {
|
|
138
|
+
try {
|
|
139
|
+
return getEffectiveRotationGracePeriod({
|
|
140
|
+
key,
|
|
141
|
+
gracePeriod,
|
|
142
|
+
envGracePeriod: process.env.PRIVATE_KEY_ROTATION_GRACE_PERIOD,
|
|
143
|
+
});
|
|
143
144
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
catch (error) {
|
|
146
|
+
return consoleLog.fatal(error instanceof Error ? error.message : 'Invalid grace period provided');
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
const pool = await createPoolFromConfig();
|
|
150
|
+
const { keys: rotated } = await pool.transaction(async (connection) => rotateConfigKey({
|
|
151
|
+
connection,
|
|
152
|
+
tenantId,
|
|
153
|
+
key,
|
|
154
|
+
privateKeyType,
|
|
155
|
+
gracePeriod: effectiveGracePeriod,
|
|
156
|
+
}));
|
|
147
157
|
await pool.end();
|
|
148
158
|
consoleLog.info(`Rotate ${chalk.green(key)} succeeded, now it has ${rotated.length} keys`);
|
|
149
159
|
},
|
|
@@ -174,20 +184,12 @@ const trimConfig = {
|
|
|
174
184
|
consoleLog.fatal('Invalid length provided');
|
|
175
185
|
}
|
|
176
186
|
const pool = await createPoolFromConfig();
|
|
177
|
-
const {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (value.length - length < 1) {
|
|
184
|
-
await pool.end();
|
|
185
|
-
consoleLog.fatal(`You should keep at least one key in the array, current length=${value.length}`);
|
|
186
|
-
}
|
|
187
|
-
return value.slice(0, -length);
|
|
188
|
-
};
|
|
189
|
-
const trimmed = await getValue();
|
|
190
|
-
await updateValueByKey(pool, tenantId, key, trimmed);
|
|
187
|
+
const { keys: trimmed } = await pool.transaction(async (connection) => trimConfigKey({
|
|
188
|
+
connection,
|
|
189
|
+
tenantId,
|
|
190
|
+
key,
|
|
191
|
+
length,
|
|
192
|
+
}));
|
|
191
193
|
await pool.end();
|
|
192
194
|
consoleLog.info(`Trim ${chalk.green(key)} succeeded, now it has ${trimmed.length} keys`);
|
|
193
195
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { LogtoOidcConfigKey, logtoConfigGuards } from '@logto/schemas';
|
|
2
|
+
import { LogtoOidcConfigKey, getSeededOidcPrivateKeys, logtoConfigGuards } from '@logto/schemas';
|
|
3
3
|
import { generateStandardId } from '@logto/shared';
|
|
4
4
|
import { getEnvAsStringArray } from '@silverhand/essentials';
|
|
5
5
|
import chalk from 'chalk';
|
|
@@ -69,7 +69,7 @@ export const oidcConfigReaders = {
|
|
|
69
69
|
const privateKeys = getEnvAsStringArray('OIDC_PRIVATE_KEYS');
|
|
70
70
|
if (privateKeys.length > 0) {
|
|
71
71
|
return {
|
|
72
|
-
value: privateKeys.map((key) => buildOidcKeyFromRawString(isBase64FormatPrivateKey(key) ? Buffer.from(key, 'base64').toString('utf8') : key)),
|
|
72
|
+
value: getSeededOidcPrivateKeys(privateKeys.map((key) => buildOidcKeyFromRawString(isBase64FormatPrivateKey(key) ? Buffer.from(key, 'base64').toString('utf8') : key))),
|
|
73
73
|
fromEnv: true,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
@@ -78,12 +78,12 @@ export const oidcConfigReaders = {
|
|
|
78
78
|
if (privateKeyPaths.length > 0) {
|
|
79
79
|
const privateKeys = await Promise.all(privateKeyPaths.map(async (path) => readFile(path, 'utf8')));
|
|
80
80
|
return {
|
|
81
|
-
value: privateKeys.map((key) => buildOidcKeyFromRawString(key)),
|
|
81
|
+
value: getSeededOidcPrivateKeys(privateKeys.map((key) => buildOidcKeyFromRawString(key))),
|
|
82
82
|
fromEnv: true,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
return {
|
|
86
|
-
value: [await generateOidcPrivateKey()],
|
|
86
|
+
value: getSeededOidcPrivateKeys([await generateOidcPrivateKey()]),
|
|
87
87
|
fromEnv: false,
|
|
88
88
|
};
|
|
89
89
|
},
|
package/lib/package-json.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"yargs": "^17.6.0",
|
|
47
47
|
"zod": "3.24.3",
|
|
48
48
|
"@logto/connector-kit": "^5.0.0",
|
|
49
|
-
"@logto/core-kit": "^2.
|
|
50
|
-
"@logto/schemas": "1.
|
|
51
|
-
"@logto/shared": "^3.
|
|
49
|
+
"@logto/core-kit": "^2.9.0",
|
|
50
|
+
"@logto/schemas": "1.39.0",
|
|
51
|
+
"@logto/shared": "^3.4.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@silverhand/eslint-config": "6.0.1",
|