@aifabrix/builder 2.51.1 → 2.52.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/jest.isolated-projects.js +25 -2
- package/jest.projects.js +4 -1
- package/lib/api/datasources-extended.api.js +8 -8
- package/lib/api/keycloak.api.js +92 -0
- package/lib/api/token-providers.api.js +77 -0
- package/lib/api/types/keycloak.types.js +23 -0
- package/lib/api/types/token-providers.types.js +65 -0
- package/lib/app/down.js +16 -1
- package/lib/app/run-container-start.js +4 -0
- package/lib/app/run-deploy-job.js +135 -27
- package/lib/app/run-env-compose.js +32 -0
- package/lib/app/run-env-recovery.js +35 -0
- package/lib/app/run-helpers.js +5 -6
- package/lib/app/run.js +11 -3
- package/lib/channels/add-channel.js +126 -19
- package/lib/channels/approval-guides/chatgpt.js +12 -9
- package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
- package/lib/channels/channel-add-ensure.js +128 -0
- package/lib/channels/channel-artifact-generator.js +11 -1
- package/lib/channels/channel-branding.js +94 -0
- package/lib/channels/channel-credential.js +8 -0
- package/lib/channels/channel-discovery.js +330 -0
- package/lib/channels/channel-external-system.js +1 -1
- package/lib/channels/channel-runtime-resolver.js +103 -14
- package/lib/channels/channel-status-display.js +7 -1
- package/lib/channels/channel-targets.js +18 -1
- package/lib/channels/chatgpt-runtime-contract.js +1 -1
- package/lib/channels/microsoft-copilot-channel-add.js +138 -0
- package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
- package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
- package/lib/channels/microsoft-copilot-icons.js +74 -0
- package/lib/channels/microsoft-copilot-limits.js +84 -0
- package/lib/channels/microsoft-copilot-package.js +130 -49
- package/lib/channels/microsoft-copilot-validate.js +197 -0
- package/lib/cli/index.js +2 -0
- package/lib/cli/infra-guided-footers.js +1 -1
- package/lib/cli/infra-guided-public-url-warn.js +55 -0
- package/lib/cli/infra-guided.js +78 -62
- package/lib/cli/setup-app.help.js +4 -0
- package/lib/cli/setup-app.test-commands.js +59 -28
- package/lib/cli/setup-external-system.js +50 -12
- package/lib/cli/setup-infra.js +31 -4
- package/lib/commands/channel-add-setup.js +35 -10
- package/lib/commands/channel.js +27 -12
- package/lib/commands/deploy-gate-orchestrator.js +3 -2
- package/lib/commands/governance-verify-external.js +2 -5
- package/lib/commands/integration-definition.js +158 -0
- package/lib/commands/lifecycle-external.js +14 -7
- package/lib/commands/repair-auth-apply.js +19 -5
- package/lib/commands/repair-datasource-sync-operations.js +4 -1
- package/lib/commands/role-assistant-test-output.js +200 -0
- package/lib/commands/role-assistant.js +131 -114
- package/lib/commands/test-governance-external.js +2 -5
- package/lib/commands/up-miso.js +61 -24
- package/lib/commands/upload-data-sync-finalize.js +20 -3
- package/lib/commands/verify-governance-external.js +2 -5
- package/lib/commands/verify-operations-external.js +2 -5
- package/lib/commands/verify-operations-step-plan.js +31 -0
- package/lib/commands/verify-trust-command-action.js +2 -5
- package/lib/datasource/capability/capability-resolve.js +15 -5
- package/lib/datasource/capability/copy-operations.js +14 -4
- package/lib/datasource/datasource-identifier.js +4 -3
- package/lib/datasource/integration-context.js +3 -3
- package/lib/datasource/resolve-app.js +90 -8
- package/lib/datasource/test-e2e.js +2 -2
- package/lib/datasource/unified-validation-run-body.js +15 -1
- package/lib/datasource/unified-validation-run-resolve.js +2 -2
- package/lib/external-system/deploy-helpers.js +9 -5
- package/lib/external-system/deploy.js +2 -1
- package/lib/external-system/test-system-level.js +21 -3
- package/lib/external-system/test.js +4 -4
- package/lib/generator/builders.js +9 -15
- package/lib/infrastructure/index.js +19 -4
- package/lib/integration-definition/apply.js +478 -0
- package/lib/integration-definition/canonical-json.js +51 -0
- package/lib/integration-definition/emit-suite-bid.js +94 -0
- package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
- package/lib/integration-definition/enrich-openapi-operations.js +434 -0
- package/lib/integration-definition/from-discovery.js +339 -0
- package/lib/integration-definition/hash.js +37 -0
- package/lib/integration-definition/index.js +37 -0
- package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
- package/lib/integration-definition/resource-type-catalog.js +59 -0
- package/lib/integration-definition/system-kind-enum.js +40 -0
- package/lib/integration-definition/validate.js +148 -0
- package/lib/programmatic/builder-api-request.js +5 -1
- package/lib/programmatic/builder-help-governance.js +3 -2
- package/lib/programmatic/capability-registry.js +19 -0
- package/lib/programmatic/openapi-component-schemas.js +6 -1
- package/lib/programmatic/openapi-descriptions.js +40 -0
- package/lib/programmatic/openapi-envelope-examples.js +269 -0
- package/lib/programmatic/openapi-operation-bindings.js +45 -0
- package/lib/programmatic/openapi-request-schemas.js +14 -0
- package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
- package/lib/programmatic/openapi-result-schemas.js +46 -99
- package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
- package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
- package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
- package/lib/programmatic/openapi-schema-helpers.js +9 -9
- package/lib/programmatic/openapi-spec.js +11 -1
- package/lib/programmatic/orchestration-openapi-meta.js +10 -0
- package/lib/programmatic/route-handler-map.js +29 -1
- package/lib/programmatic/run-channel-package.js +236 -0
- package/lib/programmatic/run-integration-definition.js +69 -0
- package/lib/role-assistant/knowledge-markdown-frontmatter.js +75 -0
- package/lib/role-assistant/knowledge-sync-logging.js +58 -0
- package/lib/role-assistant/knowledge-sync.js +104 -89
- package/lib/role-assistant/test-cases-bid.js +155 -0
- package/lib/role-assistant/test-cases-evaluate.js +9 -1
- package/lib/role-assistant/test-cases-honesty.js +24 -10
- package/lib/role-assistant/test-cases.js +28 -0
- package/lib/role-assistant/test-runner-bid-apply.js +122 -0
- package/lib/role-assistant/test-runner-bid-document.js +278 -0
- package/lib/role-assistant/test-runner-workhub-answers.js +289 -46
- package/lib/role-assistant/test-runner-workhub.js +121 -25
- package/lib/role-assistant/test-suite.js +119 -0
- package/lib/schema/application-schema.json +64 -32
- package/lib/schema/external-datasource.schema.json +103 -17
- package/lib/schema/external-system.schema.json +54 -3
- package/lib/schema/infra.parameter.yaml +0 -20
- package/lib/schema/integration-definition.schema.json +195 -0
- package/lib/utils/authentication-shape.js +6 -0
- package/lib/utils/certify-external-workspace.js +40 -0
- package/lib/utils/docker-build.js +3 -3
- package/lib/utils/docker-run-ephemeral.js +77 -17
- package/lib/utils/health-check-public-warn.js +31 -13
- package/lib/utils/health-check.js +11 -1
- package/lib/utils/help-builder.js +1 -0
- package/lib/utils/paths.js +13 -15
- package/lib/utils/promote-dev-docker-image.js +97 -18
- package/lib/utils/role-assistant-paths.js +76 -14
- package/lib/utils/transient-developer-id.js +28 -0
- package/lib/utils/upload-sync-options.js +7 -4
- package/lib/validation/reserved-system-key-prefixes.js +56 -0
- package/lib/validation/validate.js +54 -19
- package/package.json +1 -1
- package/scripts/pnpm/af.mjs +24 -9
- package/scripts/pnpm/help.mjs +1 -1
- package/templates/applications/builder-api/application.yaml +3 -3
- package/templates/applications/dataplane/application.yaml +5 -5
- package/templates/applications/dataplane/env.template +8 -7
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/application.yaml +2 -2
- package/templates/applications/keycloak/env.template +5 -2
- package/templates/applications/miso-controller/application.yaml +25 -11
- package/templates/applications/miso-controller/env.template +24 -5
- package/templates/channels/README.md +11 -1
- package/templates/channels/brand/color-192.png +0 -0
- package/templates/channels/brand/dark/128.png +0 -0
- package/templates/channels/brand/dark/192.png +0 -0
- package/templates/channels/brand/dark/256.png +0 -0
- package/templates/channels/brand/dark/32.png +0 -0
- package/templates/channels/brand/dark/512.png +0 -0
- package/templates/channels/brand/dark/64.png +0 -0
- package/templates/channels/brand/light/128.png +0 -0
- package/templates/channels/brand/light/192.png +0 -0
- package/templates/channels/brand/light/256.png +0 -0
- package/templates/channels/brand/light/32.png +0 -0
- package/templates/channels/brand/light/512.png +0 -0
- package/templates/channels/brand/light/64.png +0 -0
- package/templates/channels/brand/mono-white/128.png +0 -0
- package/templates/channels/brand/mono-white/192.png +0 -0
- package/templates/channels/brand/mono-white/256.png +0 -0
- package/templates/channels/brand/mono-white/32.png +0 -0
- package/templates/channels/brand/mono-white/512.png +0 -0
- package/templates/channels/brand/mono-white/64.png +0 -0
- package/templates/channels/brand/outline-32.png +0 -0
- package/templates/channels/chatgpt/install.template.md.hbs +76 -36
- package/templates/channels/chatgpt/integration/README.md +4 -4
- package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
- package/templates/channels/chatgpt/manifest.template.json.hbs +3 -3
- package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
- package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
- package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
- package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
- package/templates/channels/microsoft-copilot/integration/README.md +2 -3
- package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
- package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
- package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
- package/templates/channels/slack/install.template.md.hbs +6 -1
- package/templates/channels/slack/integration/README.md +2 -4
- package/templates/channels/teams-bot/install.template.md.hbs +4 -0
- package/templates/channels/teams-bot/integration/README.md +3 -5
- package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
- package/templates/python/docker-compose.hbs +10 -0
- package/templates/typescript/docker-compose.hbs +10 -0
- /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Copilot / Teams package string limits.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Truncate and normalize Copilot package display fields to schema limits
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const TEAMS_NAME_SHORT_MAX = 30;
|
|
12
|
+
const TEAMS_NAME_FULL_MAX = 100;
|
|
13
|
+
const TEAMS_DESCRIPTION_SHORT_MAX = 80;
|
|
14
|
+
const TEAMS_DESCRIPTION_FULL_MAX = 4000;
|
|
15
|
+
const PLUGIN_NAME_FOR_HUMAN_MAX = 20;
|
|
16
|
+
const PLUGIN_DESCRIPTION_FOR_HUMAN_MAX = 100;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} text
|
|
20
|
+
* @param {number} max
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function truncateForSchema(text, max) {
|
|
24
|
+
const value = String(text ?? '').trim();
|
|
25
|
+
if (!value) return '';
|
|
26
|
+
if (value.length <= max) return value;
|
|
27
|
+
if (max <= 1) return value.slice(0, max);
|
|
28
|
+
return `${value.slice(0, max - 1)}…`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Plugin namespace: lowercase alphanumeric/hyphen only (plugin schema ^[A-Za-z0-9-]+$).
|
|
33
|
+
* @param {string} baseName
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function buildPluginNamespace(baseName) {
|
|
37
|
+
const raw = String(baseName ?? 'aifabrix')
|
|
38
|
+
.trim()
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
41
|
+
.replace(/-+/g, '-')
|
|
42
|
+
.replace(/^-|-$/g, '');
|
|
43
|
+
const ns = raw || 'aifabrix';
|
|
44
|
+
return truncateForSchema(ns, 32).replace(/[^a-z0-9-]/g, '') || 'aifabrix';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {Object} runtimeValues
|
|
49
|
+
* @returns {Object}
|
|
50
|
+
*/
|
|
51
|
+
function applyMicrosoftCopilotDisplayLimits(runtimeValues) {
|
|
52
|
+
const appDescription = String(runtimeValues.appDescription || '').trim();
|
|
53
|
+
const baseName = String(runtimeValues.baseName || runtimeValues.appName || 'Assistant').trim();
|
|
54
|
+
const appName = String(runtimeValues.appName || baseName).trim();
|
|
55
|
+
runtimeValues.shortName = truncateForSchema(baseName, TEAMS_NAME_SHORT_MAX);
|
|
56
|
+
runtimeValues.fullName = truncateForSchema(appName, TEAMS_NAME_FULL_MAX);
|
|
57
|
+
runtimeValues.shortDescription = truncateForSchema(
|
|
58
|
+
appDescription || 'Enterprise AI assistant via AI Fabrix Runtime.',
|
|
59
|
+
TEAMS_DESCRIPTION_SHORT_MAX
|
|
60
|
+
);
|
|
61
|
+
runtimeValues.fullDescription = truncateForSchema(
|
|
62
|
+
appDescription || runtimeValues.shortDescription,
|
|
63
|
+
TEAMS_DESCRIPTION_FULL_MAX
|
|
64
|
+
);
|
|
65
|
+
runtimeValues.pluginNameForHuman = truncateForSchema(baseName, PLUGIN_NAME_FOR_HUMAN_MAX);
|
|
66
|
+
runtimeValues.pluginDescriptionForHuman = truncateForSchema(
|
|
67
|
+
appDescription || runtimeValues.shortDescription,
|
|
68
|
+
PLUGIN_DESCRIPTION_FOR_HUMAN_MAX
|
|
69
|
+
);
|
|
70
|
+
runtimeValues.pluginNamespace = buildPluginNamespace(baseName);
|
|
71
|
+
return runtimeValues;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
TEAMS_NAME_SHORT_MAX,
|
|
76
|
+
TEAMS_NAME_FULL_MAX,
|
|
77
|
+
TEAMS_DESCRIPTION_SHORT_MAX,
|
|
78
|
+
TEAMS_DESCRIPTION_FULL_MAX,
|
|
79
|
+
PLUGIN_NAME_FOR_HUMAN_MAX,
|
|
80
|
+
PLUGIN_DESCRIPTION_FOR_HUMAN_MAX,
|
|
81
|
+
truncateForSchema,
|
|
82
|
+
buildPluginNamespace,
|
|
83
|
+
applyMicrosoftCopilotDisplayLimits
|
|
84
|
+
};
|
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const crypto = require('crypto');
|
|
12
|
-
const fs = require('fs');
|
|
12
|
+
const fs = require('../internal/fs-real-sync');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const AdmZip = require('adm-zip');
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
const { getMicrosoftCopilotIcons } = require('./microsoft-copilot-icons');
|
|
16
|
+
const { assertValidMicrosoftCopilotPackage } = require('./microsoft-copilot-validate');
|
|
17
|
+
const { applyMicrosoftCopilotDisplayLimits } = require('./microsoft-copilot-limits');
|
|
18
|
+
const {
|
|
19
|
+
assertCopilotPackageOauthPluginVaultReference
|
|
20
|
+
} = require('./microsoft-copilot-entra-sso');
|
|
21
|
+
const { renderCopilotChannelTemplate } = require('./microsoft-copilot-entra-sso-form');
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* @param {string} outputPath
|
|
@@ -37,16 +41,20 @@ function buildCopilotOutputPaths(outputPath, runtimeValues) {
|
|
|
37
41
|
zipFileName,
|
|
38
42
|
zipPath: path.join(buildDir, zipFileName),
|
|
39
43
|
validationPath: path.join(installDir, 'validation.json'),
|
|
40
|
-
installPath: path.join(
|
|
44
|
+
installPath: path.join(rootDir, 'INSTALL.md'),
|
|
45
|
+
installReadmePath: path.join(installDir, 'README.md')
|
|
41
46
|
};
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/**
|
|
50
|
+
* @param {Object} [runtimeValues]
|
|
45
51
|
* @returns {{ color: Buffer, outline: Buffer }}
|
|
46
52
|
*/
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
function resolvePackageIcons(runtimeValues) {
|
|
54
|
+
return getMicrosoftCopilotIcons({
|
|
55
|
+
colorPath: runtimeValues?.iconColorPath,
|
|
56
|
+
outlinePath: runtimeValues?.iconOutlinePath
|
|
57
|
+
});
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
/**
|
|
@@ -67,7 +75,8 @@ function renderCopilotTemplates(runtimeValues, renderTemplateFile, templatesDir)
|
|
|
67
75
|
runtimeValues
|
|
68
76
|
)
|
|
69
77
|
};
|
|
70
|
-
|
|
78
|
+
// INSTALL prose + Entra SSO form table live in templates (partial entraSsoRegistration).
|
|
79
|
+
const installText = renderCopilotChannelTemplate('install.template.md.hbs', runtimeValues);
|
|
71
80
|
return { sourceFiles, installText };
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -112,6 +121,9 @@ function buildCopilotValidation(runtimeValues, paths, sourceFileNames) {
|
|
|
112
121
|
runtimeMcpUrl: runtimeValues.mcpUrl,
|
|
113
122
|
runtimeTransport: runtimeValues.runtimeTransport,
|
|
114
123
|
expectedLayerBToolNames: runtimeValues.layerBToolNames || [],
|
|
124
|
+
governedToolNames: runtimeValues.layerBToolNames || [],
|
|
125
|
+
installReady: runtimeValues.installReady === true,
|
|
126
|
+
blocker: runtimeValues.discoveryBlocker || null,
|
|
115
127
|
boundaries: {
|
|
116
128
|
onePackagePerEnvironment: true,
|
|
117
129
|
topLevelVendorToolsExposed: false,
|
|
@@ -142,6 +154,65 @@ function computeCopilotArtifactHash(parts) {
|
|
|
142
154
|
return hash.digest('hex');
|
|
143
155
|
}
|
|
144
156
|
|
|
157
|
+
/**
|
|
158
|
+
* @param {Object} runtimeValues
|
|
159
|
+
* @returns {Object}
|
|
160
|
+
*/
|
|
161
|
+
function buildCopilotSummaryRuntime(runtimeValues) {
|
|
162
|
+
return {
|
|
163
|
+
runtimeSystemKey: runtimeValues.runtimeSystemKey,
|
|
164
|
+
remoteMcpUrl: runtimeValues.mcpUrl,
|
|
165
|
+
transport: runtimeValues.runtimeTransport,
|
|
166
|
+
assistantCatalogUrl: runtimeValues.assistantCatalogUrl,
|
|
167
|
+
roleAssistantResolution: 'dynamic-runtime',
|
|
168
|
+
layerBToolNames: runtimeValues.layerBToolNames || [],
|
|
169
|
+
governedToolNames: runtimeValues.layerBToolNames || []
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {Object} runtimeValues
|
|
175
|
+
* @returns {Object}
|
|
176
|
+
*/
|
|
177
|
+
function buildCopilotSummaryAuthentication(runtimeValues) {
|
|
178
|
+
const discovery = runtimeValues.discoveryAuthentication || null;
|
|
179
|
+
const serialized = discovery ? JSON.stringify(discovery) : '';
|
|
180
|
+
if (/authorizationServerMetadataUrl":"[^"]+"/i.test(serialized)) {
|
|
181
|
+
throw new Error('Keycloak authorizationServerMetadata must not appear in Copilot package summary');
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
mode: runtimeValues.runtimeAuthMode,
|
|
185
|
+
pluginAuthType: 'OAuthPluginVault',
|
|
186
|
+
oauthPluginVaultReferenceId: runtimeValues.oauthPluginVaultReferenceId || null,
|
|
187
|
+
recommendedCapability: discovery?.recommendedCapability || null,
|
|
188
|
+
entraIssuer: runtimeValues.entraIssuer || discovery?.issuer || null,
|
|
189
|
+
entraEnterpriseAppId: runtimeValues.entraEnterpriseAppId || null,
|
|
190
|
+
entraTenantId: runtimeValues.entraTenantId || null,
|
|
191
|
+
entraSsoClientId: runtimeValues.entraSsoClientId || null,
|
|
192
|
+
scopes: runtimeValues.scopes || [],
|
|
193
|
+
discovery,
|
|
194
|
+
note:
|
|
195
|
+
'Microsoft Entra SSO via OAuthPluginVault (Teams auth-config reference_id); ' +
|
|
196
|
+
'miso-controller performs exchange — package carries public specs only; no Fabrix OAuth secrets'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {Object} runtimeValues
|
|
202
|
+
* @param {Object} paths
|
|
203
|
+
* @returns {Object}
|
|
204
|
+
*/
|
|
205
|
+
function buildCopilotSummaryPackage(runtimeValues, paths) {
|
|
206
|
+
return {
|
|
207
|
+
appId: runtimeValues.packageAppId,
|
|
208
|
+
version: runtimeValues.packageVersion,
|
|
209
|
+
sourceFiles: ['manifest.json', 'declarativeAgent.json', 'ai-plugin.json', 'color.png', 'outline.png'],
|
|
210
|
+
zipFileName: paths.zipFileName,
|
|
211
|
+
installGuide: 'INSTALL.md',
|
|
212
|
+
validationFile: path.relative(paths.rootDir, paths.validationPath)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
145
216
|
/**
|
|
146
217
|
* @param {Object} runtimeValues
|
|
147
218
|
* @param {Object} paths
|
|
@@ -156,27 +227,16 @@ function buildCopilotSummary(runtimeValues, paths, validation, artifactHash) {
|
|
|
156
227
|
baseName: runtimeValues.baseName,
|
|
157
228
|
description: runtimeValues.appDescription,
|
|
158
229
|
environment: runtimeValues.environment,
|
|
159
|
-
runtime:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
credentialKey: runtimeValues.credentialKey,
|
|
170
|
-
credentialPortalUrl: runtimeValues.credentialPortalUrl,
|
|
171
|
-
scopes: runtimeValues.scopes || []
|
|
172
|
-
},
|
|
173
|
-
package: {
|
|
174
|
-
appId: runtimeValues.packageAppId,
|
|
175
|
-
version: runtimeValues.packageVersion,
|
|
176
|
-
sourceFiles: ['manifest.json', 'declarativeAgent.json', 'ai-plugin.json', 'color.png', 'outline.png'],
|
|
177
|
-
zipFileName: paths.zipFileName,
|
|
178
|
-
installGuide: path.relative(paths.rootDir, paths.installPath),
|
|
179
|
-
validationFile: path.relative(paths.rootDir, paths.validationPath)
|
|
230
|
+
runtime: buildCopilotSummaryRuntime(runtimeValues),
|
|
231
|
+
authentication: buildCopilotSummaryAuthentication(runtimeValues),
|
|
232
|
+
package: buildCopilotSummaryPackage(runtimeValues, paths),
|
|
233
|
+
installReady: runtimeValues.installReady === true,
|
|
234
|
+
blocker: runtimeValues.discoveryBlocker || null,
|
|
235
|
+
links: {
|
|
236
|
+
readme: paths.installPath,
|
|
237
|
+
zip: paths.zipPath,
|
|
238
|
+
filesRoot: paths.sourceDir,
|
|
239
|
+
validation: paths.validationPath
|
|
180
240
|
},
|
|
181
241
|
probeTiers: {
|
|
182
242
|
builderValidated: true,
|
|
@@ -211,24 +271,21 @@ function writeCopilotArtifacts(params) {
|
|
|
211
271
|
fs.writeFileSync(path.join(paths.sourceDir, 'outline.png'), icons.outline);
|
|
212
272
|
fs.writeFileSync(paths.zipPath, zipBuffer);
|
|
213
273
|
fs.writeFileSync(paths.installPath, installText, 'utf8');
|
|
274
|
+
if (paths.installReadmePath) {
|
|
275
|
+
fs.writeFileSync(paths.installReadmePath, installText, 'utf8');
|
|
276
|
+
}
|
|
214
277
|
fs.writeFileSync(paths.validationPath, validationText, 'utf8');
|
|
215
278
|
fs.writeFileSync(paths.summaryPath, summaryText, 'utf8');
|
|
216
279
|
}
|
|
217
280
|
|
|
218
281
|
/**
|
|
219
282
|
* @param {Object} params
|
|
220
|
-
* @param {Object}
|
|
221
|
-
* @param {
|
|
222
|
-
* @param {
|
|
223
|
-
* @
|
|
224
|
-
* @param {(content: string) => void} params.assertNoSecretsInArtifact
|
|
225
|
-
* @param {(templatePath: string, context: Object) => string} params.renderTemplateFile
|
|
226
|
-
* @returns {{ outputPath: string, artifactHash: string, content: string, installPath: string }}
|
|
283
|
+
* @param {Object} runtimeValues
|
|
284
|
+
* @param {Object} paths
|
|
285
|
+
* @param {{ color: Buffer, outline: Buffer }} icons
|
|
286
|
+
* @returns {{ summary: Object, summaryText: string, zipBuffer: Buffer, sourceFiles: Object, installText: string, validationText: string }}
|
|
227
287
|
*/
|
|
228
|
-
function
|
|
229
|
-
const runtimeValues = params.runtimeValues || {};
|
|
230
|
-
const paths = buildCopilotOutputPaths(params.outputPath, runtimeValues);
|
|
231
|
-
const icons = getPlaceholderIcons();
|
|
288
|
+
function buildCopilotPackagePayload(params, runtimeValues, paths, icons) {
|
|
232
289
|
const { sourceFiles, installText } = renderCopilotTemplates(
|
|
233
290
|
runtimeValues,
|
|
234
291
|
params.renderTemplateFile,
|
|
@@ -236,6 +293,7 @@ function generateMicrosoftCopilotPackage(params) {
|
|
|
236
293
|
);
|
|
237
294
|
assertNoSecretsInSourceFiles(sourceFiles, params.assertNoSecretsInArtifact);
|
|
238
295
|
params.assertNoSecretsInArtifact(installText);
|
|
296
|
+
assertValidMicrosoftCopilotPackage(sourceFiles, icons);
|
|
239
297
|
const zipBuffer = buildCopilotZipBuffer(sourceFiles, icons);
|
|
240
298
|
const validation = buildCopilotValidation(runtimeValues, paths, Object.keys(sourceFiles));
|
|
241
299
|
const validationText = JSON.stringify(validation, null, 2);
|
|
@@ -250,22 +308,45 @@ function generateMicrosoftCopilotPackage(params) {
|
|
|
250
308
|
const summary = buildCopilotSummary(runtimeValues, paths, validation, artifactHash);
|
|
251
309
|
const summaryText = JSON.stringify(summary, null, 2);
|
|
252
310
|
params.assertNoSecretsInArtifact(summaryText);
|
|
311
|
+
return { summary, summaryText, zipBuffer, sourceFiles, installText, validationText };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @param {Object} params
|
|
316
|
+
* @param {Object} params.runtimeValues
|
|
317
|
+
* @param {string} params.templatesDir
|
|
318
|
+
* @param {string} params.outputPath
|
|
319
|
+
* @param {boolean} [params.dryRun]
|
|
320
|
+
* @param {(content: string) => void} params.assertNoSecretsInArtifact
|
|
321
|
+
* @param {(templatePath: string, context: Object) => string} params.renderTemplateFile
|
|
322
|
+
* @returns {{ outputPath: string, artifactHash: string, content: string, installPath: string }}
|
|
323
|
+
*/
|
|
324
|
+
function generateMicrosoftCopilotPackage(params) {
|
|
325
|
+
const runtimeValues = applyMicrosoftCopilotDisplayLimits({ ...(params.runtimeValues || {}) });
|
|
326
|
+
assertCopilotPackageOauthPluginVaultReference(runtimeValues);
|
|
327
|
+
const paths = buildCopilotOutputPaths(params.outputPath, runtimeValues);
|
|
328
|
+
const icons = resolvePackageIcons(runtimeValues);
|
|
329
|
+
const payload = buildCopilotPackagePayload(params, runtimeValues, paths, icons);
|
|
253
330
|
if (!params.dryRun) {
|
|
254
331
|
writeCopilotArtifacts({
|
|
255
332
|
paths,
|
|
256
|
-
sourceFiles,
|
|
333
|
+
sourceFiles: payload.sourceFiles,
|
|
257
334
|
icons,
|
|
258
|
-
zipBuffer,
|
|
259
|
-
installText,
|
|
260
|
-
validationText,
|
|
261
|
-
summaryText
|
|
335
|
+
zipBuffer: payload.zipBuffer,
|
|
336
|
+
installText: payload.installText,
|
|
337
|
+
validationText: payload.validationText,
|
|
338
|
+
summaryText: payload.summaryText
|
|
262
339
|
});
|
|
263
340
|
}
|
|
264
341
|
return {
|
|
265
342
|
outputPath: paths.summaryPath,
|
|
266
|
-
artifactHash,
|
|
267
|
-
content: summaryText,
|
|
268
|
-
installPath: paths.installPath
|
|
343
|
+
artifactHash: payload.summary.artifact.hash,
|
|
344
|
+
content: payload.summaryText,
|
|
345
|
+
installPath: paths.installPath,
|
|
346
|
+
zipPath: paths.zipPath,
|
|
347
|
+
links: payload.summary.links,
|
|
348
|
+
installReady: payload.summary.installReady === true,
|
|
349
|
+
blocker: payload.summary.blocker || null
|
|
269
350
|
};
|
|
270
351
|
}
|
|
271
352
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate Microsoft Copilot package source files before ZIP.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Schema-oriented checks for Teams + declarative agent + AI plugin manifests
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
TEAMS_NAME_SHORT_MAX,
|
|
13
|
+
TEAMS_NAME_FULL_MAX,
|
|
14
|
+
TEAMS_DESCRIPTION_SHORT_MAX,
|
|
15
|
+
TEAMS_DESCRIPTION_FULL_MAX,
|
|
16
|
+
PLUGIN_NAME_FOR_HUMAN_MAX,
|
|
17
|
+
PLUGIN_DESCRIPTION_FOR_HUMAN_MAX
|
|
18
|
+
} = require('./microsoft-copilot-limits');
|
|
19
|
+
const { readPngDimensions } = require('./microsoft-copilot-icons');
|
|
20
|
+
|
|
21
|
+
const ALLOWED_AUTH_TYPES = new Set(['None', 'OAuthPluginVault', 'ApiKeyPluginVault']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {unknown} value
|
|
25
|
+
* @param {string} label
|
|
26
|
+
* @param {number} max
|
|
27
|
+
* @param {string[]} errors
|
|
28
|
+
*/
|
|
29
|
+
function assertMaxLength(value, label, max, errors) {
|
|
30
|
+
if (typeof value !== 'string') {
|
|
31
|
+
errors.push(`${label} must be a string`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (value.length > max) {
|
|
35
|
+
errors.push(`${label} exceeds ${max} characters (got ${value.length})`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Record<string, string>} sourceFiles
|
|
41
|
+
* @returns {{ manifest: Object|null, agent: Object|null, plugin: Object|null, errors: string[] }}
|
|
42
|
+
*/
|
|
43
|
+
function parsePackageJsonFiles(sourceFiles) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
const parsed = { manifest: null, agent: null, plugin: null };
|
|
46
|
+
try {
|
|
47
|
+
parsed.manifest = JSON.parse(sourceFiles['manifest.json'] || '');
|
|
48
|
+
} catch {
|
|
49
|
+
errors.push('manifest.json is not valid JSON');
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
parsed.agent = JSON.parse(sourceFiles['declarativeAgent.json'] || '');
|
|
53
|
+
} catch {
|
|
54
|
+
errors.push('declarativeAgent.json is not valid JSON');
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
parsed.plugin = JSON.parse(sourceFiles['ai-plugin.json'] || '');
|
|
58
|
+
} catch {
|
|
59
|
+
errors.push('ai-plugin.json is not valid JSON');
|
|
60
|
+
}
|
|
61
|
+
return { ...parsed, errors };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {Object|null} manifest
|
|
66
|
+
* @param {string[]} errors
|
|
67
|
+
*/
|
|
68
|
+
function validateTeamsManifest(manifest, errors) {
|
|
69
|
+
if (!manifest) return;
|
|
70
|
+
assertMaxLength(manifest?.name?.short, 'manifest.name.short', TEAMS_NAME_SHORT_MAX, errors);
|
|
71
|
+
assertMaxLength(manifest?.name?.full, 'manifest.name.full', TEAMS_NAME_FULL_MAX, errors);
|
|
72
|
+
assertMaxLength(
|
|
73
|
+
manifest?.description?.short,
|
|
74
|
+
'manifest.description.short',
|
|
75
|
+
TEAMS_DESCRIPTION_SHORT_MAX,
|
|
76
|
+
errors
|
|
77
|
+
);
|
|
78
|
+
assertMaxLength(
|
|
79
|
+
manifest?.description?.full,
|
|
80
|
+
'manifest.description.full',
|
|
81
|
+
TEAMS_DESCRIPTION_FULL_MAX,
|
|
82
|
+
errors
|
|
83
|
+
);
|
|
84
|
+
if (!manifest.copilotAgents?.declarativeAgents?.length) {
|
|
85
|
+
errors.push('manifest.copilotAgents.declarativeAgents is required');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {Object|null} agent
|
|
91
|
+
* @param {string[]} errors
|
|
92
|
+
*/
|
|
93
|
+
function validateDeclarativeAgent(agent, errors) {
|
|
94
|
+
if (!agent) return;
|
|
95
|
+
if (agent.version !== 'v1.4') {
|
|
96
|
+
errors.push('declarativeAgent.version must be "v1.4"');
|
|
97
|
+
}
|
|
98
|
+
if (!agent.id || !agent.name || !agent.description || !agent.instructions) {
|
|
99
|
+
errors.push('declarativeAgent requires id, name, description, and instructions');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {Object|null} plugin
|
|
105
|
+
* @param {string[]} errors
|
|
106
|
+
*/
|
|
107
|
+
function validateAiPlugin(plugin, errors) {
|
|
108
|
+
if (!plugin) return;
|
|
109
|
+
if (plugin.schema_version !== 'v2.4') {
|
|
110
|
+
errors.push('ai-plugin.schema_version must be "v2.4"');
|
|
111
|
+
}
|
|
112
|
+
assertMaxLength(plugin.name_for_human, 'ai-plugin.name_for_human', PLUGIN_NAME_FOR_HUMAN_MAX, errors);
|
|
113
|
+
assertMaxLength(
|
|
114
|
+
plugin.description_for_human,
|
|
115
|
+
'ai-plugin.description_for_human',
|
|
116
|
+
PLUGIN_DESCRIPTION_FOR_HUMAN_MAX,
|
|
117
|
+
errors
|
|
118
|
+
);
|
|
119
|
+
if (!plugin.namespace || !/^[A-Za-z0-9-]+$/.test(plugin.namespace)) {
|
|
120
|
+
errors.push('ai-plugin.namespace is required (alphanumeric and hyphen only)');
|
|
121
|
+
}
|
|
122
|
+
validateAiPluginRuntime(plugin, errors);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {Object} plugin
|
|
127
|
+
* @param {string[]} errors
|
|
128
|
+
*/
|
|
129
|
+
function validateAiPluginRuntime(plugin, errors) {
|
|
130
|
+
const runtime = Array.isArray(plugin.runtimes) ? plugin.runtimes[0] : null;
|
|
131
|
+
if (!runtime || runtime.type !== 'RemoteMCPServer') {
|
|
132
|
+
errors.push('ai-plugin.runtimes[0].type must be RemoteMCPServer');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!ALLOWED_AUTH_TYPES.has(runtime.auth?.type)) {
|
|
136
|
+
errors.push(
|
|
137
|
+
`ai-plugin.runtimes[0].auth.type must be one of ${[...ALLOWED_AUTH_TYPES].join(', ')}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (runtime.auth?.type === 'OAuthPluginVault') {
|
|
141
|
+
const ref = runtime.auth.reference_id;
|
|
142
|
+
if (typeof ref !== 'string' || !ref.trim()) {
|
|
143
|
+
errors.push(
|
|
144
|
+
'OAuthPluginVault.reference_id is required — pass --oauth-plugin-vault-reference-id ' +
|
|
145
|
+
'with the Teams Developer Portal Entra SSO auth-config id'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (runtime.auth?.type === 'None') {
|
|
150
|
+
errors.push(
|
|
151
|
+
'ai-plugin.runtimes[0].auth.type None is not allowed; use OAuthPluginVault (Microsoft token / Entra)'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const desc = runtime.spec?.mcp_tool_description;
|
|
155
|
+
if (desc && (desc.mode || desc.layerBToolNames)) {
|
|
156
|
+
errors.push(
|
|
157
|
+
'ai-plugin mcp_tool_description must not use mode/layerBToolNames; omit for dynamic discovery or use tools/file'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {{ color: Buffer, outline: Buffer }} icons
|
|
164
|
+
* @param {string[]} errors
|
|
165
|
+
*/
|
|
166
|
+
function validatePackageIcons(icons, errors) {
|
|
167
|
+
const colorDims = readPngDimensions(icons.color);
|
|
168
|
+
const outlineDims = readPngDimensions(icons.outline);
|
|
169
|
+
if (!colorDims || colorDims.width !== 192 || colorDims.height !== 192) {
|
|
170
|
+
errors.push('color.png must be a 192x192 PNG');
|
|
171
|
+
}
|
|
172
|
+
if (!outlineDims || outlineDims.width !== 32 || outlineDims.height !== 32) {
|
|
173
|
+
errors.push('outline.png must be a 32x32 PNG');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {Record<string, string>} sourceFiles
|
|
179
|
+
* @param {{ color: Buffer, outline: Buffer }} icons
|
|
180
|
+
* @throws {Error}
|
|
181
|
+
*/
|
|
182
|
+
function assertValidMicrosoftCopilotPackage(sourceFiles, icons) {
|
|
183
|
+
const parsed = parsePackageJsonFiles(sourceFiles);
|
|
184
|
+
const errors = [...parsed.errors];
|
|
185
|
+
validateTeamsManifest(parsed.manifest, errors);
|
|
186
|
+
validateDeclarativeAgent(parsed.agent, errors);
|
|
187
|
+
validateAiPlugin(parsed.plugin, errors);
|
|
188
|
+
validatePackageIcons(icons, errors);
|
|
189
|
+
if (errors.length) {
|
|
190
|
+
throw new Error(`Microsoft Copilot package validation failed:\n- ${errors.join('\n- ')}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
assertValidMicrosoftCopilotPackage,
|
|
196
|
+
ALLOWED_AUTH_TYPES
|
|
197
|
+
};
|
package/lib/cli/index.js
CHANGED
|
@@ -67,6 +67,8 @@ function setupCoreCliCommands(program) {
|
|
|
67
67
|
setupChannelCommands(program);
|
|
68
68
|
setupDatasourceCommands(program);
|
|
69
69
|
setupManifestCommands(program);
|
|
70
|
+
const { setupIntegrationDefinitionCommands } = require('../commands/integration-definition');
|
|
71
|
+
setupIntegrationDefinitionCommands(program);
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -95,7 +95,7 @@ function logPlatformSetupDurationHint(scope = 'platform') {
|
|
|
95
95
|
const line =
|
|
96
96
|
scope === 'dataplane'
|
|
97
97
|
? 'Installation may take a few minutes. Authenticating and starting Dataplane — please wait.'
|
|
98
|
-
: 'Installation may take a few minutes. Keycloak, Miso Controller, and
|
|
98
|
+
: 'Installation may take a few minutes. Keycloak, Miso Controller, Dataplane, and Builder API are starting — please wait.';
|
|
99
99
|
logger.log(chalk.yellow(`⚠ ${line}`));
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Consolidate deferred public-URL health warnings for guided install UX.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
const { formatWarningLine } = require('../utils/cli-test-layout-chalk');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} message
|
|
12
|
+
* @returns {{ kind: 'dns'|'localhost', url: string }|null}
|
|
13
|
+
*/
|
|
14
|
+
function extractPublicUrlFromWarning(message) {
|
|
15
|
+
const text = String(message || '');
|
|
16
|
+
const dnsMatch = text.match(/Public URL was not verified \(DNS\):\s*(\S+)/);
|
|
17
|
+
if (dnsMatch) {
|
|
18
|
+
return { kind: 'dns', url: dnsMatch[1].replace(/\.$/, '') };
|
|
19
|
+
}
|
|
20
|
+
const match = text.match(/Public URL was not verified:\s*(\S+)/);
|
|
21
|
+
if (match) {
|
|
22
|
+
return { kind: 'localhost', url: match[1].replace(/\.$/, '') };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Print one consolidated public-URL warning (avoid N near-duplicate lines).
|
|
29
|
+
* @param {string[]} messages
|
|
30
|
+
*/
|
|
31
|
+
function flushDeferredPublicUrlWarnings(messages) {
|
|
32
|
+
const list = (messages || []).filter(Boolean);
|
|
33
|
+
if (list.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const parsed = list.map(extractPublicUrlFromWarning).filter(Boolean);
|
|
37
|
+
if (parsed.length === 0) {
|
|
38
|
+
for (const message of list) {
|
|
39
|
+
logger.log(formatWarningLine(message));
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const urls = [...new Set(parsed.map((row) => row.url))];
|
|
44
|
+
const hasDns = parsed.some((row) => row.kind === 'dns');
|
|
45
|
+
const urlPart = urls.length === 1 ? urls[0] : urls.join(', ');
|
|
46
|
+
const detail = hasDns
|
|
47
|
+
? 'Hostname DNS and/or Traefik routing/TLS could not be verified; health succeeded via localhost only.'
|
|
48
|
+
: 'Health checks succeeded via localhost only. Validate Traefik routing, TLS, and DNS.';
|
|
49
|
+
logger.log(formatWarningLine(`Public URL was not verified: ${urlPart}. ${detail}`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
extractPublicUrlFromWarning,
|
|
54
|
+
flushDeferredPublicUrlWarnings
|
|
55
|
+
};
|