mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 05:17:28 -08:00
feat(core): Add license support to n8n (#4566)
* add sdk * add license manager * type fix * add basic func * store to db * update default * activate license * add sharing flag * fix setup * clear license * update conosle log to info * refactor * use npm dependency * update error logs * add simple test * add license tests * update tests * update pnpm package * fix error handling types * Update packages/cli/src/config/schema.ts Co-authored-by: Cornelius Suermann <cornelius@n8n.io> * make feature enum * add warning * update sdk * Update packages/cli/src/config/schema.ts Co-authored-by: Cornelius Suermann <cornelius@n8n.io> Co-authored-by: Cornelius Suermann <cornelius@n8n.io>
This commit is contained in:
parent
a9bdc0bbfe
commit
30e5d3d04c
|
@ -103,12 +103,13 @@
|
||||||
"tsconfig-paths": "^3.14.1"
|
"tsconfig-paths": "^3.14.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@n8n_io/license-sdk": "^1.3.4",
|
||||||
"@oclif/command": "^1.8.16",
|
"@oclif/command": "^1.8.16",
|
||||||
"@oclif/core": "^1.16.4",
|
"@oclif/core": "^1.16.4",
|
||||||
"@oclif/errors": "^1.3.6",
|
"@oclif/errors": "^1.3.6",
|
||||||
"@rudderstack/rudder-sdk-node": "1.0.6",
|
"@rudderstack/rudder-sdk-node": "1.0.6",
|
||||||
"@sentry/node": "^7.17.3",
|
|
||||||
"@sentry/integrations": "^7.17.3",
|
"@sentry/integrations": "^7.17.3",
|
||||||
|
"@sentry/node": "^7.17.3",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"basic-auth": "^2.0.1",
|
"basic-auth": "^2.0.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|
121
packages/cli/src/License.ts
Normal file
121
packages/cli/src/License.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import { LicenseManager, TLicenseContainerStr } from '@n8n_io/license-sdk';
|
||||||
|
import { ILogger } from 'n8n-workflow';
|
||||||
|
import { getLogger } from './Logger';
|
||||||
|
import config from '@/config';
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import { LICENSE_FEATURES, SETTINGS_LICENSE_CERT_KEY } from './constants';
|
||||||
|
|
||||||
|
async function loadCertStr(): Promise<TLicenseContainerStr> {
|
||||||
|
const databaseSettings = await Db.collections.Settings.findOne({
|
||||||
|
where: {
|
||||||
|
key: SETTINGS_LICENSE_CERT_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return databaseSettings?.value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCertStr(value: TLicenseContainerStr): Promise<void> {
|
||||||
|
await Db.collections.Settings.upsert(
|
||||||
|
{
|
||||||
|
key: SETTINGS_LICENSE_CERT_KEY,
|
||||||
|
value,
|
||||||
|
loadOnStartup: false,
|
||||||
|
},
|
||||||
|
['key'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class License {
|
||||||
|
private logger: ILogger;
|
||||||
|
|
||||||
|
private manager: LicenseManager | undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logger = getLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(instanceId: string, version: string) {
|
||||||
|
if (this.manager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = config.getEnv('license.serverUrl');
|
||||||
|
const autoRenewEnabled = config.getEnv('license.autoRenewEnabled');
|
||||||
|
const autoRenewOffset = config.getEnv('license.autoRenewOffset');
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.manager = new LicenseManager({
|
||||||
|
server,
|
||||||
|
tenantId: 1,
|
||||||
|
productIdentifier: `n8n-${version}`,
|
||||||
|
autoRenewEnabled,
|
||||||
|
autoRenewOffset,
|
||||||
|
logger: this.logger,
|
||||||
|
loadCertStr,
|
||||||
|
saveCertStr,
|
||||||
|
deviceFingerprint: () => instanceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.manager.initialize();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
this.logger.error('Could not initialize license manager sdk', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async activate(activationKey: string): Promise<void> {
|
||||||
|
if (!this.manager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.manager.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.manager.activate(activationKey);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
this.logger.error('Could not activate license', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renew() {
|
||||||
|
if (!this.manager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.manager.renew();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
this.logger.error('Could not renew license', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isFeatureEnabled(feature: string): boolean {
|
||||||
|
if (!this.manager) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.manager.hasFeatureEnabled(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSharingEnabled() {
|
||||||
|
return this.isFeatureEnabled(LICENSE_FEATURES.SHARING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let licenseInstance: License | undefined;
|
||||||
|
|
||||||
|
export function getLicense(): License {
|
||||||
|
if (licenseInstance === undefined) {
|
||||||
|
licenseInstance = new License();
|
||||||
|
}
|
||||||
|
|
||||||
|
return licenseInstance;
|
||||||
|
}
|
|
@ -160,6 +160,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
|
||||||
import { ResponseError } from '@/ResponseHelper';
|
import { ResponseError } from '@/ResponseHelper';
|
||||||
import { toHttpNodeParameters } from '@/CurlConverterHelper';
|
import { toHttpNodeParameters } from '@/CurlConverterHelper';
|
||||||
import { setupErrorMiddleware } from '@/ErrorReporting';
|
import { setupErrorMiddleware } from '@/ErrorReporting';
|
||||||
|
import { getLicense } from '@/License';
|
||||||
|
|
||||||
require('body-parser-xml')(bodyParser);
|
require('body-parser-xml')(bodyParser);
|
||||||
|
|
||||||
|
@ -384,6 +385,16 @@ class App {
|
||||||
return this.frontendSettings;
|
return this.frontendSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initLicense(): Promise<void> {
|
||||||
|
const license = getLicense();
|
||||||
|
await license.init(this.frontendSettings.instanceId, this.frontendSettings.versionCli);
|
||||||
|
|
||||||
|
const activationKey = config.getEnv('license.activationKey');
|
||||||
|
if (activationKey) {
|
||||||
|
await license.activate(activationKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async config(): Promise<void> {
|
async config(): Promise<void> {
|
||||||
const enableMetrics = config.getEnv('endpoints.metrics.enable');
|
const enableMetrics = config.getEnv('endpoints.metrics.enable');
|
||||||
let register: Registry;
|
let register: Registry;
|
||||||
|
@ -406,6 +417,8 @@ class App {
|
||||||
|
|
||||||
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
||||||
|
|
||||||
|
await this.initLicense();
|
||||||
|
|
||||||
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
|
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
|
||||||
|
|
||||||
const ignoredEndpoints = [
|
const ignoredEndpoints = [
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Role } from '@db/entities/Role';
|
||||||
import { AuthenticatedRequest } from '@/requests';
|
import { AuthenticatedRequest } from '@/requests';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { getWebhookBaseUrl } from '../WebhookHelpers';
|
import { getWebhookBaseUrl } from '../WebhookHelpers';
|
||||||
|
import { getLicense } from '@/License';
|
||||||
import { WhereClause } from '@/Interfaces';
|
import { WhereClause } from '@/Interfaces';
|
||||||
|
|
||||||
export async function getWorkflowOwner(workflowId: string | number): Promise<User> {
|
export async function getWorkflowOwner(workflowId: string | number): Promise<User> {
|
||||||
|
@ -41,7 +42,11 @@ export function isUserManagementEnabled(): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSharingEnabled(): boolean {
|
export function isSharingEnabled(): boolean {
|
||||||
return isUserManagementEnabled() && config.getEnv('enterprise.features.sharing');
|
const license = getLicense();
|
||||||
|
return (
|
||||||
|
isUserManagementEnabled() &&
|
||||||
|
(config.getEnv('enterprise.features.sharing') || license.isSharingEnabled())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isUserManagementDisabled(): boolean {
|
export function isUserManagementDisabled(): boolean {
|
||||||
|
|
42
packages/cli/src/commands/license/clear.ts
Normal file
42
packages/cli/src/commands/license/clear.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { Command } from '@oclif/command';
|
||||||
|
|
||||||
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
|
||||||
|
import { getLogger } from '@/Logger';
|
||||||
|
import { SETTINGS_LICENSE_CERT_KEY } from '@/constants';
|
||||||
|
|
||||||
|
export class ClearLicenseCommand extends Command {
|
||||||
|
static description = 'Clear license';
|
||||||
|
|
||||||
|
static examples = [`$ n8n clear:license`];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Db.init();
|
||||||
|
|
||||||
|
console.info('Clearing license from database.');
|
||||||
|
await Db.collections.Settings.delete({
|
||||||
|
key: SETTINGS_LICENSE_CERT_KEY,
|
||||||
|
});
|
||||||
|
console.info('Done. Restart n8n to take effect.');
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error('Error updating database. See log messages for details.');
|
||||||
|
logger.error('\nGOT ERROR');
|
||||||
|
logger.info('====================================');
|
||||||
|
if (e instanceof Error) {
|
||||||
|
logger.error(e.message);
|
||||||
|
if (e.stack) {
|
||||||
|
logger.error(e.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exit();
|
||||||
|
}
|
||||||
|
}
|
|
@ -987,4 +987,31 @@ export const schema = {
|
||||||
env: 'N8N_ONBOARDING_CALL_PROMPTS_ENABLED',
|
env: 'N8N_ONBOARDING_CALL_PROMPTS_ENABLED',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
license: {
|
||||||
|
serverUrl: {
|
||||||
|
format: String,
|
||||||
|
default: 'https://license.n8n.io/v1',
|
||||||
|
env: 'N8N_LICENSE_SERVER_URL',
|
||||||
|
doc: 'License server url to retrieve license.',
|
||||||
|
},
|
||||||
|
autoRenewEnabled: {
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
env: 'N8N_LICENSE_AUTO_RENEW_ENABLED',
|
||||||
|
doc: 'Whether autorenew for licenses is enabled.',
|
||||||
|
},
|
||||||
|
autoRenewOffset: {
|
||||||
|
format: Number,
|
||||||
|
default: 60 * 60 * 72, // 72 hours
|
||||||
|
env: 'N8N_LICENSE_AUTO_RENEW_OFFSET',
|
||||||
|
doc: 'How many seconds before expiry a license should get automatically renewed. ',
|
||||||
|
},
|
||||||
|
activationKey: {
|
||||||
|
format: String,
|
||||||
|
default: '',
|
||||||
|
env: 'N8N_LICENSE_ACTIVATION_KEY',
|
||||||
|
doc: 'Activation key to initialize license',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,3 +41,9 @@ export const UNKNOWN_FAILURE_REASON = 'Unknown failure reason';
|
||||||
|
|
||||||
export const WORKFLOW_REACTIVATE_INITIAL_TIMEOUT = 1000;
|
export const WORKFLOW_REACTIVATE_INITIAL_TIMEOUT = 1000;
|
||||||
export const WORKFLOW_REACTIVATE_MAX_TIMEOUT = 180000;
|
export const WORKFLOW_REACTIVATE_MAX_TIMEOUT = 180000;
|
||||||
|
|
||||||
|
export const SETTINGS_LICENSE_CERT_KEY = 'license.cert';
|
||||||
|
|
||||||
|
export enum LICENSE_FEATURES {
|
||||||
|
SHARING = 'feat:sharing',
|
||||||
|
}
|
||||||
|
|
77
packages/cli/test/unit/License.test.ts
Normal file
77
packages/cli/test/unit/License.test.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { LicenseManager } from '@n8n_io/license-sdk';
|
||||||
|
import config from '@/config';
|
||||||
|
import { License } from '@/License';
|
||||||
|
|
||||||
|
jest.mock('@n8n_io/license-sdk');
|
||||||
|
|
||||||
|
const MOCK_SERVER_URL = 'https://server.com/v1';
|
||||||
|
const MOCK_RENEW_OFFSET = 259200;
|
||||||
|
const MOCK_INSTANCE_ID = 'instance-id';
|
||||||
|
const MOCK_N8N_VERSION = '0.27.0';
|
||||||
|
const MOCK_ACTIVATION_KEY = 'activation-key';
|
||||||
|
const MOCK_FEATURE_FLAG = 'feat:mock';
|
||||||
|
|
||||||
|
describe('License', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
config.set('license.serverUrl', MOCK_SERVER_URL);
|
||||||
|
config.set('license.autoRenewEnabled', true);
|
||||||
|
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
|
||||||
|
});
|
||||||
|
|
||||||
|
let license;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
license = new License();
|
||||||
|
await license.init(MOCK_INSTANCE_ID, MOCK_N8N_VERSION);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initializes license manager', async () => {
|
||||||
|
expect(LicenseManager).toHaveBeenCalledWith({
|
||||||
|
autoRenewEnabled: true,
|
||||||
|
autoRenewOffset: MOCK_RENEW_OFFSET,
|
||||||
|
deviceFingerprint: expect.any(Function),
|
||||||
|
productIdentifier: `n8n-${MOCK_N8N_VERSION}`,
|
||||||
|
logger: expect.anything(),
|
||||||
|
loadCertStr: expect.any(Function),
|
||||||
|
saveCertStr: expect.any(Function),
|
||||||
|
server: MOCK_SERVER_URL,
|
||||||
|
tenantId: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('activates license if current license is not valid', async () => {
|
||||||
|
LicenseManager.prototype.isValid.mockReturnValue(false);
|
||||||
|
|
||||||
|
await license.activate(MOCK_ACTIVATION_KEY);
|
||||||
|
|
||||||
|
expect(LicenseManager.prototype.isValid).toHaveBeenCalled();
|
||||||
|
expect(LicenseManager.prototype.activate).toHaveBeenCalledWith(MOCK_ACTIVATION_KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not activate license if current license is valid', async () => {
|
||||||
|
LicenseManager.prototype.isValid.mockReturnValue(true);
|
||||||
|
|
||||||
|
await license.activate(MOCK_ACTIVATION_KEY);
|
||||||
|
|
||||||
|
expect(LicenseManager.prototype.isValid).toHaveBeenCalled();
|
||||||
|
expect(LicenseManager.prototype.activate).not.toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renews license', async () => {
|
||||||
|
await license.renew();
|
||||||
|
|
||||||
|
expect(LicenseManager.prototype.renew).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('check if feature is enabled', async () => {
|
||||||
|
await license.isFeatureEnabled(MOCK_FEATURE_FLAG);
|
||||||
|
|
||||||
|
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('check if sharing feature is enabled', async () => {
|
||||||
|
await license.isFeatureEnabled(MOCK_FEATURE_FLAG);
|
||||||
|
|
||||||
|
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||||
|
});
|
||||||
|
});
|
|
@ -91,6 +91,7 @@ importers:
|
||||||
packages/cli:
|
packages/cli:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@apidevtools/swagger-cli': 4.0.0
|
'@apidevtools/swagger-cli': 4.0.0
|
||||||
|
'@n8n_io/license-sdk': ^1.3.4
|
||||||
'@oclif/command': ^1.8.16
|
'@oclif/command': ^1.8.16
|
||||||
'@oclif/core': ^1.16.4
|
'@oclif/core': ^1.16.4
|
||||||
'@oclif/dev-cli': ^1.22.2
|
'@oclif/dev-cli': ^1.22.2
|
||||||
|
@ -203,6 +204,7 @@ importers:
|
||||||
winston: ^3.3.3
|
winston: ^3.3.3
|
||||||
yamljs: ^0.3.0
|
yamljs: ^0.3.0
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@n8n_io/license-sdk': 1.3.4
|
||||||
'@oclif/command': 1.8.18_@oclif+config@1.18.5
|
'@oclif/command': 1.8.18_@oclif+config@1.18.5
|
||||||
'@oclif/core': 1.16.6
|
'@oclif/core': 1.16.6
|
||||||
'@oclif/errors': 1.3.6
|
'@oclif/errors': 1.3.6
|
||||||
|
@ -3306,6 +3308,18 @@ packages:
|
||||||
resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==}
|
resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@n8n_io/license-sdk/1.3.4:
|
||||||
|
resolution: {integrity: sha512-t/J228ftRwTbuCYTqMtjRjfqeNE8Aq0DErYFZS5N9EbX5brNIzWbgGio+25utoI9t31xRPwpviDF+e0kPz8Fvg==}
|
||||||
|
engines: {node: '>=14.0.0', npm: '>=7.10.0'}
|
||||||
|
dependencies:
|
||||||
|
axios: 1.1.3
|
||||||
|
crypto-js: 4.1.1
|
||||||
|
node-machine-id: 1.1.12
|
||||||
|
node-rsa: 1.1.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@n8n_io/riot-tmpl/1.0.1:
|
/@n8n_io/riot-tmpl/1.0.1:
|
||||||
resolution: {integrity: sha512-+ig7/rafN3LGthGEi8fs1N5XxPndmRq5YAX92DWOar9mrMDrYyIjK5XAQaTnTMDQgmKKllrAl+bVRmQXKcLFuw==}
|
resolution: {integrity: sha512-+ig7/rafN3LGthGEi8fs1N5XxPndmRq5YAX92DWOar9mrMDrYyIjK5XAQaTnTMDQgmKKllrAl+bVRmQXKcLFuw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7633,6 +7647,16 @@ packages:
|
||||||
- debug
|
- debug
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/axios/1.1.3:
|
||||||
|
resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==}
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.2_debug@3.2.7
|
||||||
|
form-data: 4.0.0
|
||||||
|
proxy-from-env: 1.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
|
|
||||||
/babel-core/7.0.0-bridge.0_@babel+core@7.19.3:
|
/babel-core/7.0.0-bridge.0_@babel+core@7.19.3:
|
||||||
resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==}
|
resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -16077,6 +16101,10 @@ packages:
|
||||||
vm-browserify: 1.1.2
|
vm-browserify: 1.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/node-machine-id/1.1.12:
|
||||||
|
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-notifier/10.0.1:
|
/node-notifier/10.0.1:
|
||||||
resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==}
|
resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -16092,6 +16120,12 @@ packages:
|
||||||
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
|
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/node-rsa/1.1.1:
|
||||||
|
resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==}
|
||||||
|
dependencies:
|
||||||
|
asn1: 0.2.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-ssh/12.0.5:
|
/node-ssh/12.0.5:
|
||||||
resolution: {integrity: sha512-uN2GTGdBRUUKkZmcNBr9OM+xKL6zq74emnkSyb1TshBdVWegj3boue6QallQeqZzo7YGVheP5gAovUL+8hZSig==}
|
resolution: {integrity: sha512-uN2GTGdBRUUKkZmcNBr9OM+xKL6zq74emnkSyb1TshBdVWegj3boue6QallQeqZzo7YGVheP5gAovUL+8hZSig==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
Loading…
Reference in a new issue