From 6a62e32fa5a4be7036f6bf14d0b292be3efedb88 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 21 Apr 2022 16:13:59 +0800 Subject: [PATCH] refactor(core): inquire when required env not found (#586) * refactor(core): inquire when required env not found * refactor(core): add comments for create DB pool --- packages/core/package.json | 2 + packages/core/src/env-set/append-dot-env.ts | 7 ++ .../core/src/env-set/create-pool-by-env.ts | 37 +++++++++ packages/core/src/env-set/index.ts | 51 ++---------- packages/core/src/env-set/oidc.ts | 56 +++++++++++++ packages/core/src/env-set/password.ts | 50 ++++++++++++ .../core/src/middleware/koa-spa-proxy.test.ts | 6 +- packages/core/src/utils/password.ts | 3 +- pnpm-lock.yaml | 81 ++++++++++++++----- 9 files changed, 225 insertions(+), 68 deletions(-) create mode 100644 packages/core/src/env-set/append-dot-env.ts create mode 100644 packages/core/src/env-set/create-pool-by-env.ts create mode 100644 packages/core/src/env-set/oidc.ts create mode 100644 packages/core/src/env-set/password.ts diff --git a/packages/core/package.json b/packages/core/package.json index 01de4e35d..aed07c6a2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -29,6 +29,7 @@ "got": "^11.8.2", "i18next": "^20.3.5", "iconv-lite": "0.6.3", + "inquirer": "^8.2.2", "jose": "^3.14.3", "koa": "^2.13.1", "koa-body": "^4.2.0", @@ -52,6 +53,7 @@ "@shopify/jest-koa-mocks": "^3.0.8", "@silverhand/eslint-config": "^0.10.2", "@silverhand/ts-config": "^0.10.2", + "@types/inquirer": "^8.2.1", "@types/jest": "^27.4.1", "@types/koa": "^2.13.3", "@types/koa-logger": "^3.1.1", diff --git a/packages/core/src/env-set/append-dot-env.ts b/packages/core/src/env-set/append-dot-env.ts new file mode 100644 index 000000000..949de2596 --- /dev/null +++ b/packages/core/src/env-set/append-dot-env.ts @@ -0,0 +1,7 @@ +import { appendFileSync } from 'fs'; + +const appendDotEnv = (key: string, value: string) => { + appendFileSync('.env', `${key}=${value}\n`); +}; + +export default appendDotEnv; diff --git a/packages/core/src/env-set/create-pool-by-env.ts b/packages/core/src/env-set/create-pool-by-env.ts new file mode 100644 index 000000000..62e828b9a --- /dev/null +++ b/packages/core/src/env-set/create-pool-by-env.ts @@ -0,0 +1,37 @@ +import { assertEnv } from '@silverhand/essentials'; +import inquirer from 'inquirer'; +import { createPool } from 'slonik'; +import { createInterceptors } from 'slonik-interceptor-preset'; + +import appendDotEnv from './append-dot-env'; + +const createPoolByEnv = async (isTest: boolean) => { + // Database connection is disabled in unit test environment + if (isTest) { + return; + } + + const key = 'DB_URL'; + const interceptors = [...createInterceptors()]; + + try { + const databaseDsn = assertEnv(key); + + return createPool(databaseDsn, { interceptors }); + } catch (error: unknown) { + const answer = await inquirer.prompt({ + name: 'dsn', + message: `No Postgres DSN (${key}) found in env variables. Please input the DSN which points to Logto database:`, + }); + + if (!answer.dsn) { + throw error; + } + + appendDotEnv(key, answer.dsn); + + return createPool(answer.dsn, { interceptors }); + } +}; + +export default createPoolByEnv; diff --git a/packages/core/src/env-set/index.ts b/packages/core/src/env-set/index.ts index a36a91281..c0212acbd 100644 --- a/packages/core/src/env-set/index.ts +++ b/packages/core/src/env-set/index.ts @@ -1,11 +1,9 @@ -import crypto from 'crypto'; -import { readFileSync } from 'fs'; +import { getEnv, Optional } from '@silverhand/essentials'; +import { DatabasePoolType } from 'slonik'; -import { assertEnv, getEnv, Optional } from '@silverhand/essentials'; -import { nanoid } from 'nanoid'; -import { createPool, DatabasePoolType } from 'slonik'; -import { createInterceptors } from 'slonik-interceptor-preset'; -import { string, number } from 'zod'; +import createPoolByEnv from './create-pool-by-env'; +import loadOidcValues from './oidc'; +import loadPasswordValues from './password'; export enum MountedApps { Api = 'api', @@ -13,50 +11,21 @@ export enum MountedApps { Console = 'console', } -const readPrivateKey = (path: string): Optional => { - try { - return readFileSync(path, 'utf-8'); - } catch {} -}; - -const loadOidcValues = (port: number) => { - const privateKeyPath = getEnv('OIDC_PRIVATE_KEY_PATH', 'oidc-private-key.pem'); - const privateKey = crypto.createPrivateKey(readPrivateKey(privateKeyPath) ?? ''); - const publicKey = crypto.createPublicKey(privateKey); - - return { - privateKeyPath, - privateKey, - publicKey, - issuer: getEnv('OIDC_ISSUER', `http://localhost:${port}/oidc`), - adminResource: getEnv('ADMIN_RESOURCE', 'https://api.logto.io'), - defaultIdTokenTtl: 60 * 60, - defaultRefreshTokenTtl: 14 * 24 * 60 * 60, - }; -}; - const loadEnvValues = async () => { const isProduction = getEnv('NODE_ENV') === 'production'; const isTest = getEnv('NODE_ENV') === 'test'; const port = Number(getEnv('PORT', '3001')); - const databaseUrl = isTest ? getEnv('DB_URL') : assertEnv('DB_URL'); return Object.freeze({ isTest, isProduction, - databaseUrl, httpsCert: process.env.HTTPS_CERT, httpsKey: process.env.HTTPS_KEY, port, developmentUserId: getEnv('DEVELOPMENT_USER_ID'), trustingTlsOffloadingProxies: getEnv('TRUSTING_TLS_OFFLOADING_PROXIES') === 'true', - passwordPeppers: string() - .array() - .parse(isTest ? [nanoid()] : JSON.parse(assertEnv('PASSWORD_PEPPERS'))), - passwordIterationCount: number() - .min(100) - .parse(Number(getEnv('PASSWORD_ITERATION_COUNT', '1000'))), - oidc: loadOidcValues(port), + password: await loadPasswordValues(isTest), + oidc: await loadOidcValues(port), }); }; @@ -89,11 +58,7 @@ function createEnvSet() { load: async () => { values = await loadEnvValues(); - - if (!values.isTest) { - const interceptors = [...createInterceptors()]; - pool = createPool(values.databaseUrl, { interceptors }); - } + pool = await createPoolByEnv(values.isTest); }, }; } diff --git a/packages/core/src/env-set/oidc.ts b/packages/core/src/env-set/oidc.ts new file mode 100644 index 000000000..115558c32 --- /dev/null +++ b/packages/core/src/env-set/oidc.ts @@ -0,0 +1,56 @@ +import crypto, { generateKeyPairSync } from 'crypto'; +import { readFileSync, writeFileSync } from 'fs'; + +import { getEnv } from '@silverhand/essentials'; +import inquirer from 'inquirer'; + +const readPrivateKey = async (path: string): Promise => { + const privateKeyPath = getEnv('OIDC_PRIVATE_KEY_PATH', 'oidc-private-key.pem'); + + try { + return readFileSync(path, 'utf-8'); + } catch (error: unknown) { + const answer = await inquirer.prompt({ + type: 'confirm', + name: 'confirm', + message: `No private key found in \`${privateKeyPath}\`, would you like to generate a new one?`, + }); + + if (!answer.confirm) { + throw error; + } + + const { privateKey } = generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }); + writeFileSync(privateKeyPath, privateKey); + + return privateKey; + } +}; + +const loadOidcValues = async (port: number) => { + const privateKeyPath = getEnv('OIDC_PRIVATE_KEY_PATH', 'oidc-private-key.pem'); + const privateKey = crypto.createPrivateKey(await readPrivateKey(privateKeyPath)); + const publicKey = crypto.createPublicKey(privateKey); + + return Object.freeze({ + privateKeyPath, + privateKey, + publicKey, + issuer: getEnv('OIDC_ISSUER', `http://localhost:${port}/oidc`), + adminResource: getEnv('ADMIN_RESOURCE', 'https://api.logto.io'), + defaultIdTokenTtl: 60 * 60, + defaultRefreshTokenTtl: 14 * 24 * 60 * 60, + }); +}; + +export default loadOidcValues; diff --git a/packages/core/src/env-set/password.ts b/packages/core/src/env-set/password.ts new file mode 100644 index 000000000..cee5bf32e --- /dev/null +++ b/packages/core/src/env-set/password.ts @@ -0,0 +1,50 @@ +import { assertEnv, getEnv } from '@silverhand/essentials'; +import inquirer from 'inquirer'; +import { nanoid } from 'nanoid'; +import { number, string } from 'zod'; + +import appendDotEnv from './append-dot-env'; + +const loadPeppers = async (isTest: boolean): Promise => { + if (isTest) { + return [nanoid()]; + } + + const key = 'PASSWORD_PEPPERS'; + + try { + return string() + .array() + .parse(JSON.parse(assertEnv(key))); + } catch (error: unknown) { + if (!(error instanceof Error && error.message === `env variable ${key} not found`)) { + throw error; + } + + const answer = await inquirer.prompt({ + type: 'confirm', + name: 'confirm', + message: `No password peppers (${key}) found in env variables, would you like to generate a new set and save it into \`.env\`?`, + }); + + if (!answer.confirm) { + throw error; + } + + const peppers = [nanoid(), nanoid(), nanoid()]; + appendDotEnv(key, JSON.stringify(peppers)); + + return peppers; + } +}; + +const loadPasswordValues = async (isTest: boolean) => { + return Object.freeze({ + peppers: await loadPeppers(isTest), + iterationCount: number() + .min(100) + .parse(Number(getEnv('PASSWORD_ITERATION_COUNT', '1000'))), + }); +}; + +export default loadPasswordValues; diff --git a/packages/core/src/middleware/koa-spa-proxy.test.ts b/packages/core/src/middleware/koa-spa-proxy.test.ts index 5ab3ddafb..b347a4903 100644 --- a/packages/core/src/middleware/koa-spa-proxy.test.ts +++ b/packages/core/src/middleware/koa-spa-proxy.test.ts @@ -51,8 +51,7 @@ describe('koaSpaProxy middleware', () => { const spy = jest.spyOn(envSet, 'values', 'get').mockReturnValue({ ...envSet.values, isProduction: true, - passwordPeppers: ['foo'], - databaseUrl: 'some_db_url', + password: { peppers: ['foo'], iterationCount: 1000 }, }); const ctx = createContextWithRouteParameters({ @@ -70,8 +69,7 @@ describe('koaSpaProxy middleware', () => { const spy = jest.spyOn(envSet, 'values', 'get').mockReturnValue({ ...envSet.values, isProduction: true, - passwordPeppers: ['foo'], - databaseUrl: 'some_db_url', + password: { peppers: ['foo'], iterationCount: 1000 }, }); const ctx = createContextWithRouteParameters({ diff --git a/packages/core/src/utils/password.ts b/packages/core/src/utils/password.ts index 8c33f14d2..c90d47639 100644 --- a/packages/core/src/utils/password.ts +++ b/packages/core/src/utils/password.ts @@ -24,9 +24,8 @@ export const encryptPassword = ( (accumulator, current) => accumulator + (current.codePointAt(0) ?? 0), 0 ); - const peppers = envSet.values.passwordPeppers; + const { peppers, iterationCount } = envSet.values.password; const pepper = peppers[sum % peppers.length]; - const iterationCount = envSet.values.passwordIterationCount; assertThat(pepper, 'password.pepper_not_found'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 671a91f42..750d56a29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,6 +164,7 @@ importers: '@silverhand/eslint-config': ^0.10.2 '@silverhand/essentials': ^1.1.0 '@silverhand/ts-config': ^0.10.2 + '@types/inquirer': ^8.2.1 '@types/jest': ^27.4.1 '@types/koa': ^2.13.3 '@types/koa-logger': ^3.1.1 @@ -181,6 +182,7 @@ importers: got: ^11.8.2 i18next: ^20.3.5 iconv-lite: 0.6.3 + inquirer: ^8.2.2 jest: ^27.5.1 jest-matcher-specific-error: ^1.0.0 jose: ^3.14.3 @@ -220,6 +222,7 @@ importers: got: 11.8.3 i18next: 20.6.1 iconv-lite: 0.6.3 + inquirer: 8.2.2 jose: 3.20.3 koa: 2.13.4 koa-body: 4.2.0 @@ -242,6 +245,7 @@ importers: '@shopify/jest-koa-mocks': 3.0.8 '@silverhand/eslint-config': 0.10.2_3a533fa6cc3da0cf8525ef55d41c4384 '@silverhand/ts-config': 0.10.2_typescript@4.6.2 + '@types/inquirer': 8.2.1 '@types/jest': 27.4.1 '@types/koa': 2.13.4 '@types/koa-logger': 3.1.2 @@ -5792,6 +5796,13 @@ packages: '@types/node': 17.0.23 dev: false + /@types/inquirer/8.2.1: + resolution: {integrity: sha512-wKW3SKIUMmltbykg4I5JzCVzUhkuD9trD6efAmYgN2MrSntY0SMRQzEnD3mkyJ/rv9NLbTC7g3hKKE86YwEDLw==} + dependencies: + '@types/through': 0.0.30 + rxjs: 7.5.5 + dev: true + /@types/istanbul-lib-coverage/2.0.3: resolution: {integrity: sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==} dev: true @@ -6066,6 +6077,12 @@ packages: '@types/superagent': 4.1.15 dev: true + /@types/through/0.0.30: + resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==} + dependencies: + '@types/node': 17.0.23 + dev: true + /@types/unist/2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} @@ -6617,7 +6634,6 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.21.3 - dev: true /ansi-html-community/0.0.8: resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} @@ -7462,7 +7478,6 @@ packages: /chardet/0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true /cheerio-select/1.6.0: resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==} @@ -7584,7 +7599,11 @@ packages: engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 - dev: true + + /cli-spinners/2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + dev: false /cli-table3/0.6.1: resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} @@ -7606,7 +7625,6 @@ packages: /cli-width/3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} - dev: true /cliui/7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -7639,7 +7657,6 @@ packages: /clone/1.0.4: resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=} engines: {node: '>=0.8'} - dev: true /clone/2.1.2: resolution: {integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=} @@ -8521,7 +8538,6 @@ packages: resolution: {integrity: sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=} dependencies: clone: 1.0.4 - dev: true /defer-to-connect/1.1.3: resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} @@ -9583,7 +9599,6 @@ packages: chardet: 0.7.0 iconv-lite: 0.4.24 tmp: 0.0.33 - dev: true /extsprintf/1.3.0: resolution: {integrity: sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=} @@ -9711,7 +9726,6 @@ packages: engines: {node: '>=8'} dependencies: escape-string-regexp: 1.0.5 - dev: true /file-entry-cache/6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} @@ -11052,6 +11066,26 @@ packages: through: 2.3.8 dev: true + /inquirer/8.2.2: + resolution: {integrity: sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.5.5 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + dev: false + /int64-buffer/0.99.1007: resolution: {integrity: sha512-XDBEu44oSTqlvCSiOZ/0FoUkpWu/vwjJLGSKDabNISPQNZ5wub1FodGHBljRsrR0IXRPq7SslshZYMuA55CgTQ==} engines: {node: '>= 4.5.0'} @@ -11235,6 +11269,11 @@ packages: is-path-inside: 3.0.3 dev: false + /is-interactive/1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + dev: false + /is-js-type/2.0.0: resolution: {integrity: sha1-c2FwBtZZtOtHKbunR9KHgt8PfiI=} dependencies: @@ -11398,7 +11437,6 @@ packages: /is-unicode-supported/0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - dev: true /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} @@ -12670,7 +12708,6 @@ packages: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - dev: true /log-update/4.0.0: resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} @@ -13602,7 +13639,6 @@ packages: /mute-stream/0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - dev: true /nan/2.15.0: resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==} @@ -14126,6 +14162,21 @@ packages: word-wrap: 1.2.3 dev: true + /ora/5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + dev: false + /ordered-binary/1.2.4: resolution: {integrity: sha512-A/csN0d3n+igxBPfUrjbV5GC69LWj2pjZzAAeeHXLukQ4+fytfP4T1Lg0ju7MSPSwq7KtHkGaiwO8URZN5IpLg==} dev: true @@ -14138,7 +14189,6 @@ packages: /os-tmpdir/1.0.2: resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} engines: {node: '>=0.10.0'} - dev: true /osenv/0.1.5: resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} @@ -16882,7 +16932,6 @@ packages: dependencies: onetime: 5.1.2 signal-exit: 3.0.6 - dev: true /retry/0.12.0: resolution: {integrity: sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=} @@ -16968,7 +17017,6 @@ packages: /run-async/2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} - dev: true /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -16992,7 +17040,6 @@ packages: resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==} dependencies: tslib: 2.3.1 - dev: false /sade/1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} @@ -18305,7 +18352,6 @@ packages: /through/2.3.8: resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} - dev: true /through2/2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} @@ -18347,7 +18393,6 @@ packages: engines: {node: '>=0.6.0'} dependencies: os-tmpdir: 1.0.2 - dev: true /tmpl/1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -18679,7 +18724,6 @@ packages: /type-fest/0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - dev: true /type-fest/0.4.1: resolution: {integrity: sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw==} @@ -19265,7 +19309,6 @@ packages: resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=} dependencies: defaults: 1.0.3 - dev: true /weak-lru-cache/1.2.2: resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==}