refactor(ai-builder): Replace hand-rolled sandbox client with @n8n/sandbox-client SDK (no-changelog) (#29879)

This commit is contained in:
Tomi Turtiainen 2026-05-08 11:32:02 +03:00 committed by GitHub
parent 6f4f0a0303
commit 7c57843cf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 21 additions and 752 deletions

View File

@ -42,6 +42,7 @@
"langsmith": "catalog:",
"@mozilla/readability": "^0.6.0",
"@n8n/api-types": "workspace:*",
"@n8n/sandbox-client": "0.0.1",
"@n8n/utils": "workspace:*",
"@n8n/workflow-sdk": "workspace:*",
"linkedom": "^0.18.9",

View File

@ -109,14 +109,6 @@ jest.mock('../n8n-sandbox-sandbox', () => ({
},
}));
jest.mock('../n8n-sandbox-image-manager', () => ({
N8nSandboxImageManager: class {
getDockerfile() {
return 'FROM node:20';
}
},
}));
jest.mock('../pack-workspace-sdk', () => ({
packWorkspaceSdk: jest.fn().mockResolvedValue(null),
isLinkWorkspaceSdkEnabled: jest.fn().mockReturnValue(false),

View File

@ -1,152 +0,0 @@
import {
DockerfileStepsBuilder,
N8nSandboxClient,
N8nSandboxServiceError,
} from '../n8n-sandbox-client';
function createJsonResponse(body: unknown, init?: ResponseInit): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
...init,
});
}
function bodyToRecord(body: unknown): Record<string, unknown> {
if (typeof body !== 'string') {
throw new Error('Expected request body to be a JSON string');
}
try {
return JSON.parse(body) as Record<string, unknown>;
} catch {
throw new Error('Expected request body to be valid JSON');
}
}
describe('N8nSandboxClient', () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
jest.restoreAllMocks();
});
it('should send dockerfile_steps when DockerfileStepsBuilder is provided', async () => {
const fetchMock = jest.fn().mockResolvedValueOnce(
createJsonResponse({
id: 'sandbox-1',
status: 'running',
provider: 'n8n-sandbox',
image_id: 'img-123',
created_at: 1,
last_active_at: 2,
}),
) as jest.MockedFunction<typeof fetch>;
global.fetch = fetchMock;
const client = new N8nSandboxClient({
baseUrl: 'https://sandbox.example.com',
apiKey: 'sandbox-key',
});
const dockerfile = new DockerfileStepsBuilder()
.run('apt-get update')
.run('apt-get install -y git');
await client.createSandbox({ dockerfile });
const body = bodyToRecord(fetchMock.mock.calls[0]?.[1]?.body);
expect(body.dockerfile_steps).toEqual(['RUN apt-get update', 'RUN apt-get install -y git']);
});
it('should send no body when no options are provided', async () => {
const fetchMock = jest.fn().mockResolvedValueOnce(
createJsonResponse({
id: 'sandbox-1',
status: 'running',
provider: 'n8n-sandbox',
image_id: '',
created_at: 1,
last_active_at: 2,
}),
) as jest.MockedFunction<typeof fetch>;
global.fetch = fetchMock;
const client = new N8nSandboxClient({
baseUrl: 'https://sandbox.example.com',
apiKey: 'sandbox-key',
});
await client.createSandbox();
expect(fetchMock.mock.calls[0]?.[1]?.body).toBeUndefined();
});
it('should parse streamed exec output', async () => {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
'{"type":"stdout","data":"hello\\n"}\n' +
'{"type":"stderr","data":"warn\\n"}\n' +
'{"type":"exit","exit_code":0,"success":true,"execution_time_ms":42,"timed_out":false,"killed":false}\n',
),
);
controller.close();
},
});
global.fetch = jest.fn().mockResolvedValue(
new Response(stream, {
status: 200,
headers: { 'Content-Type': 'application/x-ndjson' },
}),
) as jest.MockedFunction<typeof fetch>;
const client = new N8nSandboxClient({
baseUrl: 'https://sandbox.example.com',
apiKey: 'sandbox-key',
});
const stdoutChunks: string[] = [];
const stderrChunks: string[] = [];
const result = await client.exec('sandbox-1', {
command: 'echo hello',
onStdout: (data) => stdoutChunks.push(data),
onStderr: (data) => stderrChunks.push(data),
});
expect(result).toEqual({
exitCode: 0,
stdout: 'hello\n',
stderr: 'warn\n',
executionTimeMs: 42,
timedOut: false,
killed: false,
success: true,
});
expect(stdoutChunks).toEqual(['hello\n']);
expect(stderrChunks).toEqual(['warn\n']);
});
it('should convert JSON error responses into service errors', async () => {
global.fetch = jest.fn().mockResolvedValue(
createJsonResponse(
{
error: 'sandbox missing',
code: 404,
},
{ status: 404 },
),
) as jest.MockedFunction<typeof fetch>;
const client = new N8nSandboxClient({
baseUrl: 'https://sandbox.example.com',
apiKey: 'sandbox-key',
});
await expect(client.getSandbox('sandbox-1')).rejects.toMatchObject(
new N8nSandboxServiceError('sandbox missing', 404, 404),
);
});
});

View File

@ -17,7 +17,6 @@ import type { ErrorReporter, Logger } from '../logger';
import type { SandboxConfig } from './create-workspace';
import { DaytonaFilesystem } from './daytona-filesystem';
import { N8nSandboxFilesystem } from './n8n-sandbox-filesystem';
import { N8nSandboxImageManager } from './n8n-sandbox-image-manager';
import { N8nSandboxServiceSandbox } from './n8n-sandbox-sandbox';
import {
isLinkWorkspaceSdkEnabled,
@ -70,8 +69,6 @@ async function cleanupTrackedSandboxProcesses(workspace: Workspace): Promise<voi
export class BuilderSandboxFactory {
private daytona: Daytona | null = null;
private n8nSandboxImageManager: N8nSandboxImageManager | null = null;
constructor(
private readonly config: SandboxConfig,
private readonly imageManager?: SnapshotManager,
@ -151,11 +148,6 @@ export class BuilderSandboxFactory {
return this.daytona;
}
private getN8nSandboxImageManager(): N8nSandboxImageManager {
this.n8nSandboxImageManager ??= new N8nSandboxImageManager();
return this.n8nSandboxImageManager;
}
/** Cached node-types catalog string — generated once, reused across builders. */
private catalogCache: string | null = null;
@ -292,14 +284,12 @@ export class BuilderSandboxFactory {
): Promise<BuilderWorkspace> {
const config = this.assertIsN8nSandbox();
const dockerfile = this.getN8nSandboxImageManager().getDockerfile();
const catalog = await this.getNodeCatalog(context);
const sandbox = new N8nSandboxServiceSandbox({
apiKey: config.apiKey,
serviceUrl: config.serviceUrl,
timeout: config.timeout ?? 300_000,
dockerfile,
});
const destroySandbox = async (): Promise<void> => {

View File

@ -1,539 +0,0 @@
import type { FileContent } from '@mastra/core/workspace';
import { z } from 'zod';
/** Error payload returned by the sandbox service. */
export interface N8nSandboxServiceErrorPayload {
error: string;
code: number;
}
export class N8nSandboxServiceError extends Error {
constructor(
message: string,
readonly status: number,
readonly code?: number,
) {
super(message);
this.name = 'N8nSandboxServiceError';
}
}
/** Sandbox metadata exposed by the service API. */
export interface N8nSandboxRecord {
id: string;
status: string;
provider: string;
imageId: string;
createdAt: number;
lastActiveAt: number;
}
/** Directory entry returned by the service file listing API. */
export interface N8nSandboxFileEntry {
name: string;
size: number;
isDir: boolean;
type: 'file' | 'directory';
modTime: string;
}
/** File stat payload mapped into Mastra-friendly shape. */
export interface N8nSandboxFileStat {
name: string;
path: string;
type: 'file' | 'directory';
size: number;
createdAt: string;
modifiedAt: string;
}
/** Aggregated result of an execute-to-completion shell command. */
export interface N8nSandboxExecResult {
exitCode: number;
stdout: string;
stderr: string;
executionTimeMs: number;
timedOut: boolean;
killed: boolean;
success: boolean;
}
// ── Exec event schemas (streamed NDJSON from `/exec`) ────────────────────────
const execEventStdoutSchema = z.object({ type: z.literal('stdout'), data: z.string() });
const execEventStderrSchema = z.object({ type: z.literal('stderr'), data: z.string() });
const execEventExitSchema = z.object({
type: z.literal('exit'),
exit_code: z.number(),
success: z.boolean(),
execution_time_ms: z.number(),
timed_out: z.boolean(),
killed: z.boolean(),
});
const execEventErrorSchema = z.object({ type: z.literal('error'), error: z.string() });
const execEventSchema = z.discriminatedUnion('type', [
execEventStdoutSchema,
execEventStderrSchema,
execEventExitSchema,
execEventErrorSchema,
]);
type ExecEvent = z.infer<typeof execEventSchema>;
// ── Service response schemas ─────────────────────────────────────────────────
const createSandboxResponseSchema = z.object({
id: z.string(),
status: z.string(),
provider: z.string(),
image_id: z.string().optional(),
created_at: z.number(),
last_active_at: z.number(),
});
type CreateSandboxResponse = z.infer<typeof createSandboxResponseSchema>;
const fileEntryResponseSchema = z.object({
name: z.string(),
size: z.number(),
is_dir: z.boolean(),
type: z.enum(['file', 'directory']),
mod_time: z.string(),
});
type FileEntryResponse = z.infer<typeof fileEntryResponseSchema>;
const fileStatResponseSchema = z.object({
name: z.string(),
path: z.string(),
type: z.enum(['file', 'directory']),
size: z.number(),
created_at: z.string(),
modified_at: z.string(),
});
type FileStatResponse = z.infer<typeof fileStatResponseSchema>;
/** Client configuration for talking to the sandbox service. */
export interface N8nSandboxClientOptions {
apiKey?: string;
baseUrl?: string;
}
/** Fluent builder for constructing Dockerfile instructions sent at sandbox creation. */
export class DockerfileStepsBuilder {
private readonly steps: string[] = [];
/** Append one or more RUN instructions. */
run(command: string | string[]): this {
const commands = Array.isArray(command) ? command : [command];
for (const cmd of commands) {
this.steps.push(`RUN ${cmd}`);
}
return this;
}
build(): string[] {
return [...this.steps];
}
}
/** Options used when creating a sandbox instance. */
interface CreateSandboxOptions {
dockerfile?: DockerfileStepsBuilder;
}
/** Command execution request sent to `/exec`. */
interface N8nSandboxExecRequest {
command: string;
env?: Record<string, string | undefined>;
workdir?: string;
timeoutMs?: number;
abortSignal?: AbortSignal;
onStdout?: (data: string) => void;
onStderr?: (data: string) => void;
}
/** Exit metadata captured from the final `/exec` event. */
interface ExecExitMeta {
exitCode: number;
executionTimeMs: number;
timedOut: boolean;
killed: boolean;
success: boolean;
}
function normalizeBaseUrl(baseUrl?: string): string {
return (baseUrl ?? '').replace(/\/+$/, '');
}
function mapSandboxRecord(payload: CreateSandboxResponse): N8nSandboxRecord {
return {
id: payload.id,
status: payload.status,
provider: payload.provider,
imageId: payload.image_id ?? '',
createdAt: payload.created_at,
lastActiveAt: payload.last_active_at,
};
}
function asBuffer(content: FileContent): Buffer {
return typeof content === 'string' ? Buffer.from(content, 'utf-8') : Buffer.from(content);
}
/** Yields parsed objects from an NDJSON ReadableStream, one per line. */
async function* readNdjsonStream<T>(
stream: ReadableStream<Uint8Array>,
parse: (line: string) => T,
): AsyncGenerator<T> {
const decoder = new TextDecoder();
let pending = '';
for await (const chunk of stream) {
pending += decoder.decode(chunk, { stream: true });
let newlineIndex = pending.indexOf('\n');
while (newlineIndex !== -1) {
const line = pending.slice(0, newlineIndex).trim();
pending = pending.slice(newlineIndex + 1);
if (line.length > 0) {
yield parse(line);
}
newlineIndex = pending.indexOf('\n');
}
}
// Flush any remaining partial line
pending += decoder.decode();
const last = pending.trim();
if (last.length > 0) {
yield parse(last);
}
}
function parseExecEvent(line: string): ExecEvent {
try {
const json: unknown = JSON.parse(line);
return execEventSchema.parse(json);
} catch {
return { type: 'error', error: 'Invalid exec event payload' };
}
}
/**
* Thin HTTP client for the n8n sandbox service.
*
* It handles sandbox lifecycle, file operations, streamed command execution,
* and lazy image instantiation for builder prewarming.
*/
export class N8nSandboxClient {
private readonly baseUrl: string;
constructor(private readonly options: N8nSandboxClientOptions) {
this.baseUrl = normalizeBaseUrl(options.baseUrl);
}
async createSandbox(options: CreateSandboxOptions = {}): Promise<N8nSandboxRecord> {
const body: Record<string, unknown> = {};
const steps = options.dockerfile?.build();
if (steps?.length) {
body.dockerfile_steps = steps;
}
return mapSandboxRecord(
await this.requestJson<CreateSandboxResponse>('POST', '/sandboxes', {
body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
}),
);
}
async getSandbox(id: string): Promise<N8nSandboxRecord> {
return mapSandboxRecord(
await this.requestJson<CreateSandboxResponse>('GET', `/sandboxes/${id}`),
);
}
async deleteSandbox(id: string): Promise<void> {
await this.expectSuccess(this.request('DELETE', `/sandboxes/${id}`));
}
async deleteImage(id: string): Promise<void> {
await this.expectSuccess(this.request('DELETE', `/images/${id}`));
}
async exec(id: string, request: N8nSandboxExecRequest): Promise<N8nSandboxExecResult> {
const response = await this.request('POST', `/sandboxes/${id}/exec`, {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: request.command,
env: request.env,
workdir: request.workdir,
timeout_ms: request.timeoutMs,
}),
signal: request.abortSignal,
});
if (!response.ok) {
throw await this.toError(response);
}
return await this.readExecResult(response, request);
}
async readFile(id: string, path: string): Promise<Buffer> {
const response = await this.request('GET', `/sandboxes/${id}/files/content`, {
query: { path },
});
if (!response.ok) {
throw await this.toError(response);
}
return Buffer.from(await response.arrayBuffer());
}
async writeFile(id: string, path: string, content: FileContent, overwrite = true): Promise<void> {
await this.expectSuccess(
this.request('PUT', `/sandboxes/${id}/files`, {
query: { path, overwrite: String(overwrite) },
headers: { 'Content-Type': 'application/octet-stream' },
body: asBuffer(content),
}),
);
}
async appendFile(id: string, path: string, content: FileContent): Promise<void> {
await this.expectSuccess(
this.request('POST', `/sandboxes/${id}/files`, {
query: { path },
headers: { 'Content-Type': 'application/octet-stream' },
body: asBuffer(content),
}),
);
}
async deleteFile(
id: string,
path: string,
options?: { recursive?: boolean; force?: boolean },
): Promise<void> {
await this.expectSuccess(
this.request('DELETE', `/sandboxes/${id}/files`, {
query: {
path,
recursive: String(options?.recursive ?? false),
force: String(options?.force ?? false),
},
}),
);
}
async copyFile(
id: string,
request: { src: string; dest: string; recursive?: boolean; overwrite?: boolean },
): Promise<void> {
await this.expectSuccess(
this.request('POST', `/sandboxes/${id}/files/copy`, {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
src: request.src,
dest: request.dest,
recursive: request.recursive ?? false,
overwrite: request.overwrite ?? false,
}),
}),
);
}
async moveFile(
id: string,
request: { src: string; dest: string; overwrite?: boolean },
): Promise<void> {
await this.expectSuccess(
this.request('POST', `/sandboxes/${id}/files/move`, {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
src: request.src,
dest: request.dest,
overwrite: request.overwrite ?? false,
}),
}),
);
}
async mkdir(id: string, path: string, recursive = false): Promise<void> {
await this.expectSuccess(
this.request('POST', `/sandboxes/${id}/mkdir`, {
query: { path, recursive: String(recursive) },
}),
);
}
async listFiles(
id: string,
request: { path?: string; recursive?: boolean; extension?: string } = {},
): Promise<N8nSandboxFileEntry[]> {
const payload = await this.requestJson<FileEntryResponse[]>('GET', `/sandboxes/${id}/files`, {
query: {
...(request.path ? { path: request.path } : {}),
...(request.recursive !== undefined ? { recursive: String(request.recursive) } : {}),
...(request.extension ? { extension: request.extension } : {}),
},
});
return payload.map((entry) => ({
name: entry.name,
size: entry.size,
isDir: entry.is_dir,
type: entry.type,
modTime: entry.mod_time,
}));
}
async stat(id: string, path: string): Promise<N8nSandboxFileStat> {
const payload = await this.requestJson<FileStatResponse>('GET', `/sandboxes/${id}/stat`, {
query: { path },
});
return {
name: payload.name,
path: payload.path,
type: payload.type,
size: payload.size,
createdAt: payload.created_at,
modifiedAt: payload.modified_at,
};
}
private async readExecResult(
response: Response,
request: Pick<N8nSandboxExecRequest, 'onStdout' | 'onStderr'>,
): Promise<N8nSandboxExecResult> {
if (!response.body) {
throw new Error('Sandbox exec response body is not readable');
}
let stdout = '';
let stderr = '';
let exitMeta: ExecExitMeta | null = null;
for await (const event of readNdjsonStream(response.body, parseExecEvent)) {
switch (event.type) {
case 'stdout':
stdout += event.data;
request.onStdout?.(event.data);
break;
case 'stderr':
stderr += event.data;
request.onStderr?.(event.data);
break;
case 'error':
throw new Error(event.error);
case 'exit':
exitMeta = {
exitCode: event.exit_code,
executionTimeMs: event.execution_time_ms,
timedOut: event.timed_out,
killed: event.killed,
success: event.success,
};
break;
}
}
const finalExitMeta = this.requireExecExitMeta(exitMeta);
return {
exitCode: finalExitMeta.exitCode,
stdout,
stderr,
executionTimeMs: finalExitMeta.executionTimeMs,
timedOut: finalExitMeta.timedOut,
killed: finalExitMeta.killed,
success: finalExitMeta.success,
};
}
private requireExecExitMeta(exitMeta: ExecExitMeta | null): ExecExitMeta {
if (!exitMeta) {
throw new Error('Sandbox exec stream ended without an exit event');
}
return exitMeta;
}
private async expectSuccess(responsePromise: Promise<Response>): Promise<void> {
const response = await responsePromise;
if (!response.ok) {
throw await this.toError(response);
}
}
private async requestJson<T>(
method: string,
path: string,
options: {
body?: string | Buffer;
headers?: Record<string, string>;
query?: Record<string, string>;
signal?: AbortSignal;
} = {},
): Promise<T> {
const response = await this.request(method, path, options);
if (!response.ok) {
throw await this.toError(response);
}
return (await response.json()) as T;
}
private async request(
method: string,
path: string,
options: {
body?: string | Buffer;
headers?: Record<string, string>;
query?: Record<string, string>;
signal?: AbortSignal;
} = {},
): Promise<Response> {
if (!this.baseUrl) {
throw new Error('n8n sandbox service URL is not configured');
}
const url = new URL(`${this.baseUrl}${path}`);
for (const [key, value] of Object.entries(options.query ?? {})) {
url.searchParams.set(key, value);
}
const headers = new Headers(options.headers);
if (this.options.apiKey) {
headers.set('X-Api-Key', this.options.apiKey);
}
return await fetch(url, {
method,
headers,
body: options.body,
signal: options.signal,
});
}
private async toError(response: Response): Promise<Error> {
const contentType = response.headers.get('content-type') ?? '';
if (contentType.includes('application/json')) {
const payload = (await response.json()) as Partial<N8nSandboxServiceErrorPayload>;
return new N8nSandboxServiceError(
payload.error ?? `Sandbox service request failed with status ${response.status}`,
response.status,
payload.code,
);
}
const text = await response.text();
return new N8nSandboxServiceError(
text || `Sandbox service request failed with status ${response.status}`,
response.status,
);
}
}

View File

@ -10,9 +10,9 @@ import type {
WriteOptions,
} from '@mastra/core/workspace';
import { MastraFilesystem } from '@mastra/core/workspace';
import { SandboxServiceError } from '@n8n/sandbox-client';
import { dirname } from 'node:path/posix';
import { N8nSandboxServiceError } from './n8n-sandbox-client';
import type { N8nSandboxServiceSandbox } from './n8n-sandbox-sandbox';
function getParentDirectory(path: string): string | null {
@ -129,7 +129,7 @@ export class N8nSandboxFilesystem extends MastraFilesystem {
await client.stat(sandboxId, path);
return true;
} catch (error) {
if (error instanceof N8nSandboxServiceError && error.status === 404) {
if (error instanceof SandboxServiceError && error.status === 404) {
return false;
}
throw error;

View File

@ -1,30 +0,0 @@
import { DockerfileStepsBuilder } from './n8n-sandbox-client';
import {
BUILD_MJS,
N8N_SANDBOX_WORKSPACE_ROOT,
PACKAGE_JSON,
TSCONFIG_JSON,
} from './sandbox-setup';
function b64(content: string): string {
return Buffer.from(content, 'utf-8').toString('base64');
}
const ROOT = N8N_SANDBOX_WORKSPACE_ROOT;
export class N8nSandboxImageManager {
private cachedDockerfile: DockerfileStepsBuilder | null = null;
getDockerfile(): DockerfileStepsBuilder {
if (this.cachedDockerfile) return this.cachedDockerfile;
this.cachedDockerfile = new DockerfileStepsBuilder()
.run(`mkdir -p ${ROOT}/src ${ROOT}/chunks ${ROOT}/node-types`)
.run(`echo '${b64(PACKAGE_JSON)}' | base64 -d > ${ROOT}/package.json`)
.run(`echo '${b64(TSCONFIG_JSON)}' | base64 -d > ${ROOT}/tsconfig.json`)
.run(`echo '${b64(BUILD_MJS)}' | base64 -d > ${ROOT}/build.mjs`)
.run(`cd ${ROOT} && npm install --ignore-scripts`);
return this.cachedDockerfile;
}
}

View File

@ -5,17 +5,15 @@ import type {
ProviderStatus,
SandboxInfo,
} from '@mastra/core/workspace';
import { SandboxClient } from '@n8n/sandbox-client';
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { N8nSandboxClient, type DockerfileStepsBuilder } from './n8n-sandbox-client';
export interface N8nSandboxServiceSandboxOptions {
id?: string;
apiKey?: string;
serviceUrl?: string;
timeout?: number;
dockerfile?: DockerfileStepsBuilder;
}
function shellEscape(value: string): string {
@ -37,13 +35,13 @@ export class N8nSandboxServiceSandbox extends MastraSandbox {
private readonly instanceId = `n8n-sandbox-${randomUUID()}`;
private readonly client: N8nSandboxClient;
private readonly client: SandboxClient;
private sandboxId?: string;
constructor(private readonly options: N8nSandboxServiceSandboxOptions) {
super({ name: 'N8nSandboxServiceSandbox' });
this.client = new N8nSandboxClient({
this.client = new SandboxClient({
apiKey: options.apiKey,
baseUrl: options.serviceUrl,
});
@ -60,9 +58,7 @@ export class N8nSandboxServiceSandbox extends MastraSandbox {
return;
}
const sandbox = await this.client.createSandbox({
dockerfile: this.options.dockerfile,
});
const sandbox = await this.client.createSandbox();
this.sandboxId = sandbox.id;
}
@ -83,8 +79,6 @@ export class N8nSandboxServiceSandbox extends MastraSandbox {
lastUsedAt: new Date(sandbox.lastActiveAt * 1000),
metadata: {
remoteStatus: sandbox.status,
imageId: sandbox.imageId,
remoteProvider: sandbox.provider,
},
};
}
@ -118,7 +112,7 @@ export class N8nSandboxServiceSandbox extends MastraSandbox {
};
}
getClient(): N8nSandboxClient {
getClient(): SandboxClient {
return this.client;
}

View File

@ -1703,6 +1703,9 @@ importers:
'@n8n/api-types':
specifier: workspace:*
version: link:../api-types
'@n8n/sandbox-client':
specifier: 0.0.1
version: 0.0.1
'@n8n/utils':
specifier: workspace:*
version: link:../utils
@ -8402,6 +8405,10 @@ packages:
resolution: {integrity: sha512-qmDvJIjcNsZ6tXWy2G9yuCgMPTTn35GMA3dPpSLm7QJVpbQzYdw0ALy1bKoivXnEM3U93/OrK+/M719b+fg84Q==}
engines: {node: '>=18'}
'@n8n/sandbox-client@0.0.1':
resolution: {integrity: sha512-qvfc8/qv+rPz0nsM/r0pL/tWm5Rcry3X/d80/uxobBkWDtjZs9cFj10NBzU8+yrSs8F3dsk08FIWyVCzSt9jKA==}
engines: {node: '>=22.16', pnpm: '>=10.22.0'}
'@n8n/typeorm@0.3.20-16':
resolution: {integrity: sha512-XEfVKqbkDkLhU0tn3/zUvolDA/8u6/khDxP4pPvGKv68kPxC2h25eesszmXtfIKBWosE/8sAOOTtowA1b4jFYQ==}
engines: {node: '>=16.13.0'}
@ -28301,6 +28308,12 @@ snapshots:
outvariant: 1.4.3
strict-event-emitter: 0.5.1
'@n8n/sandbox-client@0.0.1':
dependencies:
axios: 1.16.0
transitivePeerDependencies:
- debug
'@n8n/typeorm@0.3.20-16(@sentry/node@10.36.0)(mysql2@3.17.0)(pg@8.17.0)(sqlite3@5.1.7)':
dependencies:
app-root-path: 3.1.0