feat: Introduce Azure Key Vault as external secrets provider (#10054)

This commit is contained in:
Iván Ovejero 2024-07-18 15:51:48 +02:00 committed by GitHub
parent 5a9a2713b4
commit 1b6c2d3a37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 361 additions and 46 deletions

View file

@ -81,6 +81,8 @@
"ts-essentials": "^7.0.3"
},
"dependencies": {
"@azure/identity": "^4.3.0",
"@azure/keyvault-secrets": "^4.8.0",
"@n8n/client-oauth2": "workspace:*",
"@n8n/config": "workspace:*",
"@n8n/localtunnel": "2.1.0",

View file

@ -3,6 +3,7 @@ import { Service } from 'typedi';
import { InfisicalProvider } from './providers/infisical';
import { VaultProvider } from './providers/vault';
import { AwsSecretsManager } from './providers/aws-secrets/aws-secrets-manager';
import { AzureKeyVault } from './providers/azure-key-vault/azure-key-vault';
@Service()
export class ExternalSecretsProviders {
@ -10,6 +11,7 @@ export class ExternalSecretsProviders {
awsSecretsManager: AwsSecretsManager,
infisical: InfisicalProvider,
vault: VaultProvider,
azureKeyVault: AzureKeyVault,
};
getProvider(name: string): { new (): SecretsProvider } | null {

View file

@ -1,5 +1,16 @@
import type { INodeProperties } from 'n8n-workflow';
export const EXTERNAL_SECRETS_DB_KEY = 'feature.externalSecrets';
export const EXTERNAL_SECRETS_INITIAL_BACKOFF = 10 * 1000;
export const EXTERNAL_SECRETS_MAX_BACKOFF = 5 * 60 * 1000;
export const EXTERNAL_SECRETS_NAME_REGEX = /^[a-zA-Z0-9\-\_\/]+$/;
export const DOCS_HELP_NOTICE: INodeProperties = {
displayName:
'Need help filling out these fields? <a href="https://docs.n8n.io/external-secrets/#connect-n8n-to-your-secrets-store" target="_blank">Open docs</a>',
name: 'notice',
type: 'notice',
default: '',
noDataExpression: true,
};

View file

@ -0,0 +1,70 @@
import { SecretClient } from '@azure/keyvault-secrets';
import type { KeyVaultSecret } from '@azure/keyvault-secrets';
import { AzureKeyVault } from '../azure-key-vault/azure-key-vault';
import { mock } from 'jest-mock-extended';
import type { AzureKeyVaultContext } from '../azure-key-vault/types';
jest.mock('@azure/identity');
jest.mock('@azure/keyvault-secrets');
describe('AzureKeyVault', () => {
const azureKeyVault = new AzureKeyVault();
afterEach(() => {
jest.clearAllMocks();
});
it('should update cached secrets', async () => {
/**
* Arrange
*/
await azureKeyVault.init(
mock<AzureKeyVaultContext>({
settings: {
vaultName: 'my-vault',
tenantId: 'my-tenant-id',
clientId: 'my-client-id',
clientSecret: 'my-client-secret',
},
}),
);
const listSpy = jest
.spyOn(SecretClient.prototype, 'listPropertiesOfSecrets')
// @ts-expect-error Partial mock
.mockImplementation(() => ({
async *[Symbol.asyncIterator]() {
yield { name: 'secret1' };
yield { name: 'secret2' };
yield { name: 'secret3' }; // no value
yield { name: '#@&' }; // invalid name
},
}));
const getSpy = jest
.spyOn(SecretClient.prototype, 'getSecret')
.mockImplementation(async (name: string) => {
return mock<KeyVaultSecret>({ value: { secret1: 'value1', secret2: 'value2' }[name] });
});
/**
* Act
*/
await azureKeyVault.connect();
await azureKeyVault.update();
/**
* Assert
*/
expect(listSpy).toHaveBeenCalled();
expect(getSpy).toHaveBeenCalledWith('secret1');
expect(getSpy).toHaveBeenCalledWith('secret2');
expect(getSpy).toHaveBeenCalledWith('secret3');
expect(getSpy).not.toHaveBeenCalledWith('#@&');
expect(azureKeyVault.getSecret('secret1')).toBe('value1');
expect(azureKeyVault.getSecret('secret2')).toBe('value2');
expect(azureKeyVault.getSecret('secret3')).toBeUndefined(); // no value
expect(azureKeyVault.getSecret('#@&')).toBeUndefined(); // invalid name
});
});

View file

@ -1,6 +1,6 @@
import { AwsSecretsClient } from './aws-secrets-client';
import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error';
import { EXTERNAL_SECRETS_NAME_REGEX } from '@/ExternalSecrets/constants';
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/ExternalSecrets/constants';
import type { SecretsProvider, SecretsProviderState } from '@/Interfaces';
import type { INodeProperties } from 'n8n-workflow';
import type { AwsSecretsManagerContext } from './types';
@ -13,14 +13,7 @@ export class AwsSecretsManager implements SecretsProvider {
state: SecretsProviderState = 'initializing';
properties: INodeProperties[] = [
{
displayName:
'Need help filling out these fields? <a href="https://docs.n8n.io/external-secrets/#connect-n8n-to-your-secrets-store" target="_blank">Open docs</a>',
name: 'notice',
type: 'notice',
default: '',
noDataExpression: true,
},
DOCS_HELP_NOTICE,
{
displayName: 'Region',
name: 'region',

View file

@ -0,0 +1,131 @@
import { ClientSecretCredential } from '@azure/identity';
import { SecretClient } from '@azure/keyvault-secrets';
import type { SecretsProvider, SecretsProviderState } from '@/Interfaces';
import type { INodeProperties } from 'n8n-workflow';
import type { AzureKeyVaultContext } from './types';
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/ExternalSecrets/constants';
export class AzureKeyVault implements SecretsProvider {
name = 'azureKeyVault';
displayName = 'Azure Key Vault';
state: SecretsProviderState = 'initializing';
properties: INodeProperties[] = [
DOCS_HELP_NOTICE,
{
displayName: 'Vault Name',
hint: 'The name of your existing Azure Key Vault.',
name: 'vaultName',
type: 'string',
default: '',
required: true,
placeholder: 'e.g. my-vault',
noDataExpression: true,
},
{
displayName: 'Tenant ID',
name: 'tenantId',
hint: 'In Azure, this can be called "Directory (Tenant) ID".',
type: 'string',
default: '',
required: true,
placeholder: 'e.g. 7dec9324-7074-72b7-a3ca-a9bb3012f466',
noDataExpression: true,
},
{
displayName: 'Client ID',
name: 'clientId',
hint: 'In Azure, this can be called "Application (Client) ID".',
type: 'string',
default: '',
required: true,
placeholder: 'e.g. 7753d8c2-e41f-22ed-3dd7-c9e96463622c',
typeOptions: { password: true },
noDataExpression: true,
},
{
displayName: 'Client Secret',
name: 'clientSecret',
hint: 'The client secret value of your registered application.',
type: 'string',
default: '',
required: true,
typeOptions: { password: true },
noDataExpression: true,
},
];
private cachedSecrets: Record<string, string> = {};
private client: SecretClient;
private settings: AzureKeyVaultContext['settings'];
async init(context: AzureKeyVaultContext) {
this.settings = context.settings;
}
async connect() {
const { vaultName, tenantId, clientId, clientSecret } = this.settings;
try {
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
this.client = new SecretClient(`https://${vaultName}.vault.azure.net/`, credential);
this.state = 'connected';
} catch (error) {
this.state = 'error';
}
}
async test(): Promise<[boolean] | [boolean, string]> {
if (!this.client) return [false, 'Failed to connect to Azure Key Vault'];
try {
await this.client.listPropertiesOfSecrets().next();
return [true];
} catch (error: unknown) {
return [false, error instanceof Error ? error.message : 'unknown error'];
}
}
async disconnect() {
// unused
}
async update() {
const secretNames: string[] = [];
for await (const secret of this.client.listPropertiesOfSecrets()) {
secretNames.push(secret.name);
}
const promises = secretNames
.filter((name) => EXTERNAL_SECRETS_NAME_REGEX.test(name))
.map(async (name) => {
const { value } = await this.client.getSecret(name);
return { name, value };
});
const secrets = await Promise.all(promises);
this.cachedSecrets = secrets.reduce<Record<string, string>>((acc, cur) => {
if (cur.value === undefined) return acc;
acc[cur.name] = cur.value;
return acc;
}, {});
}
getSecret(name: string) {
return this.cachedSecrets[name];
}
hasSecret(name: string) {
return name in this.cachedSecrets;
}
getSecretNames() {
return Object.keys(this.cachedSecrets);
}
}

View file

@ -0,0 +1,8 @@
import type { SecretsProviderSettings } from '@/Interfaces';
export type AzureKeyVaultContext = SecretsProviderSettings<{
vaultName: string;
tenantId: string;
clientId: string;
clientSecret: string;
}>;

View file

@ -3,7 +3,7 @@ import InfisicalClient from 'infisical-node';
import { populateClientWorkspaceConfigsHelper } from 'infisical-node/lib/helpers/key';
import { getServiceTokenData } from 'infisical-node/lib/api/serviceTokenData';
import { ApplicationError, type IDataObject, type INodeProperties } from 'n8n-workflow';
import { EXTERNAL_SECRETS_NAME_REGEX } from '../constants';
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '../constants';
export interface InfisicalSettings {
token: string;
@ -24,13 +24,7 @@ interface InfisicalServiceToken {
export class InfisicalProvider implements SecretsProvider {
properties: INodeProperties[] = [
{
displayName:
'Need help filling out these fields? <a href="https://docs.n8n.io/external-secrets/#connect-n8n-to-your-secrets-store" target="_blank">Open docs</a>',
name: 'notice',
type: 'notice',
default: '',
},
DOCS_HELP_NOTICE,
{
displayName: 'Service Token',
name: 'token',

View file

@ -4,7 +4,7 @@ import type { IDataObject, INodeProperties } from 'n8n-workflow';
import type { AxiosInstance, AxiosResponse } from 'axios';
import axios from 'axios';
import { Logger } from '@/Logger';
import { EXTERNAL_SECRETS_NAME_REGEX } from '../constants';
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '../constants';
import { preferGet } from '../externalSecretsHelper.ee';
import { Container } from 'typedi';
@ -85,13 +85,7 @@ interface VaultSecretList {
export class VaultProvider extends SecretsProvider {
properties: INodeProperties[] = [
{
displayName:
'Need help filling out these fields? <a href="https://docs.n8n.io/external-secrets/#connect-n8n-to-your-secrets-store" target="_blank">Open docs</a>',
name: 'notice',
type: 'notice',
default: '',
},
DOCS_HELP_NOTICE,
{
displayName: 'Vault URL',
name: 'url',

View file

@ -0,0 +1,23 @@
<svg width="150" height="150" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="e399c19f-b68f-429d-b176-18c2117ff73c" x1="-1032.172" x2="-1059.213" y1="145.312" y2="65.426" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#114a8b"/>
<stop offset="1" stop-color="#0669bc"/>
</linearGradient>
<linearGradient id="ac2a6fc2-ca48-4327-9a3c-d4dcc3256e15" x1="-1023.725" x2="-1029.98" y1="108.083" y2="105.968" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-opacity=".3"/>
<stop offset=".071" stop-opacity=".2"/>
<stop offset=".321" stop-opacity=".1"/>
<stop offset=".623" stop-opacity=".05"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="a7fee970-a784-4bb1-af8d-63d18e5f7db9" x1="-1027.165" x2="-997.482" y1="147.642" y2="68.561" gradientTransform="matrix(1 0 0 -1 1075 158)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3ccbf4"/>
<stop offset="1" stop-color="#2892df"/>
</linearGradient>
</defs>
<path fill="url(#e399c19f-b68f-429d-b176-18c2117ff73c)" d="M33.338 6.544h26.038l-27.03 80.087a4.152 4.152 0 0 1-3.933 2.824H8.149a4.145 4.145 0 0 1-3.928-5.47L29.404 9.368a4.152 4.152 0 0 1 3.934-2.825z"/>
<path fill="#0078d4" d="M71.175 60.261h-41.29a1.911 1.911 0 0 0-1.305 3.309l26.532 24.764a4.171 4.171 0 0 0 2.846 1.121h23.38z"/>
<path fill="url(#ac2a6fc2-ca48-4327-9a3c-d4dcc3256e15)" d="M33.338 6.544a4.118 4.118 0 0 0-3.943 2.879L4.252 83.917a4.14 4.14 0 0 0 3.908 5.538h20.787a4.443 4.443 0 0 0 3.41-2.9l5.014-14.777 17.91 16.705a4.237 4.237 0 0 0 2.666.972H81.24L71.024 60.261l-29.781.007L59.47 6.544z"/>
<path fill="url(#a7fee970-a784-4bb1-af8d-63d18e5f7db9)" d="M66.595 9.364a4.145 4.145 0 0 0-3.928-2.82H33.648a4.146 4.146 0 0 1 3.928 2.82l25.184 74.62a4.146 4.146 0 0 1-3.928 5.472h29.02a4.146 4.146 0 0 0 3.927-5.472z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -6,6 +6,7 @@ import infisical from '../assets/images/infisical.webp';
import doppler from '../assets/images/doppler.webp';
import vault from '../assets/images/hashicorp.webp';
import awsSecretsManager from '../assets/images/aws-secrets-manager.svg';
import azureKeyVault from '../assets/images/azure-key-vault.svg';
const props = defineProps<{
provider: ExternalSecretsProvider;
@ -18,6 +19,7 @@ const image = computed(
infisical,
vault,
awsSecretsManager,
azureKeyVault,
})[props.provider.name],
);
</script>

View file

@ -538,6 +538,12 @@ importers:
packages/cli:
dependencies:
'@azure/identity':
specifier: ^4.3.0
version: 4.3.0
'@azure/keyvault-secrets':
specifier: ^4.8.0
version: 4.8.0
'@n8n/client-oauth2':
specifier: workspace:*
version: link:../@n8n/client-oauth2
@ -1978,14 +1984,18 @@ packages:
resolution: {integrity: sha512-3X9wzaaGgRaBCwhLQZDtFp5uLIXCPrGbwJNWPPugvL4xbIGgScv77YzzxToKGLAKvG9amDoofMoP+9hsH1vs1w==}
engines: {node: '>=18.0.0'}
'@azure/core-client@1.6.1':
resolution: {integrity: sha512-mZ1MSKhZBYoV8GAWceA+PEJFWV2VpdNSpxxcj1wjIAOi00ykRuIQChT99xlQGZWLY3/NApWhSImlFwsmCEs4vA==}
engines: {node: '>=12.0.0'}
'@azure/core-client@1.9.2':
resolution: {integrity: sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==}
engines: {node: '>=18.0.0'}
'@azure/core-http-compat@1.3.0':
resolution: {integrity: sha512-ZN9avruqbQ5TxopzG3ih3KRy52n8OAbitX3fnZT5go4hzu0J+KVPSzkL+Wt3hpJpdG8WIfg1sBD1tWkgUdEpBA==}
engines: {node: '>=12.0.0'}
'@azure/core-http-compat@2.1.2':
resolution: {integrity: sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==}
engines: {node: '>=18.0.0'}
'@azure/core-http@2.3.2':
resolution: {integrity: sha512-Z4dfbglV9kNZO177CNx4bo5ekFuYwwsvjLiKdZI4r84bYGv3irrbQz7JC3/rUfFH2l4T/W6OFleJaa2X0IaQqw==}
engines: {node: '>=14.0.0'}
@ -2018,10 +2028,18 @@ packages:
resolution: {integrity: sha512-0q5DL4uyR0EZ4RXQKD8MadGH6zTIcloUoS/RVbCpNpej4pwte0xpqYxk8K97Py2RiuUvI7F4GXpoT4046VfufA==}
engines: {node: '>=14.0.0'}
'@azure/identity@4.3.0':
resolution: {integrity: sha512-LHZ58/RsIpIWa4hrrE2YuJ/vzG1Jv9f774RfTTAVDZDriubvJ0/S5u4pnw4akJDlS0TiJb6VMphmVUFsWmgodQ==}
engines: {node: '>=18.0.0'}
'@azure/keyvault-keys@4.6.0':
resolution: {integrity: sha512-0112LegxeR03L8J4k+q6HwBVvrpd9y+oInG0FG3NaHXN7YUubVBon/eb5jFI6edGrvNigpxSR0XIsprFXdkzCQ==}
engines: {node: '>=12.0.0'}
'@azure/keyvault-secrets@4.8.0':
resolution: {integrity: sha512-RGfpFk6XUXHfWuTAiokOe8t6ej5C4ijf4HVyJUmTfN6VjDBVPvTtoiOi/C5072/ENHScYZFhiYOgIjLgYjfJ/A==}
engines: {node: '>=18.0.0'}
'@azure/logger@1.0.3':
resolution: {integrity: sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==}
engines: {node: '>=12.0.0'}
@ -2030,10 +2048,22 @@ packages:
resolution: {integrity: sha512-mnmi8dCXVNZI+AGRq0jKQ3YiodlIC4W9npr6FCB9WN6NQT+6rq+cIlxgUb//BjLyzKsnYo+i4LROGeMyU+6v1A==}
engines: {node: '>=0.8.0'}
'@azure/msal-browser@3.19.0':
resolution: {integrity: sha512-3unHlh3qWtXbqks/TLq3qGWzxfmwRfk9tXSGvVCcHHnCH5QKtcg/JiDIeP/1B2qFlqnSgtYY0JPLy9EIVoZ7Ag==}
engines: {node: '>=0.8.0'}
'@azure/msal-common@14.13.0':
resolution: {integrity: sha512-b4M/tqRzJ4jGU91BiwCsLTqChveUEyFK3qY2wGfZ0zBswIBZjAxopx5CYt5wzZFKuN15HqRDYXQbztttuIC3nA==}
engines: {node: '>=0.8.0'}
'@azure/msal-common@14.7.1':
resolution: {integrity: sha512-v96btzjM7KrAu4NSEdOkhQSTGOuNUIIsUdB8wlyB9cdgl5KqEKnTonHUZ8+khvZ6Ap542FCErbnTyDWl8lZ2rA==}
engines: {node: '>=0.8.0'}
'@azure/msal-node@2.11.0':
resolution: {integrity: sha512-yNRCp4Do4CGSBe1WXq4DWhfa/vYZCUgGrweYLC5my/6eDnYMt0fYGPHuTMw0iRslQGXF3CecGAxXp7ab57V4zg==}
engines: {node: '>=16'}
'@azure/msal-node@2.6.4':
resolution: {integrity: sha512-nNvEPx009/80UATCToF+29NZYocn01uKrB91xtFr7bSqkqO1PuQGXRyYwryWRztUrYZ1YsSbw9A+LmwOhpVvcg==}
engines: {node: '>=16'}
@ -14146,9 +14176,9 @@ snapshots:
'@azure/core-util': 1.7.0
tslib: 2.6.2
'@azure/core-client@1.6.1':
'@azure/core-client@1.9.2':
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/abort-controller': 2.0.0
'@azure/core-auth': 1.6.0
'@azure/core-rest-pipeline': 1.9.2
'@azure/core-tracing': 1.0.1
@ -14161,7 +14191,15 @@ snapshots:
'@azure/core-http-compat@1.3.0':
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/core-client': 1.6.1
'@azure/core-client': 1.9.2
'@azure/core-rest-pipeline': 1.9.2
transitivePeerDependencies:
- supports-color
'@azure/core-http-compat@2.1.2':
dependencies:
'@azure/abort-controller': 2.0.0
'@azure/core-client': 1.9.2
'@azure/core-rest-pipeline': 1.9.2
transitivePeerDependencies:
- supports-color
@ -14229,7 +14267,7 @@ snapshots:
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/core-auth': 1.6.0
'@azure/core-client': 1.6.1
'@azure/core-client': 1.9.2
'@azure/core-rest-pipeline': 1.9.2
'@azure/core-tracing': 1.0.1
'@azure/core-util': 1.7.0
@ -14244,11 +14282,30 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@azure/identity@4.3.0':
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/core-auth': 1.6.0
'@azure/core-client': 1.9.2
'@azure/core-rest-pipeline': 1.9.2
'@azure/core-tracing': 1.0.1
'@azure/core-util': 1.7.0
'@azure/logger': 1.0.3
'@azure/msal-browser': 3.19.0
'@azure/msal-node': 2.11.0
events: 3.3.0
jws: 4.0.0
open: 8.4.0
stoppable: 1.1.0
tslib: 2.6.2
transitivePeerDependencies:
- supports-color
'@azure/keyvault-keys@4.6.0':
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/core-auth': 1.6.0
'@azure/core-client': 1.6.1
'@azure/core-client': 1.9.2
'@azure/core-http-compat': 1.3.0
'@azure/core-lro': 2.4.0
'@azure/core-paging': 1.3.0
@ -14260,6 +14317,22 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@azure/keyvault-secrets@4.8.0':
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/core-auth': 1.6.0
'@azure/core-client': 1.9.2
'@azure/core-http-compat': 2.1.2
'@azure/core-lro': 2.4.0
'@azure/core-paging': 1.3.0
'@azure/core-rest-pipeline': 1.9.2
'@azure/core-tracing': 1.0.1
'@azure/core-util': 1.7.0
'@azure/logger': 1.0.3
tslib: 2.6.2
transitivePeerDependencies:
- supports-color
'@azure/logger@1.0.3':
dependencies:
tslib: 2.6.2
@ -14268,8 +14341,20 @@ snapshots:
dependencies:
'@azure/msal-common': 14.7.1
'@azure/msal-browser@3.19.0':
dependencies:
'@azure/msal-common': 14.13.0
'@azure/msal-common@14.13.0': {}
'@azure/msal-common@14.7.1': {}
'@azure/msal-node@2.11.0':
dependencies:
'@azure/msal-common': 14.13.0
jsonwebtoken: 9.0.2
uuid: 8.3.2
'@azure/msal-node@2.6.4':
dependencies:
'@azure/msal-common': 14.7.1
@ -14320,7 +14405,7 @@ snapshots:
'@babel/traverse': 7.24.0
'@babel/types': 7.24.0
convert-source-map: 2.0.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 7.6.0
@ -14441,7 +14526,7 @@ snapshots:
'@babel/core': 7.24.6
'@babel/helper-compilation-targets': 7.24.6
'@babel/helper-plugin-utils': 7.24.6
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
lodash.debounce: 4.0.8
resolve: 1.22.8
transitivePeerDependencies:
@ -14452,7 +14537,7 @@ snapshots:
'@babel/core': 7.24.6
'@babel/helper-compilation-targets': 7.24.6
'@babel/helper-plugin-utils': 7.24.6
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
lodash.debounce: 4.0.8
resolve: 1.22.8
transitivePeerDependencies:
@ -15336,7 +15421,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.24.0
'@babel/types': 7.24.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -15351,7 +15436,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.24.6
'@babel/parser': 7.24.6
'@babel/types': 7.24.6
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -18923,7 +19008,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 6.7.5
'@typescript-eslint/visitor-keys': 6.7.5
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
semver: 7.6.0
@ -19344,7 +19429,7 @@ snapshots:
agent-base@7.1.0:
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -20790,7 +20875,7 @@ snapshots:
detect-port@1.5.1:
dependencies:
address: 1.2.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -21168,7 +21253,7 @@ snapshots:
esbuild-register@3.5.0(esbuild@0.20.2):
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
esbuild: 0.20.2
transitivePeerDependencies:
- supports-color
@ -22405,7 +22490,7 @@ snapshots:
dependencies:
'@tootallnate/once': 1.1.2
agent-base: 6.0.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
optional: true
@ -22414,7 +22499,7 @@ snapshots:
dependencies:
'@tootallnate/once': 2.0.0
agent-base: 6.0.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -22823,7 +22908,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@ -26241,7 +26326,7 @@ snapshots:
socks-proxy-agent@6.2.1:
dependencies:
agent-base: 6.0.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.5(supports-color@8.1.1)
socks: 2.7.1
transitivePeerDependencies:
- supports-color