refactor(core): Move S3 and Azure clients to @n8n/blob-storage (#34567)

This commit is contained in:
Iván Ovejero
2026-07-21 11:10:06 +02:00
committed by GitHub
parent 09018b9dba
commit 2f6bb60b14
36 changed files with 371 additions and 117 deletions
+8
View File
@@ -0,0 +1,8 @@
# @n8n/blob-storage
Blob storage layer for n8n. Hosts the external-storage clients used to persist blobs (execution data, binary data) outside the database:
- `ObjectStoreService` (`@n8n/blob-storage/object-store`): S3-compatible object storage
- `AzureBlobService` (`@n8n/blob-storage/azure-blob`): Azure Blob Storage
The root entrypoint is free of cloud SDK imports: it exposes only types, configs, and stream utilities. The services live behind the subpath exports above and are meant to be loaded with `await import()` so the S3/Azure SDKs are pulled in only when external storage is configured.
@@ -0,0 +1,49 @@
import { defineConfig } from 'eslint/config';
import { baseConfig } from '@n8n/eslint-config/base';
export default defineConfig(
baseConfig,
{
// Relax type-aware unsafe rules for untyped mock plumbing, mirroring n8n-core
files: ['**/__tests__/**/*.ts'],
rules: {
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
},
},
{
rules: {
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
/**
* This package is full of AWS SDK params (`Bucket`, `Key`, `Delete`) and
* HTTP header maps (`content-type`, `x-amz-meta-filename`).
*/
'@typescript-eslint/naming-convention': [
'error',
{ selector: 'default', format: ['camelCase'] },
{ selector: 'import', format: ['camelCase', 'PascalCase'] },
{
selector: 'variable',
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble',
},
{
selector: 'property',
format: ['camelCase', 'snake_case', 'UPPER_CASE'],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble',
},
{ selector: 'typeLike', format: ['PascalCase'] },
{
selector: ['method', 'function', 'parameter'],
format: ['camelCase'],
leadingUnderscore: 'allowSingleOrDouble',
},
{ selector: ['objectLiteralProperty', 'typeProperty'], format: null },
],
},
},
);
+67
View File
@@ -0,0 +1,67 @@
{
"name": "@n8n/blob-storage",
"version": "1.0.0",
"description": "Blob storage layer for n8n, with filesystem, S3, and Azure Blob backends",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"build:unchecked": "tsc -p tsconfig.build.json --noCheck",
"format": "biome format --write .",
"format:check": "biome ci .",
"lint": "eslint . --quiet",
"lint:fix": "eslint . --fix",
"watch": "tsc -p tsconfig.build.json --watch",
"test": "vitest run",
"test:unit": "vitest run",
"test:dev": "vitest --silent=false"
},
"main": "dist/index.js",
"module": "src/index.ts",
"types": "dist/index.d.ts",
"files": [
"dist/**/*"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./object-store": {
"types": "./dist/object-store/index.d.ts",
"default": "./dist/object-store/index.js"
},
"./azure-blob": {
"types": "./dist/azure-blob/index.d.ts",
"default": "./dist/azure-blob/index.js"
}
},
"dependencies": {
"@aws-sdk/client-s3": "3.808.0",
"@azure/identity": "catalog:",
"@azure/storage-blob": "catalog:",
"@n8n/backend-common": "workspace:*",
"@n8n/backend-network": "workspace:*",
"@n8n/config": "workspace:*",
"@n8n/di": "workspace:*",
"@n8n/utils": "workspace:*",
"lodash": "catalog:",
"n8n-workflow": "workspace:*",
"reflect-metadata": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@n8n/eslint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@types/lodash": "catalog:",
"@vitest/coverage-v8": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:typescript",
"vite": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -1,9 +1,9 @@
import { UnexpectedError } from 'n8n-workflow';
import { Readable } from 'node:stream';
import { createFixedSizeChunker } from '@/binary-data/utils';
import { createFixedSizeChunker } from '../stream-utils';
describe('BinaryData/utils', () => {
describe('stream-utils', () => {
describe('createFixedSizeChunker', () => {
const drain = async (source: Readable): Promise<Buffer[]> => {
return await new Promise((resolve, reject) => {
@@ -8,8 +8,8 @@ import { UnexpectedError, UserError } from 'n8n-workflow';
import { PassThrough, Readable, pipeline } from 'node:stream';
import { AzureBlobConfig } from './azure-blob.config';
import type { BinaryData } from '../types';
import { createFixedSizeChunker } from '../utils';
import { createFixedSizeChunker } from '../stream-utils';
import type { BlobMetadata, PreWriteBlobMetadata } from '../types';
@Service()
export class AzureBlobService {
@@ -71,7 +71,7 @@ export class AzureBlobService {
}
}
async put(blobName: string, body: Buffer, metadata: BinaryData.PreWriteMetadata = {}) {
async put(blobName: string, body: Buffer, metadata: PreWriteBlobMetadata = {}) {
try {
await this.containerClient.getBlockBlobClient(blobName).uploadData(body, {
blobHTTPHeaders: { blobContentType: metadata.mimeType ?? 'application/octet-stream' },
@@ -139,11 +139,11 @@ export class AzureBlobService {
}
}
async getMetadata(blobName: string): Promise<BinaryData.Metadata> {
async getMetadata(blobName: string): Promise<BlobMetadata> {
try {
const props = await this.containerClient.getBlockBlobClient(blobName).getProperties();
const metadata: BinaryData.Metadata = { fileSize: props.contentLength ?? 0 };
const metadata: BlobMetadata = { fileSize: props.contentLength ?? 0 };
if (props.contentType) metadata.mimeType = props.contentType;
@@ -0,0 +1,2 @@
export { AzureBlobService } from './azure-blob.service.ee';
export { AzureBlobConfig } from './azure-blob.config';
+9
View File
@@ -0,0 +1,9 @@
export type { BlobMetadata, PreWriteBlobMetadata } from './types';
export { createFixedSizeChunker } from './stream-utils';
export { ObjectStoreConfig } from './object-store/object-store.config';
export type { MetadataResponseHeaders } from './object-store/types';
export { AzureBlobConfig } from './azure-blob/azure-blob.config';
// type-only: the service classes stay behind the `./object-store` and `./azure-blob`
// subpath exports so the root entrypoint never loads the S3/Azure SDKs
export type { ObjectStoreService } from './object-store/object-store.service.ee';
export type { AzureBlobService } from './azure-blob/azure-blob.service.ee';
@@ -0,0 +1,3 @@
export { ObjectStoreService } from './object-store.service.ee';
export { ObjectStoreConfig } from './object-store.config';
export type { MetadataResponseHeaders } from './types';
@@ -25,8 +25,8 @@ import { PassThrough, Readable, pipeline } from 'node:stream';
import { ObjectStoreConfig } from './object-store.config';
import type { MetadataResponseHeaders } from './types';
import type { BinaryData } from '../types';
import { createFixedSizeChunker } from '../utils';
import { createFixedSizeChunker } from '../stream-utils';
import type { PreWriteBlobMetadata } from '../types';
/** How many per-key delete failures to name in the error before truncating, to keep the message bounded. */
const MAX_REPORTED_DELETE_ERRORS = 5;
@@ -103,7 +103,7 @@ export class ObjectStoreService {
/**
* Upload an object to the configured bucket.
*/
async put(filename: string, buffer: Buffer, metadata: BinaryData.PreWriteMetadata = {}) {
async put(filename: string, buffer: Buffer, metadata: PreWriteBlobMetadata = {}) {
try {
const params: PutObjectCommandInput = {
Bucket: this.bucket,
@@ -1,4 +1,4 @@
import type { BinaryData } from '../types';
import type { PreWriteBlobMetadata } from '../types';
export type MetadataResponseHeaders = Record<string, string> & {
'content-length'?: string;
@@ -6,4 +6,4 @@ export type MetadataResponseHeaders = Record<string, string> & {
'x-amz-meta-filename'?: string;
etag?: string;
'last-modified'?: string;
} & BinaryData.PreWriteMetadata;
} & PreWriteBlobMetadata;
@@ -0,0 +1,65 @@
import { UnexpectedError } from 'n8n-workflow';
import { Transform } from 'node:stream';
/**
* A `Transform` that re-emits its input as chunks of exactly `chunkSize` bytes, with a possibly smaller final chunk.
* `chunkSize` must be a positive integer: values `<= 0` throws an `UnexpectedError`.
*
* Between transforms the internal queue carries at most one partial chunk (< `chunkSize` bytes).
*
* Wire the upstream source into the chunker with `node:stream.pipeline()`, not plain `.pipe()`.
* `pipeline()` propagates errors from upstream to the chunker
* (so consumers see them) and propagates destroy from the chunker to upstream
* (so sockets don't dangle when the consumer aborts).
* `.pipe()` does neither.
*/
export function createFixedSizeChunker(chunkSize: number): Transform {
if (chunkSize <= 0) {
throw new UnexpectedError(`createFixedSizeChunker requires chunkSize > 0, got ${chunkSize}`);
}
const queue: Buffer[] = [];
let queued = 0;
const take = (size: number): Buffer => {
const out = Buffer.allocUnsafe(size);
let written = 0;
while (written < size) {
const head = queue[0];
const need = size - written;
if (head.length <= need) {
head.copy(out, written);
written += head.length;
queue.shift();
} else {
head.copy(out, written, 0, need);
queue[0] = head.subarray(need);
written += need;
}
}
queued -= size;
return out;
};
return new Transform({
transform(chunk: Buffer, _encoding, done) {
queue.push(chunk);
queued += chunk.length;
while (queued >= chunkSize) {
this.push(take(chunkSize));
}
done();
},
flush(done) {
if (queued > 0) {
this.push(take(queued));
}
done();
},
destroy(error, done) {
queue.length = 0;
queued = 0;
done(error);
},
});
}
+9
View File
@@ -0,0 +1,9 @@
/** Metadata stored alongside a blob. */
export type BlobMetadata = {
fileName?: string;
mimeType?: string;
fileSize: number;
};
/** Caller-supplied metadata for a blob write, before its size is known. */
export type PreWriteBlobMetadata = Omit<BlobMetadata, 'fileSize'>;
@@ -0,0 +1,11 @@
{
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.go.json"],
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/build.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/__tests__/**"]
}
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "@n8n/typescript-config/tsconfig.common.go.json",
"compilerOptions": {
"types": ["node", "vitest/globals"],
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*.ts"],
"references": [
{ "path": "../../workflow/tsconfig.build.cjs.json" },
{ "path": "../backend-common/tsconfig.build.json" },
{ "path": "../backend-network/tsconfig.build.json" },
{ "path": "../config/tsconfig.build.json" },
{ "path": "../di/tsconfig.build.json" }
]
}
+23
View File
@@ -0,0 +1,23 @@
import { mergeConfig } from 'vite';
import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decorators';
export default mergeConfig(
createVitestConfigWithDecorators(
{},
// Pin `zod` and `n8n-workflow` to their CJS build so cross-boundary `instanceof`
// (`ZodType`, `UnexpectedError`) holds against the externalized CJS dist. See
// `cjsPinAliases` in @n8n/vitest-config/node for the rationale.
{ pinCjs: ['zod', 'n8n-workflow'] },
),
{
oxc: {
// OXC's TS transform ignores tsconfig's `emitDecoratorMetadata` — must be enabled
// explicitly here so `@n8n/config`'s `@Env(name, zodSchema) field: z.infer<...>`
// pattern works (the decorator reads `design:type` via `Reflect.getMetadata`).
decorator: {
legacy: true,
emitDecoratorMetadata: true,
},
},
},
);
+1
View File
@@ -149,6 +149,7 @@
"@n8n/api-types": "workspace:*",
"@n8n/backend-common": "workspace:*",
"@n8n/backend-network": "workspace:*",
"@n8n/blob-storage": "workspace:*",
"@n8n/chat-hub": "workspace:*",
"@n8n/client-oauth2": "workspace:*",
"@n8n/config": "workspace:*",
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/unbound-method */
import type { AzureBlobService } from 'n8n-core';
import type { AzureBlobService } from '@n8n/blob-storage';
import type { Readable } from 'node:stream';
import { mock } from 'vitest-mock-extended';
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/unbound-method */
import type { ObjectStoreService } from 'n8n-core';
import type { ObjectStoreService } from '@n8n/blob-storage';
import type { Readable } from 'node:stream';
import { mock } from 'vitest-mock-extended';
@@ -1,6 +1,6 @@
import { ensureError } from '@n8n/utils/errors/ensure-error';
import chunk from 'lodash/chunk';
import type { AzureBlobService } from 'n8n-core';
import type { AzureBlobService } from '@n8n/blob-storage';
import type { ByteStore, ByteStoreKey } from './types';
@@ -1,5 +1,5 @@
import { ensureError } from '@n8n/utils/errors/ensure-error';
import type { ObjectStoreService } from 'n8n-core';
import type { ObjectStoreService } from '@n8n/blob-storage';
import type { ByteStore, ByteStoreKey } from './types';
+3 -8
View File
@@ -21,8 +21,7 @@ import {
ExecutionContextHookRegistry,
StorageConfig,
} from 'n8n-core';
import { ObjectStoreConfig } from 'n8n-core/dist/binary-data/object-store/object-store.config';
import { AzureBlobConfig } from 'n8n-core/dist/binary-data/azure-blob/azure-blob.config';
import { AzureBlobConfig, ObjectStoreConfig } from '@n8n/blob-storage';
import { ensureError } from '@n8n/utils/errors/ensure-error';
import { Expression, sleep, UnexpectedError } from 'n8n-workflow';
@@ -374,9 +373,7 @@ export abstract class BaseCommand<F = never> {
protected async initObjectStoreIfConfigured() {
if (Container.get(ObjectStoreConfig).bucket.name === '') return undefined;
const { ObjectStoreService } = await import(
'n8n-core/dist/binary-data/object-store/object-store.service.ee.js'
);
const { ObjectStoreService } = await import('@n8n/blob-storage/object-store');
const objectStoreService = Container.get(ObjectStoreService);
await objectStoreService.init();
@@ -392,9 +389,7 @@ export abstract class BaseCommand<F = never> {
protected async initAzureStoreIfConfigured() {
if (Container.get(AzureBlobConfig).containerName === '') return;
const { AzureBlobService } = await import(
'n8n-core/dist/binary-data/azure-blob/azure-blob.service.ee.js'
);
const { AzureBlobService } = await import('@n8n/blob-storage/azure-blob');
const azureBlobService = Container.get(AzureBlobService);
await azureBlobService.init();
+1
View File
@@ -36,6 +36,7 @@
{ "path": "../@n8n/errors/tsconfig.build.json" },
{ "path": "../@n8n/backend-common/tsconfig.build.json" },
{ "path": "../@n8n/backend-test-utils/tsconfig.build.json" },
{ "path": "../@n8n/blob-storage/tsconfig.build.json" },
{ "path": "../@n8n/di/tsconfig.build.json" },
{ "path": "../@n8n/nodes-langchain/tsconfig.build.json" },
{ "path": "../@n8n/permissions/tsconfig.build.json" },
+1 -3
View File
@@ -58,12 +58,10 @@
"zod": "catalog:"
},
"dependencies": {
"@aws-sdk/client-s3": "3.808.0",
"@azure/identity": "catalog:",
"@azure/storage-blob": "catalog:",
"@langchain/core": "catalog:",
"@n8n/backend-common": "workspace:*",
"@n8n/backend-network": "workspace:*",
"@n8n/blob-storage": "workspace:*",
"@n8n/client-oauth2": "workspace:*",
"@n8n/config": "workspace:*",
"@n8n/constants": "workspace:*",
@@ -1,7 +1,7 @@
import { AzureBlobService } from '@n8n/blob-storage/azure-blob';
import fs from 'node:fs/promises';
import { Readable } from 'node:stream';
import { AzureBlobService } from '@/binary-data/azure-blob/azure-blob.service.ee';
import { AzureBlobManager } from '@/binary-data/azure-blob.manager';
import type { BinaryData } from '@/binary-data/types';
import { mockInstance, toFileId, toStream } from '@test/utils';
@@ -1,9 +1,9 @@
import type { MetadataResponseHeaders } from '@n8n/blob-storage';
import { ObjectStoreService } from '@n8n/blob-storage/object-store';
import fs from 'node:fs/promises';
import { Readable } from 'node:stream';
import { mock } from 'vitest-mock-extended';
import { ObjectStoreService } from '@/binary-data/object-store/object-store.service.ee';
import type { MetadataResponseHeaders } from '@/binary-data/object-store/types';
import { ObjectStoreManager } from '@/binary-data/object-store.manager';
import { mockInstance, toFileId, toStream } from '@test/utils';
@@ -1,10 +1,10 @@
import { binaryToBuffer } from '@n8n/backend-network';
import { AzureBlobService } from '@n8n/blob-storage/azure-blob';
import { Service } from '@n8n/di';
import fs from 'node:fs/promises';
import type { Readable } from 'node:stream';
import { v4 as uuid } from 'uuid';
import { AzureBlobService } from './azure-blob/azure-blob.service.ee';
import type { BinaryData } from './types';
@Service()
-3
View File
@@ -1,7 +1,4 @@
export * from './binary-data.service';
export { BinaryDataConfig } from './binary-data.config';
export type * from './types';
// type-only: the runtime classes stay behind dynamic imports to avoid eagerly loading the S3/Azure SDKs
export type { AzureBlobService } from './azure-blob/azure-blob.service.ee';
export type { ObjectStoreService } from './object-store/object-store.service.ee';
export { isStoredMode as isValidNonDefaultMode, FileLocation } from './utils';
@@ -1,10 +1,10 @@
import { binaryToBuffer } from '@n8n/backend-network';
import { ObjectStoreService } from '@n8n/blob-storage/object-store';
import { Service } from '@n8n/di';
import fs from 'node:fs/promises';
import type { Readable } from 'node:stream';
import { v4 as uuid } from 'uuid';
import { ObjectStoreService } from './object-store/object-store.service.ee';
import type { BinaryData } from './types';
@Service()
+3 -6
View File
@@ -1,3 +1,4 @@
import type { BlobMetadata, PreWriteBlobMetadata } from '@n8n/blob-storage';
import type { Readable } from 'stream';
import type { BINARY_DATA_MODES } from './binary-data.config';
@@ -24,15 +25,11 @@ export namespace BinaryData {
*/
export type StoredMode = Exclude<ConfigMode | UpgradedMode, 'default'>;
export type Metadata = {
fileName?: string;
mimeType?: string;
fileSize: number;
};
export type Metadata = BlobMetadata;
export type WriteResult = { fileId: string; fileSize: number };
export type PreWriteMetadata = Omit<Metadata, 'fileSize'>;
export type PreWriteMetadata = PreWriteBlobMetadata;
export type FileLocation =
| { type: 'execution'; workflowId: string; executionId: string }
-66
View File
@@ -1,6 +1,3 @@
import { UnexpectedError } from 'n8n-workflow';
import { Transform } from 'node:stream';
import type { BinaryData } from './types';
export { assertDir, exists } from '@n8n/backend-common';
@@ -11,69 +8,6 @@ export function isStoredMode(mode: string): mode is BinaryData.StoredMode {
return STORED_MODES.includes(mode as BinaryData.StoredMode);
}
/**
* A `Transform` that re-emits its input as chunks of exactly `chunkSize` bytes, with a possibly smaller final chunk.
* `chunkSize` must be a positive integer: values `<= 0` throws an `UnexpectedError`.
*
* Between transforms the internal queue carries at most one partial chunk (< `chunkSize` bytes).
*
* Wire the upstream source into the chunker with `node:stream.pipeline()`, not plain `.pipe()`.
* `pipeline()` propagates errors from upstream to the chunker
* (so consumers see them) and propagates destroy from the chunker to upstream
* (so sockets don't dangle when the consumer aborts).
* `.pipe()` does neither.
*/
export function createFixedSizeChunker(chunkSize: number): Transform {
if (chunkSize <= 0) {
throw new UnexpectedError(`createFixedSizeChunker requires chunkSize > 0, got ${chunkSize}`);
}
const queue: Buffer[] = [];
let queued = 0;
const take = (size: number): Buffer => {
const out = Buffer.allocUnsafe(size);
let written = 0;
while (written < size) {
const head = queue[0];
const need = size - written;
if (head.length <= need) {
head.copy(out, written);
written += head.length;
queue.shift();
} else {
head.copy(out, written, 0, need);
queue[0] = head.subarray(need);
written += need;
}
}
queued -= size;
return out;
};
return new Transform({
transform(chunk: Buffer, _encoding, done) {
queue.push(chunk);
queued += chunk.length;
while (queued >= chunkSize) {
this.push(take(chunkSize));
}
done();
},
flush(done) {
if (queued > 0) {
this.push(take(queued));
}
done();
},
destroy(error, done) {
queue.length = 0;
queued = 0;
done(error);
},
});
}
export const FileLocation = {
ofExecution: (workflowId: string, executionId: string): BinaryData.FileLocation => ({
type: 'execution',
+1
View File
@@ -21,6 +21,7 @@
{ "path": "../@n8n/decorators/tsconfig.build.json" },
{ "path": "../@n8n/backend-common/tsconfig.build.json" },
{ "path": "../@n8n/backend-network/tsconfig.build.json" },
{ "path": "../@n8n/blob-storage/tsconfig.build.json" },
{ "path": "../@n8n/config/tsconfig.build.json" },
{ "path": "../@n8n/constants/tsconfig.build.json" },
{ "path": "../@n8n/di/tsconfig.build.json" },
+77 -10
View File
@@ -1590,6 +1590,76 @@ importers:
specifier: catalog:typescript
version: 7.0.2
packages/@n8n/blob-storage:
dependencies:
'@aws-sdk/client-s3':
specifier: 3.808.0
version: 3.808.0
'@azure/identity':
specifier: 4.13.0
version: 4.13.0
'@azure/storage-blob':
specifier: 'catalog:'
version: 12.32.0
'@n8n/backend-common':
specifier: workspace:*
version: link:../backend-common
'@n8n/backend-network':
specifier: workspace:*
version: link:../backend-network
'@n8n/config':
specifier: workspace:*
version: link:../config
'@n8n/di':
specifier: workspace:*
version: link:../di
'@n8n/utils':
specifier: workspace:*
version: link:../utils
lodash:
specifier: 4.18.1
version: 4.18.1
n8n-workflow:
specifier: workspace:*
version: link:../../workflow
reflect-metadata:
specifier: 'catalog:'
version: 0.2.2
zod:
specifier: 3.25.67
version: 3.25.67
devDependencies:
'@n8n/eslint-config':
specifier: workspace:*
version: link:../eslint-config
'@n8n/typescript-config':
specifier: workspace:*
version: link:../typescript-config
'@n8n/vitest-config':
specifier: workspace:*
version: link:../vitest-config
'@types/lodash':
specifier: 'catalog:'
version: 4.17.17
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9)
eslint:
specifier: 'catalog:'
version: 9.29.0(jiti@2.6.1)
typescript:
specifier: catalog:typescript
version: 7.0.2
vite:
specifier: 'catalog:'
version: 8.0.2(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)
vitest:
specifier: 'catalog:'
version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
vitest-mock-extended:
specifier: 'catalog:'
version: 3.1.0(typescript@7.0.2)(vitest@4.1.9)
packages/@n8n/chat-hub:
dependencies:
'@n8n/api-types':
@@ -3923,6 +3993,9 @@ importers:
'@n8n/backend-network':
specifier: workspace:*
version: link:../@n8n/backend-network
'@n8n/blob-storage':
specifier: workspace:*
version: link:../@n8n/blob-storage
'@n8n/chat-hub':
specifier: workspace:*
version: link:../@n8n/chat-hub
@@ -4494,15 +4567,6 @@ importers:
packages/core:
dependencies:
'@aws-sdk/client-s3':
specifier: 3.808.0
version: 3.808.0
'@azure/identity':
specifier: 4.13.0
version: 4.13.0
'@azure/storage-blob':
specifier: 'catalog:'
version: 12.32.0
'@langchain/core':
specifier: 'catalog:'
version: 1.2.0(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.46.0(@smithy/signature-v4@5.3.5)(ws@8.21.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))(ws@8.21.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))
@@ -4512,6 +4576,9 @@ importers:
'@n8n/backend-network':
specifier: workspace:*
version: link:../@n8n/backend-network
'@n8n/blob-storage':
specifier: workspace:*
version: link:../@n8n/blob-storage
'@n8n/client-oauth2':
specifier: workspace:*
version: link:../@n8n/client-oauth2
@@ -30724,7 +30791,7 @@ snapshots:
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
magicast: 0.5.2
obug: 2.1.1
obug: 2.1.3
std-env: 4.0.0
tinyrainbow: 3.1.0
vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))