@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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves configuration section values for upload (variable → .env, keyvault → secrets)
|
|
3
|
+
* and re-templates configuration on download from env.template.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Configuration env resolution for external system upload/download
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { getIntegrationPath } = require('./paths');
|
|
13
|
+
const { parseEnvToMap, resolveKvValue } = require('./credential-secrets-env');
|
|
14
|
+
const { loadSecrets, resolveKvReferences } = require('../core/secrets');
|
|
15
|
+
const { loadEnvTemplate } = require('./secrets-helpers');
|
|
16
|
+
const { getActualSecretsPath } = require('./secrets-path');
|
|
17
|
+
|
|
18
|
+
const VAR_PLACEHOLDER_REGEX = /\{\{([^}]+)\}\}/g;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds resolved env map and secrets for an integration app (for configuration resolution).
|
|
22
|
+
* If .env exists, parses it; otherwise resolves env.template with secrets and parses the result.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} systemKey - External system key (e.g. 'my-sharepoint')
|
|
25
|
+
* @returns {Promise<{ envMap: Object.<string, string>, secrets: Object }>} envMap for {{VAR}} substitution, secrets for kv://
|
|
26
|
+
* @throws {Error} If env.template is missing when .env is missing (only when building from template)
|
|
27
|
+
*/
|
|
28
|
+
async function buildResolvedEnvMapForIntegration(systemKey) {
|
|
29
|
+
if (!systemKey || typeof systemKey !== 'string') {
|
|
30
|
+
throw new Error('systemKey is required and must be a string');
|
|
31
|
+
}
|
|
32
|
+
const integrationPath = getIntegrationPath(systemKey);
|
|
33
|
+
const envPath = path.join(integrationPath, '.env');
|
|
34
|
+
const envTemplatePath = path.join(integrationPath, 'env.template');
|
|
35
|
+
|
|
36
|
+
let secrets = {};
|
|
37
|
+
try {
|
|
38
|
+
secrets = await loadSecrets(undefined, systemKey);
|
|
39
|
+
} catch {
|
|
40
|
+
secrets = {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let envMap = {};
|
|
44
|
+
if (fs.existsSync(envPath)) {
|
|
45
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
46
|
+
envMap = parseEnvToMap(content);
|
|
47
|
+
} else if (fs.existsSync(envTemplatePath)) {
|
|
48
|
+
const templateContent = loadEnvTemplate(envTemplatePath);
|
|
49
|
+
const secretsPaths = await getActualSecretsPath(undefined, systemKey);
|
|
50
|
+
const resolvedContent = await resolveKvReferences(
|
|
51
|
+
templateContent,
|
|
52
|
+
secrets,
|
|
53
|
+
'local',
|
|
54
|
+
secretsPaths,
|
|
55
|
+
systemKey
|
|
56
|
+
);
|
|
57
|
+
envMap = parseEnvToMap(resolvedContent);
|
|
58
|
+
}
|
|
59
|
+
return { envMap, secrets };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolves {{VAR}} in a string using envMap. Throws if any variable is missing.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} value - Value that may contain {{VAR}}
|
|
66
|
+
* @param {Object.<string, string>} envMap - Resolved env key-value map
|
|
67
|
+
* @param {string} [systemKey] - System key for error message
|
|
68
|
+
* @returns {string} Value with {{VAR}} replaced
|
|
69
|
+
* @throws {Error} If a {{VAR}} is missing from envMap
|
|
70
|
+
*/
|
|
71
|
+
function substituteVarPlaceholders(value, envMap, systemKey) {
|
|
72
|
+
const hint = systemKey ? ` Run 'aifabrix resolve ${systemKey}' or set the variable in .env.` : '';
|
|
73
|
+
return value.replace(VAR_PLACEHOLDER_REGEX, (match, varName) => {
|
|
74
|
+
const key = varName.trim();
|
|
75
|
+
if (envMap[key] === undefined || envMap[key] === null) {
|
|
76
|
+
throw new Error(`Missing configuration env var: ${key}.${hint}`);
|
|
77
|
+
}
|
|
78
|
+
return String(envMap[key]);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves configuration array values in place by location: variable → {{VAR}} from envMap;
|
|
84
|
+
* keyvault → kv:// from secrets. Does not log or expose secret values.
|
|
85
|
+
*
|
|
86
|
+
* @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
|
|
87
|
+
* @param {Object.<string, string>} envMap - Resolved env map from buildResolvedEnvMapForIntegration
|
|
88
|
+
* @param {Object} secrets - Loaded secrets for kv:// resolution
|
|
89
|
+
* @param {string} [systemKey] - System key for error messages
|
|
90
|
+
* @throws {Error} If variable env is missing or keyvault secret unresolved (message never contains secret values)
|
|
91
|
+
*/
|
|
92
|
+
function resolveConfigurationValues(configArray, envMap, secrets, systemKey) {
|
|
93
|
+
if (!Array.isArray(configArray)) return;
|
|
94
|
+
const hint = systemKey ? ` Run 'aifabrix resolve ${systemKey}' and ensure the key exists in the secrets file.` : '';
|
|
95
|
+
for (const item of configArray) {
|
|
96
|
+
if (!item || typeof item.value !== 'string') continue;
|
|
97
|
+
const location = (item.location || '').toLowerCase();
|
|
98
|
+
if (location === 'variable') {
|
|
99
|
+
if (item.value.trim().startsWith('kv://')) {
|
|
100
|
+
throw new Error(`Configuration entry '${item.name || 'unknown'}' has location 'variable' but value is kv://. Use location 'keyvault' for secrets.`);
|
|
101
|
+
}
|
|
102
|
+
item.value = substituteVarPlaceholders(item.value, envMap, systemKey);
|
|
103
|
+
} else if (location === 'keyvault') {
|
|
104
|
+
const resolved = resolveKvValue(secrets, item.value);
|
|
105
|
+
if (resolved === null || resolved === undefined) {
|
|
106
|
+
throw new Error(`Unresolved keyvault reference for configuration '${item.name || 'unknown'}'.${hint}`);
|
|
107
|
+
}
|
|
108
|
+
item.value = resolved;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns the set of variable names (keys) defined in env.template content.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} envTemplateContent - Raw env.template content
|
|
117
|
+
* @returns {Set<string>} Set of variable names
|
|
118
|
+
*/
|
|
119
|
+
function getEnvTemplateVariableNames(envTemplateContent) {
|
|
120
|
+
const names = new Set();
|
|
121
|
+
if (!envTemplateContent || typeof envTemplateContent !== 'string') return names;
|
|
122
|
+
const lines = envTemplateContent.split(/\r?\n/);
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
126
|
+
const eq = trimmed.indexOf('=');
|
|
127
|
+
if (eq > 0) {
|
|
128
|
+
const key = trimmed.substring(0, eq).trim();
|
|
129
|
+
if (key) names.add(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return names;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Re-templates configuration from env.template: for each entry with location === 'variable'
|
|
137
|
+
* whose name matches a key in env.template, sets value to {{name}}. Mutates configArray in place.
|
|
138
|
+
*
|
|
139
|
+
* @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
|
|
140
|
+
* @param {Set<string>} envTemplateVariableNames - Variable names present in env.template
|
|
141
|
+
*/
|
|
142
|
+
function retemplateConfigurationFromEnvTemplate(configArray, envTemplateVariableNames) {
|
|
143
|
+
if (!Array.isArray(configArray) || !envTemplateVariableNames || !envTemplateVariableNames.size) return;
|
|
144
|
+
for (const item of configArray) {
|
|
145
|
+
if (!item || (item.location || '').toLowerCase() !== 'variable') continue;
|
|
146
|
+
const name = item.name && String(item.name).trim();
|
|
147
|
+
if (name && envTemplateVariableNames.has(name)) {
|
|
148
|
+
item.value = `{{${name}}}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Reads env.template at integration path, re-templates the given configuration array,
|
|
155
|
+
* and returns the updated array (mutates in place). If env.template is missing, does nothing.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} systemKey - External system key
|
|
158
|
+
* @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
|
|
159
|
+
* @returns {Promise<boolean>} True if re-templating was applied (env.template existed)
|
|
160
|
+
*/
|
|
161
|
+
async function retemplateConfigurationForDownload(systemKey, configArray) {
|
|
162
|
+
if (!systemKey || typeof systemKey !== 'string' || !Array.isArray(configArray)) return false;
|
|
163
|
+
const integrationPath = getIntegrationPath(systemKey);
|
|
164
|
+
const envTemplatePath = path.join(integrationPath, 'env.template');
|
|
165
|
+
if (!fs.existsSync(envTemplatePath)) return false;
|
|
166
|
+
const content = fs.readFileSync(envTemplatePath, 'utf8');
|
|
167
|
+
const names = getEnvTemplateVariableNames(content);
|
|
168
|
+
retemplateConfigurationFromEnvTemplate(configArray, names);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
buildResolvedEnvMapForIntegration,
|
|
174
|
+
resolveConfigurationValues,
|
|
175
|
+
getEnvTemplateVariableNames,
|
|
176
|
+
retemplateConfigurationFromEnvTemplate,
|
|
177
|
+
retemplateConfigurationForDownload,
|
|
178
|
+
substituteVarPlaceholders
|
|
179
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential display utilities – status icons and formatting for CLI output
|
|
3
|
+
* Aligns with dataplane credential status lifecycle (pending, verified, failed, expired).
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Credential status formatter with icons and chalk colors
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
/** @type {{ verified: string, pending: string, failed: string, expired: string }} */
|
|
13
|
+
const STATUS_ICONS = {
|
|
14
|
+
verified: ' ✓',
|
|
15
|
+
pending: ' ○',
|
|
16
|
+
failed: ' ✗',
|
|
17
|
+
expired: ' ⊘'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** @type {{ verified: string, pending: string, failed: string, expired: string }} */
|
|
21
|
+
const STATUS_LABELS = {
|
|
22
|
+
verified: 'Valid',
|
|
23
|
+
pending: 'Not tested',
|
|
24
|
+
failed: 'Connection failed',
|
|
25
|
+
expired: 'Token expired'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Chalk color functions per status
|
|
30
|
+
* @type {{ verified: Function, pending: Function, failed: Function, expired: Function }}
|
|
31
|
+
*/
|
|
32
|
+
const STATUS_CHALK = {
|
|
33
|
+
verified: chalk.green,
|
|
34
|
+
pending: chalk.gray,
|
|
35
|
+
failed: chalk.red,
|
|
36
|
+
expired: chalk.yellow
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const VALID_STATUSES = ['verified', 'pending', 'failed', 'expired'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format credential status for display (icon + optional label)
|
|
43
|
+
* @param {string} [status] - Credential status (pending | verified | failed | expired)
|
|
44
|
+
* @returns {{ icon: string, color: Function, label: string } | null} Status info or null when missing/invalid
|
|
45
|
+
*/
|
|
46
|
+
function formatCredentialStatus(status) {
|
|
47
|
+
if (!status || typeof status !== 'string') return null;
|
|
48
|
+
const s = status.toLowerCase();
|
|
49
|
+
if (!VALID_STATUSES.includes(s)) return null;
|
|
50
|
+
return {
|
|
51
|
+
icon: STATUS_ICONS[s],
|
|
52
|
+
color: STATUS_CHALK[s],
|
|
53
|
+
label: STATUS_LABELS[s]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Format credential with status for CLI display
|
|
59
|
+
* @param {Object} credential - Credential object from API
|
|
60
|
+
* @param {string} [credential.key]
|
|
61
|
+
* @param {string} [credential.id]
|
|
62
|
+
* @param {string} [credential.credentialKey]
|
|
63
|
+
* @param {string} [credential.displayName]
|
|
64
|
+
* @param {string} [credential.name]
|
|
65
|
+
* @param {string} [credential.status]
|
|
66
|
+
* @returns {{ key: string, name: string, statusFormatted: string, statusLabel: string }}
|
|
67
|
+
*/
|
|
68
|
+
function formatCredentialWithStatus(credential) {
|
|
69
|
+
const key = credential?.key ?? credential?.id ?? credential?.credentialKey ?? '-';
|
|
70
|
+
const name = credential?.displayName ?? credential?.name ?? key;
|
|
71
|
+
const statusInfo = formatCredentialStatus(credential?.status);
|
|
72
|
+
const statusFormatted = statusInfo ? statusInfo.color(statusInfo.icon) : '';
|
|
73
|
+
const statusLabel = statusInfo ? ` (${statusInfo.label})` : '';
|
|
74
|
+
return { key, name, statusFormatted, statusLabel };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
STATUS_ICONS,
|
|
79
|
+
STATUS_LABELS,
|
|
80
|
+
STATUS_CHALK,
|
|
81
|
+
formatCredentialStatus,
|
|
82
|
+
formatCredentialWithStatus
|
|
83
|
+
};
|
|
@@ -16,24 +16,107 @@ const KV_PREFIX = 'KV_';
|
|
|
16
16
|
const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Converts
|
|
20
|
-
* @param {string}
|
|
21
|
-
* @returns {string
|
|
19
|
+
* Converts systemKey to KV_* prefix (e.g. hubspot -> HUBSPOT, my-hubspot -> MY_HUBSPOT).
|
|
20
|
+
* @param {string} systemKey - System key
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function systemKeyToKvPrefix(systemKey) {
|
|
24
|
+
if (!systemKey || typeof systemKey !== 'string') return '';
|
|
25
|
+
return systemKey.replace(/-/g, '_').toUpperCase();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps authentication security key (camelCase) to env VAR (UPPERCASE, no underscores).
|
|
30
|
+
* Used for canonical KV_<APPKEY>_<VAR> names (e.g. clientId → CLIENTID).
|
|
31
|
+
* @param {string} securityKey - Security key (e.g. 'clientId', 'clientSecret')
|
|
32
|
+
* @returns {string}
|
|
22
33
|
*/
|
|
23
|
-
function
|
|
24
|
-
if (!
|
|
25
|
-
|
|
34
|
+
function securityKeyToVar(securityKey) {
|
|
35
|
+
if (!securityKey || typeof securityKey !== 'string') return '';
|
|
36
|
+
return securityKey.replace(/_/g, '').toUpperCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Known single-segment variable suffixes (uppercase) for inferring var vs namespace in env keys */
|
|
40
|
+
const VAR_SUFFIXES = new Set(['ID', 'SECRET', 'KEY', 'TOKEN', 'URL', 'USERNAME', 'PASSWORD']);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Converts var segment(s) from env key to path-style camelCase (e.g. CLIENT_ID → clientId, CLIENTID → clientId).
|
|
44
|
+
* @param {string[]} varSegments - One or two segments (e.g. ['CLIENT', 'ID'], ['CLIENTID'])
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function varSegmentsToCamelCase(varSegments) {
|
|
48
|
+
if (!varSegments || varSegments.length === 0) return '';
|
|
49
|
+
if (varSegments.length === 1) {
|
|
50
|
+
const s = varSegments[0].toLowerCase();
|
|
51
|
+
if (s.endsWith('id') && s.length > 2) return s.slice(0, -2) + 'Id';
|
|
52
|
+
if (s.endsWith('secret') && s.length > 6) return s.slice(0, -6) + 'Secret';
|
|
53
|
+
if (s.endsWith('key') && s.length > 3) return s.slice(0, -3) + 'Key';
|
|
54
|
+
if (s.endsWith('token') && s.length > 5) return s.slice(0, -5) + 'Token';
|
|
55
|
+
if (s.endsWith('url') && s.length > 3) return s.slice(0, -3) + 'Url';
|
|
56
|
+
return s;
|
|
26
57
|
}
|
|
27
|
-
|
|
58
|
+
return varSegments.map((seg, i) => {
|
|
59
|
+
const lower = seg.toLowerCase();
|
|
60
|
+
return i === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
61
|
+
}).join('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Builds kv path from segments when systemKey is provided (path = kv://systemKey/variable).
|
|
66
|
+
* @param {string[]} segments - Parsed segments after KV_ prefix
|
|
67
|
+
* @param {string} systemKey - System key (e.g. 'microsoft-teams')
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
function kvPathWithSystemKey(segments, systemKey) {
|
|
71
|
+
const prefixInKey = systemKey.replace(/-/g, '_').toUpperCase();
|
|
72
|
+
const prefixSegs = prefixInKey.split('_').filter(Boolean);
|
|
73
|
+
if (segments.length <= prefixSegs.length) return null;
|
|
74
|
+
const prefixMatch = prefixSegs.every((p, i) => segments[i] === p);
|
|
75
|
+
if (!prefixMatch) return null;
|
|
76
|
+
const varSegments = segments.slice(prefixSegs.length);
|
|
77
|
+
const pathVar = varSegmentsToCamelCase(varSegments);
|
|
78
|
+
return pathVar ? `kv://${systemKey}/${pathVar}` : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Builds kv path from segments when systemKey is not provided (infers namespace and variable).
|
|
83
|
+
* @param {string[]} segments - Parsed segments after KV_ prefix
|
|
84
|
+
* @returns {string|null}
|
|
85
|
+
*/
|
|
86
|
+
function kvPathInferred(segments) {
|
|
87
|
+
if (segments.length === 1) return `kv://${segments[0].toLowerCase()}`;
|
|
88
|
+
const varSegmentCount = (segments.length >= 2 && VAR_SUFFIXES.has(segments[segments.length - 1])) ? 2 : 1;
|
|
89
|
+
const namespace = segments.slice(0, -varSegmentCount).map(s => s.toLowerCase()).join('-');
|
|
90
|
+
const varSegments = segments.slice(-varSegmentCount);
|
|
91
|
+
const pathVar = varSegmentsToCamelCase(varSegments);
|
|
92
|
+
return (namespace && pathVar) ? `kv://${namespace}/${pathVar}` : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Converts KV_* env key to kv:// path in format kv://<system-key>/<variable>.
|
|
97
|
+
* System-key uses hyphens (e.g. microsoft-teams); variable is camelCase (e.g. clientId).
|
|
98
|
+
* When systemKey is provided, uses it as the path namespace; otherwise infers from segments.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} envKey - Env var name (e.g. KV_MICROSOFT_TEAMS_CLIENT_ID)
|
|
101
|
+
* @param {string} [systemKey] - Optional system key (e.g. 'microsoft-teams'); when provided, path is kv://systemKey/variable
|
|
102
|
+
* @returns {string|null} kv:// path or null if invalid
|
|
103
|
+
*/
|
|
104
|
+
function kvEnvKeyToPath(envKey, systemKey) {
|
|
105
|
+
if (!envKey || typeof envKey !== 'string' || !envKey.toUpperCase().startsWith(KV_PREFIX)) return null;
|
|
106
|
+
const rest = envKey.slice(KV_PREFIX.length).trim();
|
|
28
107
|
if (!rest) return null;
|
|
29
108
|
const segments = rest.split('_').filter(Boolean);
|
|
30
|
-
|
|
31
|
-
|
|
109
|
+
if (segments.length === 0) return null;
|
|
110
|
+
|
|
111
|
+
const hasSystemKey = typeof systemKey === 'string' && systemKey.length > 0;
|
|
112
|
+
if (hasSystemKey) return kvPathWithSystemKey(segments, systemKey);
|
|
113
|
+
return kvPathInferred(segments);
|
|
32
114
|
}
|
|
33
115
|
|
|
34
116
|
/**
|
|
35
117
|
* Collects KV_* entries from env map as secret items (key = kv path, value = raw).
|
|
36
|
-
*
|
|
118
|
+
* When value is a kv:// path, uses it as the key so it matches payload paths (e.g. kv://microsoft-teams/clientId).
|
|
119
|
+
* Otherwise derives path from env key via kvEnvKeyToPath(envKey).
|
|
37
120
|
*
|
|
38
121
|
* @param {Object.<string, string>} envMap - Key-value map from .env
|
|
39
122
|
* @returns {Array<{ key: string, value: string }>} Items (key = kv://..., value = raw)
|
|
@@ -46,7 +129,11 @@ function collectKvEnvVarsAsSecretItems(envMap) {
|
|
|
46
129
|
for (const [envKey, rawValue] of Object.entries(envMap)) {
|
|
47
130
|
const value = typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
48
131
|
if (value === '') continue;
|
|
49
|
-
|
|
132
|
+
let kvPath = null;
|
|
133
|
+
if (value.startsWith('kv://') && isValidKvPath(value)) {
|
|
134
|
+
kvPath = value;
|
|
135
|
+
}
|
|
136
|
+
if (!kvPath) kvPath = kvEnvKeyToPath(envKey);
|
|
50
137
|
if (!kvPath) continue;
|
|
51
138
|
items.push({ key: kvPath, value });
|
|
52
139
|
}
|
|
@@ -70,10 +157,7 @@ function resolveKvValue(secrets, value) {
|
|
|
70
157
|
const pathMatch = trimmed.match(/^kv:\/\/([a-zA-Z0-9_\-/]+)$/);
|
|
71
158
|
if (!pathMatch) return null;
|
|
72
159
|
const pathKey = pathMatch[1];
|
|
73
|
-
|
|
74
|
-
if (resolved === undefined && pathKey.includes('/')) {
|
|
75
|
-
resolved = secrets[pathKey.replace(/\//g, '-')];
|
|
76
|
-
}
|
|
160
|
+
const resolved = secrets[pathKey];
|
|
77
161
|
if (resolved === undefined) return null;
|
|
78
162
|
return typeof resolved === 'string' ? resolved : String(resolved);
|
|
79
163
|
}
|
|
@@ -156,10 +240,7 @@ function buildItemsFromPayload(payload, secrets, itemsByKey) {
|
|
|
156
240
|
for (const ref of refs) {
|
|
157
241
|
if (existingKeys.has(ref)) continue;
|
|
158
242
|
const pathKey = ref.replace(/^kv:\/\//, '');
|
|
159
|
-
|
|
160
|
-
if (resolved === undefined && pathKey.includes('/')) {
|
|
161
|
-
resolved = secrets[pathKey.replace(/\//g, '-')];
|
|
162
|
-
}
|
|
243
|
+
const resolved = secrets[pathKey];
|
|
163
244
|
if (resolved !== null && resolved !== undefined && isValidKvPath(ref)) {
|
|
164
245
|
itemsByKey.set(ref, typeof resolved === 'string' ? resolved : String(resolved));
|
|
165
246
|
}
|
|
@@ -188,12 +269,14 @@ function storedCountFromResponse(res, fallback) {
|
|
|
188
269
|
async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
|
|
189
270
|
try {
|
|
190
271
|
const res = await storeCredentialSecrets(dataplaneUrl, authConfig, items);
|
|
191
|
-
|
|
272
|
+
const failed = res && (res.success === false || res.data?.success === false);
|
|
273
|
+
if (failed) {
|
|
192
274
|
const status = res.status ?? res.statusCode;
|
|
193
275
|
if (status === 403 || status === 401) {
|
|
194
276
|
return { pushed: 0, warning: 'Could not push credential secrets (permission denied or unauthenticated). Ensure dataplane role has credential:create if you use KV_* in .env.' };
|
|
195
277
|
}
|
|
196
|
-
|
|
278
|
+
const errMsg = res.formattedError || res.data?.formattedError || res.error || res.data?.error || 'Failed to push credential secrets to dataplane.';
|
|
279
|
+
return { pushed: 0, warning: errMsg };
|
|
197
280
|
}
|
|
198
281
|
return { pushed: storedCountFromResponse(res, items.length) };
|
|
199
282
|
} catch (err) {
|
|
@@ -212,7 +295,7 @@ async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
|
|
|
212
295
|
* @param {string} [options.envFilePath] - Path to .env (integration/<systemKey>/.env)
|
|
213
296
|
* @param {string} [options.appName] - App/system name for loadSecrets context
|
|
214
297
|
* @param {Object} [options.payload] - Upload payload { application, dataSources } for kv scan
|
|
215
|
-
* @returns {Promise<{ pushed: number, warning?: string }>} Count pushed
|
|
298
|
+
* @returns {Promise<{ pushed: number, keys?: string[], skipped?: boolean, warning?: string }>} Count pushed, keys (on success), skipped (when nothing to push), optional warning
|
|
216
299
|
*/
|
|
217
300
|
async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
|
|
218
301
|
const { envFilePath, appName, payload } = options;
|
|
@@ -230,8 +313,12 @@ async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
|
|
|
230
313
|
.filter(([k]) => isValidKvPath(k))
|
|
231
314
|
.map(([key, value]) => ({ key, value }));
|
|
232
315
|
|
|
233
|
-
if (items.length === 0) return { pushed: 0 };
|
|
234
|
-
|
|
316
|
+
if (items.length === 0) return { pushed: 0, skipped: true };
|
|
317
|
+
const sendResult = await sendCredentialSecrets(dataplaneUrl, authConfig, items);
|
|
318
|
+
if (sendResult.pushed > 0) {
|
|
319
|
+
sendResult.keys = items.map(i => i.key.replace(/^kv:\/\//, ''));
|
|
320
|
+
}
|
|
321
|
+
return sendResult;
|
|
235
322
|
}
|
|
236
323
|
|
|
237
324
|
/**
|
|
@@ -262,6 +349,9 @@ module.exports = {
|
|
|
262
349
|
collectKvRefsFromPayload,
|
|
263
350
|
pushCredentialSecrets,
|
|
264
351
|
kvEnvKeyToPath,
|
|
352
|
+
systemKeyToKvPrefix,
|
|
353
|
+
securityKeyToVar,
|
|
265
354
|
isValidKvPath,
|
|
266
|
-
resolveKvValue
|
|
355
|
+
resolveKvValue,
|
|
356
|
+
parseEnvToMap
|
|
267
357
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared warning message for Dataplane pipeline API usage (upload / validate / publish).
|
|
3
|
+
* Used by upload command and datasource upload so users know configuration is sent to Dataplane.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Dataplane pipeline usage warning
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('./logger');
|
|
12
|
+
|
|
13
|
+
/** Message shown when CLI is about to call Dataplane pipeline upload or publish APIs. */
|
|
14
|
+
const DATAPLANE_PIPELINE_WARNING =
|
|
15
|
+
'Configuration will be sent to the Dataplane pipeline API. Ensure you are targeting the correct environment and have the required permissions.';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Log the Dataplane pipeline warning (yellow) to the console.
|
|
19
|
+
* Call before uploadApplicationViaPipeline or publishDatasourceViaPipeline.
|
|
20
|
+
*/
|
|
21
|
+
function logDataplanePipelineWarning() {
|
|
22
|
+
logger.log(chalk.yellow(`⚠ ${DATAPLANE_PIPELINE_WARNING}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
DATAPLANE_PIPELINE_WARNING,
|
|
27
|
+
logDataplanePipelineWarning
|
|
28
|
+
};
|
|
@@ -34,7 +34,7 @@ function processSuccessfulValidation(responseData) {
|
|
|
34
34
|
function processValidationFailure(responseData) {
|
|
35
35
|
const errorMessage = responseData.errors && responseData.errors.length > 0
|
|
36
36
|
? `Validation failed: ${responseData.errors.join(', ')}`
|
|
37
|
-
: 'Validation failed: Invalid configuration';
|
|
37
|
+
: (responseData.error || responseData.formattedError || 'Validation failed: Invalid configuration');
|
|
38
38
|
const error = new Error(errorMessage);
|
|
39
39
|
error.status = 400;
|
|
40
40
|
error.data = responseData;
|
|
@@ -78,13 +78,13 @@ function handleValidationResponse(response) {
|
|
|
78
78
|
if (responseData.valid === true) {
|
|
79
79
|
return processSuccessfulValidation(responseData);
|
|
80
80
|
}
|
|
81
|
-
// Handle validation failure (valid: false)
|
|
82
|
-
if (responseData.valid === false) {
|
|
81
|
+
// Handle validation failure (valid: false or success: false in body)
|
|
82
|
+
if (responseData.valid === false || responseData.success === false) {
|
|
83
83
|
processValidationFailure(responseData);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
// Handle validation errors (non-success responses)
|
|
87
|
+
// Handle validation errors (non-success HTTP responses)
|
|
88
88
|
if (!response.success) {
|
|
89
89
|
processValidationError(response);
|
|
90
90
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev CA Install – SSL untrusted detection, fetch CA from Builder Server, install into OS trust store.
|
|
3
|
+
* Used by `aifabrix dev init` when the server certificate is self-signed. Only /install-ca uses
|
|
4
|
+
* rejectUnauthorized: false; all other requests use default TLS verification.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview CA install utilities for development Builder Server
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const https = require('https');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const { execFileSync } = require('child_process');
|
|
16
|
+
const readline = require('readline');
|
|
17
|
+
const chalk = require('chalk');
|
|
18
|
+
|
|
19
|
+
const SSL_UNTRUSTED_CODES = [
|
|
20
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
21
|
+
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
22
|
+
'CERT_UNTRUSTED',
|
|
23
|
+
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
24
|
+
'UNABLE_TO_GET_ISSUER_CERT',
|
|
25
|
+
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if the error indicates an untrusted/self-signed server certificate.
|
|
30
|
+
* @param {Error} err - Thrown error (e.g. from devApi.getHealth)
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isSslUntrustedError(err) {
|
|
34
|
+
const code = err?.code || err?.cause?.code;
|
|
35
|
+
const msg = (err?.message || '').toUpperCase();
|
|
36
|
+
return SSL_UNTRUSTED_CODES.some(c => code === c || msg.includes(c));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetch CA PEM from Builder Server via GET {baseUrl}/install-ca.
|
|
41
|
+
* Uses rejectUnauthorized: false only for this endpoint (dev setup).
|
|
42
|
+
* @param {string} baseUrl - Builder Server base URL (no trailing slash)
|
|
43
|
+
* @returns {Promise<Buffer>} CA certificate PEM
|
|
44
|
+
*/
|
|
45
|
+
function fetchInstallCa(baseUrl) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const url = `${baseUrl.replace(/\/+$/, '')}/install-ca`;
|
|
48
|
+
const urlObj = new URL(url);
|
|
49
|
+
if (urlObj.protocol !== 'https:') {
|
|
50
|
+
reject(new Error('install-ca requires https URL'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
54
|
+
const req = https.get(
|
|
55
|
+
url,
|
|
56
|
+
{ agent },
|
|
57
|
+
(res) => {
|
|
58
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
59
|
+
req.destroy();
|
|
60
|
+
fetchInstallCa(res.headers.location).then(resolve).catch(reject);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const chunks = [];
|
|
64
|
+
res.on('data', c => chunks.push(c));
|
|
65
|
+
res.on('end', () => {
|
|
66
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
67
|
+
if (!body || !body.includes('-----BEGIN CERTIFICATE-----')) {
|
|
68
|
+
reject(new Error('Invalid CA response: expected PEM certificate'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
resolve(Buffer.from(body, 'utf8'));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
req.on('error', reject);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Install CA PEM into OS trust store (platform-specific).
|
|
81
|
+
* @param {Buffer|string} caPem - CA certificate PEM
|
|
82
|
+
* @param {string} baseUrl - Builder Server base URL (for help link)
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
async function installCaPlatform(caPem, baseUrl) {
|
|
86
|
+
const pem = Buffer.isBuffer(caPem) ? caPem.toString('utf8') : String(caPem);
|
|
87
|
+
const tmpDir = os.tmpdir();
|
|
88
|
+
const tmpPath = path.join(tmpDir, 'aifabrix-root-ca.crt');
|
|
89
|
+
await fs.writeFile(tmpPath, pem, { mode: 0o644 });
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (process.platform === 'win32') {
|
|
93
|
+
execFileSync('certutil', ['-addstore', '-user', 'ROOT', tmpPath], { stdio: 'inherit' });
|
|
94
|
+
} else if (process.platform === 'darwin') {
|
|
95
|
+
const keychain = path.join(os.homedir(), 'Library', 'Keychains', 'login.keychain-db');
|
|
96
|
+
execFileSync('security', ['add-trusted-cert', '-d', '-r', 'trustRoot', '-k', keychain, tmpPath], { stdio: 'inherit' });
|
|
97
|
+
} else if (process.platform === 'linux') {
|
|
98
|
+
const certPath = '/usr/local/share/ca-certificates/aifabrix-root-ca.crt';
|
|
99
|
+
try {
|
|
100
|
+
await fs.writeFile(certPath, pem, { mode: 0o644 });
|
|
101
|
+
execFileSync('update-ca-certificates', [], { stdio: 'inherit' });
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e.code === 'EACCES' || (e.status !== undefined && e.status !== null && e.status !== 0)) {
|
|
104
|
+
const helpUrl = `${baseUrl.replace(/\/+$/, '')}/install-ca-help`;
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Linux CA install requires sudo. Save CA manually from ${helpUrl} to ${certPath} and run: sudo update-ca-certificates`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
await fs.unlink(tmpPath).catch(() => {});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Prompt user: "Download and install the development CA? (y/n)"
|
|
121
|
+
* @returns {Promise<boolean>}
|
|
122
|
+
*/
|
|
123
|
+
function promptInstallCa() {
|
|
124
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
125
|
+
return new Promise(resolve => {
|
|
126
|
+
rl.question(chalk.yellow('Server certificate not trusted. Download and install the development CA? (y/n) '), answer => {
|
|
127
|
+
rl.close();
|
|
128
|
+
const normalized = (answer || '').trim().toLowerCase();
|
|
129
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
isSslUntrustedError,
|
|
136
|
+
fetchInstallCa,
|
|
137
|
+
installCaPlatform,
|
|
138
|
+
promptInstallCa
|
|
139
|
+
};
|