mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-21 04:37:50 +08:00
feat(core): Use custom agent to handle http(s) proxies (#21264)
This commit is contained in:
@@ -178,6 +178,7 @@
|
||||
"sshpk": "1.18.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"syslog-client": "1.1.1",
|
||||
"undici": "^7.16.0",
|
||||
"uuid": "catalog:",
|
||||
"validator": "13.7.0",
|
||||
"ws": "8.17.1",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OidcConfigDto } from '@n8n/api-types';
|
||||
import type { Logger } from '@n8n/backend-common';
|
||||
import { mockInstance, mockLogger } from '@n8n/backend-test-utils';
|
||||
import type { GlobalConfig } from '@n8n/config';
|
||||
@@ -5,20 +6,24 @@ import type { AuthIdentityRepository, SettingsRepository, UserRepository } from
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { Cipher, InstanceSettings } from 'n8n-core';
|
||||
|
||||
import * as client from 'openid-client';
|
||||
|
||||
import type { JwtService } from '@/services/jwt.service';
|
||||
import type { UrlService } from '@/services/url.service';
|
||||
import { EnvHttpProxyAgent } from 'undici';
|
||||
|
||||
import * as ssoHelpers from '../../sso-helpers';
|
||||
import { OIDC_PREFERENCES_DB_KEY } from '../constants';
|
||||
import { OidcService } from '../oidc.service.ee';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import type { OidcConfigDto } from '@n8n/api-types';
|
||||
import { type ProvisioningService } from '@/modules/provisioning.ee/provisioning.service.ee';
|
||||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
import { type ProvisioningService } from '@/modules/provisioning.ee/provisioning.service.ee';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import type { JwtService } from '@/services/jwt.service';
|
||||
import type { UrlService } from '@/services/url.service';
|
||||
|
||||
jest.mock('undici', () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
EnvHttpProxyAgent: jest.fn().mockImplementation(() => ({})),
|
||||
}));
|
||||
|
||||
describe('OidcService', () => {
|
||||
let oidcService: OidcService;
|
||||
@@ -537,4 +542,86 @@ describe('OidcService', () => {
|
||||
expect(user.email).toEqual('john.doe@test.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('proxy configuration', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
// Helper function to create a proper mock Response
|
||||
const createMockResponse = () => {
|
||||
const mockData = {
|
||||
issuer: 'https://example.com',
|
||||
authorization_endpoint: 'https://example.com/auth',
|
||||
token_endpoint: 'https://example.com/token',
|
||||
userinfo_endpoint: 'https://example.com/userinfo',
|
||||
jwks_uri: 'https://example.com/jwks',
|
||||
};
|
||||
return new Response(JSON.stringify(mockData), {
|
||||
status: 200,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset environment before each test
|
||||
process.env = { ...originalEnv };
|
||||
// Reset the mock between tests
|
||||
(EnvHttpProxyAgent as unknown as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment after each test
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ envVar: 'HTTP_PROXY', value: 'http://proxy.example.com:8080' },
|
||||
{ envVar: 'HTTPS_PROXY', value: 'https://proxy.example.com:8443' },
|
||||
{ envVar: 'ALL_PROXY', value: 'http://all-proxy.example.com:8888' },
|
||||
])('should instantiate EnvHttpProxyAgent when $envVar is set', async ({ envVar, value }) => {
|
||||
// Set proxy environment variable
|
||||
process.env[envVar] = value;
|
||||
|
||||
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
|
||||
const clientId = 'test-client';
|
||||
const clientSecret = 'test-secret';
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue(createMockResponse());
|
||||
|
||||
// Call the private method directly using type assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
await (oidcService as any).createProxyAwareConfiguration(
|
||||
discoveryUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
);
|
||||
|
||||
// Verify EnvHttpProxyAgent was instantiated
|
||||
expect(EnvHttpProxyAgent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not instantiate EnvHttpProxyAgent when no proxy env vars are set', async () => {
|
||||
// Ensure no proxy env vars are set
|
||||
delete process.env.HTTP_PROXY;
|
||||
delete process.env.HTTPS_PROXY;
|
||||
delete process.env.ALL_PROXY;
|
||||
|
||||
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
|
||||
const clientId = 'test-client';
|
||||
const clientSecret = 'test-secret';
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue(createMockResponse());
|
||||
|
||||
// Call the private method directly
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
await (oidcService as any).createProxyAwareConfiguration(
|
||||
discoveryUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
);
|
||||
|
||||
// Should not instantiate EnvHttpProxyAgent when no proxy is configured
|
||||
expect(EnvHttpProxyAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,17 +10,13 @@ import {
|
||||
type User,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import { OnPubSubEvent } from '@n8n/decorators';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { Cipher, InstanceSettings } from 'n8n-core';
|
||||
import { jsonParse, UserError } from 'n8n-workflow';
|
||||
import * as client from 'openid-client';
|
||||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { EnvHttpProxyAgent } from 'undici';
|
||||
|
||||
import {
|
||||
getCurrentAuthenticationMethod,
|
||||
@@ -30,8 +26,13 @@ import {
|
||||
setCurrentAuthenticationMethod,
|
||||
} from '../sso-helpers';
|
||||
import { OIDC_CLIENT_SECRET_REDACTED_VALUE, OIDC_PREFERENCES_DB_KEY } from './constants';
|
||||
import { OnPubSubEvent } from '@n8n/decorators';
|
||||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { ProvisioningService } from '@/modules/provisioning.ee/provisioning.service.ee';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
|
||||
const DEFAULT_OIDC_CONFIG: OidcConfigDto = {
|
||||
clientId: '',
|
||||
@@ -422,7 +423,7 @@ export class OidcService {
|
||||
newConfig.clientSecret = this.oidcConfig.clientSecret;
|
||||
}
|
||||
try {
|
||||
const discoveredMetadata = await client.discovery(
|
||||
const discoveredMetadata = await this.createProxyAwareConfiguration(
|
||||
discoveryEndpoint,
|
||||
newConfig.clientId,
|
||||
newConfig.clientSecret,
|
||||
@@ -484,6 +485,46 @@ export class OidcService {
|
||||
} & OidcRuntimeConfig)
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* Creates a proxy-aware configuration for openid-client.
|
||||
* This method configures customFetch to respect HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables.
|
||||
*/
|
||||
private async createProxyAwareConfiguration(
|
||||
discoveryUrl: URL,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
): Promise<client.Configuration> {
|
||||
const configuration = await client.discovery(discoveryUrl, clientId, clientSecret);
|
||||
|
||||
// Check if proxy environment variables are set
|
||||
const hasProxyConfig =
|
||||
process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.ALL_PROXY;
|
||||
|
||||
if (hasProxyConfig) {
|
||||
this.logger.debug('Configuring OIDC client with proxy support', {
|
||||
HTTP_PROXY: process.env.HTTP_PROXY,
|
||||
HTTPS_PROXY: process.env.HTTPS_PROXY,
|
||||
NO_PROXY: process.env.NO_PROXY,
|
||||
ALL_PROXY: process.env.ALL_PROXY,
|
||||
});
|
||||
|
||||
// Create a proxy agent that automatically reads from environment variables
|
||||
const proxyAgent = new EnvHttpProxyAgent();
|
||||
|
||||
// Configure customFetch to use the proxy agent
|
||||
configuration[client.customFetch] = async (...args) => {
|
||||
const [url, options] = args;
|
||||
return await fetch(url, {
|
||||
...options,
|
||||
// @ts-expect-error - dispatcher is an undici-specific option not in standard fetch
|
||||
dispatcher: proxyAgent,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private async getOidcConfiguration(): Promise<client.Configuration> {
|
||||
const now = Date.now();
|
||||
if (
|
||||
@@ -496,7 +537,7 @@ export class OidcService {
|
||||
) {
|
||||
this.cachedOidcConfiguration = {
|
||||
...this.oidcConfig,
|
||||
configuration: client.discovery(
|
||||
configuration: this.createProxyAwareConfiguration(
|
||||
this.oidcConfig.discoveryEndpoint,
|
||||
this.oidcConfig.clientId,
|
||||
this.oidcConfig.clientSecret,
|
||||
|
||||
@@ -2,15 +2,21 @@ import type { SamlPreferences } from '@n8n/api-types';
|
||||
import { mockInstance, mockLogger } from '@n8n/backend-test-utils';
|
||||
import type { GlobalConfig } from '@n8n/config';
|
||||
import { SettingsRepository } from '@n8n/db';
|
||||
import type { UserRepository } from '@n8n/db';
|
||||
import type { Settings } from '@n8n/db';
|
||||
import type { UserRepository, Settings } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import axios from 'axios';
|
||||
import type express from 'express';
|
||||
import type { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import type { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { InstanceSettings } from 'n8n-core';
|
||||
import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify';
|
||||
|
||||
import { SAML_PREFERENCES_DB_KEY } from '../constants';
|
||||
import { InvalidSamlMetadataUrlError } from '../errors/invalid-saml-metadata-url.error';
|
||||
import { InvalidSamlMetadataError } from '../errors/invalid-saml-metadata.error';
|
||||
import { SamlValidator } from '../saml-validator';
|
||||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import type { UrlService } from '@/services/url.service';
|
||||
@@ -18,11 +24,6 @@ import * as samlHelpers from '@/sso.ee/saml/saml-helpers';
|
||||
import { SamlService } from '@/sso.ee/saml/saml.service.ee';
|
||||
import * as ssoHelpers from '@/sso.ee/sso-helpers';
|
||||
|
||||
import { SAML_PREFERENCES_DB_KEY } from '../constants';
|
||||
import { InvalidSamlMetadataUrlError } from '../errors/invalid-saml-metadata-url.error';
|
||||
import { InvalidSamlMetadataError } from '../errors/invalid-saml-metadata.error';
|
||||
import { SamlValidator } from '../saml-validator';
|
||||
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
@@ -649,4 +650,202 @@ describe('SamlService', () => {
|
||||
expect(settingsRepository.delete).toHaveBeenCalledWith({ key: SAML_PREFERENCES_DB_KEY });
|
||||
});
|
||||
});
|
||||
|
||||
describe('proxy configuration', () => {
|
||||
const originalEnv = process.env;
|
||||
const validMetadataXml =
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://saml.example.com/entityid" validUntil="2035-05-07T13:33:47.181Z">\n <md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">\n <md:KeyDescriptor use="signing">\n <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">\n <ds:X509Data>\n <ds:X509Certificate>MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV\nSzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4\nMjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK\nDAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0\nRuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd\n4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V\npwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b\n2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ\nNfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF\nAAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW\n5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4\nkhuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX\nUjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L\nr/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M\nm0eo2USlSRTVl7QHRTuiuSThHpLKQQ==</ds:X509Certificate>\n </ds:X509Data>\n </ds:KeyInfo>\n </md:KeyDescriptor>\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>\n <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://mocksaml.com/api/saml/sso"/>\n <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://mocksaml.com/api/saml/sso"/>\n </md:IDPSSODescriptor>\n</md:EntityDescriptor>';
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset environment before each test
|
||||
process.env = { ...originalEnv };
|
||||
jest.restoreAllMocks();
|
||||
jest.spyOn(samlService, 'loadSamlify').mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment after each test
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
test('should use proxy when HTTP_PROXY environment variable is set for HTTP URLs', async () => {
|
||||
// Set HTTP proxy environment variable
|
||||
const proxyUrl = 'http://proxy.example.com:8080';
|
||||
process.env.HTTP_PROXY = proxyUrl;
|
||||
|
||||
// Use an HTTP metadata URL
|
||||
const metadataUrl = 'http://saml.example.com/metadata';
|
||||
|
||||
// Mock the preferences to include a metadataUrl
|
||||
type SamlServicePrivate = { _samlPreferences: SamlPreferences };
|
||||
(samlService as unknown as SamlServicePrivate)._samlPreferences = {
|
||||
metadata: mockSamlConfig.metadata,
|
||||
metadataUrl,
|
||||
} as SamlPreferences;
|
||||
|
||||
// Mock axios response
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
status: 200,
|
||||
data: validMetadataXml,
|
||||
});
|
||||
|
||||
// Mock validator
|
||||
jest.spyOn(samlService['validator'], 'validateMetadata').mockResolvedValue(true);
|
||||
|
||||
const result = await samlService.fetchMetadataFromUrl();
|
||||
|
||||
expect(result).toBe(validMetadataXml);
|
||||
|
||||
// Verify that axios.get was called with the correct URL and agents
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
metadataUrl,
|
||||
expect.objectContaining({
|
||||
httpAgent: expect.any(Object),
|
||||
httpsAgent: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
|
||||
// Get the actual call arguments to verify the agents
|
||||
const callArgs = mockedAxios.get.mock.calls[0];
|
||||
if (!callArgs?.[1]) {
|
||||
throw new Error('Expected axios.get to be called with arguments');
|
||||
}
|
||||
|
||||
const { httpAgent, httpsAgent } = callArgs[1];
|
||||
|
||||
// Verify that both agents are created
|
||||
expect(httpAgent).toBeDefined();
|
||||
expect(httpsAgent).toBeDefined();
|
||||
|
||||
// Verify the httpAgent has proxy configuration
|
||||
expect(httpAgent).toHaveProperty('proxy');
|
||||
const httpProxyAgent = httpAgent as unknown as HttpProxyAgent<string>;
|
||||
expect(httpProxyAgent.proxy).toBeDefined();
|
||||
expect(httpProxyAgent.proxy.href).toBe(`${proxyUrl}/`);
|
||||
|
||||
// The httpsAgent should also have the proxy (both are created with same proxy)
|
||||
expect(httpsAgent).toHaveProperty('proxy');
|
||||
const httpsProxyAgent = httpsAgent as unknown as HttpsProxyAgent<string>;
|
||||
expect(httpsProxyAgent.proxy.href).toBe(`${proxyUrl}/`);
|
||||
});
|
||||
|
||||
test('should use proxy when HTTPS_PROXY environment variable is set for HTTPS URLs', async () => {
|
||||
// Set HTTPS proxy environment variable
|
||||
const proxyUrl = 'https://proxy.example.com:8080';
|
||||
process.env.HTTPS_PROXY = proxyUrl;
|
||||
|
||||
// Use an HTTPS metadata URL
|
||||
const metadataUrl = 'https://saml.example.com/metadata';
|
||||
|
||||
// Mock the preferences to include a metadataUrl
|
||||
type SamlServicePrivate = { _samlPreferences: SamlPreferences };
|
||||
(samlService as unknown as SamlServicePrivate)._samlPreferences = {
|
||||
metadata: mockSamlConfig.metadata,
|
||||
metadataUrl,
|
||||
ignoreSSL: true,
|
||||
} as SamlPreferences;
|
||||
|
||||
// Mock axios response
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
status: 200,
|
||||
data: validMetadataXml,
|
||||
});
|
||||
|
||||
// Mock validator
|
||||
jest.spyOn(samlService['validator'], 'validateMetadata').mockResolvedValue(true);
|
||||
|
||||
const result = await samlService.fetchMetadataFromUrl();
|
||||
|
||||
expect(result).toBe(validMetadataXml);
|
||||
|
||||
// Verify that axios.get was called with the correct URL and agents
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
metadataUrl,
|
||||
expect.objectContaining({
|
||||
httpAgent: expect.any(Object),
|
||||
httpsAgent: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
|
||||
// Get the actual call arguments to verify the agents
|
||||
const callArgs = mockedAxios.get.mock.calls[0];
|
||||
if (!callArgs?.[1]) {
|
||||
throw new Error('Expected axios.get to be called with arguments');
|
||||
}
|
||||
|
||||
const { httpAgent, httpsAgent } = callArgs[1];
|
||||
|
||||
// Verify that both agents are created
|
||||
expect(httpAgent).toBeDefined();
|
||||
expect(httpsAgent).toBeDefined();
|
||||
|
||||
// Verify the httpsAgent has proxy configuration
|
||||
expect(httpsAgent).toHaveProperty('proxy');
|
||||
const httpsProxyAgent = httpsAgent as unknown as HttpsProxyAgent<string>;
|
||||
expect(httpsProxyAgent.proxy).toBeDefined();
|
||||
expect(httpsProxyAgent.proxy.href).toBe(`${proxyUrl}/`);
|
||||
|
||||
// Verify that the httpsAgent has the correct rejectUnauthorized setting when ignoreSSL is true
|
||||
// HttpsProxyAgent stores connection options in connectOpts, not options
|
||||
const httpsAgentWithConnectOpts = httpsProxyAgent as unknown as {
|
||||
connectOpts?: { rejectUnauthorized?: boolean };
|
||||
};
|
||||
expect(httpsAgentWithConnectOpts.connectOpts?.rejectUnauthorized).toBe(false);
|
||||
|
||||
// The httpAgent should also have the proxy (both are created with same proxy)
|
||||
expect(httpAgent).toHaveProperty('proxy');
|
||||
const httpProxyAgent = httpAgent as unknown as HttpProxyAgent<string>;
|
||||
expect(httpProxyAgent.proxy.href).toBe(`${proxyUrl}/`);
|
||||
});
|
||||
|
||||
test('should work without proxy when no proxy environment variables are set', async () => {
|
||||
// Ensure no proxy env vars are set
|
||||
delete process.env.HTTP_PROXY;
|
||||
delete process.env.HTTPS_PROXY;
|
||||
delete process.env.ALL_PROXY;
|
||||
|
||||
// Mock the preferences to include a metadataUrl
|
||||
type SamlServicePrivate = { _samlPreferences: SamlPreferences };
|
||||
(samlService as unknown as SamlServicePrivate)._samlPreferences = {
|
||||
metadata: mockSamlConfig.metadata,
|
||||
metadataUrl: 'https://saml.example.com/metadata',
|
||||
ignoreSSL: false,
|
||||
} as SamlPreferences;
|
||||
|
||||
// Mock axios response
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
status: 200,
|
||||
data: validMetadataXml,
|
||||
});
|
||||
|
||||
// Mock validator
|
||||
jest.spyOn(samlService['validator'], 'validateMetadata').mockResolvedValue(true);
|
||||
|
||||
const result = await samlService.fetchMetadataFromUrl();
|
||||
|
||||
expect(result).toBe(validMetadataXml);
|
||||
|
||||
// Verify that axios.get was called with agents
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
'https://saml.example.com/metadata',
|
||||
expect.objectContaining({
|
||||
httpAgent: expect.any(Object),
|
||||
httpsAgent: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
|
||||
// Get the actual call arguments to verify the agents are NOT proxy agents
|
||||
const callArgs = mockedAxios.get.mock.calls[0];
|
||||
if (!callArgs?.[1]) {
|
||||
throw new Error('Expected axios.get to be called with arguments');
|
||||
}
|
||||
|
||||
const { httpAgent, httpsAgent } = callArgs[1];
|
||||
|
||||
// When no proxy is configured, regular http/https Agents are created
|
||||
// These should NOT have a proxy property
|
||||
expect(httpAgent).not.toHaveProperty('proxy');
|
||||
expect(httpsAgent).not.toHaveProperty('proxy');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,16 +7,11 @@ import { OnPubSubEvent } from '@n8n/decorators';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import axios from 'axios';
|
||||
import type express from 'express';
|
||||
import https from 'https';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
import { createHttpProxyAgent, createHttpsProxyAgent, InstanceSettings } from 'n8n-core';
|
||||
import { jsonParse, UnexpectedError } from 'n8n-workflow';
|
||||
import { type IdentityProviderInstance, type ServiceProviderInstance } from 'samlify';
|
||||
import type { BindingContext, PostBindingContext } from 'samlify/types/src/entity';
|
||||
|
||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
|
||||
import { SAML_PREFERENCES_DB_KEY } from './constants';
|
||||
import { InvalidSamlMetadataUrlError } from './errors/invalid-saml-metadata-url.error';
|
||||
import { InvalidSamlMetadataError } from './errors/invalid-saml-metadata.error';
|
||||
@@ -34,7 +29,11 @@ import { SamlValidator } from './saml-validator';
|
||||
import { getServiceProviderInstance } from './service-provider.ee';
|
||||
import type { SamlLoginBinding, SamlUserAttributes } from './types';
|
||||
import { isSsoJustInTimeProvisioningEnabled, reloadAuthenticationMethod } from '../sso-helpers';
|
||||
|
||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { PROVISIONING_PREFERENCES_DB_KEY } from '@/modules/provisioning.ee/constants';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
|
||||
@Service()
|
||||
export class SamlService {
|
||||
@@ -444,11 +443,21 @@ export class SamlService {
|
||||
if (!this._samlPreferences.metadataUrl)
|
||||
throw new BadRequestError('Error fetching SAML Metadata, no Metadata URL set');
|
||||
try {
|
||||
// TODO:SAML: this will not work once axios is upgraded to > 1.2.0 (see checkServerIdentity)
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: !this._samlPreferences.ignoreSSL,
|
||||
// Create a proxy-aware HTTPS agent that respects HTTP_PROXY, HTTPS_PROXY, and NO_PROXY
|
||||
// environment variables while also supporting SSL certificate validation options
|
||||
const httpsAgent = createHttpsProxyAgent(
|
||||
null, // Uses proxy from environment variables
|
||||
this._samlPreferences.metadataUrl,
|
||||
{
|
||||
rejectUnauthorized: !this._samlPreferences.ignoreSSL,
|
||||
},
|
||||
);
|
||||
const httpAgent = createHttpProxyAgent(null, this._samlPreferences.metadataUrl);
|
||||
|
||||
const response = await axios.get(this._samlPreferences.metadataUrl, {
|
||||
httpsAgent,
|
||||
httpAgent,
|
||||
});
|
||||
const response = await axios.get(this._samlPreferences.metadataUrl, { httpsAgent: agent });
|
||||
if (response.status === 200 && response.data) {
|
||||
const xml = (await response.data) as string;
|
||||
const validationResult = await this.validator.validateMetadata(xml);
|
||||
|
||||
Generated
+43
-28
@@ -539,7 +539,7 @@ importers:
|
||||
version: 4.0.7
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
dotenv:
|
||||
specifier: 8.6.0
|
||||
version: 8.6.0
|
||||
@@ -564,7 +564,7 @@ importers:
|
||||
dependencies:
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
devDependencies:
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
@@ -1052,7 +1052,7 @@ importers:
|
||||
version: 4.3.0
|
||||
'@getzep/zep-cloud':
|
||||
specifier: 1.0.12
|
||||
version: 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(ca377405f102dc2905a39d54ecb4d621))
|
||||
version: 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(5cc28a029307bb3da1dcaf370c8a2b8d))
|
||||
'@getzep/zep-js':
|
||||
specifier: 0.9.0
|
||||
version: 0.9.0
|
||||
@@ -1079,7 +1079,7 @@ importers:
|
||||
version: 0.3.4(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)
|
||||
'@langchain/community':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.50(a14f1c0904e0abadfde97956245c3242)
|
||||
version: 0.3.50(a75c8af281af8c64873764a9c6c64007)
|
||||
'@langchain/core':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
@@ -1202,7 +1202,7 @@ importers:
|
||||
version: 23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
langchain:
|
||||
specifier: 0.3.33
|
||||
version: 0.3.33(ca377405f102dc2905a39d54ecb4d621)
|
||||
version: 0.3.33(5cc28a029307bb3da1dcaf370c8a2b8d)
|
||||
lodash:
|
||||
specifier: 'catalog:'
|
||||
version: 4.17.21
|
||||
@@ -1318,7 +1318,7 @@ importers:
|
||||
version: link:../eslint-plugin-community-nodes
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 9.29.0(jiti@2.6.1)
|
||||
@@ -1532,7 +1532,7 @@ importers:
|
||||
version: 1.11.0
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
bcryptjs:
|
||||
specifier: 2.4.3
|
||||
version: 2.4.3
|
||||
@@ -1722,6 +1722,9 @@ importers:
|
||||
syslog-client:
|
||||
specifier: 1.1.1
|
||||
version: 1.1.1
|
||||
undici:
|
||||
specifier: ^7.16.0
|
||||
version: 7.16.0
|
||||
uuid:
|
||||
specifier: 'catalog:'
|
||||
version: 10.0.0
|
||||
@@ -1881,7 +1884,7 @@ importers:
|
||||
version: 9.42.1
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
callsites:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0
|
||||
@@ -2351,7 +2354,7 @@ importers:
|
||||
version: link:../../../@n8n/utils
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
flatted:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.7
|
||||
@@ -2608,7 +2611,7 @@ importers:
|
||||
version: 1.1.4
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
bowser:
|
||||
specifier: 2.11.0
|
||||
version: 2.11.0
|
||||
@@ -20366,7 +20369,7 @@ snapshots:
|
||||
'@gar/promisify@1.1.3':
|
||||
optional: true
|
||||
|
||||
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(ca377405f102dc2905a39d54ecb4d621))':
|
||||
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(5cc28a029307bb3da1dcaf370c8a2b8d))':
|
||||
dependencies:
|
||||
form-data: 4.0.4
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
@@ -20375,7 +20378,7 @@ snapshots:
|
||||
zod: 3.25.67
|
||||
optionalDependencies:
|
||||
'@langchain/core': 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
langchain: 0.3.33(ca377405f102dc2905a39d54ecb4d621)
|
||||
langchain: 0.3.33(5cc28a029307bb3da1dcaf370c8a2b8d)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
@@ -21067,7 +21070,7 @@ snapshots:
|
||||
- aws-crt
|
||||
- encoding
|
||||
|
||||
'@langchain/community@0.3.50(a14f1c0904e0abadfde97956245c3242)':
|
||||
'@langchain/community@0.3.50(a75c8af281af8c64873764a9c6c64007)':
|
||||
dependencies:
|
||||
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.56.0)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@16.6.1)(encoding@0.1.13)(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))(utf-8-validate@5.0.10)(zod@3.25.67)
|
||||
'@ibm-cloud/watsonx-ai': 1.1.2
|
||||
@@ -21079,7 +21082,7 @@ snapshots:
|
||||
flat: 5.0.2
|
||||
ibm-cloud-sdk-core: 5.3.2
|
||||
js-yaml: 4.1.0
|
||||
langchain: 0.3.33(ca377405f102dc2905a39d54ecb4d621)
|
||||
langchain: 0.3.33(5cc28a029307bb3da1dcaf370c8a2b8d)
|
||||
langsmith: 0.3.55(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
openai: 5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)
|
||||
uuid: 10.0.0
|
||||
@@ -21093,7 +21096,7 @@ snapshots:
|
||||
'@aws-sdk/credential-provider-node': 3.808.0
|
||||
'@azure/storage-blob': 12.26.0
|
||||
'@browserbasehq/sdk': 2.6.0(encoding@0.1.13)
|
||||
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(ca377405f102dc2905a39d54ecb4d621))
|
||||
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(5cc28a029307bb3da1dcaf370c8a2b8d))
|
||||
'@getzep/zep-js': 0.9.0
|
||||
'@google-ai/generativelanguage': 3.4.0(encoding@0.1.13)
|
||||
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
||||
@@ -22353,7 +22356,7 @@ snapshots:
|
||||
|
||||
'@rudderstack/rudder-sdk-node@2.1.4(tslib@2.8.1)':
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios-retry: 4.5.0(axios@1.12.0)
|
||||
component-type: 2.0.0
|
||||
join-component: 1.1.0
|
||||
@@ -24937,7 +24940,7 @@ snapshots:
|
||||
|
||||
axios-retry@4.5.0(axios@1.12.0):
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
is-retry-allowed: 2.2.0
|
||||
|
||||
axios@1.12.0(debug@4.3.6):
|
||||
@@ -24956,6 +24959,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axios@1.12.0(debug@4.4.3):
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11(debug@4.4.3)
|
||||
form-data: 4.0.4
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
b4a@1.6.7: {}
|
||||
|
||||
babel-jest@29.6.2(@babel/core@7.26.10):
|
||||
@@ -25331,7 +25342,7 @@ snapshots:
|
||||
|
||||
bundlemon@3.1.0(typescript@5.9.2):
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios-retry: 4.5.0(axios@1.12.0)
|
||||
brotli-size: 4.0.0
|
||||
bundlemon-utils: 2.0.1
|
||||
@@ -27717,6 +27728,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
debug: 4.4.1(supports-color@8.1.1)
|
||||
|
||||
follow-redirects@1.15.11(debug@4.4.3):
|
||||
optionalDependencies:
|
||||
debug: 4.4.3
|
||||
|
||||
for-each@0.3.5:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
@@ -28441,7 +28456,7 @@ snapshots:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 20.19.21
|
||||
'@types/tough-cookie': 4.0.5
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
camelcase: 6.3.0
|
||||
debug: 4.4.3
|
||||
dotenv: 16.6.1
|
||||
@@ -28451,7 +28466,7 @@ snapshots:
|
||||
isstream: 0.1.2
|
||||
jsonwebtoken: 9.0.2
|
||||
mime-types: 2.1.35
|
||||
retry-axios: 2.6.0(axios@1.12.0(debug@4.4.3))
|
||||
retry-axios: 2.6.0(axios@1.12.0)
|
||||
tough-cookie: 4.1.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -28529,7 +28544,7 @@ snapshots:
|
||||
|
||||
infisical-node@1.3.0:
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
dotenv: 16.3.1
|
||||
tweetnacl: 1.0.3
|
||||
tweetnacl-util: 0.15.1
|
||||
@@ -29907,7 +29922,7 @@ snapshots:
|
||||
|
||||
kuler@2.0.0: {}
|
||||
|
||||
langchain@0.3.33(ca377405f102dc2905a39d54ecb4d621):
|
||||
langchain@0.3.33(5cc28a029307bb3da1dcaf370c8a2b8d):
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
'@langchain/openai': 0.6.16(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
|
||||
@@ -29930,7 +29945,7 @@ snapshots:
|
||||
'@langchain/groq': 0.2.3(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)
|
||||
'@langchain/mistralai': 0.2.3(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))
|
||||
'@langchain/ollama': 0.2.3(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
cheerio: 1.0.0
|
||||
handlebars: 4.7.8
|
||||
transitivePeerDependencies:
|
||||
@@ -32074,7 +32089,7 @@ snapshots:
|
||||
|
||||
posthog-node@3.2.1:
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
rusha: 0.8.14
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -32779,9 +32794,9 @@ snapshots:
|
||||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
retry-axios@2.6.0(axios@1.12.0(debug@4.4.3)):
|
||||
retry-axios@2.6.0(axios@1.12.0):
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
|
||||
retry-request@7.0.2(encoding@0.1.13):
|
||||
dependencies:
|
||||
@@ -33388,7 +33403,7 @@ snapshots:
|
||||
asn1.js: 5.4.1
|
||||
asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1)
|
||||
asn1.js-rfc5280: 3.0.0
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
big-integer: 1.6.52
|
||||
bignumber.js: 9.1.2
|
||||
binascii: 0.0.2
|
||||
@@ -34914,7 +34929,7 @@ snapshots:
|
||||
vite-node@3.1.3(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.89.2)(terser@5.16.1)(tsx@4.19.3):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.1(supports-color@8.1.1)
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 6.3.5(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.89.2)(terser@5.16.1)(tsx@4.19.3)
|
||||
|
||||
Reference in New Issue
Block a user