sap-ai-dev-toolkit 0.9.9918 → 0.9.9920
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/.github/agents/sap-integration-architect.agent.md +32 -18
- package/.github/agents/sap-integration-developer.agent.md +28 -12
- package/.github/skills/integration-suite-delivery/SKILL.md +94 -37
- package/.github/skills/sap-integration-architect/SKILL.md +49 -25
- package/README.md +72 -15
- package/dist/checksums.txt +5 -5
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/inventory.md +97 -57
- package/package.json +1 -1
- package/scripts/check-live-integration.mjs +151 -0
- package/scripts/install-local.mjs +11 -4
- package/scripts/run-live-integration.mjs +48 -56
- package/src/apim-bundle.mjs +416 -13
- package/src/apim-cleanup-tools.mjs +145 -0
- package/src/apim-content.mjs +157 -0
- package/src/apim-delete-tools.mjs +139 -0
- package/src/apim-documentation.mjs +162 -0
- package/src/apim-inspection.mjs +141 -0
- package/src/apim-monitoring.mjs +140 -0
- package/src/apim-policy-steps.mjs +31 -0
- package/src/apim-runtime.mjs +262 -0
- package/src/apim-tools.mjs +202 -109
- package/src/cf-integration.mjs +46 -7
- package/src/cpi-async-monitoring.mjs +177 -0
- package/src/cpi-content.mjs +92 -0
- package/src/cpi-monitoring.mjs +225 -0
- package/src/cpi-operational.mjs +129 -0
- package/src/cpi-release.mjs +273 -0
- package/src/cpi-security.mjs +48 -0
- package/src/cpi-tools.mjs +184 -319
- package/src/devhub-retirement.mjs +345 -0
- package/src/devhub-tools.mjs +177 -0
- package/src/edge-monitoring.mjs +101 -0
- package/src/iflow-bundle.mjs +295 -7
- package/src/integration-alerts.mjs +248 -0
- package/src/integration-analysis.mjs +488 -0
- package/src/integration-capabilities.mjs +181 -0
- package/src/integration-collections.mjs +203 -0
- package/src/integration-common.mjs +140 -35
- package/src/integration-config.mjs +20 -14
- package/src/integration-credentials.mjs +16 -10
- package/src/integration-http.mjs +255 -67
- package/src/integration-messaging.mjs +118 -0
- package/src/integration-optional-config.mjs +122 -0
- package/src/integration-optional-tools.mjs +153 -0
- package/src/integration-release-store.mjs +410 -0
- package/src/integration-server.mjs +34 -16
- package/src/integration-state.mjs +31 -0
- package/src/integration-tools.mjs +44 -8
- package/src/integration-transport.mjs +184 -0
- package/src/integration-validation.mjs +26 -0
- package/src/integration-verify.mjs +143 -62
- package/src/open-connectors-tools.mjs +143 -0
- package/src/setup.mjs +66 -44
- package/tools.md +193 -26
package/src/cpi-tools.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// MCP tools for SAP Cloud Integration (CPI) over the /api/v1 OData API.
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { X509Certificate } from 'node:crypto';
|
|
4
|
+
import { registerCpiSecurityTools } from './cpi-security.mjs';
|
|
5
|
+
import { registerCpiOperationalTools } from './cpi-operational.mjs';
|
|
6
|
+
import { readCpiConfigurations, writeCpiConfigurations, cpiReleaseFailure, CPI_PACKAGE_ARTIFACT_SETS as PACKAGE_ARTIFACT_SETS } from './cpi-content.mjs';
|
|
7
|
+
import { registerCpiReleaseTools, CPI_ARTIFACT_TYPES as ARTIFACT_TYPES } from './cpi-release.mjs';
|
|
3
8
|
import {
|
|
4
9
|
CREATE_ANNOTATIONS,
|
|
5
10
|
ERROR_TEXT_CAP,
|
|
@@ -13,8 +18,6 @@ import {
|
|
|
13
18
|
confirmation,
|
|
14
19
|
entity,
|
|
15
20
|
fileEntry,
|
|
16
|
-
isoDate,
|
|
17
|
-
isoToOdataDateTime,
|
|
18
21
|
key,
|
|
19
22
|
list,
|
|
20
23
|
pick,
|
|
@@ -27,6 +30,8 @@ import {
|
|
|
27
30
|
top as topSchema
|
|
28
31
|
} from './integration-common.mjs';
|
|
29
32
|
import { IntegrationError, odataDate, odataKey } from './integration-http.mjs';
|
|
33
|
+
import { createCollectionPager, cursorSchema } from './integration-collections.mjs';
|
|
34
|
+
import { registerCpiMonitoringTools } from './cpi-monitoring.mjs';
|
|
30
35
|
import {
|
|
31
36
|
MANIFEST_PATH,
|
|
32
37
|
PARAMETERS_PROP,
|
|
@@ -46,7 +51,6 @@ import {
|
|
|
46
51
|
|
|
47
52
|
const CPI_BASE = '/api/v1';
|
|
48
53
|
const MAX_UPDATE_FILE_BYTES = 256 * 1024;
|
|
49
|
-
const MESSAGE_STATUSES = ['COMPLETED', 'PROCESSING', 'RETRY', 'ESCALATED', 'FAILED', 'CANCELLED', 'DISCARDED', 'ABANDONED'];
|
|
50
54
|
|
|
51
55
|
const packageId = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,59}$/);
|
|
52
56
|
const iflowId = z.string().regex(/^[A-Za-z][A-Za-z0-9_.-]{0,99}$/);
|
|
@@ -72,7 +76,6 @@ const templateParameters = z.object({
|
|
|
72
76
|
const scriptSchema = z.object({ name: z.string().max(90), content: z.string() }).strict();
|
|
73
77
|
const artifactId = iflowId;
|
|
74
78
|
const artifactVersion = z.string().regex(/^(?:active|\d+\.\d+\.\d+)$/).default('active');
|
|
75
|
-
const messageGuid = z.string().regex(/^[A-Za-z0-9_-]{10,64}$/);
|
|
76
79
|
const waitSeconds = fallback => z.number().int().min(0).max(50).default(fallback);
|
|
77
80
|
// Discover (SAP standard) package ids are longer than design-time ids and may contain periods.
|
|
78
81
|
const sourcePackageId = z.string().regex(/^[A-Za-z][A-Za-z0-9_.]{0,199}$/);
|
|
@@ -80,33 +83,26 @@ const securityName = z.string().regex(/^[A-Za-z0-9 _.@-]{1,150}$/);
|
|
|
80
83
|
const partnerChars = /^[A-Za-z0-9\-._~@]+$/;
|
|
81
84
|
const numberText = z.union([z.number().int().min(0).max(99_999_999_999_999), z.string().regex(/^\d{1,14}$/)]);
|
|
82
85
|
|
|
83
|
-
// Design-time artifact kinds besides iFlows: entity set and Deploy<Kind>DesigntimeArtifact function import.
|
|
84
|
-
const ARTIFACT_TYPES = Object.freeze({
|
|
85
|
-
value_mapping: { set: 'ValueMappingDesigntimeArtifacts', deploy: 'DeployValueMappingDesigntimeArtifact' },
|
|
86
|
-
script_collection: { set: 'ScriptCollectionDesigntimeArtifacts', deploy: 'DeployScriptCollectionDesigntimeArtifact' },
|
|
87
|
-
message_mapping: { set: 'MessageMappingDesigntimeArtifacts', deploy: 'DeployMessageMappingDesigntimeArtifact' }
|
|
88
|
-
});
|
|
89
86
|
const artifactType = z.enum(Object.keys(ARTIFACT_TYPES));
|
|
90
|
-
const PACKAGE_ARTIFACT_SETS = Object.freeze({
|
|
91
|
-
iflows: 'IntegrationDesigntimeArtifacts',
|
|
92
|
-
value_mappings: 'ValueMappingDesigntimeArtifacts',
|
|
93
|
-
script_collections: 'ScriptCollectionDesigntimeArtifacts',
|
|
94
|
-
message_mappings: 'MessageMappingDesigntimeArtifacts'
|
|
95
|
-
});
|
|
96
87
|
const RUNTIME_STATUSES = ['STARTED', 'STARTING', 'STOPPING', 'ERROR'];
|
|
97
88
|
const KEYSTORES = ['system', 'backup_admin_system', 'KeyRenewal'];
|
|
98
89
|
const PARTNER_CONTENT_TYPES = ['xml', 'xsl', 'xsd', 'json', 'text', 'zip', 'gz', 'zlib', 'crt'];
|
|
99
|
-
const JMS_BROKER_FIELDS = ['Capacity', 'MaxCapacity', 'CapacityOk', 'CapacityWarning', 'CapacityError', 'QueueNumber', 'MaxQueueNumber',
|
|
100
|
-
'IsQueuesHigh', 'IsMessageSpoolHigh', 'IsTransactedSessionsHigh', 'IsConsumersHigh', 'IsProducersHigh'];
|
|
101
90
|
const MAX_VALUES_PER_PAIR = 500;
|
|
102
|
-
const
|
|
103
|
-
const MAX_RUN_STEPS = 200;
|
|
104
|
-
const STEP_ERROR_CAP = 1024;
|
|
91
|
+
const collectionInputs = { top: topSchema(200, 100), cursor: cursorSchema };
|
|
105
92
|
const MAX_CERTIFICATE_BYTES = 10 * 1024;
|
|
106
93
|
|
|
107
|
-
const time = value => Date.parse(odataDate(value)) || 0;
|
|
108
94
|
const dates = (value, fields) => Object.fromEntries(fields.filter(field => value?.[field] !== undefined).map(field => [field, odataDate(value[field])]));
|
|
109
95
|
|
|
96
|
+
function endpointUrl(value) {
|
|
97
|
+
try {
|
|
98
|
+
const url = new URL(value);
|
|
99
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) return null;
|
|
100
|
+
url.search = '';
|
|
101
|
+
url.hash = '';
|
|
102
|
+
return url.href;
|
|
103
|
+
} catch { return null; }
|
|
104
|
+
}
|
|
105
|
+
|
|
110
106
|
/** Tenant error text can echo request fields: strip the secret, including a whitespace-collapsed or truncated copy. */
|
|
111
107
|
function withoutSecret(error, secret) {
|
|
112
108
|
if (error?.name !== 'IntegrationError') return error;
|
|
@@ -122,8 +118,13 @@ function withoutSecret(error, secret) {
|
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
/** Register cpi_* tools for one CPI client. */
|
|
125
|
-
export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
121
|
+
export function registerCpiTools(register, client, { pollIntervalMs, summary }) {
|
|
126
122
|
const cpi = (method, path, options) => client.request(method, `${CPI_BASE}${path}`, options);
|
|
123
|
+
const pager = createCollectionPager(client, { root: CPI_BASE, request: cpi });
|
|
124
|
+
registerCpiMonitoringTools(register, client);
|
|
125
|
+
registerCpiSecurityTools(register, client);
|
|
126
|
+
registerCpiOperationalTools(register, client, { summary });
|
|
127
|
+
registerCpiReleaseTools(register, client, { pollIntervalMs });
|
|
127
128
|
const artifactPath = (id, version = 'active') => `/IntegrationDesigntimeArtifacts(Id=${key(id)},Version=${key(version)})`;
|
|
128
129
|
const runtimePath = id => `/IntegrationRuntimeArtifacts(${key(id)})`;
|
|
129
130
|
const designtimePath = (type, id, version = 'active') => `/${ARTIFACT_TYPES[type].set}(Id=${key(id)},Version=${key(version)})`;
|
|
@@ -169,24 +170,27 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
169
170
|
title: 'Read CPI OData API',
|
|
170
171
|
service: 'Cloud Integration /api/v1',
|
|
171
172
|
request: cpi,
|
|
173
|
+
client,
|
|
174
|
+
root: CPI_BASE,
|
|
172
175
|
examples: "/JmsBrokers('Broker1') with $expand=QueueStates, /AccessPolicies, /KeystoreEntries with keystoreName=system, /MessageProcessingLogs('<guid>')/Runs"
|
|
173
176
|
});
|
|
174
177
|
|
|
175
178
|
register('cpi_list_packages', 'List CPI packages',
|
|
176
179
|
'List Cloud Integration design-time packages (Id, Name, ShortText, Version, Mode).',
|
|
177
|
-
|
|
180
|
+
collectionInputs,
|
|
178
181
|
READ_ONLY_ANNOTATIONS,
|
|
179
182
|
// The CPI tenant rejects $select on IntegrationPackages (HTTP 501); project client-side.
|
|
180
|
-
async
|
|
181
|
-
|
|
182
|
-
})).map(entry => pick(entry, ['Id', 'Name', 'ShortText', 'Version', 'Mode'])));
|
|
183
|
+
async args => pager.page({ ...args, path: '/IntegrationPackages', orderBy: 'Name',
|
|
184
|
+
project: entry => pick(entry, ['Id', 'Name', 'ShortText', 'Version', 'Mode']) }));
|
|
183
185
|
|
|
184
186
|
register('cpi_list_iflows', 'List iFlows in a package',
|
|
185
187
|
'List the integration flows of one CPI package (Id, Name, Version, Description).',
|
|
186
|
-
{ package_id: packageId },
|
|
188
|
+
{ package_id: packageId, ...collectionInputs },
|
|
187
189
|
READ_ONLY_ANNOTATIONS,
|
|
188
|
-
async ({ package_id }) =>
|
|
189
|
-
|
|
190
|
+
async ({ package_id, ...args }) => pager.page({ ...args,
|
|
191
|
+
path: `/IntegrationPackages(${key(package_id)})/IntegrationDesigntimeArtifacts`,
|
|
192
|
+
mode: 'local', sourceComplete: false,
|
|
193
|
+
project: artifact => pick(artifact, ['Id', 'Name', 'Version', 'Description']) }));
|
|
190
194
|
|
|
191
195
|
register('cpi_get_iflow', 'Get iFlow',
|
|
192
196
|
'Read an iFlow: artifact metadata, externalized configuration and bundle files (text files capped at 48 KiB each). Pass file_path to return one file (up to 90 KiB).',
|
|
@@ -228,73 +232,17 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
228
232
|
|
|
229
233
|
register('cpi_list_service_endpoints', 'List CPI service endpoints',
|
|
230
234
|
'List deployed HTTP(S)/SOAP endpoint URLs; optionally only those of one iFlow. CPI registers an endpoint up to a few minutes after the iFlow reaches STARTED, so an empty list right after deployment means retry later.',
|
|
231
|
-
{ iflow_id: iflowId.optional() },
|
|
235
|
+
{ iflow_id: iflowId.optional(), ...collectionInputs },
|
|
232
236
|
READ_ONLY_ANNOTATIONS,
|
|
233
|
-
async ({ iflow_id }) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
endpoints = byName.length ? byName : endpoints.filter(endpoint => String(endpoint.Id || '').includes(iflow_id));
|
|
238
|
-
}
|
|
239
|
-
return endpoints.map(endpoint => ({
|
|
237
|
+
async ({ iflow_id, ...args }) => pager.page({ ...args, path: '/ServiceEndpoints', mode: 'local',
|
|
238
|
+
query: { $expand: 'EntryPoints' }, identity: iflow_id,
|
|
239
|
+
filter: iflow_id ? endpoint => endpoint.Name === iflow_id || endpoint.Id === iflow_id : undefined,
|
|
240
|
+
project: endpoint => ({
|
|
240
241
|
name: endpoint.Name,
|
|
241
|
-
urls: list(endpoint.EntryPoints).map(entryPoint => entryPoint.Url).filter(Boolean)
|
|
242
|
-
})
|
|
243
|
-
});
|
|
242
|
+
urls: list(endpoint.EntryPoints).map(entryPoint => endpointUrl(entryPoint.Url)).filter(Boolean)
|
|
243
|
+
})
|
|
244
|
+
}));
|
|
244
245
|
|
|
245
|
-
register('cpi_list_message_logs', 'List message processing logs',
|
|
246
|
-
'List CPI message processing logs, newest first, filtered by iFlow, status, time window (ISO-8601), correlation id, application message id or one custom header property (name and value, exact match).',
|
|
247
|
-
{
|
|
248
|
-
iflow_id: iflowId.optional(),
|
|
249
|
-
status: z.enum(MESSAGE_STATUSES).optional(),
|
|
250
|
-
from: isoDate.optional(),
|
|
251
|
-
to: isoDate.optional(),
|
|
252
|
-
correlation_id: z.string().min(1).max(200).optional(),
|
|
253
|
-
application_message_id: z.string().min(1).max(200).optional(),
|
|
254
|
-
custom_header: z.object({ name: z.string().min(1).max(200), value: z.string().max(2000) }).strict().optional(),
|
|
255
|
-
top: z.number().int().min(1).max(100).default(20)
|
|
256
|
-
},
|
|
257
|
-
READ_ONLY_ANNOTATIONS,
|
|
258
|
-
async ({ iflow_id, status, from, to, correlation_id, application_message_id, custom_header, top }) => {
|
|
259
|
-
const filter = [
|
|
260
|
-
iflow_id && `IntegrationArtifact/Id eq ${odataKey(iflow_id)}`,
|
|
261
|
-
status && `Status eq '${status}'`,
|
|
262
|
-
from && `LogStart gt datetime'${isoToOdataDateTime(from)}'`,
|
|
263
|
-
to && `LogEnd lt datetime'${isoToOdataDateTime(to)}'`,
|
|
264
|
-
correlation_id && `CorrelationId eq ${odataKey(correlation_id)}`,
|
|
265
|
-
application_message_id && `ApplicationMessageId eq ${odataKey(application_message_id)}`
|
|
266
|
-
].filter(Boolean).join(' and ');
|
|
267
|
-
const parsed = await cpi('GET', '/MessageProcessingLogs', {
|
|
268
|
-
query: {
|
|
269
|
-
$filter: filter || undefined,
|
|
270
|
-
$orderby: 'LogEnd desc',
|
|
271
|
-
$top: top,
|
|
272
|
-
$inlinecount: 'allpages',
|
|
273
|
-
filterCustomHeaderProperties: custom_header ? `${odataKey(custom_header.name)} eq ${odataKey(custom_header.value)}` : undefined
|
|
274
|
-
},
|
|
275
|
-
unwrap: false
|
|
276
|
-
});
|
|
277
|
-
const logs = list(parsed?.d ?? parsed).map(log => ({
|
|
278
|
-
...pick(log, ['MessageGuid', 'CorrelationId', 'IntegrationFlowName', 'Status', 'CustomStatus', 'Sender', 'Receiver']),
|
|
279
|
-
LogStart: odataDate(log.LogStart),
|
|
280
|
-
LogEnd: odataDate(log.LogEnd)
|
|
281
|
-
}));
|
|
282
|
-
const total = Number(parsed?.d?.__count);
|
|
283
|
-
return { total: Number.isFinite(total) ? total : null, returned: logs.length, logs };
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
register('cpi_get_message_log', 'Get message processing log',
|
|
287
|
-
'Read one message processing log with its error text and custom header properties. Payload attachments are never read.',
|
|
288
|
-
{ message_guid: messageGuid },
|
|
289
|
-
READ_ONLY_ANNOTATIONS,
|
|
290
|
-
async ({ message_guid }) => {
|
|
291
|
-
const path = `/MessageProcessingLogs(${key(message_guid)})`;
|
|
292
|
-
const log = entity(await cpi('GET', path));
|
|
293
|
-
for (const field of ['LogStart', 'LogEnd']) if (field in log) log[field] = odataDate(log[field]);
|
|
294
|
-
const errorText = await cpi('GET', `${path}/ErrorInformation/$value`, { responseType: 'text', accept: '*/*', expect: [200, 204, 404] });
|
|
295
|
-
const headers = list(await cpi('GET', `${path}/CustomHeaderProperties`)).map(entry => pick(entry, ['Name', 'Value']));
|
|
296
|
-
return { log, error_information: errorText ? capText(errorText, ERROR_TEXT_CAP).text : null, custom_header_properties: headers };
|
|
297
|
-
});
|
|
298
246
|
|
|
299
247
|
register('cpi_create_package', 'Create CPI package',
|
|
300
248
|
'Create a Cloud Integration package. Requires confirmation exactly equal to CREATE CPI PACKAGE <package_id>.',
|
|
@@ -369,7 +317,7 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
369
317
|
});
|
|
370
318
|
|
|
371
319
|
register('cpi_update_iflow', 'Update iFlow',
|
|
372
|
-
'Replace
|
|
320
|
+
'Replace/add bundle resources in the active draft and bump its bundle version, preserving fresh configured values below explicit parameter overrides. Reapply surviving configuration after PUT and report exact partial progress. No retained history is created. Fails before writing on expected_version mismatch. Exact confirmation: UPDATE IFLOW <iflow_id>.',
|
|
373
321
|
{
|
|
374
322
|
iflow_id: iflowId,
|
|
375
323
|
expected_version: z.string().min(1).max(40),
|
|
@@ -393,18 +341,22 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
393
341
|
}
|
|
394
342
|
const newVersion = bumpPatchVersion(current);
|
|
395
343
|
const bundle = readBundle(await cpi('GET', `${path}/$value`, { responseType: 'bytes', accept: 'application/zip, application/octet-stream' }));
|
|
344
|
+
const previousDefaults = bundle.has(PARAMETERS_PROP) ? parseProperties(strFromU8(bundle.get(PARAMETERS_PROP))) : {};
|
|
396
345
|
const changed = new Set();
|
|
397
346
|
for (const file of files) {
|
|
398
347
|
bundle.set(file.path, strToU8(file.content));
|
|
399
348
|
changed.add(file.path);
|
|
400
349
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
350
|
+
// Read tenant configuration immediately before the draft replacement, not stale bundled defaults.
|
|
351
|
+
const configured = await readCpiConfigurations(client, iflow_id);
|
|
352
|
+
const defaults = bundle.has(PARAMETERS_PROP) ? parseProperties(strFromU8(bundle.get(PARAMETERS_PROP))) : {};
|
|
353
|
+
const removed = new Set(Object.keys(previousDefaults).filter(key => !Object.hasOwn(defaults, key) && !Object.hasOwn(parameters ?? {}, key)));
|
|
354
|
+
const retained = configured.filter(entry => !removed.has(entry.ParameterKey));
|
|
355
|
+
const effective = { ...defaults, ...Object.fromEntries(retained.map(entry => [entry.ParameterKey, entry.ParameterValue])), ...parameters };
|
|
356
|
+
if (retained.length || Object.keys(parameters ?? {}).length) {
|
|
357
|
+
bundle.set(PARAMETERS_PROP, strToU8(propertiesText(effective)));
|
|
406
358
|
const propdef = bundle.has(PARAMETERS_PROPDEF) ? strFromU8(bundle.get(PARAMETERS_PROPDEF)) : '';
|
|
407
|
-
const updatedPropdef = mergePropdef(propdef, Object.keys(
|
|
359
|
+
const updatedPropdef = mergePropdef(propdef, Object.keys(effective));
|
|
408
360
|
if (updatedPropdef !== propdef) {
|
|
409
361
|
bundle.set(PARAMETERS_PROPDEF, strToU8(updatedPropdef));
|
|
410
362
|
changed.add(PARAMETERS_PROPDEF);
|
|
@@ -414,11 +366,35 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
414
366
|
if (!bundle.has(MANIFEST_PATH)) throw new IntegrationError(`iFlow ${iflow_id} bundle has no ${MANIFEST_PATH}`);
|
|
415
367
|
bundle.set(MANIFEST_PATH, strToU8(replaceManifestVersion(strFromU8(bundle.get(MANIFEST_PATH)), newVersion)));
|
|
416
368
|
changed.add(MANIFEST_PATH);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
369
|
+
const reapply = new Map(retained.map(entry => [entry.ParameterKey, entry]));
|
|
370
|
+
for (const [ParameterKey, ParameterValue] of Object.entries(parameters ?? {})) reapply.set(ParameterKey, {
|
|
371
|
+
ParameterKey, ParameterValue, DataType: reapply.get(ParameterKey)?.DataType ?? 'xsd:string'
|
|
420
372
|
});
|
|
421
|
-
|
|
373
|
+
const parameter_names = { source_defaults: Object.keys(defaults).sort(), configured: [...reapply.keys()].sort(),
|
|
374
|
+
explicit_overrides: Object.keys(parameters ?? {}).sort() };
|
|
375
|
+
// Preserve exact progress within the output budget; reject before the accepted content write otherwise.
|
|
376
|
+
if (Buffer.byteLength(JSON.stringify(parameter_names)) > 40000 || Buffer.byteLength(JSON.stringify([...reapply.keys()])) > 24000) {
|
|
377
|
+
throw new IntegrationError('CPI parameter names exceed the safe mutation progress-report limit');
|
|
378
|
+
}
|
|
379
|
+
const result = { updated: true, content_updated: true, previous_version: current, new_version: newVersion,
|
|
380
|
+
retained_version_created: false, changed_files: [...changed], parameter_names };
|
|
381
|
+
try {
|
|
382
|
+
await cpi('PUT', path, {
|
|
383
|
+
json: { Name: name ?? artifact.Name, ArtifactContent: Buffer.from(writeBundle(bundle)).toString('base64') },
|
|
384
|
+
expect: [200, 202, 204], responseType: 'none', accept: '*/*'
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const status = Number(error?.status);
|
|
388
|
+
const accepted = status >= 200 && status < 300 ? true : status >= 400 ? false : null;
|
|
389
|
+
return { ...result, updated: accepted, content_updated: accepted, outcome: 'unverified',
|
|
390
|
+
configuration: { updated: [], failed: null, attempted: false }, source_result: cpiReleaseFailure(error) };
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const configuration = await writeCpiConfigurations(client, iflow_id, [...reapply.values()]);
|
|
394
|
+
return { ...result, configuration, outcome: configuration.failed ? 'partial' : 'succeeded' };
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return { ...result, outcome: 'partial', configuration: { updated: [], failed: cpiReleaseFailure(error) } };
|
|
397
|
+
}
|
|
422
398
|
});
|
|
423
399
|
|
|
424
400
|
register('cpi_update_iflow_configuration', 'Configure iFlow parameters',
|
|
@@ -435,84 +411,18 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
435
411
|
OVERWRITE_ANNOTATIONS,
|
|
436
412
|
async ({ iflow_id, parameters, confirmation: confirmed }) => {
|
|
437
413
|
requireConfirmation(confirmed, `CONFIGURE IFLOW ${iflow_id}`);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
await cpi('PUT', `${artifactPath(iflow_id)}/$links/Configurations(${key(parameter.key)})`, {
|
|
442
|
-
json: { ParameterValue: parameter.value, DataType: parameter.data_type },
|
|
443
|
-
expect: [200, 202, 204]
|
|
444
|
-
});
|
|
445
|
-
updated.push(parameter.key);
|
|
446
|
-
} catch (error) {
|
|
447
|
-
return { updated, failed: { key: parameter.key, error: toolError(error).content[0].text } };
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return { updated, failed: null };
|
|
414
|
+
return writeCpiConfigurations(client, iflow_id, parameters.map(parameter => ({
|
|
415
|
+
ParameterKey: parameter.key, ParameterValue: parameter.value, DataType: parameter.data_type
|
|
416
|
+
})));
|
|
451
417
|
});
|
|
452
418
|
|
|
453
|
-
// MCP clients commonly abort a tool call after 60 s, so the wait stays below that;
|
|
454
|
-
// a long STARTING phase is reported as timed_out and continued with cpi_get_iflow_runtime_status.
|
|
455
|
-
register('cpi_deploy_iflow', 'Deploy iFlow',
|
|
456
|
-
'Deploy the active iFlow version and wait (up to wait_seconds, max 50) for build and runtime status; returns runtime error text when it fails to start. When timed_out is true, poll cpi_get_iflow_runtime_status. Requires confirmation exactly equal to DEPLOY IFLOW <iflow_id>.',
|
|
457
|
-
{ iflow_id: iflowId, wait_seconds: z.number().int().min(0).max(50).default(45), confirmation },
|
|
458
|
-
CREATE_ANNOTATIONS,
|
|
459
|
-
async ({ iflow_id, wait_seconds, confirmation: confirmed }) => {
|
|
460
|
-
requireConfirmation(confirmed, `DEPLOY IFLOW ${iflow_id}`);
|
|
461
|
-
const deadline = Date.now() + wait_seconds * 1000;
|
|
462
|
-
const taskId = String(await cpi('POST', '/DeployIntegrationDesigntimeArtifact', {
|
|
463
|
-
query: { Id: odataKey(iflow_id), Version: "'active'" },
|
|
464
|
-
responseType: 'text',
|
|
465
|
-
accept: '*/*',
|
|
466
|
-
expect: [200, 202]
|
|
467
|
-
}) ?? '').trim().replace(/^"|"$/g, '');
|
|
468
|
-
if (!taskId) throw new IntegrationError(`CPI deploy of ${iflow_id} returned no task id`);
|
|
469
|
-
|
|
470
|
-
let buildStatus;
|
|
471
|
-
for (;;) {
|
|
472
|
-
const build = await cpi('GET', `/BuildAndDeployStatus(TaskId=${key(taskId)})`);
|
|
473
|
-
buildStatus = String(build?.Status ?? '').toUpperCase();
|
|
474
|
-
if (buildStatus !== 'DEPLOYING' || Date.now() >= deadline) break;
|
|
475
|
-
await sleep(pollIntervalMs);
|
|
476
|
-
}
|
|
477
|
-
const result = { task_id: taskId, build_status: buildStatus, runtime_status: null, runtime_version: null, error_information: null, timed_out: false };
|
|
478
|
-
const readRuntime = async () => {
|
|
479
|
-
const runtime = await cpi('GET', runtimePath(iflow_id), { expect: [200, 404], withStatus: true });
|
|
480
|
-
result.runtime_status = runtime.status === 404 ? null : String(runtime.data?.Status ?? '').toUpperCase();
|
|
481
|
-
result.runtime_version = runtime.status === 404 ? null : runtime.data?.Version ?? null;
|
|
482
|
-
};
|
|
483
|
-
if (buildStatus === 'DEPLOYING') {
|
|
484
|
-
return { ...result, timed_out: true, note: 'Build still running; poll cpi_get_iflow_runtime_status.' };
|
|
485
|
-
}
|
|
486
|
-
if (buildStatus !== 'SUCCESS') {
|
|
487
|
-
// CPI occasionally reports a transient build FAIL for a deploy that does start; report the runtime truth too.
|
|
488
|
-
await readRuntime();
|
|
489
|
-
if (result.runtime_status === 'ERROR') result.error_information = await runtimeError(iflow_id);
|
|
490
|
-
result.note = `CPI reported build status ${buildStatus || 'unknown'}; runtime_status/runtime_version show the iFlow currently deployed. Check cpi_get_iflow_runtime_status before redeploying.`;
|
|
491
|
-
return result;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
for (;;) {
|
|
495
|
-
await readRuntime();
|
|
496
|
-
const pending = result.runtime_status === null || result.runtime_status === 'STARTING';
|
|
497
|
-
if (!pending) break;
|
|
498
|
-
if (Date.now() >= deadline) {
|
|
499
|
-
result.timed_out = true;
|
|
500
|
-
result.note = 'Runtime still starting; poll cpi_get_iflow_runtime_status.';
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
await sleep(pollIntervalMs);
|
|
504
|
-
}
|
|
505
|
-
if (result.runtime_status === 'ERROR') result.error_information = await runtimeError(iflow_id);
|
|
506
|
-
return result;
|
|
507
|
-
});
|
|
508
419
|
|
|
509
420
|
register('cpi_list_runtime_artifacts', 'List deployed CPI artifacts',
|
|
510
421
|
'List deployed (runtime) Cloud Integration artifacts of every type (iFlows, value mappings, script collections, message mappings, …) with status and deployment time; optionally only one status.',
|
|
511
|
-
{ status: z.enum(RUNTIME_STATUSES).optional(), top: topSchema(500, 200) },
|
|
422
|
+
{ status: z.enum(RUNTIME_STATUSES).optional(), top: topSchema(500, 200), cursor: cursorSchema },
|
|
512
423
|
READ_ONLY_ANNOTATIONS,
|
|
513
|
-
async ({ status,
|
|
514
|
-
query: { $filter: status ? `Status eq '${status}'` : undefined,
|
|
515
|
-
})).map(runtimeSummary));
|
|
424
|
+
async ({ status, ...args }) => pager.page({ ...args, path: '/IntegrationRuntimeArtifacts', orderBy: 'Id',
|
|
425
|
+
query: { $filter: status ? `Status eq '${status}'` : undefined }, project: runtimeSummary }));
|
|
516
426
|
|
|
517
427
|
register('cpi_undeploy_artifact', 'Undeploy CPI artifact',
|
|
518
428
|
'Undeploy a runtime artifact of any type and wait (up to wait_seconds, max 50) until it is gone; the design-time artifact is kept. When timed_out is true, poll cpi_list_runtime_artifacts. Requires confirmation exactly equal to UNDEPLOY ARTIFACT <artifact_id>.',
|
|
@@ -542,14 +452,16 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
542
452
|
|
|
543
453
|
register('cpi_list_package_artifacts', 'List artifacts in a package',
|
|
544
454
|
'List the iFlows, value mappings, script collections and message mappings of one CPI package (Id, Name, Version, Description), grouped by type.',
|
|
545
|
-
{ package_id: packageId }
|
|
455
|
+
z.object({ package_id: packageId, type: z.enum(Object.keys(PACKAGE_ARTIFACT_SETS)).optional(), ...collectionInputs }).strict()
|
|
456
|
+
.refine(value => !value.cursor || value.type, { path: ['cursor'], message: 'cursor requires a concrete artifact type' }),
|
|
546
457
|
READ_ONLY_ANNOTATIONS,
|
|
547
|
-
async ({ package_id }) => {
|
|
458
|
+
async ({ package_id, type, ...args }) => {
|
|
459
|
+
const read = group => pager.page({ ...args, maxBytes: type ? 95 * 1024 : 22 * 1024, path: `/IntegrationPackages(${key(package_id)})/${PACKAGE_ARTIFACT_SETS[group]}`,
|
|
460
|
+
mode: 'local', sourceComplete: false,
|
|
461
|
+
project: artifact => pick(artifact, ['Id', 'Name', 'Version', 'Description']) });
|
|
462
|
+
if (type) return read(type);
|
|
548
463
|
const result = {};
|
|
549
|
-
for (const
|
|
550
|
-
result[group] = list(await cpi('GET', `/IntegrationPackages(${key(package_id)})/${set}`))
|
|
551
|
-
.map(artifact => pick(artifact, ['Id', 'Name', 'Version', 'Description']));
|
|
552
|
-
}
|
|
464
|
+
for (const group of Object.keys(PACKAGE_ARTIFACT_SETS)) result[group] = await read(group);
|
|
553
465
|
return result;
|
|
554
466
|
});
|
|
555
467
|
|
|
@@ -565,26 +477,42 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
565
477
|
});
|
|
566
478
|
|
|
567
479
|
register('cpi_get_value_mappings', 'Get value mapping values',
|
|
568
|
-
`Read
|
|
480
|
+
`Read bounded agency/identifier pairs of a value mapping (up to 20 pairs and ${MAX_VALUES_PER_PAIR} values/defaults per pair, subject to the response byte budget). Nested collection completeness is unverified; no continuation route is inferred.`,
|
|
569
481
|
{ artifact_id: artifactId, version: artifactVersion },
|
|
570
482
|
READ_ONLY_ANNOTATIONS,
|
|
571
483
|
async ({ artifact_id, version }) => {
|
|
572
484
|
const path = designtimePath('value_mapping', artifact_id, version);
|
|
573
485
|
const valMap = entry => ({ Id: entry.Id, SrcValue: entry.Value?.SrcValue ?? null, TgtValue: entry.Value?.TgtValue ?? null });
|
|
486
|
+
// These nested APIs have no verified traversal contract. Keep source counts, but never
|
|
487
|
+
// turn a capped or continuation-bearing response into an exhaustive value inventory.
|
|
488
|
+
const bounded = async (nestedPath, project, top, maxBytes) => {
|
|
489
|
+
const page = await pager.page({ path: nestedPath, mode: 'local', sourceComplete: false, project, top, maxBytes });
|
|
490
|
+
const envelope = { ...page, next_cursor: null, complete: false, stop_reason: 'source_limit' };
|
|
491
|
+
// Account for the actual pretty-printed result at its deepest nesting level.
|
|
492
|
+
const fits = count => Buffer.byteLength(JSON.stringify({ ...envelope, items: page.items.slice(0, count), returned: count }, null, 2)
|
|
493
|
+
.replaceAll('\n', '\n ')) <= maxBytes;
|
|
494
|
+
let low = 0;
|
|
495
|
+
let high = page.items.length;
|
|
496
|
+
while (low < high) {
|
|
497
|
+
const middle = Math.ceil((low + high) / 2);
|
|
498
|
+
if (fits(middle)) low = middle;
|
|
499
|
+
else high = middle - 1;
|
|
500
|
+
}
|
|
501
|
+
return { ...envelope, items: page.items.slice(0, low), returned: low };
|
|
502
|
+
};
|
|
503
|
+
const schemas = await bounded(`${path}/ValMapSchema`,
|
|
504
|
+
schema => pick(schema, ['SrcAgency', 'SrcId', 'TgtAgency', 'TgtId', 'State']), 20, 8 * 1024);
|
|
574
505
|
const pairs = [];
|
|
575
|
-
|
|
506
|
+
const valueBudget = Math.floor(76 * 1024 / (Math.max(1, schemas.items.length) * 2));
|
|
507
|
+
for (const schema of schemas.items) {
|
|
576
508
|
const pairPath = `${path}/ValMapSchema(SrcAgency=${key(schema.SrcAgency)},SrcId=${key(schema.SrcId)},TgtAgency=${key(schema.TgtAgency)},TgtId=${key(schema.TgtId)})`;
|
|
577
|
-
const values = list(await cpi('GET', `${pairPath}/ValMaps`)).map(valMap);
|
|
578
|
-
const defaults = list(await cpi('GET', `${pairPath}/DefaultValMaps`)).map(valMap);
|
|
579
509
|
pairs.push({
|
|
580
|
-
...
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
...(values.length > MAX_VALUES_PER_PAIR ? { values_truncated: true } : {}),
|
|
584
|
-
default_values: defaults
|
|
510
|
+
...schema,
|
|
511
|
+
values: await bounded(`${pairPath}/ValMaps`, valMap, MAX_VALUES_PER_PAIR, valueBudget),
|
|
512
|
+
default_values: await bounded(`${pairPath}/DefaultValMaps`, valMap, MAX_VALUES_PER_PAIR, valueBudget)
|
|
585
513
|
});
|
|
586
514
|
}
|
|
587
|
-
return { artifact_id, version, agency_pairs: pairs };
|
|
515
|
+
return { artifact_id, version, agency_pairs: { ...schemas, items: pairs } };
|
|
588
516
|
});
|
|
589
517
|
|
|
590
518
|
const agencyField = z.string().min(1).max(200);
|
|
@@ -667,37 +595,6 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
667
595
|
return { updated: true, artifact_id, version: artifact?.Version ?? null, changed_files: [...changed] };
|
|
668
596
|
});
|
|
669
597
|
|
|
670
|
-
register('cpi_deploy_artifact', 'Deploy CPI artifact',
|
|
671
|
-
'Deploy the active version of a value mapping, script collection or message mapping and wait (up to wait_seconds, max 50) until the runtime artifact leaves STARTING; returns runtime error text when it fails. A redeploy can briefly report the previously deployed artifact as STARTED, so check runtime_version and deployed_on. When timed_out is true, poll cpi_list_runtime_artifacts. Requires confirmation exactly equal to DEPLOY ARTIFACT <artifact_id>.',
|
|
672
|
-
{ type: artifactType, artifact_id: artifactId, wait_seconds: waitSeconds(45), confirmation },
|
|
673
|
-
CREATE_ANNOTATIONS,
|
|
674
|
-
async ({ type, artifact_id, wait_seconds, confirmation: confirmed }) => {
|
|
675
|
-
requireConfirmation(confirmed, `DEPLOY ARTIFACT ${artifact_id}`);
|
|
676
|
-
const deadline = Date.now() + wait_seconds * 1000;
|
|
677
|
-
await cpi('POST', `/${ARTIFACT_TYPES[type].deploy}`, {
|
|
678
|
-
query: { Id: odataKey(artifact_id), Version: "'active'" },
|
|
679
|
-
responseType: 'text',
|
|
680
|
-
accept: '*/*',
|
|
681
|
-
expect: [200, 201, 202]
|
|
682
|
-
});
|
|
683
|
-
const result = { type, artifact_id, runtime_status: null, runtime_version: null, deployed_on: null, error_information: null, timed_out: false };
|
|
684
|
-
for (;;) {
|
|
685
|
-
const runtime = await cpi('GET', runtimePath(artifact_id), { expect: [200, 404], withStatus: true });
|
|
686
|
-
const deployed = runtime.status === 200;
|
|
687
|
-
result.runtime_status = deployed ? String(runtime.data?.Status ?? '').toUpperCase() : null;
|
|
688
|
-
result.runtime_version = deployed ? runtime.data?.Version ?? null : null;
|
|
689
|
-
result.deployed_on = deployed ? odataDate(runtime.data?.DeployedOn) ?? null : null;
|
|
690
|
-
if (deployed && result.runtime_status !== 'STARTING') break;
|
|
691
|
-
if (Date.now() >= deadline) {
|
|
692
|
-
result.timed_out = true;
|
|
693
|
-
result.note = 'Runtime artifact not started yet; poll cpi_list_runtime_artifacts.';
|
|
694
|
-
break;
|
|
695
|
-
}
|
|
696
|
-
await sleep(pollIntervalMs);
|
|
697
|
-
}
|
|
698
|
-
if (result.runtime_status === 'ERROR') result.error_information = await runtimeError(artifact_id);
|
|
699
|
-
return result;
|
|
700
|
-
});
|
|
701
598
|
|
|
702
599
|
register('cpi_update_package', 'Update CPI package',
|
|
703
600
|
'Change the name, short text, description or version of a CPI package; omitted fields keep their current values. Requires confirmation exactly equal to UPDATE CPI PACKAGE <package_id>.',
|
|
@@ -740,7 +637,7 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
740
637
|
try {
|
|
741
638
|
copied = await cpi('POST', '/CopyIntegrationPackage', {
|
|
742
639
|
query: { Id: odataKey(package_id), ImportMode: `'${import_mode}'`, Suffix: suffix ? `'${suffix}'` : undefined },
|
|
743
|
-
expect: [200, 201]
|
|
640
|
+
expect: [200, 201], withStatus: true, responseType: 'text', accept: 'application/json'
|
|
744
641
|
});
|
|
745
642
|
} catch (error) {
|
|
746
643
|
if (/\(HTTP 409\)/.test(error?.message)) {
|
|
@@ -748,7 +645,14 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
748
645
|
}
|
|
749
646
|
throw error;
|
|
750
647
|
}
|
|
751
|
-
|
|
648
|
+
let metadata;
|
|
649
|
+
try {
|
|
650
|
+
const body = JSON.parse(copied.data);
|
|
651
|
+
metadata = entity(body?.d ?? body);
|
|
652
|
+
} catch { /* HTTP acceptance is still evidence when the reply is not usable JSON. */ }
|
|
653
|
+
const identified = typeof metadata?.Id === 'string' && metadata.Id.length > 0;
|
|
654
|
+
return { accepted: true, copied: identified, outcome: identified ? 'succeeded' : 'unverified',
|
|
655
|
+
http_status: copied.status, import_mode, package: pick(metadata, ['Id', 'Name', 'Version']) };
|
|
752
656
|
});
|
|
753
657
|
|
|
754
658
|
// ---------------------------------------------------------------- security material
|
|
@@ -758,26 +662,28 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
758
662
|
'List user credentials, OAuth2 client credentials or keystore entries (keystore system, backup_admin_system or KeyRenewal; default system) with deployment status. Passwords, client secrets and keys are never returned.',
|
|
759
663
|
z.object({
|
|
760
664
|
kind: z.enum(['user_credentials', 'oauth2_client_credentials', 'keystore_entries']),
|
|
761
|
-
keystore: z.enum(KEYSTORES).optional()
|
|
665
|
+
keystore: z.enum(KEYSTORES).optional(),
|
|
666
|
+
...collectionInputs
|
|
762
667
|
}).strict().refine(value => !value.keystore || value.kind === 'keystore_entries', { path: ['keystore'], message: 'keystore is only allowed with kind keystore_entries' }),
|
|
763
668
|
READ_ONLY_ANNOTATIONS,
|
|
764
|
-
async ({ kind, keystore }) => {
|
|
669
|
+
async ({ kind, keystore, ...args }) => {
|
|
765
670
|
if (kind === 'user_credentials') {
|
|
766
|
-
return
|
|
767
|
-
...pick(entry, ['Name', 'Kind', 'Description'
|
|
671
|
+
return pager.page({ ...args, path: '/UserCredentials', mode: 'local', project: entry => redactSecrets({
|
|
672
|
+
...pick(entry, ['Name', 'Kind', 'Description']),
|
|
768
673
|
deployment: deployment(entry.SecurityArtifactDescriptor)
|
|
769
|
-
}))
|
|
674
|
+
}) });
|
|
770
675
|
}
|
|
771
676
|
if (kind === 'oauth2_client_credentials') {
|
|
772
|
-
return
|
|
773
|
-
...pick(entry, ['Name', 'Description', '
|
|
677
|
+
return pager.page({ ...args, path: '/OAuth2ClientCredentials', mode: 'local', project: entry => redactSecrets({
|
|
678
|
+
...pick(entry, ['Name', 'Description', 'ClientAuthentication', 'ScopeContentType']),
|
|
774
679
|
deployment: deployment(entry.SecurityArtifactDescriptor)
|
|
775
|
-
}))
|
|
680
|
+
}) });
|
|
776
681
|
}
|
|
777
|
-
return
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
682
|
+
return pager.page({ ...args, path: '/KeystoreEntries', mode: 'local', query: { keystoreName: keystore ?? 'system' },
|
|
683
|
+
project: entry => ({
|
|
684
|
+
...pick(entry, ['Alias', 'Hexalias', 'Type', 'KeyType', 'Status']),
|
|
685
|
+
...dates(entry, ['ValidNotBefore', 'ValidNotAfter'])
|
|
686
|
+
}) });
|
|
781
687
|
});
|
|
782
688
|
|
|
783
689
|
register('cpi_set_user_credential', 'Set CPI user credential',
|
|
@@ -816,9 +722,9 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
816
722
|
token_service_url: z.string().max(2048).regex(/^https:\/\/\S+$/, 'must be an https URL'),
|
|
817
723
|
client_id: z.string().min(1).max(1024),
|
|
818
724
|
client_secret: secretValue,
|
|
819
|
-
client_authentication: z.enum(['body', 'header']).
|
|
725
|
+
client_authentication: z.enum(['body', 'header']).optional(),
|
|
820
726
|
scope: z.string().max(2048).optional(),
|
|
821
|
-
scope_content_type: z.enum(['urlencoded', 'json']).
|
|
727
|
+
scope_content_type: z.enum(['urlencoded', 'json']).optional(),
|
|
822
728
|
resource: z.string().max(2048).optional(),
|
|
823
729
|
audience: z.string().max(2048).optional(),
|
|
824
730
|
description: z.string().max(1024).optional(),
|
|
@@ -834,15 +740,16 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
834
740
|
TokenServiceUrl: token_service_url,
|
|
835
741
|
ClientId: client_id,
|
|
836
742
|
ClientSecret: client_secret,
|
|
837
|
-
ClientAuthentication: client_authentication,
|
|
838
|
-
Scope: scope ?? '',
|
|
839
|
-
ScopeContentType: scope_content_type,
|
|
840
|
-
Resource: resource ?? '',
|
|
841
|
-
Audience: audience ?? ''
|
|
743
|
+
ClientAuthentication: client_authentication ?? existing?.ClientAuthentication ?? 'body',
|
|
744
|
+
Scope: scope ?? existing?.Scope ?? '',
|
|
745
|
+
ScopeContentType: scope_content_type ?? existing?.ScopeContentType ?? 'urlencoded',
|
|
746
|
+
Resource: resource ?? existing?.Resource ?? '',
|
|
747
|
+
Audience: audience ?? existing?.Audience ?? ''
|
|
842
748
|
}));
|
|
843
749
|
return { [outcome]: true, name };
|
|
844
750
|
} catch (error) {
|
|
845
|
-
|
|
751
|
+
const evidence = cpiReleaseFailure(error);
|
|
752
|
+
throw new IntegrationError(`CPI OAuth2 client credential write failed${evidence.status ? ` (HTTP ${evidence.status})` : ''}; tenant response withheld`, evidence);
|
|
846
753
|
}
|
|
847
754
|
});
|
|
848
755
|
|
|
@@ -859,6 +766,10 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
859
766
|
OVERWRITE_ANNOTATIONS,
|
|
860
767
|
async ({ alias, certificate, update, confirmation: confirmed }) => {
|
|
861
768
|
requireConfirmation(confirmed, `UPLOAD CERTIFICATE ${alias}`);
|
|
769
|
+
let parsedCertificate;
|
|
770
|
+
try { parsedCertificate = new X509Certificate(certificate); }
|
|
771
|
+
catch { throw new IntegrationError('Malformed PEM certificate'); }
|
|
772
|
+
const fingerprint_sha256 = parsedCertificate.fingerprint256.replaceAll(':', '').toLowerCase();
|
|
862
773
|
// Keystore keys are the hex of the UTF-8 alias because slashes are not allowed in the URI even when encoded.
|
|
863
774
|
const hexalias = Buffer.from(alias, 'utf8').toString('hex');
|
|
864
775
|
try {
|
|
@@ -870,22 +781,31 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
870
781
|
expect: [200, 204]
|
|
871
782
|
});
|
|
872
783
|
} catch (error) {
|
|
873
|
-
if (!update && /\(HTTP 400\)/.test(error?.message))
|
|
784
|
+
if (!update && (error?.status === 400 || /\(HTTP 400\)/.test(error?.message))) {
|
|
785
|
+
let exists = /\b(?:entry|alias|certificate)\b.{0,80}\b(?:already exists|exists already|exists)\b/i.test(error.message);
|
|
786
|
+
if (!exists) {
|
|
787
|
+
try {
|
|
788
|
+
const fresh = await cpi('GET', '/KeystoreEntries', { query: { keystoreName: 'system' } });
|
|
789
|
+
exists = list(fresh).some(entry => entry.Alias === alias || entry.Hexalias === hexalias);
|
|
790
|
+
} catch { /* Preserve the original upload cause when alias evidence is unavailable. */ }
|
|
791
|
+
}
|
|
792
|
+
if (exists) throw new IntegrationError(`Keystore alias ${alias} already exists; pass update: true to replace it`);
|
|
793
|
+
}
|
|
874
794
|
throw error;
|
|
875
795
|
}
|
|
876
|
-
return { uploaded: true, alias, hexalias, replaced: update };
|
|
796
|
+
return { uploaded: true, alias, hexalias, replaced: update, fingerprint_sha256, fingerprint_source: 'local_X509Certificate', tenant_fingerprint_verified: false };
|
|
877
797
|
});
|
|
878
798
|
|
|
879
799
|
// ---------------------------------------------------------------- number ranges, partner directory
|
|
880
800
|
|
|
881
801
|
register('cpi_list_number_ranges', 'List CPI number ranges',
|
|
882
802
|
'List number range objects (Name, Description, MinValue, MaxValue, CurrentValue = next value, Rotate, FieldLength, deployment).',
|
|
883
|
-
|
|
803
|
+
collectionInputs,
|
|
884
804
|
READ_ONLY_ANNOTATIONS,
|
|
885
|
-
async
|
|
886
|
-
...pick(range, ['Name', 'Description', 'MinValue', 'MaxValue', 'CurrentValue', 'Rotate', 'FieldLength'
|
|
805
|
+
async args => pager.page({ ...args, path: '/NumberRanges', mode: 'local', project: range => ({
|
|
806
|
+
...pick(range, ['Name', 'Description', 'MinValue', 'MaxValue', 'CurrentValue', 'Rotate', 'FieldLength']),
|
|
887
807
|
...dates(range, ['DeployedOn'])
|
|
888
|
-
})));
|
|
808
|
+
}) }));
|
|
889
809
|
|
|
890
810
|
register('cpi_set_number_range', 'Set CPI number range',
|
|
891
811
|
'Create or replace a number range object. Values are non-negative integers of at most 14 digits with min_value ≤ current_value ≤ max_value; current_value is the next value handed out (default: kept on update, min_value on create). field_length 0 means no zero padding. Requires confirmation exactly equal to SET NUMBER RANGE <name>.',
|
|
@@ -931,15 +851,19 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
931
851
|
});
|
|
932
852
|
|
|
933
853
|
register('cpi_list_partner_parameters', 'List Partner Directory parameters',
|
|
934
|
-
'List Partner Directory
|
|
935
|
-
{ pid: z.string().max(60).regex(partnerChars).optional(),
|
|
854
|
+
'List Partner Directory parameter identities and binary content types, optionally for one partner id. Parameter values and binary content are not returned.',
|
|
855
|
+
z.object({ pid: z.string().max(60).regex(partnerChars).optional(), type: z.enum(['string', 'binary', 'all']).default('all'), ...collectionInputs }).strict()
|
|
856
|
+
.refine(value => !value.cursor || value.type !== 'all', { path: ['cursor'], message: 'cursor requires string or binary type' }),
|
|
936
857
|
READ_ONLY_ANNOTATIONS,
|
|
937
|
-
async ({ pid,
|
|
938
|
-
const query = { $filter: pid ? `Pid eq ${odataKey(pid)}` : undefined
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
858
|
+
async ({ pid, type, ...args }) => {
|
|
859
|
+
const query = { $filter: pid ? `Pid eq ${odataKey(pid)}` : undefined };
|
|
860
|
+
const read = selected => pager.page({ ...args, maxBytes: type === 'all' ? 46 * 1024 : 95 * 1024, path: selected === 'string' ? '/StringParameters' : '/BinaryParameters',
|
|
861
|
+
query, orderBy: 'Pid,Id', project: entry => ({
|
|
862
|
+
...pick(entry, selected === 'string' ? ['Pid', 'Id'] : ['Pid', 'Id', 'ContentType']),
|
|
863
|
+
...dates(entry, ['LastModifiedTime'])
|
|
864
|
+
}) });
|
|
865
|
+
if (type !== 'all') return read(type);
|
|
866
|
+
return { string_parameters: await read('string'), binary_parameters: await read('binary') };
|
|
943
867
|
});
|
|
944
868
|
|
|
945
869
|
register('cpi_set_partner_parameter', 'Set Partner Directory parameter',
|
|
@@ -973,65 +897,6 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
|
|
|
973
897
|
return { [outcome]: true, pid, id, type };
|
|
974
898
|
});
|
|
975
899
|
|
|
976
|
-
// ---------------------------------------------------------------- data stores, variables, JMS (the data store APIs accept no query options)
|
|
977
|
-
|
|
978
|
-
register('cpi_list_data_stores', 'List CPI data stores',
|
|
979
|
-
'List data stores with their iFlow, type, visibility and message counts; optionally only stores with overdue entries.',
|
|
980
|
-
{ overdue_only: z.boolean().default(false) },
|
|
981
|
-
READ_ONLY_ANNOTATIONS,
|
|
982
|
-
async ({ overdue_only }) => list(await cpi('GET', '/DataStores', { query: { overdueonly: overdue_only ? 'true' : undefined } }))
|
|
983
|
-
.map(store => pick(store, ['DataStoreName', 'IntegrationFlow', 'Type', 'Visibility', 'NumberOfMessages', 'NumberOfOverdueMessages'])));
|
|
984
|
-
|
|
985
|
-
register('cpi_list_data_store_entries', 'List data store entries',
|
|
986
|
-
`List entries of one data store (Id, Status, MessageId, due/created/retain dates; up to ${MAX_LIST_ENTRIES}). Entry payloads are never read. type is empty for Global and iFlow data stores.`,
|
|
987
|
-
{ data_store: z.string().min(1).max(200), integration_flow: z.string().min(1).max(200), type: z.string().max(40).default('') },
|
|
988
|
-
READ_ONLY_ANNOTATIONS,
|
|
989
|
-
async ({ data_store, integration_flow, type }) => {
|
|
990
|
-
const entries = list(await cpi('GET', `/DataStores(DataStoreName=${key(data_store)},IntegrationFlow=${key(integration_flow)},Type=${key(type)})/Entries`))
|
|
991
|
-
.map(entry => ({ ...pick(entry, ['Id', 'Status', 'MessageId']), ...dates(entry, ['DueAt', 'CreatedAt', 'RetainUntil']) }));
|
|
992
|
-
return { total: entries.length, returned: Math.min(entries.length, MAX_LIST_ENTRIES), entries: entries.slice(0, MAX_LIST_ENTRIES) };
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
register('cpi_list_variables', 'List CPI variables',
|
|
996
|
-
'List global and iFlow variables (name, iFlow, visibility, updated/retain dates). Values are never read.',
|
|
997
|
-
{},
|
|
998
|
-
READ_ONLY_ANNOTATIONS,
|
|
999
|
-
async () => list(await cpi('GET', '/Variables'))
|
|
1000
|
-
.map(variable => ({ ...pick(variable, ['VariableName', 'IntegrationFlow', 'Visibility']), ...dates(variable, ['UpdatedAt', 'RetainUntil']) })));
|
|
1001
|
-
|
|
1002
|
-
register('cpi_get_jms_broker_status', 'Get JMS broker status',
|
|
1003
|
-
'Return JMS broker capacity and limit flags (0 ok, 1 over limit) and the queues in warning (1) or critical (2) state.',
|
|
1004
|
-
{},
|
|
1005
|
-
READ_ONLY_ANNOTATIONS,
|
|
1006
|
-
async () => {
|
|
1007
|
-
const broker = await cpi('GET', `/JmsBrokers(${key('Broker1')})`, { query: { $expand: 'QueueStates' } });
|
|
1008
|
-
const queues = list(broker?.QueueStates);
|
|
1009
|
-
const unhealthy = queues.filter(queue => Number(queue.State) !== 0).map(queue => pick(queue, ['Name', 'State']));
|
|
1010
|
-
return { broker: pick(broker, JMS_BROKER_FIELDS), queue_count: queues.length, unhealthy_queue_count: unhealthy.length, unhealthy_queues: unhealthy };
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
register('cpi_get_message_log_steps', 'Get message processing steps',
|
|
1014
|
-
`Return the processing steps of the latest run of a message (StepId, ModelStepId, Activity, Status, Error, start/stop), ordered by start time, up to ${MAX_RUN_STEPS}. Payloads are never read.`,
|
|
1015
|
-
{ message_guid: messageGuid },
|
|
1016
|
-
READ_ONLY_ANNOTATIONS,
|
|
1017
|
-
async ({ message_guid }) => {
|
|
1018
|
-
const runs = list(await cpi('GET', `/MessageProcessingLogs(${key(message_guid)})/Runs`));
|
|
1019
|
-
if (!runs.length) return { run_count: 0, run: null, total_steps: 0, steps: [] };
|
|
1020
|
-
const latest = runs.reduce((best, run) => (time(run.RunStart) > time(best.RunStart) ? run : best));
|
|
1021
|
-
const steps = list(await cpi('GET', `/MessageProcessingLogRuns(${key(latest.Id)})/RunSteps`))
|
|
1022
|
-
.sort((left, right) => time(left.StepStart) - time(right.StepStart));
|
|
1023
|
-
return {
|
|
1024
|
-
run_count: runs.length,
|
|
1025
|
-
run: { ...pick(latest, ['Id', 'OverallState', 'LogLevel']), ...dates(latest, ['RunStart', 'RunStop']) },
|
|
1026
|
-
total_steps: steps.length,
|
|
1027
|
-
...(steps.length > MAX_RUN_STEPS ? { truncated: true } : {}),
|
|
1028
|
-
steps: steps.slice(0, MAX_RUN_STEPS).map(step => ({
|
|
1029
|
-
...pick(step, ['StepId', 'ModelStepId', 'Activity', 'Status']),
|
|
1030
|
-
Error: step.Error ? capText(String(step.Error), STEP_ERROR_CAP).text : null,
|
|
1031
|
-
...dates(step, ['StepStart', 'StepStop'])
|
|
1032
|
-
}))
|
|
1033
|
-
};
|
|
1034
|
-
});
|
|
1035
900
|
}
|
|
1036
901
|
|
|
1037
902
|
/** Names of every cpi_* tool, in registration order. */
|