@logto/cli 1.0.0-rc.1 → 1.0.0-rc.3
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 +6 -0
- package/lib/commands/connector/add.d.ts +1 -0
- package/lib/commands/connector/add.js +13 -3
- package/lib/commands/connector/remove.js +1 -1
- package/lib/commands/connector/utils.d.ts +2 -2
- package/lib/commands/connector/utils.js +6 -5
- package/lib/commands/database/alteration/index.js +1 -1
- package/lib/commands/database/alteration/index.test.js +1 -1
- package/lib/commands/database/alteration/utils.js +1 -1
- package/lib/commands/database/alteration/version.js +1 -1
- package/lib/commands/database/config.js +39 -17
- package/lib/commands/database/index.js +2 -1
- package/lib/commands/database/seed/cloud.d.ts +10 -0
- package/lib/commands/database/seed/cloud.js +34 -0
- package/lib/commands/database/seed/index.d.ts +2 -4
- package/lib/commands/database/seed/index.js +24 -129
- package/lib/commands/database/seed/oidc-config.d.ts +2 -0
- package/lib/commands/database/seed/oidc-config.js +45 -2
- package/lib/commands/database/seed/tables.d.ts +4 -0
- package/lib/commands/database/seed/tables.js +109 -0
- package/lib/commands/database/seed/tenant.d.ts +5 -0
- package/lib/commands/database/seed/tenant.js +53 -0
- package/lib/commands/database/system.d.ts +3 -0
- package/lib/commands/database/system.js +63 -0
- package/lib/commands/database/{utilities.js → utils.js} +2 -2
- package/lib/commands/install/index.d.ts +2 -2
- package/lib/commands/install/index.js +9 -8
- package/lib/commands/install/utils.d.ts +1 -1
- package/lib/commands/install/utils.js +3 -3
- package/lib/connector/consts.d.ts +7 -0
- package/lib/connector/consts.js +10 -0
- package/lib/connector/factories.d.ts +2 -0
- package/lib/connector/factories.js +34 -0
- package/lib/connector/index.d.ts +4 -0
- package/lib/connector/index.js +4 -0
- package/lib/connector/loader.d.ts +2 -0
- package/lib/connector/loader.js +48 -0
- package/lib/connector/types.d.ts +12 -0
- package/lib/connector/types.js +1 -0
- package/lib/connector/utils.d.ts +303 -0
- package/lib/connector/utils.js +51 -0
- package/lib/database.js +3 -4
- package/lib/index.js +1 -1
- package/lib/package-json.d.ts +2 -3
- package/lib/package-json.js +10 -14
- package/lib/queries/database.d.ts +2 -0
- package/lib/queries/database.js +7 -0
- package/lib/queries/logto-config.d.ts +2 -2
- package/lib/queries/logto-config.js +6 -5
- package/lib/queries/system.d.ts +5 -0
- package/lib/queries/system.js +13 -4
- package/lib/queries/system.test.js +7 -3
- package/package.json +12 -16
- /package/lib/commands/database/{utilities.d.ts → utils.d.ts} +0 -0
- /package/lib/{test-utilities.d.ts → test-utils.d.ts} +0 -0
- /package/lib/{test-utilities.js → test-utils.js} +0 -0
- /package/lib/{utilities.d.ts → utils.d.ts} +0 -0
- /package/lib/{utilities.js → utils.js} +0 -0
package/LICENSE
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
Mozilla Public License Version 2.0
|
|
2
8
|
==================================
|
|
3
9
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { log } from '../../
|
|
1
|
+
import { log } from '../../utils.js';
|
|
2
2
|
import { addConnectors, addOfficialConnectors, inquireInstancePath } from './utils.js';
|
|
3
3
|
const add = {
|
|
4
4
|
command: ['add [packages...]', 'a', 'install', 'i'],
|
|
@@ -16,11 +16,21 @@ const add = {
|
|
|
16
16
|
default: false,
|
|
17
17
|
describe: 'Add all official connectors.\n' +
|
|
18
18
|
"If it's true, the specified package names will be ignored.",
|
|
19
|
+
})
|
|
20
|
+
.option('cloud', {
|
|
21
|
+
describe: 'Add additional connectors for Logto Cloud',
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
default: false,
|
|
24
|
+
hidden: true,
|
|
19
25
|
}),
|
|
20
|
-
handler: async ({ packages: packageNames, path, official }) => {
|
|
26
|
+
handler: async ({ packages: packageNames, path, official, cloud }) => {
|
|
21
27
|
const instancePath = await inquireInstancePath(path);
|
|
28
|
+
if (cloud && !official) {
|
|
29
|
+
log.error('--cloud option can only be used with --official option');
|
|
30
|
+
}
|
|
22
31
|
if (official) {
|
|
23
|
-
|
|
32
|
+
// Add demo connectors for Logto Cloud only
|
|
33
|
+
await addOfficialConnectors(instancePath, cloud);
|
|
24
34
|
}
|
|
25
35
|
else {
|
|
26
36
|
if (!packageNames?.length) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { log } from '../../
|
|
3
|
+
import { log } from '../../utils.js';
|
|
4
4
|
import { getConnectorPackagesFrom } from './utils.js';
|
|
5
5
|
const remove = {
|
|
6
6
|
command: ['remove [packages...]', 'rm', 'delete'],
|
|
@@ -15,6 +15,6 @@ export declare const npmPackResultGuard: z.ZodArray<z.ZodObject<{
|
|
|
15
15
|
export declare const inquireInstancePath: (initialPath?: string) => Promise<string>;
|
|
16
16
|
export declare const normalizePackageName: (name: string) => string;
|
|
17
17
|
export declare const isOfficialConnector: (packageName: string) => boolean;
|
|
18
|
-
export declare const getConnectorPackagesFrom: (instancePath?: string) => Promise<import("../../
|
|
18
|
+
export declare const getConnectorPackagesFrom: (instancePath?: string) => Promise<import("../../utils.js").ConnectorPackage[]>;
|
|
19
19
|
export declare const addConnectors: (instancePath: string, packageNames: string[]) => Promise<void>;
|
|
20
|
-
export declare const addOfficialConnectors: (instancePath: string) => Promise<void>;
|
|
20
|
+
export declare const addOfficialConnectors: (instancePath: string, includingCloudConnectors?: boolean) => Promise<void>;
|
|
@@ -12,7 +12,7 @@ import pRetry from 'p-retry';
|
|
|
12
12
|
import tar from 'tar';
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
import { connectorDirectory } from '../../constants.js';
|
|
15
|
-
import { getConnectorPackagesFromDirectory, isTty, log, oraPromise } from '../../
|
|
15
|
+
import { getConnectorPackagesFromDirectory, isTty, log, oraPromise } from '../../utils.js';
|
|
16
16
|
import { defaultPath } from '../install/utils.js';
|
|
17
17
|
const coreDirectory = 'packages/core';
|
|
18
18
|
const execPromise = promisify(exec);
|
|
@@ -127,7 +127,7 @@ export const addConnectors = async (instancePath, packageNames) => {
|
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
const officialConnectorPrefix = '@logto/connector-';
|
|
130
|
-
const fetchOfficialConnectorList = async () => {
|
|
130
|
+
const fetchOfficialConnectorList = async (includingCloudConnectors = false) => {
|
|
131
131
|
const fetchList = async (from = 0, size = 20) => {
|
|
132
132
|
const parameters = new URLSearchParams({
|
|
133
133
|
text: officialConnectorPrefix,
|
|
@@ -142,10 +142,11 @@ const fetchOfficialConnectorList = async () => {
|
|
|
142
142
|
for (let page = 0;; ++page) {
|
|
143
143
|
// eslint-disable-next-line no-await-in-loop
|
|
144
144
|
const { objects } = await fetchList(page * 20, 20);
|
|
145
|
+
const excludeList = ['mock', 'kit', ...(includingCloudConnectors ? [] : ['logto'])];
|
|
145
146
|
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
146
147
|
packages.push(...objects
|
|
147
148
|
.filter(({ package: { name, scope } }) => scope === 'logto' &&
|
|
148
|
-
|
|
149
|
+
excludeList.every((excluded) => !name.slice(officialConnectorPrefix.length).startsWith(excluded)))
|
|
149
150
|
.map(({ package: data }) => data));
|
|
150
151
|
if (objects.length < 20) {
|
|
151
152
|
break;
|
|
@@ -153,8 +154,8 @@ const fetchOfficialConnectorList = async () => {
|
|
|
153
154
|
}
|
|
154
155
|
return packages;
|
|
155
156
|
};
|
|
156
|
-
export const addOfficialConnectors = async (instancePath) => {
|
|
157
|
-
const packages = await oraPromise(fetchOfficialConnectorList(), {
|
|
157
|
+
export const addOfficialConnectors = async (instancePath, includingCloudConnectors = false) => {
|
|
158
|
+
const packages = await oraPromise(fetchOfficialConnectorList(includingCloudConnectors), {
|
|
158
159
|
text: 'Fetch official connector list',
|
|
159
160
|
prefixText: chalk.blue('[info]'),
|
|
160
161
|
});
|
|
@@ -2,7 +2,7 @@ import { conditionalString } from '@silverhand/essentials';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { createPoolFromConfig } from '../../../database.js';
|
|
4
4
|
import { getCurrentDatabaseAlterationTimestamp, updateDatabaseTimestamp, } from '../../../queries/system.js';
|
|
5
|
-
import { log } from '../../../
|
|
5
|
+
import { log } from '../../../utils.js';
|
|
6
6
|
import { getAlterationFiles, getTimestampFromFilename } from './utils.js';
|
|
7
7
|
import { chooseAlterationsByVersion, chooseRevertAlterationsByVersion } from './version.js';
|
|
8
8
|
const importAlterationScript = async (filePath) => {
|
|
@@ -25,7 +25,7 @@ describe('getAvailableAlterations()', () => {
|
|
|
25
25
|
await expect(getAvailableAlterations(pool)).resolves.toEqual(files);
|
|
26
26
|
});
|
|
27
27
|
it('returns files whose timestamp is greater then database timestamp', async () => {
|
|
28
|
-
getCurrentDatabaseAlterationTimestamp.mockResolvedValue(
|
|
28
|
+
getCurrentDatabaseAlterationTimestamp.mockResolvedValue(1_663_923_770);
|
|
29
29
|
await expect(getAvailableAlterations(pool)).resolves.toEqual([files[1], files[2]]);
|
|
30
30
|
});
|
|
31
31
|
});
|
|
@@ -3,7 +3,7 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { findPackage } from '@logto/shared';
|
|
6
|
-
import { getPathInModule } from '../../../
|
|
6
|
+
import { getPathInModule } from '../../../utils.js';
|
|
7
7
|
const currentDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const alterationFilenameRegex = /-([\d.]+)-?.*\.js$/;
|
|
9
9
|
export const getTimestampFromFilename = (filename) => {
|
|
@@ -2,7 +2,7 @@ import { assert, conditional } from '@silverhand/essentials';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import { SemVer, compare, eq, gt } from 'semver';
|
|
5
|
-
import { findLastIndex, isTty, log } from '../../../
|
|
5
|
+
import { findLastIndex, isTty, log } from '../../../utils.js';
|
|
6
6
|
const getVersionStringFromFilename = (filename) => filename.split('-')[0]?.replaceAll('_', '-') ?? 'unknown';
|
|
7
7
|
const getVersionFromFilename = (filename) => {
|
|
8
8
|
try {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { LogtoOidcConfigKey, logtoConfigGuards, logtoConfigKeys } from '@logto/schemas';
|
|
1
|
+
import { defaultTenantId, LogtoOidcConfigKey, logtoConfigGuards, logtoConfigKeys, } from '@logto/schemas';
|
|
2
2
|
import { deduplicate, noop } from '@silverhand/essentials';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { createPoolFromConfig } from '../../database.js';
|
|
5
5
|
import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config.js';
|
|
6
|
-
import { log } from '../../
|
|
7
|
-
import { generateOidcCookieKey, generateOidcPrivateKey } from './
|
|
6
|
+
import { log } from '../../utils.js';
|
|
7
|
+
import { generateOidcCookieKey, generateOidcPrivateKey } from './utils.js';
|
|
8
8
|
const validKeysDisplay = chalk.green(logtoConfigKeys.join(', '));
|
|
9
9
|
const validateKeys = (keys) => {
|
|
10
10
|
const invalidKey = (Array.isArray(keys) ? keys : [keys]).find(
|
|
@@ -40,12 +40,17 @@ const getConfig = {
|
|
|
40
40
|
type: 'string',
|
|
41
41
|
array: true,
|
|
42
42
|
default: [],
|
|
43
|
+
})
|
|
44
|
+
.option('tenantId', {
|
|
45
|
+
describe: 'The tenant to operate',
|
|
46
|
+
type: 'string',
|
|
47
|
+
default: defaultTenantId,
|
|
43
48
|
}),
|
|
44
|
-
handler: async ({ key, keys }) => {
|
|
49
|
+
handler: async ({ key, keys, tenantId }) => {
|
|
45
50
|
const queryKeys = deduplicate([key, ...keys]);
|
|
46
51
|
validateKeys(queryKeys);
|
|
47
52
|
const pool = await createPoolFromConfig();
|
|
48
|
-
const { rows } = await getRowsByKeys(pool, queryKeys);
|
|
53
|
+
const { rows } = await getRowsByKeys(pool, tenantId, queryKeys);
|
|
49
54
|
await pool.end();
|
|
50
55
|
console.log(queryKeys
|
|
51
56
|
.map((currentKey) => {
|
|
@@ -70,12 +75,17 @@ const setConfig = {
|
|
|
70
75
|
describe: 'The value to set, should be a valid JSON string',
|
|
71
76
|
type: 'string',
|
|
72
77
|
demandOption: true,
|
|
78
|
+
})
|
|
79
|
+
.option('tenantId', {
|
|
80
|
+
describe: 'The tenant to operate',
|
|
81
|
+
type: 'string',
|
|
82
|
+
default: defaultTenantId,
|
|
73
83
|
}),
|
|
74
|
-
handler: async ({ key, value }) => {
|
|
84
|
+
handler: async ({ key, value, tenantId }) => {
|
|
75
85
|
validateKeys(key);
|
|
76
86
|
const guarded = logtoConfigGuards[key].parse(JSON.parse(value));
|
|
77
87
|
const pool = await createPoolFromConfig();
|
|
78
|
-
await updateValueByKey(pool, key, guarded);
|
|
88
|
+
await updateValueByKey(pool, tenantId, key, guarded);
|
|
79
89
|
await pool.end();
|
|
80
90
|
log.info(`Update ${chalk.green(key)} succeeded`);
|
|
81
91
|
},
|
|
@@ -83,15 +93,21 @@ const setConfig = {
|
|
|
83
93
|
const rotateConfig = {
|
|
84
94
|
command: 'rotate <key>',
|
|
85
95
|
describe: 'Generate a new private or secret key for the given config key and prepend to the key array',
|
|
86
|
-
builder: (yargs) => yargs
|
|
96
|
+
builder: (yargs) => yargs
|
|
97
|
+
.positional('key', {
|
|
87
98
|
describe: `The key to rotate, one of ${chalk.green(validRotateKeys.join(', '))}`,
|
|
88
99
|
type: 'string',
|
|
89
100
|
demandOption: true,
|
|
101
|
+
})
|
|
102
|
+
.option('tenantId', {
|
|
103
|
+
describe: 'The tenant to operate',
|
|
104
|
+
type: 'string',
|
|
105
|
+
default: defaultTenantId,
|
|
90
106
|
}),
|
|
91
|
-
handler: async ({ key }) => {
|
|
107
|
+
handler: async ({ key, tenantId }) => {
|
|
92
108
|
validateRotateKey(key);
|
|
93
109
|
const pool = await createPoolFromConfig();
|
|
94
|
-
const { rows } = await getRowsByKeys(pool, [key]);
|
|
110
|
+
const { rows } = await getRowsByKeys(pool, tenantId, [key]);
|
|
95
111
|
if (!rows[0]) {
|
|
96
112
|
log.warn('No key found, create a new one');
|
|
97
113
|
}
|
|
@@ -99,16 +115,17 @@ const rotateConfig = {
|
|
|
99
115
|
const parsed = logtoConfigGuards[key].safeParse(rows[0]?.value);
|
|
100
116
|
const original = parsed.success ? parsed.data : [];
|
|
101
117
|
// No need for default. It's already exhaustive
|
|
102
|
-
// eslint-disable-next-line default-case
|
|
103
118
|
switch (key) {
|
|
104
|
-
case LogtoOidcConfigKey.PrivateKeys:
|
|
119
|
+
case LogtoOidcConfigKey.PrivateKeys: {
|
|
105
120
|
return [await generateOidcPrivateKey(), ...original];
|
|
106
|
-
|
|
121
|
+
}
|
|
122
|
+
case LogtoOidcConfigKey.CookieKeys: {
|
|
107
123
|
return [generateOidcCookieKey(), ...original];
|
|
124
|
+
}
|
|
108
125
|
}
|
|
109
126
|
};
|
|
110
127
|
const rotated = await getValue();
|
|
111
|
-
await updateValueByKey(pool, key, rotated);
|
|
128
|
+
await updateValueByKey(pool, tenantId, key, rotated);
|
|
112
129
|
await pool.end();
|
|
113
130
|
log.info(`Rotate ${chalk.green(key)} succeeded, now it has ${rotated.length} keys`);
|
|
114
131
|
},
|
|
@@ -127,14 +144,19 @@ const trimConfig = {
|
|
|
127
144
|
type: 'number',
|
|
128
145
|
default: 1,
|
|
129
146
|
demandOption: true,
|
|
147
|
+
})
|
|
148
|
+
.option('tenantId', {
|
|
149
|
+
describe: 'The tenant to operate',
|
|
150
|
+
type: 'string',
|
|
151
|
+
default: defaultTenantId,
|
|
130
152
|
}),
|
|
131
|
-
handler: async ({ key, length }) => {
|
|
153
|
+
handler: async ({ key, length, tenantId }) => {
|
|
132
154
|
validateRotateKey(key);
|
|
133
155
|
if (length < 1) {
|
|
134
156
|
log.error('Invalid length provided');
|
|
135
157
|
}
|
|
136
158
|
const pool = await createPoolFromConfig();
|
|
137
|
-
const { rows } = await getRowsByKeys(pool, [key]);
|
|
159
|
+
const { rows } = await getRowsByKeys(pool, tenantId, [key]);
|
|
138
160
|
if (!rows[0]) {
|
|
139
161
|
log.warn('No key found, create a new one');
|
|
140
162
|
}
|
|
@@ -147,7 +169,7 @@ const trimConfig = {
|
|
|
147
169
|
return value.slice(0, -length);
|
|
148
170
|
};
|
|
149
171
|
const trimmed = await getValue();
|
|
150
|
-
await updateValueByKey(pool, key, trimmed);
|
|
172
|
+
await updateValueByKey(pool, tenantId, key, trimmed);
|
|
151
173
|
await pool.end();
|
|
152
174
|
log.info(`Trim ${chalk.green(key)} succeeded, now it has ${trimmed.length} keys`);
|
|
153
175
|
},
|
|
@@ -2,10 +2,11 @@ import { noop } from '@silverhand/essentials';
|
|
|
2
2
|
import alteration from './alteration/index.js';
|
|
3
3
|
import config from './config.js';
|
|
4
4
|
import seed from './seed/index.js';
|
|
5
|
+
import system from './system.js';
|
|
5
6
|
const database = {
|
|
6
7
|
command: ['database', 'db'],
|
|
7
8
|
describe: 'Commands for Logto database',
|
|
8
|
-
builder: (yargs) => yargs.command(config).command(seed).command(alteration).demandCommand(1),
|
|
9
|
+
builder: (yargs) => yargs.command(config).command(seed).command(alteration).command(system).demandCommand(1),
|
|
9
10
|
handler: noop,
|
|
10
11
|
};
|
|
11
12
|
export default database;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommonQueryMethods } from 'slonik';
|
|
2
|
+
/**
|
|
3
|
+
* Append Redirect URIs for the default tenant callback in cloud Admin Console.
|
|
4
|
+
* It reads the same env variables as core to construct the cloud `UrlSet`.
|
|
5
|
+
*
|
|
6
|
+
* E.g., by default, it will appends `http://localhost:3003/default/callback` to the Redirect URIs.
|
|
7
|
+
*
|
|
8
|
+
* For why it is necessary, see the redirect lifecycle of cloud Admin Console.
|
|
9
|
+
*/
|
|
10
|
+
export declare const appendAdminConsoleRedirectUris: (pool: CommonQueryMethods) => Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { adminConsoleApplicationId, adminTenantId, defaultTenantId } from '@logto/schemas';
|
|
2
|
+
import { GlobalValues } from '@logto/shared';
|
|
3
|
+
import { appendPath } from '@silverhand/essentials';
|
|
4
|
+
import { sql } from 'slonik';
|
|
5
|
+
import { log } from '../../../utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* Append Redirect URIs for the default tenant callback in cloud Admin Console.
|
|
8
|
+
* It reads the same env variables as core to construct the cloud `UrlSet`.
|
|
9
|
+
*
|
|
10
|
+
* E.g., by default, it will appends `http://localhost:3003/default/callback` to the Redirect URIs.
|
|
11
|
+
*
|
|
12
|
+
* For why it is necessary, see the redirect lifecycle of cloud Admin Console.
|
|
13
|
+
*/
|
|
14
|
+
export const appendAdminConsoleRedirectUris = async (pool) => {
|
|
15
|
+
const redirectUris = new GlobalValues().cloudUrlSet
|
|
16
|
+
.deduplicated()
|
|
17
|
+
.flatMap((endpoint) => [defaultTenantId, adminTenantId].map((tenantId) => appendPath(endpoint, tenantId, 'callback')));
|
|
18
|
+
const metadataKey = sql.identifier(['oidc_client_metadata']);
|
|
19
|
+
// Copied from packages/cloud/src/queries/tenants.ts
|
|
20
|
+
// Can be merged into the original once we remove slonik
|
|
21
|
+
await pool.query(sql `
|
|
22
|
+
update applications
|
|
23
|
+
set ${metadataKey} = jsonb_set(
|
|
24
|
+
${metadataKey},
|
|
25
|
+
'{redirectUris}',
|
|
26
|
+
(select jsonb_agg(distinct value) from jsonb_array_elements(
|
|
27
|
+
${metadataKey}->'redirectUris' || ${sql.jsonb(redirectUris.map(String))}
|
|
28
|
+
))
|
|
29
|
+
)
|
|
30
|
+
where id = ${adminConsoleApplicationId}
|
|
31
|
+
and tenant_id = ${adminTenantId}
|
|
32
|
+
`);
|
|
33
|
+
log.succeed('Appended initial Redirect URIs to Admin Console:', redirectUris.map(String));
|
|
34
|
+
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { DatabasePool } from 'slonik';
|
|
2
2
|
import type { CommandModule } from 'yargs';
|
|
3
|
-
declare const
|
|
4
|
-
type SeedChoice = typeof seedChoices[number];
|
|
5
|
-
export declare const seedByPool: (pool: DatabasePool, type: SeedChoice) => Promise<void>;
|
|
3
|
+
export declare const seedByPool: (pool: DatabasePool, cloud?: boolean) => Promise<void>;
|
|
6
4
|
declare const seed: CommandModule<Record<string, unknown>, {
|
|
7
|
-
type: string;
|
|
8
5
|
swe?: boolean;
|
|
6
|
+
cloud?: boolean;
|
|
9
7
|
}>;
|
|
10
8
|
export default seed;
|
|
@@ -1,127 +1,26 @@
|
|
|
1
|
-
import { readdir, readFile } from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { generateStandardId } from '@logto/core-kit';
|
|
4
|
-
import { logtoConfigGuards, LogtoOidcConfigKey, managementResource, defaultSignInExperience, createDefaultAdminConsoleConfig, createDemoAppApplication, defaultRole, managementResourceScope, defaultRoleScopeRelation, defaultTenant, } from '@logto/schemas';
|
|
5
|
-
import { Hooks, Tenants } from '@logto/schemas/models';
|
|
6
1
|
import chalk from 'chalk';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { createPoolAndDatabaseIfNeeded, insertInto } from '../../../database.js';
|
|
11
|
-
import { getRowsByKeys, doesConfigsTableExist, updateValueByKey, } from '../../../queries/logto-config.js';
|
|
12
|
-
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
|
13
|
-
import { getPathInModule, log, oraPromise } from '../../../utilities.js';
|
|
2
|
+
import { createPoolAndDatabaseIfNeeded } from '../../../database.js';
|
|
3
|
+
import { doesConfigsTableExist } from '../../../queries/logto-config.js';
|
|
4
|
+
import { log, oraPromise } from '../../../utils.js';
|
|
14
5
|
import { getLatestAlterationTimestamp } from '../alteration/index.js';
|
|
15
6
|
import { getAlterationDirectory } from '../alteration/utils.js';
|
|
16
|
-
import {
|
|
17
|
-
const
|
|
18
|
-
const matched = /\/\*\s*init_order\s*=\s*([\d.]+)\s*\*\//.exec(query)?.[1];
|
|
19
|
-
return matched ? Number(matched) : undefined;
|
|
20
|
-
};
|
|
21
|
-
const compareQuery = ([t1, q1], [t2, q2]) => {
|
|
22
|
-
const o1 = getExplicitOrder(q1);
|
|
23
|
-
const o2 = getExplicitOrder(q2);
|
|
24
|
-
if (o1 === undefined && o2 === undefined) {
|
|
25
|
-
return t1.localeCompare(t2);
|
|
26
|
-
}
|
|
27
|
-
if (o1 === undefined) {
|
|
28
|
-
return 1;
|
|
29
|
-
}
|
|
30
|
-
if (o2 === undefined) {
|
|
31
|
-
return -1;
|
|
32
|
-
}
|
|
33
|
-
return o1 - o2;
|
|
34
|
-
};
|
|
35
|
-
const createTables = async (connection) => {
|
|
36
|
-
const tableDirectory = getPathInModule('@logto/schemas', 'tables');
|
|
37
|
-
const directoryFiles = await readdir(tableDirectory);
|
|
38
|
-
const tableFiles = directoryFiles.filter((file) => file.endsWith('.sql'));
|
|
39
|
-
const queries = await Promise.all(tableFiles.map(async (file) => [
|
|
40
|
-
file,
|
|
41
|
-
await readFile(path.join(tableDirectory, file), 'utf8'),
|
|
42
|
-
]));
|
|
43
|
-
const allQueries = [
|
|
44
|
-
[Hooks.tableName, Hooks.raw],
|
|
45
|
-
[Tenants.tableName, Tenants.raw],
|
|
46
|
-
...queries,
|
|
47
|
-
];
|
|
48
|
-
const sorted = allQueries.slice().sort(compareQuery);
|
|
49
|
-
for (const [, query] of sorted) {
|
|
50
|
-
// eslint-disable-next-line no-await-in-loop
|
|
51
|
-
await connection.query(sql `${raw(query)}`);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
const seedTables = async (connection, latestTimestamp) => {
|
|
55
|
-
await connection.query(insertInto(defaultTenant, 'tenants'));
|
|
56
|
-
await Promise.all([
|
|
57
|
-
connection.query(insertInto(managementResource, 'resources')),
|
|
58
|
-
connection.query(insertInto(managementResourceScope, 'scopes')),
|
|
59
|
-
connection.query(insertInto(createDefaultAdminConsoleConfig(), 'logto_configs')),
|
|
60
|
-
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
|
61
|
-
connection.query(insertInto(createDemoAppApplication(generateStandardId()), 'applications')),
|
|
62
|
-
connection.query(insertInto(defaultRole, 'roles')),
|
|
63
|
-
connection.query(insertInto(defaultRoleScopeRelation, 'roles_scopes')),
|
|
64
|
-
updateDatabaseTimestamp(connection, latestTimestamp),
|
|
65
|
-
]);
|
|
66
|
-
};
|
|
67
|
-
const seedOidcConfigs = async (pool) => {
|
|
68
|
-
const configGuard = z.object({
|
|
69
|
-
key: z.nativeEnum(LogtoOidcConfigKey),
|
|
70
|
-
value: z.unknown(),
|
|
71
|
-
});
|
|
72
|
-
const { rows } = await getRowsByKeys(pool, Object.values(LogtoOidcConfigKey));
|
|
73
|
-
// Filter out valid keys that hold a valid value
|
|
74
|
-
const result = await Promise.all(rows.map(async (row) => {
|
|
75
|
-
try {
|
|
76
|
-
const { key, value } = await configGuard.parseAsync(row);
|
|
77
|
-
await logtoConfigGuards[key].parseAsync(value);
|
|
78
|
-
return key;
|
|
79
|
-
}
|
|
80
|
-
catch { }
|
|
81
|
-
}));
|
|
82
|
-
const existingKeys = new Set(result.filter(Boolean));
|
|
83
|
-
const validOptions = Object.values(LogtoOidcConfigKey).filter((key) => {
|
|
84
|
-
const included = existingKeys.has(key);
|
|
85
|
-
if (included) {
|
|
86
|
-
log.info(`Key ${chalk.green(key)} exists, skipping`);
|
|
87
|
-
}
|
|
88
|
-
return !included;
|
|
89
|
-
});
|
|
90
|
-
// The awaits in loop is intended since we'd like to log info in sequence
|
|
91
|
-
/* eslint-disable no-await-in-loop */
|
|
92
|
-
for (const key of validOptions) {
|
|
93
|
-
const { value, fromEnv } = await oidcConfigReaders[key]();
|
|
94
|
-
if (fromEnv) {
|
|
95
|
-
log.info(`Read config ${chalk.green(key)} from env`);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
log.info(`Generated config ${chalk.green(key)}`);
|
|
99
|
-
}
|
|
100
|
-
await updateValueByKey(pool, key, value);
|
|
101
|
-
}
|
|
102
|
-
/* eslint-enable no-await-in-loop */
|
|
103
|
-
log.succeed('Seed OIDC config');
|
|
104
|
-
};
|
|
105
|
-
const seedChoices = Object.freeze(['all', 'oidc']);
|
|
106
|
-
export const seedByPool = async (pool, type) => {
|
|
7
|
+
import { createTables, seedCloud, seedTables } from './tables.js';
|
|
8
|
+
export const seedByPool = async (pool, cloud = false) => {
|
|
107
9
|
await pool.transaction(async (connection) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
prefixText: chalk.blue('[info]'),
|
|
122
|
-
});
|
|
10
|
+
// Check alteration scripts available in order to insert correct timestamp
|
|
11
|
+
const latestTimestamp = await getLatestAlterationTimestamp();
|
|
12
|
+
if (latestTimestamp < 1) {
|
|
13
|
+
throw new Error(`No alteration script found when seeding the database.\n` +
|
|
14
|
+
`Please check \`${getAlterationDirectory()}\` to see if there are alteration scripts available.\n`);
|
|
15
|
+
}
|
|
16
|
+
await oraPromise(createTables(connection), {
|
|
17
|
+
text: 'Create tables',
|
|
18
|
+
prefixText: chalk.blue('[info]'),
|
|
19
|
+
});
|
|
20
|
+
await seedTables(connection, latestTimestamp);
|
|
21
|
+
if (cloud) {
|
|
22
|
+
await seedCloud(connection);
|
|
123
23
|
}
|
|
124
|
-
await seedOidcConfigs(connection);
|
|
125
24
|
});
|
|
126
25
|
};
|
|
127
26
|
const seed = {
|
|
@@ -133,13 +32,12 @@ const seed = {
|
|
|
133
32
|
alias: 'skip-when-exists',
|
|
134
33
|
type: 'boolean',
|
|
135
34
|
})
|
|
136
|
-
.
|
|
137
|
-
describe: '
|
|
138
|
-
type: '
|
|
139
|
-
|
|
140
|
-
default: 'all',
|
|
35
|
+
.option('cloud', {
|
|
36
|
+
describe: 'Seed additional cloud data',
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
hidden: true,
|
|
141
39
|
}),
|
|
142
|
-
handler: async ({
|
|
40
|
+
handler: async ({ swe, cloud }) => {
|
|
143
41
|
const pool = await createPoolAndDatabaseIfNeeded();
|
|
144
42
|
if (swe && (await doesConfigsTableExist(pool))) {
|
|
145
43
|
log.info('Seeding skipped');
|
|
@@ -147,10 +45,7 @@ const seed = {
|
|
|
147
45
|
return;
|
|
148
46
|
}
|
|
149
47
|
try {
|
|
150
|
-
|
|
151
|
-
// The value of `type` can be ensured, so it's safe to use `as` here.
|
|
152
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
153
|
-
await seedByPool(pool, type);
|
|
48
|
+
await seedByPool(pool, cloud);
|
|
154
49
|
}
|
|
155
50
|
catch (error) {
|
|
156
51
|
console.error(error);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { LogtoOidcConfigType } from '@logto/schemas';
|
|
2
2
|
import { LogtoOidcConfigKey } from '@logto/schemas';
|
|
3
|
+
import type { DatabaseTransactionConnection } from 'slonik';
|
|
4
|
+
export declare const seedOidcConfigs: (pool: DatabaseTransactionConnection, tenantId: string) => Promise<void>;
|
|
3
5
|
/**
|
|
4
6
|
* Each config reader will do the following things in order:
|
|
5
7
|
* 1. Try to read value from env (mimic the behavior from the original core)
|
|
@@ -1,8 +1,51 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
|
-
import { LogtoOidcConfigKey } from '@logto/schemas';
|
|
2
|
+
import { LogtoOidcConfigKey, logtoConfigGuards } from '@logto/schemas';
|
|
3
3
|
import { getEnvAsStringArray } from '@silverhand/essentials';
|
|
4
|
-
import
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { getRowsByKeys, updateValueByKey } from '../../../queries/logto-config.js';
|
|
7
|
+
import { log } from '../../../utils.js';
|
|
8
|
+
import { generateOidcCookieKey, generateOidcPrivateKey } from '../utils.js';
|
|
5
9
|
const isBase64FormatPrivateKey = (key) => !key.includes('-');
|
|
10
|
+
export const seedOidcConfigs = async (pool, tenantId) => {
|
|
11
|
+
const tenantPrefix = `[${tenantId}]`;
|
|
12
|
+
const configGuard = z.object({
|
|
13
|
+
key: z.nativeEnum(LogtoOidcConfigKey),
|
|
14
|
+
value: z.unknown(),
|
|
15
|
+
});
|
|
16
|
+
const { rows } = await getRowsByKeys(pool, tenantId, Object.values(LogtoOidcConfigKey));
|
|
17
|
+
// Filter out valid keys that hold a valid value
|
|
18
|
+
const result = await Promise.all(rows.map(async (row) => {
|
|
19
|
+
try {
|
|
20
|
+
const { key, value } = await configGuard.parseAsync(row);
|
|
21
|
+
await logtoConfigGuards[key].parseAsync(value);
|
|
22
|
+
return key;
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
}));
|
|
26
|
+
const existingKeys = new Set(result.filter(Boolean));
|
|
27
|
+
const validOptions = Object.values(LogtoOidcConfigKey).filter((key) => {
|
|
28
|
+
const included = existingKeys.has(key);
|
|
29
|
+
if (included) {
|
|
30
|
+
log.info(tenantPrefix, `Key ${chalk.green(key)} exists, skipping`);
|
|
31
|
+
}
|
|
32
|
+
return !included;
|
|
33
|
+
});
|
|
34
|
+
// The awaits in loop is intended since we'd like to log info in sequence
|
|
35
|
+
/* eslint-disable no-await-in-loop */
|
|
36
|
+
for (const key of validOptions) {
|
|
37
|
+
const { value, fromEnv } = await oidcConfigReaders[key]();
|
|
38
|
+
if (fromEnv) {
|
|
39
|
+
log.info(tenantPrefix, `Read config ${chalk.green(key)} from env`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
log.info(tenantPrefix, `Generated config ${chalk.green(key)}`);
|
|
43
|
+
}
|
|
44
|
+
await updateValueByKey(pool, tenantId, key, value);
|
|
45
|
+
}
|
|
46
|
+
/* eslint-enable no-await-in-loop */
|
|
47
|
+
log.succeed(tenantPrefix, 'Seed OIDC config');
|
|
48
|
+
};
|
|
6
49
|
/**
|
|
7
50
|
* Each config reader will do the following things in order:
|
|
8
51
|
* 1. Try to read value from env (mimic the behavior from the original core)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DatabaseTransactionConnection } from 'slonik';
|
|
2
|
+
export declare const createTables: (connection: DatabaseTransactionConnection) => Promise<void>;
|
|
3
|
+
export declare const seedTables: (connection: DatabaseTransactionConnection, latestTimestamp: number) => Promise<void>;
|
|
4
|
+
export declare const seedCloud: (connection: DatabaseTransactionConnection) => Promise<void>;
|