@aifabrix/builder 2.40.2 → 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 +7 -5
- 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/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +2 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- 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/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- 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 +44 -11
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +12 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +9 -6
- package/lib/app/run-env-compose.js +264 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show-display.js +1 -1
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +172 -15
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +206 -16
- package/lib/cli/setup-environment.js +16 -6
- package/lib/cli/setup-external-system.js +89 -24
- package/lib/cli/setup-infra.js +82 -15
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +129 -24
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- 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-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +347 -0
- 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/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +96 -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/admin-secrets.js +96 -0
- package/lib/core/config.js +7 -1
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +176 -89
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/deployer.js +7 -5
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +188 -203
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-auth.js +7 -3
- 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 +56 -19
- 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 +177 -25
- 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 +155 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +98 -12
- package/lib/infrastructure/services.js +88 -22
- package/lib/schema/application-schema.json +32 -8
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +509 -411
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +41 -13
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +77 -76
- package/lib/utils/compose-handlebars-helpers.js +54 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +357 -0
- 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/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +103 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -2
- 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 +16 -2
- package/lib/utils/infra-status.js +80 -45
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +128 -37
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +114 -6
- package/lib/utils/secrets-helpers.js +108 -114
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +3 -3
- 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 +72 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +8 -3
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +15 -10
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +12 -10
- 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 +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
- package/integration/hubspot/application.yaml +0 -37
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect KV_* env vars as secret store items and push to dataplane.
|
|
3
|
+
* Reads integration .env, resolves kv:// in values, scans payload for kv:// refs,
|
|
4
|
+
* and pushes plain values to POST /api/v1/credential/secret.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Credential secrets push from .env and payload (Dataplane)
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { loadSecrets } = require('../core/secrets');
|
|
13
|
+
const { storeCredentialSecrets } = require('../api/credential.api');
|
|
14
|
+
|
|
15
|
+
const KV_PREFIX = 'KV_';
|
|
16
|
+
const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
|
|
17
|
+
|
|
18
|
+
/**
|
|
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}
|
|
33
|
+
*/
|
|
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;
|
|
57
|
+
}
|
|
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();
|
|
107
|
+
if (!rest) return null;
|
|
108
|
+
const segments = rest.split('_').filter(Boolean);
|
|
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);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Collects KV_* entries from env map as secret items (key = kv path, value = raw).
|
|
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).
|
|
120
|
+
*
|
|
121
|
+
* @param {Object.<string, string>} envMap - Key-value map from .env
|
|
122
|
+
* @returns {Array<{ key: string, value: string }>} Items (key = kv://..., value = raw)
|
|
123
|
+
*/
|
|
124
|
+
function collectKvEnvVarsAsSecretItems(envMap) {
|
|
125
|
+
if (!envMap || typeof envMap !== 'object') {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const items = [];
|
|
129
|
+
for (const [envKey, rawValue] of Object.entries(envMap)) {
|
|
130
|
+
const value = typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
131
|
+
if (value === '') continue;
|
|
132
|
+
let kvPath = null;
|
|
133
|
+
if (value.startsWith('kv://') && isValidKvPath(value)) {
|
|
134
|
+
kvPath = value;
|
|
135
|
+
}
|
|
136
|
+
if (!kvPath) kvPath = kvEnvKeyToPath(envKey);
|
|
137
|
+
if (!kvPath) continue;
|
|
138
|
+
items.push({ key: kvPath, value });
|
|
139
|
+
}
|
|
140
|
+
return items;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves a single value if it is a kv:// reference using secrets map.
|
|
145
|
+
* Supports path-style keys (e.g. secrets/foo) and hyphen-style (secrets-foo).
|
|
146
|
+
*
|
|
147
|
+
* @param {Object} secrets - Loaded secrets object
|
|
148
|
+
* @param {string} value - Value (may be "kv://..." or plain)
|
|
149
|
+
* @returns {string|null} Resolved plain value or null if unresolved
|
|
150
|
+
*/
|
|
151
|
+
function resolveKvValue(secrets, value) {
|
|
152
|
+
if (typeof value !== 'string') return null;
|
|
153
|
+
const trimmed = value.trim();
|
|
154
|
+
if (!trimmed.startsWith('kv://')) {
|
|
155
|
+
return trimmed;
|
|
156
|
+
}
|
|
157
|
+
const pathMatch = trimmed.match(/^kv:\/\/([a-zA-Z0-9_\-/]+)$/);
|
|
158
|
+
if (!pathMatch) return null;
|
|
159
|
+
const pathKey = pathMatch[1];
|
|
160
|
+
const resolved = secrets[pathKey];
|
|
161
|
+
if (resolved === undefined) return null;
|
|
162
|
+
return typeof resolved === 'string' ? resolved : String(resolved);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Recursively collects all kv:// references from a JSON-serializable payload.
|
|
167
|
+
*
|
|
168
|
+
* @param {Object|Array|string|number|boolean|null} obj - Upload payload (application + dataSources)
|
|
169
|
+
* @param {Set<string>} [acc] - Accumulator set (internal)
|
|
170
|
+
* @returns {string[]} Unique kv:// refs (e.g. ["kv://secrets/foo"])
|
|
171
|
+
*/
|
|
172
|
+
function collectKvRefsFromPayload(obj, acc = new Set()) {
|
|
173
|
+
if (obj === null || obj === undefined) {
|
|
174
|
+
return Array.from(acc);
|
|
175
|
+
}
|
|
176
|
+
if (typeof obj === 'string') {
|
|
177
|
+
let m;
|
|
178
|
+
KV_REF_PATTERN.lastIndex = 0;
|
|
179
|
+
while ((m = KV_REF_PATTERN.exec(obj)) !== null) {
|
|
180
|
+
acc.add(`kv://${m[1]}`);
|
|
181
|
+
}
|
|
182
|
+
return Array.from(acc);
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(obj)) {
|
|
185
|
+
for (const item of obj) {
|
|
186
|
+
collectKvRefsFromPayload(item, acc);
|
|
187
|
+
}
|
|
188
|
+
return Array.from(acc);
|
|
189
|
+
}
|
|
190
|
+
if (typeof obj === 'object') {
|
|
191
|
+
for (const v of Object.values(obj)) {
|
|
192
|
+
collectKvRefsFromPayload(v, acc);
|
|
193
|
+
}
|
|
194
|
+
return Array.from(acc);
|
|
195
|
+
}
|
|
196
|
+
return Array.from(acc);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validates that key is a well-formed kv path (kv:// with alphanumeric/hyphen/slash segments).
|
|
201
|
+
*
|
|
202
|
+
* @param {string} key - kv:// path
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
function isValidKvPath(key) {
|
|
206
|
+
return typeof key === 'string' && /^kv:\/\/[a-z0-9][a-z0-9\-/]*$/i.test(key.trim());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Builds secret items from .env file (KV_* vars, values resolved).
|
|
211
|
+
* @param {string} envFilePath - Path to .env
|
|
212
|
+
* @param {Object} secrets - Loaded secrets
|
|
213
|
+
* @param {Map<string, string>} itemsByKey - Mutable map to add items to
|
|
214
|
+
*/
|
|
215
|
+
function buildItemsFromEnv(envFilePath, secrets, itemsByKey) {
|
|
216
|
+
if (!envFilePath || typeof envFilePath !== 'string' || !fs.existsSync(envFilePath)) return;
|
|
217
|
+
try {
|
|
218
|
+
const content = fs.readFileSync(envFilePath, 'utf8');
|
|
219
|
+
const envMap = parseEnvToMap(content);
|
|
220
|
+
const fromEnv = collectKvEnvVarsAsSecretItems(envMap);
|
|
221
|
+
for (const { key, value } of fromEnv) {
|
|
222
|
+
const resolved = resolveKvValue(secrets, value);
|
|
223
|
+
if (resolved !== null && resolved !== undefined && isValidKvPath(key)) itemsByKey.set(key, resolved);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Best-effort: continue without .env items
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Builds secret items from payload kv:// refs not already in itemsByKey.
|
|
232
|
+
* @param {Object} payload - Upload payload
|
|
233
|
+
* @param {Object} secrets - Loaded secrets
|
|
234
|
+
* @param {Map<string, string>} itemsByKey - Mutable map to add items to
|
|
235
|
+
*/
|
|
236
|
+
function buildItemsFromPayload(payload, secrets, itemsByKey) {
|
|
237
|
+
if (!payload || typeof payload !== 'object') return;
|
|
238
|
+
const refs = collectKvRefsFromPayload(payload);
|
|
239
|
+
const existingKeys = new Set(itemsByKey.keys());
|
|
240
|
+
for (const ref of refs) {
|
|
241
|
+
if (existingKeys.has(ref)) continue;
|
|
242
|
+
const pathKey = ref.replace(/^kv:\/\//, '');
|
|
243
|
+
const resolved = secrets[pathKey];
|
|
244
|
+
if (resolved !== null && resolved !== undefined && isValidKvPath(ref)) {
|
|
245
|
+
itemsByKey.set(ref, typeof resolved === 'string' ? resolved : String(resolved));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Derives stored count from API response.
|
|
252
|
+
* @param {Object} res - Response from storeCredentialSecrets
|
|
253
|
+
* @param {number} fallback - Fallback when stored not in response
|
|
254
|
+
* @returns {number}
|
|
255
|
+
*/
|
|
256
|
+
function storedCountFromResponse(res, fallback) {
|
|
257
|
+
if (res && typeof res.stored === 'number') return res.stored;
|
|
258
|
+
if (res && res.data && res.data.stored !== undefined && res.data.stored !== null) return res.data.stored;
|
|
259
|
+
return fallback;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Sends items to dataplane credential API; returns pushed count or warning.
|
|
264
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
265
|
+
* @param {Object} authConfig - Auth config
|
|
266
|
+
* @param {Array<{ key: string, value: string }>} items - Items to send
|
|
267
|
+
* @returns {Promise<{ pushed: number, warning?: string }>}
|
|
268
|
+
*/
|
|
269
|
+
async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
|
|
270
|
+
try {
|
|
271
|
+
const res = await storeCredentialSecrets(dataplaneUrl, authConfig, items);
|
|
272
|
+
const failed = res && (res.success === false || res.data?.success === false);
|
|
273
|
+
if (failed) {
|
|
274
|
+
const status = res.status ?? res.statusCode;
|
|
275
|
+
if (status === 403 || status === 401) {
|
|
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.' };
|
|
277
|
+
}
|
|
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 };
|
|
280
|
+
}
|
|
281
|
+
return { pushed: storedCountFromResponse(res, items.length) };
|
|
282
|
+
} catch (err) {
|
|
283
|
+
return { pushed: 0, warning: err.message || 'Failed to push credential secrets to dataplane.' };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Pushes credential secrets to dataplane: from .env (KV_*) and from payload kv:// refs.
|
|
289
|
+
* Resolves all kv:// in values via loadSecrets; sends only plain values. Best-effort:
|
|
290
|
+
* on 403/401 logs warning and returns; never logs secret values.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} dataplaneUrl - Dataplane base URL
|
|
293
|
+
* @param {Object} authConfig - Auth config (Bearer)
|
|
294
|
+
* @param {Object} options - Options
|
|
295
|
+
* @param {string} [options.envFilePath] - Path to .env (integration/<systemKey>/.env)
|
|
296
|
+
* @param {string} [options.appName] - App/system name for loadSecrets context
|
|
297
|
+
* @param {Object} [options.payload] - Upload payload { application, dataSources } for kv scan
|
|
298
|
+
* @returns {Promise<{ pushed: number, keys?: string[], skipped?: boolean, warning?: string }>} Count pushed, keys (on success), skipped (when nothing to push), optional warning
|
|
299
|
+
*/
|
|
300
|
+
async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
|
|
301
|
+
const { envFilePath, appName, payload } = options;
|
|
302
|
+
let secrets;
|
|
303
|
+
try {
|
|
304
|
+
secrets = await loadSecrets(undefined, appName);
|
|
305
|
+
} catch {
|
|
306
|
+
secrets = {};
|
|
307
|
+
}
|
|
308
|
+
const itemsByKey = new Map();
|
|
309
|
+
buildItemsFromEnv(envFilePath, secrets, itemsByKey);
|
|
310
|
+
buildItemsFromPayload(payload, secrets, itemsByKey);
|
|
311
|
+
|
|
312
|
+
const items = Array.from(itemsByKey.entries())
|
|
313
|
+
.filter(([k]) => isValidKvPath(k))
|
|
314
|
+
.map(([key, value]) => ({ key, value }));
|
|
315
|
+
|
|
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;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Parses .env-style content into key-value map (first = separates key and value).
|
|
326
|
+
*
|
|
327
|
+
* @param {string} content - Raw .env content
|
|
328
|
+
* @returns {Object.<string, string>}
|
|
329
|
+
*/
|
|
330
|
+
function parseEnvToMap(content) {
|
|
331
|
+
if (!content || typeof content !== 'string') return {};
|
|
332
|
+
const map = {};
|
|
333
|
+
const lines = content.split(/\r?\n/);
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
const trimmed = line.trim();
|
|
336
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
337
|
+
const eq = trimmed.indexOf('=');
|
|
338
|
+
if (eq > 0) {
|
|
339
|
+
const key = trimmed.substring(0, eq).trim();
|
|
340
|
+
const value = trimmed.substring(eq + 1);
|
|
341
|
+
map[key] = value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return map;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
collectKvEnvVarsAsSecretItems,
|
|
349
|
+
collectKvRefsFromPayload,
|
|
350
|
+
pushCredentialSecrets,
|
|
351
|
+
kvEnvKeyToPath,
|
|
352
|
+
systemKeyToKvPrefix,
|
|
353
|
+
securityKeyToVar,
|
|
354
|
+
isValidKvPath,
|
|
355
|
+
resolveKvValue,
|
|
356
|
+
parseEnvToMap
|
|
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
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Helper for generating CSR and saving dev certificates (Builder Server onboarding)
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a key pair and CSR for developer certificate. Uses OpenSSL when available.
|
|
14
|
+
* CN in CSR is set to dev-<developerId> per Builder Server convention.
|
|
15
|
+
* @param {string} developerId - Developer ID (e.g. "01")
|
|
16
|
+
* @returns {{ csrPem: string, keyPem: string }} PEM-encoded CSR and private key
|
|
17
|
+
* @throws {Error} If OpenSSL is not available or generation fails
|
|
18
|
+
*/
|
|
19
|
+
function generateCSR(developerId) {
|
|
20
|
+
if (!developerId || typeof developerId !== 'string') {
|
|
21
|
+
throw new Error('developerId is required and must be a string');
|
|
22
|
+
}
|
|
23
|
+
const cn = `dev-${developerId}`;
|
|
24
|
+
const tmpDir = path.join(os.tmpdir(), `aifabrix-csr-${Date.now()}`);
|
|
25
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
26
|
+
const keyPath = path.join(tmpDir, 'key.pem');
|
|
27
|
+
const csrPath = path.join(tmpDir, 'csr.pem');
|
|
28
|
+
try {
|
|
29
|
+
execSync(
|
|
30
|
+
`openssl req -new -newkey rsa:2048 -keyout "${keyPath}" -nodes -subj "/CN=${cn}" -out "${csrPath}"`,
|
|
31
|
+
{ stdio: 'pipe', encoding: 'utf8' }
|
|
32
|
+
);
|
|
33
|
+
const keyPem = fs.readFileSync(keyPath, 'utf8');
|
|
34
|
+
const csrPem = fs.readFileSync(csrPath, 'utf8');
|
|
35
|
+
return { csrPem, keyPem };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err.message && (err.message.includes('openssl') || err.message.includes('ENOENT'))) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'OpenSSL is required for certificate generation. Install OpenSSL and ensure it is on PATH, or use a system that provides it (e.g. Git for Windows).'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`CSR generation failed: ${err.message}`);
|
|
43
|
+
} finally {
|
|
44
|
+
try {
|
|
45
|
+
fs.unlinkSync(keyPath);
|
|
46
|
+
fs.unlinkSync(csrPath);
|
|
47
|
+
fs.rmdirSync(tmpDir);
|
|
48
|
+
} catch {
|
|
49
|
+
// ignore cleanup errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return path to the directory where dev certificates are stored for a developer.
|
|
56
|
+
* @param {string} configDir - Config directory (e.g. from getConfigDirForPaths())
|
|
57
|
+
* @param {string} developerId - Developer ID
|
|
58
|
+
* @returns {string} Absolute path to certs/<developerId>/
|
|
59
|
+
*/
|
|
60
|
+
function getCertDir(configDir, developerId) {
|
|
61
|
+
return path.join(configDir, 'certs', developerId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read client certificate PEM from cert dir (cert.pem).
|
|
66
|
+
* @param {string} certDir - Directory containing cert.pem
|
|
67
|
+
* @returns {string|null} PEM content or null if not found
|
|
68
|
+
*/
|
|
69
|
+
function readClientCertPem(certDir) {
|
|
70
|
+
const certPath = path.join(certDir, 'cert.pem');
|
|
71
|
+
try {
|
|
72
|
+
return fs.readFileSync(certPath, 'utf8');
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e.code === 'ENOENT') return null;
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read client private key PEM from cert dir (key.pem). Used for mTLS (e.g. getSettings).
|
|
81
|
+
* @param {string} certDir - Directory containing key.pem
|
|
82
|
+
* @returns {string|null} PEM content or null if not found
|
|
83
|
+
*/
|
|
84
|
+
function readClientKeyPem(certDir) {
|
|
85
|
+
const keyPath = path.join(certDir, 'key.pem');
|
|
86
|
+
try {
|
|
87
|
+
return fs.readFileSync(keyPath, 'utf8');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (e.code === 'ENOENT') return null;
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get certificate validity end (notAfter) from cert.pem in certDir using OpenSSL.
|
|
96
|
+
* @param {string} certDir - Directory containing cert.pem
|
|
97
|
+
* @returns {Date|null} Expiry date or null if cert missing/invalid or OpenSSL fails
|
|
98
|
+
*/
|
|
99
|
+
function getCertValidNotAfter(certDir) {
|
|
100
|
+
const certPath = path.join(certDir, 'cert.pem');
|
|
101
|
+
try {
|
|
102
|
+
if (!fs.existsSync(certPath)) return null;
|
|
103
|
+
const out = execSync(
|
|
104
|
+
`openssl x509 -enddate -noout -in "${certPath}"`,
|
|
105
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
106
|
+
);
|
|
107
|
+
const match = out.match(/notAfter=(.+)/);
|
|
108
|
+
if (!match) return null;
|
|
109
|
+
const date = new Date(match[1].trim());
|
|
110
|
+
return isNaN(date.getTime()) ? null : date;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
generateCSR,
|
|
118
|
+
getCertDir,
|
|
119
|
+
readClientCertPem,
|
|
120
|
+
readClientKeyPem,
|
|
121
|
+
getCertValidNotAfter
|
|
122
|
+
};
|