first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,22 @@
# @n8n/rest-api-client
This package contains the REST API calls for n8n.
## Table of Contents
- [Features](#features)
- [Contributing](#contributing)
- [License](#license)
## Features
- Provides a REST API for n8n
- Supports authentication and authorization
## Contributing
For more details, please read our [CONTRIBUTING.md](CONTRIBUTING.md).
## License
For more details, please read our [LICENSE.md](LICENSE.md).
@@ -0,0 +1,4 @@
{
"$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../../../../biome.jsonc"]
}
@@ -0,0 +1,15 @@
import { defineConfig } from 'eslint/config';
import { frontendConfig } from '@n8n/eslint-config/frontend';
export default defineConfig(frontendConfig, {
rules: {
// TODO: Remove these
'@typescript-eslint/naming-convention': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
},
});
@@ -0,0 +1,58 @@
{
"name": "@n8n/rest-api-client",
"type": "module",
"version": "2.11.0",
"files": [
"dist"
],
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./*": {
"types": "./dist/*.d.mts",
"import": "./dist/*.mjs",
"require": "./dist/*.cjs"
}
},
"scripts": {
"dev": "tsdown --watch",
"build": "tsdown",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit",
"test": "vitest run",
"test:dev": "vitest --silent=false",
"lint": "eslint src --quiet",
"lint:fix": "eslint src --fix",
"format": "biome format --write . && prettier --write . --ignore-path ../../../../.prettierignore",
"format:check": "biome ci . && prettier --check . --ignore-path ../../../../.prettierignore"
},
"dependencies": {
"@n8n/api-types": "workspace:*",
"@n8n/constants": "workspace:*",
"@n8n/permissions": "workspace:*",
"@n8n/utils": "workspace:*",
"js-base64": "catalog:",
"n8n-workflow": "workspace:*",
"axios": "catalog:",
"flatted": "catalog:"
},
"devDependencies": {
"@n8n/eslint-config": "workspace:*",
"@n8n/i18n": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@testing-library/jest-dom": "catalog:frontend",
"@testing-library/user-event": "catalog:frontend",
"tsdown": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
},
"license": "See LICENSE.md file in the root of the repository"
}
@@ -0,0 +1,11 @@
import type { AiUsageSettingsRequestDto } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function updateAiUsageSettings(
context: IRestApiContext,
data: AiUsageSettingsRequestDto,
): Promise<void> {
return await makeRestApiRequest(context, 'POST', '/ai/usage-settings', data);
}
@@ -0,0 +1,40 @@
import type {
CreateApiKeyRequestDto,
UpdateApiKeyRequestDto,
ApiKey,
ApiKeyWithRawValue,
} from '@n8n/api-types';
import type { ApiKeyScope } from '@n8n/permissions';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function getApiKeys(context: IRestApiContext): Promise<ApiKey[]> {
return await makeRestApiRequest(context, 'GET', '/api-keys');
}
export async function getApiKeyScopes(context: IRestApiContext): Promise<ApiKeyScope[]> {
return await makeRestApiRequest(context, 'GET', '/api-keys/scopes');
}
export async function createApiKey(
context: IRestApiContext,
payload: CreateApiKeyRequestDto,
): Promise<ApiKeyWithRawValue> {
return await makeRestApiRequest(context, 'POST', '/api-keys', payload);
}
export async function deleteApiKey(
context: IRestApiContext,
id: string,
): Promise<{ success: boolean }> {
return await makeRestApiRequest(context, 'DELETE', `/api-keys/${id}`);
}
export async function updateApiKey(
context: IRestApiContext,
id: string,
payload: UpdateApiKeyRequestDto,
): Promise<{ success: boolean }> {
return await makeRestApiRequest(context, 'PATCH', `/api-keys/${id}`, payload);
}
@@ -0,0 +1,37 @@
import type {
BreakingChangeLightReportResult,
BreakingChangeWorkflowRuleResult,
BreakingChangeVersion,
} from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest, get } from '../utils';
type BreakingChangeQuery = {
version?: BreakingChangeVersion;
};
export async function getReport(
context: IRestApiContext,
query?: BreakingChangeQuery,
): Promise<BreakingChangeLightReportResult> {
return (await get(context.baseUrl, '/breaking-changes/report', query)).data;
}
export async function refreshReport(
context: IRestApiContext,
query?: BreakingChangeQuery,
): Promise<BreakingChangeLightReportResult> {
const refreshUrl = query?.version
? `/breaking-changes/report/refresh?version=${query.version}`
: '/breaking-changes/report/refresh';
return await makeRestApiRequest(context, 'POST', refreshUrl);
}
export async function getReportForRule(
context: IRestApiContext,
ruleId: string,
): Promise<BreakingChangeWorkflowRuleResult> {
return (await get(context.baseUrl, `/breaking-changes/report/${ruleId}`)).data;
}
@@ -0,0 +1,108 @@
import type { IRestApiContext } from '../types';
import { get, post } from '../utils';
export declare namespace Cloud {
export interface PlanData {
planId: number;
monthlyExecutionsLimit: number;
activeWorkflowsLimit: number;
credentialsLimit: number;
isActive: boolean;
displayName: string;
expirationDate: string;
metadata: PlanMetadata;
userIsTrialing?: boolean;
bannerConfig?: BannerConfig;
}
export interface PlanMetadata {
version: 'v1';
group: 'opt-out' | 'opt-in' | 'trial';
slug: 'pro-1' | 'pro-2' | 'starter' | 'trial-1';
trial?: Trial;
}
interface Trial {
length: number;
gracePeriod: number;
}
export interface BannerConfig {
// If set, show time left section
// - If text provided, use it
// - If text not provided, compute from expirationDate
timeLeft?: {
text?: string;
};
// If true, show executions section with progress bar and x/y text
showExecutions?: boolean;
// CTA button configuration
cta?: {
text?: string;
icon?: string;
size?: 'small' | 'medium';
/** @deprecated Use variant instead */
style?: 'primary' | 'success' | 'warning' | 'danger';
variant?: 'solid' | 'subtle' | 'ghost' | 'outline' | 'destructive' | 'success';
href?: string; // If provided, navigate to this URL; otherwise use upgrade flow
};
// Banner icon (left side) - if not set, no icon shown
icon?: string;
dismissible?: boolean;
forceShow?: boolean; // Override localStorage dismissal
}
export type UserAccount = {
confirmed: boolean;
username: string;
email: string;
hasEarlyAccess?: boolean;
role?: string;
selectedApps?: string[];
information?: {
[key: string]: string | string[];
};
};
}
export interface InstanceUsage {
timeframe?: string;
executions: number;
activeWorkflows: number;
}
export async function getCurrentPlan(context: IRestApiContext): Promise<Cloud.PlanData> {
return await get(context.baseUrl, '/admin/cloud-plan');
}
export async function getCurrentUsage(context: IRestApiContext): Promise<InstanceUsage> {
return await get(context.baseUrl, '/cloud/limits');
}
export async function getCloudUserInfo(context: IRestApiContext): Promise<Cloud.UserAccount> {
return await get(context.baseUrl, '/cloud/proxy/user/me');
}
export async function sendConfirmationEmail(context: IRestApiContext): Promise<Cloud.UserAccount> {
return await post(context.baseUrl, '/cloud/proxy/user/resend-confirmation-email');
}
export async function getAdminPanelLoginCode(context: IRestApiContext): Promise<{ code: string }> {
return await get(context.baseUrl, '/cloud/proxy/login/code');
}
export interface DynamicNotification {
title?: string;
message?: string;
}
export async function sendUserEvent(
context: IRestApiContext,
eventData: { eventType: string; metadata?: Record<string, unknown> },
): Promise<DynamicNotification> {
return await post(context.baseUrl, '/cloud/proxy/user/event', eventData);
}
@@ -0,0 +1,47 @@
import { NPM_COMMUNITY_NODE_SEARCH_API_URL } from '@n8n/constants';
import type { PublicInstalledPackage } from 'n8n-workflow';
import type { IRestApiContext } from '../types';
import { get, post, makeRestApiRequest } from '../utils';
export async function getInstalledCommunityNodes(
context: IRestApiContext,
): Promise<PublicInstalledPackage[]> {
const response = await get(context.baseUrl, '/community-packages');
return response.data || [];
}
export async function installNewPackage(
context: IRestApiContext,
name: string,
verify?: boolean,
version?: string,
): Promise<PublicInstalledPackage> {
return await post(context.baseUrl, '/community-packages', { name, verify, version });
}
export async function uninstallPackage(context: IRestApiContext, name: string): Promise<void> {
return await makeRestApiRequest(context, 'DELETE', '/community-packages', { name });
}
export async function updatePackage(
context: IRestApiContext,
name: string,
version?: string,
checksum?: string,
): Promise<PublicInstalledPackage> {
return await makeRestApiRequest(context, 'PATCH', '/community-packages', {
name,
version,
checksum,
});
}
export async function getAvailableCommunityPackageCount(): Promise<number> {
const response = await get(
NPM_COMMUNITY_NODE_SEARCH_API_URL,
'search?q=keywords:n8n-community-node-package',
);
return response.total || 0;
}
@@ -0,0 +1,23 @@
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export interface ConsentDetails {
clientName: string;
clientId: string;
}
export interface ConsentApprovalResponse {
status: string;
redirectUrl: string;
}
export async function getConsentDetails(context: IRestApiContext): Promise<ConsentDetails> {
return await makeRestApiRequest(context, 'GET', '/consent/details');
}
export async function approveConsent(
context: IRestApiContext,
approved: boolean,
): Promise<ConsentApprovalResponse> {
return await makeRestApiRequest(context, 'POST', '/consent/approve', { approved });
}
@@ -0,0 +1,50 @@
import type { CredentialResolver, CredentialResolverType } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function getCredentialResolvers(
context: IRestApiContext,
): Promise<CredentialResolver[]> {
return await makeRestApiRequest(context, 'GET', '/credential-resolvers');
}
export async function getCredentialResolverTypes(
context: IRestApiContext,
): Promise<CredentialResolverType[]> {
return await makeRestApiRequest(context, 'GET', '/credential-resolvers/types');
}
export async function getCredentialResolver(
context: IRestApiContext,
resolverId: string,
): Promise<CredentialResolver> {
return await makeRestApiRequest(context, 'GET', `/credential-resolvers/${resolverId}`);
}
export async function createCredentialResolver(
context: IRestApiContext,
payload: { name: string; type: string; config: Record<string, unknown> },
): Promise<CredentialResolver> {
return await makeRestApiRequest(context, 'POST', '/credential-resolvers', payload);
}
export async function updateCredentialResolver(
context: IRestApiContext,
resolverId: string,
payload: {
name: string;
type: string;
config: Record<string, unknown>;
clearCredentials?: boolean;
},
): Promise<CredentialResolver> {
return await makeRestApiRequest(context, 'PATCH', `/credential-resolvers/${resolverId}`, payload);
}
export async function deleteCredentialResolver(
context: IRestApiContext,
resolverId: string,
): Promise<void> {
return await makeRestApiRequest(context, 'DELETE', `/credential-resolvers/${resolverId}`);
}
@@ -0,0 +1,8 @@
import type { IRestApiContext } from '../types';
import { get } from '../utils';
export async function getBecomeCreatorCta(context: IRestApiContext): Promise<boolean> {
const response = await get(context.baseUrl, '/cta/become-creator');
return response;
}
@@ -0,0 +1,30 @@
import type { BannerName } from '@n8n/api-types';
import type { Role } from '@n8n/api-types/dist/schemas/user.schema';
import { get } from '../utils';
export type DynamicBanner = {
id: BannerName;
content: string;
isDismissible: boolean;
dismissPermanently: boolean | null;
theme: 'info' | 'warning' | 'danger';
priority: number;
};
type DynamicBannerFilters = {
version: string;
deploymentType: string;
planName?: string;
instanceId: string;
userCreatedAt?: string;
isOwner?: boolean;
role?: Role;
};
export async function getDynamicBanners(
endpoint: string,
filters: DynamicBannerFilters,
): Promise<DynamicBanner[]> {
return await get(endpoint, '', filters);
}
@@ -0,0 +1,50 @@
import type { IDataObject, MessageEventBusDestinationOptions } from 'n8n-workflow';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export type ApiMessageEventBusDestinationOptions = MessageEventBusDestinationOptions & {
id: string;
};
export function hasDestinationId(
destination: MessageEventBusDestinationOptions,
): destination is ApiMessageEventBusDestinationOptions {
return destination.id !== undefined;
}
export async function saveDestinationToDb(
context: IRestApiContext,
destination: ApiMessageEventBusDestinationOptions,
subscribedEvents: string[] = [],
) {
const data: IDataObject = {
...destination,
subscribedEvents,
};
return await makeRestApiRequest(context, 'POST', '/eventbus/destination', data);
}
export async function deleteDestinationFromDb(context: IRestApiContext, destinationId: string) {
return await makeRestApiRequest(context, 'DELETE', `/eventbus/destination?id=${destinationId}`);
}
export async function sendTestMessageToDestination(
context: IRestApiContext,
destination: ApiMessageEventBusDestinationOptions,
): Promise<boolean> {
const data: IDataObject = {
...destination,
};
return await makeRestApiRequest(context, 'GET', '/eventbus/testmessage', data);
}
export async function getEventNamesFromBackend(context: IRestApiContext): Promise<string[]> {
return await makeRestApiRequest(context, 'GET', '/eventbus/eventnames');
}
export async function getDestinationsFromBackend(
context: IRestApiContext,
): Promise<MessageEventBusDestinationOptions[]> {
return await makeRestApiRequest(context, 'GET', '/eventbus/destination');
}
@@ -0,0 +1,6 @@
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function sessionStarted(context: IRestApiContext): Promise<void> {
return await makeRestApiRequest(context, 'GET', '/events/session-started');
}
@@ -0,0 +1,79 @@
import type { ExternalSecretsProvider } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export const getExternalSecrets = async (
context: IRestApiContext,
): Promise<Record<string, string[]>> => {
return await makeRestApiRequest(context, 'GET', '/external-secrets/secrets');
};
/**
* @beta still under development
*/
export const getGlobalExternalSecrets = async (
context: IRestApiContext,
): Promise<Record<string, string[]>> => {
return await makeRestApiRequest(context, 'GET', '/secret-providers/completions/secrets/global');
};
/**
* @beta still under development
*/
export const getProjectExternalSecrets = async (
context: IRestApiContext,
projectId: string,
): Promise<Record<string, string[]>> => {
return await makeRestApiRequest(
context,
'GET',
`/secret-providers/completions/secrets/project/${projectId}`,
);
};
export const getExternalSecretsProviders = async (
context: IRestApiContext,
): Promise<ExternalSecretsProvider[]> => {
return await makeRestApiRequest(context, 'GET', '/external-secrets/providers');
};
export const getExternalSecretsProvider = async (
context: IRestApiContext,
id: string,
): Promise<ExternalSecretsProvider> => {
return await makeRestApiRequest(context, 'GET', `/external-secrets/providers/${id}`);
};
export const testExternalSecretsProviderConnection = async (
context: IRestApiContext,
id: string,
data: ExternalSecretsProvider['data'],
): Promise<{ testState: ExternalSecretsProvider['state'] }> => {
return await makeRestApiRequest(context, 'POST', `/external-secrets/providers/${id}/test`, data);
};
export const updateProvider = async (
context: IRestApiContext,
id: string,
data: ExternalSecretsProvider['data'],
): Promise<boolean> => {
return await makeRestApiRequest(context, 'POST', `/external-secrets/providers/${id}`, data);
};
export const reloadProvider = async (
context: IRestApiContext,
id: string,
): Promise<{ updated: boolean }> => {
return await makeRestApiRequest(context, 'POST', `/external-secrets/providers/${id}/update`);
};
export const connectProvider = async (
context: IRestApiContext,
id: string,
connected: boolean,
): Promise<boolean> => {
return await makeRestApiRequest(context, 'POST', `/external-secrets/providers/${id}/connect`, {
connected,
});
};
@@ -0,0 +1,31 @@
export * from './ai-usage';
export * from './api-keys';
export * from './cloudPlans';
export * from './communityNodes';
export * from './credentialResolvers';
export * from './ctas';
export * from './eventbus.ee';
export * from './events';
export * from './externalSecrets.ee';
export * from './secretsProvider.ee';
export * from './ldap';
export * from './mfa';
export * from './nodeTypes';
export * from './npsSurvey';
export * from './orchestration';
export * from './provisioning';
export * from './roles';
export * from './security-settings';
export * from './settings';
export * from './module-settings';
export * from './sso';
export type * from './tags';
export * from './templates';
export * from './third-party-licenses';
export * from './ui';
export * from './usage';
export * from './users';
export * from './versions';
export * from './webhooks';
export * from './workflowHistory';
export type * from './workflows';
@@ -0,0 +1,79 @@
import type { IDataObject } from 'n8n-workflow';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export interface LdapSyncData {
id: number;
startedAt: string;
endedAt: string;
created: number;
updated: number;
disabled: number;
scanned: number;
status: string;
error: string;
runMode: string;
}
export interface LdapSyncTable {
status: string;
endedAt: string;
runTime: string;
runMode: string;
details: string;
}
export interface LdapConfig {
loginEnabled: boolean;
loginLabel: string;
connectionUrl: string;
allowUnauthorizedCerts: boolean;
connectionSecurity: string;
connectionPort: number;
baseDn: string;
bindingAdminDn: string;
bindingAdminPassword: string;
firstNameAttribute: string;
lastNameAttribute: string;
emailAttribute: string;
loginIdAttribute: string;
ldapIdAttribute: string;
userFilter: string;
synchronizationEnabled: boolean;
synchronizationInterval: number; // minutes
searchPageSize: number;
searchTimeout: number;
enforceEmailUniqueness: boolean;
}
export async function getLdapConfig(context: IRestApiContext): Promise<LdapConfig> {
return await makeRestApiRequest(context, 'GET', '/ldap/config');
}
export async function testLdapConnection(context: IRestApiContext): Promise<{}> {
return await makeRestApiRequest(context, 'POST', '/ldap/test-connection');
}
export async function updateLdapConfig(
context: IRestApiContext,
adConfig: LdapConfig,
): Promise<LdapConfig> {
return await makeRestApiRequest(
context,
'PUT',
'/ldap/config',
adConfig as unknown as IDataObject,
);
}
export async function runLdapSync(context: IRestApiContext, data: IDataObject): Promise<{}> {
return await makeRestApiRequest(context, 'POST', '/ldap/sync', data as unknown as IDataObject);
}
export async function getLdapSynchronizations(
context: IRestApiContext,
pagination: { page: number },
): Promise<LdapSyncData[]> {
return await makeRestApiRequest(context, 'GET', '/ldap/sync', pagination);
}
@@ -0,0 +1,41 @@
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function canEnableMFA(context: IRestApiContext) {
return await makeRestApiRequest(context, 'POST', '/mfa/can-enable');
}
export async function getMfaQR(
context: IRestApiContext,
): Promise<{ qrCode: string; secret: string; recoveryCodes: string[] }> {
return await makeRestApiRequest(context, 'GET', '/mfa/qr');
}
export async function enableMfa(
context: IRestApiContext,
data: { mfaCode: string },
): Promise<void> {
return await makeRestApiRequest(context, 'POST', '/mfa/enable', data);
}
export async function verifyMfaCode(
context: IRestApiContext,
data: { mfaCode: string },
): Promise<void> {
return await makeRestApiRequest(context, 'POST', '/mfa/verify', data);
}
export type DisableMfaParams = {
mfaCode?: string;
mfaRecoveryCode?: string;
};
export async function disableMfa(context: IRestApiContext, data: DisableMfaParams): Promise<void> {
return await makeRestApiRequest(context, 'POST', '/mfa/disable', data);
}
export async function updateEnforceMfa(context: IRestApiContext, enforce: boolean): Promise<void> {
return await makeRestApiRequest(context, 'POST', '/mfa/enforce-mfa', {
enforce,
});
}
@@ -0,0 +1,8 @@
import type { FrontendModuleSettings } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function getModuleSettings(context: IRestApiContext): Promise<FrontendModuleSettings> {
return await makeRestApiRequest(context, 'GET', '/module-settings');
}
@@ -0,0 +1,145 @@
import type {
ActionResultRequestDto,
CommunityNodeType,
GetNodeTypesByIdentifierRequestDto,
OptionsRequestDto,
ResourceLocatorRequestDto,
ResourceMapperFieldsRequestDto,
} from '@n8n/api-types';
import type { INodeTranslationHeaders } from '@n8n/i18n';
import axios from 'axios';
import type {
INodeListSearchResult,
INodePropertyOptions,
INodeTypeDescription,
INodeTypeNameVersion,
NodeParameterValueType,
ResourceMapperFields,
} from 'n8n-workflow';
import { sleep } from 'n8n-workflow';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
async function fetchNodeTypesJsonWithRetry(url: string, retries = 5, delay = 500) {
for (let attempt = 0; attempt < retries; attempt++) {
const response = await axios.get(url, { withCredentials: true });
if (typeof response.data === 'object' && response.data !== null) {
return response.data;
}
await sleep(delay * attempt);
}
throw new Error('Could not fetch node types');
}
export async function getNodeTypes(baseUrl: string) {
return await fetchNodeTypesJsonWithRetry(baseUrl + 'types/nodes.json');
}
export async function getNodeTypeVersions(baseUrl: string): Promise<string[]> {
return await fetchNodeTypesJsonWithRetry(baseUrl + 'types/node-versions.json');
}
/**
* Fetch specific node types by their identifier (name@version format)
* This is useful for incremental syncs where only missing node types need to be fetched
*
* @param context - The REST API context containing base URL and auth info
* @param identifiers - Array of node type identifiers in "name@version" format
* @returns Array of node type descriptions for the requested identifiers
*/
export async function getNodeTypesByIdentifier(
context: IRestApiContext,
identifiers: string[],
): Promise<INodeTypeDescription[]> {
const body: GetNodeTypesByIdentifierRequestDto = { identifiers };
return await makeRestApiRequest(context, 'POST', '/node-types/by-identifier', body);
}
export async function fetchCommunityNodeTypes(
context: IRestApiContext,
): Promise<CommunityNodeType[]> {
return await makeRestApiRequest(context, 'GET', '/community-node-types');
}
export async function fetchCommunityNodeAttributes(
context: IRestApiContext,
type: string,
): Promise<CommunityNodeType | null> {
return await makeRestApiRequest(
context,
'GET',
`/community-node-types/${encodeURIComponent(type)}`,
);
}
export async function getNodeTranslationHeaders(
context: IRestApiContext,
): Promise<INodeTranslationHeaders | undefined> {
return await makeRestApiRequest(context, 'GET', '/node-translation-headers');
}
export async function getNodesInformation(
context: IRestApiContext,
nodeInfos: INodeTypeNameVersion[],
): Promise<INodeTypeDescription[]> {
return await makeRestApiRequest(context, 'POST', '/node-types', { nodeInfos });
}
export async function getNodeParameterOptions(
context: IRestApiContext,
sendData: OptionsRequestDto,
): Promise<INodePropertyOptions[]> {
return await makeRestApiRequest(context, 'POST', '/dynamic-node-parameters/options', sendData);
}
export async function getResourceLocatorResults(
context: IRestApiContext,
sendData: ResourceLocatorRequestDto,
): Promise<INodeListSearchResult> {
return await makeRestApiRequest(
context,
'POST',
'/dynamic-node-parameters/resource-locator-results',
sendData,
);
}
export async function getResourceMapperFields(
context: IRestApiContext,
sendData: ResourceMapperFieldsRequestDto,
): Promise<ResourceMapperFields> {
return await makeRestApiRequest(
context,
'POST',
'/dynamic-node-parameters/resource-mapper-fields',
sendData,
);
}
export async function getLocalResourceMapperFields(
context: IRestApiContext,
sendData: ResourceMapperFieldsRequestDto,
): Promise<ResourceMapperFields> {
return await makeRestApiRequest(
context,
'POST',
'/dynamic-node-parameters/local-resource-mapper-fields',
sendData,
);
}
export async function getNodeParameterActionResult(
context: IRestApiContext,
sendData: ActionResultRequestDto,
): Promise<NodeParameterValueType> {
return await makeRestApiRequest(
context,
'POST',
'/dynamic-node-parameters/action-result',
sendData,
);
}
@@ -0,0 +1,8 @@
import type { NpsSurveyState } from 'n8n-workflow';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function updateNpsSurveyState(context: IRestApiContext, state: NpsSurveyState) {
await makeRestApiRequest(context, 'PATCH', '/user-settings/nps-survey', state);
}
@@ -0,0 +1,8 @@
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
const GET_STATUS_ENDPOINT = '/orchestration/worker/status';
export const sendGetWorkerStatus = async (context: IRestApiContext): Promise<void> => {
await makeRestApiRequest(context, 'POST', GET_STATUS_ENDPOINT);
};
@@ -0,0 +1,23 @@
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export interface ProvisioningConfig {
scopesInstanceRoleClaimName: string;
scopesName: string;
scopesProjectsRolesClaimName: string;
scopesProvisionInstanceRole: boolean;
scopesProvisionProjectRoles: boolean;
}
export const getProvisioningConfig = async (
context: IRestApiContext,
): Promise<ProvisioningConfig> => {
return await makeRestApiRequest(context, 'GET', '/sso/provisioning/config');
};
export const saveProvisioningConfig = async (
context: IRestApiContext,
config: Partial<ProvisioningConfig>,
): Promise<ProvisioningConfig> => {
return await makeRestApiRequest(context, 'PATCH', '/sso/provisioning/config', config);
};
@@ -0,0 +1,59 @@
import type {
CreateRoleDto,
RoleAssignmentsResponse,
RoleProjectMembersResponse,
UpdateRoleDto,
} from '@n8n/api-types';
import type { AllRolesMap, Role } from '@n8n/permissions';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export const getRoles = async (context: IRestApiContext): Promise<AllRolesMap> => {
return await makeRestApiRequest(context, 'GET', '/roles?withUsageCount=true');
};
export const createProjectRole = async (
context: IRestApiContext,
body: CreateRoleDto,
): Promise<Role> => {
return await makeRestApiRequest(context, 'POST', '/roles', body);
};
export const getRoleBySlug = async (
context: IRestApiContext,
body: { slug: string },
): Promise<Role> => {
return await makeRestApiRequest(context, 'GET', `/roles/${body.slug}?withUsageCount=true`);
};
export const updateProjectRole = async (
context: IRestApiContext,
slug: string,
body: UpdateRoleDto,
): Promise<Role> => {
return await makeRestApiRequest(context, 'PATCH', `/roles/${slug}`, body);
};
export const deleteProjectRole = async (context: IRestApiContext, slug: string): Promise<Role> => {
return await makeRestApiRequest(context, 'DELETE', `/roles/${slug}`);
};
export const getRoleAssignments = async (
context: IRestApiContext,
slug: string,
): Promise<RoleAssignmentsResponse> => {
return await makeRestApiRequest(context, 'GET', `/roles/${slug}/assignments`);
};
export const getRoleProjectMembers = async (
context: IRestApiContext,
slug: string,
projectId: string,
): Promise<RoleProjectMembersResponse> => {
return await makeRestApiRequest(
context,
'GET',
`/roles/${slug}/assignments/${projectId}/members`,
);
};
@@ -0,0 +1,172 @@
import type {
ReloadSecretProviderConnectionResponse,
SecretProviderConnection,
SecretProviderTypeResponse,
TestSecretProviderConnectionResponse,
} from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export const getSecretProviderTypes = async (
context: IRestApiContext,
): Promise<SecretProviderTypeResponse[]> => {
return await makeRestApiRequest(context, 'GET', '/secret-providers/types');
};
export const getSecretProviderConnections = async (
context: IRestApiContext,
): Promise<SecretProviderConnection[]> => {
return await makeRestApiRequest(context, 'GET', '/secret-providers/connections');
};
export const getSecretProviderConnectionByKey = async (
context: IRestApiContext,
providerKey: string,
): Promise<SecretProviderConnection> => {
return await makeRestApiRequest(context, 'GET', `/secret-providers/connections/${providerKey}`);
};
export const createSecretProviderConnection = async (
context: IRestApiContext,
data: {
providerKey: string;
type: string;
isGlobal: boolean;
projectIds: string[];
settings: Record<string, unknown>;
},
): Promise<SecretProviderConnection> => {
return await makeRestApiRequest(context, 'POST', '/secret-providers/connections', data);
};
export const updateSecretProviderConnection = async (
context: IRestApiContext,
providerKey: string,
data: {
isGlobal: boolean;
projectIds: string[];
settings: Record<string, unknown>;
},
): Promise<SecretProviderConnection> => {
return await makeRestApiRequest(
context,
'PATCH',
`/secret-providers/connections/${providerKey}`,
data,
);
};
export const testSecretProviderConnection = async (
context: IRestApiContext,
providerKey: string,
): Promise<TestSecretProviderConnectionResponse> => {
return await makeRestApiRequest(
context,
'POST',
`/secret-providers/connections/${providerKey}/test`,
);
};
export const reloadSecretProviderConnection = async (
context: IRestApiContext,
providerKey: string,
): Promise<ReloadSecretProviderConnectionResponse> => {
return await makeRestApiRequest(
context,
'POST',
`/secret-providers/connections/${providerKey}/reload`,
);
};
export const deleteSecretProviderConnection = async (
context: IRestApiContext,
providerKey: string,
): Promise<void> => {
return await makeRestApiRequest(
context,
'DELETE',
`/secret-providers/connections/${providerKey}`,
);
};
export const getProjectSecretProviderConnectionsByProjectId = async (
context: IRestApiContext,
projectId: string,
): Promise<SecretProviderConnection[]> => {
return await makeRestApiRequest(
context,
'GET',
`/secret-providers/projects/${projectId}/connections`,
);
};
export const getProjectSecretProviderConnectionByKey = async (
context: IRestApiContext,
projectId: string,
providerKey: string,
): Promise<SecretProviderConnection> => {
return await makeRestApiRequest(
context,
'GET',
`/secret-providers/projects/${projectId}/connections/${providerKey}`,
);
};
export const createProjectSecretProviderConnection = async (
context: IRestApiContext,
projectId: string,
data: {
providerKey: string;
type: string;
projectIds: string[];
settings: Record<string, unknown>;
},
): Promise<SecretProviderConnection> => {
return await makeRestApiRequest(
context,
'POST',
`/secret-providers/projects/${projectId}/connections`,
data,
);
};
export const updateProjectSecretProviderConnection = async (
context: IRestApiContext,
projectId: string,
providerKey: string,
data: {
settings: Record<string, unknown>;
},
): Promise<SecretProviderConnection> => {
return await makeRestApiRequest(
context,
'PATCH',
`/secret-providers/projects/${projectId}/connections/${providerKey}`,
data,
);
};
export const testProjectSecretProviderConnection = async (
context: IRestApiContext,
projectId: string,
providerKey: string,
): Promise<TestSecretProviderConnectionResponse> => {
return await makeRestApiRequest(
context,
'POST',
`/secret-providers/projects/${projectId}/connections/${providerKey}/test`,
);
};
export const deleteProjectSecretProviderConnection = async (
context: IRestApiContext,
projectId: string,
providerKey: string,
): Promise<void> => {
return await makeRestApiRequest(
context,
'DELETE',
`/secret-providers/projects/${projectId}/connections/${providerKey}`,
);
};
@@ -0,0 +1,15 @@
import type { SecuritySettingsDto, UpdateSecuritySettingsDto } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function getSecuritySettings(context: IRestApiContext): Promise<SecuritySettingsDto> {
return await makeRestApiRequest(context, 'GET', '/settings/security');
}
export async function updateSecuritySettings(
context: IRestApiContext,
data: UpdateSecuritySettingsDto,
): Promise<SecuritySettingsDto> {
return await makeRestApiRequest(context, 'POST', '/settings/security', data);
}
@@ -0,0 +1,8 @@
import type { FrontendSettings } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function getSettings(context: IRestApiContext): Promise<FrontendSettings> {
return await makeRestApiRequest(context, 'GET', '/settings');
}
@@ -0,0 +1,59 @@
import type { OidcConfigDto, SamlPreferences, SamlToggleDto } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export type SamlPreferencesExtractedData = {
entityID: string;
returnUrl: string;
};
export const initSSO = async (context: IRestApiContext, redirectUrl = ''): Promise<string> => {
return await makeRestApiRequest(context, 'GET', `/sso/saml/initsso?redirect=${redirectUrl}`);
};
export const getSamlMetadata = async (context: IRestApiContext): Promise<SamlPreferences> => {
return await makeRestApiRequest(context, 'GET', '/sso/saml/metadata');
};
export const getSamlConfig = async (
context: IRestApiContext,
): Promise<SamlPreferences & SamlPreferencesExtractedData> => {
return await makeRestApiRequest(context, 'GET', '/sso/saml/config');
};
export const saveSamlConfig = async (
context: IRestApiContext,
data: Partial<SamlPreferences>,
): Promise<SamlPreferences | undefined> => {
return await makeRestApiRequest(context, 'POST', '/sso/saml/config', data);
};
export const toggleSamlConfig = async (
context: IRestApiContext,
data: SamlToggleDto,
): Promise<void> => {
return await makeRestApiRequest(context, 'POST', '/sso/saml/config/toggle', data);
};
export const testSamlConfig = async (
context: IRestApiContext,
data: Partial<SamlPreferences>,
): Promise<string> => {
return await makeRestApiRequest(context, 'POST', '/sso/saml/config/test', data);
};
export const getOidcConfig = async (context: IRestApiContext): Promise<OidcConfigDto> => {
return await makeRestApiRequest(context, 'GET', '/sso/oidc/config');
};
export const saveOidcConfig = async (
context: IRestApiContext,
data: OidcConfigDto,
): Promise<OidcConfigDto> => {
return await makeRestApiRequest(context, 'POST', '/sso/oidc/config', data);
};
export const initOidcLogin = async (context: IRestApiContext): Promise<string> => {
return await makeRestApiRequest(context, 'GET', '/sso/oidc/login');
};
@@ -0,0 +1,7 @@
export interface ITag {
id: string;
name: string;
usageCount?: number;
createdAt?: string;
updatedAt?: string;
}
@@ -0,0 +1,212 @@
import type { RawAxiosRequestHeaders } from 'axios';
import type { INode, INodeCredentialsDetails } from 'n8n-workflow';
import type { VersionNode } from './versions';
import type { WorkflowData } from './workflows';
import { get } from '../utils';
export interface IWorkflowTemplateNode
extends Pick<
INode,
'name' | 'type' | 'position' | 'parameters' | 'typeVersion' | 'webhookId' | 'id' | 'disabled'
> {
// The credentials in a template workflow have a different type than in a regular workflow
credentials?: IWorkflowTemplateNodeCredentials;
}
export interface IWorkflowTemplateNodeCredentials {
[key: string]: string | INodeCredentialsDetails;
}
export interface IWorkflowTemplate {
id: number;
name: string;
workflow: Pick<WorkflowData, 'connections' | 'settings' | 'pinData'> & {
nodes: IWorkflowTemplateNode[];
};
}
export interface ITemplatesNode extends VersionNode {
id: number;
categories?: ITemplatesCategory[];
}
export interface ITemplatesCollection {
id: number;
name: string;
nodes: ITemplatesNode[];
workflows: Array<{ id: number }>;
}
interface ITemplatesImage {
id: number;
url: string;
}
interface ITemplatesCollectionExtended extends ITemplatesCollection {
description: string | null;
image: ITemplatesImage[];
categories: ITemplatesCategory[];
createdAt: string;
}
export interface ITemplatesCollectionFull extends ITemplatesCollectionExtended {
full: true;
}
export interface ITemplatesCollectionResponse extends ITemplatesCollectionExtended {
workflows: ITemplatesWorkflow[];
}
/**
* A template without the actual workflow definition
*/
export interface ITemplatesWorkflow {
id: number;
createdAt: string;
name: string;
nodes: ITemplatesNode[];
totalViews: number;
user: {
username: string;
name: string;
avatar: string;
verified: boolean;
};
readyToDemo?: boolean | null;
}
export interface ITemplatesWorkflowInfo {
nodeCount: number;
nodeTypes: {
[key: string]: {
count: number;
};
};
}
export type TemplateSearchFacet = {
field_name: string;
sampled: boolean;
stats: {
total_values: number;
};
counts: Array<{
count: number;
highlighted: string;
value: string;
}>;
};
export interface ITemplatesWorkflowResponse extends ITemplatesWorkflow, IWorkflowTemplate {
description: string | null;
image: ITemplatesImage[];
categories: ITemplatesCategory[];
workflowInfo: ITemplatesWorkflowInfo;
}
/**
* A template with also the full workflow definition
*/
export interface ITemplatesWorkflowFull extends ITemplatesWorkflowResponse {
full: true;
}
export interface ITemplatesQuery {
categories: string[];
search: string;
apps?: string[];
nodes?: string[];
sort?: string;
combineWith?: string;
}
export interface ITemplatesCategory {
id: number;
name: string;
}
function stringifyArray(arr: string[]) {
return arr.join(',');
}
export async function testHealthEndpoint(apiEndpoint: string) {
return await get(apiEndpoint, '/health');
}
export async function getCategories(
apiEndpoint: string,
headers?: RawAxiosRequestHeaders,
): Promise<{ categories: ITemplatesCategory[] }> {
return await get(apiEndpoint, '/templates/categories', undefined, headers);
}
export async function getCollections(
apiEndpoint: string,
query: ITemplatesQuery,
headers?: RawAxiosRequestHeaders,
): Promise<{ collections: ITemplatesCollection[] }> {
return await get(
apiEndpoint,
'/templates/collections',
{ category: query.categories, search: query.search },
headers,
);
}
export async function getWorkflows(
apiEndpoint: string,
query: {
page: number;
limit: number;
categories: string[];
search: string;
sort?: string;
apps?: string[];
nodes?: string[];
combineWith?: string;
},
headers?: RawAxiosRequestHeaders,
): Promise<{
totalWorkflows: number;
workflows: ITemplatesWorkflow[];
filters: TemplateSearchFacet[];
}> {
const { apps, sort, combineWith, categories, nodes, ...restQuery } = query;
const finalQuery = {
...restQuery,
category: stringifyArray(categories),
...(apps && { apps: stringifyArray(apps) }),
...(nodes && { nodes: stringifyArray(nodes) }),
...(sort && { sort }),
...(combineWith && { combineWith }),
};
return await get(apiEndpoint, '/templates/search', finalQuery, headers);
}
export async function getCollectionById(
apiEndpoint: string,
collectionId: string,
headers?: RawAxiosRequestHeaders,
): Promise<{ collection: ITemplatesCollectionResponse }> {
return await get(apiEndpoint, `/templates/collections/${collectionId}`, undefined, headers);
}
export async function getTemplateById(
apiEndpoint: string,
templateId: string,
headers?: RawAxiosRequestHeaders,
): Promise<{ workflow: ITemplatesWorkflowResponse }> {
return await get(apiEndpoint, `/templates/workflows/${templateId}`, undefined, headers);
}
export async function getWorkflowTemplate(
apiEndpoint: string,
templateId: string,
headers?: RawAxiosRequestHeaders,
): Promise<IWorkflowTemplate> {
return await get(apiEndpoint, `/workflows/templates/${templateId}`, undefined, headers);
}
@@ -0,0 +1,10 @@
import type { IRestApiContext } from '../types';
import { request } from '../utils';
export async function getThirdPartyLicenses(context: IRestApiContext): Promise<string> {
return await request({
method: 'GET',
baseURL: context.baseUrl,
endpoint: '/third-party-licenses',
});
}
@@ -0,0 +1,13 @@
import type { BannerName } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export async function dismissBannerPermanently(
context: IRestApiContext,
data: { bannerName: BannerName; dismissedBanners: string[] },
): Promise<void> {
return await makeRestApiRequest(context, 'POST', '/owner/dismiss-banner', {
banner: data.bannerName,
});
}
@@ -0,0 +1,40 @@
import type { CommunityRegisteredRequestDto, UsageState } from '@n8n/api-types';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export const getLicense = async (context: IRestApiContext): Promise<UsageState['data']> => {
return await makeRestApiRequest(context, 'GET', '/license');
};
export const activateLicenseKey = async (
context: IRestApiContext,
data: {
activationKey: string;
eulaUri?: string;
},
): Promise<UsageState['data']> => {
return await makeRestApiRequest(context, 'POST', '/license/activate', data);
};
export const renewLicense = async (context: IRestApiContext): Promise<UsageState['data']> => {
return await makeRestApiRequest(context, 'POST', '/license/renew');
};
export const requestLicenseTrial = async (
context: IRestApiContext,
): Promise<UsageState['data']> => {
return await makeRestApiRequest(context, 'POST', '/license/enterprise/request_trial');
};
export const registerCommunityEdition = async (
context: IRestApiContext,
params: CommunityRegisteredRequestDto,
): Promise<{ title: string; text: string }> => {
return await makeRestApiRequest(
context,
'POST',
'/license/enterprise/community-registered',
params,
);
};
@@ -0,0 +1,245 @@
import type {
LoginRequestDto,
PasswordUpdateRequestDto,
SettingsUpdateRequestDto,
UserSelfSettingsUpdateRequestDto,
UsersListFilterDto,
UserUpdateRequestDto,
Role,
UsersList,
User,
} from '@n8n/api-types';
import type { Scope } from '@n8n/permissions';
import type {
FeatureFlags,
IDataObject,
IPersonalizationSurveyAnswersV4,
IUserSettings,
} from 'n8n-workflow';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
export type IPersonalizationSurveyAnswersV1 = {
codingSkill?: string | null;
companyIndustry?: string[] | null;
companySize?: string | null;
otherCompanyIndustry?: string | null;
otherWorkArea?: string | null;
workArea?: string[] | string | null;
};
export type IPersonalizationSurveyAnswersV2 = {
version: 'v2';
automationGoal?: string | null;
codingSkill?: string | null;
companyIndustryExtended?: string[] | null;
companySize?: string | null;
companyType?: string | null;
customerType?: string | null;
mspFocus?: string[] | null;
mspFocusOther?: string | null;
otherAutomationGoal?: string | null;
otherCompanyIndustryExtended?: string[] | null;
};
export type IPersonalizationSurveyAnswersV3 = {
version: 'v3';
automationGoal?: string | null;
otherAutomationGoal?: string | null;
companyIndustryExtended?: string[] | null;
otherCompanyIndustryExtended?: string[] | null;
companySize?: string | null;
companyType?: string | null;
automationGoalSm?: string[] | null;
automationGoalSmOther?: string | null;
usageModes?: string[] | null;
email?: string | null;
};
export type IPersonalizationLatestVersion = IPersonalizationSurveyAnswersV4;
export type IPersonalizationSurveyVersions =
| IPersonalizationSurveyAnswersV1
| IPersonalizationSurveyAnswersV2
| IPersonalizationSurveyAnswersV3
| IPersonalizationSurveyAnswersV4;
export interface IUserResponse extends User {
globalScopes?: Scope[];
personalizationAnswers?: IPersonalizationSurveyVersions | null;
settings?: IUserSettings | null;
}
export interface CurrentUserResponse extends IUserResponse {
featureFlags?: FeatureFlags;
}
export interface IUser extends IUserResponse {
isDefaultUser: boolean;
isPendingUser: boolean;
inviteAcceptUrl?: string;
fullName?: string;
createdAt?: string;
mfaEnabled: boolean;
mfaAuthenticated?: boolean;
}
export async function loginCurrentUser(
context: IRestApiContext,
): Promise<CurrentUserResponse | null> {
return await makeRestApiRequest(context, 'GET', '/login');
}
export async function login(
context: IRestApiContext,
params: LoginRequestDto,
): Promise<CurrentUserResponse> {
return await makeRestApiRequest(context, 'POST', '/login', params);
}
export async function logout(context: IRestApiContext): Promise<void> {
await makeRestApiRequest(context, 'POST', '/logout');
}
export async function setupOwner(
context: IRestApiContext,
params: { firstName: string; lastName: string; email: string; password: string },
): Promise<CurrentUserResponse> {
return await makeRestApiRequest(
context,
'POST',
'/owner/setup',
params as unknown as IDataObject,
);
}
export async function validateSignupToken(
context: IRestApiContext,
params: { token?: string } | { inviterId?: string; inviteeId?: string },
): Promise<{ inviter: { firstName: string; lastName: string } }> {
return await makeRestApiRequest(context, 'GET', '/resolve-signup-token', params);
}
export async function signup(
context: IRestApiContext,
params: {
inviterId: string;
inviteeId: string;
firstName: string;
lastName: string;
password: string;
},
): Promise<CurrentUserResponse> {
const { inviteeId, ...props } = params;
return await makeRestApiRequest(
context,
'POST',
`/users/${params.inviteeId}`,
props as unknown as IDataObject,
);
}
export async function sendForgotPasswordEmail(
context: IRestApiContext,
params: { email: string },
): Promise<void> {
await makeRestApiRequest(context, 'POST', '/forgot-password', params);
}
export async function validatePasswordToken(
context: IRestApiContext,
params: { token: string },
): Promise<void> {
await makeRestApiRequest(context, 'GET', '/resolve-password-token', params);
}
export async function changePassword(
context: IRestApiContext,
params: { token: string; password: string; mfaCode?: string },
): Promise<void> {
await makeRestApiRequest(context, 'POST', '/change-password', params);
}
export async function updateCurrentUser(
context: IRestApiContext,
params: UserUpdateRequestDto,
): Promise<IUserResponse> {
return await makeRestApiRequest(context, 'PATCH', '/me', params);
}
export async function updateCurrentUserSettings(
context: IRestApiContext,
settings: UserSelfSettingsUpdateRequestDto,
): Promise<IUserSettings> {
return await makeRestApiRequest(context, 'PATCH', '/me/settings', settings);
}
export async function updateOtherUserSettings(
context: IRestApiContext,
userId: string,
settings: SettingsUpdateRequestDto,
): Promise<IUserSettings> {
return await makeRestApiRequest(context, 'PATCH', `/users/${userId}/settings`, settings);
}
export async function updateCurrentUserPassword(
context: IRestApiContext,
params: PasswordUpdateRequestDto,
): Promise<void> {
return await makeRestApiRequest(context, 'PATCH', '/me/password', params);
}
export async function deleteUser(
context: IRestApiContext,
{ id, transferId }: { id: string; transferId?: string },
): Promise<void> {
await makeRestApiRequest(context, 'DELETE', `/users/${id}`, transferId ? { transferId } : {});
}
export async function getUsers(
context: IRestApiContext,
filter?: UsersListFilterDto,
): Promise<UsersList> {
return await makeRestApiRequest(context, 'GET', '/users', filter);
}
export async function getInviteLink(
context: IRestApiContext,
{ id }: { id: string },
): Promise<{ link: string }> {
return await makeRestApiRequest(context, 'GET', `/users/${id}/invite-link`);
}
export async function generateInviteLink(
context: IRestApiContext,
{ id }: { id: string },
): Promise<{ link: string }> {
return await makeRestApiRequest(context, 'POST', `/users/${id}/invite-link`);
}
export async function getPasswordResetLink(
context: IRestApiContext,
{ id }: { id: string },
): Promise<{ link: string }> {
return await makeRestApiRequest(context, 'GET', `/users/${id}/password-reset-link`);
}
export async function submitPersonalizationSurvey(
context: IRestApiContext,
params: IPersonalizationLatestVersion,
): Promise<void> {
await makeRestApiRequest(context, 'POST', '/me/survey', params as unknown as IDataObject);
}
export interface UpdateGlobalRolePayload {
id: string;
newRoleName: Role;
}
export async function updateGlobalRole(
context: IRestApiContext,
{ id, newRoleName }: UpdateGlobalRolePayload,
): Promise<IUserResponse> {
return await makeRestApiRequest(context, 'PATCH', `/users/${id}/role`, { newRoleName });
}
@@ -0,0 +1,69 @@
import { INSTANCE_ID_HEADER, INSTANCE_VERSION_HEADER } from '@n8n/constants';
import type { INodeParameters } from 'n8n-workflow';
import { get } from '../utils';
export interface VersionNode {
name: string;
displayName: string;
icon: string;
iconUrl?: string;
defaults: INodeParameters;
iconData: {
type: string;
icon?: string;
fileBuffer?: string;
};
typeVersion?: number;
}
export interface Version {
name: string;
nodes: VersionNode[];
createdAt: string;
description: string;
documentationUrl: string;
hasBreakingChange: boolean;
hasSecurityFix: boolean;
hasSecurityIssue: boolean;
securityIssueFixVersion: string;
}
export interface WhatsNewSection {
title: string;
calloutText: string;
footer: string;
items: WhatsNewArticle[];
createdAt: string;
updatedAt: string | null;
}
export interface WhatsNewArticle {
id: number;
createdAt: string;
updatedAt: string | null;
publishedAt: string;
title: string;
content: string;
}
export async function getNextVersions(
endpoint: string,
currentVersion: string,
instanceId: string,
): Promise<Version[]> {
const headers = { [INSTANCE_ID_HEADER as string]: instanceId };
return await get(endpoint, currentVersion, {}, headers);
}
export async function getWhatsNewSection(
endpoint: string,
currentVersion: string,
instanceId: string,
): Promise<WhatsNewSection> {
const headers = {
[INSTANCE_ID_HEADER as string]: instanceId,
[INSTANCE_VERSION_HEADER as string]: currentVersion,
};
return await get(endpoint, '', {}, headers);
}
@@ -0,0 +1,18 @@
import type { IHttpRequestMethods } from 'n8n-workflow';
import type { IRestApiContext } from '../types';
import { makeRestApiRequest } from '../utils';
type WebhookData = {
workflowId: string;
webhookPath: string;
method: IHttpRequestMethods;
node: string;
};
export const findWebhook = async (
context: IRestApiContext,
data: { path: string; method: string },
): Promise<WebhookData | null> => {
return await makeRestApiRequest(context, 'POST', '/webhooks/find', data);
};
@@ -0,0 +1,98 @@
import type { IConnections, INode } from 'n8n-workflow';
import type { IRestApiContext } from '../types';
import { get, patch, post } from '../utils';
export type WorkflowHistory = {
versionId: string;
authors: string;
createdAt: string;
updatedAt: string;
workflowPublishHistory: WorkflowPublishHistory[];
name: string | null;
description: string | null;
};
export type WorkflowVersionData = Pick<WorkflowHistory, 'versionId' | 'name' | 'description'>;
export type WorkflowPublishHistory = {
createdAt: string;
id: number;
event: 'activated' | 'deactivated';
userId: string | null;
versionId: string;
workflowId: string;
};
export type WorkflowVersionId = WorkflowHistory['versionId'];
export type WorkflowVersion = WorkflowHistory & {
workflowId: string;
nodes: INode[];
connections: IConnections;
};
export type WorkflowHistoryActionTypes = Array<
'restore' | 'publish' | 'unpublish' | 'clone' | 'open' | 'download' | 'name'
>;
export type WorkflowHistoryRequestParams = { take: number; skip?: number };
export type UpdateWorkflowHistoryVersion = {
nodes?: INode[];
connections?: IConnections;
authors?: string;
name?: string | null;
description?: string | null;
};
export const getWorkflowHistory = async (
context: IRestApiContext,
workflowId: string,
queryParams: WorkflowHistoryRequestParams,
): Promise<WorkflowHistory[]> => {
const { data } = await get(
context.baseUrl,
`/workflow-history/workflow/${workflowId}`,
queryParams,
);
return data;
};
export const getWorkflowVersion = async (
context: IRestApiContext,
workflowId: string,
versionId: string,
): Promise<WorkflowVersion> => {
const { data } = await get(
context.baseUrl,
`/workflow-history/workflow/${workflowId}/version/${versionId}`,
);
return data;
};
export const getWorkflowVersionsByIds = async (
context: IRestApiContext,
workflowId: string,
versionIds: string[],
): Promise<{ versions: Array<{ versionId: string; createdAt: string }> }> => {
const { data } = await post(
context.baseUrl,
`/workflow-history/workflow/${workflowId}/versions`,
{ versionIds },
);
return data;
};
export const updateWorkflowHistoryVersion = async (
context: IRestApiContext,
workflowId: string,
versionId: string,
data: UpdateWorkflowHistoryVersion,
): Promise<void> => {
await patch(
context.baseUrl,
`/workflow-history/workflow/${workflowId}/versions/${versionId}`,
data,
);
};
@@ -0,0 +1,49 @@
import type { IWorkflowSettings, IConnections, INode, IPinData } from 'n8n-workflow';
import type { ITag } from './tags';
export interface WorkflowMetadata {
onboardingId?: string;
templateId?: string;
instanceId?: string;
templateCredsSetupCompleted?: boolean;
}
// Simple version of n8n-workflow.Workflow
export interface WorkflowData {
id?: string;
name?: string;
active?: boolean;
nodes: INode[];
connections: IConnections;
settings?: IWorkflowSettings;
tags?: string[];
pinData?: IPinData;
versionId?: string;
activeVersionId?: string | null;
meta?: WorkflowMetadata;
}
export interface WorkflowDataUpdate {
id?: string;
name?: string;
description?: string | null;
nodes?: INode[];
connections?: IConnections;
settings?: IWorkflowSettings;
active?: boolean;
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
pinData?: IPinData;
versionId?: string;
meta?: WorkflowMetadata;
parentFolderId?: string;
uiContext?: string;
// checksum of workflow snapshot for conflict detection
expectedChecksum?: string;
aiBuilderAssisted?: boolean;
autosaved?: boolean;
}
export interface WorkflowDataCreate extends WorkflowDataUpdate {
projectId?: string;
}
@@ -0,0 +1,3 @@
export * from './api';
export type * from './types';
export * from './utils';
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,4 @@
export interface IRestApiContext {
baseUrl: string;
pushRef: string;
}
@@ -0,0 +1,205 @@
import { ResponseError, STREAM_SEPARATOR, streamRequest } from './utils';
describe('streamRequest', () => {
it('should stream data from the API endpoint', async () => {
const encoder = new TextEncoder();
const mockResponse = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 1 })}${STREAM_SEPARATOR}`));
controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 2 })}${STREAM_SEPARATOR}`));
controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 3 })}${STREAM_SEPARATOR}`));
controller.close();
},
});
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
body: mockResponse,
});
global.fetch = mockFetch;
const onChunkMock = vi.fn();
const onDoneMock = vi.fn();
const onErrorMock = vi.fn();
await streamRequest(
{
baseUrl: 'https://api.example.com',
pushRef: '',
},
'/data',
{ key: 'value' },
onChunkMock,
onDoneMock,
onErrorMock,
);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
method: 'POST',
body: JSON.stringify({ key: 'value' }),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'browser-id': expect.stringContaining('-'),
},
});
expect(onChunkMock).toHaveBeenCalledTimes(3);
expect(onChunkMock).toHaveBeenNthCalledWith(1, { chunk: 1 });
expect(onChunkMock).toHaveBeenNthCalledWith(2, { chunk: 2 });
expect(onChunkMock).toHaveBeenNthCalledWith(3, { chunk: 3 });
expect(onDoneMock).toHaveBeenCalledTimes(1);
expect(onErrorMock).not.toHaveBeenCalled();
});
it('should stream error response with error data from the API endpoint', async () => {
const testError = { code: 500, message: 'Error happened' };
const encoder = new TextEncoder();
const mockResponse = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(JSON.stringify(testError)));
controller.close();
},
});
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
statusText: 'Internal Server Error',
body: mockResponse,
});
global.fetch = mockFetch;
const onChunkMock = vi.fn();
const onDoneMock = vi.fn();
const onErrorMock = vi.fn();
await streamRequest(
{
baseUrl: 'https://api.example.com',
pushRef: '',
},
'/data',
{ key: 'value' },
onChunkMock,
onDoneMock,
onErrorMock,
);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
method: 'POST',
body: JSON.stringify({ key: 'value' }),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'browser-id': expect.stringContaining('-'),
},
});
expect(onChunkMock).not.toHaveBeenCalled();
expect(onDoneMock).not.toHaveBeenCalled();
expect(onErrorMock).toHaveBeenCalledExactlyOnceWith(
new ResponseError(testError.message, { httpStatusCode: 500 }),
);
});
it('should call onError when stream ends immediately with non-ok status and no chunks', async () => {
const mockResponse = new ReadableStream({
start(controller) {
// Empty stream that just closes without sending any chunks
controller.close();
},
});
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 403,
statusText: 'Forbidden',
body: mockResponse,
});
global.fetch = mockFetch;
const onChunkMock = vi.fn();
const onDoneMock = vi.fn();
const onErrorMock = vi.fn();
await streamRequest(
{
baseUrl: 'https://api.example.com',
pushRef: '',
},
'/data',
{ key: 'value' },
onChunkMock,
onDoneMock,
onErrorMock,
);
expect(onChunkMock).not.toHaveBeenCalled();
expect(onDoneMock).not.toHaveBeenCalled();
expect(onErrorMock).toHaveBeenCalledTimes(1);
expect(onErrorMock).toHaveBeenCalledExactlyOnceWith(
new ResponseError('Forbidden', { httpStatusCode: 403 }),
);
});
it('should handle broken stream data', async () => {
const encoder = new TextEncoder();
const mockResponse = new ReadableStream({
start(controller) {
controller.enqueue(
encoder.encode(`${JSON.stringify({ chunk: 1 })}${STREAM_SEPARATOR}{"chunk": `),
);
controller.enqueue(encoder.encode(`2}${STREAM_SEPARATOR}{"ch`));
controller.enqueue(encoder.encode('unk":'));
controller.enqueue(encoder.encode(`3}${STREAM_SEPARATOR}`));
controller.close();
},
});
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
body: mockResponse,
});
global.fetch = mockFetch;
const onChunkMock = vi.fn();
const onDoneMock = vi.fn();
const onErrorMock = vi.fn();
await streamRequest(
{
baseUrl: 'https://api.example.com',
pushRef: '',
},
'/data',
{ key: 'value' },
onChunkMock,
onDoneMock,
onErrorMock,
);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
method: 'POST',
body: JSON.stringify({ key: 'value' }),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'browser-id': expect.stringContaining('-'),
},
});
expect(onChunkMock).toHaveBeenCalledTimes(3);
expect(onChunkMock).toHaveBeenNthCalledWith(1, { chunk: 1 });
expect(onChunkMock).toHaveBeenNthCalledWith(2, { chunk: 2 });
expect(onChunkMock).toHaveBeenNthCalledWith(3, { chunk: 3 });
expect(onDoneMock).toHaveBeenCalledTimes(1);
expect(onErrorMock).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,335 @@
import { BROWSER_ID_STORAGE_KEY } from '@n8n/constants';
import { assert } from '@n8n/utils/assert';
import type { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios';
import axios from 'axios';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import type { GenericValue, IDataObject } from 'n8n-workflow';
import type { IRestApiContext } from './types';
const getBrowserId = () => {
let browserId = localStorage.getItem(BROWSER_ID_STORAGE_KEY);
if (!browserId) {
browserId = crypto.randomUUID();
localStorage.setItem(BROWSER_ID_STORAGE_KEY, browserId);
}
return browserId;
};
export const NO_NETWORK_ERROR_CODE = 999;
export const STREAM_SEPARATOR = '⧉⇋⇋➽⌑⧉§§\n';
export class MfaRequiredError extends ApplicationError {
constructor() {
super('MFA is required to access this resource. Please set up MFA in your user settings.');
this.name = 'MfaRequiredError';
}
}
export class ResponseError extends ApplicationError {
// The HTTP status code of response
httpStatusCode?: number;
// The error code in the response
errorCode?: number;
// The stack trace of the server
serverStackTrace?: string;
// Additional metadata from the server (e.g., EULA URL)
meta?: Record<string, unknown>;
// Additional hint from the server
hint?: string;
/**
* Creates an instance of ResponseError.
* @param {string} message The error message
* @param {number} [errorCode] The error code which can be used by frontend to identify the actual error
* @param {number} [httpStatusCode] The HTTP status code the response should have
* @param {string} [stack] The stack trace
* @param {Record<string, unknown>} [meta] Additional metadata from the server
* @param {string} [hint] Additional hint from the server
*/
constructor(
message: string,
options: {
errorCode?: number;
httpStatusCode?: number;
stack?: string;
meta?: Record<string, unknown>;
hint?: ResponseError['hint'];
} = {},
) {
super(message);
this.name = 'ResponseError';
const { errorCode, httpStatusCode, stack, meta, hint } = options;
if (errorCode) {
this.errorCode = errorCode;
}
if (httpStatusCode) {
this.httpStatusCode = httpStatusCode;
}
if (stack) {
this.serverStackTrace = stack;
}
if (meta) {
this.meta = meta;
}
if (hint) {
this.hint = hint;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacyParamSerializer = (params: Record<string, any>) =>
Object.keys(params)
.filter((key) => params[key] !== undefined)
.map((key) => {
if (Array.isArray(params[key])) {
return params[key].map((v: string) => `${key}[]=${encodeURIComponent(v)}`).join('&');
}
if (typeof params[key] === 'object') {
params[key] = JSON.stringify(params[key]);
}
return `${key}=${encodeURIComponent(params[key])}`;
})
.join('&');
export async function request(config: {
method: Method;
baseURL: string;
endpoint: string;
headers?: RawAxiosRequestHeaders;
data?: GenericValue | GenericValue[];
withCredentials?: boolean;
}) {
const { method, baseURL, endpoint, headers, data } = config;
const options: AxiosRequestConfig = {
method,
url: endpoint,
baseURL,
headers: headers ?? {},
};
if (baseURL.startsWith('/')) {
options.headers!['browser-id'] = getBrowserId();
}
if (
import.meta.env.NODE_ENV !== 'production' &&
!baseURL.includes('api.n8n.io') &&
!baseURL.includes('n8n.cloud')
) {
options.withCredentials = options.withCredentials ?? true;
}
if (['POST', 'PATCH', 'PUT'].includes(method)) {
options.data = data;
} else if (data) {
options.params = data;
options.paramsSerializer = legacyParamSerializer;
}
try {
const response = await axios.request(options);
return response.data;
} catch (error) {
if (error.message === 'Network Error') {
throw new ResponseError("Can't connect to n8n.", {
errorCode: NO_NETWORK_ERROR_CODE,
});
}
const errorResponseData = error.response?.data;
if (errorResponseData?.mfaRequired === true) {
throw new MfaRequiredError();
}
if (errorResponseData?.message !== undefined) {
if (errorResponseData.name === 'NodeApiError') {
errorResponseData.httpStatusCode = error.response.status;
throw errorResponseData;
}
throw new ResponseError(errorResponseData.message, {
errorCode: errorResponseData.code,
httpStatusCode: error.response.status,
stack: errorResponseData.stack,
meta: errorResponseData.meta,
hint: errorResponseData.hint,
});
}
throw error;
}
}
/**
* Sends a request to the API and returns the response without extracting the data key.
* @param context Rest API context
* @param method HTTP method
* @param endpoint relative path to the API endpoint
* @param data request data
* @returns data and total count
*/
export async function getFullApiResponse<T>(
context: IRestApiContext,
method: Method,
endpoint: string,
data?: GenericValue | GenericValue[],
) {
const response = await request({
method,
baseURL: context.baseUrl,
endpoint,
headers: { 'push-ref': context.pushRef },
data,
});
return response as { count: number; data: T };
}
export async function makeRestApiRequest<T>(
context: IRestApiContext,
method: Method,
endpoint: string,
data?: GenericValue | GenericValue[],
) {
const response = await request({
method,
baseURL: context.baseUrl,
endpoint,
headers: { 'push-ref': context.pushRef },
data,
});
// All cli rest api endpoints return data wrapped in `data` key
return response.data as T;
}
export async function get(
baseURL: string,
endpoint: string,
params?: IDataObject,
headers?: RawAxiosRequestHeaders,
) {
return await request({ method: 'GET', baseURL, endpoint, headers, data: params });
}
export async function post(
baseURL: string,
endpoint: string,
params?: IDataObject,
headers?: RawAxiosRequestHeaders,
) {
return await request({ method: 'POST', baseURL, endpoint, headers, data: params });
}
export async function patch(
baseURL: string,
endpoint: string,
params?: IDataObject,
headers?: RawAxiosRequestHeaders,
) {
return await request({ method: 'PATCH', baseURL, endpoint, headers, data: params });
}
export async function streamRequest<T extends object>(
context: IRestApiContext,
apiEndpoint: string,
payload: object,
onChunk?: (chunk: T) => void,
onDone?: () => void,
onError?: (e: Error) => void,
separator = STREAM_SEPARATOR,
abortSignal?: AbortSignal,
): Promise<void> {
let onErrorOnce: ((e: Error) => void) | undefined = (e: Error) => {
onErrorOnce = undefined;
onError?.(e);
};
const headers: Record<string, string> = {
'browser-id': getBrowserId(),
'Content-Type': 'application/json',
};
const assistantRequest: RequestInit = {
headers,
method: 'POST',
credentials: 'include',
body: JSON.stringify(payload),
signal: abortSignal,
};
try {
const response = await fetch(`${context.baseUrl}${apiEndpoint}`, assistantRequest);
if (response.body) {
// Handle the streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
async function readStream() {
const { done, value } = await reader.read();
if (done) {
if (response.ok) {
onDone?.();
} else {
onErrorOnce?.(
new ResponseError(response.statusText, {
httpStatusCode: response.status,
}),
);
}
return;
}
const chunk = decoder.decode(value);
buffer += chunk;
const splitChunks = buffer.split(separator);
buffer = '';
for (const splitChunk of splitChunks) {
if (splitChunk) {
let data: T;
try {
data = jsonParse<T>(splitChunk, { errorMessage: 'Invalid json' });
} catch (e) {
// incomplete json. append to buffer to complete
buffer += splitChunk;
continue;
}
try {
if (response.ok) {
// Call chunk callback if request was successful
onChunk?.(data);
} else {
// Otherwise, call error callback
const message = 'message' in data ? data.message : response.statusText;
onErrorOnce?.(
new ResponseError(String(message), {
httpStatusCode: response.status,
}),
);
}
} catch (e: unknown) {
if (e instanceof Error) {
onErrorOnce?.(e);
}
}
}
}
await readStream();
}
// Start reading the stream
await readStream();
} else if (onErrorOnce) {
onErrorOnce(new Error(response.statusText));
}
} catch (e: unknown) {
assert(e instanceof Error);
onErrorOnce?.(e);
}
}
@@ -0,0 +1,14 @@
{
"extends": "@n8n/typescript-config/tsconfig.frontend.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist",
"useUnknownInCatchVariables": false,
"types": ["vite/client", "vitest/globals"],
"isolatedModules": true,
"paths": {
"@n8n/utils/*": ["../../../@n8n/utils/src/*"]
}
},
"include": ["src/**/*.ts", "vite.config.ts", "tsdown.config.ts"]
}
@@ -0,0 +1,10 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__/**/*'],
format: ['cjs', 'esm'],
clean: true,
dts: true,
sourcemap: true,
hash: false,
});
@@ -0,0 +1,4 @@
import { defineConfig, mergeConfig } from 'vite';
import { createVitestConfig } from '@n8n/vitest-config/frontend';
export default mergeConfig(defineConfig({}), createVitestConfig({ setupFiles: [] }));