n8nac 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/commands/base.d.ts +39 -0
- package/dist/commands/base.d.ts.map +1 -1
- package/dist/commands/base.js +84 -36
- package/dist/commands/base.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +2 -2
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +28 -6
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +2 -2
- package/dist/commands/test.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/services/folder-path-resolver.d.ts +51 -0
- package/dist/core/services/folder-path-resolver.d.ts.map +1 -0
- package/dist/core/services/folder-path-resolver.js +123 -0
- package/dist/core/services/folder-path-resolver.js.map +1 -0
- package/dist/core/services/n8n-api-client.d.ts +53 -1
- package/dist/core/services/n8n-api-client.d.ts.map +1 -1
- package/dist/core/services/n8n-api-client.js +184 -2
- package/dist/core/services/n8n-api-client.js.map +1 -1
- package/dist/core/services/resolution-manager.d.ts.map +1 -1
- package/dist/core/services/resolution-manager.js +4 -4
- package/dist/core/services/resolution-manager.js.map +1 -1
- package/dist/core/services/state-manager.d.ts +1 -1
- package/dist/core/services/state-manager.d.ts.map +1 -1
- package/dist/core/services/sync-engine.d.ts +48 -1
- package/dist/core/services/sync-engine.d.ts.map +1 -1
- package/dist/core/services/sync-engine.js +200 -6
- package/dist/core/services/sync-engine.js.map +1 -1
- package/dist/core/services/sync-manager.d.ts.map +1 -1
- package/dist/core/services/sync-manager.js +10 -8
- package/dist/core/services/sync-manager.js.map +1 -1
- package/dist/core/services/tls-certificates.d.ts +105 -0
- package/dist/core/services/tls-certificates.d.ts.map +1 -0
- package/dist/core/services/tls-certificates.js +329 -0
- package/dist/core/services/tls-certificates.js.map +1 -0
- package/dist/core/services/workflow-path-utils.d.ts +56 -0
- package/dist/core/services/workflow-path-utils.d.ts.map +1 -0
- package/dist/core/services/workflow-path-utils.js +102 -0
- package/dist/core/services/workflow-path-utils.js.map +1 -0
- package/dist/core/services/workflow-state-tracker.d.ts +56 -0
- package/dist/core/services/workflow-state-tracker.d.ts.map +1 -1
- package/dist/core/services/workflow-state-tracker.js +225 -32
- package/dist/core/services/workflow-state-tracker.js.map +1 -1
- package/dist/core/services/workflow-transformer-adapter.d.ts +7 -1
- package/dist/core/services/workflow-transformer-adapter.d.ts.map +1 -1
- package/dist/core/services/workflow-transformer-adapter.js +8 -2
- package/dist/core/services/workflow-transformer-adapter.js.map +1 -1
- package/dist/core/types.d.ts +84 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/index.js +46 -10
- package/dist/index.js.map +1 -1
- package/dist/services/config-service.d.ts +33 -6
- package/dist/services/config-service.d.ts.map +1 -1
- package/dist/services/config-service.js +54 -3
- package/dist/services/config-service.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IFolder, IWorkflow } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turns an n8n folder name into a path segment that is safe on every platform.
|
|
4
|
+
*
|
|
5
|
+
* Replaces filesystem-reserved characters, collapses whitespace, strips trailing
|
|
6
|
+
* dots and spaces (which Windows silently drops), and escapes reserved device
|
|
7
|
+
* names such as `CON` or `LPT1`. Never returns an empty string — unusable names
|
|
8
|
+
* fall back to `folder`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sanitizePathSegment(name: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Turns a flat list of n8n folders into local path segments.
|
|
13
|
+
*
|
|
14
|
+
* Resolution is deterministic: the same remote tree always yields the same local
|
|
15
|
+
* layout. Two siblings whose sanitised names collide case-insensitively (which
|
|
16
|
+
* would be the same directory on Windows and macOS) are disambiguated with a
|
|
17
|
+
* short id suffix, and results are memoised per folder id.
|
|
18
|
+
*
|
|
19
|
+
* Only useful once n8n reports a workflow's folder — see the note on
|
|
20
|
+
* WorkflowStateTracker.createFolderResolver.
|
|
21
|
+
*/
|
|
22
|
+
export declare class FolderPathResolver {
|
|
23
|
+
private foldersById;
|
|
24
|
+
private memo;
|
|
25
|
+
private siblingSegmentById;
|
|
26
|
+
constructor(folders: IFolder[]);
|
|
27
|
+
/**
|
|
28
|
+
* Path segments for the folder a workflow lives in.
|
|
29
|
+
*
|
|
30
|
+
* @returns Segments from the project root down, or an empty array when the
|
|
31
|
+
* workflow sits at the root or carries no folder information.
|
|
32
|
+
*/
|
|
33
|
+
getPathForWorkflow(workflow: IWorkflow): string[];
|
|
34
|
+
/** Path segments for a folder id, root first. */
|
|
35
|
+
getPathForFolderId(folderId: string): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Precomputes one segment per folder, disambiguating case-insensitive
|
|
38
|
+
* collisions between siblings. Sorted by name then id so the folder that
|
|
39
|
+
* keeps the plain name is stable across runs.
|
|
40
|
+
*/
|
|
41
|
+
private buildSiblingSegments;
|
|
42
|
+
/**
|
|
43
|
+
* Walks a folder up to the root, memoising each result.
|
|
44
|
+
*
|
|
45
|
+
* `seen` guards against a cycle in the remote data: a folder that cannot be
|
|
46
|
+
* resolved — unknown id or a loop — lands under `_Unresolved Folder` rather
|
|
47
|
+
* than recursing forever.
|
|
48
|
+
*/
|
|
49
|
+
private resolve;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=folder-path-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-path-resolver.d.ts","sourceRoot":"","sources":["../../../src/core/services/folder-path-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAQjD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiBxD;AAED;;;;;;;;;;GAUG;AACH,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,IAAI,CAA+B;IAC3C,OAAO,CAAC,kBAAkB,CAA6B;gBAE3C,OAAO,EAAE,OAAO,EAAE;IAO9B;;;;;OAKG;IACH,kBAAkB,CAAC,QAAQ,EAAE,SAAS,GAAG,MAAM,EAAE;IAMjD,iDAAiD;IACjD,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;IAI9C;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAwB5B;;;;;;OAMG;IACH,OAAO,CAAC,OAAO;CAqBlB"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const WINDOWS_RESERVED_FILENAMES = new Set([
|
|
2
|
+
'CON', 'PRN', 'AUX', 'NUL',
|
|
3
|
+
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
|
4
|
+
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
|
|
5
|
+
]);
|
|
6
|
+
/**
|
|
7
|
+
* Turns an n8n folder name into a path segment that is safe on every platform.
|
|
8
|
+
*
|
|
9
|
+
* Replaces filesystem-reserved characters, collapses whitespace, strips trailing
|
|
10
|
+
* dots and spaces (which Windows silently drops), and escapes reserved device
|
|
11
|
+
* names such as `CON` or `LPT1`. Never returns an empty string — unusable names
|
|
12
|
+
* fall back to `folder`.
|
|
13
|
+
*/
|
|
14
|
+
export function sanitizePathSegment(name) {
|
|
15
|
+
let safeName = (name || '')
|
|
16
|
+
.replace(/[\u0000-\u001f\u007f<>:"/\\|?*]/g, '_')
|
|
17
|
+
.replace(/\s+/g, ' ')
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/[. ]+$/g, '');
|
|
20
|
+
if (!safeName || safeName === '.' || safeName === '..')
|
|
21
|
+
safeName = 'folder';
|
|
22
|
+
const firstDotIndex = safeName.indexOf('.');
|
|
23
|
+
const baseName = firstDotIndex === -1 ? safeName : safeName.slice(0, firstDotIndex);
|
|
24
|
+
const rest = firstDotIndex === -1 ? '' : safeName.slice(firstDotIndex);
|
|
25
|
+
if (WINDOWS_RESERVED_FILENAMES.has(baseName.toUpperCase())) {
|
|
26
|
+
safeName = `${baseName}_${rest}`;
|
|
27
|
+
}
|
|
28
|
+
return safeName.replace(/[. ]+$/g, '') || 'folder';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Turns a flat list of n8n folders into local path segments.
|
|
32
|
+
*
|
|
33
|
+
* Resolution is deterministic: the same remote tree always yields the same local
|
|
34
|
+
* layout. Two siblings whose sanitised names collide case-insensitively (which
|
|
35
|
+
* would be the same directory on Windows and macOS) are disambiguated with a
|
|
36
|
+
* short id suffix, and results are memoised per folder id.
|
|
37
|
+
*
|
|
38
|
+
* Only useful once n8n reports a workflow's folder — see the note on
|
|
39
|
+
* WorkflowStateTracker.createFolderResolver.
|
|
40
|
+
*/
|
|
41
|
+
export class FolderPathResolver {
|
|
42
|
+
foldersById = new Map();
|
|
43
|
+
memo = new Map();
|
|
44
|
+
siblingSegmentById = new Map();
|
|
45
|
+
constructor(folders) {
|
|
46
|
+
for (const folder of folders) {
|
|
47
|
+
if (folder?.id)
|
|
48
|
+
this.foldersById.set(folder.id, folder);
|
|
49
|
+
}
|
|
50
|
+
this.buildSiblingSegments(folders);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Path segments for the folder a workflow lives in.
|
|
54
|
+
*
|
|
55
|
+
* @returns Segments from the project root down, or an empty array when the
|
|
56
|
+
* workflow sits at the root or carries no folder information.
|
|
57
|
+
*/
|
|
58
|
+
getPathForWorkflow(workflow) {
|
|
59
|
+
const folderId = workflow.parentFolderId ?? workflow.parentFolder?.id ?? null;
|
|
60
|
+
if (!folderId)
|
|
61
|
+
return [];
|
|
62
|
+
return this.getPathForFolderId(folderId);
|
|
63
|
+
}
|
|
64
|
+
/** Path segments for a folder id, root first. */
|
|
65
|
+
getPathForFolderId(folderId) {
|
|
66
|
+
return this.resolve(folderId, new Set());
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Precomputes one segment per folder, disambiguating case-insensitive
|
|
70
|
+
* collisions between siblings. Sorted by name then id so the folder that
|
|
71
|
+
* keeps the plain name is stable across runs.
|
|
72
|
+
*/
|
|
73
|
+
buildSiblingSegments(folders) {
|
|
74
|
+
const siblings = new Map();
|
|
75
|
+
for (const folder of folders) {
|
|
76
|
+
const parentKey = folder.parentFolderId ?? '';
|
|
77
|
+
const current = siblings.get(parentKey) ?? [];
|
|
78
|
+
current.push(folder);
|
|
79
|
+
siblings.set(parentKey, current);
|
|
80
|
+
}
|
|
81
|
+
for (const group of siblings.values()) {
|
|
82
|
+
const used = new Map();
|
|
83
|
+
for (const folder of group.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id))) {
|
|
84
|
+
const base = sanitizePathSegment(folder.name);
|
|
85
|
+
let segment = base;
|
|
86
|
+
const existingId = used.get(segment.toLowerCase());
|
|
87
|
+
if (existingId && existingId !== folder.id) {
|
|
88
|
+
segment = `${base}_${folder.id.slice(0, 8)}`;
|
|
89
|
+
}
|
|
90
|
+
used.set(segment.toLowerCase(), folder.id);
|
|
91
|
+
this.siblingSegmentById.set(folder.id, segment);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Walks a folder up to the root, memoising each result.
|
|
97
|
+
*
|
|
98
|
+
* `seen` guards against a cycle in the remote data: a folder that cannot be
|
|
99
|
+
* resolved — unknown id or a loop — lands under `_Unresolved Folder` rather
|
|
100
|
+
* than recursing forever.
|
|
101
|
+
*/
|
|
102
|
+
resolve(folderId, seen) {
|
|
103
|
+
const cached = this.memo.get(folderId);
|
|
104
|
+
if (cached)
|
|
105
|
+
return cached;
|
|
106
|
+
const folder = this.foldersById.get(folderId);
|
|
107
|
+
if (!folder || seen.has(folderId)) {
|
|
108
|
+
const fallback = ['_Unresolved Folder', sanitizePathSegment(folderId)];
|
|
109
|
+
this.memo.set(folderId, fallback);
|
|
110
|
+
return fallback;
|
|
111
|
+
}
|
|
112
|
+
seen.add(folderId);
|
|
113
|
+
const parentSegments = folder.parentFolderId
|
|
114
|
+
? this.resolve(folder.parentFolderId, seen)
|
|
115
|
+
: [];
|
|
116
|
+
const segment = this.siblingSegmentById.get(folderId) ?? sanitizePathSegment(folder.name);
|
|
117
|
+
const segments = [...parentSegments, segment];
|
|
118
|
+
this.memo.set(folderId, segments);
|
|
119
|
+
seen.delete(folderId);
|
|
120
|
+
return segments;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=folder-path-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-path-resolver.js","sourceRoot":"","sources":["../../../src/core/services/folder-path-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import { IN8nCredentials, IWorkflow, IProject, ITag, ITriggerInfo, ITestPlan, ITestResult, IExecutionDetails, IExecutionList, ExecutionStatus } from '../types.js';
|
|
1
|
+
import { IN8nCredentials, IWorkflow, IProject, ITag, ITriggerInfo, ITestPlan, ITestResult, IExecutionDetails, IExecutionList, ExecutionStatus, IFolder } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a combined CA certificate bundle from the trust anchors the user configured
|
|
4
|
+
* (`NODE_EXTRA_CA_CERTS`, `N8NAC_EXTRA_CA_CERTS`) plus the OS trust store, which is the
|
|
5
|
+
* programmatic equivalent of the `--use-system-ca` flag.
|
|
6
|
+
*
|
|
7
|
+
* Returns undefined when no source provides extra certificates. When it does return a bundle,
|
|
8
|
+
* the Node.js default Mozilla roots are included, because supplying `ca` replaces them.
|
|
9
|
+
*
|
|
10
|
+
* Note that this only covers the axios agent. Anything reaching n8n through the global `fetch`
|
|
11
|
+
* — the manager-core health probe, project listing and credential calls — ignores `https.Agent`
|
|
12
|
+
* entirely, which is why {@link installExtraCaCertificates} applies the same anchors
|
|
13
|
+
* process-wide.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildCaBundle(): string[] | undefined;
|
|
2
16
|
export declare class N8nApiClient {
|
|
3
17
|
private client;
|
|
4
18
|
private apiKey;
|
|
5
19
|
private projectsCache;
|
|
6
20
|
private projectsCachePromise;
|
|
7
21
|
private static readonly PERSONAL_PROJECT_PLACEHOLDER_ID;
|
|
22
|
+
/** Memoized result of resolveFolderProjectId(); `null` means "resolved to nothing". */
|
|
23
|
+
private folderProjectIdPromise;
|
|
8
24
|
/** Shared agents keep DNS/TLS behavior consistent for API, health, and webhook calls. */
|
|
9
25
|
private httpAgent;
|
|
10
26
|
private httpsAgent;
|
|
@@ -34,6 +50,42 @@ export declare class N8nApiClient {
|
|
|
34
50
|
* @returns Array of IProject
|
|
35
51
|
*/
|
|
36
52
|
getProjects(): Promise<IProject[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Lists every folder in a project, following n8n's skip/take pagination.
|
|
55
|
+
*
|
|
56
|
+
* Requires an instance where folders are licensed (`feat:folders`, unlocked by
|
|
57
|
+
* the free Community registration) and n8n 2.19+; callers are expected to
|
|
58
|
+
* handle 403/404 by falling back to a flat layout.
|
|
59
|
+
*
|
|
60
|
+
* @param projectId Real project id — the endpoint does not accept the
|
|
61
|
+
* `personal` alias that folder creation does. See resolveFolderProjectId().
|
|
62
|
+
*/
|
|
63
|
+
getFolders(projectId: string): Promise<IFolder[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a folder in a project.
|
|
66
|
+
*
|
|
67
|
+
* @param projectId Project id, or `personal` to target the calling user's own
|
|
68
|
+
* project (n8n 2.32+).
|
|
69
|
+
* @param input `parentFolderId` nests the new folder; omit it for a
|
|
70
|
+
* project-root folder.
|
|
71
|
+
*/
|
|
72
|
+
createFolder(projectId: string, input: {
|
|
73
|
+
name: string;
|
|
74
|
+
parentFolderId?: string | null;
|
|
75
|
+
}): Promise<IFolder>;
|
|
76
|
+
/**
|
|
77
|
+
* Resolves a project id usable with the folder endpoints.
|
|
78
|
+
*
|
|
79
|
+
* `GET /api/v1/projects` is gated behind `feat:projectRole:admin` (Business /
|
|
80
|
+
* Enterprise), and `GET /projects/:id/folders` does not accept the `personal`
|
|
81
|
+
* alias that `POST` does — so on a Community instance we have no direct way to
|
|
82
|
+
* name our own project. Workflow payloads carry it though: the public API loads
|
|
83
|
+
* the `shared` relation, and `shared[0].projectId` is the owning project.
|
|
84
|
+
*
|
|
85
|
+
* Returns null when nothing could be resolved (e.g. no workflows exist yet), in
|
|
86
|
+
* which case callers should skip folder assignment rather than fail the sync.
|
|
87
|
+
*/
|
|
88
|
+
resolveFolderProjectId(preferredProjectId?: string): Promise<string | null>;
|
|
37
89
|
/**
|
|
38
90
|
* Fetches all projects from n8n and caches them.
|
|
39
91
|
* Returns a Map of projectId -> IProject for quick lookups.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../../src/core/services/n8n-api-client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../../src/core/services/n8n-api-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,31 @@ import axios from 'axios';
|
|
|
2
2
|
import * as dns from 'dns';
|
|
3
3
|
import * as http from 'http';
|
|
4
4
|
import * as https from 'https';
|
|
5
|
+
import * as tls from 'tls';
|
|
6
|
+
import { resolveExtraCaCertificates, shouldVerifyServerCertificate } from './tls-certificates.js';
|
|
7
|
+
/**
|
|
8
|
+
* Builds a combined CA certificate bundle from the trust anchors the user configured
|
|
9
|
+
* (`NODE_EXTRA_CA_CERTS`, `N8NAC_EXTRA_CA_CERTS`) plus the OS trust store, which is the
|
|
10
|
+
* programmatic equivalent of the `--use-system-ca` flag.
|
|
11
|
+
*
|
|
12
|
+
* Returns undefined when no source provides extra certificates. When it does return a bundle,
|
|
13
|
+
* the Node.js default Mozilla roots are included, because supplying `ca` replaces them.
|
|
14
|
+
*
|
|
15
|
+
* Note that this only covers the axios agent. Anything reaching n8n through the global `fetch`
|
|
16
|
+
* — the manager-core health probe, project listing and credential calls — ignores `https.Agent`
|
|
17
|
+
* entirely, which is why {@link installExtraCaCertificates} applies the same anchors
|
|
18
|
+
* process-wide.
|
|
19
|
+
*/
|
|
20
|
+
export function buildCaBundle() {
|
|
21
|
+
return resolveAgentTrust().ca;
|
|
22
|
+
}
|
|
23
|
+
function resolveAgentTrust() {
|
|
24
|
+
const resolution = resolveExtraCaCertificates({ useSystemCertificateAuthorities: true });
|
|
25
|
+
return {
|
|
26
|
+
ca: resolution.certificates.length ? [...tls.rootCertificates, ...resolution.certificates] : undefined,
|
|
27
|
+
hasConfiguredAnchors: resolution.hasConfiguredAnchors,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
5
30
|
function createIpv4FirstLookup() {
|
|
6
31
|
return ((hostname, options, callback) => {
|
|
7
32
|
const lookupOptions = { ...options, verbatim: false };
|
|
@@ -25,6 +50,8 @@ export class N8nApiClient {
|
|
|
25
50
|
projectsCache = null;
|
|
26
51
|
projectsCachePromise = null;
|
|
27
52
|
static PERSONAL_PROJECT_PLACEHOLDER_ID = 'personal';
|
|
53
|
+
/** Memoized result of resolveFolderProjectId(); `null` means "resolved to nothing". */
|
|
54
|
+
folderProjectIdPromise = null;
|
|
28
55
|
/** Shared agents keep DNS/TLS behavior consistent for API, health, and webhook calls. */
|
|
29
56
|
httpAgent;
|
|
30
57
|
httpsAgent;
|
|
@@ -34,12 +61,23 @@ export class N8nApiClient {
|
|
|
34
61
|
if (host.endsWith('/')) {
|
|
35
62
|
host = host.slice(0, -1);
|
|
36
63
|
}
|
|
37
|
-
// Allow self-signed certificates by default to avoid issues in local environments.
|
|
38
64
|
// Prefer IPv4 when both A and AAAA records exist; remote self-hosted n8n instances
|
|
39
65
|
// often publish IPv6 records that are unreachable from the extension host.
|
|
40
66
|
const lookup = createIpv4FirstLookup();
|
|
41
67
|
this.httpAgent = new http.Agent({ lookup });
|
|
42
|
-
|
|
68
|
+
// Trust whatever the user configured (--use-system-ca / NODE_EXTRA_CA_CERTS) and verify
|
|
69
|
+
// against it. Verification is only relaxed for a loopback or private-network host that
|
|
70
|
+
// the user has not configured an anchor for: those cannot hold a publicly trusted
|
|
71
|
+
// certificate, so a self-signed one there is the normal setup rather than an attack, and
|
|
72
|
+
// that is the backward compatibility the fallback exists for. A public host name is
|
|
73
|
+
// always verified — there is no benign reason to skip it, and skipping it is what makes
|
|
74
|
+
// the connection interceptable. See `shouldVerifyServerCertificate` for the opt-out.
|
|
75
|
+
const { ca, hasConfiguredAnchors } = resolveAgentTrust();
|
|
76
|
+
this.httpsAgent = new https.Agent({
|
|
77
|
+
rejectUnauthorized: shouldVerifyServerCertificate({ host, hasConfiguredAnchors }),
|
|
78
|
+
ca,
|
|
79
|
+
lookup,
|
|
80
|
+
});
|
|
43
81
|
this.client = axios.create({
|
|
44
82
|
baseURL: host,
|
|
45
83
|
headers: {
|
|
@@ -251,6 +289,128 @@ export class N8nApiClient {
|
|
|
251
289
|
return [];
|
|
252
290
|
}
|
|
253
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Lists every folder in a project, following n8n's skip/take pagination.
|
|
294
|
+
*
|
|
295
|
+
* Requires an instance where folders are licensed (`feat:folders`, unlocked by
|
|
296
|
+
* the free Community registration) and n8n 2.19+; callers are expected to
|
|
297
|
+
* handle 403/404 by falling back to a flat layout.
|
|
298
|
+
*
|
|
299
|
+
* @param projectId Real project id — the endpoint does not accept the
|
|
300
|
+
* `personal` alias that folder creation does. See resolveFolderProjectId().
|
|
301
|
+
*/
|
|
302
|
+
async getFolders(projectId) {
|
|
303
|
+
const folders = [];
|
|
304
|
+
const seen = new Set();
|
|
305
|
+
let skip = 0;
|
|
306
|
+
const take = 100;
|
|
307
|
+
let useSelect = true;
|
|
308
|
+
while (true) {
|
|
309
|
+
const params = {
|
|
310
|
+
skip: String(skip),
|
|
311
|
+
take: String(take),
|
|
312
|
+
};
|
|
313
|
+
if (useSelect) {
|
|
314
|
+
// Field names come from n8n's ListFolderQueryDto allow-list
|
|
315
|
+
// (id, name, createdAt, updatedAt, project, tags, parentFolder,
|
|
316
|
+
// workflowCount, subFolderCount, path). `parentFolderId` is NOT
|
|
317
|
+
// a valid select field — asking for it makes n8n reject the whole
|
|
318
|
+
// query with a 400, which used to send every call through the
|
|
319
|
+
// no-select fallback below.
|
|
320
|
+
params.select = JSON.stringify(['id', 'name', 'parentFolder', 'path', 'createdAt', 'updatedAt']);
|
|
321
|
+
}
|
|
322
|
+
let res;
|
|
323
|
+
try {
|
|
324
|
+
res = await this.client.get(`/api/v1/projects/${encodeURIComponent(projectId)}/folders`, { params });
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (useSelect && error?.response?.status === 400) {
|
|
328
|
+
useSelect = false;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
const data = Array.isArray(res.data?.data) ? res.data.data : (Array.isArray(res.data) ? res.data : []);
|
|
334
|
+
const beforeCount = folders.length;
|
|
335
|
+
for (const folder of data) {
|
|
336
|
+
if (!folder?.id || seen.has(folder.id))
|
|
337
|
+
continue;
|
|
338
|
+
seen.add(folder.id);
|
|
339
|
+
folders.push({
|
|
340
|
+
id: folder.id,
|
|
341
|
+
name: folder.name ?? 'Folder',
|
|
342
|
+
parentFolderId: folder.parentFolderId ?? folder.parentFolder?.id ?? null,
|
|
343
|
+
path: folder.path,
|
|
344
|
+
createdAt: folder.createdAt,
|
|
345
|
+
updatedAt: folder.updatedAt,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const count = typeof res.data?.count === 'number' ? res.data.count : undefined;
|
|
349
|
+
if (count !== undefined && folders.length >= count)
|
|
350
|
+
break;
|
|
351
|
+
if (folders.length === beforeCount)
|
|
352
|
+
break;
|
|
353
|
+
if (data.length === 0 || (count === undefined && data.length < take))
|
|
354
|
+
break;
|
|
355
|
+
skip += take;
|
|
356
|
+
}
|
|
357
|
+
return folders;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Creates a folder in a project.
|
|
361
|
+
*
|
|
362
|
+
* @param projectId Project id, or `personal` to target the calling user's own
|
|
363
|
+
* project (n8n 2.32+).
|
|
364
|
+
* @param input `parentFolderId` nests the new folder; omit it for a
|
|
365
|
+
* project-root folder.
|
|
366
|
+
*/
|
|
367
|
+
async createFolder(projectId, input) {
|
|
368
|
+
const payload = { name: input.name };
|
|
369
|
+
if (input.parentFolderId)
|
|
370
|
+
payload.parentFolderId = input.parentFolderId;
|
|
371
|
+
const res = await this.client.post(`/api/v1/projects/${encodeURIComponent(projectId)}/folders`, payload);
|
|
372
|
+
return {
|
|
373
|
+
id: res.data.id,
|
|
374
|
+
name: res.data.name,
|
|
375
|
+
parentFolderId: res.data.parentFolderId ?? res.data.parentFolder?.id ?? null,
|
|
376
|
+
path: res.data.path,
|
|
377
|
+
createdAt: res.data.createdAt,
|
|
378
|
+
updatedAt: res.data.updatedAt,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Resolves a project id usable with the folder endpoints.
|
|
383
|
+
*
|
|
384
|
+
* `GET /api/v1/projects` is gated behind `feat:projectRole:admin` (Business /
|
|
385
|
+
* Enterprise), and `GET /projects/:id/folders` does not accept the `personal`
|
|
386
|
+
* alias that `POST` does — so on a Community instance we have no direct way to
|
|
387
|
+
* name our own project. Workflow payloads carry it though: the public API loads
|
|
388
|
+
* the `shared` relation, and `shared[0].projectId` is the owning project.
|
|
389
|
+
*
|
|
390
|
+
* Returns null when nothing could be resolved (e.g. no workflows exist yet), in
|
|
391
|
+
* which case callers should skip folder assignment rather than fail the sync.
|
|
392
|
+
*/
|
|
393
|
+
async resolveFolderProjectId(preferredProjectId) {
|
|
394
|
+
if (preferredProjectId && !this.isPlaceholderPersonalProjectId(preferredProjectId)) {
|
|
395
|
+
return preferredProjectId;
|
|
396
|
+
}
|
|
397
|
+
this.folderProjectIdPromise ??= (async () => {
|
|
398
|
+
try {
|
|
399
|
+
const res = await this.client.get('/api/v1/workflows', { params: { limit: 1 } });
|
|
400
|
+
const data = res.data?.data ?? (Array.isArray(res.data) ? res.data : []);
|
|
401
|
+
const workflow = Array.isArray(data) ? data[0] : undefined;
|
|
402
|
+
const shared = Array.isArray(workflow?.shared) ? workflow.shared[0] : undefined;
|
|
403
|
+
return shared?.projectId ?? shared?.project?.id ?? null;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
if (process.env.DEBUG) {
|
|
407
|
+
console.debug(`[N8nApiClient] Could not resolve project id from workflows: ${error?.message || error}`);
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
})();
|
|
412
|
+
return await this.folderProjectIdPromise;
|
|
413
|
+
}
|
|
254
414
|
/**
|
|
255
415
|
* Fetches all projects from n8n and caches them.
|
|
256
416
|
* Returns a Map of projectId -> IProject for quick lookups.
|
|
@@ -510,6 +670,23 @@ export class N8nApiClient {
|
|
|
510
670
|
if (workflow.isArchived !== undefined) {
|
|
511
671
|
enriched.isArchived = workflow.isArchived;
|
|
512
672
|
}
|
|
673
|
+
if ('parentFolderId' in workflow) {
|
|
674
|
+
enriched.parentFolderId = workflow.parentFolderId ?? null;
|
|
675
|
+
}
|
|
676
|
+
else if (workflow.parentFolder?.id) {
|
|
677
|
+
enriched.parentFolderId = workflow.parentFolder.id;
|
|
678
|
+
}
|
|
679
|
+
if ('parentFolder' in workflow) {
|
|
680
|
+
enriched.parentFolder = workflow.parentFolder ?? null;
|
|
681
|
+
}
|
|
682
|
+
if (workflow.folderPath && Array.isArray(workflow.folderPath)) {
|
|
683
|
+
enriched.folderPath = workflow.folderPath;
|
|
684
|
+
enriched.folderPathString = workflow.folderPath.join('/');
|
|
685
|
+
}
|
|
686
|
+
else if (typeof workflow.folderPathString === 'string') {
|
|
687
|
+
enriched.folderPathString = workflow.folderPathString;
|
|
688
|
+
enriched.folderPath = workflow.folderPathString.split('/').filter(Boolean);
|
|
689
|
+
}
|
|
513
690
|
return enriched;
|
|
514
691
|
}
|
|
515
692
|
async createWorkflow(payload) {
|
|
@@ -542,6 +719,7 @@ export class N8nApiClient {
|
|
|
542
719
|
'settings',
|
|
543
720
|
'staticData',
|
|
544
721
|
'projectId',
|
|
722
|
+
'parentFolderId',
|
|
545
723
|
]);
|
|
546
724
|
const clean = {};
|
|
547
725
|
for (const [key, value] of Object.entries(payload)) {
|
|
@@ -641,6 +819,10 @@ export class N8nApiClient {
|
|
|
641
819
|
'settings',
|
|
642
820
|
'staticData',
|
|
643
821
|
'pinData',
|
|
822
|
+
// parentFolderId is forwarded on update so a folderSync push of an
|
|
823
|
+
// already-tracked workflow moves it to the inferred folder on n8n.
|
|
824
|
+
// The create path was already wired up; the update path was missing.
|
|
825
|
+
'parentFolderId',
|
|
644
826
|
]);
|
|
645
827
|
const clean = {};
|
|
646
828
|
for (const [key, value] of Object.entries(payload)) {
|