@aifabrix/builder 2.60.0 → 2.62.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/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/api/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-env-compose.js +5 -6
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/run.js +6 -4
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -2
- package/lib/channels/channel-artifact-generator.js +13 -3
- package/lib/cli/infra-guided-footers.js +10 -8
- package/lib/cli/infra-guided.js +30 -10
- package/lib/cli/setup-app.js +5 -0
- package/lib/cli/setup-environment.js +156 -0
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +2 -0
- package/lib/commands/auth-config.js +2 -0
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +5 -1
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +8 -9
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/setup.js +3 -0
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +70 -14
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +37 -18
- package/lib/commands/up-integration-server.js +112 -0
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +12 -5
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/env-platform-expand.js +5 -1
- package/lib/core/environment-access-policy.js +121 -0
- package/lib/core/environment-mode-policy.js +177 -0
- package/lib/core/local-env-overrides.js +149 -0
- package/lib/core/secrets-ensure.js +4 -2
- package/lib/core/secrets-env-content.js +17 -10
- package/lib/core/secrets-env-write.js +11 -4
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/capability-manifest.js +1 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- package/lib/deployment/installation/index.js +14 -1
- package/lib/deployment/installation/infra-catalog.js +3 -0
- package/lib/deployment/installation/local-environment-stage.js +285 -0
- package/lib/deployment/installation/local-installation.js +76 -0
- package/lib/deployment/installation/stage-input.js +5 -1
- package/lib/generator/builders.js +3 -1
- package/lib/generator/index.js +3 -0
- package/lib/generator/split-variables.js +2 -2
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/parameters/physical-database-naming.js +136 -0
- package/lib/parameters/physical-secret-name.js +194 -0
- package/lib/parameters/rsa-secret-values.js +165 -0
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- package/lib/programmatic/run-channel-install-artifacts.js +28 -2
- package/lib/programmatic/run-channel-package.js +3 -1
- package/lib/programmatic/workspace-context.js +37 -2
- package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +14 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra.parameter.yaml +440 -56
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +9 -14
- package/lib/utils/compose-generator.js +4 -7
- package/lib/utils/compose-miso-env.js +2 -2
- package/lib/utils/compose-traefik-ingress-base.js +3 -4
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/declarative-url-ports.js +3 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/redis-env-scope.js +21 -1
- package/lib/utils/registry-auth-sources.js +154 -0
- package/lib/utils/registry-credentials.js +131 -14
- package/lib/utils/secrets-generator.js +30 -21
- package/lib/utils/secrets-helpers.js +5 -8
- package/lib/utils/secrets-kv-scope.js +127 -28
- package/lib/utils/secrets-materialize-local.js +9 -0
- package/lib/utils/secrets-missing-error.js +35 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +6 -4
- package/templates/agent-kit/agent-kit.yaml +1 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +3 -12
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +2 -2
- package/templates/applications/dataplane/env.template +10 -12
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +116 -2
- package/templates/applications/miso-controller/env.template +62 -17
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +88 -10
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which environments a local installation may build in and select (plan 210.0).
|
|
3
|
+
*
|
|
4
|
+
* Two rules, kept together because they describe one position: `tst` and `pro` are
|
|
5
|
+
* **promotion targets**, not working environments.
|
|
6
|
+
*
|
|
7
|
+
* **Build is a dev activity.** `tst` and `pro` receive a promoted image — the same image run
|
|
8
|
+
* as another container under an environment-suffixed name. Building into them would mean the
|
|
9
|
+
* artifact in production was never the artifact that was tested, which is the property
|
|
10
|
+
* promotion exists to guarantee. Miso does not police this; a build leaves no trace of where
|
|
11
|
+
* it happened, so the refusal has to be here.
|
|
12
|
+
*
|
|
13
|
+
* **`tst` and `pro` need the front door.** They exist only in `full` platform mode, where
|
|
14
|
+
* Traefik routes each environment under its own path. `single-traefik` is a front door for
|
|
15
|
+
* **one** environment, and `single` is direct ports: several environments on one host collide
|
|
16
|
+
* on ports there. Allowing the selection in either would produce an environment that is
|
|
17
|
+
* unreachable rather than one that failed to start, so it is refused up front.
|
|
18
|
+
*
|
|
19
|
+
* Pure except for `resolveEffectiveBuildEnvironment`, which reads the configured environment.
|
|
20
|
+
*
|
|
21
|
+
* @fileoverview Build and platform-mode environment policy for local installations
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
SETUP_PLATFORM_MODE,
|
|
28
|
+
normalizeSetupPlatformMode
|
|
29
|
+
} = require('./setup-platform-mode');
|
|
30
|
+
|
|
31
|
+
/** Environments that receive a promoted artifact and are never built into. */
|
|
32
|
+
const PROMOTION_ONLY_ENVIRONMENTS = Object.freeze(['tst', 'pro']);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {unknown} envKey - Environment key
|
|
36
|
+
* @returns {string} Lowercase trimmed key
|
|
37
|
+
*/
|
|
38
|
+
function normalizeEnvKey(envKey) {
|
|
39
|
+
return String(envKey ?? '').trim().toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {unknown} envKey - Environment key
|
|
44
|
+
* @returns {boolean} True when the environment only ever receives a promoted artifact
|
|
45
|
+
*/
|
|
46
|
+
function isPromotionOnlyEnvironment(envKey) {
|
|
47
|
+
return PROMOTION_ONLY_ENVIRONMENTS.includes(normalizeEnvKey(envKey));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Refuse a build that would target a promotion-only environment.
|
|
52
|
+
*
|
|
53
|
+
* `--env` has no default, so an omitted flag silently takes the configured environment. The
|
|
54
|
+
* message distinguishes the two cases: being told "build cannot target pro" after typing no
|
|
55
|
+
* environment at all is not actionable on its own.
|
|
56
|
+
*
|
|
57
|
+
* @param {unknown} envKey - Resolved environment key
|
|
58
|
+
* @param {Object} [opts] - Context
|
|
59
|
+
* @param {boolean} [opts.explicit] - Whether the caller passed --env
|
|
60
|
+
* @throws {Error} When the environment is tst or pro
|
|
61
|
+
*/
|
|
62
|
+
function assertBuildEnvironmentAllowed(envKey, opts = {}) {
|
|
63
|
+
const key = normalizeEnvKey(envKey);
|
|
64
|
+
if (!isPromotionOnlyEnvironment(key)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const cause = opts.explicit
|
|
68
|
+
? `--env ${key}`
|
|
69
|
+
: `the current environment is ${key}`;
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Cannot build into ${key} (${cause}). ${key} receives a promoted image, never a locally ` +
|
|
72
|
+
'built one, so that what runs there is what was tested in dev.\n' +
|
|
73
|
+
'Build in dev with: aifabrix build <appKey> --env dev\n' +
|
|
74
|
+
`Then promote into ${key} with: aifabrix env`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Refuse selecting a promotion-only environment outside the front-door platform mode.
|
|
80
|
+
*
|
|
81
|
+
* @param {unknown} envKey - Environment key being selected
|
|
82
|
+
* @param {unknown} mode - setupPlatformMode value
|
|
83
|
+
* @throws {Error} When the environment needs full mode and the install is not in it
|
|
84
|
+
*/
|
|
85
|
+
function assertEnvironmentAllowedForPlatformMode(envKey, mode) {
|
|
86
|
+
const key = normalizeEnvKey(envKey);
|
|
87
|
+
if (!isPromotionOnlyEnvironment(key)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const normalized = normalizeSetupPlatformMode(mode);
|
|
91
|
+
if (normalized === SETUP_PLATFORM_MODE.FULL) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const reason =
|
|
95
|
+
normalized === SETUP_PLATFORM_MODE.SINGLE_TRAEFIK
|
|
96
|
+
? 'single-traefik is a front door for one environment only'
|
|
97
|
+
: 'single mode publishes direct ports, and several environments on one host collide on ports';
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Environment ${key} requires platform mode "full" (HTTPS front door with per-environment ` +
|
|
100
|
+
`paths). This installation is "${normalized}": ${reason}.\n` +
|
|
101
|
+
'Switch with: aifabrix setup --platform full'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Refuse a promotion-only environment against the installation's configured mode.
|
|
107
|
+
*
|
|
108
|
+
* @param {unknown} envKey - Environment key being selected
|
|
109
|
+
* @returns {Promise<void>}
|
|
110
|
+
* @throws {Error} When the environment needs full mode and the install is not in it
|
|
111
|
+
*/
|
|
112
|
+
async function assertEnvironmentAllowedForCurrentMode(envKey) {
|
|
113
|
+
if (!isPromotionOnlyEnvironment(envKey)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const config = require('./config');
|
|
117
|
+
const mode =
|
|
118
|
+
typeof config.getSetupPlatformMode === 'function'
|
|
119
|
+
? await config.getSetupPlatformMode()
|
|
120
|
+
: SETUP_PLATFORM_MODE.SINGLE;
|
|
121
|
+
assertEnvironmentAllowedForPlatformMode(envKey, mode);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The environment a build will actually target.
|
|
126
|
+
*
|
|
127
|
+
* Mirrors how the secret path resolves it (`buildScopedKvContext`): an omitted `--env` falls
|
|
128
|
+
* through to the configured environment, so a guard that only inspected the flag would let
|
|
129
|
+
* `aifabrix build` run against `pro` on a machine configured for it.
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} [options] - Build options carrying `env`
|
|
132
|
+
* @returns {Promise<{ envKey: string, explicit: boolean }>}
|
|
133
|
+
*/
|
|
134
|
+
async function resolveEffectiveBuildEnvironment(options = {}) {
|
|
135
|
+
if (options && options.env) {
|
|
136
|
+
return { envKey: normalizeEnvKey(options.env), explicit: true };
|
|
137
|
+
}
|
|
138
|
+
const config = require('./config');
|
|
139
|
+
if (typeof config.getCurrentEnvironment !== 'function') {
|
|
140
|
+
return { envKey: 'dev', explicit: false };
|
|
141
|
+
}
|
|
142
|
+
let current;
|
|
143
|
+
try {
|
|
144
|
+
current = await config.getCurrentEnvironment();
|
|
145
|
+
} catch {
|
|
146
|
+
// An unreadable config is a different failure, reported properly by whatever next needs
|
|
147
|
+
// it — secrets resolution cannot proceed without it either. Turning it into "cannot
|
|
148
|
+
// build" would stop every build on a machine whose config has an unrelated problem, and
|
|
149
|
+
// it protects nothing: reaching tst or pro requires either an explicit --env, which
|
|
150
|
+
// never reads config, or a readable config naming one.
|
|
151
|
+
return { envKey: 'dev', explicit: false };
|
|
152
|
+
}
|
|
153
|
+
return { envKey: normalizeEnvKey(current) || 'dev', explicit: false };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the build environment and refuse it when it is promotion-only.
|
|
158
|
+
*
|
|
159
|
+
* @param {Object} [options] - Build options carrying `env`
|
|
160
|
+
* @returns {Promise<string>} The allowed environment key
|
|
161
|
+
* @throws {Error} When the environment is tst or pro
|
|
162
|
+
*/
|
|
163
|
+
async function assertBuildEnvironmentForOptions(options = {}) {
|
|
164
|
+
const { envKey, explicit } = await resolveEffectiveBuildEnvironment(options);
|
|
165
|
+
assertBuildEnvironmentAllowed(envKey, { explicit });
|
|
166
|
+
return envKey;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
PROMOTION_ONLY_ENVIRONMENTS,
|
|
171
|
+
isPromotionOnlyEnvironment,
|
|
172
|
+
assertBuildEnvironmentAllowed,
|
|
173
|
+
assertEnvironmentAllowedForPlatformMode,
|
|
174
|
+
assertEnvironmentAllowedForCurrentMode,
|
|
175
|
+
resolveEffectiveBuildEnvironment,
|
|
176
|
+
assertBuildEnvironmentForOptions
|
|
177
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder-owned env values for locally installed platform applications.
|
|
3
|
+
*
|
|
4
|
+
* `templates/applications/{miso-controller,builder-api,keycloak}/env.template` are
|
|
5
|
+
* byte-identical mirrors of the Miso master, and `dataplane` mirrors the dataplane
|
|
6
|
+
* repository. An edit to a mirror survives until the next `chore(templates): sync`
|
|
7
|
+
* commit and then disappears without a diff anyone reads, so a value Builder decides
|
|
8
|
+
* cannot live there. It is applied here instead, after the template is resolved.
|
|
9
|
+
*
|
|
10
|
+
* @fileoverview Local-install env overrides owned by Builder, not by a mirrored template
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/** Applications the CLI registers itself through `.../applications/bootstrap`. */
|
|
16
|
+
const CLI_BOOTSTRAPPED_APP_KEYS = Object.freeze(['dataplane']);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Controller onboarding owns the miso-only platform applications. Dataplane remains a
|
|
20
|
+
* CLI-owned workload and must never be inserted into this list.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_LOCAL_ONBOARDING_APPS = Object.freeze(['builder-api']);
|
|
23
|
+
|
|
24
|
+
/** Config key an installation may set to choose its own onboarding list. */
|
|
25
|
+
const ONBOARDING_APPS_CONFIG_KEY = 'onboardingApps';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Keys Builder's mirror carried that the Miso master does not.
|
|
29
|
+
*
|
|
30
|
+
* They were edits to a mirrored file, which is the thing this module exists to prevent: the
|
|
31
|
+
* mirror has since fallen 57 lines behind its master, and re-syncing it would have dropped
|
|
32
|
+
* them silently. They are declared here instead, so the next sync cannot lose them.
|
|
33
|
+
*
|
|
34
|
+
* - `INFRASTRUCTURE_NAME` is the only one carrying a value, used by controller and onboarding
|
|
35
|
+
* flows. Losing it would change behaviour, not merely remove a blank line.
|
|
36
|
+
* - The other three shipped empty. They are kept so a local install's env has exactly the keys
|
|
37
|
+
* it had before this sync: an absent key and an empty one are usually the same thing, and
|
|
38
|
+
* "usually" is not a basis for changing every installation at once.
|
|
39
|
+
*
|
|
40
|
+
* `ACR_LOGIN_SERVER` is deliberately empty. Miso removed it with the note that its proper home
|
|
41
|
+
* is Builder's run tooling, which knows the per-developer answer — so an installation that
|
|
42
|
+
* needs it should have it injected there rather than shipped one test installation's registry
|
|
43
|
+
* to everybody, which is what happened the last time it carried a value.
|
|
44
|
+
*/
|
|
45
|
+
const MISO_CONTROLLER_LOCAL_ONLY_ENV = Object.freeze({
|
|
46
|
+
INFRASTRUCTURE_NAME: '${DEV_USERNAME}.${DEV_ROOT_DOMAIN}',
|
|
47
|
+
KEYCLOAK_PUBLIC_KEY: '',
|
|
48
|
+
OTEL_SERVICE_VERSION: '',
|
|
49
|
+
ACR_LOGIN_SERVER: ''
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function normalizeAppKeyList(value) {
|
|
53
|
+
const raw = Array.isArray(value) ? value : String(value ?? '').split(',');
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const entry of raw) {
|
|
56
|
+
const key = String(entry ?? '').trim().toLowerCase();
|
|
57
|
+
if (key) {
|
|
58
|
+
seen.add(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...seen];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Applications controller onboarding may register on this installation.
|
|
66
|
+
*
|
|
67
|
+
* Builder API is always retained because Controller onboarding owns that miso-only
|
|
68
|
+
* application. This also migrates older explicit-empty setup values safely.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
|
|
71
|
+
* @returns {string[]} Application keys, never including one the CLI bootstraps
|
|
72
|
+
*/
|
|
73
|
+
function resolveLocalOnboardingApps(cfg) {
|
|
74
|
+
const configured = cfg ? cfg[ONBOARDING_APPS_CONFIG_KEY] : undefined;
|
|
75
|
+
const list = configured === undefined || configured === null
|
|
76
|
+
? [...DEFAULT_LOCAL_ONBOARDING_APPS]
|
|
77
|
+
: normalizeAppKeyList(configured);
|
|
78
|
+
const allowed = list.filter((key) => !CLI_BOOTSTRAPPED_APP_KEYS.includes(key));
|
|
79
|
+
return ['builder-api', ...allowed.filter((key) => key !== 'builder-api')];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Env values Builder owns for a locally installed application.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} appName - Application key
|
|
86
|
+
* @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
|
|
87
|
+
* @returns {Object.<string, string>} Map to merge into resolved env content; empty when none apply
|
|
88
|
+
*/
|
|
89
|
+
function localEnvOverridesForApp(appName, cfg) {
|
|
90
|
+
const key = String(appName ?? '').trim().toLowerCase();
|
|
91
|
+
if (key === 'miso-controller') {
|
|
92
|
+
return {
|
|
93
|
+
ONBOARDING_APPS: resolveLocalOnboardingApps(cfg).join(','),
|
|
94
|
+
...MISO_CONTROLLER_LOCAL_ONLY_ENV
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (key === 'builder-api' || key === 'integration-server') {
|
|
98
|
+
return { MISO_ENVIRONMENT: 'miso' };
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Merge Builder-owned values into resolved env content for a locally installed application.
|
|
105
|
+
*
|
|
106
|
+
* Applied after the template is resolved, so the installation's choice is the authority for
|
|
107
|
+
* these keys. The merge function is passed in rather than imported, because the only caller
|
|
108
|
+
* owns it and importing it here would close a require cycle.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} content - Resolved env content
|
|
111
|
+
* @param {string} appName - Application key
|
|
112
|
+
* @param {(content: string, map: Object) => string} mergeEnvMapIntoContent - Merge helper
|
|
113
|
+
* @returns {Promise<string>} Content with overrides merged, unchanged when none apply
|
|
114
|
+
*/
|
|
115
|
+
async function applyLocalEnvOverrides(content, appName, mergeEnvMapIntoContent) {
|
|
116
|
+
let cfg = null;
|
|
117
|
+
try {
|
|
118
|
+
cfg = await require('./config').getConfig();
|
|
119
|
+
} catch {
|
|
120
|
+
// No readable config (fresh machine, or resolve outside an installation): use defaults.
|
|
121
|
+
}
|
|
122
|
+
const overrides = localEnvOverridesForApp(appName, cfg);
|
|
123
|
+
if (!overrides || Object.keys(overrides).length === 0) {
|
|
124
|
+
return content;
|
|
125
|
+
}
|
|
126
|
+
return mergeEnvMapIntoContent(content, overrides);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The onboarding list for an installation that does or does not carry the integration
|
|
131
|
+
* server. Written to config so the choice survives, rather than being asked on every run.
|
|
132
|
+
*
|
|
133
|
+
* @param {boolean} integrationServer - Whether the installation carries it
|
|
134
|
+
* @returns {string[]} Value for the `onboardingApps` config key
|
|
135
|
+
*/
|
|
136
|
+
function onboardingAppsForIntegrationServer(integrationServer) {
|
|
137
|
+
return integrationServer ? ['builder-api', 'integration-server'] : ['builder-api'];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
CLI_BOOTSTRAPPED_APP_KEYS,
|
|
142
|
+
DEFAULT_LOCAL_ONBOARDING_APPS,
|
|
143
|
+
ONBOARDING_APPS_CONFIG_KEY,
|
|
144
|
+
MISO_CONTROLLER_LOCAL_ONLY_ENV,
|
|
145
|
+
resolveLocalOnboardingApps,
|
|
146
|
+
localEnvOverridesForApp,
|
|
147
|
+
applyLocalEnvOverrides,
|
|
148
|
+
onboardingAppsForIntegrationServer
|
|
149
|
+
};
|
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
saveSecretsFile
|
|
30
30
|
} = require('../utils/secrets-generator');
|
|
31
31
|
const { encryptSecret } = require('../utils/secrets-encryption');
|
|
32
|
+
const { generateRsaSecretValues } = require('../parameters/rsa-secret-values');
|
|
32
33
|
const { loadEnvTemplate } = require('../utils/secrets-helpers');
|
|
33
34
|
const {
|
|
34
35
|
buildInfraPlaceholderContext,
|
|
@@ -287,12 +288,13 @@ async function ensureSecretsForKeys(keys, options = {}) {
|
|
|
287
288
|
if (toAdd.length === 0) return [];
|
|
288
289
|
|
|
289
290
|
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
291
|
+
const rsaValues = emptyForCredentials ? {} : generateRsaSecretValues(toAdd, { ...existing, ...suggested }, encryptionKey);
|
|
290
292
|
const added = [];
|
|
291
293
|
|
|
292
294
|
return addSecretsToFile({
|
|
293
295
|
filePath: target.filePath,
|
|
294
|
-
toAdd,
|
|
295
|
-
suggested,
|
|
296
|
+
toAdd: [...new Set([...toAdd, ...Object.keys(rsaValues)])],
|
|
297
|
+
suggested: { ...rsaValues, ...suggested },
|
|
296
298
|
emptyForCredentials,
|
|
297
299
|
encryptionKey,
|
|
298
300
|
added,
|
|
@@ -12,6 +12,7 @@ const fs = require('fs');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
14
14
|
const config = require('./config');
|
|
15
|
+
const { applyLocalEnvOverrides } = require('./local-env-overrides');
|
|
15
16
|
const {
|
|
16
17
|
interpolateEnvVars,
|
|
17
18
|
collectMissingSecrets,
|
|
@@ -40,8 +41,9 @@ const secretsEnsure = require('./secrets-ensure');
|
|
|
40
41
|
const { resolveSecretsPath, getActualSecretsPath } = require('../utils/secrets-path');
|
|
41
42
|
const pathsUtil = require('../utils/paths');
|
|
42
43
|
const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
|
|
43
|
-
const { computeEffectiveEnvironmentScopedResources
|
|
44
|
-
const {
|
|
44
|
+
const { computeEffectiveEnvironmentScopedResources } = require('../utils/environment-scoped-resources');
|
|
45
|
+
const { isStrictScopedEnvironment } = require('../utils/secrets-kv-scope');
|
|
46
|
+
const { applyScopedRedisDbIndex } = require('../utils/redis-env-scope');
|
|
45
47
|
const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
|
|
46
48
|
const {
|
|
47
49
|
mergeAdditionalPublishedPorts,
|
|
@@ -106,7 +108,8 @@ async function resolveKvReferences(
|
|
|
106
108
|
secretsFilePaths,
|
|
107
109
|
appName,
|
|
108
110
|
envTemplatePath,
|
|
109
|
-
envTemplate
|
|
111
|
+
envTemplate,
|
|
112
|
+
scopedKv
|
|
110
113
|
})
|
|
111
114
|
);
|
|
112
115
|
}
|
|
@@ -237,12 +240,14 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
|
|
|
237
240
|
const variablesPath = (options && options.envOnly) ? null : resolveApplicationConfigPath(appPath);
|
|
238
241
|
const template = loadEnvTemplate(templatePath);
|
|
239
242
|
const secretsPaths = await getActualSecretsPath(secretsPath, appName);
|
|
240
|
-
|
|
243
|
+
const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
|
|
244
|
+
// --force backfills a missing secret by generating one. A strict environment must not be
|
|
245
|
+
// handed a value the Controller has never seen, so it errors on the missing key instead.
|
|
246
|
+
if (force && !isStrictScopedEnvironment(runEnvKey)) {
|
|
241
247
|
const preferredPath = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
|
|
242
248
|
await secretsEnsure.ensureSecretsFromEnvTemplate(templatePath, { preferredFilePath: preferredPath });
|
|
243
249
|
}
|
|
244
250
|
const secrets = await loadSecrets(secretsPath, appName);
|
|
245
|
-
const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
|
|
246
251
|
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, {
|
|
247
252
|
envKey: runEnvKey,
|
|
248
253
|
effective,
|
|
@@ -262,10 +267,8 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
|
|
|
262
267
|
Boolean(options.envOnly)
|
|
263
268
|
);
|
|
264
269
|
resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
resolved = applyRedisDbIndexToEnvContent(resolved, idx);
|
|
268
|
-
}
|
|
270
|
+
resolved = applyScopedRedisDbIndex(resolved, runEnvKey, effective);
|
|
271
|
+
resolved = await applyLocalEnvOverrides(resolved, appName, mergeEnvMapIntoContent);
|
|
269
272
|
|
|
270
273
|
return resolved;
|
|
271
274
|
}
|
|
@@ -470,7 +473,11 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
470
473
|
}
|
|
471
474
|
|
|
472
475
|
// Always resolve so missing-secret / kv:// errors still surface in noWrite mode.
|
|
473
|
-
const resolved = await generateEnvContent(appName, secretsPath, environment, force, {
|
|
476
|
+
const resolved = await generateEnvContent(appName, secretsPath, environment, force, {
|
|
477
|
+
appPath,
|
|
478
|
+
envOnly,
|
|
479
|
+
runEnvKey: opts.runEnvKey
|
|
480
|
+
});
|
|
474
481
|
|
|
475
482
|
if (noWrite) {
|
|
476
483
|
return null;
|
|
@@ -157,8 +157,11 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
|
|
|
157
157
|
const environment = options.environment || 'docker';
|
|
158
158
|
const secretsPath = options.secretsPath || null;
|
|
159
159
|
const force = options.force === true;
|
|
160
|
+
const genOpts = options.runEnvKey ? { runEnvKey: String(options.runEnvKey) } : undefined;
|
|
160
161
|
|
|
161
|
-
let resolved =
|
|
162
|
+
let resolved = genOpts
|
|
163
|
+
? await secrets.generateEnvContent(appName, secretsPath, environment, force, genOpts)
|
|
164
|
+
: await secrets.generateEnvContent(appName, secretsPath, environment, force);
|
|
162
165
|
resolved = await injectRegistryTokens(resolved, secretsPath, appName);
|
|
163
166
|
resolved = await injectBashPrefixedExportLines(resolved, secretsPath, appName);
|
|
164
167
|
|
|
@@ -185,7 +188,8 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
|
|
|
185
188
|
* @async
|
|
186
189
|
* @function resolveAndGetEnvMap
|
|
187
190
|
* @param {string} appName - Application name
|
|
188
|
-
* @param {Object} [options] - Options (same as resolveAndWriteEnvFile)
|
|
191
|
+
* @param {Object} [options] - Options (same as resolveAndWriteEnvFile); `runEnvKey` selects the
|
|
192
|
+
* manifest environment for {NAME} resolution
|
|
189
193
|
* @returns {Promise<Object.<string, string>>} Map of variable name to value
|
|
190
194
|
*/
|
|
191
195
|
async function resolveAndGetEnvMap(appName, options = {}) {
|
|
@@ -193,7 +197,10 @@ async function resolveAndGetEnvMap(appName, options = {}) {
|
|
|
193
197
|
const environment = options.environment || 'docker';
|
|
194
198
|
const secretsPath = options.secretsPath || null;
|
|
195
199
|
const force = options.force === true;
|
|
196
|
-
|
|
200
|
+
const genOpts = options.runEnvKey ? { runEnvKey: String(options.runEnvKey) } : undefined;
|
|
201
|
+
let content = genOpts
|
|
202
|
+
? await secrets.generateEnvContent(appName, secretsPath, environment, force, genOpts)
|
|
203
|
+
: await secrets.generateEnvContent(appName, secretsPath, environment, force);
|
|
197
204
|
content = await injectRegistryTokens(content, secretsPath, appName);
|
|
198
205
|
content = await injectBashPrefixedExportLines(content, secretsPath, appName);
|
|
199
206
|
const map = parseEnvContentToMap(content);
|
|
@@ -201,7 +208,7 @@ async function resolveAndGetEnvMap(appName, options = {}) {
|
|
|
201
208
|
const adminSecrets = require('./admin-secrets');
|
|
202
209
|
const { applyAdminSecretsPlatformOverlay } = require('./admin-secrets-env-overlay');
|
|
203
210
|
const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
|
|
204
|
-
applyAdminSecretsPlatformOverlay(map, adminObj);
|
|
211
|
+
applyAdminSecretsPlatformOverlay(map, adminObj, appName);
|
|
205
212
|
} catch {
|
|
206
213
|
// admin-secrets.env optional until up-infra/setup
|
|
207
214
|
}
|
package/lib/core/secrets-load.js
CHANGED
|
@@ -167,9 +167,11 @@ async function mergeFromConfiguredSecretsPath(configSecretsPath, userSecrets, he
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
async function loadMergedConfigAndUserSecrets() {
|
|
170
|
-
const userSecrets = loadPrimaryUserSecrets();
|
|
171
|
-
const helpers = createMergeHelpers(userSecrets);
|
|
172
170
|
const userPath = pathsUtil.getPrimaryUserSecretsLocalPath();
|
|
171
|
+
const userSecrets = await decryptSecretsObject(loadPrimaryUserSecrets(), {
|
|
172
|
+
defaultSourceLabel: userPath
|
|
173
|
+
});
|
|
174
|
+
const helpers = createMergeHelpers(userSecrets);
|
|
173
175
|
/** @type {Record<string, string>} */
|
|
174
176
|
const keySources = {};
|
|
175
177
|
for (const k of Object.keys(userSecrets || {})) {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline semantic checks for the binary/documents pairing (dataplane schema 2.9.13).
|
|
3
|
+
*
|
|
4
|
+
* The schema alone cannot express these: `output.mode: documents` and
|
|
5
|
+
* `fetch.responseType: binary` are individually valid anywhere, but only mean something
|
|
6
|
+
* when paired with a documentStorage entity and a binary-capable fetch source. The
|
|
7
|
+
* dataplane rejects the bad combinations at upload; these checks give the same answer
|
|
8
|
+
* offline, under the same error codes, so an author is not told "valid" by the CLI and
|
|
9
|
+
* "invalid" by the server.
|
|
10
|
+
*
|
|
11
|
+
* Codes mirror the dataplane validator and must not be renamed here.
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Semantic validation for binary responses and document output
|
|
14
|
+
* @author AI Fabrix Team
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
/** Sources whose response body the runtime can read as bytes. */
|
|
21
|
+
const BINARY_FETCH_SOURCES = new Set(['openapi', 'http']);
|
|
22
|
+
|
|
23
|
+
/** documentStorage.binaryOperationRef default, per type/document-storage.json. */
|
|
24
|
+
const DEFAULT_BINARY_OPERATION = 'get';
|
|
25
|
+
|
|
26
|
+
const CODES = Object.freeze({
|
|
27
|
+
entityType: 'cip_documents_output_entity_type',
|
|
28
|
+
storageDisabled: 'cip_documents_output_storage_disabled',
|
|
29
|
+
binarySource: 'cip_fetch_binary_source_invalid',
|
|
30
|
+
withoutBinaryFetch: 'cip_documents_output_without_binary_fetch'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} datasource - Datasource definition
|
|
35
|
+
* @returns {Array<[string, Object]>} CIP operation entries, empty when not a CIP datasource
|
|
36
|
+
*/
|
|
37
|
+
function cipOperations(datasource) {
|
|
38
|
+
const cip = datasource?.execution?.engine === 'cip' ? datasource.execution.cip : null;
|
|
39
|
+
const operations = cip && cip.operations;
|
|
40
|
+
if (!operations || typeof operations !== 'object') {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return Object.entries(operations).filter(([, operation]) => operation && typeof operation === 'object');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {Object} operation - CIP operation
|
|
48
|
+
* @returns {Array<Object>} Its steps
|
|
49
|
+
*/
|
|
50
|
+
function stepsOf(operation) {
|
|
51
|
+
return Array.isArray(operation.steps) ? operation.steps : [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A step declares a binary body. The schema also promotes a binary Content-Type when
|
|
56
|
+
* responseType is omitted, but that is a runtime decision we cannot make offline, so only
|
|
57
|
+
* the explicit declaration counts here.
|
|
58
|
+
* @param {Object} step - CIP step
|
|
59
|
+
* @returns {boolean} True when the step fetches a binary body
|
|
60
|
+
*/
|
|
61
|
+
function isBinaryFetch(step) {
|
|
62
|
+
return Boolean(step && step.fetch && String(step.fetch.responseType || '').trim().toLowerCase() === 'binary');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {Object} step - CIP step
|
|
67
|
+
* @returns {boolean} True when the step finalizes as documents
|
|
68
|
+
*/
|
|
69
|
+
function isDocumentsOutput(step) {
|
|
70
|
+
return Boolean(step && step.output && step.output.mode === 'documents');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `binary` is a body contract, so it only applies where the runtime holds the response
|
|
75
|
+
* bytes. A datasource, viewpoint or record source produces rows, never a file body.
|
|
76
|
+
* @param {string} operationName - Operation key
|
|
77
|
+
* @param {Array<Object>} steps - Operation steps
|
|
78
|
+
* @param {Array<Object>} errors - Collected errors
|
|
79
|
+
* @returns {void}
|
|
80
|
+
*/
|
|
81
|
+
function checkBinaryFetchSources(operationName, steps, errors) {
|
|
82
|
+
steps.forEach((step, index) => {
|
|
83
|
+
if (!isBinaryFetch(step)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const source = String(step.fetch.source || 'openapi').trim().toLowerCase();
|
|
87
|
+
if (BINARY_FETCH_SOURCES.has(source)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
errors.push({
|
|
91
|
+
code: CODES.binarySource,
|
|
92
|
+
message:
|
|
93
|
+
`operations.${operationName}.steps[${index}].fetch.responseType is "binary" but ` +
|
|
94
|
+
`fetch.source is "${source || 'unset'}"; binary bodies come from openapi or http sources only`
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {Object} datasource - Datasource definition
|
|
101
|
+
* @param {string} operationName - Operation key that emits documents
|
|
102
|
+
* @param {number} stepIndex - Index of the output step
|
|
103
|
+
* @param {Array<Object>} errors - Collected errors
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
function checkDocumentsEntity(datasource, operationName, stepIndex, errors) {
|
|
107
|
+
const where = `operations.${operationName}.steps[${stepIndex}].output.mode is "documents"`;
|
|
108
|
+
if (!['documentstorage', 'document-storage'].includes(String(datasource.entityType || '').trim().toLowerCase().replace(/_/g, '-'))) {
|
|
109
|
+
errors.push({
|
|
110
|
+
code: CODES.entityType,
|
|
111
|
+
message: `${where} but entityType is "${datasource.entityType || 'unset'}"; documents output requires entityType documentStorage`
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const storage = datasource.documentStorage;
|
|
116
|
+
if (!storage || storage.enabled === false) {
|
|
117
|
+
errors.push({
|
|
118
|
+
code: CODES.storageDisabled,
|
|
119
|
+
message: `${where} but documentStorage is ${storage ? 'not enabled' : 'missing'}; set documentStorage.enabled true`
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The operation named by binaryOperationRef must actually retrieve bytes, otherwise the
|
|
126
|
+
* document pipeline has nothing to store. A missing reference and a reference to a
|
|
127
|
+
* JSON-only operation are the same defect from the author's point of view.
|
|
128
|
+
* @param {Object} datasource - Datasource definition
|
|
129
|
+
* @param {string} operationName - Operation key that emits documents
|
|
130
|
+
* @param {Array<Object>} warnings - Collected warnings
|
|
131
|
+
* @returns {void}
|
|
132
|
+
*/
|
|
133
|
+
function checkBinaryOperationRef(datasource, operationName, warnings) {
|
|
134
|
+
const storage = datasource.documentStorage || {};
|
|
135
|
+
const ref = String(storage.binaryOperationRef || DEFAULT_BINARY_OPERATION);
|
|
136
|
+
const operations = new Map(cipOperations(datasource));
|
|
137
|
+
const referenced = operations.get(ref);
|
|
138
|
+
if (ref === operationName && referenced && stepsOf(referenced).some(isBinaryFetch)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const reason = ref !== operationName && referenced
|
|
142
|
+
? `binaryOperationRef "${ref}" does not name this operation "${operationName}"`
|
|
143
|
+
: referenced
|
|
144
|
+
? `operation "${ref}" has no step with fetch.responseType "binary"`
|
|
145
|
+
: `operation "${ref}" does not exist in execution.cip.operations`;
|
|
146
|
+
warnings.push({
|
|
147
|
+
code: CODES.withoutBinaryFetch,
|
|
148
|
+
message:
|
|
149
|
+
`operations.${operationName} emits documents but ${reason}; ` +
|
|
150
|
+
'set documentStorage.binaryOperationRef to the operation that fetches the file'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Semantic checks for the binary/documents pairing.
|
|
156
|
+
* @param {Object} datasource - Datasource definition
|
|
157
|
+
* @returns {{ errors: Array<Object>, warnings: Array<Object> }} Findings with dataplane codes
|
|
158
|
+
*/
|
|
159
|
+
function validateBinaryDocumentsContract(datasource) {
|
|
160
|
+
const errors = [];
|
|
161
|
+
const warnings = [];
|
|
162
|
+
if (!datasource || typeof datasource !== 'object') {
|
|
163
|
+
return { errors, warnings };
|
|
164
|
+
}
|
|
165
|
+
for (const [operationName, operation] of cipOperations(datasource)) {
|
|
166
|
+
if (operation.enabled === false) continue;
|
|
167
|
+
const steps = stepsOf(operation);
|
|
168
|
+
checkBinaryFetchSources(operationName, steps, errors);
|
|
169
|
+
steps.forEach((step, index) => {
|
|
170
|
+
if (!isDocumentsOutput(step)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
checkDocumentsEntity(datasource, operationName, index, errors);
|
|
174
|
+
const preceding = steps.slice(0, index + 1).some(candidate =>
|
|
175
|
+
isBinaryFetch(candidate) && BINARY_FETCH_SOURCES.has(String(candidate.fetch.source || 'openapi').trim().toLowerCase()));
|
|
176
|
+
if (!preceding) {
|
|
177
|
+
warnings.push({ code: CODES.withoutBinaryFetch,
|
|
178
|
+
message: `operations.${operationName} has no step with fetch.responseType "binary" preceding documents output` });
|
|
179
|
+
} else {
|
|
180
|
+
checkBinaryOperationRef(datasource, operationName, warnings);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return { errors, warnings };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
CODES,
|
|
189
|
+
validateBinaryDocumentsContract
|
|
190
|
+
};
|