@aifabrix/builder 2.41.0 → 2.42.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/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +1 -1
- package/integration/hubspot/README.md +8 -4
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/jest.config.manual.js +2 -1
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +34 -1
- package/lib/app/config.js +23 -11
- package/lib/app/index.js +3 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/readme.js +8 -3
- package/lib/app/run-env-compose.js +64 -1
- package/lib/app/run-helpers.js +1 -1
- package/lib/app/show-display.js +1 -1
- package/lib/cli/setup-app.js +42 -11
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +27 -0
- package/lib/cli/setup-environment.js +12 -4
- package/lib/cli/setup-external-system.js +19 -4
- package/lib/cli/setup-infra.js +54 -14
- package/lib/cli/setup-utility.js +117 -21
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-init.js +39 -1
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +507 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/upload.js +71 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -64
- package/lib/core/config.js +7 -1
- package/lib/core/secrets.js +33 -12
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/deployment/deployer.js +7 -5
- package/lib/external-system/download.js +182 -204
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +51 -18
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +1 -1
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +4 -1
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +147 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/index.js +11 -3
- package/lib/infrastructure/services.js +22 -11
- package/lib/schema/application-schema.json +8 -5
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +82 -6
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +38 -10
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/compose-generator.js +1 -1
- package/lib/utils/compose-handlebars-helpers.js +11 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +115 -25
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/env-copy.js +23 -3
- package/lib/utils/error-formatters/http-status-errors.js +0 -1
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +56 -29
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +1 -0
- package/lib/utils/infra-status.js +50 -44
- package/lib/utils/local-secrets.js +5 -5
- package/lib/utils/paths.js +85 -4
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +20 -0
- package/lib/utils/secrets-helpers.js +75 -89
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager.js +24 -32
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +7 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +7 -2
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +5 -5
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/README.md.hbs +65 -25
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +4 -4
- package/templates/typescript/docker-compose.hbs +4 -4
- package/integration/hubspot/application.yaml +0 -37
package/lib/utils/paths.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Path Utilities for AI Fabrix Builder
|
|
3
|
-
*
|
|
4
|
-
* Centralized helpers for resolving filesystem locations with support for
|
|
5
|
-
* AIFABRIX_HOME override. Defaults to ~/.aifabrix when not specified.
|
|
6
|
-
*
|
|
3
|
+
* Centralized helpers for resolving filesystem locations with AIFABRIX_HOME override.
|
|
7
4
|
* @fileoverview Path resolution utilities with environment overrides
|
|
8
5
|
* @author AI Fabrix Team
|
|
9
6
|
* @version 2.0.0
|
|
10
7
|
*/
|
|
8
|
+
/* eslint-disable max-lines -- Central path resolution; resolveIntegrationAppKeyFromCwd for datasource commands */
|
|
11
9
|
|
|
12
10
|
'use strict';
|
|
13
11
|
|
|
@@ -279,6 +277,76 @@ function getIntegrationBuilderBaseDir() {
|
|
|
279
277
|
return cwd;
|
|
280
278
|
}
|
|
281
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Returns the integration root directory (used for listing apps).
|
|
282
|
+
* @returns {string} Absolute path to integration/ directory
|
|
283
|
+
*/
|
|
284
|
+
function getIntegrationRoot() {
|
|
285
|
+
return path.join(getIntegrationBuilderBaseDir(), 'integration');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Returns the builder root directory. Uses AIFABRIX_BUILDER_DIR when set, else project/cwd + builder.
|
|
290
|
+
* @returns {string} Absolute path to builder/ directory
|
|
291
|
+
*/
|
|
292
|
+
function getBuilderRoot() {
|
|
293
|
+
const envDir = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
|
|
294
|
+
? process.env.AIFABRIX_BUILDER_DIR.trim()
|
|
295
|
+
: null;
|
|
296
|
+
if (envDir) {
|
|
297
|
+
return path.resolve(envDir);
|
|
298
|
+
}
|
|
299
|
+
return path.join(getIntegrationBuilderBaseDir(), 'builder');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Lists app names (directories) under integration root. Excludes dot-prefixed entries.
|
|
304
|
+
* Returns [] if root does not exist.
|
|
305
|
+
* @returns {string[]} Sorted list of app directory names
|
|
306
|
+
*/
|
|
307
|
+
function listIntegrationAppNames() {
|
|
308
|
+
const root = getIntegrationRoot();
|
|
309
|
+
if (!fs.existsSync(root)) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
const stat = fs.statSync(root);
|
|
313
|
+
if (!stat || typeof stat.isDirectory !== 'function' || !stat.isDirectory()) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
const entries = fs.readdirSync(root);
|
|
317
|
+
return entries
|
|
318
|
+
.filter(name => !name.startsWith('.'))
|
|
319
|
+
.filter(name => {
|
|
320
|
+
const fullPath = path.join(root, name);
|
|
321
|
+
return fs.statSync(fullPath).isDirectory();
|
|
322
|
+
})
|
|
323
|
+
.sort();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Lists app names (directories) under builder root. Excludes dot-prefixed entries.
|
|
328
|
+
* Returns [] if root does not exist.
|
|
329
|
+
* @returns {string[]} Sorted list of app directory names
|
|
330
|
+
*/
|
|
331
|
+
function listBuilderAppNames() {
|
|
332
|
+
const root = getBuilderRoot();
|
|
333
|
+
if (!fs.existsSync(root)) {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
const stat = fs.statSync(root);
|
|
337
|
+
if (!stat || typeof stat.isDirectory !== 'function' || !stat.isDirectory()) {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
const entries = fs.readdirSync(root);
|
|
341
|
+
return entries
|
|
342
|
+
.filter(name => !name.startsWith('.'))
|
|
343
|
+
.filter(name => {
|
|
344
|
+
const fullPath = path.join(root, name);
|
|
345
|
+
return fs.statSync(fullPath).isDirectory();
|
|
346
|
+
})
|
|
347
|
+
.sort();
|
|
348
|
+
}
|
|
349
|
+
|
|
282
350
|
/**
|
|
283
351
|
* Gets the integration folder path for external systems.
|
|
284
352
|
* @param {string} appName - Application name
|
|
@@ -470,6 +538,14 @@ async function getResolveAppPath(appName) {
|
|
|
470
538
|
return { appPath: result.appPath, envOnly: false };
|
|
471
539
|
}
|
|
472
540
|
|
|
541
|
+
/** Resolve appKey when cwd is inside integration/<appKey>/. */
|
|
542
|
+
function resolveIntegrationAppKeyFromCwd() {
|
|
543
|
+
const integrationNorm = path.resolve(path.join(getIntegrationBuilderBaseDir(), 'integration'));
|
|
544
|
+
const cwd = path.resolve(process.cwd());
|
|
545
|
+
if (cwd !== integrationNorm && !cwd.startsWith(integrationNorm + path.sep)) return null;
|
|
546
|
+
return path.relative(integrationNorm, cwd).split(path.sep)[0] || null;
|
|
547
|
+
}
|
|
548
|
+
|
|
473
549
|
module.exports = {
|
|
474
550
|
getAifabrixHome,
|
|
475
551
|
getConfigDirForPaths,
|
|
@@ -479,11 +555,16 @@ module.exports = {
|
|
|
479
555
|
getProjectRoot,
|
|
480
556
|
getIntegrationPath,
|
|
481
557
|
getBuilderPath,
|
|
558
|
+
getIntegrationRoot,
|
|
559
|
+
getBuilderRoot,
|
|
560
|
+
listIntegrationAppNames,
|
|
561
|
+
listBuilderAppNames,
|
|
482
562
|
resolveBuildContext,
|
|
483
563
|
getDeployJsonPath,
|
|
484
564
|
resolveApplicationConfigPath,
|
|
485
565
|
detectAppType,
|
|
486
566
|
getResolveAppPath,
|
|
567
|
+
resolveIntegrationAppKeyFromCwd,
|
|
487
568
|
clearProjectRootCache
|
|
488
569
|
};
|
|
489
570
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical secrets path and YAML helpers
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Read/merge canonical secrets from config path
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
14
|
+
const config = require('../core/config');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read a YAML file and return parsed object
|
|
18
|
+
* @function readYamlAtPath
|
|
19
|
+
* @param {string} filePath - Absolute file path
|
|
20
|
+
* @returns {Object} Parsed YAML object
|
|
21
|
+
*/
|
|
22
|
+
function readYamlAtPath(filePath) {
|
|
23
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
return yaml.load(content);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Merge a single secret value from canonical into result
|
|
29
|
+
* @function mergeSecretValue
|
|
30
|
+
* @param {Object} result - Result object to merge into
|
|
31
|
+
* @param {string} key - Secret key
|
|
32
|
+
* @param {*} canonicalValue - Value from canonical secrets
|
|
33
|
+
*/
|
|
34
|
+
function mergeSecretValue(result, key, canonicalValue) {
|
|
35
|
+
const currentValue = result[key];
|
|
36
|
+
// Fill missing, empty, or undefined values
|
|
37
|
+
if (!(key in result) || currentValue === undefined || currentValue === null || currentValue === '') {
|
|
38
|
+
result[key] = canonicalValue;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Only replace values that are encrypted (have secure:// prefix)
|
|
42
|
+
// Plaintext values (no secure://) are used as-is
|
|
43
|
+
if (typeof currentValue === 'string' && typeof canonicalValue === 'string') {
|
|
44
|
+
if (currentValue.startsWith('secure://')) {
|
|
45
|
+
result[key] = canonicalValue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Apply canonical secrets path override if configured and file exists
|
|
52
|
+
* @async
|
|
53
|
+
* @function applyCanonicalSecretsOverride
|
|
54
|
+
* @param {Object} currentSecrets - Current secrets map
|
|
55
|
+
* @returns {Promise<Object>} Possibly overridden secrets
|
|
56
|
+
*/
|
|
57
|
+
async function applyCanonicalSecretsOverride(currentSecrets) {
|
|
58
|
+
let mergedSecrets = currentSecrets || {};
|
|
59
|
+
try {
|
|
60
|
+
const canonicalPath = await config.getSecretsPath();
|
|
61
|
+
if (!canonicalPath) {
|
|
62
|
+
return mergedSecrets;
|
|
63
|
+
}
|
|
64
|
+
const resolvedCanonical = path.isAbsolute(canonicalPath)
|
|
65
|
+
? canonicalPath
|
|
66
|
+
: path.resolve(process.cwd(), canonicalPath);
|
|
67
|
+
if (!fs.existsSync(resolvedCanonical)) {
|
|
68
|
+
return mergedSecrets;
|
|
69
|
+
}
|
|
70
|
+
const configSecrets = readYamlAtPath(resolvedCanonical);
|
|
71
|
+
if (!configSecrets || typeof configSecrets !== 'object') {
|
|
72
|
+
return mergedSecrets;
|
|
73
|
+
}
|
|
74
|
+
// Apply canonical secrets as a fallback source:
|
|
75
|
+
// - Do NOT override any existing keys from user/build
|
|
76
|
+
// - Add only missing keys from canonical path
|
|
77
|
+
// - Also fill in empty/undefined values from canonical path
|
|
78
|
+
// - Replace encrypted values (secure://) with canonical plaintext
|
|
79
|
+
const result = { ...mergedSecrets };
|
|
80
|
+
for (const [key, canonicalValue] of Object.entries(configSecrets)) {
|
|
81
|
+
mergeSecretValue(result, key, canonicalValue);
|
|
82
|
+
}
|
|
83
|
+
mergedSecrets = result;
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore and fall through
|
|
86
|
+
}
|
|
87
|
+
return mergedSecrets;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
readYamlAtPath,
|
|
92
|
+
applyCanonicalSecretsOverride
|
|
93
|
+
};
|
|
@@ -208,6 +208,25 @@ function saveSecretsFile(resolvedPath, secrets) {
|
|
|
208
208
|
|
|
209
209
|
const YAML_DUMP_OPTS = { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false };
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Merges secret keys into the secrets file (load existing, merge, overwrite file).
|
|
213
|
+
* Use when setting or updating keys so that existing keys are updated in place instead of duplicated.
|
|
214
|
+
* Creates the file if it does not exist. Tolerant of duplicate keys in existing file (last wins when loading).
|
|
215
|
+
*
|
|
216
|
+
* @function mergeSecretsIntoFile
|
|
217
|
+
* @param {string} resolvedPath - Path to secrets file
|
|
218
|
+
* @param {Object} secrets - Key-value object to merge (overwrites existing same keys)
|
|
219
|
+
* @throws {Error} If write fails
|
|
220
|
+
*/
|
|
221
|
+
function mergeSecretsIntoFile(resolvedPath, secrets) {
|
|
222
|
+
if (!secrets || typeof secrets !== 'object' || Object.keys(secrets).length === 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const existing = loadExistingSecrets(resolvedPath);
|
|
226
|
+
const merged = { ...existing, ...secrets };
|
|
227
|
+
saveSecretsFile(resolvedPath, merged);
|
|
228
|
+
}
|
|
229
|
+
|
|
211
230
|
/**
|
|
212
231
|
* Appends secret keys to the end of the secrets file without modifying existing content (preserves comments and structure).
|
|
213
232
|
* Creates the file if it does not exist. For existing files, new keys are appended.
|
|
@@ -329,6 +348,7 @@ module.exports = {
|
|
|
329
348
|
loadYamlTolerantOfDuplicateKeys,
|
|
330
349
|
loadExistingSecrets,
|
|
331
350
|
saveSecretsFile,
|
|
351
|
+
mergeSecretsIntoFile,
|
|
332
352
|
appendSecretsToFile,
|
|
333
353
|
generateMissingSecrets,
|
|
334
354
|
createDefaultSecrets
|
|
@@ -18,6 +18,7 @@ const { loadEnvConfig } = require('./env-config-loader');
|
|
|
18
18
|
const { updateContainerPortInEnvFile } = require('./env-ports');
|
|
19
19
|
const { buildEnvVarMap } = require('./env-map');
|
|
20
20
|
const { getLocalPortFromPath } = require('./port-resolver');
|
|
21
|
+
const { readYamlAtPath, applyCanonicalSecretsOverride } = require('./secrets-canonical');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Interpolate ${VAR} occurrences with values from envVars map
|
|
@@ -42,25 +43,84 @@ function isCommentOrEmptyLine(line) {
|
|
|
42
43
|
return t === '' || t.startsWith('#');
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** Regex for kv:// path (allows slashes, e.g. kv://hubspot/clientId) */
|
|
47
|
+
const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find object key that matches part case-insensitively.
|
|
51
|
+
* @param {Object} obj - Object to search
|
|
52
|
+
* @param {string} part - Key to match (e.g. 'clientid')
|
|
53
|
+
* @returns {string|undefined} Actual key in obj or undefined
|
|
54
|
+
*/
|
|
55
|
+
function findKeyCaseInsensitive(obj, part) {
|
|
56
|
+
if (!obj || typeof obj !== 'object' || part === null || part === undefined) return undefined;
|
|
57
|
+
const lower = String(part).toLowerCase();
|
|
58
|
+
for (const key of Object.keys(obj)) {
|
|
59
|
+
if (key.toLowerCase() === lower) return key;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve value by walking path parts (nested keys).
|
|
66
|
+
* @param {Object} secrets - Root secrets object
|
|
67
|
+
* @param {string[]} parts - Path parts (e.g. ['hubspot', 'clientId'])
|
|
68
|
+
* @returns {*} Value or undefined
|
|
69
|
+
*/
|
|
70
|
+
function getValueByNestedPath(secrets, parts) {
|
|
71
|
+
let value = secrets;
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
74
|
+
const key = part in value ? part : findKeyCaseInsensitive(value, part);
|
|
75
|
+
value = key !== undefined ? value[key] : undefined;
|
|
76
|
+
if (value === undefined) return undefined;
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
45
81
|
/**
|
|
46
|
-
*
|
|
82
|
+
* Get secret value by path. Supports flat key (hubspot/clientId), nested object (hubspot.clientId),
|
|
83
|
+
* and case-insensitive matching (clientid matches clientId). Path-style and hyphen-style are distinct:
|
|
84
|
+
* hubspot/clientid and hubspot-clientid are different keys.
|
|
85
|
+
* @param {Object} secrets - Secrets object (may be nested)
|
|
86
|
+
* @param {string} pathStr - Path after kv:// (e.g. 'hubspot/clientId' or 'hubspot/clientid')
|
|
87
|
+
* @returns {*} Value or undefined if not found
|
|
88
|
+
*/
|
|
89
|
+
function getValueByPath(secrets, pathStr) {
|
|
90
|
+
if (!secrets || typeof secrets !== 'object' || !pathStr) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const direct = secrets[pathStr];
|
|
94
|
+
if (direct !== undefined) return direct;
|
|
95
|
+
const flatKey = findKeyCaseInsensitive(secrets, pathStr);
|
|
96
|
+
if (flatKey !== undefined) return secrets[flatKey];
|
|
97
|
+
if (!pathStr.includes('/')) return undefined;
|
|
98
|
+
return getValueByNestedPath(secrets, pathStr.split('/'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Collect missing kv:// secrets referenced in content (skips commented and empty lines).
|
|
103
|
+
* Supports path-style refs (e.g. kv://hubspot/clientId). Returns unique refs.
|
|
47
104
|
* @function collectMissingSecrets
|
|
48
105
|
* @param {string} content - Text content
|
|
49
|
-
* @param {Object} secrets - Available secrets
|
|
50
|
-
* @returns {string[]} Array of missing kv://<
|
|
106
|
+
* @param {Object} secrets - Available secrets (flat or nested)
|
|
107
|
+
* @returns {string[]} Array of missing kv://<path> references (unique)
|
|
51
108
|
*/
|
|
52
109
|
function collectMissingSecrets(content, secrets) {
|
|
53
|
-
const
|
|
110
|
+
const seen = new Set();
|
|
54
111
|
const missing = [];
|
|
55
112
|
const lines = content.split('\n');
|
|
56
113
|
for (const line of lines) {
|
|
57
114
|
if (isCommentOrEmptyLine(line)) continue;
|
|
58
115
|
let match;
|
|
59
|
-
|
|
60
|
-
while ((match =
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
116
|
+
KV_REF_PATTERN.lastIndex = 0;
|
|
117
|
+
while ((match = KV_REF_PATTERN.exec(line)) !== null) {
|
|
118
|
+
const pathStr = match[1];
|
|
119
|
+
if (seen.has(pathStr)) continue;
|
|
120
|
+
seen.add(pathStr);
|
|
121
|
+
const value = getValueByPath(secrets, pathStr);
|
|
122
|
+
if (value === undefined || value === null) {
|
|
123
|
+
missing.push(`kv://${pathStr}`);
|
|
64
124
|
}
|
|
65
125
|
}
|
|
66
126
|
}
|
|
@@ -91,26 +151,26 @@ function formatMissingSecretsFileInfo(secretsFilePaths) {
|
|
|
91
151
|
}
|
|
92
152
|
|
|
93
153
|
/**
|
|
94
|
-
* Replace kv:// references with actual values (skips commented and empty lines)
|
|
154
|
+
* Replace kv:// references with actual values (skips commented and empty lines).
|
|
155
|
+
* Supports path-style refs (e.g. kv://hubspot/clientId) and nested secrets.
|
|
95
156
|
* @function replaceKvInContent
|
|
96
157
|
* @param {string} content - Text content containing kv:// references
|
|
97
|
-
* @param {Object} secrets - Secrets map
|
|
158
|
+
* @param {Object} secrets - Secrets map (flat or nested)
|
|
98
159
|
* @param {Object} envVars - Environment variables map for nested interpolation
|
|
99
160
|
* @returns {string} Content with kv:// references replaced
|
|
100
161
|
*/
|
|
101
162
|
function replaceKvInContent(content, secrets, envVars) {
|
|
102
|
-
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
103
163
|
const lines = content.split('\n');
|
|
104
164
|
const result = lines.map(line => {
|
|
105
165
|
if (isCommentOrEmptyLine(line)) return line;
|
|
106
|
-
return line.replace(
|
|
107
|
-
let value = secrets
|
|
166
|
+
return line.replace(KV_REF_PATTERN, (match, pathStr) => {
|
|
167
|
+
let value = getValueByPath(secrets, pathStr);
|
|
108
168
|
if (typeof value === 'string') {
|
|
109
169
|
value = value.replace(/\$\{([A-Z_]+)\}/g, (m, envVar) => {
|
|
110
170
|
return envVars[envVar] || m;
|
|
111
171
|
});
|
|
112
172
|
}
|
|
113
|
-
return value;
|
|
173
|
+
return value !== null && value !== undefined ? String(value) : match;
|
|
114
174
|
});
|
|
115
175
|
});
|
|
116
176
|
return result.join('\n');
|
|
@@ -377,80 +437,6 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
|
377
437
|
return updated;
|
|
378
438
|
}
|
|
379
439
|
|
|
380
|
-
/**
|
|
381
|
-
* Read a YAML file and return parsed object
|
|
382
|
-
* @function readYamlAtPath
|
|
383
|
-
* @param {string} filePath - Absolute file path
|
|
384
|
-
* @returns {Object} Parsed YAML object
|
|
385
|
-
*/
|
|
386
|
-
function readYamlAtPath(filePath) {
|
|
387
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
388
|
-
return yaml.load(content);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Merge a single secret value from canonical into result
|
|
393
|
-
* @function mergeSecretValue
|
|
394
|
-
* @param {Object} result - Result object to merge into
|
|
395
|
-
* @param {string} key - Secret key
|
|
396
|
-
* @param {*} canonicalValue - Value from canonical secrets
|
|
397
|
-
*/
|
|
398
|
-
function mergeSecretValue(result, key, canonicalValue) {
|
|
399
|
-
const currentValue = result[key];
|
|
400
|
-
// Fill missing, empty, or undefined values
|
|
401
|
-
if (!(key in result) || currentValue === undefined || currentValue === null || currentValue === '') {
|
|
402
|
-
result[key] = canonicalValue;
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
// Only replace values that are encrypted (have secure:// prefix)
|
|
406
|
-
// Plaintext values (no secure://) are used as-is
|
|
407
|
-
if (typeof currentValue === 'string' && typeof canonicalValue === 'string') {
|
|
408
|
-
if (currentValue.startsWith('secure://')) {
|
|
409
|
-
result[key] = canonicalValue;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Apply canonical secrets path override if configured and file exists
|
|
416
|
-
* @async
|
|
417
|
-
* @function applyCanonicalSecretsOverride
|
|
418
|
-
* @param {Object} currentSecrets - Current secrets map
|
|
419
|
-
* @returns {Promise<Object>} Possibly overridden secrets
|
|
420
|
-
*/
|
|
421
|
-
async function applyCanonicalSecretsOverride(currentSecrets) {
|
|
422
|
-
let mergedSecrets = currentSecrets || {};
|
|
423
|
-
try {
|
|
424
|
-
const canonicalPath = await config.getSecretsPath();
|
|
425
|
-
if (!canonicalPath) {
|
|
426
|
-
return mergedSecrets;
|
|
427
|
-
}
|
|
428
|
-
const resolvedCanonical = path.isAbsolute(canonicalPath)
|
|
429
|
-
? canonicalPath
|
|
430
|
-
: path.resolve(process.cwd(), canonicalPath);
|
|
431
|
-
if (!fs.existsSync(resolvedCanonical)) {
|
|
432
|
-
return mergedSecrets;
|
|
433
|
-
}
|
|
434
|
-
const configSecrets = readYamlAtPath(resolvedCanonical);
|
|
435
|
-
if (!configSecrets || typeof configSecrets !== 'object') {
|
|
436
|
-
return mergedSecrets;
|
|
437
|
-
}
|
|
438
|
-
// Apply canonical secrets as a fallback source:
|
|
439
|
-
// - Do NOT override any existing keys from user/build
|
|
440
|
-
// - Add only missing keys from canonical path
|
|
441
|
-
// - Also fill in empty/undefined values from canonical path
|
|
442
|
-
// - Replace encrypted values (secure://) with canonical plaintext
|
|
443
|
-
const result = { ...mergedSecrets };
|
|
444
|
-
for (const [key, canonicalValue] of Object.entries(configSecrets)) {
|
|
445
|
-
mergeSecretValue(result, key, canonicalValue);
|
|
446
|
-
}
|
|
447
|
-
mergedSecrets = result;
|
|
448
|
-
} catch {
|
|
449
|
-
// ignore and fall through
|
|
450
|
-
}
|
|
451
|
-
return mergedSecrets;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
440
|
/**
|
|
455
441
|
* Ensure secrets map is non-empty or throw a friendly guidance error
|
|
456
442
|
* @function ensureNonEmptySecrets
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test log writer - writes debug logs to integration/<appKey>/logs/
|
|
3
|
+
* Sanitization (tokens, secrets) is done by dataplane before responses are returned.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Write test request/response logs for debugging
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs').promises;
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prepare object for JSON serialization (handles circular refs)
|
|
15
|
+
* @param {*} obj - Object to prepare
|
|
16
|
+
* @param {Set} [seen] - Set of seen object references (for circular refs)
|
|
17
|
+
* @returns {*} Copy safe for JSON.stringify
|
|
18
|
+
*/
|
|
19
|
+
function sanitizeForLog(obj, seen = new Set()) {
|
|
20
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') return obj;
|
|
21
|
+
if (seen.has(obj)) return '[Circular]';
|
|
22
|
+
seen.add(obj);
|
|
23
|
+
if (Array.isArray(obj)) return obj.map(item => sanitizeForLog(item, seen));
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
26
|
+
out[key] = sanitizeForLog(value, seen);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Write test log to integration/<appKey>/logs/<logType>-<timestamp>.json
|
|
33
|
+
* @async
|
|
34
|
+
* @param {string} appKey - Application key (used for path)
|
|
35
|
+
* @param {Object} data - Log data (request, response) - will be sanitized
|
|
36
|
+
* @param {string} [logType] - Log type prefix (default: test-integration)
|
|
37
|
+
* @param {string} [integrationBaseDir] - Base dir for integration (default: cwd/integration)
|
|
38
|
+
* @returns {Promise<string>} Path to written file
|
|
39
|
+
* @throws {Error} If write fails
|
|
40
|
+
*/
|
|
41
|
+
async function writeTestLog(appKey, data, logType = 'test-integration', integrationBaseDir) {
|
|
42
|
+
const baseDir = integrationBaseDir || path.join(process.cwd(), 'integration');
|
|
43
|
+
const logsDir = path.join(baseDir, appKey, 'logs');
|
|
44
|
+
await fs.mkdir(logsDir, { recursive: true });
|
|
45
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
46
|
+
const filename = `${logType}-${timestamp}.json`;
|
|
47
|
+
const filePath = path.join(logsDir, filename);
|
|
48
|
+
const sanitized = sanitizeForLog(data);
|
|
49
|
+
await fs.writeFile(filePath, JSON.stringify(sanitized, null, 2), 'utf8');
|
|
50
|
+
return filePath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
sanitizeForLog,
|
|
55
|
+
writeTestLog
|
|
56
|
+
};
|
|
@@ -247,48 +247,29 @@ async function tryClientTokenAuth(environment, appName, controllerUrl) {
|
|
|
247
247
|
const clientToken = await getOrRefreshClientToken(environment, appName, controllerUrl);
|
|
248
248
|
if (clientToken && clientToken.token) {
|
|
249
249
|
return {
|
|
250
|
-
type: '
|
|
250
|
+
type: 'client-token',
|
|
251
251
|
token: clientToken.token,
|
|
252
252
|
controller: clientToken.controller
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
255
|
} catch {
|
|
256
|
-
// Client token unavailable; getDeploymentAuth will try
|
|
257
|
-
}
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Tries to get client credentials for deployment auth
|
|
263
|
-
* @async
|
|
264
|
-
* @function tryClientCredentialsAuth
|
|
265
|
-
* @param {string} appName - Application name
|
|
266
|
-
* @param {string} controllerUrl - Controller URL
|
|
267
|
-
* @returns {Promise<Object|null>} Auth config with client credentials or null
|
|
268
|
-
*/
|
|
269
|
-
async function tryClientCredentialsAuth(appName, controllerUrl) {
|
|
270
|
-
const credentials = await loadClientCredentials(appName);
|
|
271
|
-
if (credentials && credentials.clientId && credentials.clientSecret) {
|
|
272
|
-
return {
|
|
273
|
-
type: 'client-credentials',
|
|
274
|
-
clientId: credentials.clientId,
|
|
275
|
-
clientSecret: credentials.clientSecret,
|
|
276
|
-
controller: controllerUrl
|
|
277
|
-
};
|
|
256
|
+
// Client token unavailable; getDeploymentAuth will try exchanging credentials for token (no warning here to avoid misleading output when refresh succeeds)
|
|
278
257
|
}
|
|
279
258
|
return null;
|
|
280
259
|
}
|
|
281
260
|
|
|
282
261
|
/**
|
|
283
262
|
* Get deployment authentication configuration with priority:
|
|
284
|
-
* 1. Device token (
|
|
285
|
-
* 2. Client token (
|
|
286
|
-
* 3.
|
|
263
|
+
* 1. Device token → type 'bearer' (user token) → send as Authorization: Bearer
|
|
264
|
+
* 2. Client token → type 'client-token' (application token) → send as x-client-token header
|
|
265
|
+
* 3. When no token available: if client credentials exist, exchange for client token and return type 'client-token'.
|
|
266
|
+
*
|
|
267
|
+
* x-client-id/x-client-secret are used only at the token-issuing endpoint (e.g. POST /api/v1/auth/token).
|
|
287
268
|
*
|
|
288
269
|
* @param {string} controllerUrl - Controller URL
|
|
289
270
|
* @param {string} environment - Environment key
|
|
290
271
|
* @param {string} appName - Application name
|
|
291
|
-
* @returns {Promise<{type: 'bearer'|'client-
|
|
272
|
+
* @returns {Promise<{type: 'bearer'|'client-token', token: string, controller: string}>} Auth config: bearer = user token, client-token = app token (x-client-token header)
|
|
292
273
|
* @throws {Error} If no authentication method is available
|
|
293
274
|
*/
|
|
294
275
|
async function getDeploymentAuth(controllerUrl, environment, appName) {
|
|
@@ -306,10 +287,21 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
|
|
|
306
287
|
return clientTokenAuth;
|
|
307
288
|
}
|
|
308
289
|
|
|
309
|
-
// Priority 3:
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
|
|
290
|
+
// Priority 3: Exchange client credentials for a token (never return client-credentials for app endpoints)
|
|
291
|
+
const credentials = await loadClientCredentials(appName);
|
|
292
|
+
if (credentials && credentials.clientId && credentials.clientSecret) {
|
|
293
|
+
try {
|
|
294
|
+
const refreshed = await refreshClientToken(environment, appName, controllerUrl);
|
|
295
|
+
if (refreshed && refreshed.token) {
|
|
296
|
+
return {
|
|
297
|
+
type: 'client-token',
|
|
298
|
+
token: refreshed.token,
|
|
299
|
+
controller: controllerUrl
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
// Refresh failed; fall through to throw below
|
|
304
|
+
}
|
|
313
305
|
}
|
|
314
306
|
|
|
315
307
|
throw new Error(`No authentication method available. Run 'aifabrix login' for device token, or add credentials to ~/.aifabrix/secrets.local.yaml as '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`);
|
|
@@ -337,7 +329,7 @@ async function extractClientCredentials(authConfig, appKey, envKey, _options = {
|
|
|
337
329
|
};
|
|
338
330
|
}
|
|
339
331
|
|
|
340
|
-
if (authConfig.type === 'bearer') {
|
|
332
|
+
if (authConfig.type === 'bearer' || authConfig.type === 'client-token') {
|
|
341
333
|
if (authConfig.clientId && authConfig.clientSecret) {
|
|
342
334
|
return {
|
|
343
335
|
clientId: authConfig.clientId,
|