@logto/cli 1.0.0-beta.11
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 +373 -0
- package/bin/logto +2 -0
- package/lib/commands/connector/add.d.ts +9 -0
- package/lib/commands/connector/add.js +36 -0
- package/lib/commands/connector/index.d.ts +3 -0
- package/lib/commands/connector/index.js +25 -0
- package/lib/commands/connector/list.d.ts +7 -0
- package/lib/commands/connector/list.js +28 -0
- package/lib/commands/connector/remove.d.ts +8 -0
- package/lib/commands/connector/remove.js +47 -0
- package/lib/commands/connector/utils.d.ts +24 -0
- package/lib/commands/connector/utils.js +165 -0
- package/lib/commands/database/alteration/index.d.ts +11 -0
- package/lib/commands/database/alteration/index.js +142 -0
- package/lib/commands/database/alteration/index.test.d.ts +1 -0
- package/lib/commands/database/alteration/index.test.js +87 -0
- package/lib/commands/database/alteration/type.d.ts +4 -0
- package/lib/commands/database/alteration/type.js +2 -0
- package/lib/commands/database/alteration/version.d.ts +2 -0
- package/lib/commands/database/alteration/version.js +69 -0
- package/lib/commands/database/config.d.ts +3 -0
- package/lib/commands/database/config.js +82 -0
- package/lib/commands/database/index.d.ts +3 -0
- package/lib/commands/database/index.js +16 -0
- package/lib/commands/database/seed/index.d.ts +10 -0
- package/lib/commands/database/seed/index.js +138 -0
- package/lib/commands/database/seed/oidc-config.d.ts +12 -0
- package/lib/commands/database/seed/oidc-config.js +77 -0
- package/lib/commands/install/index.d.ts +14 -0
- package/lib/commands/install/index.js +71 -0
- package/lib/commands/install/utils.d.ts +11 -0
- package/lib/commands/install/utils.js +176 -0
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +4 -0
- package/lib/database.d.ts +12 -0
- package/lib/database.js +71 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +56 -0
- package/lib/package-json.d.ts +87 -0
- package/lib/package-json.js +95 -0
- package/lib/queries/logto-config.d.ts +8 -0
- package/lib/queries/logto-config.js +49 -0
- package/lib/queries/logto-config.test.d.ts +1 -0
- package/lib/queries/logto-config.test.js +81 -0
- package/lib/test-utilities.d.ts +7 -0
- package/lib/test-utilities.js +17 -0
- package/lib/utilities.d.ts +35 -0
- package/lib/utilities.js +136 -0
- package/package.json +92 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.seedByPool = void 0;
|
|
7
|
+
const promises_1 = require("fs/promises");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const schemas_1 = require("@logto/schemas");
|
|
10
|
+
const shared_1 = require("@logto/shared");
|
|
11
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
const slonik_1 = require("slonik");
|
|
13
|
+
const slonik_sql_tag_raw_1 = require("slonik-sql-tag-raw");
|
|
14
|
+
const zod_1 = require("zod");
|
|
15
|
+
const database_1 = require("../../../database");
|
|
16
|
+
const logto_config_1 = require("../../../queries/logto-config");
|
|
17
|
+
const utilities_1 = require("../../../utilities");
|
|
18
|
+
const alteration_1 = require("../alteration");
|
|
19
|
+
const oidc_config_1 = require("./oidc-config");
|
|
20
|
+
const createTables = async (connection) => {
|
|
21
|
+
const tableDirectory = (0, utilities_1.getPathInModule)('@logto/schemas', 'tables');
|
|
22
|
+
const directoryFiles = await (0, promises_1.readdir)(tableDirectory);
|
|
23
|
+
const tableFiles = directoryFiles.filter((file) => file.endsWith('.sql'));
|
|
24
|
+
const queries = await Promise.all(tableFiles.map(async (file) => [
|
|
25
|
+
file,
|
|
26
|
+
await (0, promises_1.readFile)(path_1.default.join(tableDirectory, file), 'utf8'),
|
|
27
|
+
]));
|
|
28
|
+
// Await in loop is intended for better error handling
|
|
29
|
+
for (const [, query] of queries) {
|
|
30
|
+
// eslint-disable-next-line no-await-in-loop
|
|
31
|
+
await connection.query((0, slonik_1.sql) `${(0, slonik_sql_tag_raw_1.raw)(query)}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const seedTables = async (connection) => {
|
|
35
|
+
const { managementResource, defaultSignInExperience, createDefaultSetting, createDemoAppApplication, defaultRole, } = schemas_1.seeds;
|
|
36
|
+
await Promise.all([
|
|
37
|
+
connection.query((0, database_1.insertInto)(managementResource, 'resources')),
|
|
38
|
+
connection.query((0, database_1.insertInto)(createDefaultSetting(), 'settings')),
|
|
39
|
+
connection.query((0, database_1.insertInto)(defaultSignInExperience, 'sign_in_experiences')),
|
|
40
|
+
connection.query((0, database_1.insertInto)(createDemoAppApplication((0, shared_1.buildApplicationSecret)()), 'applications')),
|
|
41
|
+
connection.query((0, database_1.insertInto)(defaultRole, 'roles')),
|
|
42
|
+
(0, logto_config_1.updateDatabaseTimestamp)(connection, await (0, alteration_1.getLatestAlterationTimestamp)()),
|
|
43
|
+
]);
|
|
44
|
+
};
|
|
45
|
+
const seedOidcConfigs = async (pool) => {
|
|
46
|
+
const configGuard = zod_1.z.object({
|
|
47
|
+
key: zod_1.z.nativeEnum(schemas_1.LogtoOidcConfigKey),
|
|
48
|
+
value: zod_1.z.unknown(),
|
|
49
|
+
});
|
|
50
|
+
const { rows } = await (0, logto_config_1.getRowsByKeys)(pool, Object.values(schemas_1.LogtoOidcConfigKey));
|
|
51
|
+
// Filter out valid keys that hold a valid value
|
|
52
|
+
const result = await Promise.all(rows.map(async (row) => {
|
|
53
|
+
try {
|
|
54
|
+
const { key, value } = await configGuard.parseAsync(row);
|
|
55
|
+
await schemas_1.logtoConfigGuards[key].parseAsync(value);
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}));
|
|
60
|
+
const existingKeys = new Set(result.filter(Boolean));
|
|
61
|
+
const validOptions = Object.values(schemas_1.LogtoOidcConfigKey).filter((key) => {
|
|
62
|
+
const included = existingKeys.has(key);
|
|
63
|
+
if (included) {
|
|
64
|
+
utilities_1.log.info(`Key ${chalk_1.default.green(key)} exists, skipping`);
|
|
65
|
+
}
|
|
66
|
+
return !included;
|
|
67
|
+
});
|
|
68
|
+
// The awaits in loop is intended since we'd like to log info in sequence
|
|
69
|
+
/* eslint-disable no-await-in-loop */
|
|
70
|
+
for (const key of validOptions) {
|
|
71
|
+
const { value, fromEnv } = await oidc_config_1.oidcConfigReaders[key]();
|
|
72
|
+
if (fromEnv) {
|
|
73
|
+
utilities_1.log.info(`Read config ${chalk_1.default.green(key)} from env`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
utilities_1.log.info(`Generated config ${chalk_1.default.green(key)}`);
|
|
77
|
+
}
|
|
78
|
+
await (0, logto_config_1.updateValueByKey)(pool, key, value);
|
|
79
|
+
}
|
|
80
|
+
/* eslint-enable no-await-in-loop */
|
|
81
|
+
utilities_1.log.succeed('Seed OIDC config');
|
|
82
|
+
};
|
|
83
|
+
const seedChoices = Object.freeze(['all', 'oidc']);
|
|
84
|
+
const seedByPool = async (pool, type) => {
|
|
85
|
+
await pool.transaction(async (connection) => {
|
|
86
|
+
if (type !== 'oidc') {
|
|
87
|
+
await (0, utilities_1.oraPromise)(createTables(connection), {
|
|
88
|
+
text: 'Create tables',
|
|
89
|
+
prefixText: chalk_1.default.blue('[info]'),
|
|
90
|
+
});
|
|
91
|
+
await (0, utilities_1.oraPromise)(seedTables(connection), {
|
|
92
|
+
text: 'Seed data',
|
|
93
|
+
prefixText: chalk_1.default.blue('[info]'),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
await seedOidcConfigs(connection);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
exports.seedByPool = seedByPool;
|
|
100
|
+
const seed = {
|
|
101
|
+
command: 'seed [type]',
|
|
102
|
+
describe: 'Create database then seed tables and data',
|
|
103
|
+
builder: (yargs) => yargs
|
|
104
|
+
.option('swe', {
|
|
105
|
+
describe: 'Skip the seeding process when Logto configs table exists',
|
|
106
|
+
alias: 'skip-when-exists',
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
})
|
|
109
|
+
.positional('type', {
|
|
110
|
+
describe: 'Optional seed type',
|
|
111
|
+
type: 'string',
|
|
112
|
+
choices: seedChoices,
|
|
113
|
+
default: 'all',
|
|
114
|
+
}),
|
|
115
|
+
handler: async ({ type, swe }) => {
|
|
116
|
+
const pool = await (0, database_1.createPoolAndDatabaseIfNeeded)();
|
|
117
|
+
if (swe && (await (0, logto_config_1.doesConfigsTableExist)(pool))) {
|
|
118
|
+
utilities_1.log.info('Seeding skipped');
|
|
119
|
+
await pool.end();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
// Cannot avoid `as` since the official type definition of `yargs` doesn't work.
|
|
124
|
+
// The value of `type` can be ensured, so it's safe to use `as` here.
|
|
125
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
126
|
+
await (0, exports.seedByPool)(pool, type);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error(error);
|
|
130
|
+
console.log();
|
|
131
|
+
utilities_1.log.warn('Error ocurred during seeding your database.\n\n' +
|
|
132
|
+
' Nothing has changed since the seeding process was in a transaction.\n' +
|
|
133
|
+
' Try to fix the error and seed again.');
|
|
134
|
+
}
|
|
135
|
+
await pool.end();
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
exports.default = seed;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LogtoOidcConfigKey, LogtoOidcConfigType } from '@logto/schemas';
|
|
2
|
+
/**
|
|
3
|
+
* Each config reader will do the following things in order:
|
|
4
|
+
* 1. Try to read value from env (mimic the behavior from the original core)
|
|
5
|
+
* 2. Generate value if #1 doesn't work
|
|
6
|
+
*/
|
|
7
|
+
export declare const oidcConfigReaders: {
|
|
8
|
+
[key in LogtoOidcConfigKey]: () => Promise<{
|
|
9
|
+
value: LogtoOidcConfigType[key];
|
|
10
|
+
fromEnv: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.oidcConfigReaders = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const promises_1 = require("fs/promises");
|
|
6
|
+
const util_1 = require("util");
|
|
7
|
+
const schemas_1 = require("@logto/schemas");
|
|
8
|
+
const essentials_1 = require("@silverhand/essentials");
|
|
9
|
+
const nanoid_1 = require("nanoid");
|
|
10
|
+
const isBase64FormatPrivateKey = (key) => !key.includes('-');
|
|
11
|
+
/**
|
|
12
|
+
* Each config reader will do the following things in order:
|
|
13
|
+
* 1. Try to read value from env (mimic the behavior from the original core)
|
|
14
|
+
* 2. Generate value if #1 doesn't work
|
|
15
|
+
*/
|
|
16
|
+
exports.oidcConfigReaders = {
|
|
17
|
+
/**
|
|
18
|
+
* Try to read private keys with the following order:
|
|
19
|
+
*
|
|
20
|
+
* 1. From `process.env.OIDC_PRIVATE_KEYS`.
|
|
21
|
+
* 2. Fetch path from `process.env.OIDC_PRIVATE_KEY_PATHS` then read from that path.
|
|
22
|
+
*
|
|
23
|
+
*
|
|
24
|
+
* @returns The private keys for OIDC provider.
|
|
25
|
+
* @throws An error when failed to read a private key.
|
|
26
|
+
*/
|
|
27
|
+
[schemas_1.LogtoOidcConfigKey.PrivateKeys]: async () => {
|
|
28
|
+
// Direct keys in env
|
|
29
|
+
const privateKeys = (0, essentials_1.getEnvAsStringArray)('OIDC_PRIVATE_KEYS');
|
|
30
|
+
if (privateKeys.length > 0) {
|
|
31
|
+
return {
|
|
32
|
+
value: privateKeys.map((key) => {
|
|
33
|
+
if (isBase64FormatPrivateKey(key)) {
|
|
34
|
+
return Buffer.from(key, 'base64').toString('utf8');
|
|
35
|
+
}
|
|
36
|
+
return key;
|
|
37
|
+
}),
|
|
38
|
+
fromEnv: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Read keys from files
|
|
42
|
+
const privateKeyPaths = (0, essentials_1.getEnvAsStringArray)('OIDC_PRIVATE_KEY_PATHS');
|
|
43
|
+
if (privateKeyPaths.length > 0) {
|
|
44
|
+
return {
|
|
45
|
+
value: await Promise.all(privateKeyPaths.map(async (path) => (0, promises_1.readFile)(path, 'utf8'))),
|
|
46
|
+
fromEnv: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Generate a new key
|
|
50
|
+
const { privateKey } = await (0, util_1.promisify)(crypto_1.generateKeyPair)('rsa', {
|
|
51
|
+
modulusLength: 4096,
|
|
52
|
+
publicKeyEncoding: {
|
|
53
|
+
type: 'spki',
|
|
54
|
+
format: 'pem',
|
|
55
|
+
},
|
|
56
|
+
privateKeyEncoding: {
|
|
57
|
+
type: 'pkcs8',
|
|
58
|
+
format: 'pem',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
value: [privateKey],
|
|
63
|
+
fromEnv: false,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
[schemas_1.LogtoOidcConfigKey.CookieKeys]: async () => {
|
|
67
|
+
const envKey = 'OIDC_COOKIE_KEYS';
|
|
68
|
+
const keys = (0, essentials_1.getEnvAsStringArray)(envKey);
|
|
69
|
+
return { value: keys.length > 0 ? keys : [(0, nanoid_1.nanoid)()], fromEnv: keys.length > 0 };
|
|
70
|
+
},
|
|
71
|
+
[schemas_1.LogtoOidcConfigKey.RefreshTokenReuseInterval]: async () => {
|
|
72
|
+
const envKey = 'OIDC_REFRESH_TOKEN_REUSE_INTERVAL';
|
|
73
|
+
const raw = Number((0, essentials_1.getEnv)(envKey));
|
|
74
|
+
const value = Math.max(3, raw || 0);
|
|
75
|
+
return { value, fromEnv: raw === value };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CommandModule } from 'yargs';
|
|
2
|
+
export declare type InstallArgs = {
|
|
3
|
+
path?: string;
|
|
4
|
+
skipSeed: boolean;
|
|
5
|
+
officialConnectors?: boolean;
|
|
6
|
+
downloadUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const install: CommandModule<unknown, {
|
|
9
|
+
p?: string;
|
|
10
|
+
ss: boolean;
|
|
11
|
+
oc?: boolean;
|
|
12
|
+
du?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export default install;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
7
|
+
const database_1 = require("../../database");
|
|
8
|
+
const utilities_1 = require("../../utilities");
|
|
9
|
+
const utils_1 = require("../connector/utils");
|
|
10
|
+
const utils_2 = require("./utils");
|
|
11
|
+
const installLogto = async ({ path, skipSeed, officialConnectors, downloadUrl }) => {
|
|
12
|
+
(0, utils_2.validateNodeVersion)();
|
|
13
|
+
// Get instance path
|
|
14
|
+
const instancePath = await (0, utils_2.inquireInstancePath)(path);
|
|
15
|
+
// Validate if user has a valid database
|
|
16
|
+
await (0, utils_2.validateDatabase)();
|
|
17
|
+
// Download and decompress
|
|
18
|
+
const tarPath = !downloadUrl || (0, utils_2.isUrl)(downloadUrl) ? await (0, utils_2.downloadRelease)(downloadUrl) : downloadUrl;
|
|
19
|
+
await (0, utils_2.decompress)(instancePath, tarPath);
|
|
20
|
+
// Seed database
|
|
21
|
+
if (skipSeed) {
|
|
22
|
+
utilities_1.log.info(`Skipped database seeding.\n\n' + ' You can use the ${chalk_1.default.green('db seed')} command to seed database when ready.\n`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await (0, utils_2.seedDatabase)(instancePath);
|
|
26
|
+
}
|
|
27
|
+
// Save to dot env
|
|
28
|
+
await (0, utils_2.createEnv)(instancePath, await (0, database_1.getDatabaseUrlFromConfig)());
|
|
29
|
+
// Add official connectors
|
|
30
|
+
if (await (0, utils_2.inquireOfficialConnectors)(officialConnectors)) {
|
|
31
|
+
await (0, utils_1.addOfficialConnectors)(instancePath);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
utilities_1.log.info('Skipped adding official connectors.\n\n' +
|
|
35
|
+
` You can use the ${chalk_1.default.green('connector add')} command to add connectors at any time.\n`);
|
|
36
|
+
}
|
|
37
|
+
// Finale
|
|
38
|
+
(0, utils_2.logFinale)(instancePath);
|
|
39
|
+
};
|
|
40
|
+
const install = {
|
|
41
|
+
command: ['init', 'i', 'install'],
|
|
42
|
+
describe: 'Download and run the latest Logto release',
|
|
43
|
+
builder: (yargs) => yargs.options({
|
|
44
|
+
p: {
|
|
45
|
+
alias: 'path',
|
|
46
|
+
describe: 'Path of Logto, must be a non-existing path',
|
|
47
|
+
type: 'string',
|
|
48
|
+
},
|
|
49
|
+
ss: {
|
|
50
|
+
alias: 'skip-seed',
|
|
51
|
+
describe: 'Skip Logto database seeding',
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
default: false,
|
|
54
|
+
},
|
|
55
|
+
oc: {
|
|
56
|
+
alias: 'official-connectors',
|
|
57
|
+
describe: 'Add official connectors after downloading Logto',
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
},
|
|
60
|
+
du: {
|
|
61
|
+
alias: 'download-url',
|
|
62
|
+
describe: 'URL for downloading Logto, can be a local path to tar',
|
|
63
|
+
type: 'string',
|
|
64
|
+
hidden: true,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
handler: async ({ p, ss, oc, du }) => {
|
|
68
|
+
await installLogto({ path: p, skipSeed: ss, officialConnectors: oc, downloadUrl: du });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
exports.default = install;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const defaultPath: string;
|
|
2
|
+
export declare const validateNodeVersion: () => void;
|
|
3
|
+
export declare const inquireInstancePath: (initialPath?: string) => Promise<string>;
|
|
4
|
+
export declare const validateDatabase: () => Promise<void>;
|
|
5
|
+
export declare const downloadRelease: (url?: string) => Promise<string>;
|
|
6
|
+
export declare const decompress: (toPath: string, tarPath: string) => Promise<void>;
|
|
7
|
+
export declare const seedDatabase: (instancePath: string) => Promise<void>;
|
|
8
|
+
export declare const createEnv: (instancePath: string, databaseUrl: string) => Promise<void>;
|
|
9
|
+
export declare const logFinale: (instancePath: string) => void;
|
|
10
|
+
export declare const inquireOfficialConnectors: (initialAnswer?: boolean) => Promise<boolean>;
|
|
11
|
+
export declare const isUrl: (string: string) => boolean;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.isUrl = exports.inquireOfficialConnectors = exports.logFinale = exports.createEnv = exports.seedDatabase = exports.decompress = exports.downloadRelease = exports.validateDatabase = exports.inquireInstancePath = exports.validateNodeVersion = exports.defaultPath = void 0;
|
|
30
|
+
const child_process_1 = require("child_process");
|
|
31
|
+
const fs_1 = require("fs");
|
|
32
|
+
const promises_1 = require("fs/promises");
|
|
33
|
+
const os_1 = __importDefault(require("os"));
|
|
34
|
+
const path_1 = __importDefault(require("path"));
|
|
35
|
+
const essentials_1 = require("@silverhand/essentials");
|
|
36
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
37
|
+
const fs_extra_1 = require("fs-extra");
|
|
38
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
39
|
+
const semver = __importStar(require("semver"));
|
|
40
|
+
const tar_1 = __importDefault(require("tar"));
|
|
41
|
+
const database_1 = require("../../database");
|
|
42
|
+
const utilities_1 = require("../../utilities");
|
|
43
|
+
const seed_1 = require("../database/seed");
|
|
44
|
+
exports.defaultPath = path_1.default.join(os_1.default.homedir(), 'logto');
|
|
45
|
+
const pgRequired = new semver.SemVer('14.0.0');
|
|
46
|
+
const validateNodeVersion = () => {
|
|
47
|
+
const required = new semver.SemVer('16.0.0');
|
|
48
|
+
const current = new semver.SemVer((0, child_process_1.execSync)('node -v', { encoding: 'utf8', stdio: 'pipe' }));
|
|
49
|
+
if (required.compare(current) > 0) {
|
|
50
|
+
utilities_1.log.error(`Logto requires NodeJS >=${required.version}, but ${current.version} found.`);
|
|
51
|
+
}
|
|
52
|
+
if (current.major > required.major) {
|
|
53
|
+
utilities_1.log.warn(`Logto is tested under NodeJS ^${required.version}, but version ${current.version} found.`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
exports.validateNodeVersion = validateNodeVersion;
|
|
57
|
+
const validatePath = (value) => (0, fs_1.existsSync)(path_1.default.resolve(value))
|
|
58
|
+
? `The path ${chalk_1.default.green(value)} already exists, please try another.`
|
|
59
|
+
: true;
|
|
60
|
+
const inquireInstancePath = async (initialPath) => {
|
|
61
|
+
if (!(0, utilities_1.isTty)()) {
|
|
62
|
+
(0, essentials_1.assert)(initialPath, new Error('Path is missing'));
|
|
63
|
+
return initialPath;
|
|
64
|
+
}
|
|
65
|
+
const { instancePath } = await inquirer_1.default.prompt({
|
|
66
|
+
name: 'instancePath',
|
|
67
|
+
message: 'Where should we create your Logto instance?',
|
|
68
|
+
type: 'input',
|
|
69
|
+
default: exports.defaultPath,
|
|
70
|
+
filter: (value) => value.trim(),
|
|
71
|
+
validate: validatePath,
|
|
72
|
+
}, { instancePath: initialPath });
|
|
73
|
+
// Validate for initialPath
|
|
74
|
+
const validated = validatePath(instancePath);
|
|
75
|
+
if (validated !== true) {
|
|
76
|
+
utilities_1.log.error(validated);
|
|
77
|
+
}
|
|
78
|
+
return instancePath;
|
|
79
|
+
};
|
|
80
|
+
exports.inquireInstancePath = inquireInstancePath;
|
|
81
|
+
const validateDatabase = async () => {
|
|
82
|
+
if (utilities_1.cliConfig.has(utilities_1.ConfigKey.DatabaseUrl) || !(0, utilities_1.isTty)()) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { hasPostgresUrl } = await inquirer_1.default.prompt({
|
|
86
|
+
name: 'hasPostgresUrl',
|
|
87
|
+
message: `Logto requires PostgreSQL >=${pgRequired.version} but cannot find in the current environment.\n Do you have a remote PostgreSQL instance ready?`,
|
|
88
|
+
type: 'confirm',
|
|
89
|
+
when: () => {
|
|
90
|
+
const pgOutput = (0, utilities_1.safeExecSync)('postgres --version') ?? '';
|
|
91
|
+
// Filter out all brackets in the output since Homebrew will append `(Homebrew)`.
|
|
92
|
+
const pgArray = pgOutput.split(' ').filter((value) => !value.startsWith('('));
|
|
93
|
+
const pgCurrent = semver.coerce(pgArray[pgArray.length - 1]);
|
|
94
|
+
return !pgCurrent || pgCurrent.compare(pgRequired) < 0;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
if (hasPostgresUrl === false) {
|
|
98
|
+
utilities_1.log.error('Logto requires a Postgres instance to run.');
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
exports.validateDatabase = validateDatabase;
|
|
102
|
+
const downloadRelease = async (url) => {
|
|
103
|
+
const tarFilePath = path_1.default.resolve(os_1.default.tmpdir(), './logto.tar.gz');
|
|
104
|
+
utilities_1.log.info(`Download Logto to ${tarFilePath}`);
|
|
105
|
+
await (0, utilities_1.downloadFile)(url ?? 'https://github.com/logto-io/logto/releases/latest/download/logto.tar.gz', tarFilePath);
|
|
106
|
+
return tarFilePath;
|
|
107
|
+
};
|
|
108
|
+
exports.downloadRelease = downloadRelease;
|
|
109
|
+
const decompress = async (toPath, tarPath) => {
|
|
110
|
+
const run = async () => {
|
|
111
|
+
try {
|
|
112
|
+
await (0, promises_1.mkdir)(toPath);
|
|
113
|
+
await tar_1.default.extract({ file: tarPath, cwd: toPath, strip: 1 });
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
utilities_1.log.error(error);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return (0, utilities_1.oraPromise)(run(), {
|
|
120
|
+
text: `Decompress to ${toPath}`,
|
|
121
|
+
prefixText: chalk_1.default.blue('[info]'),
|
|
122
|
+
}, true);
|
|
123
|
+
};
|
|
124
|
+
exports.decompress = decompress;
|
|
125
|
+
const seedDatabase = async (instancePath) => {
|
|
126
|
+
try {
|
|
127
|
+
const pool = await (0, database_1.createPoolAndDatabaseIfNeeded)();
|
|
128
|
+
await (0, seed_1.seedByPool)(pool, 'all');
|
|
129
|
+
await pool.end();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error(error);
|
|
133
|
+
await (0, utilities_1.oraPromise)((0, fs_extra_1.remove)(instancePath), {
|
|
134
|
+
text: 'Clean up',
|
|
135
|
+
prefixText: chalk_1.default.blue('[info]'),
|
|
136
|
+
});
|
|
137
|
+
utilities_1.log.error('Error occurred during seeding your Logto database. Nothing has changed since the seeding process was in a transaction.\n\n' +
|
|
138
|
+
` To skip the database seeding, append ${chalk_1.default.green('--skip-seed')} to the command options.`);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
exports.seedDatabase = seedDatabase;
|
|
142
|
+
const createEnv = async (instancePath, databaseUrl) => {
|
|
143
|
+
const dotEnvPath = path_1.default.resolve(instancePath, '.env');
|
|
144
|
+
await (0, fs_extra_1.writeFile)(dotEnvPath, `DB_URL=${databaseUrl}`, {
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
});
|
|
147
|
+
utilities_1.log.info(`Saved database URL to ${chalk_1.default.blue(dotEnvPath)}`);
|
|
148
|
+
};
|
|
149
|
+
exports.createEnv = createEnv;
|
|
150
|
+
const logFinale = (instancePath) => {
|
|
151
|
+
const startCommand = `cd ${instancePath} && npm start`;
|
|
152
|
+
utilities_1.log.info(`Use the command below to start Logto. Happy hacking!\n\n ${chalk_1.default.green(startCommand)}`);
|
|
153
|
+
};
|
|
154
|
+
exports.logFinale = logFinale;
|
|
155
|
+
const inquireOfficialConnectors = async (initialAnswer) => {
|
|
156
|
+
const { value } = await inquirer_1.default.prompt({
|
|
157
|
+
name: 'value',
|
|
158
|
+
message: 'Do you want to add official connectors?',
|
|
159
|
+
type: 'confirm',
|
|
160
|
+
default: true,
|
|
161
|
+
}, { value: initialAnswer });
|
|
162
|
+
return value;
|
|
163
|
+
};
|
|
164
|
+
exports.inquireOfficialConnectors = inquireOfficialConnectors;
|
|
165
|
+
const isUrl = (string) => {
|
|
166
|
+
try {
|
|
167
|
+
// On purpose to test
|
|
168
|
+
// eslint-disable-next-line no-new
|
|
169
|
+
new URL(string);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
exports.isUrl = isUrl;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const connectorDirectory = "connectors";
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SchemaLike } from '@logto/schemas';
|
|
2
|
+
export declare const defaultDatabaseUrl = "postgresql://localhost:5432/logto";
|
|
3
|
+
export declare const getDatabaseUrlFromConfig: () => Promise<string>;
|
|
4
|
+
export declare const createPoolFromConfig: () => Promise<import("slonik").DatabasePool>;
|
|
5
|
+
/**
|
|
6
|
+
* Create a database pool with the URL in CLI config; if no URL found, prompt to input.
|
|
7
|
+
* If the given database does not exists, it will try to create a new database by connecting to the maintenance database `postgres`.
|
|
8
|
+
*
|
|
9
|
+
* @returns A new database pool with the database URL in config.
|
|
10
|
+
*/
|
|
11
|
+
export declare const createPoolAndDatabaseIfNeeded: () => Promise<import("slonik").DatabasePool>;
|
|
12
|
+
export declare const insertInto: <T extends SchemaLike<string>>(object: T, table: string) => import("slonik").TaggedTemplateLiteralInvocation<import("slonik").QueryResultRow>;
|
package/lib/database.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.insertInto = exports.createPoolAndDatabaseIfNeeded = exports.createPoolFromConfig = exports.getDatabaseUrlFromConfig = exports.defaultDatabaseUrl = void 0;
|
|
7
|
+
const shared_1 = require("@logto/shared");
|
|
8
|
+
const decamelize_1 = __importDefault(require("decamelize"));
|
|
9
|
+
const slonik_1 = require("slonik");
|
|
10
|
+
const slonik_interceptor_preset_1 = require("slonik-interceptor-preset");
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const utilities_1 = require("./utilities");
|
|
13
|
+
exports.defaultDatabaseUrl = 'postgresql://localhost:5432/logto';
|
|
14
|
+
const getDatabaseUrlFromConfig = async () => (await (0, utilities_1.getCliConfigWithPrompt)({
|
|
15
|
+
key: utilities_1.ConfigKey.DatabaseUrl,
|
|
16
|
+
readableKey: 'Logto database URL',
|
|
17
|
+
defaultValue: exports.defaultDatabaseUrl,
|
|
18
|
+
})) ?? '';
|
|
19
|
+
exports.getDatabaseUrlFromConfig = getDatabaseUrlFromConfig;
|
|
20
|
+
const createPoolFromConfig = async () => {
|
|
21
|
+
const databaseUrl = await (0, exports.getDatabaseUrlFromConfig)();
|
|
22
|
+
return (0, slonik_1.createPool)(databaseUrl, {
|
|
23
|
+
interceptors: (0, slonik_interceptor_preset_1.createInterceptors)(),
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
exports.createPoolFromConfig = createPoolFromConfig;
|
|
27
|
+
/**
|
|
28
|
+
* Create a database pool with the URL in CLI config; if no URL found, prompt to input.
|
|
29
|
+
* If the given database does not exists, it will try to create a new database by connecting to the maintenance database `postgres`.
|
|
30
|
+
*
|
|
31
|
+
* @returns A new database pool with the database URL in config.
|
|
32
|
+
*/
|
|
33
|
+
const createPoolAndDatabaseIfNeeded = async () => {
|
|
34
|
+
try {
|
|
35
|
+
return await (0, exports.createPoolFromConfig)();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const result = zod_1.z.object({ code: zod_1.z.string() }).safeParse(error);
|
|
39
|
+
// Database does not exist, try to create one
|
|
40
|
+
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
|
41
|
+
if (!(result.success && result.data.code === '3D000')) {
|
|
42
|
+
utilities_1.log.error(error);
|
|
43
|
+
}
|
|
44
|
+
const databaseUrl = await (0, exports.getDatabaseUrlFromConfig)();
|
|
45
|
+
const dsn = (0, slonik_1.parseDsn)(databaseUrl);
|
|
46
|
+
// It's ok to fall back to '?' since:
|
|
47
|
+
// - Database name is required to connect in the previous pool
|
|
48
|
+
// - It will throw error when creating database using '?'
|
|
49
|
+
const databaseName = dsn.databaseName ?? '?';
|
|
50
|
+
const maintenancePool = await (0, slonik_1.createPool)((0, slonik_1.stringifyDsn)({ ...dsn, databaseName: 'postgres' }));
|
|
51
|
+
await maintenancePool.query((0, slonik_1.sql) `
|
|
52
|
+
create database ${slonik_1.sql.identifier([databaseName])}
|
|
53
|
+
with
|
|
54
|
+
encoding = 'UTF8'
|
|
55
|
+
connection_limit = -1;
|
|
56
|
+
`);
|
|
57
|
+
await maintenancePool.end();
|
|
58
|
+
utilities_1.log.succeed(`Created database ${databaseName}`);
|
|
59
|
+
return (0, exports.createPoolFromConfig)();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
exports.createPoolAndDatabaseIfNeeded = createPoolAndDatabaseIfNeeded;
|
|
63
|
+
const insertInto = (object, table) => {
|
|
64
|
+
const keys = Object.keys(object);
|
|
65
|
+
return (0, slonik_1.sql) `
|
|
66
|
+
insert into ${slonik_1.sql.identifier([table])}
|
|
67
|
+
(${slonik_1.sql.join(keys.map((key) => slonik_1.sql.identifier([(0, decamelize_1.default)(key)])), (0, slonik_1.sql) `, `)})
|
|
68
|
+
values (${slonik_1.sql.join(keys.map((key) => (0, shared_1.convertToPrimitiveOrSql)(key, object[key] ?? null)), (0, slonik_1.sql) `, `)})
|
|
69
|
+
`;
|
|
70
|
+
};
|
|
71
|
+
exports.insertInto = insertInto;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
const yargs_1 = __importDefault(require("yargs"));
|
|
9
|
+
const helpers_1 = require("yargs/helpers");
|
|
10
|
+
const connector_1 = __importDefault(require("./commands/connector"));
|
|
11
|
+
const database_1 = __importDefault(require("./commands/database"));
|
|
12
|
+
const install_1 = __importDefault(require("./commands/install"));
|
|
13
|
+
const package_json_1 = require("./package-json");
|
|
14
|
+
const utilities_1 = require("./utilities");
|
|
15
|
+
void (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
16
|
+
.version(false)
|
|
17
|
+
.option('env', {
|
|
18
|
+
alias: ['e', 'env-file'],
|
|
19
|
+
describe: 'The path to your `.env` file',
|
|
20
|
+
type: 'string',
|
|
21
|
+
})
|
|
22
|
+
.option('db', {
|
|
23
|
+
alias: ['db-url', 'database-url'],
|
|
24
|
+
describe: 'The Postgres URL to Logto database',
|
|
25
|
+
type: 'string',
|
|
26
|
+
})
|
|
27
|
+
.option('version', {
|
|
28
|
+
alias: 'v',
|
|
29
|
+
describe: 'Print Logto CLI version',
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
global: false,
|
|
32
|
+
})
|
|
33
|
+
.middleware(({ version }) => {
|
|
34
|
+
if (version) {
|
|
35
|
+
console.log(package_json_1.packageJson.name + ' v' + package_json_1.packageJson.version);
|
|
36
|
+
// eslint-disable-next-line unicorn/no-process-exit
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
}, true)
|
|
40
|
+
.middleware(({ env, db: databaseUrl }) => {
|
|
41
|
+
dotenv_1.default.config({ path: env });
|
|
42
|
+
const initialDatabaseUrl = databaseUrl ?? process.env[utilities_1.ConfigKey.DatabaseUrl];
|
|
43
|
+
if (initialDatabaseUrl) {
|
|
44
|
+
utilities_1.cliConfig.set(utilities_1.ConfigKey.DatabaseUrl, initialDatabaseUrl);
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.command(install_1.default)
|
|
48
|
+
.command(database_1.default)
|
|
49
|
+
.command(connector_1.default)
|
|
50
|
+
.demandCommand(1)
|
|
51
|
+
.showHelpOnFail(false, `Specify ${chalk_1.default.green('--help')} for available options`)
|
|
52
|
+
.strict()
|
|
53
|
+
.parserConfiguration({
|
|
54
|
+
'dot-notation': false,
|
|
55
|
+
})
|
|
56
|
+
.parse();
|