mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-13 00:20:27 +02:00
Compare commits
17 Commits
master
...
n8n@2.20.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29d42560ad | ||
|
|
f259afa978 | ||
|
|
6707c67f50 | ||
|
|
4d9a9f8079 | ||
|
|
ce016859cb | ||
|
|
8b1103bfa6 | ||
|
|
3bcb6378c5 | ||
|
|
c9b1463c58 | ||
|
|
ae115d199f | ||
|
|
2f31aca2dc | ||
|
|
71d4122438 | ||
|
|
98004c6269 | ||
|
|
ad6e890d85 | ||
|
|
513f7cd3dc | ||
|
|
2c19035590 | ||
|
|
9dd7ce9486 | ||
|
|
642fc2e18e |
4
.github/actions/setup-nodejs/action.yml
vendored
4
.github/actions/setup-nodejs/action.yml
vendored
|
|
@ -54,6 +54,10 @@ runs:
|
|||
echo "${EXPECTED_SHA256} install-safe-chain.sh" | sha256sum -c -
|
||||
sh install-safe-chain.sh --ci
|
||||
rm install-safe-chain.sh
|
||||
# Exclude first-party @n8n/* packages from SafeChain's minimum-package-age
|
||||
# filter so freshly-published versions stay visible to every subsequent
|
||||
# step in the job (install, build, and publish).
|
||||
echo "SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS=@n8n/*,n8n,n8n-containers,n8n-core,n8n-editor-ui,n8n-node-dev,n8n-nodes-base,n8n-playwright,n8n-workflow" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
|
||||
- name: Install Dependencies
|
||||
|
|
|
|||
2
.github/workflows/release-publish.yml
vendored
2
.github/workflows/release-publish.yml
vendored
|
|
@ -107,7 +107,7 @@ jobs:
|
|||
|
||||
build-daytona-snapshot:
|
||||
name: Build Daytona snapshot
|
||||
needs: [determine-version-info]
|
||||
needs: [determine-version-info, publish-to-npm]
|
||||
if: github.event.pull_request.merged == true
|
||||
uses: ./.github/workflows/release-build-daytona-snapshot.yml
|
||||
with:
|
||||
|
|
|
|||
28
CHANGELOG.md
28
CHANGELOG.md
|
|
@ -1,3 +1,31 @@
|
|||
## [2.20.6](https://github.com/n8n-io/n8n/compare/n8n@2.20.5...n8n@2.20.6) (2026-05-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Salesforce Node:** Fix trigger not firing on repeated record updates ([#30006](https://github.com/n8n-io/n8n/issues/30006)) ([f259afa](https://github.com/n8n-io/n8n/commit/f259afa978b72effcf194d40bfe78d5256fef7dc))
|
||||
|
||||
|
||||
## [2.20.5](https://github.com/n8n-io/n8n/compare/n8n@2.20.4...n8n@2.20.5) (2026-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Simple-git update broke https connection ([#30003](https://github.com/n8n-io/n8n/issues/30003)) ([ce01685](https://github.com/n8n-io/n8n/commit/ce016859cb055b7d193208412e5e656b0048a1fa))
|
||||
|
||||
|
||||
## [2.20.4](https://github.com/n8n-io/n8n/compare/n8n@2.20.0...n8n@2.20.4) (2026-05-07)
|
||||
|
||||
## [2.20.3](https://github.com/n8n-io/n8n/compare/n8n@2.20.0...n8n@2.20.3) (2026-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Add support for context establishment hooks in webhook mode ([#29900](https://github.com/n8n-io/n8n/issues/29900)) ([71d4122](https://github.com/n8n-io/n8n/commit/71d41224385e64098000569bf9ac4838a61c669c))
|
||||
* **core:** Allow GIT_SSH_COMMAND in simple-git after 3.36.0 upgrade ([#29946](https://github.com/n8n-io/n8n/issues/29946)) ([2f31aca](https://github.com/n8n-io/n8n/commit/2f31aca2dc4b5258492a678a44464146a2a29d01))
|
||||
* **Snowflake Node:** Fix issue with Insert and Update operations not working ([#29809](https://github.com/n8n-io/n8n/issues/29809)) ([98004c6](https://github.com/n8n-io/n8n/commit/98004c6269456c3bfe600da951856c81b3861034))
|
||||
|
||||
|
||||
# [2.20.0](https://github.com/n8n-io/n8n/compare/n8n@2.19.0...n8n@2.20.0) (2026-05-05)
|
||||
|
||||
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.6",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22.16",
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
"jest-mock-extended": "^3.0.4",
|
||||
"lefthook": "^1.7.15",
|
||||
"license-checker": "^25.0.1",
|
||||
"nock": "^14.0.13",
|
||||
"nock": "^14.0.14",
|
||||
"nodemon": "^3.0.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"p-limit": "^3.1.0",
|
||||
|
|
@ -103,7 +103,6 @@
|
|||
"@mistralai/mistralai": "^1.10.0",
|
||||
"@n8n/typeorm>@sentry/node": "catalog:sentry",
|
||||
"@types/node": "^20.17.50",
|
||||
"axios": "1.15.0",
|
||||
"chokidar": "4.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"expr-eval@2.0.2": "npm:expr-eval-fork@3.0.0",
|
||||
|
|
@ -140,7 +139,6 @@
|
|||
"undici@6": "^6.24.0",
|
||||
"undici@7": "^7.24.0",
|
||||
"tar": "^7.5.11",
|
||||
"hono": "4.12.14",
|
||||
"ajv@6": "6.14.0",
|
||||
"ajv@7": "8.18.0",
|
||||
"ajv@8": "8.18.0",
|
||||
|
|
@ -167,7 +165,9 @@
|
|||
"@xmldom/xmldom": "0.8.13",
|
||||
"langsmith": "0.5.19",
|
||||
"yaml@<=2.8.3": "2.8.3",
|
||||
"fast-xml-parser": "5.7.0"
|
||||
"hono": "4.12.16",
|
||||
"axios": "1.16.0",
|
||||
"fast-xml-parser": "5.7.2"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"bull@4.16.4": "patches/bull@4.16.4.patch",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/ai-node-sdk",
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.1",
|
||||
"description": "SDK for building AI nodes in n8n",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"module": "dist/esm/index.js",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/ai-utilities",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.1",
|
||||
"description": "Utilities for building AI nodes in n8n",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"module": "dist/esm/index.js",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/ai-workflow-builder",
|
||||
"version": "1.20.0",
|
||||
"version": "1.20.1",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/backend-common",
|
||||
"version": "1.20.0",
|
||||
"version": "1.20.1",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/backend-test-utils",
|
||||
"version": "1.20.0",
|
||||
"version": "1.20.2",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-benchmark",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.1",
|
||||
"description": "Cli for running benchmark tests for n8n",
|
||||
"main": "dist/index",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/client-oauth2",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/config",
|
||||
"version": "2.19.0",
|
||||
"version": "2.19.1",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
|||
|
|
@ -13,4 +13,8 @@ export class MultiMainSetupConfig {
|
|||
/** Interval in seconds between leader eligibility checks in multi-main setup. */
|
||||
@Env('N8N_MULTI_MAIN_SETUP_CHECK_INTERVAL')
|
||||
interval: number = 3;
|
||||
|
||||
/** Whether to use the new leader election implementation (Lua-script based). */
|
||||
@Env('N8N_NEW_LEADER_ELECTION_IMPLEMENTATION')
|
||||
newLeaderElection: boolean = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export { ChatTriggerConfig } from './configs/chat-trigger.config';
|
|||
export { InstanceAiConfig } from './configs/instance-ai.config';
|
||||
export { ExpressionEngineConfig } from './configs/expression-engine.config';
|
||||
export { PasswordConfig } from './configs/password.config';
|
||||
export { RedisConfig } from './configs/redis.config';
|
||||
|
||||
const protocolSchema = z.enum(['http', 'https']);
|
||||
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@ describe('GlobalConfig', () => {
|
|||
enabled: false,
|
||||
ttl: 10,
|
||||
interval: 3,
|
||||
newLeaderElection: false,
|
||||
},
|
||||
evaluation: {
|
||||
parallelExecutionEnabled: false,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/create-node",
|
||||
"version": "0.29.0",
|
||||
"version": "0.29.1",
|
||||
"description": "Official CLI to create new community nodes for n8n",
|
||||
"bin": {
|
||||
"create-node": "bin/create-node.cjs"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/db",
|
||||
"version": "1.20.0",
|
||||
"version": "1.20.2",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/node-cli",
|
||||
"version": "0.30.0",
|
||||
"version": "0.30.1",
|
||||
"description": "Official CLI for developing community nodes for n8n",
|
||||
"bin": {
|
||||
"n8n-node": "bin/n8n-node.mjs"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-nodes-langchain",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/scan-community-package",
|
||||
"version": "0.17.0",
|
||||
"version": "0.17.1",
|
||||
"description": "Static code analyser for n8n community packages",
|
||||
"license": "none",
|
||||
"bin": "scanner/cli.mjs",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/task-runner",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.2",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"start": "node dist/start.js",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.6",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"main": "dist/index",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { DbConnection, DeploymentKeyRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { BinaryDataConfig } from 'n8n-core';
|
||||
|
||||
import { DeprecationService } from '@/deprecation/deprecation.service';
|
||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { PubSubRegistry } from '@/scaling/pubsub/pubsub.registry';
|
||||
import { Subscriber } from '@/scaling/pubsub/subscriber.service';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import { RedisClientService } from '@/services/redis-client.service';
|
||||
import { WebhookServer } from '@/webhooks/webhook-server';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
import { Webhook } from '../webhook';
|
||||
|
||||
jest.mock('@/scaling/scaling.service', () => ({
|
||||
|
|
@ -26,7 +32,14 @@ mockInstance(RedisClientService);
|
|||
mockInstance(PubSubRegistry);
|
||||
const mockSubscriber = mockInstance(Subscriber);
|
||||
const mockWebhookServer = mockInstance(WebhookServer);
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const mockLoadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
||||
mockLoadNodesAndCredentials.postProcessLoaders.mockResolvedValue(undefined);
|
||||
|
||||
mockInstance(DeprecationService);
|
||||
mockInstance(JwtService, { initialize: jest.fn().mockResolvedValue(undefined) });
|
||||
mockInstance(BinaryDataConfig, { initialize: jest.fn().mockResolvedValue(undefined) });
|
||||
mockInstance(MessageEventBus, { initialize: jest.fn().mockResolvedValue(undefined) });
|
||||
mockInstance(LogStreamingEventRelay);
|
||||
|
||||
describe('Webhook', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -74,4 +87,57 @@ describe('Webhook', () => {
|
|||
expect(mockWebhookServer.markAsReady).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
let baseInitSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
baseInitSpy = jest.spyOn(BaseCommand.prototype, 'init').mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
baseInitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should call executionContextHookRegistry.init before LoadNodesAndCredentials.postProcessLoaders', async () => {
|
||||
const webhook = new Webhook();
|
||||
|
||||
// @ts-expect-error - Accessing protected property for testing
|
||||
webhook.globalConfig = { executions: { mode: 'queue' } };
|
||||
// @ts-expect-error - Accessing protected method for testing
|
||||
webhook.initCrashJournal = jest.fn().mockResolvedValue(undefined);
|
||||
webhook.initLicense = jest.fn().mockResolvedValue(undefined);
|
||||
// @ts-expect-error - Accessing protected method for testing
|
||||
webhook.initCommunityPackages = jest.fn().mockResolvedValue(undefined);
|
||||
webhook.initOrchestration = jest.fn().mockResolvedValue(undefined);
|
||||
webhook.initBinaryDataService = jest.fn().mockResolvedValue(undefined);
|
||||
// @ts-expect-error - Accessing protected method for testing
|
||||
webhook.initDataDeduplicationService = jest.fn().mockResolvedValue(undefined);
|
||||
webhook.initExternalHooks = jest.fn().mockResolvedValue(undefined);
|
||||
// @ts-expect-error - Accessing protected property for testing
|
||||
webhook.moduleRegistry = { initModules: jest.fn().mockResolvedValue(undefined) };
|
||||
// @ts-expect-error - Accessing protected property for testing
|
||||
webhook.instanceSettings = {
|
||||
hostId: 'test',
|
||||
instanceType: 'webhook',
|
||||
initialize: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
// @ts-expect-error - Accessing protected property for testing
|
||||
webhook.executionContextHookRegistry = {
|
||||
init: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await webhook.init();
|
||||
|
||||
// @ts-expect-error - Accessing protected property for testing
|
||||
const hookInitMock = webhook.executionContextHookRegistry.init as jest.Mock;
|
||||
const postProcessMock = mockLoadNodesAndCredentials.postProcessLoaders as jest.Mock;
|
||||
|
||||
expect(hookInitMock).toHaveBeenCalled();
|
||||
expect(postProcessMock).toHaveBeenCalled();
|
||||
expect(hookInitMock.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
postProcessMock.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ export class Webhook extends BaseCommand {
|
|||
Container.get(LogStreamingEventRelay).init();
|
||||
|
||||
await this.moduleRegistry.initModules(this.instanceSettings.instanceType);
|
||||
|
||||
await this.executionContextHookRegistry.init();
|
||||
await Container.get(LoadNodesAndCredentials).postProcessLoaders();
|
||||
}
|
||||
|
||||
async run() {
|
||||
|
|
|
|||
|
|
@ -1349,8 +1349,10 @@ function createWorkflowAdapterForTests(overrides?: {
|
|||
|
||||
const mockWorkflowService = {
|
||||
archive: jest.fn().mockResolvedValue(undefined),
|
||||
activateWorkflow: jest.fn().mockResolvedValue({ activeVersionId: 'version-1' }),
|
||||
update: jest.fn().mockResolvedValue(savedWorkflow),
|
||||
};
|
||||
const mockTelemetry = { track: jest.fn() };
|
||||
|
||||
const mockUser = { id: 'user-1', role: { slug: 'global:member' } } as unknown as User;
|
||||
|
||||
|
|
@ -1403,7 +1405,7 @@ function createWorkflowAdapterForTests(overrides?: {
|
|||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[26],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[27],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[28],
|
||||
{ track: jest.fn() } as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[29],
|
||||
mockTelemetry as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[29],
|
||||
mockAiBuilderTemporaryWorkflowRepository as unknown as AiBuilderTemporaryWorkflowRepository,
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[31],
|
||||
);
|
||||
|
|
@ -1420,6 +1422,7 @@ function createWorkflowAdapterForTests(overrides?: {
|
|||
mockSharedWorkflowRepository,
|
||||
mockAiBuilderTemporaryWorkflowRepository,
|
||||
mockWorkflowService,
|
||||
mockTelemetry,
|
||||
mockUser,
|
||||
};
|
||||
}
|
||||
|
|
@ -1477,6 +1480,18 @@ describe('createWorkflowAdapter', () => {
|
|||
).rejects.toThrow('User does not have the required permissions in this project');
|
||||
});
|
||||
|
||||
it('tracks workflow id when publishing a builder workflow', async () => {
|
||||
const { adapter, mockTelemetry } = createWorkflowAdapterForTests();
|
||||
|
||||
await adapter.publish('wf-new');
|
||||
|
||||
expect(mockTelemetry.track).toHaveBeenCalledWith('Builder published workflow', {
|
||||
thread_id: 'thread-1',
|
||||
workflow_id: 'wf-new',
|
||||
executed_by: 'ai',
|
||||
});
|
||||
});
|
||||
|
||||
it('marks the workflow as AI-builder temporary when markAsAiTemporary is true', async () => {
|
||||
const {
|
||||
adapter,
|
||||
|
|
@ -1928,7 +1943,15 @@ describe('resolveDataTableByIdOrName', () => {
|
|||
// createExecutionAdapter – run() forces save settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createRunAdapterForTests(workflow: Record<string, unknown>) {
|
||||
function createRunAdapterForTests(
|
||||
workflow: Record<string, unknown>,
|
||||
options?: {
|
||||
activeExecution?: boolean;
|
||||
execution?: ReturnType<typeof makeExecution>;
|
||||
postExecutePromise?: Promise<unknown>;
|
||||
threadId?: string;
|
||||
},
|
||||
) {
|
||||
const mockWorkflowFinderService = {
|
||||
findWorkflowForUser: jest.fn().mockResolvedValue(workflow),
|
||||
};
|
||||
|
|
@ -1938,12 +1961,17 @@ function createRunAdapterForTests(workflow: Record<string, unknown>) {
|
|||
};
|
||||
|
||||
const mockActiveExecutions = {
|
||||
has: jest.fn().mockReturnValue(false),
|
||||
getPostExecutePromise: jest
|
||||
.fn()
|
||||
.mockReturnValue(options?.postExecutePromise ?? Promise.resolve()),
|
||||
has: jest.fn().mockReturnValue(options?.activeExecution ?? false),
|
||||
stopExecution: jest.fn(),
|
||||
};
|
||||
|
||||
const mockExecutionRepository = {
|
||||
findSingleExecution: jest.fn().mockResolvedValue(undefined),
|
||||
findSingleExecution: jest.fn().mockResolvedValue(options?.execution),
|
||||
};
|
||||
const mockTelemetry = { track: jest.fn() };
|
||||
|
||||
const mockUser = { id: 'user-1', role: { slug: 'global:member' } } as unknown as User;
|
||||
|
||||
|
|
@ -1987,14 +2015,14 @@ function createRunAdapterForTests(workflow: Record<string, unknown>) {
|
|||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[26],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[27],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[28],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[29],
|
||||
mockTelemetry as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[29],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[30],
|
||||
{} as unknown as ConstructorParameters<typeof InstanceAiAdapterService>[31],
|
||||
);
|
||||
|
||||
const adapter = service.createContext(mockUser).executionService;
|
||||
const adapter = service.createContext(mockUser, { threadId: options?.threadId }).executionService;
|
||||
|
||||
return { adapter, mockWorkflowRunner };
|
||||
return { adapter, mockActiveExecutions, mockTelemetry, mockWorkflowRunner };
|
||||
}
|
||||
|
||||
describe('createExecutionAdapter run()', () => {
|
||||
|
|
@ -2041,4 +2069,78 @@ describe('createExecutionAdapter run()', () => {
|
|||
saveDataErrorExecution: 'all',
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks workflow id and success status when a builder execution finishes', async () => {
|
||||
const { adapter, mockTelemetry } = createRunAdapterForTests(
|
||||
{
|
||||
id: 'wf-1',
|
||||
nodes: [],
|
||||
},
|
||||
{
|
||||
execution: makeExecution({ status: 'success' }),
|
||||
threadId: 'thread-1',
|
||||
},
|
||||
);
|
||||
|
||||
await adapter.run('wf-1');
|
||||
|
||||
expect(mockTelemetry.track).toHaveBeenCalledWith('Builder executed workflow', {
|
||||
thread_id: 'thread-1',
|
||||
workflow_id: 'wf-1',
|
||||
executed_by: 'ai',
|
||||
pinned_node_count: 0,
|
||||
exec_type: 'manual',
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks error status when a builder execution fails', async () => {
|
||||
const { adapter, mockTelemetry } = createRunAdapterForTests(
|
||||
{
|
||||
id: 'wf-1',
|
||||
nodes: [],
|
||||
},
|
||||
{
|
||||
execution: makeExecution({ status: 'error', error: { message: 'boom' } }),
|
||||
threadId: 'thread-1',
|
||||
},
|
||||
);
|
||||
|
||||
await adapter.run('wf-1');
|
||||
|
||||
expect(mockTelemetry.track).toHaveBeenCalledWith(
|
||||
'Builder executed workflow',
|
||||
expect.objectContaining({
|
||||
workflow_id: 'wf-1',
|
||||
status: 'error',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('tracks timeout cancellation as an error status', async () => {
|
||||
const { adapter, mockActiveExecutions, mockTelemetry } = createRunAdapterForTests(
|
||||
{
|
||||
id: 'wf-1',
|
||||
nodes: [],
|
||||
},
|
||||
{
|
||||
activeExecution: true,
|
||||
postExecutePromise: new Promise(() => {}),
|
||||
threadId: 'thread-1',
|
||||
},
|
||||
);
|
||||
|
||||
await expect(adapter.run('wf-1', undefined, { timeout: 1 })).resolves.toMatchObject({
|
||||
status: 'error',
|
||||
});
|
||||
|
||||
expect(mockActiveExecutions.stopExecution).toHaveBeenCalled();
|
||||
expect(mockTelemetry.track).toHaveBeenCalledWith(
|
||||
'Builder executed workflow',
|
||||
expect.objectContaining({
|
||||
workflow_id: 'wf-1',
|
||||
status: 'error',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ export class InstanceAiAdapterService {
|
|||
if (threadId) {
|
||||
telemetry.track('Builder published workflow', {
|
||||
thread_id: threadId,
|
||||
workflow_id: workflowId,
|
||||
executed_by: 'ai',
|
||||
});
|
||||
}
|
||||
|
|
@ -807,6 +808,19 @@ export class InstanceAiAdapterService {
|
|||
runData.pinData = basePinData;
|
||||
}
|
||||
|
||||
const trackBuilderExecutedWorkflow = (status: ExecutionResult['status']) => {
|
||||
if (!threadId) return;
|
||||
|
||||
telemetry.track('Builder executed workflow', {
|
||||
thread_id: threadId,
|
||||
workflow_id: workflowId,
|
||||
executed_by: 'ai',
|
||||
pinned_node_count: Object.keys(runData.pinData ?? {}).length,
|
||||
exec_type: runData.executionMode,
|
||||
status,
|
||||
});
|
||||
};
|
||||
|
||||
const executionId = await workflowRunner.run(runData);
|
||||
|
||||
// Wait for completion with timeout protection
|
||||
|
|
@ -838,30 +852,25 @@ export class InstanceAiAdapterService {
|
|||
} catch {
|
||||
// Execution may have completed between timeout and cancel
|
||||
}
|
||||
return {
|
||||
const result = {
|
||||
executionId,
|
||||
status: 'error',
|
||||
error: `Execution timed out after ${timeoutMs}ms and was cancelled`,
|
||||
} satisfies ExecutionResult;
|
||||
trackBuilderExecutedWorkflow(result.status);
|
||||
return result;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (threadId) {
|
||||
telemetry.track('Builder executed workflow', {
|
||||
thread_id: threadId,
|
||||
executed_by: 'ai',
|
||||
pinned_node_count: Object.keys(runData.pinData ?? {}).length,
|
||||
exec_type: runData.executionMode,
|
||||
});
|
||||
}
|
||||
|
||||
return await extractExecutionResult(
|
||||
const result = await extractExecutionResult(
|
||||
executionRepository,
|
||||
executionId,
|
||||
allowSendingParameterValues,
|
||||
);
|
||||
trackBuilderExecutedWorkflow(result.status);
|
||||
return result;
|
||||
},
|
||||
|
||||
async getStatus(executionId: string) {
|
||||
|
|
|
|||
|
|
@ -329,6 +329,7 @@ describe('SourceControlGitService', () => {
|
|||
maxConcurrentProcesses: 6,
|
||||
trimmed: false,
|
||||
config: [`credential.helper=${expectedCredentialScript}`, 'credential.useHttpPath=true'],
|
||||
unsafe: { allowUnsafeCredentialHelper: true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -366,9 +367,15 @@ describe('SourceControlGitService', () => {
|
|||
mockSourceControlPreferencesService.getPreferences.mockReturnValue({
|
||||
connectionType: 'ssh',
|
||||
} as never);
|
||||
(simpleGit as jest.Mock).mockClear();
|
||||
|
||||
await sourceControlGitService.setGitCommand();
|
||||
|
||||
expect(simpleGit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
unsafe: { allowUnsafeSshCommand: true },
|
||||
}),
|
||||
);
|
||||
expect(mockGitInstance.env).toHaveBeenCalledWith(
|
||||
'GIT_SSH_COMMAND',
|
||||
'ssh -o UserKnownHostsFile=".ssh/known_hosts" -o StrictHostKeyChecking=accept-new -i "private-key"',
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ export class SourceControlGitService {
|
|||
// ensures that the credentials are only used for the configured repositoryUrl of the environment
|
||||
'credential.useHttpPath=true',
|
||||
],
|
||||
unsafe: { allowUnsafeCredentialHelper: true },
|
||||
};
|
||||
|
||||
// Add proxy configuration if proxy environment variables are set
|
||||
|
|
@ -171,7 +172,12 @@ export class SourceControlGitService {
|
|||
// - Subsequent connections: verifies against saved key
|
||||
const sshCommand = `ssh -o UserKnownHostsFile="${escapedKnownHostsPath}" -o StrictHostKeyChecking=accept-new -i "${escapedPrivateKeyPath}"`;
|
||||
|
||||
this.git = simpleGit(this.gitOptions)
|
||||
// Allow GIT_SSH_COMMAND so we can point SSH at n8n's own private key and known_hosts.
|
||||
// This is safe because the command is constructed internally above, not from user input.
|
||||
this.git = simpleGit({
|
||||
...this.gitOptions,
|
||||
unsafe: { allowUnsafeSshCommand: true },
|
||||
})
|
||||
.env('GIT_SSH_COMMAND', sshCommand)
|
||||
.env('GIT_TERMINAL_PROMPT', '0');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,110 +1,382 @@
|
|||
import type { LeaderElectionClient } from '@/scaling/leader-election-client';
|
||||
import type { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import type { RedisClientService } from '@/services/redis-client.service';
|
||||
import { mockLogger } from '@n8n/backend-test-utils';
|
||||
import type { GlobalConfig } from '@n8n/config';
|
||||
import { MultiMainMetadata } from '@n8n/decorators';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { ErrorReporter, InstanceSettings } from 'n8n-core';
|
||||
|
||||
import type { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import type { RedisClientService } from '@/services/redis-client.service';
|
||||
import { createResultOk, createResultError } from 'n8n-workflow';
|
||||
|
||||
import { MultiMainSetup } from '../multi-main-setup.ee';
|
||||
|
||||
function createInstanceSettings(hostId: string) {
|
||||
let isLeader = false;
|
||||
const settings = mock<InstanceSettings>({ hostId });
|
||||
Object.defineProperty(settings, 'isLeader', {
|
||||
get: () => isLeader,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(settings, 'markAsLeader', {
|
||||
value: jest.fn(() => {
|
||||
isLeader = true;
|
||||
}),
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(settings, 'markAsFollower', {
|
||||
value: jest.fn(() => {
|
||||
isLeader = false;
|
||||
}),
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
return settings;
|
||||
}
|
||||
|
||||
describe('MultiMainSetup', () => {
|
||||
const hostId = 'main-n8n-main-0';
|
||||
|
||||
const logger = mockLogger();
|
||||
const publisher = mock<Publisher>();
|
||||
const redisClientService = mock<RedisClientService>();
|
||||
const errorReporter = mock<ErrorReporter>();
|
||||
const metadata = new MultiMainMetadata();
|
||||
|
||||
const globalConfig = mock<GlobalConfig>({
|
||||
redis: { prefix: 'n8n' },
|
||||
multiMainSetup: { ttl: 10, interval: 3, enabled: true },
|
||||
describe('with legacy implementation (flag off)', () => {
|
||||
const logger = mockLogger();
|
||||
const publisher = mock<Publisher>();
|
||||
const redisClientService = mock<RedisClientService>();
|
||||
const errorReporter = mock<ErrorReporter>();
|
||||
|
||||
const globalConfig = mock<GlobalConfig>({
|
||||
redis: { prefix: 'n8n' },
|
||||
multiMainSetup: { ttl: 10, interval: 3, enabled: true, newLeaderElection: false },
|
||||
});
|
||||
|
||||
let instanceSettings: InstanceSettings;
|
||||
let multiMainSetup: MultiMainSetup;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
instanceSettings = createInstanceSettings(hostId);
|
||||
redisClientService.toValidPrefix.mockReturnValue('n8n');
|
||||
multiMainSetup = new MultiMainSetup(
|
||||
logger,
|
||||
instanceSettings,
|
||||
globalConfig,
|
||||
metadata,
|
||||
errorReporter,
|
||||
publisher,
|
||||
redisClientService,
|
||||
);
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should become leader if setIfNotExists succeeds', async () => {
|
||||
publisher.setIfNotExists.mockResolvedValue(true);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup.init();
|
||||
|
||||
expect(publisher.setIfNotExists).toHaveBeenCalled();
|
||||
expect(instanceSettings.markAsLeader).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
|
||||
it('should remain follower if setIfNotExists returns false', async () => {
|
||||
publisher.setIfNotExists.mockResolvedValue(false);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup.init();
|
||||
|
||||
expect(instanceSettings.markAsLeader).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkLeader', () => {
|
||||
it('should renew TTL when this instance is the leader', async () => {
|
||||
publisher.setIfNotExists.mockResolvedValue(true);
|
||||
await multiMainSetup.init();
|
||||
jest.clearAllMocks();
|
||||
|
||||
publisher.get.mockResolvedValue(hostId);
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(publisher.setExpiration).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit leader-takeover on mismatch recovery', async () => {
|
||||
publisher.setIfNotExists.mockResolvedValue(false);
|
||||
await multiMainSetup.init();
|
||||
jest.clearAllMocks();
|
||||
|
||||
publisher.get.mockResolvedValue(hostId);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsLeader).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
|
||||
it('should step down when another instance is leader', async () => {
|
||||
publisher.setIfNotExists.mockResolvedValue(true);
|
||||
await multiMainSetup.init();
|
||||
jest.clearAllMocks();
|
||||
|
||||
publisher.get.mockResolvedValue('main-n8n-main-1');
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsFollower).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
|
||||
it('should attempt to become leader when leadership is vacant', async () => {
|
||||
publisher.setIfNotExists.mockResolvedValue(true);
|
||||
await multiMainSetup.init();
|
||||
jest.clearAllMocks();
|
||||
|
||||
publisher.get.mockResolvedValue(null);
|
||||
publisher.setIfNotExists.mockResolvedValue(true);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(emit).toHaveBeenCalledWith('leader-stepdown');
|
||||
expect(publisher.setIfNotExists).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
redisClientService.toValidPrefix.mockReturnValue('n8n');
|
||||
describe('with new implementation (flag on)', () => {
|
||||
const logger = mockLogger();
|
||||
const publisher = mock<Publisher>();
|
||||
const redisClientService = mock<RedisClientService>();
|
||||
const errorReporter = mock<ErrorReporter>();
|
||||
const client = mock<LeaderElectionClient>();
|
||||
|
||||
let instanceSettings: InstanceSettings;
|
||||
let multiMainSetup: MultiMainSetup;
|
||||
|
||||
beforeEach(() => {
|
||||
instanceSettings = mock<InstanceSettings>({ hostId, isLeader: false });
|
||||
multiMainSetup = new MultiMainSetup(
|
||||
logger,
|
||||
instanceSettings,
|
||||
publisher,
|
||||
redisClientService,
|
||||
globalConfig,
|
||||
metadata,
|
||||
errorReporter,
|
||||
);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('checkLeader', () => {
|
||||
beforeEach(async () => {
|
||||
await multiMainSetup.init();
|
||||
const globalConfig = mock<GlobalConfig>({
|
||||
redis: { prefix: 'n8n' },
|
||||
multiMainSetup: { ttl: 10, interval: 3, enabled: true, newLeaderElection: true },
|
||||
});
|
||||
|
||||
it('should emit `leader-takeover` when Redis has own `hostId` but instance thinks it is follower', async () => {
|
||||
publisher.get.mockResolvedValue(hostId);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
let instanceSettings: InstanceSettings;
|
||||
let multiMainSetup: MultiMainSetup;
|
||||
|
||||
// @ts-expect-error - private method
|
||||
await multiMainSetup.checkLeader();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
instanceSettings = createInstanceSettings(hostId);
|
||||
|
||||
expect(instanceSettings.markAsLeader).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-takeover');
|
||||
jest.mock('@/scaling/leader-election-client', () => ({
|
||||
LeaderElectionClient: jest.fn(),
|
||||
}));
|
||||
|
||||
const { Container } = jest.requireActual('@n8n/di');
|
||||
Container.set(
|
||||
jest.requireMock('@/scaling/leader-election-client').LeaderElectionClient,
|
||||
client,
|
||||
);
|
||||
|
||||
multiMainSetup = new MultiMainSetup(
|
||||
logger,
|
||||
instanceSettings,
|
||||
globalConfig,
|
||||
metadata,
|
||||
errorReporter,
|
||||
publisher,
|
||||
redisClientService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not emit `leader-takeover` when already leader', async () => {
|
||||
publisher.get.mockResolvedValue(hostId);
|
||||
Object.defineProperty(instanceSettings, 'isLeader', { get: () => true });
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
describe('init', () => {
|
||||
it('should become leader if setLeaderIfNotExists succeeds', async () => {
|
||||
client.setLeaderIfNotExists.mockResolvedValue(createResultOk(true));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
// @ts-expect-error - private method
|
||||
await multiMainSetup.checkLeader();
|
||||
await multiMainSetup.init();
|
||||
|
||||
expect(instanceSettings.markAsLeader).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-takeover');
|
||||
expect(publisher.setExpiration).toHaveBeenCalled();
|
||||
expect(client.setLeaderIfNotExists).toHaveBeenCalled();
|
||||
expect(instanceSettings.markAsLeader).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
|
||||
it('should remain follower if setLeaderIfNotExists returns false', async () => {
|
||||
client.setLeaderIfNotExists.mockResolvedValue(createResultOk(false));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup.init();
|
||||
|
||||
expect(instanceSettings.markAsLeader).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
|
||||
it('should remain follower if setLeaderIfNotExists fails', async () => {
|
||||
client.setLeaderIfNotExists.mockResolvedValue(
|
||||
createResultError(new Error('Command timed out')),
|
||||
);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup.init();
|
||||
|
||||
expect(instanceSettings.markAsLeader).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit `leader-stepdown` when another instance is leader', async () => {
|
||||
publisher.get.mockResolvedValue('main-n8n-main-1');
|
||||
Object.defineProperty(instanceSettings, 'isLeader', { get: () => true });
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
describe('checkLeader (leader path)', () => {
|
||||
beforeEach(async () => {
|
||||
client.setLeaderIfNotExists.mockResolvedValue(createResultOk(true));
|
||||
await multiMainSetup.init();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// @ts-expect-error - private method
|
||||
await multiMainSetup.checkLeader();
|
||||
it('should stay leader when TTL renewal succeeds', async () => {
|
||||
client.tryRenewLeaderTtl.mockResolvedValue(createResultOk({ id: 'success' }));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
expect(instanceSettings.markAsFollower).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-stepdown');
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(client.tryRenewLeaderTtl).toHaveBeenCalled();
|
||||
expect(instanceSettings.markAsFollower).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
|
||||
it('should step down when another host is leader', async () => {
|
||||
client.tryRenewLeaderTtl.mockResolvedValue(
|
||||
createResultOk({ id: 'other-host-is-leader', currentLeaderId: 'main-n8n-main-1' }),
|
||||
);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsFollower).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
|
||||
it('should try to re-acquire leader key when key is missing', async () => {
|
||||
client.tryRenewLeaderTtl.mockResolvedValue(createResultOk({ id: 'key-missing' }));
|
||||
client.setLeaderIfNotExists.mockResolvedValue(createResultOk(true));
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(client.setLeaderIfNotExists).toHaveBeenCalled();
|
||||
expect(instanceSettings.markAsFollower).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should step down when key is missing and re-acquire fails', async () => {
|
||||
client.tryRenewLeaderTtl.mockResolvedValue(createResultOk({ id: 'key-missing' }));
|
||||
client.setLeaderIfNotExists.mockResolvedValue(createResultOk(false));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsFollower).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
|
||||
it('should step down when key is missing and Redis command fails', async () => {
|
||||
client.tryRenewLeaderTtl.mockResolvedValue(createResultOk({ id: 'key-missing' }));
|
||||
client.setLeaderIfNotExists.mockResolvedValue(
|
||||
createResultError(new Error('Command timed out')),
|
||||
);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsFollower).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
|
||||
it('should stay leader when TTL renewal Redis command fails', async () => {
|
||||
client.tryRenewLeaderTtl.mockResolvedValue(
|
||||
createResultError(new Error('Command timed out')),
|
||||
);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsFollower).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not emit `leader-stepdown` when already a follower', async () => {
|
||||
publisher.get.mockResolvedValue('main-n8n-main-1');
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
describe('checkLeader (follower path)', () => {
|
||||
beforeEach(async () => {
|
||||
client.setLeaderIfNotExists.mockResolvedValue(createResultOk(false));
|
||||
await multiMainSetup.init();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// @ts-expect-error - private method
|
||||
await multiMainSetup.checkLeader();
|
||||
it('should become leader when Redis shows own hostId as leader (mismatch recovery)', async () => {
|
||||
client.getLeader.mockResolvedValue(createResultOk(hostId));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
it('should attempt to become leader when leadership is vacant', async () => {
|
||||
publisher.get.mockResolvedValue(null);
|
||||
publisher.setIfNotExists.mockResolvedValue(true);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
expect(errorReporter.info).toHaveBeenCalled();
|
||||
expect(instanceSettings.markAsLeader).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
|
||||
// @ts-expect-error - private method
|
||||
await multiMainSetup.checkLeader();
|
||||
it('should stay follower when another instance is leader', async () => {
|
||||
client.getLeader.mockResolvedValue(createResultOk('main-n8n-main-1'));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
expect(instanceSettings.markAsFollower).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-stepdown');
|
||||
expect(emit).toHaveBeenCalledWith('leader-takeover');
|
||||
expect(instanceSettings.markAsLeader).toHaveBeenCalled();
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsLeader).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-takeover');
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
|
||||
it('should attempt to become leader when leadership is vacant', async () => {
|
||||
client.getLeader.mockResolvedValue(createResultOk(null));
|
||||
client.setLeaderIfNotExists.mockResolvedValue(createResultOk(true));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(client.setLeaderIfNotExists).toHaveBeenCalled();
|
||||
expect(instanceSettings.markAsLeader).toHaveBeenCalled();
|
||||
expect(emit).toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
|
||||
it('should stay follower when leadership is vacant but setLeaderIfNotExists fails', async () => {
|
||||
client.getLeader.mockResolvedValue(createResultOk(null));
|
||||
client.setLeaderIfNotExists.mockResolvedValue(createResultOk(false));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsLeader).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
|
||||
it('should stay follower when Redis is unreachable', async () => {
|
||||
client.getLeader.mockResolvedValue(createResultError(new Error('Command timed out')));
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsLeader).not.toHaveBeenCalled();
|
||||
expect(instanceSettings.markAsFollower).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-takeover');
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-stepdown');
|
||||
});
|
||||
|
||||
it('should stay follower when leadership is vacant and Redis command fails', async () => {
|
||||
client.getLeader.mockResolvedValue(createResultOk(null));
|
||||
client.setLeaderIfNotExists.mockResolvedValue(
|
||||
createResultError(new Error('Command timed out')),
|
||||
);
|
||||
const emit = jest.spyOn(multiMainSetup, 'emit');
|
||||
|
||||
await multiMainSetup['strategy'].checkLeader();
|
||||
|
||||
expect(instanceSettings.markAsLeader).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalledWith('leader-takeover');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
151
packages/cli/src/scaling/leader-election-client.ts
Normal file
151
packages/cli/src/scaling/leader-election-client.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { RedisClientService } from '@/services/redis-client.service';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { Cluster, Redis } from 'ioredis';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
import { ensureError, type Result, createResultOk, createResultError } from 'n8n-workflow';
|
||||
|
||||
const COMMAND_TIMEOUT_MS = 5_000;
|
||||
|
||||
/**
|
||||
* Atomically extends the leader key TTL only if the current value matches the
|
||||
* provided hostId
|
||||
*
|
||||
* KEYS[1] - leader key
|
||||
* ARGV[1] - hostId
|
||||
* ARGV[2] - TTL in seconds
|
||||
*
|
||||
* Returns:
|
||||
* -1 key does not exist
|
||||
* "actual-value" key exists but value is not the expected value
|
||||
* 1 value matched and TTL was extended
|
||||
* 0 value matched, but TTL was not extended
|
||||
*/
|
||||
const INCREASE_TTL_IF_LEADER = `
|
||||
-- Renew only if we still hold the lock
|
||||
local currentValue = redis.call("GET", KEYS[1])
|
||||
|
||||
if not currentValue then
|
||||
return -1
|
||||
end
|
||||
|
||||
if currentValue ~= ARGV[1] then
|
||||
return currentValue
|
||||
end
|
||||
|
||||
return redis.call("EXPIRE", KEYS[1], tonumber(ARGV[2]))
|
||||
`;
|
||||
|
||||
export type TtlRenewalResultKeyMissing = { id: 'key-missing' };
|
||||
export type TtlRenewalResultOtherHostIsLeader = {
|
||||
id: 'other-host-is-leader';
|
||||
currentLeaderId: string;
|
||||
};
|
||||
export type TtlRenewalResultSuccess = { id: 'success' };
|
||||
|
||||
export type TtlRenewalResult =
|
||||
| TtlRenewalResultKeyMissing
|
||||
| TtlRenewalResultOtherHostIsLeader
|
||||
| TtlRenewalResultSuccess;
|
||||
|
||||
/**
|
||||
* Redis-backed client for leader election in multi-main setups. Uses a TTL-based key to
|
||||
* track which instance is the current leader.
|
||||
*/
|
||||
@Service()
|
||||
export class LeaderElectionClient {
|
||||
private readonly redisClient: Redis | Cluster;
|
||||
|
||||
private readonly leaderKey: string;
|
||||
|
||||
private readonly leaderKeyTtlInS: number;
|
||||
|
||||
private get hostId() {
|
||||
return this.instanceSettings.hostId;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
globalConfig: GlobalConfig,
|
||||
redisClientService: RedisClientService,
|
||||
) {
|
||||
const prefix = redisClientService.toValidPrefix(globalConfig.redis.prefix);
|
||||
this.leaderKey = prefix + ':main_instance_leader';
|
||||
|
||||
this.leaderKeyTtlInS = globalConfig.multiMainSetup.ttl;
|
||||
|
||||
this.redisClient = redisClientService.createClient({
|
||||
type: 'leader(n8n)',
|
||||
extraOptions: { commandTimeout: COMMAND_TIMEOUT_MS },
|
||||
});
|
||||
}
|
||||
|
||||
/** Return the current leader's hostId, or `null` if the key is absent. */
|
||||
async getLeader(): Promise<Result<string | null, Error>> {
|
||||
try {
|
||||
return createResultOk(await this.redisClient.get(this.leaderKey));
|
||||
} catch (e) {
|
||||
return createResultError(ensureError(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** Claim leadership with a TTL. Returns `true` if the key was set (i.e. no leader yet). */
|
||||
async setLeaderIfNotExists(): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const result = await this.redisClient.set(
|
||||
this.leaderKey,
|
||||
this.hostId,
|
||||
'EX',
|
||||
this.leaderKeyTtlInS,
|
||||
'NX',
|
||||
);
|
||||
return createResultOk(result === 'OK');
|
||||
} catch (e) {
|
||||
return createResultError(ensureError(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** Atomically extend the leader key TTL only if this host still holds it. */
|
||||
async tryRenewLeaderTtl(): Promise<Result<TtlRenewalResult, Error>> {
|
||||
try {
|
||||
const result = await this.redisClient.eval(
|
||||
INCREASE_TTL_IF_LEADER,
|
||||
1,
|
||||
this.leaderKey,
|
||||
this.hostId,
|
||||
this.leaderKeyTtlInS,
|
||||
);
|
||||
|
||||
if (result === -1 || result === 0) {
|
||||
return createResultOk({ id: 'key-missing' });
|
||||
}
|
||||
if (result === 1) {
|
||||
return createResultOk({ id: 'success' });
|
||||
}
|
||||
if (typeof result === 'string') {
|
||||
return createResultOk({ id: 'other-host-is-leader', currentLeaderId: result });
|
||||
}
|
||||
|
||||
return createResultError(
|
||||
new Error(`Unexpected result from Redis script: ${JSON.stringify(result)}`),
|
||||
);
|
||||
} catch (e) {
|
||||
return createResultError(ensureError(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete the leader key so another instance can claim leadership. */
|
||||
async clearLeader(): Promise<Result<void, Error>> {
|
||||
try {
|
||||
await this.redisClient.del(this.leaderKey);
|
||||
return createResultOk(undefined);
|
||||
} catch (e) {
|
||||
return createResultError(ensureError(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** Disconnect the underlying Redis client. */
|
||||
destroy() {
|
||||
this.redisClient.disconnect();
|
||||
}
|
||||
}
|
||||
120
packages/cli/src/scaling/multi-main-setup-legacy.ts
Normal file
120
packages/cli/src/scaling/multi-main-setup-legacy.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import type { Logger } from '@n8n/backend-common';
|
||||
import type { GlobalConfig } from '@n8n/config';
|
||||
import type { ErrorReporter, InstanceSettings } from 'n8n-core';
|
||||
|
||||
import type { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import type { RedisClientService } from '@/services/redis-client.service';
|
||||
|
||||
import type { MultiMainStrategy } from './multi-main-setup.types';
|
||||
|
||||
type EmitFn = (event: 'leader-takeover' | 'leader-stepdown') => void;
|
||||
|
||||
export class MultiMainSetupLegacy implements MultiMainStrategy {
|
||||
private leaderKey: string;
|
||||
|
||||
private readonly leaderKeyTtl: number;
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly publisher: Publisher,
|
||||
private readonly redisClientService: RedisClientService,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly errorReporter: ErrorReporter,
|
||||
private readonly emit: EmitFn,
|
||||
) {
|
||||
this.leaderKeyTtl = this.globalConfig.multiMainSetup.ttl;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const prefix = this.globalConfig.redis.prefix;
|
||||
const validPrefix = this.redisClientService.toValidPrefix(prefix);
|
||||
this.leaderKey = validPrefix + ':main_instance_leader';
|
||||
|
||||
await this.tryBecomeLeader();
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
const { isLeader } = this.instanceSettings;
|
||||
|
||||
if (isLeader) await this.publisher.clear(this.leaderKey);
|
||||
}
|
||||
|
||||
async checkLeader() {
|
||||
const leaderId = await this.publisher.get(this.leaderKey);
|
||||
|
||||
const { hostId } = this.instanceSettings;
|
||||
|
||||
if (leaderId === hostId) {
|
||||
if (!this.instanceSettings.isLeader) {
|
||||
this.errorReporter.info(
|
||||
`[Instance ID ${hostId}] Remote/Local leadership mismatch, marking self as leader`,
|
||||
{
|
||||
shouldBeLogged: true,
|
||||
shouldReport: true,
|
||||
},
|
||||
);
|
||||
|
||||
this.instanceSettings.markAsLeader();
|
||||
|
||||
this.emit('leader-takeover');
|
||||
}
|
||||
|
||||
this.logger.debug(`[Instance ID ${hostId}] Leader is this instance`);
|
||||
|
||||
await this.publisher.setExpiration(this.leaderKey, this.leaderKeyTtl);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (leaderId && leaderId !== hostId) {
|
||||
this.logger.debug(`[Instance ID ${hostId}] Leader is other instance "${leaderId}"`);
|
||||
|
||||
if (this.instanceSettings.isLeader) {
|
||||
this.instanceSettings.markAsFollower();
|
||||
|
||||
this.emit('leader-stepdown');
|
||||
|
||||
this.logger.warn('[Multi-main setup] Leader failed to renew leader key');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!leaderId) {
|
||||
this.logger.debug(
|
||||
`[Instance ID ${hostId}] Leadership vacant, attempting to become leader...`,
|
||||
);
|
||||
|
||||
this.instanceSettings.markAsFollower();
|
||||
|
||||
this.emit('leader-stepdown');
|
||||
|
||||
await this.tryBecomeLeader();
|
||||
}
|
||||
}
|
||||
|
||||
private async tryBecomeLeader() {
|
||||
const { hostId } = this.instanceSettings;
|
||||
|
||||
const keySetSuccessfully = await this.publisher.setIfNotExists(
|
||||
this.leaderKey,
|
||||
hostId,
|
||||
this.leaderKeyTtl,
|
||||
);
|
||||
|
||||
if (keySetSuccessfully) {
|
||||
this.logger.info(`[Instance ID ${hostId}] Leader is now this instance`);
|
||||
|
||||
this.instanceSettings.markAsLeader();
|
||||
|
||||
this.emit('leader-takeover');
|
||||
} else {
|
||||
this.instanceSettings.markAsFollower();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLeaderKey() {
|
||||
return await this.publisher.get(this.leaderKey);
|
||||
}
|
||||
}
|
||||
191
packages/cli/src/scaling/multi-main-setup-v2.ts
Normal file
191
packages/cli/src/scaling/multi-main-setup-v2.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import type { LeaderElectionClient } from '@/scaling/leader-election-client';
|
||||
import type { Logger } from '@n8n/backend-common';
|
||||
import type { ErrorReporter, InstanceSettings } from 'n8n-core';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import type { MultiMainStrategy } from './multi-main-setup.types';
|
||||
|
||||
type EmitFn = (event: 'leader-takeover' | 'leader-stepdown') => void;
|
||||
|
||||
export class MultiMainSetupV2 implements MultiMainStrategy {
|
||||
private leaderCheckInProgress = false;
|
||||
|
||||
private get hostId() {
|
||||
return this.instanceSettings.hostId;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly errorReporter: ErrorReporter,
|
||||
private readonly client: LeaderElectionClient,
|
||||
private readonly emit: EmitFn,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
const result = await this.client.setLeaderIfNotExists();
|
||||
if (!result.ok) {
|
||||
this.logRedisCommandFailure('Failed to set leader key in Redis during init', result.error);
|
||||
this.instanceSettings.markAsFollower();
|
||||
} else if (result.result) {
|
||||
this.takeOverAsLeader();
|
||||
} else {
|
||||
this.instanceSettings.markAsFollower();
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
const { isLeader } = this.instanceSettings;
|
||||
|
||||
if (isLeader) {
|
||||
// TODO: We should guard here that we only remove the key the key in Redis matches
|
||||
// our host ID.
|
||||
const result = await this.client.clearLeader();
|
||||
if (!result.ok) {
|
||||
this.logger.warn('Failed to clear leader key from Redis', { error: result.error });
|
||||
}
|
||||
}
|
||||
|
||||
this.client.destroy();
|
||||
}
|
||||
|
||||
async checkLeader() {
|
||||
if (this.leaderCheckInProgress) {
|
||||
this.logger.warn('Previous leader check is still in progress, skipping this check');
|
||||
return;
|
||||
}
|
||||
|
||||
this.leaderCheckInProgress = true;
|
||||
try {
|
||||
if (this.instanceSettings.isLeader) {
|
||||
await this.checkAreWeStillLeader();
|
||||
} else {
|
||||
await this.checkCanBecomeLeader();
|
||||
}
|
||||
} finally {
|
||||
this.leaderCheckInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Renew our leadership lease. If we've lost the lease, step down to follower. */
|
||||
private async checkAreWeStillLeader() {
|
||||
assert(this.instanceSettings.isLeader);
|
||||
|
||||
const renewTtlResult = await this.client.tryRenewLeaderTtl();
|
||||
if (!renewTtlResult.ok) {
|
||||
this.logRedisCommandFailure('Failed to renew leader TTL', renewTtlResult.error);
|
||||
// There's a decision to be made here: Do we step down or not? Redis might
|
||||
// be unavailable for all clients or only for us. We could also track the TTL
|
||||
// locally, but this would make the implementation more complex and error-prone.
|
||||
// For now we accept that this might cause some inconsistencies in a network
|
||||
// partition scenario, but eventually the system will recover once Redis is available again.
|
||||
return;
|
||||
}
|
||||
|
||||
const renewalResult = renewTtlResult.result;
|
||||
if (renewalResult.id === 'success') {
|
||||
this.logger.debug(`[Instance ID ${this.hostId}] Leader is this instance`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.warn('[Multi-main setup] Leader failed to renew leader key');
|
||||
|
||||
if (renewalResult.id === 'other-host-is-leader') {
|
||||
this.logger.debug(
|
||||
`[Instance ID ${this.hostId}] Leader is other instance "${renewalResult.currentLeaderId}"`,
|
||||
);
|
||||
this.stepDownToFollower();
|
||||
return;
|
||||
}
|
||||
|
||||
// The only remaining case is 'key-missing', which means we lost leadership
|
||||
// (e.g. due to Redis unavailability or a network partition). In this case
|
||||
// we try to become leader and step down if that fails.
|
||||
assert(renewalResult.id === 'key-missing');
|
||||
|
||||
const result = await this.client.setLeaderIfNotExists();
|
||||
if (!result.ok) {
|
||||
this.logRedisCommandFailure('Failed to set leader key in Redis', result.error);
|
||||
this.stepDownToFollower();
|
||||
return;
|
||||
}
|
||||
|
||||
const wasSet = result.result;
|
||||
if (!wasSet) {
|
||||
this.stepDownToFollower();
|
||||
}
|
||||
}
|
||||
|
||||
private async checkCanBecomeLeader() {
|
||||
assert(!this.instanceSettings.isLeader);
|
||||
|
||||
const getResult = await this.client.getLeader();
|
||||
if (!getResult.ok) {
|
||||
this.logRedisCommandFailure('Failed to get leader key from Redis', getResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const leaderId = getResult.result;
|
||||
if (leaderId && leaderId === this.hostId) {
|
||||
this.errorReporter.info(
|
||||
`[Instance ID ${this.hostId}] Remote/Local leadership mismatch, marking self as leader`,
|
||||
{
|
||||
shouldBeLogged: true,
|
||||
shouldReport: true,
|
||||
},
|
||||
);
|
||||
|
||||
this.takeOverAsLeader();
|
||||
return;
|
||||
}
|
||||
|
||||
if (leaderId) {
|
||||
this.logger.debug(`[Instance ID ${this.hostId}] Leader is other instance "${leaderId}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`[Instance ID ${this.hostId}] Leadership vacant, attempting to become leader...`,
|
||||
);
|
||||
|
||||
const result = await this.client.setLeaderIfNotExists();
|
||||
if (!result.ok) {
|
||||
this.logger.warn('Failed to try leader key set in Redis', { error: result.error });
|
||||
return;
|
||||
}
|
||||
|
||||
const wasSet = result.result;
|
||||
if (wasSet) {
|
||||
this.takeOverAsLeader();
|
||||
}
|
||||
}
|
||||
|
||||
private takeOverAsLeader() {
|
||||
assert(!this.instanceSettings.isLeader);
|
||||
|
||||
this.logger.info(`[Instance ID ${this.hostId}] Leader is now this instance`);
|
||||
|
||||
this.instanceSettings.markAsLeader();
|
||||
|
||||
this.emit('leader-takeover');
|
||||
}
|
||||
|
||||
private stepDownToFollower() {
|
||||
assert(this.instanceSettings.isLeader);
|
||||
|
||||
this.logger.info(`[Instance ID ${this.hostId}] This is now a follower instance`);
|
||||
|
||||
this.instanceSettings.markAsFollower();
|
||||
|
||||
this.emit('leader-stepdown');
|
||||
}
|
||||
|
||||
async fetchLeaderKey() {
|
||||
const result = await this.client.getLeader();
|
||||
return result.ok ? result.result : null;
|
||||
}
|
||||
|
||||
private logRedisCommandFailure(message: string, error: Error) {
|
||||
this.logger.warn(`${message}: ${error.message}`, { error });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { TypedEmitter } from '@/typed-emitter';
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Time } from '@n8n/constants';
|
||||
|
|
@ -5,9 +6,13 @@ import { MultiMainMetadata } from '@n8n/decorators';
|
|||
import { Container, Service } from '@n8n/di';
|
||||
import { ErrorReporter, InstanceSettings } from 'n8n-core';
|
||||
|
||||
import type * as LeaderElectionClientModule from '@/scaling/leader-election-client';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import { RedisClientService } from '@/services/redis-client.service';
|
||||
import { TypedEmitter } from '@/typed-emitter';
|
||||
|
||||
import { MultiMainSetupLegacy } from './multi-main-setup-legacy';
|
||||
import type { MultiMainStrategy } from './multi-main-setup.types';
|
||||
import { MultiMainSetupV2 } from './multi-main-setup-v2';
|
||||
|
||||
type MultiMainEvents = {
|
||||
/**
|
||||
|
|
@ -28,34 +33,53 @@ type MultiMainEvents = {
|
|||
/** Designates leader and followers when running multiple main processes. */
|
||||
@Service()
|
||||
export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly publisher: Publisher,
|
||||
private readonly redisClientService: RedisClientService,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly metadata: MultiMainMetadata,
|
||||
private readonly errorReporter: ErrorReporter,
|
||||
) {
|
||||
super();
|
||||
this.logger = this.logger.scoped(['scaling', 'multi-main-setup']);
|
||||
}
|
||||
|
||||
private leaderKey: string;
|
||||
|
||||
private readonly leaderKeyTtl = this.globalConfig.multiMainSetup.ttl;
|
||||
private readonly strategy: MultiMainStrategy;
|
||||
|
||||
private leaderCheckInterval: NodeJS.Timeout | undefined;
|
||||
|
||||
async init() {
|
||||
const prefix = this.globalConfig.redis.prefix;
|
||||
const validPrefix = this.redisClientService.toValidPrefix(prefix);
|
||||
this.leaderKey = validPrefix + ':main_instance_leader';
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly metadata: MultiMainMetadata,
|
||||
private readonly errorReporter: ErrorReporter,
|
||||
private readonly publisher: Publisher,
|
||||
private readonly redisClientService: RedisClientService,
|
||||
) {
|
||||
super();
|
||||
this.logger = this.logger.scoped(['scaling', 'multi-main-setup']);
|
||||
|
||||
await this.tryBecomeLeader(); // prevent initial wait
|
||||
const emitFn = (event: 'leader-takeover' | 'leader-stepdown') => this.emit(event);
|
||||
|
||||
if (this.globalConfig.multiMainSetup.newLeaderElection) {
|
||||
const { LeaderElectionClient } =
|
||||
require('@/scaling/leader-election-client') as typeof LeaderElectionClientModule;
|
||||
const client = Container.get(LeaderElectionClient);
|
||||
this.strategy = new MultiMainSetupV2(
|
||||
this.logger,
|
||||
this.instanceSettings,
|
||||
this.errorReporter,
|
||||
client,
|
||||
emitFn,
|
||||
);
|
||||
} else {
|
||||
this.strategy = new MultiMainSetupLegacy(
|
||||
this.logger,
|
||||
this.instanceSettings,
|
||||
this.publisher,
|
||||
this.redisClientService,
|
||||
this.globalConfig,
|
||||
this.errorReporter,
|
||||
emitFn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.strategy.init();
|
||||
|
||||
this.leaderCheckInterval = setInterval(async () => {
|
||||
await this.checkLeader();
|
||||
await this.strategy.checkLeader();
|
||||
}, this.globalConfig.multiMainSetup.interval * Time.seconds.toMilliseconds);
|
||||
}
|
||||
|
||||
|
|
@ -63,90 +87,11 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
|
|||
async shutdown() {
|
||||
clearInterval(this.leaderCheckInterval);
|
||||
|
||||
const { isLeader } = this.instanceSettings;
|
||||
|
||||
if (isLeader) await this.publisher.clear(this.leaderKey);
|
||||
await this.strategy.shutdown();
|
||||
}
|
||||
|
||||
private async checkLeader() {
|
||||
const leaderId = await this.publisher.get(this.leaderKey);
|
||||
|
||||
const { hostId } = this.instanceSettings;
|
||||
|
||||
if (leaderId === hostId) {
|
||||
if (!this.instanceSettings.isLeader) {
|
||||
// This indicates that the remote state indicated that this host is the leader, but this
|
||||
// host believed it was a follower. See CAT-2200 for more context.
|
||||
this.errorReporter.info(
|
||||
`[Instance ID ${hostId}] Remote/Local leadership mismatch, marking self as leader`,
|
||||
{
|
||||
shouldBeLogged: true,
|
||||
shouldReport: true,
|
||||
},
|
||||
);
|
||||
|
||||
this.instanceSettings.markAsLeader();
|
||||
|
||||
this.emit('leader-takeover');
|
||||
}
|
||||
|
||||
this.logger.debug(`[Instance ID ${hostId}] Leader is this instance`);
|
||||
|
||||
await this.publisher.setExpiration(this.leaderKey, this.leaderKeyTtl);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (leaderId && leaderId !== hostId) {
|
||||
this.logger.debug(`[Instance ID ${hostId}] Leader is other instance "${leaderId}"`);
|
||||
|
||||
if (this.instanceSettings.isLeader) {
|
||||
this.instanceSettings.markAsFollower();
|
||||
|
||||
this.emit('leader-stepdown');
|
||||
|
||||
this.logger.warn('[Multi-main setup] Leader failed to renew leader key');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!leaderId) {
|
||||
this.logger.debug(
|
||||
`[Instance ID ${hostId}] Leadership vacant, attempting to become leader...`,
|
||||
);
|
||||
|
||||
this.instanceSettings.markAsFollower();
|
||||
|
||||
this.emit('leader-stepdown');
|
||||
|
||||
await this.tryBecomeLeader();
|
||||
}
|
||||
}
|
||||
|
||||
private async tryBecomeLeader() {
|
||||
const { hostId } = this.instanceSettings;
|
||||
|
||||
// this can only succeed if leadership is currently vacant
|
||||
const keySetSuccessfully = await this.publisher.setIfNotExists(
|
||||
this.leaderKey,
|
||||
hostId,
|
||||
this.leaderKeyTtl,
|
||||
);
|
||||
|
||||
if (keySetSuccessfully) {
|
||||
this.logger.info(`[Instance ID ${hostId}] Leader is now this instance`);
|
||||
|
||||
this.instanceSettings.markAsLeader();
|
||||
|
||||
this.emit('leader-takeover');
|
||||
} else {
|
||||
this.instanceSettings.markAsFollower();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLeaderKey() {
|
||||
return await this.publisher.get(this.leaderKey);
|
||||
async fetchLeaderKey(): Promise<string | null> {
|
||||
return await this.strategy.fetchLeaderKey();
|
||||
}
|
||||
|
||||
registerEventHandlers() {
|
||||
|
|
@ -155,7 +100,6 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
|
|||
for (const { eventHandlerClass, methodName, eventName } of handlers) {
|
||||
const instance = Container.get(eventHandlerClass);
|
||||
this.on(eventName, async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return await instance[methodName].call(instance);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
6
packages/cli/src/scaling/multi-main-setup.types.ts
Normal file
6
packages/cli/src/scaling/multi-main-setup.types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface MultiMainStrategy {
|
||||
init(): Promise<void>;
|
||||
shutdown(): Promise<void>;
|
||||
checkLeader(): Promise<void>;
|
||||
fetchLeaderKey(): Promise<string | null>;
|
||||
}
|
||||
|
|
@ -118,9 +118,7 @@ export class Publisher {
|
|||
|
||||
// #endregion
|
||||
|
||||
// #region Utils for multi-main setup
|
||||
|
||||
// @TODO: The following methods are not pubsub-specific. Consider a dedicated client for multi-main setup.
|
||||
// #region Key-value utils (used by MCP session store and legacy leader election)
|
||||
|
||||
async setIfNotExists(key: string, value: string, ttl: number) {
|
||||
const result = await this.client.set(key, value, 'EX', ttl, 'NX');
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ export type RedisClientType = N8nRedisClientType | BullRedisClientType;
|
|||
* - `publisher(n8n)` to send messages into scaling mode pubsub channels
|
||||
* - `cache(n8n)` for caching operations (variables, resource ownership, etc.)
|
||||
*/
|
||||
type N8nRedisClientType = 'subscriber(n8n)' | 'publisher(n8n)' | 'cache(n8n)' | 'registry(n8n)';
|
||||
type N8nRedisClientType =
|
||||
| 'subscriber(n8n)'
|
||||
| 'publisher(n8n)'
|
||||
| 'cache(n8n)'
|
||||
| 'registry(n8n)'
|
||||
| 'leader(n8n)';
|
||||
|
||||
/**
|
||||
* Redis client used internally by Bull. Suffixed with `(bull)` at `ScalingService.setupQueue`.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { WorkflowEntity } from '@n8n/db';
|
|||
import { generateNanoId, WorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import { MultiMainSetup } from '@/scaling/multi-main-setup.ee';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-core",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.2",
|
||||
"description": "Core functionality of n8n",
|
||||
"main": "dist/index",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
|||
|
|
@ -501,7 +501,7 @@ describe('Request Helper Functions', () => {
|
|||
hostname: 'example.de',
|
||||
href: requestObject.uri,
|
||||
};
|
||||
axiosOptions.beforeRedirect!(redirectOptions, mock());
|
||||
axiosOptions.beforeRedirect!(redirectOptions, mock(), mock());
|
||||
expect(redirectOptions.agent).toEqual(redirectOptions.agents.https);
|
||||
expect((redirectOptions.agent as HttpsAgent).options).toMatchObject({
|
||||
servername: 'example.de',
|
||||
|
|
@ -2240,7 +2240,7 @@ describe('Request Helper Functions', () => {
|
|||
};
|
||||
|
||||
expect(axiosOptions.beforeRedirect).toBeDefined();
|
||||
expect(() => axiosOptions.beforeRedirect!(redirectOptions, mock())).toThrow(
|
||||
expect(() => axiosOptions.beforeRedirect!(redirectOptions, mock(), mock())).toThrow(
|
||||
'Domain not allowed',
|
||||
);
|
||||
});
|
||||
|
|
@ -2259,7 +2259,9 @@ describe('Request Helper Functions', () => {
|
|||
};
|
||||
|
||||
expect(axiosOptions.beforeRedirect).toBeDefined();
|
||||
expect(() => axiosOptions.beforeRedirect!(redirectOptions, mock())).not.toThrow();
|
||||
expect(() =>
|
||||
axiosOptions.beforeRedirect!(redirectOptions, mock(), mock()),
|
||||
).not.toThrow();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -2278,7 +2280,7 @@ describe('Request Helper Functions', () => {
|
|||
};
|
||||
|
||||
expect(axiosConfig.beforeRedirect).toBeDefined();
|
||||
expect(() => axiosConfig.beforeRedirect!(redirectOptions, mock())).toThrow(
|
||||
expect(() => axiosConfig.beforeRedirect!(redirectOptions, mock(), mock())).toThrow(
|
||||
'Domain not allowed',
|
||||
);
|
||||
});
|
||||
|
|
@ -2298,7 +2300,9 @@ describe('Request Helper Functions', () => {
|
|||
};
|
||||
|
||||
expect(axiosConfig.beforeRedirect).toBeDefined();
|
||||
expect(() => axiosConfig.beforeRedirect!(redirectOptions, mock())).not.toThrow();
|
||||
expect(() =>
|
||||
axiosConfig.beforeRedirect!(redirectOptions, mock(), mock()),
|
||||
).not.toThrow();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -424,7 +424,11 @@ export async function proxyRequestToAxios(
|
|||
): Promise<any> {
|
||||
let axiosConfig: AxiosRequestConfig = {
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
// -1 is the Axios sentinel for "no limit". Infinity also means no limit but
|
||||
// Axios 1.15.1+ treats any value > -1 as a finite cap, wrapping stream responses
|
||||
// in Readable.from() even when the limit is Infinity. That breaks the downstream
|
||||
// `instanceof IncomingMessage` checks in parseIncomingMessage / prepareBinaryData.
|
||||
maxContentLength: -1,
|
||||
};
|
||||
let configObject: IRequestOptions;
|
||||
if (typeof uriOrObject === 'string') {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/chat",
|
||||
"version": "1.20.0",
|
||||
"version": "1.20.1",
|
||||
"scripts": {
|
||||
"dev": "pnpm run --dir=../storybook dev --initial-path=/docs/chat-chat--docs",
|
||||
"build": "pnpm build:vite && pnpm build:bundle",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "@n8n/design-system",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.1",
|
||||
"main": "src/index.ts",
|
||||
"import": "src/index.ts",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
// Canvas
|
||||
--canvas--color--background: var(--color--neutral-125);
|
||||
--canvas--dot--color: var(--color--neutral-500);
|
||||
--canvas--read-only-line--color: var(--color--neutral-100);
|
||||
--canvas--read-only-line--color: var(--color--neutral-200);
|
||||
--canvas--color--selected: var(--color--neutral-150);
|
||||
--canvas--color--selected-transparent: hsla(220, 47%, 30%, 0.1);
|
||||
--canvas--label--color: var(--color--neutral-600);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@n8n/rest-api-client",
|
||||
"type": "module",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.1",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.1",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import nock from 'nock';
|
|||
describe('Test Binary Data Download', () => {
|
||||
const baseUrl = 'https://dummy.domain';
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeAll(() => {
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.get('/path/to/image.png')
|
||||
|
|
|
|||
|
|
@ -415,14 +415,14 @@ export function getConditions(options: IDataObject): string | undefined {
|
|||
export function getDefaultFields(sobject: string) {
|
||||
return (
|
||||
{
|
||||
Account: 'id,name,type',
|
||||
Lead: 'id,company,firstname,lastname,street,postalCode,city,email,status',
|
||||
Contact: 'id,firstname,lastname,email',
|
||||
Opportunity: 'id,accountId,amount,probability,type',
|
||||
Case: 'id,accountId,contactId,priority,status,subject,type',
|
||||
Task: 'id,subject,status,priority',
|
||||
Attachment: 'id,name',
|
||||
User: 'id,name,email',
|
||||
Account: 'id,name,type,LastModifiedDate',
|
||||
Lead: 'id,company,firstname,lastname,street,postalCode,city,email,status,LastModifiedDate',
|
||||
Contact: 'id,firstname,lastname,email,LastModifiedDate',
|
||||
Opportunity: 'id,accountId,amount,probability,type,LastModifiedDate',
|
||||
Case: 'id,accountId,contactId,priority,status,subject,type,LastModifiedDate',
|
||||
Task: 'id,subject,status,priority,LastModifiedDate',
|
||||
Attachment: 'id,name,LastModifiedDate',
|
||||
User: 'id,name,email,LastModifiedDate',
|
||||
} as IDataObject
|
||||
)[sobject];
|
||||
}
|
||||
|
|
@ -446,7 +446,10 @@ export function getQuery(options: IDataObject, sobject: string, returnAll: boole
|
|||
);
|
||||
}
|
||||
} else {
|
||||
fields.push.apply(fields, ((getDefaultFields(validSobject) as string) || 'id').split(','));
|
||||
fields.push.apply(
|
||||
fields,
|
||||
((getDefaultFields(validSobject) as string) || 'id,LastModifiedDate').split(','),
|
||||
);
|
||||
}
|
||||
const conditions = getConditions(options);
|
||||
|
||||
|
|
@ -487,10 +490,12 @@ export function filterAndManageProcessedItems(
|
|||
for (const item of responseData) {
|
||||
if (typeof item.Id !== 'string') continue;
|
||||
|
||||
const itemId = item.Id;
|
||||
if (!processedIdsSet.has(itemId)) {
|
||||
const itemKey =
|
||||
typeof item.LastModifiedDate === 'string' ? `${item.Id}_${item.LastModifiedDate}` : item.Id;
|
||||
|
||||
if (!processedIdsSet.has(itemKey)) {
|
||||
newItems.push(item);
|
||||
newItemIds.push(itemId);
|
||||
newItemIds.push(itemKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -194,16 +194,20 @@ describe('Salesforce -> GenericFunctions', () => {
|
|||
|
||||
describe('getDefaultFields', () => {
|
||||
it('should return default fields', () => {
|
||||
expect(getDefaultFields('Account')).toBe('id,name,type');
|
||||
expect(getDefaultFields('Account')).toBe('id,name,type,LastModifiedDate');
|
||||
expect(getDefaultFields('Lead')).toBe(
|
||||
'id,company,firstname,lastname,street,postalCode,city,email,status',
|
||||
'id,company,firstname,lastname,street,postalCode,city,email,status,LastModifiedDate',
|
||||
);
|
||||
expect(getDefaultFields('Contact')).toBe('id,firstname,lastname,email');
|
||||
expect(getDefaultFields('Opportunity')).toBe('id,accountId,amount,probability,type');
|
||||
expect(getDefaultFields('Case')).toBe('id,accountId,contactId,priority,status,subject,type');
|
||||
expect(getDefaultFields('Task')).toBe('id,subject,status,priority');
|
||||
expect(getDefaultFields('Attachment')).toBe('id,name');
|
||||
expect(getDefaultFields('User')).toBe('id,name,email');
|
||||
expect(getDefaultFields('Contact')).toBe('id,firstname,lastname,email,LastModifiedDate');
|
||||
expect(getDefaultFields('Opportunity')).toBe(
|
||||
'id,accountId,amount,probability,type,LastModifiedDate',
|
||||
);
|
||||
expect(getDefaultFields('Case')).toBe(
|
||||
'id,accountId,contactId,priority,status,subject,type,LastModifiedDate',
|
||||
);
|
||||
expect(getDefaultFields('Task')).toBe('id,subject,status,priority,LastModifiedDate');
|
||||
expect(getDefaultFields('Attachment')).toBe('id,name,LastModifiedDate');
|
||||
expect(getDefaultFields('User')).toBe('id,name,email,LastModifiedDate');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -233,7 +237,7 @@ describe('Salesforce -> GenericFunctions', () => {
|
|||
|
||||
const result = getQuery(options, 'Account', true);
|
||||
|
||||
expect(result).toBe('SELECT id,name,type FROM Account ');
|
||||
expect(result).toBe('SELECT id,name,type,LastModifiedDate FROM Account ');
|
||||
});
|
||||
|
||||
it('should return query with a condition', () => {
|
||||
|
|
@ -470,6 +474,42 @@ describe('Salesforce -> GenericFunctions', () => {
|
|||
|
||||
expect(result.updatedProcessedIds).toEqual(['100', '200', '003', '001', '004']);
|
||||
});
|
||||
|
||||
it('should trigger again when same Id has a different LastModifiedDate', () => {
|
||||
const processedIds = ['001_2024-01-01T00:00:00Z'];
|
||||
const responseData: IDataObject[] = [
|
||||
{ Id: '001', LastModifiedDate: '2024-01-02T00:00:00Z', Name: 'Updated Account' },
|
||||
];
|
||||
|
||||
const result = filterAndManageProcessedItems(responseData, processedIds);
|
||||
|
||||
expect(result.newItems).toEqual(responseData);
|
||||
expect(result.updatedProcessedIds).toContain('001_2024-01-02T00:00:00Z');
|
||||
});
|
||||
|
||||
it('should not trigger again when same Id and LastModifiedDate already processed', () => {
|
||||
const processedIds = ['001_2024-01-01T00:00:00Z'];
|
||||
const responseData: IDataObject[] = [
|
||||
{ Id: '001', LastModifiedDate: '2024-01-01T00:00:00Z', Name: 'Account' },
|
||||
];
|
||||
|
||||
const result = filterAndManageProcessedItems(responseData, processedIds);
|
||||
|
||||
expect(result.newItems).toEqual([]);
|
||||
});
|
||||
|
||||
it('should trigger for records stored with Id-only key before upgrade', () => {
|
||||
const processedIds = ['001']; // old format from before fix
|
||||
const responseData: IDataObject[] = [
|
||||
{ Id: '001', LastModifiedDate: '2024-01-01T00:00:00Z', Name: 'Account' },
|
||||
];
|
||||
|
||||
const result = filterAndManageProcessedItems(responseData, processedIds);
|
||||
|
||||
// One-time re-trigger on upgrade — old Id-only key does not match new composite key
|
||||
expect(result.newItems).toEqual(responseData);
|
||||
expect(result.updatedProcessedIds).toContain('001_2024-01-01T00:00:00Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('salesforceApiRequest - JWT Authentication', () => {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ describe('Salesforce Node', () => {
|
|||
salesforceNock
|
||||
.get('/query')
|
||||
.query({
|
||||
q: 'SELECT id,name,email FROM User ',
|
||||
q: 'SELECT id,name,email,LastModifiedDate FROM User ',
|
||||
})
|
||||
.reply(200, { records: users })
|
||||
.get('/sobjects/user/id1')
|
||||
|
|
@ -56,7 +56,7 @@ describe('Salesforce Node', () => {
|
|||
.reply(200, { id: 'id1', success: true, errors: [] })
|
||||
.get('/query')
|
||||
.query({
|
||||
q: 'SELECT id,subject,status,priority FROM Task ',
|
||||
q: 'SELECT id,subject,status,priority,LastModifiedDate FROM Task ',
|
||||
})
|
||||
.reply(200, { records: tasks })
|
||||
.get('/sobjects/task/id1')
|
||||
|
|
@ -84,7 +84,7 @@ describe('Salesforce Node', () => {
|
|||
.reply(200, { id: 'id1', success: true, errors: [] })
|
||||
.get('/query')
|
||||
.query({
|
||||
q: 'SELECT id,name,type FROM Account ',
|
||||
q: 'SELECT id,name,type,LastModifiedDate FROM Account ',
|
||||
})
|
||||
.reply(200, { records: accounts })
|
||||
.post('/sobjects/note', { Title: 'New note', ParentId: 'id1' })
|
||||
|
|
@ -141,7 +141,7 @@ describe('Salesforce Node', () => {
|
|||
.reply(200, { id: 'id1', success: true, errors: [] })
|
||||
.get('/query')
|
||||
.query({
|
||||
q: 'SELECT id,accountId,amount,probability,type FROM Opportunity ',
|
||||
q: 'SELECT id,accountId,amount,probability,type,LastModifiedDate FROM Opportunity ',
|
||||
})
|
||||
.reply(200, { records: opportunities })
|
||||
.post('/sobjects/note', { Title: 'New Note', ParentId: 'id1' })
|
||||
|
|
|
|||
|
|
@ -73,6 +73,44 @@ export async function destroy(conn: snowflake.Connection) {
|
|||
});
|
||||
}
|
||||
|
||||
export function escapeSnowflakeIdentifier(identifier: string): string {
|
||||
if (identifier.startsWith('"') && identifier.endsWith('"') && identifier.length > 2) {
|
||||
// Already quoted — preserve case (Snowflake quoted identifiers are case-sensitive)
|
||||
const bare = identifier.slice(1, -1).replace(/""/g, '"');
|
||||
return `"${bare.replace(/"/g, '""')}"`;
|
||||
}
|
||||
// Snowflake stores unquoted identifiers as UPPERCASE by default; uppercase for compatibility
|
||||
return `"${identifier.toUpperCase().replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
export function escapeSnowflakeObjectIdentifier(identifier: string): string {
|
||||
const parts: string[] = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
|
||||
for (let i = 0; i < identifier.length; i++) {
|
||||
const char = identifier[i];
|
||||
if (char === '"') {
|
||||
if (inQuotes && identifier[i + 1] === '"') {
|
||||
// Escaped double-quote inside a quoted identifier
|
||||
current += '""';
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
current += char;
|
||||
}
|
||||
} else if (char === '.' && !inQuotes) {
|
||||
parts.push(current);
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
parts.push(current);
|
||||
|
||||
return parts.map(escapeSnowflakeIdentifier).join('.');
|
||||
}
|
||||
|
||||
export async function execute(
|
||||
conn: snowflake.Connection,
|
||||
sqlText: string,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { getResolvables } from '@utils/utilities';
|
|||
import {
|
||||
connect,
|
||||
destroy,
|
||||
escapeSnowflakeIdentifier,
|
||||
escapeSnowflakeObjectIdentifier,
|
||||
execute,
|
||||
getConnectionOptions,
|
||||
type SnowflakeCredential,
|
||||
|
|
@ -215,9 +217,11 @@ export class Snowflake implements INodeType {
|
|||
const table = this.getNodeParameter('table', 0) as string;
|
||||
const columnString = this.getNodeParameter('columns', 0) as string;
|
||||
const columns = columnString.split(',').map((column) => column.trim());
|
||||
const query = `INSERT INTO IDENTIFIER(?)(${columns.map(() => 'IDENTIFIER(?)').join(',')}) VALUES (${columns.map(() => '?').join(',')})`;
|
||||
const quotedTable = escapeSnowflakeObjectIdentifier(table);
|
||||
const quotedColumns = columns.map(escapeSnowflakeIdentifier);
|
||||
const query = `INSERT INTO ${quotedTable} (${quotedColumns.join(',')}) VALUES (${columns.map(() => '?').join(',')})`;
|
||||
const data = this.helpers.copyInputItems(items, columns);
|
||||
const binds = data.map((element) => [table, ...columns, ...Object.values(element)]);
|
||||
const binds = data.map((element) => [...Object.values(element)]);
|
||||
await execute(connection, query, binds as unknown as snowflake.InsertBinds);
|
||||
data.forEach((d, i) => {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
|
|
@ -242,15 +246,18 @@ export class Snowflake implements INodeType {
|
|||
columns.unshift(updateKey);
|
||||
}
|
||||
|
||||
const query = `UPDATE IDENTIFIER(?) SET ${columns.map(() => 'IDENTIFIER(?) = ?').join(',')} WHERE IDENTIFIER(?) = ?;`;
|
||||
const quotedTable = escapeSnowflakeObjectIdentifier(table);
|
||||
const quotedColumns = columns.map(escapeSnowflakeIdentifier);
|
||||
const quotedUpdateKey = escapeSnowflakeIdentifier(updateKey);
|
||||
const query = `UPDATE ${quotedTable} SET ${quotedColumns.map((col) => `${col} = ?`).join(',')} WHERE ${quotedUpdateKey} = ?;`;
|
||||
const data = this.helpers.copyInputItems(items, columns);
|
||||
const binds = data.map((element) => {
|
||||
const values = Object.values(element);
|
||||
const rowBinds: unknown[] = [table];
|
||||
columns.forEach((col, idx) => {
|
||||
rowBinds.push(col, values[idx]);
|
||||
const rowBinds: unknown[] = [];
|
||||
columns.forEach((_col, idx) => {
|
||||
rowBinds.push(values[idx]);
|
||||
});
|
||||
rowBinds.push(updateKey, element[updateKey]);
|
||||
rowBinds.push(element[updateKey]);
|
||||
return rowBinds;
|
||||
});
|
||||
for (let i = 0; i < binds.length; i++) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,33 @@
|
|||
import crypto from 'crypto';
|
||||
|
||||
import { getConnectionOptions } from '../GenericFunctions';
|
||||
import { escapeSnowflakeObjectIdentifier, getConnectionOptions } from '../GenericFunctions';
|
||||
|
||||
jest.mock('crypto');
|
||||
|
||||
describe('escapeSnowflakeObjectIdentifier', () => {
|
||||
it('quotes a single-part identifier', () => {
|
||||
expect(escapeSnowflakeObjectIdentifier('orders')).toBe('"ORDERS"');
|
||||
});
|
||||
|
||||
it('quotes each segment of a two-part identifier', () => {
|
||||
expect(escapeSnowflakeObjectIdentifier('schema.orders')).toBe('"SCHEMA"."ORDERS"');
|
||||
});
|
||||
|
||||
it('quotes each segment of a three-part identifier', () => {
|
||||
expect(escapeSnowflakeObjectIdentifier('db.schema.orders')).toBe('"DB"."SCHEMA"."ORDERS"');
|
||||
});
|
||||
|
||||
it('preserves case for pre-quoted identifiers', () => {
|
||||
expect(escapeSnowflakeObjectIdentifier('"myTable"')).toBe('"myTable"');
|
||||
});
|
||||
|
||||
it('does not split on dots inside quoted segments', () => {
|
||||
expect(escapeSnowflakeObjectIdentifier('"my.schema"."my.table"')).toBe(
|
||||
'"my.schema"."my.table"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConnectionOptions', () => {
|
||||
const commonOptions = {
|
||||
account: 'test-account',
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ describe('Test Snowflake, insert - parameter binding', () => {
|
|||
expect(mockExecute).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sqlText: 'INSERT INTO IDENTIFIER(?)(IDENTIFIER(?),IDENTIFIER(?)) VALUES (?,?)',
|
||||
binds: [['orders', 'name', 'status', 'Alice', 'active']],
|
||||
sqlText: 'INSERT INTO "ORDERS" ("NAME","STATUS") VALUES (?,?)',
|
||||
binds: [['Alice', 'active']],
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,12 +39,11 @@ describe('Test Snowflake, update - parameter binding', () => {
|
|||
// UPDATE executes one query per row; one input row → one call
|
||||
expect(mockExecute).toHaveBeenCalledTimes(1);
|
||||
// Columns list is ["id", "status"] (updateKey "id" prepended since not in "status")
|
||||
// Binds: [table, col1, val1, col2, val2, updateKey, updateKeyValue]
|
||||
// Binds: [id_value, status_value, updateKey_value]
|
||||
expect(mockExecute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sqlText:
|
||||
'UPDATE IDENTIFIER(?) SET IDENTIFIER(?) = ?,IDENTIFIER(?) = ? WHERE IDENTIFIER(?) = ?;',
|
||||
binds: ['orders', 'id', 1, 'status', 'shipped', 'id', 1],
|
||||
sqlText: 'UPDATE "ORDERS" SET "ID" = ?,"STATUS" = ? WHERE "ID" = ?;',
|
||||
binds: [1, 'shipped', 1],
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-nodes-base",
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.3",
|
||||
"description": "Base nodes of n8n",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
314
pnpm-lock.yaml
314
pnpm-lock.yaml
|
|
@ -202,8 +202,8 @@ catalogs:
|
|||
specifier: ^1.98.0
|
||||
version: 1.98.0
|
||||
simple-git:
|
||||
specifier: 3.32.3
|
||||
version: 3.32.3
|
||||
specifier: 3.36.0
|
||||
version: 3.36.0
|
||||
stream-json:
|
||||
specifier: 1.9.1
|
||||
version: 1.9.1
|
||||
|
|
@ -232,8 +232,8 @@ catalogs:
|
|||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
vm2:
|
||||
specifier: ^3.10.5
|
||||
version: 3.10.5
|
||||
specifier: 3.11.2
|
||||
version: 3.11.2
|
||||
xml2js:
|
||||
specifier: 0.6.2
|
||||
version: 0.6.2
|
||||
|
|
@ -361,7 +361,6 @@ overrides:
|
|||
'@mistralai/mistralai': ^1.10.0
|
||||
'@n8n/typeorm>@sentry/node': ^10.36.0
|
||||
'@types/node': ^20.17.50
|
||||
axios: 1.15.0
|
||||
chokidar: 4.0.3
|
||||
esbuild: ^0.25.0
|
||||
expr-eval@2.0.2: npm:expr-eval-fork@3.0.0
|
||||
|
|
@ -398,7 +397,6 @@ overrides:
|
|||
undici@6: ^6.24.0
|
||||
undici@7: ^7.24.0
|
||||
tar: ^7.5.11
|
||||
hono: 4.12.14
|
||||
ajv@6: 6.14.0
|
||||
ajv@7: 8.18.0
|
||||
ajv@8: 8.18.0
|
||||
|
|
@ -425,7 +423,9 @@ overrides:
|
|||
'@xmldom/xmldom': 0.8.13
|
||||
langsmith: 0.5.19
|
||||
yaml@<=2.8.3: 2.8.3
|
||||
fast-xml-parser: 5.7.0
|
||||
hono: 4.12.16
|
||||
axios: 1.16.0
|
||||
fast-xml-parser: 5.7.2
|
||||
|
||||
patchedDependencies:
|
||||
'@lezer/highlight':
|
||||
|
|
@ -530,8 +530,8 @@ importers:
|
|||
specifier: ^25.0.1
|
||||
version: 25.0.1
|
||||
nock:
|
||||
specifier: ^14.0.13
|
||||
version: 14.0.13
|
||||
specifier: ^14.0.14
|
||||
version: 14.0.14
|
||||
nodemon:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
|
|
@ -644,7 +644,7 @@ importers:
|
|||
version: 1.0.27(@langchain/core@1.1.41(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.34.0(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)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(cheerio@1.0.0)(openai@6.34.0(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))
|
||||
'@langchain/community':
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.27(9d1780938e2a3bdbdbc6aaf41ad92004)
|
||||
version: 1.1.27(fc62cbc93d74cace03ba310d8e53131b)
|
||||
'@langchain/core':
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.41(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.34.0(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))
|
||||
|
|
@ -692,8 +692,8 @@ importers:
|
|||
specifier: 'catalog:'
|
||||
version: 3.0.1
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
jest-mock-extended:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(jest@29.7.0(@types/node@20.19.21)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@20.19.21)(typescript@6.0.2)))(typescript@6.0.2)
|
||||
|
|
@ -927,8 +927,8 @@ importers:
|
|||
specifier: 4.0.7
|
||||
version: 4.0.7
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
dotenv:
|
||||
specifier: 17.2.3
|
||||
version: 17.2.3
|
||||
|
|
@ -981,8 +981,8 @@ importers:
|
|||
packages/@n8n/client-oauth2:
|
||||
dependencies:
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
devDependencies:
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
|
|
@ -1961,7 +1961,7 @@ importers:
|
|||
version: 1.0.1(@langchain/core@1.1.41(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.34.0(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)))(encoding@0.1.13)
|
||||
'@langchain/community':
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.27(7d9a0518aab37da79d9f494cd5b9494b)
|
||||
version: 1.1.27(f2f54e7010350c3b50a1b81272c39ebc)
|
||||
'@langchain/core':
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.41(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.34.0(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))
|
||||
|
|
@ -2150,7 +2150,7 @@ importers:
|
|||
version: 3.0.3
|
||||
vm2:
|
||||
specifier: 'catalog:'
|
||||
version: 3.10.5
|
||||
version: 3.11.2
|
||||
weaviate-client:
|
||||
specifier: 3.9.0
|
||||
version: 3.9.0(encoding@0.1.13)
|
||||
|
|
@ -2226,8 +2226,8 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../eslint-plugin-community-nodes
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 9.29.0(jiti@2.6.1)
|
||||
|
|
@ -2601,8 +2601,8 @@ importers:
|
|||
specifier: 1.11.0
|
||||
version: 1.11.0
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
bcryptjs:
|
||||
specifier: 2.4.3
|
||||
version: 2.4.3
|
||||
|
|
@ -2779,7 +2779,7 @@ importers:
|
|||
version: 0.8.5
|
||||
simple-git:
|
||||
specifier: 'catalog:'
|
||||
version: 3.32.3
|
||||
version: 3.36.0
|
||||
source-map-support:
|
||||
specifier: 0.5.21
|
||||
version: 0.5.21
|
||||
|
|
@ -2962,8 +2962,8 @@ importers:
|
|||
specifier: catalog:sentry
|
||||
version: 10.36.0
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
callsites:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0
|
||||
|
|
@ -3435,8 +3435,8 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../../@n8n/utils
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
flatted:
|
||||
specifier: 3.4.2
|
||||
version: 3.4.2
|
||||
|
|
@ -3762,8 +3762,8 @@ importers:
|
|||
specifier: 1.1.4
|
||||
version: 1.1.4
|
||||
axios:
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
specifier: 1.16.0
|
||||
version: 1.16.0
|
||||
bowser:
|
||||
specifier: 2.11.0
|
||||
version: 2.11.0
|
||||
|
|
@ -4254,7 +4254,7 @@ importers:
|
|||
version: 2.1.0
|
||||
simple-git:
|
||||
specifier: 'catalog:'
|
||||
version: 3.32.3
|
||||
version: 3.36.0
|
||||
snowflake-sdk:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0(asn1.js@5.4.1)(encoding@0.1.13)
|
||||
|
|
@ -4272,7 +4272,7 @@ importers:
|
|||
version: 10.0.0
|
||||
vm2:
|
||||
specifier: 'catalog:'
|
||||
version: 3.10.5
|
||||
version: 3.11.2
|
||||
xlsx:
|
||||
specifier: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz
|
||||
version: https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz
|
||||
|
|
@ -6914,7 +6914,7 @@ packages:
|
|||
resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
hono: 4.12.14
|
||||
hono: 4.12.16
|
||||
|
||||
'@huggingface/inference@4.0.5':
|
||||
resolution: {integrity: sha512-/Qc45BGrN+FBA3JfdeoHfafxfNShH/dxvOsXbBdcxyxIRIYOyefeiXSlShZGVCaiqYpm+10na28D0YtvjKPTlw==}
|
||||
|
|
@ -7467,7 +7467,7 @@ packages:
|
|||
duck-duck-scrape: ^2.2.5
|
||||
epub2: ^3.0.1
|
||||
faiss-node: '*'
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
firebase-admin: ^13.6.1
|
||||
google-auth-library: '*'
|
||||
googleapis: '*'
|
||||
|
|
@ -8546,12 +8546,6 @@ packages:
|
|||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/context-async-hooks@2.5.0':
|
||||
resolution: {integrity: sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/context-async-hooks@2.6.0':
|
||||
resolution: {integrity: sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
|
|
@ -10148,6 +10142,12 @@ packages:
|
|||
pinia:
|
||||
optional: true
|
||||
|
||||
'@simple-git/args-pathspec@1.0.3':
|
||||
resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==}
|
||||
|
||||
'@simple-git/argv-parser@1.1.1':
|
||||
resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==}
|
||||
|
||||
'@sinclair/typebox@0.25.21':
|
||||
resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==}
|
||||
|
||||
|
|
@ -12638,10 +12638,10 @@ packages:
|
|||
axios-retry@4.5.0:
|
||||
resolution: {integrity: sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==}
|
||||
peerDependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
|
||||
axios@1.15.0:
|
||||
resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==}
|
||||
axios@1.16.0:
|
||||
resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==}
|
||||
|
||||
b4a@1.6.7:
|
||||
resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
|
||||
|
|
@ -14831,10 +14831,6 @@ packages:
|
|||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
execa@9.6.0:
|
||||
resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==}
|
||||
engines: {node: ^18.19.0 || >=20.5.0}
|
||||
|
||||
execa@9.6.1:
|
||||
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
|
||||
engines: {node: ^18.19.0 || >=20.5.0}
|
||||
|
|
@ -14989,8 +14985,8 @@ packages:
|
|||
fast-xml-builder@1.1.5:
|
||||
resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==}
|
||||
|
||||
fast-xml-parser@5.7.0:
|
||||
resolution: {integrity: sha512-MTcrUoRQ1GSQ9iG3QJzBGquYYYeA7piZaJoIWbPFGbRn6Jj6z7xgoAyi4DrZX4y2ZIQQBF59gc/zmvvejjgoFQ==}
|
||||
fast-xml-parser@5.7.2:
|
||||
resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==}
|
||||
hasBin: true
|
||||
|
||||
fastest-levenshtein@1.0.16:
|
||||
|
|
@ -15615,7 +15611,7 @@ packages:
|
|||
'@standard-community/standard-json': ^0.3.5
|
||||
'@standard-community/standard-openapi': ^0.2.9
|
||||
'@types/json-schema': ^7.0.15
|
||||
hono: 4.12.14
|
||||
hono: 4.12.16
|
||||
openapi-types: ^12.1.3
|
||||
peerDependenciesMeta:
|
||||
'@hono/standard-validator':
|
||||
|
|
@ -15623,8 +15619,8 @@ packages:
|
|||
hono:
|
||||
optional: true
|
||||
|
||||
hono@4.12.14:
|
||||
resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==}
|
||||
hono@4.12.16:
|
||||
resolution: {integrity: sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
hookable@5.5.3:
|
||||
|
|
@ -18092,8 +18088,8 @@ packages:
|
|||
no-case@3.0.4:
|
||||
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||
|
||||
nock@14.0.13:
|
||||
resolution: {integrity: sha512-SCPsQmGVNY8h1rfS3aU0MzOGYY+wKIFukHEsoSIwPRCYocZkya7MFIlWIEYPWQZj+Gaksg6EyUaY255ZDqpQuA==}
|
||||
nock@14.0.14:
|
||||
resolution: {integrity: sha512-PKk7tex0O3RRXUZC5XDKJ9yM3rYRPS13myduT85VIIYDBnib42Fpxoe6KxRSzqB4iL2NDxkcJ2yiskZ18hGLEQ==}
|
||||
engines: {node: '>=18.20.0 <20 || >=20.12.1'}
|
||||
|
||||
node-abi@3.75.0:
|
||||
|
|
@ -18794,10 +18790,6 @@ packages:
|
|||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
path-scurry@2.0.2:
|
||||
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
|
@ -19393,7 +19385,6 @@ packages:
|
|||
resolution: {integrity: sha512-gv6vLGcmAOg96/fgo3d9tvA4dJNZL3fMyBqVRrGxQ+Q/o4k9QzbJ3NQF9cOO/71wRodoXhaPgphvMFU68qVAJQ==}
|
||||
deprecated: |-
|
||||
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
|
||||
|
||||
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
|
||||
|
||||
qrcode.vue@3.3.4:
|
||||
|
|
@ -19762,7 +19753,7 @@ packages:
|
|||
resolution: {integrity: sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==}
|
||||
engines: {node: '>=10.7.0'}
|
||||
peerDependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
|
||||
retry-request@7.0.2:
|
||||
resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==}
|
||||
|
|
@ -20267,8 +20258,8 @@ packages:
|
|||
simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
|
||||
simple-git@3.32.3:
|
||||
resolution: {integrity: sha512-56a5oxFdWlsGygOXHWrG+xjj5w9ZIt2uQbzqiIGdR/6i5iococ7WQ/bNPzWxCJdEUGUCmyMH0t9zMpRJTaKxmw==}
|
||||
simple-git@3.36.0:
|
||||
resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==}
|
||||
|
||||
simple-lru-cache@0.0.2:
|
||||
resolution: {integrity: sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw==}
|
||||
|
|
@ -21574,6 +21565,7 @@ packages:
|
|||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
uuid@11.1.0:
|
||||
|
|
@ -21591,6 +21583,7 @@ packages:
|
|||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
v-code-diff@1.13.1:
|
||||
|
|
@ -21772,8 +21765,8 @@ packages:
|
|||
vm-browserify@1.1.2:
|
||||
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
|
||||
|
||||
vm2@3.10.5:
|
||||
resolution: {integrity: sha512-3P/2QDccVFBcujfCOeP8vVNuGfuBJHEuvGR8eMmI10p/iwLL2UwF5PDaNaoOS2pRGQEDmJRyeEcc8kmm2Z59RA==}
|
||||
vm2@3.11.2:
|
||||
resolution: {integrity: sha512-hnsYAgBSAgwwPM/Gq66gMmUY0VlY9mKC8nvVRAZiJp+tYxF4sfNRlZymP8uqzIUK2U/7+SVZ/H8p7USxNHLlZA==}
|
||||
engines: {node: '>=6.0'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -21815,8 +21808,8 @@ packages:
|
|||
vue-component-type-helpers@2.2.12:
|
||||
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||
|
||||
vue-component-type-helpers@3.2.7:
|
||||
resolution: {integrity: sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==}
|
||||
vue-component-type-helpers@3.2.8:
|
||||
resolution: {integrity: sha512-9689efAXhN/EV86plgkL/XFiJSXhGtWPG6JDboZ+QnjlUWUUQrQ0ILKQtw4iQsuwIwu5k6Aw+JnehDe7161e7A==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
|
|
@ -22382,7 +22375,7 @@ snapshots:
|
|||
|
||||
'@1password/connect@1.4.2':
|
||||
dependencies:
|
||||
axios: 1.15.0(debug@4.4.3)
|
||||
axios: 1.16.0(debug@4.4.3)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
lodash.clonedeep: 4.5.0
|
||||
slugify: 1.6.6
|
||||
|
|
@ -23247,7 +23240,7 @@ snapshots:
|
|||
'@smithy/smithy-client': 4.9.8
|
||||
'@smithy/types': 4.9.0
|
||||
'@smithy/util-middleware': 4.2.5
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/core@3.916.0':
|
||||
|
|
@ -24139,13 +24132,13 @@ snapshots:
|
|||
'@aws-sdk/xml-builder@3.914.0':
|
||||
dependencies:
|
||||
'@smithy/types': 4.13.1
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/xml-builder@3.930.0':
|
||||
dependencies:
|
||||
'@smithy/types': 4.13.1
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws/lambda-invoke-store@0.0.1': {}
|
||||
|
|
@ -24280,7 +24273,7 @@ snapshots:
|
|||
|
||||
'@azure/core-xml@1.4.5':
|
||||
dependencies:
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@azure/identity@4.13.0':
|
||||
|
|
@ -24402,6 +24395,7 @@ snapshots:
|
|||
'@babel/helper-validator-identifier': 7.28.5
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
optional: true
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
|
|
@ -25817,7 +25811,7 @@ snapshots:
|
|||
|
||||
'@codspeed/core@5.2.0':
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
find-up: 6.3.0
|
||||
form-data: 4.0.4
|
||||
node-gyp-build: 4.8.4
|
||||
|
|
@ -25876,19 +25870,19 @@ snapshots:
|
|||
|
||||
'@currents/playwright@1.15.3(bufferutil@4.0.9)(magicast@0.3.5)(utf-8-validate@5.0.10)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
'@babel/code-frame': 7.29.0
|
||||
'@commander-js/extra-typings': 12.1.0(commander@12.1.0)
|
||||
'@currents/commit-info': 1.0.1-beta.0
|
||||
async-retry: 1.3.3
|
||||
axios: 1.15.0(debug@4.4.3)
|
||||
axios-retry: 4.5.0(axios@1.15.0)
|
||||
axios: 1.16.0(debug@4.4.3)
|
||||
axios-retry: 4.5.0(axios@1.16.0)
|
||||
c12: 1.11.2(magicast@0.3.5)
|
||||
chalk: 4.1.2
|
||||
commander: 12.1.0
|
||||
date-fns: 2.30.0
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
dotenv: 16.6.1
|
||||
execa: 9.6.0
|
||||
execa: 9.6.1
|
||||
getos: 3.2.1
|
||||
https-proxy-agent: 7.0.6
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
|
|
@ -25931,13 +25925,13 @@ snapshots:
|
|||
|
||||
'@daytonaio/api-client@0.143.0':
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@daytonaio/api-client@0.149.0':
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
|
|
@ -25956,7 +25950,7 @@ snapshots:
|
|||
'@opentelemetry/sdk-node': 0.207.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
busboy: 1.6.0
|
||||
dotenv: 17.2.3
|
||||
expand-tilde: 2.0.2
|
||||
|
|
@ -25987,7 +25981,7 @@ snapshots:
|
|||
'@opentelemetry/sdk-node': 0.207.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
busboy: 1.6.0
|
||||
dotenv: 17.2.3
|
||||
expand-tilde: 2.0.2
|
||||
|
|
@ -26005,13 +25999,13 @@ snapshots:
|
|||
|
||||
'@daytonaio/toolbox-api-client@0.143.0':
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@daytonaio/toolbox-api-client@0.149.0':
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
|
|
@ -26382,7 +26376,7 @@ snapshots:
|
|||
abort-controller: 3.0.0
|
||||
async-retry: 1.3.3
|
||||
duplexify: 4.1.3
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
gaxios: 7.1.3
|
||||
google-auth-library: 9.15.1
|
||||
html-entities: 2.5.2
|
||||
|
|
@ -26436,9 +26430,9 @@ snapshots:
|
|||
protobufjs: 7.5.5
|
||||
yargs: 17.7.2
|
||||
|
||||
'@hono/node-server@1.19.13(hono@4.12.14)':
|
||||
'@hono/node-server@1.19.13(hono@4.12.16)':
|
||||
dependencies:
|
||||
hono: 4.12.14
|
||||
hono: 4.12.16
|
||||
|
||||
'@huggingface/inference@4.0.5':
|
||||
dependencies:
|
||||
|
|
@ -27121,7 +27115,7 @@ snapshots:
|
|||
- aws-crt
|
||||
- encoding
|
||||
|
||||
'@langchain/community@1.1.27(7d9a0518aab37da79d9f494cd5b9494b)':
|
||||
'@langchain/community@1.1.27(f2f54e7010350c3b50a1b81272c39ebc)':
|
||||
dependencies:
|
||||
'@browserbasehq/stagehand': 1.14.0(@playwright/test@1.58.0)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@17.3.1)(encoding@0.1.13)(openai@6.34.0(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
|
||||
|
|
@ -27161,7 +27155,7 @@ snapshots:
|
|||
crypto-js: 4.2.0
|
||||
d3-dsv: 2.0.0
|
||||
epub2: 3.0.2(ts-toolbelt@9.6.0)
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
google-auth-library: 10.1.0
|
||||
html-to-text: 9.0.5
|
||||
ignore: 5.2.4
|
||||
|
|
@ -27183,7 +27177,7 @@ snapshots:
|
|||
- '@opentelemetry/sdk-trace-base'
|
||||
- peggy
|
||||
|
||||
'@langchain/community@1.1.27(9d1780938e2a3bdbdbc6aaf41ad92004)':
|
||||
'@langchain/community@1.1.27(fc62cbc93d74cace03ba310d8e53131b)':
|
||||
dependencies:
|
||||
'@browserbasehq/stagehand': 1.14.0(@playwright/test@1.58.0)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@17.3.1)(encoding@0.1.13)(openai@6.34.0(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
|
||||
|
|
@ -27214,7 +27208,7 @@ snapshots:
|
|||
chromadb: 3.2.0
|
||||
crypto-js: 4.2.0
|
||||
epub2: 3.0.2(ts-toolbelt@9.6.0)
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
google-auth-library: 10.1.0
|
||||
html-to-text: 9.0.5
|
||||
ignore: 7.0.5
|
||||
|
|
@ -27586,8 +27580,8 @@ snapshots:
|
|||
dotenv: 17.3.1
|
||||
execa: 9.6.1
|
||||
gray-matter: 4.0.3
|
||||
hono: 4.12.14
|
||||
hono-openapi: 1.3.0(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.67))(@types/json-schema@7.0.15)(hono@4.12.14)(openapi-types@12.1.3)
|
||||
hono: 4.12.16
|
||||
hono-openapi: 1.3.0(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.67))(@types/json-schema@7.0.15)(hono@4.12.16)(openapi-types@12.1.3)
|
||||
ignore: 7.0.5
|
||||
js-tiktoken: 1.0.21
|
||||
json-schema: 0.4.0
|
||||
|
|
@ -27788,7 +27782,7 @@ snapshots:
|
|||
'@microsoft/agents-a365-runtime': 0.1.0-preview.113
|
||||
'@microsoft/agents-a365-tooling': 0.1.0-preview.113(zod@3.25.67)
|
||||
'@microsoft/agents-hosting': 1.2.3
|
||||
hono: 4.12.14
|
||||
hono: 4.12.16
|
||||
langchain: 1.2.30(@langchain/core@1.1.41(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.34.0(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)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.34.0(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vue@3.5.26(typescript@6.0.2))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod-to-json-schema@3.23.3(zod@3.25.67))
|
||||
uuid: 9.0.1
|
||||
optionalDependencies:
|
||||
|
|
@ -27816,7 +27810,7 @@ snapshots:
|
|||
'@microsoft/agents-hosting': 1.2.3
|
||||
'@modelcontextprotocol/sdk': 1.26.0(zod@3.25.67)
|
||||
express: 5.2.1
|
||||
hono: 4.12.14
|
||||
hono: 4.12.16
|
||||
transitivePeerDependencies:
|
||||
- '@cfworker/json-schema'
|
||||
- debug
|
||||
|
|
@ -27836,7 +27830,7 @@ snapshots:
|
|||
'@azure/core-auth': 1.10.1
|
||||
'@azure/msal-node': 3.8.4
|
||||
'@microsoft/agents-activity': 1.2.3
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
jsonwebtoken: 9.0.3
|
||||
jwks-rsa: 3.2.2
|
||||
object-path: 0.11.8
|
||||
|
|
@ -27895,7 +27889,7 @@ snapshots:
|
|||
|
||||
'@modelcontextprotocol/sdk@1.26.0(zod@3.25.67)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.13(hono@4.12.14)
|
||||
'@hono/node-server': 1.19.13(hono@4.12.16)
|
||||
ajv: 8.18.0
|
||||
ajv-formats: 3.0.1(ajv@8.18.0)
|
||||
content-type: 1.0.5
|
||||
|
|
@ -27905,7 +27899,7 @@ snapshots:
|
|||
eventsource-parser: 3.0.6
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.2.2(express@5.2.1)
|
||||
hono: 4.12.14
|
||||
hono: 4.12.16
|
||||
jose: 6.1.3
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.0(patch_hash=651e785d0b7bbf5be9210e1e895c39a16dc3ce8a5a3843b4819565fb6e175b90)
|
||||
|
|
@ -27917,7 +27911,7 @@ snapshots:
|
|||
|
||||
'@modelcontextprotocol/sdk@1.28.0(zod@3.25.67)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.13(hono@4.12.14)
|
||||
'@hono/node-server': 1.19.13(hono@4.12.16)
|
||||
ajv: 8.18.0
|
||||
ajv-formats: 3.0.1(ajv@8.18.0)
|
||||
content-type: 1.0.5
|
||||
|
|
@ -27927,7 +27921,7 @@ snapshots:
|
|||
eventsource-parser: 3.0.6
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.2.2(express@5.2.1)
|
||||
hono: 4.12.14
|
||||
hono: 4.12.16
|
||||
jose: 6.2.2
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.0(patch_hash=651e785d0b7bbf5be9210e1e895c39a16dc3ce8a5a3843b4819565fb6e175b90)
|
||||
|
|
@ -28319,10 +28313,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
||||
'@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
||||
'@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
|
@ -29804,8 +29794,8 @@ snapshots:
|
|||
|
||||
'@rudderstack/rudder-sdk-node@3.0.5':
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios-retry: 4.5.0(axios@1.15.0)
|
||||
axios: 1.16.0
|
||||
axios-retry: 4.5.0(axios@1.16.0)
|
||||
component-type: 2.0.0
|
||||
join-component: 1.1.0
|
||||
lodash.clonedeep: 4.5.0
|
||||
|
|
@ -29960,18 +29950,18 @@ snapshots:
|
|||
|
||||
'@sentry/core@10.36.0': {}
|
||||
|
||||
'@sentry/node-core@10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.210.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)':
|
||||
'@sentry/node-core@10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.210.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)':
|
||||
dependencies:
|
||||
'@apm-js-collab/tracing-hooks': 0.3.1
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
'@sentry/core': 10.36.0
|
||||
'@sentry/opentelemetry': 10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)
|
||||
'@sentry/opentelemetry': 10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)
|
||||
import-in-the-middle: 2.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -29987,8 +29977,8 @@ snapshots:
|
|||
'@sentry/node@10.36.0':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/instrumentation-amqplib': 0.57.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/instrumentation-connect': 0.53.0(@opentelemetry/api@1.9.0)
|
||||
|
|
@ -30017,18 +30007,18 @@ snapshots:
|
|||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
'@prisma/instrumentation': 7.2.0(@opentelemetry/api@1.9.0)
|
||||
'@sentry/core': 10.36.0
|
||||
'@sentry/node-core': 10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.210.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)
|
||||
'@sentry/opentelemetry': 10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)
|
||||
'@sentry/node-core': 10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.210.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)
|
||||
'@sentry/opentelemetry': 10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)
|
||||
import-in-the-middle: 2.0.5
|
||||
minimatch: 9.0.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@sentry/opentelemetry@10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)':
|
||||
'@sentry/opentelemetry@10.36.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
'@sentry/core': 10.36.0
|
||||
|
|
@ -30057,6 +30047,12 @@ snapshots:
|
|||
optionalDependencies:
|
||||
pinia: 2.2.4(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))
|
||||
|
||||
'@simple-git/args-pathspec@1.0.3': {}
|
||||
|
||||
'@simple-git/argv-parser@1.1.1':
|
||||
dependencies:
|
||||
'@simple-git/args-pathspec': 1.0.3
|
||||
|
||||
'@sinclair/typebox@0.25.21': {}
|
||||
|
||||
'@sinclair/typebox@0.27.8': {}
|
||||
|
|
@ -31098,7 +31094,7 @@ snapshots:
|
|||
storybook: 10.1.11(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(utf-8-validate@5.0.10)
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.26(typescript@6.0.2)
|
||||
vue-component-type-helpers: 3.2.7
|
||||
vue-component-type-helpers: 3.2.8
|
||||
|
||||
'@stylistic/eslint-plugin@5.0.0(eslint@9.29.0(jiti@2.6.1))':
|
||||
dependencies:
|
||||
|
|
@ -32520,7 +32516,7 @@ snapshots:
|
|||
'@vue/shared': 3.5.26
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.21
|
||||
postcss: 8.5.6
|
||||
postcss: 8.5.8
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-ssr@3.5.26':
|
||||
|
|
@ -33212,12 +33208,12 @@ snapshots:
|
|||
|
||||
axe-core@4.7.2: {}
|
||||
|
||||
axios-retry@4.5.0(axios@1.15.0):
|
||||
axios-retry@4.5.0(axios@1.16.0):
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
is-retry-allowed: 2.2.0
|
||||
|
||||
axios@1.15.0:
|
||||
axios@1.16.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0(debug@4.4.1)
|
||||
form-data: 4.0.4
|
||||
|
|
@ -33225,7 +33221,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axios@1.15.0(debug@4.4.3):
|
||||
axios@1.16.0(debug@4.4.3):
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0(debug@4.4.3)
|
||||
form-data: 4.0.4
|
||||
|
|
@ -34870,11 +34866,11 @@ snapshots:
|
|||
dependencies:
|
||||
node-source-walk: 7.0.1
|
||||
|
||||
detective-postcss@7.0.1(postcss@8.5.6):
|
||||
detective-postcss@7.0.1(postcss@8.5.8):
|
||||
dependencies:
|
||||
is-url: 1.2.4
|
||||
postcss: 8.5.6
|
||||
postcss-values-parser: 6.0.2(postcss@8.5.6)
|
||||
postcss: 8.5.8
|
||||
postcss-values-parser: 6.0.2(postcss@8.5.8)
|
||||
|
||||
detective-sass@6.0.1:
|
||||
dependencies:
|
||||
|
|
@ -35575,7 +35571,7 @@ snapshots:
|
|||
|
||||
eslint-import-context@0.1.8(unrs-resolver@1.9.2):
|
||||
dependencies:
|
||||
get-tsconfig: 4.10.1
|
||||
get-tsconfig: 4.13.0
|
||||
stable-hash-x: 0.1.1
|
||||
optionalDependencies:
|
||||
unrs-resolver: 1.9.2
|
||||
|
|
@ -35911,21 +35907,6 @@ snapshots:
|
|||
signal-exit: 3.0.7
|
||||
strip-final-newline: 2.0.0
|
||||
|
||||
execa@9.6.0:
|
||||
dependencies:
|
||||
'@sindresorhus/merge-streams': 4.0.0
|
||||
cross-spawn: 7.0.6
|
||||
figures: 6.1.0
|
||||
get-stream: 9.0.1
|
||||
human-signals: 8.0.1
|
||||
is-plain-obj: 4.1.0
|
||||
is-stream: 4.0.1
|
||||
npm-run-path: 6.0.0
|
||||
pretty-ms: 9.2.0
|
||||
signal-exit: 4.1.0
|
||||
strip-final-newline: 4.0.0
|
||||
yoctocolors: 2.1.1
|
||||
|
||||
execa@9.6.1:
|
||||
dependencies:
|
||||
'@sindresorhus/merge-streams': 4.0.0
|
||||
|
|
@ -36209,7 +36190,7 @@ snapshots:
|
|||
dependencies:
|
||||
path-expression-matcher: 1.5.0
|
||||
|
||||
fast-xml-parser@5.7.0:
|
||||
fast-xml-parser@5.7.2:
|
||||
dependencies:
|
||||
'@nodable/entities': 2.1.0
|
||||
fast-xml-builder: 1.1.5
|
||||
|
|
@ -36679,9 +36660,9 @@ snapshots:
|
|||
foreground-child: 3.3.1
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.2.3
|
||||
minipass: 7.1.2
|
||||
minipass: 7.1.3
|
||||
package-json-from-dist: 1.0.0
|
||||
path-scurry: 2.0.1
|
||||
path-scurry: 2.0.2
|
||||
|
||||
glob@13.0.6:
|
||||
dependencies:
|
||||
|
|
@ -36970,16 +36951,16 @@ snapshots:
|
|||
dependencies:
|
||||
parse-passwd: 1.0.0
|
||||
|
||||
hono-openapi@1.3.0(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.67))(@types/json-schema@7.0.15)(hono@4.12.14)(openapi-types@12.1.3):
|
||||
hono-openapi@1.3.0(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.67))(@types/json-schema@7.0.15)(hono@4.12.16)(openapi-types@12.1.3):
|
||||
dependencies:
|
||||
'@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67)
|
||||
'@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.1(zod@3.25.67))(zod@3.25.67))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.67)
|
||||
'@types/json-schema': 7.0.15
|
||||
openapi-types: 12.1.3
|
||||
optionalDependencies:
|
||||
hono: 4.12.14
|
||||
hono: 4.12.16
|
||||
|
||||
hono@4.12.14: {}
|
||||
hono@4.12.16: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
|
|
@ -37174,7 +37155,7 @@ snapshots:
|
|||
'@types/debug': 4.1.12
|
||||
'@types/node': 20.19.21
|
||||
'@types/tough-cookie': 4.0.5
|
||||
axios: 1.15.0(debug@4.4.3)
|
||||
axios: 1.16.0(debug@4.4.3)
|
||||
camelcase: 6.3.0
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
dotenv: 16.6.1
|
||||
|
|
@ -37184,7 +37165,7 @@ snapshots:
|
|||
isstream: 0.1.2
|
||||
jsonwebtoken: 9.0.3
|
||||
mime-types: 2.1.35
|
||||
retry-axios: 2.6.0(axios@1.15.0)
|
||||
retry-axios: 2.6.0(axios@1.16.0)
|
||||
tough-cookie: 4.1.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -37288,7 +37269,7 @@ snapshots:
|
|||
|
||||
infisical-node@1.3.0:
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
dotenv: 16.6.1
|
||||
tweetnacl: 1.0.3
|
||||
tweetnacl-util: 0.15.1
|
||||
|
|
@ -40301,7 +40282,7 @@ snapshots:
|
|||
lower-case: 2.0.2
|
||||
tslib: 2.8.1
|
||||
|
||||
nock@14.0.13:
|
||||
nock@14.0.14:
|
||||
dependencies:
|
||||
'@mswjs/interceptors': 0.41.6
|
||||
json-stringify-safe: 5.0.1
|
||||
|
|
@ -41107,11 +41088,6 @@ snapshots:
|
|||
lru-cache: 10.2.2
|
||||
minipass: 7.1.3
|
||||
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 11.2.7
|
||||
minipass: 7.1.3
|
||||
|
||||
path-scurry@2.0.2:
|
||||
dependencies:
|
||||
lru-cache: 11.2.7
|
||||
|
|
@ -41401,11 +41377,11 @@ snapshots:
|
|||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss-values-parser@6.0.2(postcss@8.5.6):
|
||||
postcss-values-parser@6.0.2(postcss@8.5.8):
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
is-url-superb: 4.0.0
|
||||
postcss: 8.5.6
|
||||
postcss: 8.5.8
|
||||
quote-unquote: 1.0.0
|
||||
|
||||
postcss@8.4.31:
|
||||
|
|
@ -41444,7 +41420,7 @@ snapshots:
|
|||
|
||||
posthog-node@3.2.1:
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
rusha: 0.8.14
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
|
@ -41476,7 +41452,7 @@ snapshots:
|
|||
detective-amd: 6.0.1
|
||||
detective-cjs: 6.0.1
|
||||
detective-es6: 5.0.1
|
||||
detective-postcss: 7.0.1(postcss@8.5.6)
|
||||
detective-postcss: 7.0.1(postcss@8.5.8)
|
||||
detective-sass: 6.0.1
|
||||
detective-scss: 5.0.1
|
||||
detective-stylus: 5.0.1
|
||||
|
|
@ -41484,7 +41460,7 @@ snapshots:
|
|||
detective-vue2: 2.2.0(typescript@6.0.2)
|
||||
module-definition: 6.0.1
|
||||
node-source-walk: 7.0.1
|
||||
postcss: 8.5.6
|
||||
postcss: 8.5.8
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -41692,7 +41668,7 @@ snapshots:
|
|||
jstransformer: 1.0.0
|
||||
pug-error: 2.1.0
|
||||
pug-walk: 2.0.0
|
||||
resolve: 1.22.10
|
||||
resolve: 1.22.11
|
||||
|
||||
pug-lexer@5.0.1:
|
||||
dependencies:
|
||||
|
|
@ -42250,9 +42226,9 @@ snapshots:
|
|||
|
||||
retimer@3.0.0: {}
|
||||
|
||||
retry-axios@2.6.0(axios@1.15.0):
|
||||
retry-axios@2.6.0(axios@1.16.0):
|
||||
dependencies:
|
||||
axios: 1.15.0
|
||||
axios: 1.16.0
|
||||
|
||||
retry-request@7.0.2(encoding@0.1.13):
|
||||
dependencies:
|
||||
|
|
@ -42875,10 +42851,12 @@ snapshots:
|
|||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
|
||||
simple-git@3.32.3:
|
||||
simple-git@3.36.0:
|
||||
dependencies:
|
||||
'@kwsites/file-exists': 1.1.1
|
||||
'@kwsites/promise-deferred': 1.1.1
|
||||
'@simple-git/args-pathspec': 1.0.3
|
||||
'@simple-git/argv-parser': 1.1.1
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -42962,14 +42940,14 @@ 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.15.0
|
||||
axios: 1.16.0
|
||||
big-integer: 1.6.52
|
||||
bignumber.js: 9.1.2
|
||||
binascii: 0.0.2
|
||||
bn.js: 5.2.3
|
||||
browser-request: 0.3.3
|
||||
expand-tilde: 2.0.2
|
||||
fast-xml-parser: 5.7.0
|
||||
fast-xml-parser: 5.7.2
|
||||
fastest-levenshtein: 1.0.16
|
||||
generic-pool: 3.9.0
|
||||
glob: 10.5.0
|
||||
|
|
@ -44715,9 +44693,9 @@ snapshots:
|
|||
|
||||
vm-browserify@1.1.2: {}
|
||||
|
||||
vm2@3.10.5:
|
||||
vm2@3.11.2:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn: 8.16.0
|
||||
acorn-walk: 8.3.4
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
|
@ -44753,7 +44731,7 @@ snapshots:
|
|||
|
||||
vue-component-type-helpers@2.2.12: {}
|
||||
|
||||
vue-component-type-helpers@3.2.7: {}
|
||||
vue-component-type-helpers@3.2.8: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.26(typescript@6.0.2)):
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ catalog:
|
|||
'@types/uuid': ^10.0.0
|
||||
'@types/xml2js': ^0.4.14
|
||||
'@vitest/coverage-v8': 4.1.1
|
||||
axios: 1.15.1
|
||||
axios: 1.16.0
|
||||
basic-auth: 2.0.1
|
||||
callsites: 3.1.0
|
||||
chokidar: 4.0.3
|
||||
|
|
@ -89,7 +89,7 @@ catalog:
|
|||
reflect-metadata: 0.2.2
|
||||
rimraf: 6.0.1
|
||||
sass-embedded: ^1.98.0
|
||||
simple-git: 3.32.3
|
||||
simple-git: 3.36.0
|
||||
stream-json: 1.9.1
|
||||
testcontainers: ^11.13.0
|
||||
ts-morph: ^27.0.2
|
||||
|
|
@ -101,7 +101,7 @@ catalog:
|
|||
vite-plugin-dts: ^4.5.4
|
||||
vitest: ^4.1.1
|
||||
vitest-mock-extended: ^3.1.0
|
||||
vm2: ^3.10.5
|
||||
vm2: 3.11.2
|
||||
xml2js: 0.6.2
|
||||
xss: 1.0.15
|
||||
yaml: 2.8.3
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user