@logto/cli 1.7.0 → 1.9.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/connector/add.js +2 -2
- package/lib/commands/connector/link.js +2 -2
- package/lib/commands/connector/utils.d.ts +2 -3
- package/lib/commands/connector/utils.js +3 -50
- package/lib/commands/database/alteration/index.js +45 -29
- package/lib/commands/database/alteration/utils.d.ts +1 -0
- package/lib/commands/database/alteration/utils.js +8 -0
- package/lib/commands/database/config.js +17 -3
- package/lib/commands/database/seed/cloud.d.ts +8 -0
- package/lib/commands/database/seed/cloud.js +24 -1
- package/lib/commands/database/seed/index.js +0 -2
- package/lib/commands/database/seed/oidc-config.js +2 -2
- package/lib/commands/database/seed/tables.js +11 -10
- package/lib/commands/database/utils.d.ts +5 -1
- package/lib/commands/database/utils.js +8 -3
- package/lib/commands/install/index.js +7 -7
- package/lib/commands/install/utils.d.ts +3 -4
- package/lib/commands/install/utils.js +6 -8
- package/lib/commands/translate/create.js +4 -5
- package/lib/commands/translate/list-tags.js +2 -2
- package/lib/commands/translate/prompts.d.ts +2 -1
- package/lib/commands/translate/prompts.js +10 -7
- package/lib/commands/translate/sync-keys/index.js +7 -15
- package/lib/commands/translate/sync-keys/utils.d.ts +6 -5
- package/lib/commands/translate/sync-keys/utils.js +46 -22
- package/lib/commands/translate/sync.js +8 -5
- package/lib/commands/translate/utils.d.ts +1 -1
- package/lib/commands/translate/utils.js +1 -2
- package/lib/connector/utils.d.ts +3 -3
- package/lib/connector/utils.js +19 -6
- package/lib/constants.d.ts +2 -0
- package/lib/constants.js +4 -0
- package/lib/package-json.d.ts +1 -1
- package/lib/package-json.js +9 -9
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +66 -4
- package/package.json +9 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { consoleLog } from '../../utils.js';
|
|
2
|
-
import { addConnectors, addOfficialConnectors
|
|
1
|
+
import { consoleLog, inquireInstancePath } from '../../utils.js';
|
|
2
|
+
import { addConnectors, addOfficialConnectors } from './utils.js';
|
|
3
3
|
const add = {
|
|
4
4
|
command: ['add [packages...]', 'a', 'install', 'i'],
|
|
5
5
|
describe: 'Add specific Logto connectors',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { consoleLog } from '../../utils.js';
|
|
4
|
-
import { getConnectorDirectory, getLocalConnectorPackages
|
|
3
|
+
import { consoleLog, inquireInstancePath } from '../../utils.js';
|
|
4
|
+
import { getConnectorDirectory, getLocalConnectorPackages } from './utils.js';
|
|
5
5
|
const link = {
|
|
6
6
|
command: ['link', 'ln'],
|
|
7
7
|
describe: 'Link all connectors in `packages/connectors`, useful for adding local connectors.',
|
|
@@ -5,14 +5,13 @@ export declare const npmPackResultGuard: z.ZodArray<z.ZodObject<{
|
|
|
5
5
|
filename: z.ZodString;
|
|
6
6
|
}, "strip", z.ZodTypeAny, {
|
|
7
7
|
name: string;
|
|
8
|
-
filename: string;
|
|
9
8
|
version: string;
|
|
9
|
+
filename: string;
|
|
10
10
|
}, {
|
|
11
11
|
name: string;
|
|
12
|
-
filename: string;
|
|
13
12
|
version: string;
|
|
13
|
+
filename: string;
|
|
14
14
|
}>, "many">;
|
|
15
|
-
export declare const inquireInstancePath: (initialPath?: string) => Promise<string>;
|
|
16
15
|
export declare const normalizePackageName: (name: string) => string;
|
|
17
16
|
export declare const getConnectorDirectory: (instancePath: string) => string;
|
|
18
17
|
export declare const isOfficialConnector: (packageName: string) => boolean;
|
|
@@ -3,18 +3,15 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import {
|
|
6
|
+
import { conditionalArray, conditionalString, trySafe } from '@silverhand/essentials';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { got } from 'got';
|
|
9
|
-
import inquirer from 'inquirer';
|
|
10
9
|
import pLimit from 'p-limit';
|
|
11
10
|
import pRetry from 'p-retry';
|
|
12
11
|
import tar from 'tar';
|
|
13
12
|
import { z } from 'zod';
|
|
14
|
-
import { connectorDirectory } from '../../constants.js';
|
|
15
|
-
import { consoleLog, getConnectorPackagesFromDirectory,
|
|
16
|
-
import { defaultPath } from '../install/utils.js';
|
|
17
|
-
const coreDirectory = 'packages/core';
|
|
13
|
+
import { connectorDirectory, coreDirectory } from '../../constants.js';
|
|
14
|
+
import { consoleLog, getConnectorPackagesFromDirectory, inquireInstancePath, oraPromise, } from '../../utils.js';
|
|
18
15
|
const execPromise = promisify(exec);
|
|
19
16
|
export const npmPackResultGuard = z
|
|
20
17
|
.object({
|
|
@@ -23,49 +20,6 @@ export const npmPackResultGuard = z
|
|
|
23
20
|
filename: z.string(),
|
|
24
21
|
})
|
|
25
22
|
.array();
|
|
26
|
-
const buildPathErrorMessage = (value) => `The path ${chalk.green(value)} does not contain a Logto instance, please try another.`;
|
|
27
|
-
const validatePath = async (value) => {
|
|
28
|
-
const corePackageJsonPath = path.resolve(path.join(value, coreDirectory, 'package.json'));
|
|
29
|
-
if (!existsSync(corePackageJsonPath)) {
|
|
30
|
-
return buildPathErrorMessage(value);
|
|
31
|
-
}
|
|
32
|
-
const packageJson = await fs.readFile(corePackageJsonPath, { encoding: 'utf8' });
|
|
33
|
-
const packageName = await z
|
|
34
|
-
.object({ name: z.string() })
|
|
35
|
-
.parseAsync(JSON.parse(packageJson))
|
|
36
|
-
.then(({ name }) => name)
|
|
37
|
-
.catch(() => '');
|
|
38
|
-
if (packageName !== '@logto/core') {
|
|
39
|
-
return buildPathErrorMessage(value);
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
};
|
|
43
|
-
export const inquireInstancePath = async (initialPath) => {
|
|
44
|
-
const inquire = async () => {
|
|
45
|
-
if (!initialPath && (await validatePath('.')) === true) {
|
|
46
|
-
return path.resolve('.');
|
|
47
|
-
}
|
|
48
|
-
if (!isTty()) {
|
|
49
|
-
assert(initialPath, new Error('Path is missing'));
|
|
50
|
-
return initialPath;
|
|
51
|
-
}
|
|
52
|
-
const { instancePath } = await inquirer.prompt({
|
|
53
|
-
name: 'instancePath',
|
|
54
|
-
message: 'Where is your Logto instance?',
|
|
55
|
-
type: 'input',
|
|
56
|
-
default: defaultPath,
|
|
57
|
-
filter: (value) => value.trim(),
|
|
58
|
-
validate: validatePath,
|
|
59
|
-
}, { instancePath: initialPath });
|
|
60
|
-
return instancePath;
|
|
61
|
-
};
|
|
62
|
-
const instancePath = await inquire();
|
|
63
|
-
const validated = await validatePath(instancePath);
|
|
64
|
-
if (validated !== true) {
|
|
65
|
-
consoleLog.fatal(validated);
|
|
66
|
-
}
|
|
67
|
-
return instancePath;
|
|
68
|
-
};
|
|
69
23
|
const packagePrefix = 'connector-';
|
|
70
24
|
export const normalizePackageName = (name) => name
|
|
71
25
|
.split('/')
|
|
@@ -172,7 +126,6 @@ export const fetchOfficialConnectorList = async (includingCloudConnectors = fals
|
|
|
172
126
|
export const addOfficialConnectors = async (instancePath, includingCloudConnectors = false) => {
|
|
173
127
|
const packages = await oraPromise(fetchOfficialConnectorList(includingCloudConnectors), {
|
|
174
128
|
text: 'Fetch official connector list',
|
|
175
|
-
prefixText: chalk.blue('[info]'),
|
|
176
129
|
});
|
|
177
130
|
consoleLog.info(`Found ${packages.length} official connectors`);
|
|
178
131
|
await addConnectors(instancePath, packages.map(({ name }) => name));
|
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { createPoolFromConfig } from '../../../database.js';
|
|
4
4
|
import { getCurrentDatabaseAlterationTimestamp, updateDatabaseTimestamp, } from '../../../queries/system.js';
|
|
5
5
|
import { consoleLog } from '../../../utils.js';
|
|
6
|
-
import { getAlterationFiles, getTimestampFromFilename } from './utils.js';
|
|
6
|
+
import { getAlterationFiles, getTimestampFromFilename, chooseRevertAlterationsByTimestamp, } from './utils.js';
|
|
7
7
|
import { chooseAlterationsByVersion, chooseRevertAlterationsByVersion } from './version.js';
|
|
8
8
|
const importAlterationScript = async (filePath) => {
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
@@ -52,6 +52,15 @@ const deployAlteration = async (pool, { path: filePath, filename }, action = 'up
|
|
|
52
52
|
}
|
|
53
53
|
consoleLog.info(`Run alteration ${filename} \`${action}()\` function succeeded`);
|
|
54
54
|
};
|
|
55
|
+
const revertAlterations = async (alterations, pool) => {
|
|
56
|
+
consoleLog.info(`Found ${alterations.length} alteration${conditionalString(alterations.length > 1 && 's')} to revert`);
|
|
57
|
+
// The await inside the loop is intended, alterations should run in order
|
|
58
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
59
|
+
for (const alteration of alterations.slice().reverse()) {
|
|
60
|
+
// eslint-disable-next-line no-await-in-loop
|
|
61
|
+
await deployAlteration(pool, alteration, 'down');
|
|
62
|
+
}
|
|
63
|
+
};
|
|
55
64
|
const alteration = {
|
|
56
65
|
command: ['alteration <action> [target]', 'alt', 'alter'],
|
|
57
66
|
describe: 'Perform database alteration',
|
|
@@ -66,37 +75,44 @@ const alteration = {
|
|
|
66
75
|
type: 'string',
|
|
67
76
|
}),
|
|
68
77
|
handler: async ({ action, target }) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
switch (action) {
|
|
79
|
+
case 'list': {
|
|
80
|
+
const files = await getAlterationFiles();
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
consoleLog.plain(file.filename);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
73
85
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
case 'deploy': {
|
|
87
|
+
const pool = await createPoolFromConfig();
|
|
88
|
+
const alterations = await chooseAlterationsByVersion(await getAvailableAlterations(pool), target);
|
|
89
|
+
consoleLog.info(`Found ${alterations.length} alteration${conditionalString(alterations.length > 1 && 's')} to deploy`);
|
|
90
|
+
// The await inside the loop is intended, alterations should run in order
|
|
91
|
+
for (const alteration of alterations) {
|
|
92
|
+
// eslint-disable-next-line no-await-in-loop
|
|
93
|
+
await deployAlteration(pool, alteration);
|
|
94
|
+
}
|
|
95
|
+
await pool.end();
|
|
96
|
+
break;
|
|
83
97
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await
|
|
98
|
+
case 'rollback':
|
|
99
|
+
case 'r': {
|
|
100
|
+
const pool = await createPoolFromConfig();
|
|
101
|
+
const alterations = await chooseRevertAlterationsByVersion(await getAvailableAlterations(pool, 'lte'), target ?? '');
|
|
102
|
+
await revertAlterations(alterations, pool);
|
|
103
|
+
await pool.end();
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'rollback-to-timestamp': {
|
|
107
|
+
const pool = await createPoolFromConfig();
|
|
108
|
+
const alterations = await chooseRevertAlterationsByTimestamp(target ?? '');
|
|
109
|
+
await revertAlterations(alterations, pool);
|
|
110
|
+
await pool.end();
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
default: {
|
|
114
|
+
consoleLog.fatal('Unsupported action');
|
|
95
115
|
}
|
|
96
|
-
await pool.end();
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
consoleLog.fatal('Unsupported action');
|
|
100
116
|
}
|
|
101
117
|
},
|
|
102
118
|
};
|
|
@@ -2,3 +2,4 @@ import type { AlterationFile } from './type.js';
|
|
|
2
2
|
export declare const getTimestampFromFilename: (filename: string) => number;
|
|
3
3
|
export declare const getAlterationDirectory: () => string;
|
|
4
4
|
export declare const getAlterationFiles: () => Promise<AlterationFile[]>;
|
|
5
|
+
export declare const chooseRevertAlterationsByTimestamp: (target: string) => Promise<AlterationFile[]>;
|
|
@@ -36,3 +36,11 @@ export const getAlterationFiles = async () => {
|
|
|
36
36
|
.sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2))
|
|
37
37
|
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
|
|
38
38
|
};
|
|
39
|
+
export const chooseRevertAlterationsByTimestamp = async (target) => {
|
|
40
|
+
const files = await getAlterationFiles();
|
|
41
|
+
const targetTimestamp = Number(target);
|
|
42
|
+
if (Number.isNaN(targetTimestamp)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return files.filter(({ filename }) => getTimestampFromFilename(filename) > targetTimestamp);
|
|
46
|
+
};
|
|
@@ -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 { generateOidcCookieKey, generateOidcPrivateKey } from './utils.js';
|
|
7
|
+
import { PrivateKeyType, 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(
|
|
@@ -19,6 +19,7 @@ const validRotateKeys = Object.freeze([
|
|
|
19
19
|
LogtoOidcConfigKey.PrivateKeys,
|
|
20
20
|
LogtoOidcConfigKey.CookieKeys,
|
|
21
21
|
]);
|
|
22
|
+
const validPrivateKeyTypes = Object.freeze([PrivateKeyType.RSA, PrivateKeyType.EC]);
|
|
22
23
|
const validateRotateKey = (key) => {
|
|
23
24
|
// Using `.includes()` will result a type error
|
|
24
25
|
// eslint-disable-next-line unicorn/prefer-includes
|
|
@@ -26,6 +27,13 @@ const validateRotateKey = (key) => {
|
|
|
26
27
|
consoleLog.fatal(`Invalid config key ${chalk.red(key)} found, expected one of ${validKeysDisplay}`);
|
|
27
28
|
}
|
|
28
29
|
};
|
|
30
|
+
const validatePrivateKeyType = (key) => {
|
|
31
|
+
// Using `.includes()` will result a type error
|
|
32
|
+
// eslint-disable-next-line unicorn/prefer-includes
|
|
33
|
+
if (!validPrivateKeyTypes.some((element) => element === key)) {
|
|
34
|
+
consoleLog.fatal(`Invalid private key type ${chalk.red(key)} found, expected one of ${validPrivateKeyTypes.join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
29
37
|
const getConfig = {
|
|
30
38
|
command: 'get <key> [keys...]',
|
|
31
39
|
describe: 'Get config value(s) of the given key(s) in Logto database',
|
|
@@ -103,9 +111,15 @@ const rotateConfig = {
|
|
|
103
111
|
describe: 'The tenant to operate',
|
|
104
112
|
type: 'string',
|
|
105
113
|
default: defaultTenantId,
|
|
114
|
+
})
|
|
115
|
+
.option('type', {
|
|
116
|
+
describe: `The key type for ${LogtoOidcConfigKey.PrivateKeys}, one of ${validPrivateKeyTypes.join(', ')}`,
|
|
117
|
+
type: 'string',
|
|
118
|
+
default: 'ec',
|
|
106
119
|
}),
|
|
107
|
-
handler: async ({ key, tenantId }) => {
|
|
120
|
+
handler: async ({ key, tenantId, type }) => {
|
|
108
121
|
validateRotateKey(key);
|
|
122
|
+
validatePrivateKeyType(type);
|
|
109
123
|
const pool = await createPoolFromConfig();
|
|
110
124
|
const { rows } = await getRowsByKeys(pool, tenantId, [key]);
|
|
111
125
|
if (!rows[0]) {
|
|
@@ -117,7 +131,7 @@ const rotateConfig = {
|
|
|
117
131
|
// No need for default. It's already exhaustive
|
|
118
132
|
switch (key) {
|
|
119
133
|
case LogtoOidcConfigKey.PrivateKeys: {
|
|
120
|
-
return [await generateOidcPrivateKey(), ...original];
|
|
134
|
+
return [await generateOidcPrivateKey(type), ...original];
|
|
121
135
|
}
|
|
122
136
|
case LogtoOidcConfigKey.CookieKeys: {
|
|
123
137
|
return [generateOidcCookieKey(), ...original];
|
|
@@ -8,3 +8,11 @@ import type { CommonQueryMethods } from 'slonik';
|
|
|
8
8
|
* For why it is necessary, see the redirect lifecycle of cloud Admin Console.
|
|
9
9
|
*/
|
|
10
10
|
export declare const appendAdminConsoleRedirectUris: (pool: CommonQueryMethods) => Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Create Cloud Service M2M application for the tenant.
|
|
13
|
+
*
|
|
14
|
+
* This is a built-in M2M application for the tenant to communicate with the cloud service.
|
|
15
|
+
* - default
|
|
16
|
+
* - admin
|
|
17
|
+
*/
|
|
18
|
+
export declare const seedTenantCloudServiceApplication: (pool: CommonQueryMethods, tenantId: string) => Promise<void>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { adminConsoleApplicationId, adminTenantId, defaultTenantId } from '@logto/schemas';
|
|
1
|
+
import { adminConsoleApplicationId, adminTenantId, defaultTenantId, createTenantMachineToMachineApplication, createAdminTenantApplicationRole, createCloudConnectionConfig, AdminTenantRole, } from '@logto/schemas';
|
|
2
2
|
import { GlobalValues } from '@logto/shared';
|
|
3
3
|
import { appendPath } from '@silverhand/essentials';
|
|
4
4
|
import { sql } from 'slonik';
|
|
5
|
+
import { insertInto } from '../../../database.js';
|
|
5
6
|
import { consoleLog } from '../../../utils.js';
|
|
6
7
|
/**
|
|
7
8
|
* Append Redirect URIs for the default tenant callback in cloud Admin Console.
|
|
@@ -32,3 +33,25 @@ export const appendAdminConsoleRedirectUris = async (pool) => {
|
|
|
32
33
|
`);
|
|
33
34
|
consoleLog.succeed('Appended initial Redirect URIs to Admin Console:', redirectUris.map(String));
|
|
34
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Create Cloud Service M2M application for the tenant.
|
|
38
|
+
*
|
|
39
|
+
* This is a built-in M2M application for the tenant to communicate with the cloud service.
|
|
40
|
+
* - default
|
|
41
|
+
* - admin
|
|
42
|
+
*/
|
|
43
|
+
export const seedTenantCloudServiceApplication = async (pool, tenantId) => {
|
|
44
|
+
// Create Cloud Service M2M application
|
|
45
|
+
const cloudServiceApplication = createTenantMachineToMachineApplication(tenantId);
|
|
46
|
+
await pool.query(insertInto(cloudServiceApplication, 'applications'));
|
|
47
|
+
// Assign tenantApplicationRole to Cloud Service M2M application
|
|
48
|
+
const { id: tenantApplicationRoleId } = await pool.one(sql `
|
|
49
|
+
select id from roles
|
|
50
|
+
where name = ${AdminTenantRole.TenantApplication}
|
|
51
|
+
and tenant_id = ${adminTenantId}
|
|
52
|
+
`);
|
|
53
|
+
await pool.query(insertInto(createAdminTenantApplicationRole(cloudServiceApplication.id, tenantApplicationRoleId), 'applications_roles'));
|
|
54
|
+
// Create Cloud Service M2M application logto_config
|
|
55
|
+
await pool.query(insertInto(createCloudConnectionConfig(tenantId, cloudServiceApplication.id, cloudServiceApplication.secret), 'logto_configs'));
|
|
56
|
+
consoleLog.succeed('Cloud Service Application successfully created for:', tenantId);
|
|
57
|
+
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { createPoolAndDatabaseIfNeeded } from '../../../database.js';
|
|
3
2
|
import { doesConfigsTableExist } from '../../../queries/logto-config.js';
|
|
4
3
|
import { consoleLog, oraPromise } from '../../../utils.js';
|
|
@@ -15,7 +14,6 @@ export const seedByPool = async (pool, cloud = false) => {
|
|
|
15
14
|
}
|
|
16
15
|
await oraPromise(createTables(connection), {
|
|
17
16
|
text: 'Create tables',
|
|
18
|
-
prefixText: chalk.blue('[info]'),
|
|
19
17
|
});
|
|
20
18
|
await seedTables(connection, latestTimestamp, cloud);
|
|
21
19
|
if (cloud) {
|
|
@@ -36,10 +36,10 @@ export const seedOidcConfigs = async (pool, tenantId) => {
|
|
|
36
36
|
for (const key of validOptions) {
|
|
37
37
|
const { value, fromEnv } = await oidcConfigReaders[key]();
|
|
38
38
|
if (fromEnv) {
|
|
39
|
-
consoleLog.
|
|
39
|
+
consoleLog.succeed(tenantPrefix, `Read config ${chalk.green(key)} from env`);
|
|
40
40
|
}
|
|
41
41
|
else {
|
|
42
|
-
consoleLog.
|
|
42
|
+
consoleLog.succeed(tenantPrefix, `Generated config ${chalk.green(key)}`);
|
|
43
43
|
}
|
|
44
44
|
await updateValueByKey(pool, tenantId, key, value);
|
|
45
45
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { createDefaultAdminConsoleConfig,
|
|
3
|
+
import { createDefaultAdminConsoleConfig, defaultTenantId, adminTenantId, defaultManagementApi, createAdminDataInAdminTenant, createMeApiInAdminTenant, createDefaultSignInExperience, createAdminTenantSignInExperience, createDefaultAdminConsoleApplication, createCloudApi, createTenantApplicationRole, CloudScope, } from '@logto/schemas';
|
|
4
4
|
import { Tenants } from '@logto/schemas/models';
|
|
5
5
|
import { sql } from 'slonik';
|
|
6
6
|
import { raw } from 'slonik-sql-tag-raw';
|
|
@@ -8,7 +8,7 @@ import { insertInto } from '../../../database.js';
|
|
|
8
8
|
import { getDatabaseName } from '../../../queries/database.js';
|
|
9
9
|
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
|
10
10
|
import { consoleLog, getPathInModule } from '../../../utils.js';
|
|
11
|
-
import { appendAdminConsoleRedirectUris } from './cloud.js';
|
|
11
|
+
import { appendAdminConsoleRedirectUris, seedTenantCloudServiceApplication } from './cloud.js';
|
|
12
12
|
import { seedOidcConfigs } from './oidc-config.js';
|
|
13
13
|
import { assignScopesToRole, createTenant, seedAdminData } from './tenant.js';
|
|
14
14
|
const getExplicitOrder = (query) => {
|
|
@@ -82,23 +82,21 @@ export const seedTables = async (connection, latestTimestamp, isCloud) => {
|
|
|
82
82
|
await seedAdminData(connection, adminAdminData);
|
|
83
83
|
await seedAdminData(connection, createMeApiInAdminTenant());
|
|
84
84
|
const [cloudData, ...cloudAdditionalScopes] = createCloudApi();
|
|
85
|
-
const applicationRole = createTenantApplicationRole();
|
|
86
85
|
await seedAdminData(connection, cloudData, ...cloudAdditionalScopes);
|
|
86
|
+
// Create tenant application role
|
|
87
|
+
const applicationRole = createTenantApplicationRole();
|
|
87
88
|
await connection.query(insertInto(applicationRole, 'roles'));
|
|
88
89
|
await assignScopesToRole(connection, adminTenantId, applicationRole.id, ...cloudAdditionalScopes
|
|
89
90
|
.filter(({ name }) => name === CloudScope.SendSms || name === CloudScope.SendEmail)
|
|
90
91
|
.map(({ id }) => id));
|
|
91
|
-
// Add M2M app for default tenant
|
|
92
|
-
const defaultTenantApplication = createTenantMachineToMachineApplication(defaultTenantId);
|
|
93
|
-
await connection.query(insertInto(defaultTenantApplication, 'applications'));
|
|
94
|
-
await connection.query(insertInto(createAdminTenantApplicationRole(defaultTenantApplication.id, applicationRole.id), 'applications_roles'));
|
|
95
92
|
// Assign all cloud API scopes to role `admin:admin`
|
|
96
93
|
await assignScopesToRole(connection, adminTenantId, adminAdminData.role.id, ...cloudAdditionalScopes.map(({ id }) => id));
|
|
94
|
+
// FIXME: @wangsijie should not create tenant Cloud Service application in the OSS DB.
|
|
95
|
+
await seedTenantCloudServiceApplication(connection, defaultTenantId);
|
|
97
96
|
await Promise.all([
|
|
98
97
|
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
|
99
|
-
connection.query(insertInto(createCloudConnectionConfig(defaultTenantId, defaultTenantApplication.id, defaultTenantApplication.secret), 'logto_configs')),
|
|
100
|
-
connection.query(insertInto(createDefaultSignInExperience(defaultTenantId, isCloud), 'sign_in_experiences')),
|
|
101
98
|
connection.query(insertInto(createDefaultAdminConsoleConfig(adminTenantId), 'logto_configs')),
|
|
99
|
+
connection.query(insertInto(createDefaultSignInExperience(defaultTenantId, isCloud), 'sign_in_experiences')),
|
|
102
100
|
connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')),
|
|
103
101
|
connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')),
|
|
104
102
|
updateDatabaseTimestamp(connection, latestTimestamp),
|
|
@@ -106,5 +104,8 @@ export const seedTables = async (connection, latestTimestamp, isCloud) => {
|
|
|
106
104
|
consoleLog.succeed('Seed data');
|
|
107
105
|
};
|
|
108
106
|
export const seedCloud = async (connection) => {
|
|
109
|
-
await
|
|
107
|
+
await Promise.all([
|
|
108
|
+
appendAdminConsoleRedirectUris(connection),
|
|
109
|
+
seedTenantCloudServiceApplication(connection, adminTenantId),
|
|
110
|
+
]);
|
|
110
111
|
};
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare enum PrivateKeyType {
|
|
2
|
+
RSA = "rsa",
|
|
3
|
+
EC = "ec"
|
|
4
|
+
}
|
|
5
|
+
export declare const generateOidcPrivateKey: (type?: PrivateKeyType) => Promise<string>;
|
|
2
6
|
export declare const generateOidcCookieKey: () => string;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { generateKeyPair } from 'node:crypto';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { generateStandardId } from '@logto/shared';
|
|
4
|
-
export
|
|
5
|
-
|
|
4
|
+
export var PrivateKeyType;
|
|
5
|
+
(function (PrivateKeyType) {
|
|
6
|
+
PrivateKeyType["RSA"] = "rsa";
|
|
7
|
+
PrivateKeyType["EC"] = "ec";
|
|
8
|
+
})(PrivateKeyType || (PrivateKeyType = {}));
|
|
9
|
+
export const generateOidcPrivateKey = async (type = PrivateKeyType.EC) => {
|
|
10
|
+
if (type === PrivateKeyType.RSA) {
|
|
6
11
|
const { privateKey } = await promisify(generateKeyPair)('rsa', {
|
|
7
12
|
modulusLength: 4096,
|
|
8
13
|
publicKeyEncoding: {
|
|
@@ -17,7 +22,7 @@ export const generateOidcPrivateKey = async (type = 'ec') => {
|
|
|
17
22
|
return privateKey;
|
|
18
23
|
}
|
|
19
24
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
20
|
-
if (type ===
|
|
25
|
+
if (type === PrivateKeyType.EC) {
|
|
21
26
|
const { privateKey } = await promisify(generateKeyPair)('ec', {
|
|
22
27
|
// https://security.stackexchange.com/questions/78621/which-elliptic-curve-should-i-use
|
|
23
28
|
namedCurve: 'secp384r1',
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { getDatabaseUrlFromConfig } from '../../database.js';
|
|
3
3
|
import { consoleLog } from '../../utils.js';
|
|
4
|
-
import { validateNodeVersion,
|
|
4
|
+
import { validateNodeVersion, inquireInstallPath, validateDatabase, downloadRelease, seedDatabase, createEnv, logFinale, decompress, isUrl, } from './utils.js';
|
|
5
5
|
const installLogto = async ({ path, skipSeed, downloadUrl, cloud }) => {
|
|
6
6
|
validateNodeVersion();
|
|
7
|
-
// Get
|
|
8
|
-
const
|
|
7
|
+
// Get install location path
|
|
8
|
+
const installPath = await inquireInstallPath(path);
|
|
9
9
|
// Validate if user has a valid database
|
|
10
10
|
await validateDatabase();
|
|
11
11
|
// Download and decompress
|
|
12
12
|
const tarPath = !downloadUrl || isUrl(downloadUrl) ? await downloadRelease(downloadUrl) : downloadUrl;
|
|
13
|
-
await decompress(
|
|
13
|
+
await decompress(installPath, tarPath);
|
|
14
14
|
// Seed database
|
|
15
15
|
if (skipSeed) {
|
|
16
16
|
consoleLog.info(`Skipped database seeding.\n\n' + ' You can use the ${chalk.green('db seed')} command to seed database when ready.\n`);
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
|
-
await seedDatabase(
|
|
19
|
+
await seedDatabase(installPath, cloud);
|
|
20
20
|
}
|
|
21
21
|
// Save to dot env
|
|
22
|
-
await createEnv(
|
|
22
|
+
await createEnv(installPath, await getDatabaseUrlFromConfig());
|
|
23
23
|
// Finale
|
|
24
|
-
logFinale(
|
|
24
|
+
logFinale(installPath);
|
|
25
25
|
};
|
|
26
26
|
const install = {
|
|
27
27
|
command: ['init', 'i', 'install'],
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
export declare const defaultPath: string;
|
|
2
1
|
export declare const validateNodeVersion: () => void;
|
|
3
|
-
export declare const
|
|
2
|
+
export declare const inquireInstallPath: (initialPath?: string) => Promise<string>;
|
|
4
3
|
export declare const validateDatabase: () => Promise<void>;
|
|
5
4
|
export declare const downloadRelease: (url?: string) => Promise<string>;
|
|
6
5
|
export declare const decompress: (toPath: string, tarPath: string) => Promise<void>;
|
|
7
6
|
export declare const seedDatabase: (instancePath: string, cloud: boolean) => Promise<void>;
|
|
8
|
-
export declare const createEnv: (
|
|
9
|
-
export declare const logFinale: (
|
|
7
|
+
export declare const createEnv: (installPath: string, databaseUrl: string) => Promise<void>;
|
|
8
|
+
export declare const logFinale: (installPath: string) => void;
|
|
10
9
|
export declare const isUrl: (string: string) => boolean;
|
|
@@ -9,11 +9,11 @@ import { got, RequestError } from 'got';
|
|
|
9
9
|
import inquirer from 'inquirer';
|
|
10
10
|
import * as semver from 'semver';
|
|
11
11
|
import tar from 'tar';
|
|
12
|
+
import { defaultPath } from '../../constants.js';
|
|
12
13
|
import { createPoolAndDatabaseIfNeeded } from '../../database.js';
|
|
13
14
|
import { packageJson } from '../../package-json.js';
|
|
14
15
|
import { cliConfig, ConfigKey, consoleLog, downloadFile, isTty, oraPromise, safeExecSync, } from '../../utils.js';
|
|
15
16
|
import { seedByPool } from '../database/seed/index.js';
|
|
16
|
-
export const defaultPath = path.join(os.homedir(), 'logto');
|
|
17
17
|
const pgRequired = new semver.SemVer('14.0.0');
|
|
18
18
|
export const validateNodeVersion = () => {
|
|
19
19
|
const required = [new semver.SemVer('18.12.0')];
|
|
@@ -29,7 +29,7 @@ export const validateNodeVersion = () => {
|
|
|
29
29
|
const validatePath = (value) => existsSync(path.resolve(value))
|
|
30
30
|
? `The path ${chalk.green(value)} already exists, please try another.`
|
|
31
31
|
: true;
|
|
32
|
-
export const
|
|
32
|
+
export const inquireInstallPath = async (initialPath) => {
|
|
33
33
|
if (!isTty()) {
|
|
34
34
|
assert(initialPath, new Error('Path is missing'));
|
|
35
35
|
return initialPath;
|
|
@@ -106,7 +106,6 @@ export const decompress = async (toPath, tarPath) => {
|
|
|
106
106
|
};
|
|
107
107
|
return oraPromise(run(), {
|
|
108
108
|
text: `Decompress to ${toPath}`,
|
|
109
|
-
prefixText: chalk.blue('[info]'),
|
|
110
109
|
}, true);
|
|
111
110
|
};
|
|
112
111
|
export const seedDatabase = async (instancePath, cloud) => {
|
|
@@ -119,19 +118,18 @@ export const seedDatabase = async (instancePath, cloud) => {
|
|
|
119
118
|
consoleLog.error(error);
|
|
120
119
|
await oraPromise(fs.rm(instancePath, { force: true, recursive: true }), {
|
|
121
120
|
text: 'Clean up',
|
|
122
|
-
prefixText: chalk.blue('[info]'),
|
|
123
121
|
});
|
|
124
122
|
consoleLog.fatal('Error occurred during seeding your Logto database. Nothing has changed since the seeding process was in a transaction.\n\n' +
|
|
125
123
|
` To skip the database seeding, append ${chalk.green('--skip-seed')} to the command options.`);
|
|
126
124
|
}
|
|
127
125
|
};
|
|
128
|
-
export const createEnv = async (
|
|
129
|
-
const dotEnvPath = path.resolve(
|
|
126
|
+
export const createEnv = async (installPath, databaseUrl) => {
|
|
127
|
+
const dotEnvPath = path.resolve(installPath, '.env');
|
|
130
128
|
await fs.writeFile(dotEnvPath, `DB_URL=${databaseUrl}`, 'utf8');
|
|
131
129
|
consoleLog.info(`Saved database URL to ${chalk.blue(dotEnvPath)}`);
|
|
132
130
|
};
|
|
133
|
-
export const logFinale = (
|
|
134
|
-
const startCommand = `cd ${
|
|
131
|
+
export const logFinale = (installPath) => {
|
|
132
|
+
const startCommand = `cd ${installPath} && npm start`;
|
|
135
133
|
consoleLog.info(`Use the command below to start Logto. Happy hacking!\n\n ${chalk.green(startCommand)}`);
|
|
136
134
|
};
|
|
137
135
|
export const isUrl = (string) => {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { isLanguageTag } from '@logto/language-kit';
|
|
2
2
|
import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
|
|
3
|
-
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-
|
|
4
|
-
import { consoleLog } from '../../utils.js';
|
|
5
|
-
import { inquireInstancePath } from '../connector/utils.js';
|
|
3
|
+
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-experience';
|
|
4
|
+
import { consoleLog, inquireInstancePath } from '../../utils.js';
|
|
6
5
|
import { createFullTranslation } from './utils.js';
|
|
7
6
|
const create = {
|
|
8
7
|
command: ['create <language-tag>', 'c'],
|
|
@@ -26,11 +25,11 @@ const create = {
|
|
|
26
25
|
languageTag,
|
|
27
26
|
});
|
|
28
27
|
if (isPhrasesUiBuiltInLanguageTag(languageTag)) {
|
|
29
|
-
consoleLog.info(languageTag + ' is a built-in tag of phrases-
|
|
28
|
+
consoleLog.info(languageTag + ' is a built-in tag of phrases-experience, updating untranslated phrases');
|
|
30
29
|
}
|
|
31
30
|
await createFullTranslation({
|
|
32
31
|
instancePath,
|
|
33
|
-
packageName: 'phrases-
|
|
32
|
+
packageName: 'phrases-experience',
|
|
34
33
|
languageTag,
|
|
35
34
|
});
|
|
36
35
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { languages } from '@logto/language-kit';
|
|
2
2
|
import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
|
|
3
|
-
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-
|
|
3
|
+
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-experience';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { consoleLog } from '../../utils.js';
|
|
6
6
|
const listTags = {
|
|
@@ -11,7 +11,7 @@ const listTags = {
|
|
|
11
11
|
consoleLog.plain(...[
|
|
12
12
|
tag,
|
|
13
13
|
isPhrasesBuiltInLanguageTag(tag) && chalk.blue('phrases'),
|
|
14
|
-
isPhrasesUiBuiltInLanguageTag(tag) && chalk.blue('phrases-
|
|
14
|
+
isPhrasesUiBuiltInLanguageTag(tag) && chalk.blue('phrases-experience'),
|
|
15
15
|
].filter(Boolean));
|
|
16
16
|
}
|
|
17
17
|
},
|
|
@@ -4,9 +4,10 @@ type GetTranslationPromptProperties = {
|
|
|
4
4
|
targetLanguage: LanguageTag;
|
|
5
5
|
extraPrompt?: string;
|
|
6
6
|
};
|
|
7
|
+
export declare const untranslatedMark = "/** UNTRANSLATED */";
|
|
7
8
|
/**
|
|
8
9
|
* Note:
|
|
9
|
-
* The input token limit of GPT 3.5 is 2048, the following prompt tokens with sourceFileContent is about
|
|
10
|
+
* The input token limit of GPT 3.5 is 2048, the following prompt tokens with sourceFileContent is about 1600.
|
|
10
11
|
* Remember to check the token limit before adding more prompt.
|
|
11
12
|
* Tokens can be counted in https://platform.openai.com/tokenizer
|
|
12
13
|
*/
|