@tadnt2003/n8n-nodes-infisical 0.2.0 → 0.3.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.
@@ -0,0 +1,750 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeSyncOperation = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ // Maps each supported credential type to the fields to sync.
6
+ // `param` is the n8n parameter name; `secretKey` is the Infisical secret key name.
7
+ const CREDENTIAL_FIELD_MAPS = {
8
+ googleApi: [
9
+ { param: 'email', secretKey: 'email' },
10
+ { param: 'privateKey', secretKey: 'privateKey' },
11
+ { param: 'delegatedEmail', secretKey: 'delegatedEmail' },
12
+ { param: 'scopes', secretKey: 'scopes' },
13
+ // Fix 7.1: condition-controlling fields needed for delegated auth and HTTP scopes branches
14
+ { param: 'inpersonate', secretKey: 'inpersonate' },
15
+ { param: 'httpNode', secretKey: 'httpNode' },
16
+ ],
17
+ googleOAuth2Api: [
18
+ { param: 'clientId', secretKey: 'clientId' },
19
+ { param: 'clientSecret', secretKey: 'clientSecret' },
20
+ { param: 'scope', secretKey: 'scope' },
21
+ ],
22
+ jiraSoftwareCloudApi: [
23
+ { param: 'email', secretKey: 'email' },
24
+ { param: 'apiToken', secretKey: 'apiToken' },
25
+ { param: 'domain', secretKey: 'domain' },
26
+ ],
27
+ openAiApi: [
28
+ { param: 'apiKey', secretKey: 'apiKey' },
29
+ { param: 'organizationId', secretKey: 'organizationId' },
30
+ { param: 'url', secretKey: 'url' },
31
+ ],
32
+ anthropicApi: [
33
+ { param: 'apiKey', secretKey: 'apiKey' },
34
+ { param: 'url', secretKey: 'url' },
35
+ ],
36
+ groqApi: [{ param: 'apiKey', secretKey: 'apiKey' }],
37
+ cohereApi: [{ param: 'apiKey', secretKey: 'apiKey' }],
38
+ huggingFaceApi: [{ param: 'apiKey', secretKey: 'apiKey' }],
39
+ mistralCloudApi: [{ param: 'apiKey', secretKey: 'apiKey' }],
40
+ discordBotApi: [{ param: 'botToken', secretKey: 'botToken' }],
41
+ discordWebhookApi: [{ param: 'webhookUri', secretKey: 'webhookUri' }],
42
+ mySql: [
43
+ { param: 'host', secretKey: 'host' },
44
+ { param: 'database', secretKey: 'database' },
45
+ { param: 'user', secretKey: 'user' },
46
+ { param: 'password', secretKey: 'password' },
47
+ { param: 'port', secretKey: 'port' },
48
+ { param: 'ssl', secretKey: 'ssl' },
49
+ { param: 'sshTunnel', secretKey: 'sshTunnel' },
50
+ { param: 'sshHost', secretKey: 'sshHost' },
51
+ { param: 'sshPort', secretKey: 'sshPort' },
52
+ { param: 'sshUser', secretKey: 'sshUser' },
53
+ { param: 'sshPassword', secretKey: 'sshPassword' },
54
+ // Fix 7.1: SSH key-auth fields for the sshTunnel conditional branch
55
+ { param: 'sshAuthenticateWith', secretKey: 'sshAuthenticateWith' },
56
+ { param: 'privateKey', secretKey: 'privateKey' },
57
+ { param: 'passphrase', secretKey: 'passphrase' },
58
+ ],
59
+ postgres: [
60
+ { param: 'host', secretKey: 'host' },
61
+ { param: 'database', secretKey: 'database' },
62
+ { param: 'user', secretKey: 'user' },
63
+ { param: 'password', secretKey: 'password' },
64
+ { param: 'port', secretKey: 'port' },
65
+ { param: 'ssl', secretKey: 'ssl' },
66
+ // Fix 7.1: controls which SSL branch fires; missing caused wrong defaults on create
67
+ { param: 'allowUnauthorizedCerts', secretKey: 'allowUnauthorizedCerts' },
68
+ { param: 'sshTunnel', secretKey: 'sshTunnel' },
69
+ { param: 'sshHost', secretKey: 'sshHost' },
70
+ { param: 'sshPort', secretKey: 'sshPort' },
71
+ { param: 'sshUser', secretKey: 'sshUser' },
72
+ { param: 'sshPassword', secretKey: 'sshPassword' },
73
+ // Fix 7.1: SSH key-auth fields for the sshTunnel conditional branch
74
+ { param: 'sshAuthenticateWith', secretKey: 'sshAuthenticateWith' },
75
+ { param: 'privateKey', secretKey: 'privateKey' },
76
+ { param: 'passphrase', secretKey: 'passphrase' },
77
+ ],
78
+ mongoDb: [
79
+ { param: 'configurationType', secretKey: 'configurationType' },
80
+ { param: 'connectionString', secretKey: 'connectionString' },
81
+ { param: 'host', secretKey: 'host' },
82
+ { param: 'database', secretKey: 'database' },
83
+ { param: 'user', secretKey: 'user' },
84
+ { param: 'password', secretKey: 'password' },
85
+ { param: 'port', secretKey: 'port' },
86
+ { param: 'tls', secretKey: 'tls' },
87
+ ],
88
+ redis: [
89
+ { param: 'host', secretKey: 'host' },
90
+ { param: 'port', secretKey: 'port' },
91
+ { param: 'user', secretKey: 'user' },
92
+ { param: 'password', secretKey: 'password' },
93
+ { param: 'database', secretKey: 'database' },
94
+ { param: 'ssl', secretKey: 'ssl' },
95
+ ],
96
+ microsoftSql: [
97
+ { param: 'server', secretKey: 'server' },
98
+ { param: 'database', secretKey: 'database' },
99
+ { param: 'user', secretKey: 'user' },
100
+ { param: 'password', secretKey: 'password' },
101
+ { param: 'port', secretKey: 'port' },
102
+ { param: 'domain', secretKey: 'domain' },
103
+ ],
104
+ googleSheetsOAuth2Api: [
105
+ { param: 'clientId', secretKey: 'clientId' },
106
+ { param: 'clientSecret', secretKey: 'clientSecret' },
107
+ // condition-controlling field: drives the allowedDomains conditional branch
108
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
109
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
110
+ ],
111
+ googleDriveOAuth2Api: [
112
+ { param: 'clientId', secretKey: 'clientId' },
113
+ { param: 'clientSecret', secretKey: 'clientSecret' },
114
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
115
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
116
+ ],
117
+ googleDocsOAuth2Api: [
118
+ { param: 'clientId', secretKey: 'clientId' },
119
+ { param: 'clientSecret', secretKey: 'clientSecret' },
120
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
121
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
122
+ ],
123
+ n8nApi: [
124
+ { param: 'apiKey', secretKey: 'apiKey' },
125
+ { param: 'baseUrl', secretKey: 'baseUrl' },
126
+ // condition-controlling field: drives the allowedDomains conditional branch
127
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
128
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
129
+ ],
130
+ infisicalApi: [
131
+ { param: 'apiUrl', secretKey: 'apiUrl' },
132
+ // condition-controlling field: universalAuth vs serviceToken branches
133
+ { param: 'authType', secretKey: 'authType' },
134
+ { param: 'clientId', secretKey: 'clientId' },
135
+ { param: 'clientSecret', secretKey: 'clientSecret' },
136
+ { param: 'organizationSlug', secretKey: 'organizationSlug' },
137
+ // service token value (only active when authType === 'serviceToken')
138
+ { param: 'apiKey', secretKey: 'apiKey' },
139
+ ],
140
+ // ── Generic HTTP auth types ────────────────────────────────────────────────
141
+ httpBearerAuth: [
142
+ { param: 'token', secretKey: 'token' },
143
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
144
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
145
+ ],
146
+ httpBasicAuth: [
147
+ { param: 'user', secretKey: 'user' },
148
+ { param: 'password', secretKey: 'password' },
149
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
150
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
151
+ ],
152
+ httpDigestAuth: [
153
+ { param: 'user', secretKey: 'user' },
154
+ { param: 'password', secretKey: 'password' },
155
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
156
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
157
+ ],
158
+ httpHeaderAuth: [
159
+ { param: 'name', secretKey: 'name' },
160
+ { param: 'value', secretKey: 'value' },
161
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
162
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
163
+ ],
164
+ httpQueryAuth: [
165
+ { param: 'name', secretKey: 'name' },
166
+ { param: 'value', secretKey: 'value' },
167
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
168
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
169
+ ],
170
+ httpCustomAuth: [
171
+ { param: 'json', secretKey: 'json' },
172
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
173
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
174
+ ],
175
+ httpSslAuth: [
176
+ { param: 'ca', secretKey: 'ca' },
177
+ { param: 'cert', secretKey: 'cert' },
178
+ { param: 'key', secretKey: 'key' },
179
+ { param: 'passphrase', secretKey: 'passphrase' },
180
+ ],
181
+ oAuth1Api: [
182
+ { param: 'signatureMethod', secretKey: 'signatureMethod' },
183
+ { param: 'consumerKey', secretKey: 'consumerKey' },
184
+ { param: 'consumerSecret', secretKey: 'consumerSecret' },
185
+ { param: 'requestTokenUrl', secretKey: 'requestTokenUrl' },
186
+ { param: 'authUrl', secretKey: 'authUrl' },
187
+ { param: 'accessTokenUrl', secretKey: 'accessTokenUrl' },
188
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
189
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
190
+ ],
191
+ oAuth2Api: [
192
+ // condition-controlling: grantType drives authUrl/authQueryParameters branch
193
+ { param: 'grantType', secretKey: 'grantType' },
194
+ { param: 'authUrl', secretKey: 'authUrl' },
195
+ { param: 'accessTokenUrl', secretKey: 'accessTokenUrl' },
196
+ { param: 'clientId', secretKey: 'clientId' },
197
+ { param: 'clientSecret', secretKey: 'clientSecret' },
198
+ { param: 'scope', secretKey: 'scope' },
199
+ { param: 'authQueryParameters', secretKey: 'authQueryParameters' },
200
+ { param: 'authentication', secretKey: 'authentication' },
201
+ { param: 'allowedHttpRequestDomains', secretKey: 'allowedHttpRequestDomains' },
202
+ { param: 'allowedDomains', secretKey: 'allowedDomains' },
203
+ ],
204
+ jwtAuth: [
205
+ // condition-controlling: keyType drives secret vs privateKey/publicKey branches
206
+ { param: 'keyType', secretKey: 'keyType' },
207
+ { param: 'secret', secretKey: 'secret' },
208
+ { param: 'privateKey', secretKey: 'privateKey' },
209
+ { param: 'publicKey', secretKey: 'publicKey' },
210
+ { param: 'algorithm', secretKey: 'algorithm' },
211
+ ],
212
+ };
213
+ function buildSecretPath(rootPath, credentialName) {
214
+ const root = rootPath.replace(/\/+$/, '') || '/';
215
+ return root === '/' ? `/${credentialName}` : `${root}/${credentialName}`;
216
+ }
217
+ // Fix 7.5: removed `typeof value === 'boolean' && value === false` — false is a meaningful value
218
+ // that must be written to Infisical (e.g. ssl:false, sshTunnel:false as condition keys).
219
+ function isEmptyValue(value) {
220
+ if (value === null || value === undefined)
221
+ return true;
222
+ if (typeof value === 'string' && value.trim() === '')
223
+ return true;
224
+ return false;
225
+ }
226
+ // Fix 7.4: handles `number` type (port, connectTimeout, maxConnections, etc.)
227
+ // Fix 7.7: reads schema's own `default` before falling back to enum heuristic
228
+ function applyDefaultForProp(key, def, defaults, required) {
229
+ var _a;
230
+ if (required.has(key) || key in defaults)
231
+ return;
232
+ if (Array.isArray(def.enum) && def.enum.length > 0) {
233
+ defaults[key] = ((_a = def.default) !== null && _a !== void 0 ? _a : (key === 'allowedHttpRequestDomains' ? 'all' : def.enum[0]));
234
+ }
235
+ else if (def.type === 'boolean') {
236
+ defaults[key] = false;
237
+ }
238
+ else if (def.type === 'string') {
239
+ defaults[key] = '';
240
+ }
241
+ else if (def.type === 'number') {
242
+ defaults[key] = 0;
243
+ }
244
+ else if (def.type === 'json') {
245
+ defaults[key] = '{}';
246
+ }
247
+ }
248
+ // Coerce a string value from Infisical to the type the n8n schema expects.
249
+ function coerceValue(raw, def) {
250
+ if (!def)
251
+ return raw;
252
+ if (def.type === 'number') {
253
+ const n = Number(raw);
254
+ return Number.isNaN(n) ? raw : n;
255
+ }
256
+ if (def.type === 'boolean')
257
+ return raw === 'true' || raw === '1';
258
+ return raw;
259
+ }
260
+ // Validate credential data against the n8n schema's required fields and conditional requirements.
261
+ // For form mode, pass availableFormFields to skip checks for fields the form cannot provide.
262
+ function validateAgainstSchema(data, schemaInfo, availableFormFields) {
263
+ const errors = [];
264
+ for (const field of schemaInfo.topRequired) {
265
+ if (availableFormFields && !availableFormFields.has(field))
266
+ continue;
267
+ if (isEmptyValue(data[field])) {
268
+ errors.push(`"${field}" is required but missing or empty`);
269
+ }
270
+ }
271
+ for (const { condKey, condValues, thenRequired } of schemaInfo.condBranches) {
272
+ const condVal = data[condKey];
273
+ const condKeyInSchema = condKey in schemaInfo.props;
274
+ if (!condKeyInSchema || condValues.includes(condVal)) {
275
+ for (const field of thenRequired) {
276
+ if (availableFormFields && !availableFormFields.has(field))
277
+ continue;
278
+ if (isEmptyValue(data[field])) {
279
+ const when = condKeyInSchema ? ` when "${condKey}" is "${String(condVal)}"` : '';
280
+ errors.push(`"${field}" is required${when} but missing or empty`);
281
+ }
282
+ }
283
+ }
284
+ }
285
+ return errors;
286
+ }
287
+ async function syncFromInfisical(ctx, apiUrl, baseHeaders, i) {
288
+ var _a;
289
+ const projectId = ctx.getNodeParameter('projectId', i);
290
+ const environment = ctx.getNodeParameter('environment', i);
291
+ const rootPath = ctx.getNodeParameter('rootPath', i, '/') || '/';
292
+ const credentialName = ctx.getNodeParameter('credentialName', i);
293
+ const n8nCredentialId = ctx.getNodeParameter('n8nCredentialId', i);
294
+ const n8nCreds = await ctx.getCredentials('n8nApi');
295
+ const n8nApiUrl = (n8nCreds.baseUrl || 'http://localhost:5678').replace(/\/$/, '').replace(/\/api\/v1$/, '');
296
+ const n8nApiKey = n8nCreds.apiKey;
297
+ const secretPath = buildSecretPath(rootPath, credentialName);
298
+ // 1. Read all secrets from the Infisical folder
299
+ const infisicalResponse = await ctx.helpers.httpRequest({
300
+ method: 'GET',
301
+ url: `${apiUrl}/v4/secrets`,
302
+ headers: baseHeaders,
303
+ qs: { projectId, environment, secretPath },
304
+ });
305
+ const secrets = ((_a = infisicalResponse.secrets) !== null && _a !== void 0 ? _a : []);
306
+ if (secrets.length === 0) {
307
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `No secrets found at path "${secretPath}" in environment "${environment}"`, { itemIndex: i });
308
+ }
309
+ // 2. Build n8n credential data from Infisical secret key/value pairs
310
+ const credentialData = {};
311
+ for (const secret of secrets) {
312
+ credentialData[secret.secretKey] = secret.secretValue;
313
+ }
314
+ // 3. Update the n8n credential via its REST API
315
+ const updated = await ctx.helpers.httpRequest({
316
+ method: 'PATCH',
317
+ url: `${n8nApiUrl}/api/v1/credentials/${n8nCredentialId}`,
318
+ headers: { 'X-N8N-API-KEY': n8nApiKey, 'Content-Type': 'application/json' },
319
+ body: { data: credentialData },
320
+ });
321
+ return [{ json: updated, pairedItem: { item: i } }];
322
+ }
323
+ // Collect field names from then/else allOf sub-schemas.
324
+ function collectClauseFields(clause) {
325
+ var _a, _b, _c, _d, _e;
326
+ const required = [];
327
+ const notRequired = [];
328
+ if (!clause)
329
+ return { required, notRequired };
330
+ for (const f of (_a = clause.required) !== null && _a !== void 0 ? _a : [])
331
+ required.push(f);
332
+ for (const sub of (_b = clause.allOf) !== null && _b !== void 0 ? _b : []) {
333
+ for (const f of (_c = sub.required) !== null && _c !== void 0 ? _c : [])
334
+ required.push(f);
335
+ for (const f of (_e = (_d = sub.not) === null || _d === void 0 ? void 0 : _d.required) !== null && _e !== void 0 ? _e : [])
336
+ notRequired.push(f);
337
+ }
338
+ return { required, notRequired };
339
+ }
340
+ // Fetch schema, derive safe defaults, and return conditional branch info for post-merge handling.
341
+ //
342
+ // The schema's allOf branches use if/then/else. Each else block typically has
343
+ // `not: { required: [field] }` entries that PROHIBIT those fields when the condition is off.
344
+ // We must not pre-populate those fields as defaults, or the else block rejects the payload.
345
+ // Conversely, when the condition fires (e.g. sshTunnel:true from Infisical), the then block
346
+ // requires those fields, so post-merge we fill any still-missing ones with safe values.
347
+ async function fetchN8nSchema(n8nApiUrl, credentialType, n8nHeaders, ctx) {
348
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
349
+ const schema = await ctx.helpers.httpRequest({
350
+ method: 'GET',
351
+ url: `${n8nApiUrl}/api/v1/credentials/schema/${credentialType}`,
352
+ headers: n8nHeaders,
353
+ });
354
+ const topLevelRequired = new Set(Array.isArray(schema.required) ? schema.required : []);
355
+ const props = ((_a = schema.properties) !== null && _a !== void 0 ? _a : {});
356
+ // Analyse allOf branches to determine which fields must be excluded from defaults.
357
+ // A field is "excluded" when its controlling condition key defaults to OFF, causing the
358
+ // else block to fire and prohibit that field.
359
+ const excludedFields = new Set();
360
+ const condBranches = [];
361
+ const allOf = ((_b = schema.allOf) !== null && _b !== void 0 ? _b : []);
362
+ for (const branch of allOf) {
363
+ const ifKeys = Object.keys((_d = (_c = branch.if) === null || _c === void 0 ? void 0 : _c.properties) !== null && _d !== void 0 ? _d : {});
364
+ if (ifKeys.length !== 1)
365
+ continue;
366
+ const condKey = ifKeys[0];
367
+ const condValues = (_h = (_g = ((_f = (_e = branch.if) === null || _e === void 0 ? void 0 : _e.properties) !== null && _f !== void 0 ? _f : {})[condKey]) === null || _g === void 0 ? void 0 : _g.enum) !== null && _h !== void 0 ? _h : [];
368
+ const { required: thenRequired } = collectClauseFields(branch.then);
369
+ const { required: elseRequired, notRequired: elseProhibited } = collectClauseFields(branch.else);
370
+ // Fix 7.6: elseProhibited covers both `not.required` and plain `required` in else blocks,
371
+ // ensuring post-merge deletion handles any field that must be absent when condition is off.
372
+ condBranches.push({ condKey, condValues, thenRequired, elseProhibited: [...elseProhibited, ...elseRequired] });
373
+ if (condKey in props) {
374
+ // Determine the default value for the condition key.
375
+ const condKeyDef = props[condKey];
376
+ let condKeyDefault;
377
+ if (Array.isArray(condKeyDef === null || condKeyDef === void 0 ? void 0 : condKeyDef.enum) && ((_j = condKeyDef === null || condKeyDef === void 0 ? void 0 : condKeyDef.enum) === null || _j === void 0 ? void 0 : _j.length) > 0) {
378
+ // Fix 7.7: read schema's own default first, fall back to enum heuristic
379
+ condKeyDefault = (_k = condKeyDef === null || condKeyDef === void 0 ? void 0 : condKeyDef.default) !== null && _k !== void 0 ? _k : (condKey === 'allowedHttpRequestDomains' ? 'all' : (_l = condKeyDef === null || condKeyDef === void 0 ? void 0 : condKeyDef.enum) === null || _l === void 0 ? void 0 : _l[0]);
380
+ }
381
+ else if ((condKeyDef === null || condKeyDef === void 0 ? void 0 : condKeyDef.type) === 'boolean') {
382
+ condKeyDefault = false;
383
+ }
384
+ if (!condValues.includes(condKeyDefault)) {
385
+ // Default makes the condition FALSE → else fires → these fields are prohibited.
386
+ for (const f of elseProhibited)
387
+ excludedFields.add(f);
388
+ for (const f of elseRequired)
389
+ excludedFields.add(f);
390
+ for (const f of thenRequired)
391
+ excludedFields.add(f);
392
+ }
393
+ // (If default makes condition TRUE → then fires → fields go into condBranches for post-merge fill)
394
+ }
395
+ else {
396
+ // condKey not in schema properties → can't be set → always absent →
397
+ // properties validator skips it (vacuously passes) → then always fires.
398
+ // The thenRequired fields must always be present; generate safe defaults for them.
399
+ // (handled in the post-main-loop step below)
400
+ }
401
+ }
402
+ // Generate base defaults: skip unconditionally required fields AND conditionally excluded ones.
403
+ const defaults = {};
404
+ for (const [key, def] of Object.entries(props)) {
405
+ if (topLevelRequired.has(key) || excludedFields.has(key))
406
+ continue;
407
+ applyDefaultForProp(key, def, defaults, topLevelRequired);
408
+ }
409
+ // For branches whose condition key is absent from the schema (vacuously always fires),
410
+ // ensure all then-required fields have at least a safe empty default.
411
+ for (const { condKey, thenRequired } of condBranches) {
412
+ if (condKey in props)
413
+ continue; // handled above
414
+ for (const field of thenRequired) {
415
+ if (field in defaults || topLevelRequired.has(field))
416
+ continue;
417
+ const def = (_m = props[field]) !== null && _m !== void 0 ? _m : {};
418
+ applyDefaultForProp(field, def, defaults, topLevelRequired);
419
+ if (!(field in defaults))
420
+ defaults[field] = ''; // fallback for types we don't handle
421
+ }
422
+ }
423
+ return { defaults, props, condBranches, topRequired: topLevelRequired };
424
+ }
425
+ // Apply conditional branch logic to fullData in-place.
426
+ // Shared by both create and update paths (fix 7.2) to avoid duplication.
427
+ // When a condition fires: fill any missing thenRequired fields with safe defaults.
428
+ // When a condition does not fire: delete elseProhibited fields to satisfy the else block.
429
+ function applyCondBranches(fullData, schemaInfo) {
430
+ var _a, _b;
431
+ for (const { condKey, condValues, thenRequired, elseProhibited } of schemaInfo.condBranches) {
432
+ const condVal = fullData[condKey];
433
+ // When condKey is absent from schema properties it can't appear in the data,
434
+ // so JSON Schema's `properties` validator skips it → condition fires vacuously.
435
+ const condKeyInSchema = condKey in schemaInfo.props;
436
+ if (!condKeyInSchema || condValues.includes(condVal)) {
437
+ // Condition fires → fill any missing then-required fields with safe defaults.
438
+ for (const field of thenRequired) {
439
+ if (field in fullData)
440
+ continue;
441
+ const def = (_a = schemaInfo.props[field]) !== null && _a !== void 0 ? _a : {};
442
+ if (def.type === 'number') {
443
+ fullData[field] = 0;
444
+ }
445
+ else if (def.type === 'boolean') {
446
+ fullData[field] = false;
447
+ }
448
+ else if (Array.isArray(def.enum) && def.enum.length > 0) {
449
+ // Fix 7.7: read schema's own default first
450
+ fullData[field] = ((_b = def.default) !== null && _b !== void 0 ? _b : def.enum[0]);
451
+ }
452
+ else {
453
+ fullData[field] = '';
454
+ }
455
+ }
456
+ }
457
+ else {
458
+ // Condition doesn't fire → remove prohibited fields to satisfy else block.
459
+ for (const field of elseProhibited) {
460
+ delete fullData[field];
461
+ }
462
+ }
463
+ }
464
+ }
465
+ async function autoSyncFromInfisical(ctx, apiUrl, baseHeaders, i) {
466
+ var _a, _b, _c, _d, _e, _f;
467
+ const projectId = ctx.getNodeParameter('projectId', i);
468
+ const environment = ctx.getNodeParameter('environment', i);
469
+ const rootPath = ctx.getNodeParameter('rootPath', i, '/') || '/';
470
+ const n8nCreds = await ctx.getCredentials('n8nApi');
471
+ const n8nApiUrl = (n8nCreds.baseUrl || 'http://localhost:5678').replace(/\/$/, '').replace(/\/api\/v1$/, '');
472
+ const n8nApiKey = n8nCreds.apiKey;
473
+ const n8nHeaders = { 'X-N8N-API-KEY': n8nApiKey, 'Content-Type': 'application/json' };
474
+ // 1. Discover all credential folders at rootPath
475
+ const folderPath = rootPath.replace(/\/+$/, '') || '/';
476
+ const folderResponse = await ctx.helpers.httpRequest({
477
+ method: 'GET',
478
+ url: `${apiUrl}/v2/folders`,
479
+ headers: baseHeaders,
480
+ qs: { projectId, environment, path: folderPath },
481
+ });
482
+ const folders = ((_a = folderResponse.folders) !== null && _a !== void 0 ? _a : []);
483
+ if (folders.length === 0) {
484
+ return [{
485
+ json: { success: true, message: `No folders found at "${folderPath}"`, synced: 0 },
486
+ pairedItem: { item: i },
487
+ }];
488
+ }
489
+ // 2. Fetch all existing n8n credentials (paginated) and index by name
490
+ const allN8nCreds = [];
491
+ let cursor;
492
+ do {
493
+ const qs = { limit: 250 };
494
+ if (cursor)
495
+ qs.cursor = cursor;
496
+ const resp = await ctx.helpers.httpRequest({
497
+ method: 'GET',
498
+ url: `${n8nApiUrl}/api/v1/credentials`,
499
+ headers: n8nHeaders,
500
+ qs,
501
+ });
502
+ allN8nCreds.push(...((_b = resp.data) !== null && _b !== void 0 ? _b : []));
503
+ cursor = resp.nextCursor;
504
+ } while (cursor);
505
+ const credByName = new Map(allN8nCreds.map((c) => [c.name, c]));
506
+ // Fix 7.3: cache schema results so the same credential type is only fetched once per execution,
507
+ // avoiding N redundant HTTP calls when multiple folders share the same type.
508
+ const schemaCache = new Map();
509
+ // 3. Process each folder
510
+ const results = [];
511
+ for (const folder of folders) {
512
+ const folderName = folder.name;
513
+ const secretPath = folderPath === '/' ? `/${folderName}` : `${folderPath}/${folderName}`;
514
+ // Read all secrets in this folder
515
+ const secretsResponse = await ctx.helpers.httpRequest({
516
+ method: 'GET',
517
+ url: `${apiUrl}/v4/secrets`,
518
+ headers: baseHeaders,
519
+ qs: { projectId, environment, secretPath },
520
+ });
521
+ const secrets = ((_c = secretsResponse.secrets) !== null && _c !== void 0 ? _c : []);
522
+ if (secrets.length === 0) {
523
+ results.push({
524
+ json: { folderName, secretPath, action: 'skipped', reason: 'no secrets in folder' },
525
+ pairedItem: { item: i },
526
+ });
527
+ continue;
528
+ }
529
+ // Extract n8n_credential_type from any secret's metadata
530
+ let credentialType;
531
+ for (const secret of secrets) {
532
+ const meta = ((_d = secret.secretMetadata) !== null && _d !== void 0 ? _d : []);
533
+ const entry = meta.find((m) => m.key === 'n8n_credential_type');
534
+ if (entry) {
535
+ credentialType = entry.value;
536
+ break;
537
+ }
538
+ }
539
+ // Fix 7.3: use cached schema when available
540
+ let schemaInfo;
541
+ if (credentialType) {
542
+ try {
543
+ if (!schemaCache.has(credentialType)) {
544
+ schemaCache.set(credentialType, await fetchN8nSchema(n8nApiUrl, credentialType, n8nHeaders, ctx));
545
+ }
546
+ schemaInfo = schemaCache.get(credentialType);
547
+ }
548
+ catch (_g) {
549
+ // proceed without schema — no coercion or defaults applied
550
+ }
551
+ }
552
+ // Build n8n credential data applying secretKey→param mapping (if available)
553
+ // and coercing string values to the types the schema expects (e.g. port → number).
554
+ const fieldMap = credentialType ? CREDENTIAL_FIELD_MAPS[credentialType] : undefined;
555
+ const credentialData = {};
556
+ if (fieldMap) {
557
+ const secretsByKey = new Map(secrets.map((s) => [s.secretKey, s.secretValue]));
558
+ for (const { param, secretKey } of fieldMap) {
559
+ const raw = secretsByKey.get(secretKey);
560
+ if (raw === undefined)
561
+ continue;
562
+ credentialData[param] = coerceValue(raw, schemaInfo === null || schemaInfo === void 0 ? void 0 : schemaInfo.props[param]);
563
+ }
564
+ }
565
+ else {
566
+ for (const secret of secrets) {
567
+ const key = secret.secretKey;
568
+ const raw = secret.secretValue;
569
+ credentialData[key] = coerceValue(raw, schemaInfo === null || schemaInfo === void 0 ? void 0 : schemaInfo.props[key]);
570
+ }
571
+ }
572
+ const existing = credByName.get(folderName);
573
+ if (existing) {
574
+ // Fix 7.2: apply the same fullData build logic as the create path so that condition-key
575
+ // changes (e.g. ssl:false→true) get their required fields filled and their prohibited
576
+ // fields removed, preventing spurious 422 errors on update.
577
+ const fullData = Object.assign(Object.assign({}, ((_e = schemaInfo === null || schemaInfo === void 0 ? void 0 : schemaInfo.defaults) !== null && _e !== void 0 ? _e : {})), credentialData);
578
+ if (schemaInfo)
579
+ applyCondBranches(fullData, schemaInfo);
580
+ const updated = await ctx.helpers.httpRequest({
581
+ method: 'PATCH',
582
+ url: `${n8nApiUrl}/api/v1/credentials/${existing.id}`,
583
+ headers: n8nHeaders,
584
+ body: { data: fullData },
585
+ });
586
+ results.push({
587
+ json: Object.assign(Object.assign({}, updated), { action: 'updated', secretPath, secretCount: secrets.length }),
588
+ pairedItem: { item: i },
589
+ });
590
+ }
591
+ else {
592
+ // Create new credential — needs type from metadata
593
+ if (!credentialType) {
594
+ results.push({
595
+ json: { folderName, secretPath, action: 'skipped', reason: 'credential not found in n8n and no n8n_credential_type metadata to create it' },
596
+ pairedItem: { item: i },
597
+ });
598
+ continue;
599
+ }
600
+ // Merge schema defaults (Infisical values take precedence) then apply
601
+ // conditional field rules: fill any still-missing then-required fields,
602
+ // and remove any fields prohibited by else blocks given actual merged values.
603
+ const fullData = Object.assign(Object.assign({}, ((_f = schemaInfo === null || schemaInfo === void 0 ? void 0 : schemaInfo.defaults) !== null && _f !== void 0 ? _f : {})), credentialData);
604
+ if (schemaInfo)
605
+ applyCondBranches(fullData, schemaInfo);
606
+ const created = await ctx.helpers.httpRequest({
607
+ method: 'POST',
608
+ url: `${n8nApiUrl}/api/v1/credentials`,
609
+ headers: n8nHeaders,
610
+ body: { name: folderName, type: credentialType, data: fullData },
611
+ });
612
+ results.push({
613
+ json: Object.assign(Object.assign({}, created), { action: 'created', secretPath, secretCount: secrets.length }),
614
+ pairedItem: { item: i },
615
+ });
616
+ }
617
+ }
618
+ return results;
619
+ }
620
+ async function executeSyncOperation(ctx, apiUrl, baseHeaders, operation, i) {
621
+ var _a, _b, _c, _d;
622
+ if (operation === 'syncFromInfisical') {
623
+ return syncFromInfisical(ctx, apiUrl, baseHeaders, i);
624
+ }
625
+ if (operation === 'autoSyncFromInfisical') {
626
+ return autoSyncFromInfisical(ctx, apiUrl, baseHeaders, i);
627
+ }
628
+ if (operation !== 'syncToInfisical') {
629
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Unknown sync operation: ${operation}`, {
630
+ itemIndex: i,
631
+ });
632
+ }
633
+ const projectId = ctx.getNodeParameter('projectId', i);
634
+ const environment = ctx.getNodeParameter('environment', i);
635
+ const rootPath = ctx.getNodeParameter('rootPath', i, '/') || '/';
636
+ const credentialName = ctx.getNodeParameter('credentialName', i);
637
+ const inputMode = ctx.getNodeParameter('inputMode', i, 'form');
638
+ const credentialType = inputMode === 'json'
639
+ ? ctx.getNodeParameter('credentialTypeJson', i)
640
+ : ctx.getNodeParameter('credentialType', i);
641
+ // Parse JSON early so it can be used for both validation and secret collection.
642
+ let parsedJson;
643
+ if (inputMode === 'json') {
644
+ const rawJson = ctx.getNodeParameter('credentialJson', i, '{}');
645
+ try {
646
+ parsedJson = JSON.parse(rawJson);
647
+ }
648
+ catch (_e) {
649
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Credential Fields (JSON) is not valid JSON', { itemIndex: i });
650
+ }
651
+ }
652
+ // Validate against the n8n credential schema if n8nApi credentials are available.
653
+ // Silently skipped when n8nApi is not configured or the schema endpoint is unreachable.
654
+ let fetchedSchemaProps;
655
+ try {
656
+ const n8nCreds = await ctx.getCredentials('n8nApi');
657
+ const n8nApiUrl = (n8nCreds.baseUrl || 'http://localhost:5678')
658
+ .replace(/\/$/, '').replace(/\/api\/v1$/, '');
659
+ const n8nHeaders = { 'X-N8N-API-KEY': n8nCreds.apiKey, 'Content-Type': 'application/json' };
660
+ const schemaInfo = await fetchN8nSchema(n8nApiUrl, credentialType, n8nHeaders, ctx);
661
+ fetchedSchemaProps = schemaInfo.props;
662
+ const validationData = {};
663
+ let availableFormFields;
664
+ if (inputMode === 'json') {
665
+ Object.assign(validationData, parsedJson);
666
+ }
667
+ else {
668
+ const fieldMap = CREDENTIAL_FIELD_MAPS[credentialType];
669
+ if (fieldMap) {
670
+ availableFormFields = new Set(fieldMap.map((f) => f.param));
671
+ for (const { param } of fieldMap) {
672
+ validationData[param] = ctx.getNodeParameter(param, i, '');
673
+ }
674
+ }
675
+ }
676
+ const errors = validateAgainstSchema(validationData, schemaInfo, availableFormFields);
677
+ if (errors.length > 0) {
678
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Credential validation failed for "${credentialType}":\n${errors.map((e) => `• ${e}`).join('\n')}`, { itemIndex: i });
679
+ }
680
+ }
681
+ catch (err) {
682
+ if (err instanceof n8n_workflow_1.NodeOperationError)
683
+ throw err;
684
+ // n8nApi not configured or schema fetch failed — skip validation
685
+ }
686
+ const folderPath = rootPath.replace(/\/+$/, '') || '/';
687
+ const secretPath = buildSecretPath(rootPath, credentialName);
688
+ // Ensure the credential folder exists; ignore conflict if it already does
689
+ try {
690
+ await ctx.helpers.httpRequest({
691
+ method: 'POST',
692
+ url: `${apiUrl}/v2/folders`,
693
+ headers: baseHeaders,
694
+ body: { projectId, environment, name: credentialName, path: folderPath },
695
+ });
696
+ }
697
+ catch (err) {
698
+ const e = err;
699
+ const status = (_b = (_a = e === null || e === void 0 ? void 0 : e.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : e === null || e === void 0 ? void 0 : e.statusCode;
700
+ if (status !== 409 && !((_c = e === null || e === void 0 ? void 0 : e.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('already exist'))) {
701
+ throw err;
702
+ }
703
+ }
704
+ // Collect non-empty credential fields as Infisical secrets
705
+ const secretMetadata = [{ key: 'n8n_credential_type', value: credentialType }];
706
+ const secrets = [];
707
+ if (inputMode === 'json') {
708
+ for (const [key, value] of Object.entries(parsedJson !== null && parsedJson !== void 0 ? parsedJson : {})) {
709
+ if (isEmptyValue(value))
710
+ continue;
711
+ if (fetchedSchemaProps && !(key in fetchedSchemaProps))
712
+ continue;
713
+ const secretValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
714
+ secrets.push({ secretKey: key, secretValue, secretMetadata });
715
+ }
716
+ }
717
+ else {
718
+ const fieldMap = CREDENTIAL_FIELD_MAPS[credentialType];
719
+ if (!fieldMap) {
720
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Unsupported credential type: ${credentialType}`, { itemIndex: i });
721
+ }
722
+ for (const { param, secretKey } of fieldMap) {
723
+ const value = ctx.getNodeParameter(param, i, '');
724
+ if (isEmptyValue(value))
725
+ continue;
726
+ secrets.push({ secretKey, secretValue: String(value), secretMetadata });
727
+ }
728
+ }
729
+ if (secrets.length === 0) {
730
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'No credential fields provided — all fields are empty', { itemIndex: i });
731
+ }
732
+ // Upsert all secrets in one batch call (mode=upsert: create if missing, update if exists)
733
+ const response = await ctx.helpers.httpRequest({
734
+ method: 'PATCH',
735
+ url: `${apiUrl}/v4/secrets/batch`,
736
+ headers: baseHeaders,
737
+ body: { projectId, environment, secretPath, secrets, mode: 'upsert' },
738
+ });
739
+ const synced = ((_d = response.secrets) !== null && _d !== void 0 ? _d : []);
740
+ if (synced.length === 0) {
741
+ return [
742
+ {
743
+ json: { success: true, credentialType, credentialName, secretPath, syncedCount: secrets.length },
744
+ pairedItem: { item: i },
745
+ },
746
+ ];
747
+ }
748
+ return synced.map((secret) => ({ json: secret, pairedItem: { item: i } }));
749
+ }
750
+ exports.executeSyncOperation = executeSyncOperation;