fix(core): Beta import/export API not available on cloud (no-changelog) (#33382)

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Konstantin Tieber
2026-07-01 15:58:02 +00:00
committed by GitHub
co-authored by cubic-dev-ai[bot]
parent 613b2c5acd
commit 3ead311851
13 changed files with 4 additions and 76 deletions
+1 -4
View File
@@ -2,10 +2,7 @@
Export and import workflows as portable n8n packages (`.n8np` archives).
> **Beta — disabled by default.** These commands call beta endpoints that the
> target instance must opt into with `N8N_PUBLIC_API_PACKAGES_ENABLED=true`, and
> the n8n Packages feature must be licensed. While disabled, the instance returns
> `404` and the CLI prints a hint explaining how to enable it.
> **Beta feature:** n8n packages are still under development and there may be breaking changes on APIs.
## `package export`
@@ -9,14 +9,6 @@ describe('toPackagesError', () => {
expect(toPackagesError(error)).toBe(error);
});
it('adds a beta-enablement hint for a 404', () => {
const result = toPackagesError(new ApiError(404, 'Not Found'));
expect(result).toBeInstanceOf(ApiError);
expect((result as ApiError).statusCode).toBe(404);
expect((result as ApiError).hint).toContain('N8N_PUBLIC_API_PACKAGES_ENABLED=true');
});
it('lists workflow-conflict issues for a 409', () => {
const result = toPackagesError(
new ApiError(409, 'Import blocked', undefined, {
@@ -1,9 +1,5 @@
import { ApiError } from '../../client';
const BETA_HINT =
'The n8n Packages API is beta and disabled by default. Enable it on the instance with ' +
'N8N_PUBLIC_API_PACKAGES_ENABLED=true (requires the n8n Packages license).';
type BlockingIssue =
| {
type: 'workflow-conflict';
@@ -35,9 +31,6 @@ function issuesHint(details: unknown): string | undefined {
export function toPackagesError(error: unknown): unknown {
if (!(error instanceof ApiError)) return error;
if (error.statusCode === 404) {
return new ApiError(error.statusCode, error.message, BETA_HINT, error.details);
}
const hint = issuesHint(error.details);
if (hint) {
return new ApiError(error.statusCode, error.message, hint, error.details);
@@ -13,11 +13,4 @@ export class PublicApiConfig {
/** When true, the Swagger UI for the Public API is not served. */
@Env('N8N_PUBLIC_API_SWAGGERUI_DISABLED')
swaggerUiDisabled: boolean = false;
/**
* When true, the n8n-packages public API endpoints are enabled. This feature is
* currently in beta and disabled by default.
*/
@Env('N8N_PUBLIC_API_PACKAGES_ENABLED')
packagesEnabled: boolean = false;
}
-1
View File
@@ -194,7 +194,6 @@ describe('GlobalConfig', () => {
disabled: false,
path: 'api',
swaggerUiDisabled: false,
packagesEnabled: false,
},
templates: {
enabled: true,
@@ -64,9 +64,3 @@ The in-tree addition of `workflowIdPolicy` is the canonical example — it lande
in the module types + importer, the DTO (+ form-fields list), the handler, and
the CLI. Grep `workflowIdPolicy` to see every site a new import knob must touch.
## More context
See the module structure and import-pipeline flow notes in the root
`AGENTS.md` and the package import/export RFC. Licensed behind
`feat:n8nPackages`; the public-API endpoints are beta and gated by
`N8N_PUBLIC_API_PACKAGES_ENABLED`.
@@ -1,5 +1,4 @@
import { mockInstance } from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config';
import type { AuthenticatedRequest } from '@n8n/db';
import { Container } from '@n8n/di';
import type { Response } from 'express';
@@ -8,7 +7,6 @@ import type { Mocked } from 'vitest';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { N8nPackagesService } from '@/modules/n8n-packages/n8n-packages.service';
import * as middlewares from '@/public-api/v1/shared/middlewares/global.middleware';
@@ -30,7 +28,6 @@ beforeAll(async () => {
});
describe('n8n-packages handler', () => {
let packagesEnabled: boolean;
let mockService: Mocked<N8nPackagesService>;
function makeRequest(
@@ -62,13 +59,9 @@ describe('n8n-packages handler', () => {
beforeEach(() => {
vi.clearAllMocks();
packagesEnabled = true;
mockService = mockInstance(N8nPackagesService);
vi.spyOn(Container, 'get').mockImplementation((serviceClass) => {
if (serviceClass === GlobalConfig) {
return { publicApi: { packagesEnabled } } as never;
}
if (
serviceClass === N8nPackagesService ||
(typeof serviceClass === 'function' && serviceClass.name === 'N8nPackagesService')
@@ -79,18 +72,6 @@ describe('n8n-packages handler', () => {
});
});
it('throws NotFoundError when packages are disabled', async () => {
packagesEnabled = false;
const caught = await run(
makeRequest({ workflowIds: ['wf-1'] }, ['workflow:export']),
makeResponse(),
);
expect(caught).toBeInstanceOf(NotFoundError);
expect(mockService.exportPackage).not.toHaveBeenCalled();
});
it('throws BadRequestError when the payload fails DTO validation', async () => {
const caught = await run(
makeRequest({ workflowIds: [''] }, ['workflow:export']),
@@ -1,5 +1,4 @@
import { ExportPackageRequestDto, ImportPackageRequestDto } from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config';
import type { AuthenticatedRequest } from '@n8n/db';
import { Container } from '@n8n/di';
import type { ApiKeyScope } from '@n8n/permissions';
@@ -15,7 +14,6 @@ import {
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { N8nPackagesService } from '@/modules/n8n-packages/n8n-packages.service';
import { resolveImportPackageUpload } from '@/modules/n8n-packages/utils/import-package-upload';
@@ -80,10 +78,6 @@ const n8nPackagesHandlers: N8nPackagesHandlers = {
exportPackage: [
publicApiCompositeScope(PACKAGE_EXPORT_SCOPES),
async (req, res) => {
if (!Container.get(GlobalConfig).publicApi.packagesEnabled) {
throw new NotFoundError('Not Found');
}
const payload = ExportPackageRequestDto.safeParse(req.body);
if (!payload.success) {
throw new BadRequestError(payload.error.errors.map(({ message }) => message).join('; '));
@@ -113,10 +107,6 @@ const n8nPackagesHandlers: N8nPackagesHandlers = {
importPackage: [
publicApiScope('workflow:import'),
async (req, res) => {
if (!Container.get(GlobalConfig).publicApi.packagesEnabled) {
throw new NotFoundError('Not Found');
}
const packageFile = resolveImportPackageUpload(req);
const payload = ImportPackageRequestDto.safeParse(req.body ?? {});
@@ -6,8 +6,7 @@ post:
- N8nPackage
summary: 'Beta: Export workflows and projects as an n8n package'
description: |
**Beta — disabled by default.** Set `N8N_PUBLIC_API_PACKAGES_ENABLED=true` on the
instance to enable this endpoint. While disabled, requests return `404`.
**Beta** — breaking changes may still occur without major version bump.
Export either workflows or projects as a gzipped tar archive (.n8np).
Provide `workflowIds` or `projectIds`, but not both. Empty projects export
@@ -6,8 +6,7 @@ post:
- N8nPackage
summary: 'Beta: Import an n8n package into a project'
description: |
**Beta — disabled by default.** Set `N8N_PUBLIC_API_PACKAGES_ENABLED=true` on the
instance to enable this endpoint. While disabled, requests return `404`.
**Beta** — breaking changes may still occur without major version bump.
Imports a gzip-compressed tar package (`.n8np`) into the target project. Send the
archive as the multipart field `package`. Optional routing uses form fields
+1 -1
View File
@@ -45,7 +45,7 @@ tags:
- name: Folders
description: Operations about folders
- name: N8nPackage
description: Beta — operations about n8n packages. Disabled by default; enable with `N8N_PUBLIC_API_PACKAGES_ENABLED=true`.
description: Beta — breaking changes may still occur without major version bump.
paths:
/audit:
@@ -1,5 +1,4 @@
import { testDb } from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config';
import type { User } from '@n8n/db';
import { Container } from '@n8n/di';
import { InstanceSettings } from 'n8n-core';
@@ -20,11 +19,9 @@ beforeAll(async () => {
beforeEach(async () => {
authOwnerAgent = testServer.publicApiAgentFor(owner);
Container.get(GlobalConfig).publicApi.packagesEnabled = true;
});
afterEach(async () => {
Container.get(GlobalConfig).publicApi.packagesEnabled = false;
await testDb.truncate(['WorkflowEntity', 'SharedWorkflow', 'ProjectRelation', 'Project']);
});
@@ -1,5 +1,4 @@
import { mockInstance, testDb } from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config';
import type { Project, User } from '@n8n/db';
import { ProjectRepository } from '@n8n/db';
import { Container } from '@n8n/di';
@@ -44,11 +43,6 @@ beforeEach(async () => {
'SharedCredentials',
]);
authOwnerAgent = testServer.publicApiAgentFor(owner);
Container.get(GlobalConfig).publicApi.packagesEnabled = true;
});
afterEach(() => {
Container.get(GlobalConfig).publicApi.packagesEnabled = false;
});
const testWithAPIKey = (method: 'post', url: string, apiKey: string | null) => async () => {