@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
|
@@ -5,41 +5,18 @@
|
|
|
5
5
|
"type": "openapi",
|
|
6
6
|
"enabled": true,
|
|
7
7
|
"authentication": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
"tokenUrl": "
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"crm.objects.contacts.read",
|
|
18
|
-
"crm.objects.contacts.write",
|
|
19
|
-
"crm.objects.deals.read",
|
|
20
|
-
"crm.objects.deals.write"
|
|
21
|
-
]
|
|
8
|
+
"method": "oauth2",
|
|
9
|
+
"variables": {
|
|
10
|
+
"baseUrl": "https://api.hubapi.com",
|
|
11
|
+
"tokenUrl": "https://api.hubapi.com/oauth/v1/token",
|
|
12
|
+
"scope": "crm.objects.companies.read crm.objects.companies.write crm.objects.contacts.read crm.objects.contacts.write crm.objects.deals.read crm.objects.deals.write"
|
|
13
|
+
},
|
|
14
|
+
"security": {
|
|
15
|
+
"clientId": "kv://hubspot/clientid",
|
|
16
|
+
"clientSecret": "kv://hubspot/clientsecret"
|
|
22
17
|
}
|
|
23
18
|
},
|
|
24
19
|
"configuration": [
|
|
25
|
-
{
|
|
26
|
-
"name": "CLIENTID",
|
|
27
|
-
"value": "hubspot-clientidKeyVault",
|
|
28
|
-
"location": "keyvault",
|
|
29
|
-
"required": true
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"name": "CLIENTSECRET",
|
|
33
|
-
"value": "hubspot-clientsecretKeyVault",
|
|
34
|
-
"location": "keyvault",
|
|
35
|
-
"required": true
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"name": "TOKENURL",
|
|
39
|
-
"value": "https://api.hubapi.com/oauth/v1/token",
|
|
40
|
-
"location": "variable",
|
|
41
|
-
"required": true
|
|
42
|
-
},
|
|
43
20
|
{
|
|
44
21
|
"name": "HUBSPOT_API_VERSION",
|
|
45
22
|
"value": "v3",
|
|
@@ -86,5 +63,11 @@
|
|
|
86
63
|
"sales",
|
|
87
64
|
"marketing",
|
|
88
65
|
"hubspot"
|
|
66
|
+
],
|
|
67
|
+
"dataSources": [
|
|
68
|
+
"hubspot-company",
|
|
69
|
+
"hubspot-contact",
|
|
70
|
+
"hubspot-deal",
|
|
71
|
+
"hubspot-users-datasource"
|
|
89
72
|
]
|
|
90
73
|
}
|
|
@@ -194,27 +194,27 @@ async function createTestDatasource(datasourcePath) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
|
-
* Builds datasource
|
|
198
|
-
* @function
|
|
197
|
+
* Builds datasource upload command arguments
|
|
198
|
+
* @function buildDatasourceUploadArgs
|
|
199
199
|
* @param {string} datasourcePath - Path to datasource file
|
|
200
200
|
* @returns {string[]} Command arguments
|
|
201
201
|
*/
|
|
202
|
-
function
|
|
202
|
+
function buildDatasourceUploadArgs(datasourcePath) {
|
|
203
203
|
return [
|
|
204
204
|
'bin/aifabrix.js',
|
|
205
205
|
'datasource',
|
|
206
|
-
'
|
|
206
|
+
'upload',
|
|
207
207
|
'test-app',
|
|
208
208
|
datasourcePath
|
|
209
209
|
];
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
* Gets expected error patterns for datasource
|
|
214
|
-
* @function
|
|
213
|
+
* Gets expected error patterns for datasource upload
|
|
214
|
+
* @function getDatasourceUploadErrorPatterns
|
|
215
215
|
* @returns {string[]} Expected error patterns
|
|
216
216
|
*/
|
|
217
|
-
function
|
|
217
|
+
function getDatasourceUploadErrorPatterns() {
|
|
218
218
|
return [
|
|
219
219
|
'failed to connect',
|
|
220
220
|
'connection refused',
|
|
@@ -224,24 +224,25 @@ function getDatasourceDeployErrorPatterns() {
|
|
|
224
224
|
'timeout',
|
|
225
225
|
'unreachable',
|
|
226
226
|
'failed to publish',
|
|
227
|
+
'upload failed',
|
|
227
228
|
'deployment failed'
|
|
228
229
|
];
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
/**
|
|
232
|
-
* Test datasource
|
|
233
|
+
* Test datasource upload command with invalid dataplane
|
|
233
234
|
* @async
|
|
234
|
-
* @function
|
|
235
|
+
* @function testDatasourceUpload
|
|
235
236
|
* @returns {Promise<Object>} Test result
|
|
236
237
|
*/
|
|
237
|
-
async function
|
|
238
|
-
logInfo('\n🚀 Testing: datasource
|
|
238
|
+
async function testDatasourceUpload() {
|
|
239
|
+
logInfo('\n🚀 Testing: datasource upload command');
|
|
239
240
|
|
|
240
241
|
const datasourcePath = path.join(process.cwd(), 'integration', 'test-datasource.json');
|
|
241
242
|
|
|
242
243
|
try {
|
|
243
244
|
await createTestDatasource(datasourcePath);
|
|
244
|
-
const args =
|
|
245
|
+
const args = buildDatasourceUploadArgs(datasourcePath);
|
|
245
246
|
const result = await runCommand('node', args);
|
|
246
247
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
247
248
|
|
|
@@ -252,18 +253,18 @@ async function testDatasourceDeploy() {
|
|
|
252
253
|
// Ignore cleanup errors
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
const expectedPatterns =
|
|
256
|
+
const expectedPatterns = getDatasourceUploadErrorPatterns();
|
|
256
257
|
const isValid = !result.success && validateError(output, expectedPatterns);
|
|
257
258
|
|
|
258
259
|
return {
|
|
259
|
-
name: 'datasource
|
|
260
|
+
name: 'datasource upload',
|
|
260
261
|
success: isValid,
|
|
261
262
|
output,
|
|
262
263
|
expectedPatterns
|
|
263
264
|
};
|
|
264
265
|
} catch (error) {
|
|
265
266
|
return {
|
|
266
|
-
name: 'datasource
|
|
267
|
+
name: 'datasource upload',
|
|
267
268
|
success: false,
|
|
268
269
|
output: error.message,
|
|
269
270
|
error: error.message
|
|
@@ -385,7 +386,7 @@ module.exports = {
|
|
|
385
386
|
testWizard,
|
|
386
387
|
testDownload,
|
|
387
388
|
testDelete,
|
|
388
|
-
|
|
389
|
+
testDatasourceUpload,
|
|
389
390
|
testIntegration,
|
|
390
391
|
testDataplaneDiscovery
|
|
391
392
|
};
|
|
@@ -24,7 +24,7 @@ const {
|
|
|
24
24
|
testWizard,
|
|
25
25
|
testDownload,
|
|
26
26
|
testDelete,
|
|
27
|
-
|
|
27
|
+
testDatasourceUpload,
|
|
28
28
|
testIntegration,
|
|
29
29
|
testDataplaneDiscovery
|
|
30
30
|
} = require('./test-dataplane-down-tests');
|
|
@@ -139,7 +139,7 @@ async function runTests() {
|
|
|
139
139
|
testWizard,
|
|
140
140
|
testDownload,
|
|
141
141
|
testDelete,
|
|
142
|
-
|
|
142
|
+
testDatasourceUpload,
|
|
143
143
|
testIntegration,
|
|
144
144
|
testDataplaneDiscovery
|
|
145
145
|
];
|
|
@@ -281,7 +281,7 @@ async function loadEnvFile(envPath, options) {
|
|
|
281
281
|
/**
|
|
282
282
|
* Load test config (controller, environment, dataplane, openapi file).
|
|
283
283
|
* Reads integration/hubspot/.env; missing CONTROLLER_URL/ENVIRONMENT fall back to
|
|
284
|
-
* the same resolution as the CLI (
|
|
284
|
+
* the same resolution as the CLI (af auth status) so tests use the same controller.
|
|
285
285
|
* @async
|
|
286
286
|
* @function loadTestConfigFromEnv
|
|
287
287
|
* @returns {Promise<Object>} Context with controllerUrl, environment, dataplaneUrl, openapiFile
|
package/jest.config.manual.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
const baseProject = require('./jest.config').projects[0];
|
|
11
11
|
|
|
12
12
|
module.exports = {
|
|
13
|
+
// Top-level so Jest actually applies it (project-level testTimeout is ignored in some Jest versions)
|
|
14
|
+
testTimeout: 60000,
|
|
13
15
|
projects: [
|
|
14
16
|
{
|
|
15
17
|
...baseProject,
|
|
@@ -22,7 +24,6 @@ module.exports = {
|
|
|
22
24
|
'\\\\node_modules\\\\'
|
|
23
25
|
],
|
|
24
26
|
setupFilesAfterEnv: ['<rootDir>/tests/manual/setup.js'],
|
|
25
|
-
testTimeout: 60000,
|
|
26
27
|
maxWorkers: 1
|
|
27
28
|
}
|
|
28
29
|
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Credential API functions (Dataplane secret store)
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { ApiClient } = require('./index');
|
|
8
|
+
|
|
9
|
+
const CREDENTIAL_SECRET_ENDPOINT = '/api/v1/credential/secret';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Store credential secrets in the dataplane secret store.
|
|
13
|
+
* Values are encrypted at rest by the dataplane; send plain values only (no kv:// as value).
|
|
14
|
+
*
|
|
15
|
+
* POST /api/v1/credential/secret
|
|
16
|
+
* @requiresPermission {Dataplane} credential:create
|
|
17
|
+
* @async
|
|
18
|
+
* @function storeCredentialSecrets
|
|
19
|
+
* @param {string} dataplaneUrl - Dataplane base URL
|
|
20
|
+
* @param {Object} authConfig - Authentication configuration (Bearer token required)
|
|
21
|
+
* @param {Array<{ key: string, value: string }>} items - Secret items (key = kv path, value = plain)
|
|
22
|
+
* @returns {Promise<{ stored?: number, success?: boolean, error?: string }>} Secret store response
|
|
23
|
+
* @throws {Error} If request fails (non-2xx) and caller may handle 403/401 as warning
|
|
24
|
+
*/
|
|
25
|
+
async function storeCredentialSecrets(dataplaneUrl, authConfig, items) {
|
|
26
|
+
if (!dataplaneUrl || typeof dataplaneUrl !== 'string') {
|
|
27
|
+
throw new Error('dataplaneUrl is required and must be a string');
|
|
28
|
+
}
|
|
29
|
+
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
30
|
+
return { stored: 0 };
|
|
31
|
+
}
|
|
32
|
+
const client = new ApiClient(dataplaneUrl, authConfig);
|
|
33
|
+
return await client.post(CREDENTIAL_SECRET_ENDPOINT, {
|
|
34
|
+
body: items
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
storeCredentialSecrets
|
|
40
|
+
};
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Builder Server (dev) API – issue-cert, settings, users, SSH keys, secrets.
|
|
3
|
+
* First call: issue-cert is public (no client cert). All other routes require client certificate:
|
|
4
|
+
* when clientKeyPem is provided, requests use mTLS (TLS client cert); otherwise X-Client-Cert header only.
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const https = require('https');
|
|
10
|
+
const { makeApiCall } = require('../utils/api');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Encode PEM for use in X-Client-Cert header. HTTP header values must not contain newlines;
|
|
16
|
+
* we send certPem as base64: Buffer.from(pem, 'utf8').toString('base64').
|
|
17
|
+
* Server should decode with Buffer.from(headerVal, 'base64').toString('utf8').
|
|
18
|
+
* @param {string} clientCertPem - PEM-encoded client certificate
|
|
19
|
+
* @returns {string} Base64-encoded PEM for header
|
|
20
|
+
*/
|
|
21
|
+
function encodeCertForHeader(clientCertPem) {
|
|
22
|
+
if (!clientCertPem || typeof clientCertPem !== 'string') return '';
|
|
23
|
+
return Buffer.from(clientCertPem, 'utf8').toString('base64');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize base URL (no trailing slash)
|
|
28
|
+
* @param {string} serverUrl - Base URL of Builder Server
|
|
29
|
+
* @returns {string} Normalized URL
|
|
30
|
+
*/
|
|
31
|
+
function normalizeBaseUrl(serverUrl) {
|
|
32
|
+
if (!serverUrl || typeof serverUrl !== 'string') {
|
|
33
|
+
throw new Error('remote-server URL is required and must be a string');
|
|
34
|
+
}
|
|
35
|
+
return serverUrl.trim().replace(/\/+$/, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build full URL for an endpoint path
|
|
40
|
+
* @param {string} baseUrl - Normalized base URL
|
|
41
|
+
* @param {string} path - Path (e.g. /api/dev/issue-cert)
|
|
42
|
+
* @returns {string} Full URL
|
|
43
|
+
*/
|
|
44
|
+
function buildUrl(baseUrl, path) {
|
|
45
|
+
const p = path.startsWith('/') ? path : `/${path}`;
|
|
46
|
+
return `${baseUrl}${p}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Make request to Builder Server. Throws on !success with message from response or error.
|
|
51
|
+
* @param {string} url - Full URL
|
|
52
|
+
* @param {Object} options - Fetch options (method, headers, body)
|
|
53
|
+
* @returns {Promise<Object>} result.data when success
|
|
54
|
+
*/
|
|
55
|
+
async function request(url, options = {}) {
|
|
56
|
+
const fetchOptions = {
|
|
57
|
+
method: options.method || 'GET',
|
|
58
|
+
headers: { 'Content-Type': 'application/json', ...options.headers },
|
|
59
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
60
|
+
};
|
|
61
|
+
if (options.body !== undefined) {
|
|
62
|
+
fetchOptions.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
|
|
63
|
+
}
|
|
64
|
+
const result = await makeApiCall(url, fetchOptions);
|
|
65
|
+
if (!result.success) {
|
|
66
|
+
const msg = result.formattedError || result.error || result.message || `Request failed (${result.status})`;
|
|
67
|
+
const err = new Error(msg);
|
|
68
|
+
err.status = result.status;
|
|
69
|
+
err.errorData = result.errorData;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
return result.data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build request options and TLS agent for mTLS request.
|
|
77
|
+
* @param {string} url - Full URL
|
|
78
|
+
* @param {Object} options - { method, headers, body }
|
|
79
|
+
* @param {string} certPem - PEM client certificate
|
|
80
|
+
* @param {string} keyPem - PEM client key
|
|
81
|
+
* @returns {{ urlObj: URL, method: string, headers: Object, body: string|undefined, agent: https.Agent }}
|
|
82
|
+
*/
|
|
83
|
+
function buildMtlsRequestOptions(url, options, certPem, keyPem) {
|
|
84
|
+
const urlObj = new URL(url);
|
|
85
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
86
|
+
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
|
87
|
+
let body = options.body;
|
|
88
|
+
if (body !== undefined && typeof body !== 'string') {
|
|
89
|
+
body = JSON.stringify(body);
|
|
90
|
+
}
|
|
91
|
+
if (body) {
|
|
92
|
+
headers['Content-Length'] = Buffer.byteLength(body, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
const tlsOptions = { cert: certPem, key: keyPem, rejectUnauthorized: true };
|
|
95
|
+
const agent = new https.Agent(tlsOptions);
|
|
96
|
+
return { urlObj, method, headers, body, agent, tlsOptions };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Handle mTLS response: collect body, parse JSON, resolve or reject.
|
|
101
|
+
* @param {import('http').IncomingMessage} res - HTTP response
|
|
102
|
+
* @param {Function} resolve - Promise resolve
|
|
103
|
+
* @param {Function} reject - Promise reject
|
|
104
|
+
*/
|
|
105
|
+
function handleMtlsResponse(res, resolve, reject) {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
res.on('data', (c) => chunks.push(c));
|
|
108
|
+
res.on('end', () => {
|
|
109
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
110
|
+
let data;
|
|
111
|
+
try {
|
|
112
|
+
data = raw ? JSON.parse(raw) : {};
|
|
113
|
+
} catch {
|
|
114
|
+
data = raw;
|
|
115
|
+
}
|
|
116
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
117
|
+
const msg = (data && (data.message || data.error)) || res.statusMessage || `Request failed (${res.statusCode})`;
|
|
118
|
+
const err = new Error(msg);
|
|
119
|
+
err.status = res.statusCode;
|
|
120
|
+
err.errorData = data;
|
|
121
|
+
reject(err);
|
|
122
|
+
} else {
|
|
123
|
+
resolve(data);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Make request with mTLS (TLS client certificate). Uses Node https module so the client cert
|
|
130
|
+
* is presented in the TLS handshake. Also sends X-Client-Cert header for backends that read it.
|
|
131
|
+
* @param {string} url - Full URL (https only)
|
|
132
|
+
* @param {Object} options - { method, headers, body }
|
|
133
|
+
* @param {string} certPem - PEM-encoded client certificate
|
|
134
|
+
* @param {string} keyPem - PEM-encoded client private key
|
|
135
|
+
* @returns {Promise<Object>} response data when success
|
|
136
|
+
*/
|
|
137
|
+
function requestWithCertImpl(url, options, certPem, keyPem) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const urlObj = new URL(url);
|
|
140
|
+
if (urlObj.protocol !== 'https:') {
|
|
141
|
+
reject(new Error('mTLS request requires https URL'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const { method, headers, body, agent, tlsOptions } = buildMtlsRequestOptions(url, options, certPem, keyPem);
|
|
145
|
+
const req = https.request(
|
|
146
|
+
{
|
|
147
|
+
hostname: urlObj.hostname,
|
|
148
|
+
port: urlObj.port || 443,
|
|
149
|
+
path: urlObj.pathname + urlObj.search,
|
|
150
|
+
method,
|
|
151
|
+
headers,
|
|
152
|
+
agent,
|
|
153
|
+
...tlsOptions
|
|
154
|
+
},
|
|
155
|
+
(res) => handleMtlsResponse(res, resolve, reject)
|
|
156
|
+
);
|
|
157
|
+
req.on('error', reject);
|
|
158
|
+
req.setTimeout(DEFAULT_TIMEOUT_MS, () => {
|
|
159
|
+
req.destroy();
|
|
160
|
+
reject(new Error(`Request timed out after ${DEFAULT_TIMEOUT_MS}ms`));
|
|
161
|
+
});
|
|
162
|
+
if (body) {
|
|
163
|
+
req.write(body, 'utf8');
|
|
164
|
+
}
|
|
165
|
+
req.end();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Issue developer certificate (public; no client cert). POST /api/dev/issue-cert
|
|
171
|
+
* @requiresPermission {BuilderServer} Public (no auth required)
|
|
172
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
173
|
+
* @param {Object} body - IssueCertDto: developerId, pin, csr
|
|
174
|
+
* @returns {Promise<Object>} IssueCertResponseDto: certificate, validDays, validNotAfter
|
|
175
|
+
*/
|
|
176
|
+
async function issueCert(serverUrl, body) {
|
|
177
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
178
|
+
return request(buildUrl(base, '/api/dev/issue-cert'), { method: 'POST', body });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get health. GET /health (public)
|
|
183
|
+
* @requiresPermission {BuilderServer} Public (no auth required)
|
|
184
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
185
|
+
* @returns {Promise<Object>} HealthResponseDto: status, checks (dataDir, encryptionKey, ca, users, tokens)
|
|
186
|
+
*/
|
|
187
|
+
async function getHealth(serverUrl) {
|
|
188
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
189
|
+
return request(buildUrl(base, '/health'));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get developer settings (cert-authenticated). GET /api/dev/settings
|
|
194
|
+
* When clientKeyPem is provided, uses mTLS (TLS client cert); otherwise X-Client-Cert header only.
|
|
195
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert or mTLS)
|
|
196
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
197
|
+
* @param {string} clientCertPem - PEM-encoded client certificate
|
|
198
|
+
* @param {string} [clientKeyPem] - PEM-encoded client private key (enables mTLS when provided)
|
|
199
|
+
* @returns {Promise<Object>} SettingsResponseDto
|
|
200
|
+
*/
|
|
201
|
+
async function getSettings(serverUrl, clientCertPem, clientKeyPem) {
|
|
202
|
+
if (!clientCertPem || typeof clientCertPem !== 'string') {
|
|
203
|
+
throw new Error('Client certificate PEM is required for getSettings');
|
|
204
|
+
}
|
|
205
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
206
|
+
const url = buildUrl(base, '/api/dev/settings');
|
|
207
|
+
const reqOptions = { method: 'GET', headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) } };
|
|
208
|
+
if (clientKeyPem && typeof clientKeyPem === 'string') {
|
|
209
|
+
return requestWithCertImpl(url, reqOptions, clientCertPem, clientKeyPem);
|
|
210
|
+
}
|
|
211
|
+
return request(url, reqOptions);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* List developers. GET /api/dev/users
|
|
216
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
217
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
218
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
219
|
+
* @returns {Promise<Object[]>} Array of UserResponseDto (empty when none)
|
|
220
|
+
*/
|
|
221
|
+
async function listUsers(serverUrl, clientCertPem) {
|
|
222
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
223
|
+
const data = await request(buildUrl(base, '/api/dev/users'), {
|
|
224
|
+
method: 'GET',
|
|
225
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
|
|
226
|
+
});
|
|
227
|
+
return Array.isArray(data) ? data : [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create developer. POST /api/dev/users
|
|
232
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
233
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
234
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
235
|
+
* @param {Object} body - CreateUserDto: developerId, name, email, optional groups
|
|
236
|
+
* @returns {Promise<Object>} UserResponseDto
|
|
237
|
+
*/
|
|
238
|
+
async function createUser(serverUrl, clientCertPem, body) {
|
|
239
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
240
|
+
return request(buildUrl(base, '/api/dev/users'), {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) },
|
|
243
|
+
body
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Update developer. PATCH /api/dev/users/:id
|
|
249
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
250
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
251
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
252
|
+
* @param {string} id - Developer ID
|
|
253
|
+
* @param {Object} body - UpdateUserDto: at least one of name, email, groups
|
|
254
|
+
* @returns {Promise<Object>} UserResponseDto
|
|
255
|
+
*/
|
|
256
|
+
async function updateUser(serverUrl, clientCertPem, id, body) {
|
|
257
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
258
|
+
return request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}`), {
|
|
259
|
+
method: 'PATCH',
|
|
260
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) },
|
|
261
|
+
body
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Delete developer. DELETE /api/dev/users/:id
|
|
267
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
268
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
269
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
270
|
+
* @param {string} id - Developer ID
|
|
271
|
+
* @returns {Promise<Object>} DeletedResponseDto
|
|
272
|
+
*/
|
|
273
|
+
async function deleteUser(serverUrl, clientCertPem, id) {
|
|
274
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
275
|
+
return request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}`), {
|
|
276
|
+
method: 'DELETE',
|
|
277
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Create or regenerate one-time PIN. POST /api/dev/users/:id/pin
|
|
283
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
284
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
285
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
286
|
+
* @param {string} id - Developer ID
|
|
287
|
+
* @returns {Promise<Object>} CreatePinResponseDto: pin, expiresAt
|
|
288
|
+
*/
|
|
289
|
+
async function createPin(serverUrl, clientCertPem, id) {
|
|
290
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
291
|
+
return request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}/pin`), {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* List SSH keys for developer. GET /api/dev/users/:id/ssh-keys
|
|
299
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
300
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
301
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
302
|
+
* @param {string} id - Developer ID
|
|
303
|
+
* @returns {Promise<Object[]>} Array of SshKeyItemDto
|
|
304
|
+
*/
|
|
305
|
+
async function listSshKeys(serverUrl, clientCertPem, id) {
|
|
306
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
307
|
+
const data = await request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}/ssh-keys`), {
|
|
308
|
+
method: 'GET',
|
|
309
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
|
|
310
|
+
});
|
|
311
|
+
return Array.isArray(data) ? data : [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Add SSH public key for developer. POST /api/dev/users/:id/ssh-keys
|
|
316
|
+
* When clientKeyPem is provided, uses mTLS (TLS client cert); otherwise X-Client-Cert header only.
|
|
317
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert or mTLS)
|
|
318
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
319
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
320
|
+
* @param {string} id - Developer ID
|
|
321
|
+
* @param {Object} body - AddSshKeyDto: publicKey, optional label
|
|
322
|
+
* @param {string} [clientKeyPem] - PEM-encoded client private key (enables mTLS when provided)
|
|
323
|
+
* @returns {Promise<Object>} SshKeyItemDto
|
|
324
|
+
*/
|
|
325
|
+
async function addSshKey(serverUrl, clientCertPem, id, body, clientKeyPem) {
|
|
326
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
327
|
+
const url = buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}/ssh-keys`);
|
|
328
|
+
const reqOptions = {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) },
|
|
331
|
+
body
|
|
332
|
+
};
|
|
333
|
+
if (clientKeyPem && typeof clientKeyPem === 'string') {
|
|
334
|
+
return requestWithCertImpl(url, reqOptions, clientCertPem, clientKeyPem);
|
|
335
|
+
}
|
|
336
|
+
return request(url, reqOptions);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Remove SSH key by fingerprint. DELETE /api/dev/users/:id/ssh-keys/:fingerprint
|
|
341
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
342
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
343
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
344
|
+
* @param {string} id - Developer ID
|
|
345
|
+
* @param {string} fingerprint - Key fingerprint
|
|
346
|
+
* @returns {Promise<Object>} DeletedResponseDto
|
|
347
|
+
*/
|
|
348
|
+
async function removeSshKey(serverUrl, clientCertPem, id, fingerprint) {
|
|
349
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
350
|
+
return request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}/ssh-keys/${encodeURIComponent(fingerprint)}`), {
|
|
351
|
+
method: 'DELETE',
|
|
352
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* List secrets. GET /api/dev/secrets
|
|
358
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
359
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
360
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
361
|
+
* @returns {Promise<Object[]>} Array of SecretItemDto: name, value
|
|
362
|
+
*/
|
|
363
|
+
async function listSecrets(serverUrl, clientCertPem) {
|
|
364
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
365
|
+
const data = await request(buildUrl(base, '/api/dev/secrets'), {
|
|
366
|
+
method: 'GET',
|
|
367
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
|
|
368
|
+
});
|
|
369
|
+
return Array.isArray(data) ? data : [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Add or update secret. POST /api/dev/secrets
|
|
374
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
375
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
376
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
377
|
+
* @param {Object} body - AddSecretDto: key, value
|
|
378
|
+
* @returns {Promise<Object>} AddSecretResponseDto
|
|
379
|
+
*/
|
|
380
|
+
async function addSecret(serverUrl, clientCertPem, body) {
|
|
381
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
382
|
+
return request(buildUrl(base, '/api/dev/secrets'), {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) },
|
|
385
|
+
body
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Delete secret by key. DELETE /api/dev/secrets/:key
|
|
391
|
+
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
392
|
+
* @param {string} serverUrl - Builder Server base URL
|
|
393
|
+
* @param {string} clientCertPem - PEM client certificate
|
|
394
|
+
* @param {string} key - Secret key
|
|
395
|
+
* @returns {Promise<Object>} DeleteSecretResponseDto
|
|
396
|
+
*/
|
|
397
|
+
async function deleteSecret(serverUrl, clientCertPem, key) {
|
|
398
|
+
const base = normalizeBaseUrl(serverUrl);
|
|
399
|
+
return request(buildUrl(base, `/api/dev/secrets/${encodeURIComponent(key)}`), {
|
|
400
|
+
method: 'DELETE',
|
|
401
|
+
headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = {
|
|
406
|
+
issueCert,
|
|
407
|
+
getHealth,
|
|
408
|
+
getSettings,
|
|
409
|
+
listUsers,
|
|
410
|
+
createUser,
|
|
411
|
+
updateUser,
|
|
412
|
+
deleteUser,
|
|
413
|
+
createPin,
|
|
414
|
+
listSshKeys,
|
|
415
|
+
addSshKey,
|
|
416
|
+
removeSshKey,
|
|
417
|
+
listSecrets,
|
|
418
|
+
addSecret,
|
|
419
|
+
deleteSecret,
|
|
420
|
+
normalizeBaseUrl,
|
|
421
|
+
buildUrl,
|
|
422
|
+
encodeCertForHeader
|
|
423
|
+
};
|