@aifabrix/builder 2.50.0 → 2.51.1
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/plans/Done/157.1-security-quality-violations-by-type.csv +374 -0
- package/.cursor/rules/assistant-driven-integration-build.mdc +40 -0
- package/integration/hubspot-test/create-hubspot.js +1 -16
- package/jest.isolated-projects.js +77 -1
- package/jest.projects.js +5 -426
- package/lib/api/assistants.api.js +67 -0
- package/lib/api/datasources-extended.api.js +6 -6
- package/lib/api/evidence-authoring-helpers.js +267 -0
- package/lib/api/evidence-authoring.api.js +337 -0
- package/lib/api/evidence-kinds.api.js +128 -0
- package/lib/api/evidence.api.js +57 -1
- package/lib/api/index.js +24 -7
- package/lib/api/pipeline.api.js +47 -3
- package/lib/api/records-bulk.api.js +28 -3
- package/lib/api/resource-types.api.js +4 -4
- package/lib/api/roles.api.js +121 -0
- package/lib/api/types/evidence-kinds.types.js +42 -0
- package/lib/api/types/evidence.types.js +156 -8
- package/lib/api/types/roles.types.js +36 -0
- package/lib/api/work.api.js +283 -0
- package/lib/app/certification-verify-rows.js +1 -1
- package/lib/app/run-deploy-job.js +108 -0
- package/lib/app/run-helpers.js +10 -3
- package/lib/app/run.js +8 -0
- package/lib/app/show.js +2 -2
- package/lib/certification/merge-certification-from-artifact.js +1 -1
- package/lib/channels/add-channel.js +54 -23
- package/lib/channels/approval-guides/chatgpt.js +8 -7
- package/lib/channels/approval-guides/microsoft-copilot.js +7 -8
- package/lib/channels/channel-artifact-generator.js +16 -3
- package/lib/channels/channel-credential.js +3 -1
- package/lib/channels/channel-probe-evidence.js +79 -0
- package/lib/channels/channel-probe.js +265 -13
- package/lib/channels/channel-runtime-resolver.js +120 -29
- package/lib/channels/channel-status-display.js +44 -0
- package/lib/channels/channel-targets.js +7 -0
- package/lib/channels/chatgpt-runtime-contract.js +29 -0
- package/lib/channels/microsoft-copilot-package.js +280 -0
- package/lib/channels/runtime-tool-catalog.js +165 -0
- package/lib/cli/index.js +6 -0
- package/lib/cli/setup-app.js +26 -5
- package/lib/cli/setup-dev.js +23 -7
- package/lib/cli/setup-external-system-help.js +8 -2
- package/lib/cli/setup-external-system.js +12 -0
- package/lib/cli/setup-utility-help-after.js +12 -0
- package/lib/cli/setup-utility-validate.js +41 -2
- package/lib/commands/app-test.js +2 -14
- package/lib/commands/datasource-load-export-cli.js +6 -1
- package/lib/commands/deploy-gate-failure-details.js +23 -0
- package/lib/commands/deploy-gate-orchestrator.js +64 -18
- package/lib/commands/dev-cli-handlers.js +6 -3
- package/lib/commands/dev-down.js +2 -6
- package/lib/commands/dev-init.js +7 -109
- package/lib/commands/dev-refresh.js +197 -0
- package/lib/commands/evidence-kind.js +478 -0
- package/lib/commands/governance-command-action.js +4 -2
- package/lib/commands/governance-generate-external.js +56 -21
- package/lib/commands/lifecycle-external.js +10 -4
- package/lib/commands/repair-datasource-apply.js +10 -1
- package/lib/commands/repair-datasource-run.js +4 -2
- package/lib/commands/repair-datasource-test-payload.js +80 -19
- package/lib/commands/repair-datasource-webhooks-sync-bridge.js +42 -3
- package/lib/commands/repair-internal.js +9 -1
- package/lib/commands/repair-openapi-export.js +1 -1
- package/lib/commands/repair-openapi-sync.js +43 -18
- package/lib/commands/resource-type.js +101 -18
- package/lib/commands/role-assistant.js +313 -0
- package/lib/commands/role.js +452 -0
- package/lib/commands/setup.js +1 -1
- package/lib/commands/upload-data-sync-finalize.js +5 -4
- package/lib/commands/upload-sidecar-followups.js +144 -47
- package/lib/commands/upload.js +43 -6
- package/lib/commands/validate-json.js +230 -0
- package/lib/commands/verify-governance-combined-external.js +73 -9
- package/lib/commands/verify-governance-db.js +80 -25
- package/lib/commands/verify-governance-external.js +29 -5
- package/lib/commands/verify-operations-step-plan.js +78 -40
- package/lib/commands/wizard-core-helpers.js +7 -3
- package/lib/commands/wizard-core.js +78 -9
- package/lib/core/audit-logger.js +1 -1
- package/lib/core/secrets-load.js +1 -3
- package/lib/datasource/bulk-loader-run-batches.js +4 -2
- package/lib/datasource/bulk-loader-service.js +5 -1
- package/lib/datasource/capability/relate-operations.js +1 -1
- package/lib/datasource/datasource-validate-summary.js +2 -2
- package/lib/datasource/validate.js +13 -6
- package/lib/external-system/deploy-helpers.js +35 -1
- package/lib/external-system/deploy.js +23 -10
- package/lib/external-system/download-helpers.js +111 -2
- package/lib/external-system/download-role-assistant.js +55 -0
- package/lib/external-system/download.js +45 -26
- package/lib/external-system/test-helpers.js +1 -1
- package/lib/external-system/test.js +39 -3
- package/lib/generator/builders.js +3 -0
- package/lib/generator/helpers.js +13 -5
- package/lib/generator/split-variables.js +9 -3
- package/lib/generator/wizard.js +51 -4
- package/lib/infrastructure/helpers-docker-check.js +6 -0
- package/lib/infrastructure/helpers.js +1 -1
- package/lib/lifecycle/access-control-abac.js +121 -0
- package/lib/lifecycle/lifecycle-governance-options.js +4 -1
- package/lib/lifecycle/product-model.js +3 -2
- package/lib/lifecycle/report-display.js +14 -2
- package/lib/programmatic/auth-config.js +3 -2
- package/lib/programmatic/bearer-auth.js +1 -1
- package/lib/programmatic/capability-registry.js +20 -1
- package/lib/programmatic/command-options.js +1 -0
- package/lib/programmatic/enterprise-mcp-client.js +18 -2
- package/lib/programmatic/index.js +4 -0
- package/lib/programmatic/manifest-patch-section.js +48 -9
- package/lib/programmatic/manifest-section-registry-data.js +3 -2
- package/lib/programmatic/manifest-section-registry.js +18 -1
- package/lib/programmatic/openapi-apply-result-schemas.js +44 -0
- package/lib/programmatic/openapi-component-schemas.js +16 -1
- package/lib/programmatic/openapi-description-enricher.js +3 -1
- package/lib/programmatic/openapi-descriptions.js +55 -5
- package/lib/programmatic/openapi-evidence-schemas.js +161 -0
- package/lib/programmatic/openapi-manifest-schemas.js +6 -2
- package/lib/programmatic/openapi-operation-bindings.js +99 -0
- package/lib/programmatic/openapi-request-schemas.js +36 -1
- package/lib/programmatic/openapi-result-schemas.js +29 -12
- package/lib/programmatic/openapi-schema-descriptions-certify.js +5 -1
- package/lib/programmatic/openapi-schema-descriptions-core.js +6 -3
- package/lib/programmatic/openapi-schema-descriptions-evidence.js +156 -0
- package/lib/programmatic/openapi-schema-descriptions-manifest.js +4 -3
- package/lib/programmatic/openapi-schema-descriptions-operations.js +70 -0
- package/lib/programmatic/openapi-schema-helpers.js +16 -1
- package/lib/programmatic/openapi-spec.js +48 -29
- package/lib/programmatic/orchestration-openapi-meta.js +5 -0
- package/lib/programmatic/request-timeout.js +3 -0
- package/lib/programmatic/route-handler-map.js +89 -2
- package/lib/programmatic/run-certify.js +15 -2
- package/lib/programmatic/run-deploy.js +74 -0
- package/lib/programmatic/run-evidence-authoring.js +80 -0
- package/lib/programmatic/run-protection.js +124 -0
- package/lib/programmatic/run-repository-emcp.js +11 -3
- package/lib/programmatic/run-template-workspace.js +42 -2
- package/lib/programmatic/workspace-context.js +6 -0
- package/lib/protection/auth-context.js +48 -6
- package/lib/protection/paths.js +22 -0
- package/lib/protection/run-protection-create-helpers.js +22 -1
- package/lib/protection/run-protection-create.js +1 -1
- package/lib/protection/sync-after-upload.js +1 -1
- package/lib/resolvers/evidence-kind-file.js +230 -0
- package/lib/resolvers/resource-type-file.js +36 -1
- package/lib/resolvers/role-file.js +227 -0
- package/lib/role-assistant/evidence-sync.js +450 -0
- package/lib/role-assistant/github-workflow.js +96 -0
- package/lib/role-assistant/knowledge-sync.js +465 -0
- package/lib/role-assistant/settings.js +126 -0
- package/lib/role-assistant/test-cases-evaluate.js +170 -0
- package/lib/role-assistant/test-cases-honesty.js +184 -0
- package/lib/role-assistant/test-cases.js +364 -0
- package/lib/role-assistant/test-runner-workhub-answers.js +203 -0
- package/lib/role-assistant/test-runner-workhub.js +385 -0
- package/lib/role-assistant/test-runner.js +500 -0
- package/lib/schema/application-schema.json +36 -2
- package/lib/schema/deployment-rules.yaml +1 -0
- package/lib/schema/external-datasource.schema.json +121 -4
- package/lib/schema/external-system.schema.json +20 -6
- package/lib/schema/infra.parameter.yaml +45 -1
- package/lib/schema/role-assistant-settings.schema.json +108 -0
- package/lib/schema/type/credential-field-registry.json +19 -3
- package/lib/utils/api.js +52 -21
- package/lib/utils/app-path-resolve.js +72 -24
- package/lib/utils/app-register-config.js +4 -3
- package/lib/utils/authentication-shape.js +3 -1
- package/lib/utils/cli-sync-options.js +59 -3
- package/lib/utils/cli-utils.js +11 -3
- package/lib/utils/deployment-validation.js +28 -3
- package/lib/utils/dev-ca-install.js +5 -6
- package/lib/utils/dev-config.js +2 -2
- package/lib/utils/dev-list-sort.js +145 -0
- package/lib/utils/dev-platform-tls.js +0 -63
- package/lib/utils/docker-build.js +46 -27
- package/lib/utils/docker-daemon-tls-ca.js +1 -1
- package/lib/utils/docker-exec.js +91 -6
- package/lib/utils/docker-run-ephemeral.js +55 -0
- package/lib/utils/enterprise-knowledge-paths.js +29 -12
- package/lib/utils/env-copy-keycloak.js +196 -0
- package/lib/utils/env-copy.js +35 -4
- package/lib/utils/error-formatters/error-parser.js +0 -3
- package/lib/utils/external-env-template.js +1 -1
- package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
- package/lib/utils/external-system-readiness-compact-display.js +3 -0
- package/lib/utils/health-check-request.js +2 -3
- package/lib/utils/help-builder.js +4 -0
- package/lib/utils/image-name.js +22 -4
- package/lib/utils/logger.js +37 -4
- package/lib/utils/manifest-source-emit.js +1 -1
- package/lib/utils/paths.js +98 -26
- package/lib/utils/pipeline-upload-auth-retry.js +19 -3
- package/lib/utils/postgres-platform-bootstrap.js +24 -17
- package/lib/utils/postgres-wipe.js +5 -2
- package/lib/utils/remote-dev-auth.js +39 -4
- package/lib/utils/resolve-docker-image-ref.js +36 -6
- package/lib/utils/role-assistant-paths.js +98 -0
- package/lib/utils/schema-loader.js +41 -0
- package/lib/utils/schema-resolver.js +36 -4
- package/lib/utils/token-manager.js +47 -22
- package/lib/utils/upload-sync-options.js +17 -3
- package/lib/utils/url-declarative-resolve-load-doc.js +38 -1
- package/lib/validation/external-manifest-validator.js +31 -8
- package/lib/validation/openapi-contract-surface-validator.js +4 -0
- package/lib/validation/validate-external-file.js +8 -1
- package/lib/validation/validator.js +6 -5
- package/package.json +35 -12
- package/scripts/export-builder-api-evidence.js +3 -3
- package/scripts/lib/plan-actor.js +2 -2
- package/scripts/lib/plan-meta.js +1 -1
- package/scripts/lib/plan-parse.js +1 -1
- package/scripts/lib/plan-prevalidate-gates.js +12 -1
- package/scripts/lib/plan-run-report.js +13 -2
- package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
- package/templates/applications/dataplane/application.yaml +47 -17
- package/templates/applications/dataplane/env.template +28 -9
- package/templates/applications/dataplane/rbac.yaml +78 -2
- package/templates/applications/miso-controller/application.yaml +10 -1
- package/templates/applications/miso-controller/env.template +13 -8
- package/templates/applications/miso-controller/rbac.yaml +72 -0
- package/templates/channels/chatgpt/install.template.md.hbs +42 -24
- package/templates/channels/chatgpt/integration/README.md +10 -7
- package/templates/channels/chatgpt/integration/application.json +2 -2
- package/templates/channels/chatgpt/integration/chatgpt-system.json +3 -3
- package/templates/channels/chatgpt/integration/env.template +1 -1
- package/templates/channels/chatgpt/manifest.template.json.hbs +36 -5
- package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +28 -0
- package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +22 -0
- package/templates/channels/microsoft-copilot/install.template.md.hbs +32 -22
- package/templates/channels/microsoft-copilot/integration/README.md +9 -7
- package/templates/channels/microsoft-copilot/integration/application.json +2 -2
- package/templates/channels/microsoft-copilot/integration/env.template +1 -1
- package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +7 -6
- package/templates/channels/microsoft-copilot/manifest.template.json.hbs +34 -6
- package/templates/channels/slack/integration/slack-system.json +60 -2
- package/templates/python/requirements.txt +1 -1
|
@@ -15,6 +15,11 @@ const {
|
|
|
15
15
|
SYSTEM_KIND_ENTERPRISE_KNOWLEDGE,
|
|
16
16
|
getEnterpriseKnowledgePath
|
|
17
17
|
} = require('./enterprise-knowledge-paths');
|
|
18
|
+
const {
|
|
19
|
+
WORKSPACE_ROLE_ASSISTANT,
|
|
20
|
+
SYSTEM_KIND_DIGITAL_WORKER,
|
|
21
|
+
getRoleAssistantPath
|
|
22
|
+
} = require('./role-assistant-paths');
|
|
18
23
|
|
|
19
24
|
const WORKSPACE_INTEGRATION = 'integration';
|
|
20
25
|
|
|
@@ -62,10 +67,14 @@ function normalizeWorkspaceOverride(raw) {
|
|
|
62
67
|
return null;
|
|
63
68
|
}
|
|
64
69
|
const v = String(raw).trim().toLowerCase();
|
|
65
|
-
if (
|
|
70
|
+
if (
|
|
71
|
+
v === WORKSPACE_INTEGRATION ||
|
|
72
|
+
v === WORKSPACE_ENTERPRISE_KNOWLEDGE ||
|
|
73
|
+
v === WORKSPACE_ROLE_ASSISTANT
|
|
74
|
+
) {
|
|
66
75
|
return v;
|
|
67
76
|
}
|
|
68
|
-
throw new Error(`Unknown workspace "${raw}". Use integration or
|
|
77
|
+
throw new Error(`Unknown workspace "${raw}". Use integration, enterprise-knowledge, or role-assistant.`);
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
function workspacePair(appPath, baseDir) {
|
|
@@ -76,47 +85,84 @@ function resolveFromWorkspaceOverride(workspaceOverride, appName, getIntegration
|
|
|
76
85
|
if (workspaceOverride === WORKSPACE_ENTERPRISE_KNOWLEDGE) {
|
|
77
86
|
return workspacePair(getEnterpriseKnowledgePath(appName), WORKSPACE_ENTERPRISE_KNOWLEDGE);
|
|
78
87
|
}
|
|
88
|
+
if (workspaceOverride === WORKSPACE_ROLE_ASSISTANT) {
|
|
89
|
+
return workspacePair(getRoleAssistantPath(appName), WORKSPACE_ROLE_ASSISTANT);
|
|
90
|
+
}
|
|
79
91
|
if (workspaceOverride === WORKSPACE_INTEGRATION) {
|
|
80
92
|
return workspacePair(getIntegrationPath(appName), WORKSPACE_INTEGRATION);
|
|
81
93
|
}
|
|
82
94
|
return null;
|
|
83
95
|
}
|
|
84
96
|
|
|
85
|
-
function
|
|
86
|
-
const kind =
|
|
87
|
-
explicitKind ||
|
|
88
|
-
readSystemKindFromAppDir(ekPath, appName) ||
|
|
89
|
-
readSystemKindFromAppDir(intPath, appName);
|
|
97
|
+
function resolveWorkspaceFromSystemKind(kind, ekPath, intPath, roleAssistantPath) {
|
|
90
98
|
if (kind === SYSTEM_KIND_ENTERPRISE_KNOWLEDGE) {
|
|
91
99
|
return workspacePair(ekPath, WORKSPACE_ENTERPRISE_KNOWLEDGE);
|
|
92
100
|
}
|
|
101
|
+
if (kind === SYSTEM_KIND_DIGITAL_WORKER) {
|
|
102
|
+
return workspacePair(roleAssistantPath, WORKSPACE_ROLE_ASSISTANT);
|
|
103
|
+
}
|
|
93
104
|
return workspacePair(intPath, WORKSPACE_INTEGRATION);
|
|
94
105
|
}
|
|
95
106
|
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
function resolveAmbiguousWorkspace(ekPath, intPath, roleAssistantPath, explicitKind, appName) {
|
|
108
|
+
const kind =
|
|
109
|
+
explicitKind ||
|
|
110
|
+
readSystemKindFromAppDir(roleAssistantPath, appName) ||
|
|
111
|
+
readSystemKindFromAppDir(ekPath, appName) ||
|
|
112
|
+
readSystemKindFromAppDir(intPath, appName);
|
|
113
|
+
return resolveWorkspaceFromSystemKind(kind, ekPath, intPath, roleAssistantPath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveFromExistingPaths(paths, existence, explicitKind, appName) {
|
|
117
|
+
const { ekPath, intPath, roleAssistantPath } = paths;
|
|
118
|
+
const { ekExists, intExists, roleAssistantExists } = existence;
|
|
119
|
+
const existingCount = [ekExists, intExists, roleAssistantExists].filter(Boolean).length;
|
|
120
|
+
if (existingCount === 1) {
|
|
121
|
+
if (ekExists) {
|
|
122
|
+
return workspacePair(ekPath, WORKSPACE_ENTERPRISE_KNOWLEDGE);
|
|
123
|
+
}
|
|
124
|
+
if (roleAssistantExists) {
|
|
125
|
+
return workspacePair(roleAssistantPath, WORKSPACE_ROLE_ASSISTANT);
|
|
126
|
+
}
|
|
101
127
|
return workspacePair(intPath, WORKSPACE_INTEGRATION);
|
|
102
128
|
}
|
|
103
|
-
if (
|
|
104
|
-
return resolveAmbiguousWorkspace(ekPath, intPath, explicitKind, appName);
|
|
129
|
+
if (existingCount > 1) {
|
|
130
|
+
return resolveAmbiguousWorkspace(ekPath, intPath, roleAssistantPath, explicitKind, appName);
|
|
105
131
|
}
|
|
106
|
-
const kind =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
const kind =
|
|
133
|
+
explicitKind ||
|
|
134
|
+
readSystemKindFromAppDir(roleAssistantPath, appName) ||
|
|
135
|
+
readSystemKindFromAppDir(ekPath, appName) ||
|
|
136
|
+
readSystemKindFromAppDir(intPath, appName);
|
|
137
|
+
return resolveWorkspaceFromSystemKind(kind, ekPath, intPath, roleAssistantPath);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveFromExistingPathsBackwardCompatible(
|
|
141
|
+
ekPath,
|
|
142
|
+
intPath,
|
|
143
|
+
roleAssistantPath,
|
|
144
|
+
explicitKind,
|
|
145
|
+
appName
|
|
146
|
+
) {
|
|
147
|
+
return resolveFromExistingPaths(
|
|
148
|
+
{ ekPath, intPath, roleAssistantPath },
|
|
149
|
+
{
|
|
150
|
+
ekExists: isExistingDirectory(ekPath),
|
|
151
|
+
intExists: isExistingDirectory(intPath),
|
|
152
|
+
roleAssistantExists: isExistingDirectory(roleAssistantPath)
|
|
153
|
+
},
|
|
154
|
+
explicitKind,
|
|
155
|
+
appName
|
|
156
|
+
);
|
|
111
157
|
}
|
|
112
158
|
|
|
113
159
|
/**
|
|
114
160
|
* Resolves authoring tree for an external system or Enterprise Knowledge anchor.
|
|
115
161
|
* @param {string} appName - App or anchor key
|
|
116
162
|
* @param {Object} [options]
|
|
117
|
-
* @param {string} [options.workspace] - integration | enterprise-knowledge
|
|
163
|
+
* @param {string} [options.workspace] - integration | enterprise-knowledge | role-assistant
|
|
118
164
|
* @param {string} [options.systemKind] - Explicit systemKind override
|
|
119
|
-
* @returns {{ appPath: string, baseDir: 'integration' | 'enterprise-knowledge' }}
|
|
165
|
+
* @returns {{ appPath: string, baseDir: 'integration' | 'enterprise-knowledge' | 'role-assistant' }}
|
|
120
166
|
*/
|
|
121
167
|
function resolveAppWorkspace(appName, options = {}) {
|
|
122
168
|
if (!appName || typeof appName !== 'string') {
|
|
@@ -137,11 +183,11 @@ function resolveAppWorkspace(appName, options = {}) {
|
|
|
137
183
|
|
|
138
184
|
const ekPath = getEnterpriseKnowledgePath(appName);
|
|
139
185
|
const intPath = getIntegrationPath(appName);
|
|
140
|
-
|
|
186
|
+
const roleAssistantPath = getRoleAssistantPath(appName);
|
|
187
|
+
return resolveFromExistingPathsBackwardCompatible(
|
|
141
188
|
ekPath,
|
|
142
189
|
intPath,
|
|
143
|
-
|
|
144
|
-
isExistingDirectory(intPath),
|
|
190
|
+
roleAssistantPath,
|
|
145
191
|
explicitKind,
|
|
146
192
|
appName
|
|
147
193
|
);
|
|
@@ -160,7 +206,9 @@ function resolveExternalSystemPath(appName, options = {}) {
|
|
|
160
206
|
module.exports = {
|
|
161
207
|
WORKSPACE_INTEGRATION,
|
|
162
208
|
WORKSPACE_ENTERPRISE_KNOWLEDGE,
|
|
209
|
+
WORKSPACE_ROLE_ASSISTANT,
|
|
163
210
|
SYSTEM_KIND_ENTERPRISE_KNOWLEDGE,
|
|
211
|
+
SYSTEM_KIND_DIGITAL_WORKER,
|
|
164
212
|
readSystemKindFromAppDir,
|
|
165
213
|
normalizeWorkspaceOverride,
|
|
166
214
|
resolveAppWorkspace,
|
|
@@ -228,11 +228,11 @@ async function extractExternalAppConfiguration(appKey, variables, appKeyFromFile
|
|
|
228
228
|
* @returns {Object} Webapp configuration
|
|
229
229
|
*/
|
|
230
230
|
function extractWebappConfiguration(variables, appKeyFromFile, displayName, description, options) {
|
|
231
|
-
const appType = variables.
|
|
231
|
+
const appType = variables.app?.type || 'webapp';
|
|
232
232
|
const registryMode = variables.image?.registryMode || 'external';
|
|
233
233
|
const port = options.port ?? getContainerPort(variables);
|
|
234
234
|
const localPort = port;
|
|
235
|
-
const language = variables.build?.language || 'typescript';
|
|
235
|
+
const language = variables.build?.language || variables.app?.language || 'typescript';
|
|
236
236
|
const image = buildImageReference(variables, appKeyFromFile);
|
|
237
237
|
const url = variables.app?.url || variables.deployment?.dataplaneUrl || variables.deployment?.appUrl || null;
|
|
238
238
|
|
|
@@ -252,7 +252,8 @@ function extractWebappConfiguration(variables, appKeyFromFile, displayName, desc
|
|
|
252
252
|
|
|
253
253
|
async function extractAppConfiguration(variables, appKey, options) {
|
|
254
254
|
const appKeyFromFile = variables.app?.key || appKey;
|
|
255
|
-
const displayName =
|
|
255
|
+
const displayName =
|
|
256
|
+
variables.app?.displayName || variables.app?.name || options.name || appKey;
|
|
256
257
|
const description = variables.app?.description || '';
|
|
257
258
|
|
|
258
259
|
if (variables.app?.type === 'external') {
|
|
@@ -64,7 +64,9 @@ function buildFlatCredentialTemplate(systemKey, authType) {
|
|
|
64
64
|
security: { clientId: kvPath('clientId'), clientSecret: kvPath('clientSecret') }
|
|
65
65
|
},
|
|
66
66
|
apikey: {
|
|
67
|
-
|
|
67
|
+
// Match credential-field-registry placeholders (Authorization + Bearer).
|
|
68
|
+
// Integrations that need a literal custom header set headerName after repair.
|
|
69
|
+
variables: { baseUrl: base, headerName: 'Authorization', prefix: 'Bearer' },
|
|
68
70
|
security: { apiKey: kvPath('apiKey') }
|
|
69
71
|
},
|
|
70
72
|
bearerToken: {
|
|
@@ -6,9 +6,60 @@
|
|
|
6
6
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* True when `node` looks like a Commander command (or a test double), not a plain opts bag.
|
|
11
|
+
*
|
|
12
|
+
* @param {*} node
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
function isCommanderLike(node) {
|
|
16
|
+
if (!node || typeof node !== 'object') {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
Array.isArray(node.rawArgs) ||
|
|
21
|
+
typeof node.opts === 'function' ||
|
|
22
|
+
(Object.prototype.hasOwnProperty.call(node, 'parent') &&
|
|
23
|
+
(node.parent === null ||
|
|
24
|
+
node.parent === undefined ||
|
|
25
|
+
typeof node.parent === 'object'))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Collect argv-like arrays from a Commander command and its parents.
|
|
31
|
+
* Subcommand `rawArgs` is often empty; the flag lives on the root command.
|
|
32
|
+
* Safely no-ops for null/undefined or plain options objects (not Command-like).
|
|
33
|
+
*
|
|
34
|
+
* @param {import('commander').Command|null|undefined} cmd
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
function collectCommanderRawArgs(cmd) {
|
|
38
|
+
if (!isCommanderLike(cmd)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const chunks = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
let current = cmd;
|
|
44
|
+
while (isCommanderLike(current)) {
|
|
45
|
+
if (seen.has(current)) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
seen.add(current);
|
|
49
|
+
if (Array.isArray(current.rawArgs) && current.rawArgs.length > 0) {
|
|
50
|
+
chunks.push(...current.rawArgs);
|
|
51
|
+
}
|
|
52
|
+
current = current.parent;
|
|
53
|
+
}
|
|
54
|
+
return chunks;
|
|
55
|
+
}
|
|
56
|
+
|
|
9
57
|
/**
|
|
10
58
|
* True when the user passed `--no-publish` or set `noPublish: true` (programmatic).
|
|
11
59
|
*
|
|
60
|
+
* Commander maps `.option('--no-publish', …)` to `publish: false` (default `true`),
|
|
61
|
+
* not `noPublish: true`. Detect that shape as well as programmatic `noPublish`.
|
|
62
|
+
*
|
|
12
63
|
* @param {Object|null|undefined} options - Commander-parsed options
|
|
13
64
|
* @param {import('commander').Command} [cmd] - Commander command (for rawArgs fallback)
|
|
14
65
|
* @returns {boolean}
|
|
@@ -20,10 +71,15 @@ function cliOptsSkipPublish(options, cmd) {
|
|
|
20
71
|
if (options.noPublish === true) {
|
|
21
72
|
return true;
|
|
22
73
|
}
|
|
23
|
-
|
|
24
|
-
|
|
74
|
+
// Commander `--no-X` → property `X` defaults true; flag sets it false
|
|
75
|
+
if (options.publish === false) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return collectCommanderRawArgs(cmd).includes('--no-publish');
|
|
25
79
|
}
|
|
26
80
|
|
|
27
81
|
module.exports = {
|
|
28
|
-
cliOptsSkipPublish
|
|
82
|
+
cliOptsSkipPublish,
|
|
83
|
+
collectCommanderRawArgs,
|
|
84
|
+
isCommanderLike
|
|
29
85
|
};
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -219,9 +219,14 @@ function formatAzureError(errorMsg) {
|
|
|
219
219
|
if (errorMsg.includes('Docker build failed') || errorMsg.includes('Build failed: Docker')) {
|
|
220
220
|
return null;
|
|
221
221
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
const lowerMsg = String(errorMsg || '').toLowerCase();
|
|
223
|
+
const hasAcrDomain = /\b[a-z0-9.-]+\.azurecr\.io\b/i.test(errorMsg);
|
|
224
|
+
const hasAcrKeyword = /\bacr\b/.test(lowerMsg);
|
|
225
|
+
const hasAuthAndRegistryContext =
|
|
226
|
+
/\bauthenticate\b/.test(lowerMsg) &&
|
|
227
|
+
(/\bregistry\b/.test(lowerMsg) || /\bcontainer\b/.test(lowerMsg));
|
|
228
|
+
// Only match ACR-specific authentication errors, not general authentication failures.
|
|
229
|
+
if (hasAcrKeyword || hasAcrDomain || hasAuthAndRegistryContext) {
|
|
225
230
|
return [
|
|
226
231
|
' Azure Container Registry authentication failed.',
|
|
227
232
|
' Run: az acr login --name <registry-name>',
|
|
@@ -414,6 +419,9 @@ function isAuthenticationError(error) {
|
|
|
414
419
|
* @param {string} command - Command that failed
|
|
415
420
|
*/
|
|
416
421
|
function handleCommandError(error, command) {
|
|
422
|
+
if (error && error.compactGateAbortLogged === true) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
417
425
|
const errorMessages = formatError(error);
|
|
418
426
|
const skipDoctor = isMissingSecretsErrorMessage(error && error.message);
|
|
419
427
|
logError(command, errorMessages, { skipDoctor });
|
|
@@ -8,6 +8,27 @@
|
|
|
8
8
|
* @version 2.0.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* True when URL is http to a Docker Compose service DNS name (single hostname label).
|
|
13
|
+
* Example: http://miso-controller:3000/miso — used by builder-api in compose, not public HTTP.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} url
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
function isDockerComposeServiceHttpUrl(url) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(url);
|
|
21
|
+
if (parsed.protocol !== 'http:') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const host = parsed.hostname || '';
|
|
25
|
+
// Compose DNS: one label, no dots (miso-controller, dataplane, …)
|
|
26
|
+
return Boolean(host) && !host.includes('.') && host !== 'localhost';
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
/**
|
|
12
33
|
* Validates and sanitizes controller URL
|
|
13
34
|
* Enforces HTTPS-only communication for security
|
|
@@ -20,9 +41,13 @@ function validateControllerUrl(url) {
|
|
|
20
41
|
throw new Error('Controller URL is required and must be a string');
|
|
21
42
|
}
|
|
22
43
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
// HTTPS required in public networks; allow localhost and compose-service HTTP for local Docker
|
|
45
|
+
const allowedHttp =
|
|
46
|
+
url.startsWith('http://localhost') || isDockerComposeServiceHttpUrl(url);
|
|
47
|
+
if (!url.startsWith('https://') && !allowedHttp) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'Controller URL must use HTTPS (https://), http://localhost, or a Docker Compose service HTTP URL'
|
|
50
|
+
);
|
|
26
51
|
}
|
|
27
52
|
|
|
28
53
|
// Basic URL format validation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
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.
|
|
4
|
-
*
|
|
3
|
+
* Used by `aifabrix dev init` when the server certificate is self-signed.
|
|
4
|
+
* Requests keep TLS verification enabled by default.
|
|
5
5
|
*
|
|
6
6
|
* @fileoverview CA install utilities for development Builder Server
|
|
7
7
|
* @author AI Fabrix Team
|
|
@@ -163,7 +163,7 @@ function finalizeInstallCaResponse(res, absoluteUrl, chunks, resolve, reject) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
* GET absolute install-ca URL with
|
|
166
|
+
* GET absolute install-ca URL with TLS verification enabled; follow redirects.
|
|
167
167
|
* @param {string} absoluteUrl - Full https URL
|
|
168
168
|
* @param {number} redirectCount - Redirect depth
|
|
169
169
|
* @returns {Promise<Buffer>} CA PEM
|
|
@@ -179,10 +179,9 @@ function fetchInstallCaAbsolute(absoluteUrl, redirectCount) {
|
|
|
179
179
|
reject(new Error('install-ca requires https URL'));
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
|
-
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
183
182
|
const req = https.get(
|
|
184
183
|
absoluteUrl,
|
|
185
|
-
{
|
|
184
|
+
{},
|
|
186
185
|
(res) => {
|
|
187
186
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
188
187
|
res.destroy();
|
|
@@ -208,7 +207,7 @@ function fetchInstallCaAbsolute(absoluteUrl, redirectCount) {
|
|
|
208
207
|
|
|
209
208
|
/**
|
|
210
209
|
* Fetch CA PEM from Builder Server via GET {baseUrl}/install-ca.
|
|
211
|
-
* Uses
|
|
210
|
+
* Uses default TLS verification for this endpoint.
|
|
212
211
|
* @param {string} baseUrl - Builder Server base URL (no trailing slash)
|
|
213
212
|
* @returns {Promise<Buffer>} CA certificate PEM
|
|
214
213
|
*/
|
package/lib/utils/dev-config.js
CHANGED
|
@@ -44,8 +44,8 @@ function getDevPorts(developerId) {
|
|
|
44
44
|
throw new Error('Developer ID must be a positive number');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// Handle
|
|
48
|
-
if (
|
|
47
|
+
// Handle invalid numeric values
|
|
48
|
+
if (Number.isNaN(developerId)) {
|
|
49
49
|
throw new Error('Developer ID must be a positive number');
|
|
50
50
|
}
|
|
51
51
|
if (developerId < 0 || !Number.isInteger(developerId)) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Client-side sort for aifabrix dev list (--sort field / -field).
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ALLOWED_SORT_FIELDS = Object.freeze(['id', 'name', 'email', 'until']);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {unknown} id
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function normalizeIdKey(id) {
|
|
14
|
+
const trimmed = String(id ?? '').trim();
|
|
15
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
return String(parseInt(trimmed, 10)).padStart(2, '0');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {unknown} a
|
|
23
|
+
* @param {unknown} b
|
|
24
|
+
* @returns {number}
|
|
25
|
+
*/
|
|
26
|
+
function compareIds(a, b) {
|
|
27
|
+
const sa = String(a ?? '').trim();
|
|
28
|
+
const sb = String(b ?? '').trim();
|
|
29
|
+
if (/^\d+$/.test(sa) && /^\d+$/.test(sb)) {
|
|
30
|
+
return parseInt(sa, 10) - parseInt(sb, 10);
|
|
31
|
+
}
|
|
32
|
+
return normalizeIdKey(sa).localeCompare(normalizeIdKey(sb), undefined, { sensitivity: 'base' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} a
|
|
37
|
+
* @param {string} b
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
function compareText(a, b) {
|
|
41
|
+
return a.localeCompare(b, undefined, { sensitivity: 'base' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {Object} user
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function untilValue(user) {
|
|
49
|
+
const raw = user && user.certificateValidNotAfter;
|
|
50
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse --sort option. Default: name ascending.
|
|
55
|
+
* @param {string|undefined|null} sortOption
|
|
56
|
+
* @returns {{ field: string, descending: boolean }}
|
|
57
|
+
* @throws {Error} When field is empty or not allowed
|
|
58
|
+
*/
|
|
59
|
+
function parseSortSpec(sortOption) {
|
|
60
|
+
if (sortOption === undefined || sortOption === null) {
|
|
61
|
+
return { field: 'name', descending: false };
|
|
62
|
+
}
|
|
63
|
+
const trimmed = String(sortOption).trim();
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'Invalid --sort value "". Allowed: id, name, email, until (prefix - for descending).'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
let descending = false;
|
|
70
|
+
let fieldPart = trimmed;
|
|
71
|
+
if (fieldPart.startsWith('-')) {
|
|
72
|
+
descending = true;
|
|
73
|
+
fieldPart = fieldPart.slice(1).trim();
|
|
74
|
+
}
|
|
75
|
+
if (!fieldPart) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Invalid --sort value "-". Allowed: id, name, email, until (prefix - for descending).'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const field = fieldPart.toLowerCase();
|
|
81
|
+
if (!ALLOWED_SORT_FIELDS.includes(field)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Invalid --sort value "${trimmed}". Allowed: id, name, email, until (prefix - for descending).`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return { field, descending };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {Object} a
|
|
91
|
+
* @param {Object} b
|
|
92
|
+
* @param {string} field
|
|
93
|
+
* @returns {number}
|
|
94
|
+
*/
|
|
95
|
+
function compareByField(a, b, field) {
|
|
96
|
+
if (field === 'id') {
|
|
97
|
+
return compareIds(a.id, b.id);
|
|
98
|
+
}
|
|
99
|
+
if (field === 'name') {
|
|
100
|
+
return compareText(String(a.name || ''), String(b.name || ''));
|
|
101
|
+
}
|
|
102
|
+
if (field === 'email') {
|
|
103
|
+
return compareText(String(a.email || ''), String(b.email || ''));
|
|
104
|
+
}
|
|
105
|
+
const ua = untilValue(a);
|
|
106
|
+
const ub = untilValue(b);
|
|
107
|
+
if (!ua && !ub) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
if (!ua) {
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
if (!ub) {
|
|
114
|
+
return -1;
|
|
115
|
+
}
|
|
116
|
+
return compareText(ua, ub);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sort developer users without mutating the input array.
|
|
121
|
+
* @param {Object[]} users
|
|
122
|
+
* @param {string|undefined|null} sortOption
|
|
123
|
+
* @returns {Object[]}
|
|
124
|
+
*/
|
|
125
|
+
function sortDevUsers(users, sortOption) {
|
|
126
|
+
const { field, descending } = parseSortSpec(sortOption);
|
|
127
|
+
const list = Array.isArray(users) ? [...users] : [];
|
|
128
|
+
list.sort((a, b) => {
|
|
129
|
+
let cmp = compareByField(a, b, field);
|
|
130
|
+
if (descending) {
|
|
131
|
+
cmp = -cmp;
|
|
132
|
+
}
|
|
133
|
+
if (cmp !== 0) {
|
|
134
|
+
return cmp;
|
|
135
|
+
}
|
|
136
|
+
return compareIds(a.id, b.id);
|
|
137
|
+
});
|
|
138
|
+
return list;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
ALLOWED_SORT_FIELDS,
|
|
143
|
+
parseSortSpec,
|
|
144
|
+
sortDevUsers
|
|
145
|
+
};
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const https = require('https');
|
|
12
11
|
const config = require('../core/config');
|
|
13
12
|
const { isLocalhostPublicSchemeHostname } = require('./localhost-public-host');
|
|
14
13
|
|
|
@@ -68,65 +67,6 @@ async function shouldAllowInsecureDevPlatformTls(url) {
|
|
|
68
67
|
}
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
/**
|
|
72
|
-
* Minimal fetch for self-signed dev platform HTTPS (Node global fetch cannot set rejectUnauthorized).
|
|
73
|
-
*
|
|
74
|
-
* @param {string} url
|
|
75
|
-
* @param {Object} options
|
|
76
|
-
* @returns {Promise<{ status: number, statusText: string, headers: { get: Function }, text: Function, json: Function }>}
|
|
77
|
-
*/
|
|
78
|
-
function nodeHttpsFetchInsecure(url, options = {}) {
|
|
79
|
-
return new Promise((resolve, reject) => {
|
|
80
|
-
const u = new URL(url);
|
|
81
|
-
const method = String(options.method || 'GET').toUpperCase();
|
|
82
|
-
const headers = { ...(options.headers || {}) };
|
|
83
|
-
const body = options.body;
|
|
84
|
-
const reqOpts = {
|
|
85
|
-
hostname: u.hostname,
|
|
86
|
-
port: u.port || 443,
|
|
87
|
-
path: `${u.pathname}${u.search}`,
|
|
88
|
-
method,
|
|
89
|
-
headers,
|
|
90
|
-
rejectUnauthorized: false
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const req = https.request(reqOpts, (res) => {
|
|
94
|
-
const chunks = [];
|
|
95
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
96
|
-
res.on('end', () => {
|
|
97
|
-
const raw = Buffer.concat(chunks).toString('utf8');
|
|
98
|
-
const status = res.statusCode || 0;
|
|
99
|
-
resolve({
|
|
100
|
-
ok: status >= 200 && status < 300,
|
|
101
|
-
status,
|
|
102
|
-
statusText: res.statusMessage || '',
|
|
103
|
-
headers: {
|
|
104
|
-
get: (name) => res.headers[String(name || '').toLowerCase()] || null
|
|
105
|
-
},
|
|
106
|
-
text: async() => raw,
|
|
107
|
-
json: async() => JSON.parse(raw)
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
req.on('error', reject);
|
|
113
|
-
if (options.signal) {
|
|
114
|
-
const onAbort = () => {
|
|
115
|
-
req.destroy(new Error('The operation was aborted'));
|
|
116
|
-
};
|
|
117
|
-
if (options.signal.aborted) {
|
|
118
|
-
onAbort();
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
122
|
-
}
|
|
123
|
-
if (body !== undefined && body !== null) {
|
|
124
|
-
req.write(typeof body === 'string' ? body : String(body));
|
|
125
|
-
}
|
|
126
|
-
req.end();
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
70
|
/**
|
|
131
71
|
* @async
|
|
132
72
|
* @param {string} url
|
|
@@ -134,9 +74,6 @@ function nodeHttpsFetchInsecure(url, options = {}) {
|
|
|
134
74
|
* @returns {Promise<Response>}
|
|
135
75
|
*/
|
|
136
76
|
async function fetchWithOptionalDevPlatformTls(url, options = {}) {
|
|
137
|
-
if (await shouldAllowInsecureDevPlatformTls(url)) {
|
|
138
|
-
return nodeHttpsFetchInsecure(url, options);
|
|
139
|
-
}
|
|
140
77
|
return fetch(url, options);
|
|
141
78
|
}
|
|
142
79
|
|