sap-ai-dev-toolkit 0.9.9918 → 0.9.9921

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.
Files changed (59) hide show
  1. package/.github/agents/sap-integration-architect.agent.md +32 -18
  2. package/.github/agents/sap-integration-developer.agent.md +28 -12
  3. package/.github/skills/integration-suite-delivery/SKILL.md +94 -37
  4. package/.github/skills/sap-integration-architect/SKILL.md +49 -25
  5. package/README.md +72 -15
  6. package/dist/checksums.txt +5 -5
  7. package/dist/vsp-darwin-arm64 +0 -0
  8. package/dist/vsp-darwin-x64 +0 -0
  9. package/dist/vsp-linux-arm64 +0 -0
  10. package/dist/vsp-linux-x64 +0 -0
  11. package/dist/vsp-win32-x64.exe +0 -0
  12. package/inventory.md +97 -57
  13. package/package.json +1 -1
  14. package/scripts/check-live-integration.mjs +151 -0
  15. package/scripts/install-local.mjs +11 -4
  16. package/scripts/run-live-integration.mjs +55 -63
  17. package/src/apim-bundle.mjs +416 -13
  18. package/src/apim-cleanup-tools.mjs +145 -0
  19. package/src/apim-content.mjs +157 -0
  20. package/src/apim-delete-tools.mjs +139 -0
  21. package/src/apim-documentation.mjs +162 -0
  22. package/src/apim-inspection.mjs +141 -0
  23. package/src/apim-monitoring.mjs +140 -0
  24. package/src/apim-policy-steps.mjs +31 -0
  25. package/src/apim-runtime.mjs +262 -0
  26. package/src/apim-tools.mjs +202 -109
  27. package/src/cf-integration.mjs +46 -7
  28. package/src/cpi-async-monitoring.mjs +177 -0
  29. package/src/cpi-content.mjs +92 -0
  30. package/src/cpi-monitoring.mjs +225 -0
  31. package/src/cpi-operational.mjs +129 -0
  32. package/src/cpi-release.mjs +273 -0
  33. package/src/cpi-security.mjs +48 -0
  34. package/src/cpi-tools.mjs +184 -319
  35. package/src/devhub-retirement.mjs +345 -0
  36. package/src/devhub-tools.mjs +177 -0
  37. package/src/edge-monitoring.mjs +101 -0
  38. package/src/iflow-bundle.mjs +295 -7
  39. package/src/integration-alerts.mjs +248 -0
  40. package/src/integration-analysis.mjs +488 -0
  41. package/src/integration-capabilities.mjs +181 -0
  42. package/src/integration-collections.mjs +203 -0
  43. package/src/integration-common.mjs +140 -35
  44. package/src/integration-config.mjs +20 -14
  45. package/src/integration-credentials.mjs +16 -10
  46. package/src/integration-http.mjs +255 -67
  47. package/src/integration-messaging.mjs +118 -0
  48. package/src/integration-optional-config.mjs +122 -0
  49. package/src/integration-optional-tools.mjs +153 -0
  50. package/src/integration-release-store.mjs +410 -0
  51. package/src/integration-server.mjs +34 -16
  52. package/src/integration-state.mjs +31 -0
  53. package/src/integration-tools.mjs +44 -8
  54. package/src/integration-transport.mjs +184 -0
  55. package/src/integration-validation.mjs +26 -0
  56. package/src/integration-verify.mjs +143 -62
  57. package/src/open-connectors-tools.mjs +143 -0
  58. package/src/setup.mjs +86 -52
  59. package/tools.md +193 -26
@@ -0,0 +1,177 @@
1
+ // Metadata-only diagnosis of CPI asynchronous processing. No queue/store message content is read.
2
+ import { z } from 'zod';
3
+ import { READ_ONLY_ANNOTATIONS, key, top as topSchema } from './integration-common.mjs';
4
+ import { IntegrationError, odataDate } from './integration-http.mjs';
5
+ import { createCollectionPager, cursorSchema, decodeCollection } from './integration-collections.mjs';
6
+
7
+ const ROOT = '/api/v1';
8
+ const PAGE_BYTES = 88 * 1024;
9
+ const SOURCE_BYTES = 2 * 1024 * 1024;
10
+ const collectionInputs = { top: topSchema(200, 100), cursor: cursorSchema };
11
+ const exactFilters = { integration_flow: z.string().max(200).optional(), visibility: z.string().max(100).optional() };
12
+ const BROKER_NUMBERS = ['Capacity', 'MaxCapacity', 'QueueNumber', 'MaxQueueNumber'];
13
+ const BROKER_FLAGS = ['CapacityOk', 'CapacityWarning', 'CapacityError', 'IsQueuesHigh', 'IsMessageSpoolHigh', 'IsTransactedSessionsHigh', 'IsConsumersHigh', 'IsProducersHigh'];
14
+ const text = value => typeof value === 'string' ? value.slice(0, 512) : null;
15
+ const number = value => (typeof value === 'number' || typeof value === 'string' && /^\d+(?:\.\d+)?$/.test(value)) && Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : null;
16
+ export const normalizeCpiFlag = value => value === true || value === 1 || value === '1' || value === 'true' ? 1
17
+ : value === false || value === 0 || value === '0' || value === 'false' ? 0 : null;
18
+ const selectText = (row, fields) => Object.fromEntries(fields.filter(field => row?.[field] !== undefined).map(field => [field, text(row[field])]));
19
+ function dates(row, fields) {
20
+ return Object.fromEntries(fields.filter(field => row?.[field] !== undefined).map(field => {
21
+ let value;
22
+ try { value = odataDate(row[field]); } catch { value = null; }
23
+ return [field, typeof value === 'string' && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : null];
24
+ }));
25
+ }
26
+ export function projectCpiDataStore(row) {
27
+ return { ...selectText(row, ['DataStoreName', 'IntegrationFlow', 'Type', 'Visibility']),
28
+ NumberOfMessages: number(row?.NumberOfMessages), NumberOfOverdueMessages: number(row?.NumberOfOverdueMessages) };
29
+ }
30
+ export function projectCpiJmsQueue(row) {
31
+ const state = number(row?.State);
32
+ return { ...selectText(row, ['Name', 'Type']), NumbOfMsgs: number(row?.NumbOfMsgs), Size: number(row?.Size),
33
+ State: state, state: state === 0 ? 'healthy' : state === 1 ? 'warning' : state === 2 ? 'critical' : 'unknown',
34
+ FillGrade: number(row?.FillGrade), Active: normalizeCpiFlag(row?.Active), Exclusive: normalizeCpiFlag(row?.Exclusive) };
35
+ }
36
+ export function projectCpiVariable(row) {
37
+ return { ...selectText(row, ['Name', 'VariableName', 'IntegrationFlow', 'Visibility']), ...dates(row, ['UpdatedAt', 'RetainUntil']) };
38
+ }
39
+ function projectEntry(row) {
40
+ return { ...selectText(row, ['Id', 'Status', 'MessageId']), ...dates(row, ['DueAt', 'CreatedAt', 'RetainUntil']),
41
+ ...(typeof row?.MessageId === 'string' && row.MessageId ? { message_log_link: 'unverified' } : {}) };
42
+ }
43
+ function sourceFailure(error) {
44
+ const status = Number.isInteger(error?.status) ? error.status : Number(/HTTP (\d{3})\b/.exec(error?.message ?? '')?.[1]) || null;
45
+ const stop_reason = error?.code === 'INTEGRATION_DEADLINE' ? 'deadline' : /safe size limit/.test(error?.message ?? '') ? 'source_limit' : null;
46
+ return { result: status === 401 ? 'unauthenticated' : status === 403 ? 'forbidden' : status === 404 || status === 501 ? 'unavailable' : 'error',
47
+ ...(status ? { status } : {}), ...(stop_reason ? { stop_reason } : {}) };
48
+ }
49
+ const emptyPage = failure => ({ items: [], returned: 0, total: null, complete: false, next_cursor: null,
50
+ stop_reason: failure.stop_reason ?? 'source_limit', source_result: failure });
51
+ function service(client) {
52
+ const request = (method, path, options) => client.request(method, `${ROOT}${path}`, options);
53
+ return { request, pager: createCollectionPager(client, { root: ROOT, request }) };
54
+ }
55
+ async function pageSafely(pager, options) {
56
+ try {
57
+ const page = await pager.page(options);
58
+ return !page.source_result && !page.complete && !page.next_cursor && ['deadline', 'source_limit'].includes(page.stop_reason)
59
+ ? { ...page, source_result: { result: 'error', stop_reason: page.stop_reason } } : page;
60
+ }
61
+ catch (error) {
62
+ // Cursor validation is a local input error, not evidence about the tenant.
63
+ if (/collection cursor/.test(error?.message ?? '')) throw new IntegrationError('Invalid or expired collection cursor; restart the listing');
64
+ return emptyPage(sourceFailure(error));
65
+ }
66
+ }
67
+ const matches = ({ integration_flow, visibility }) => row => (integration_flow === undefined || row.IntegrationFlow === integration_flow)
68
+ && (visibility === undefined || row.Visibility === visibility);
69
+
70
+ /** Shared bounded metadata helpers; deadline is the transport's absolute internal deadline. */
71
+ export async function readCpiDataStores(client, { top = 100, cursor, integration_flow, visibility, overdue_only = false, deadline } = {}) {
72
+ const { pager } = service(client);
73
+ const exact = matches({ integration_flow, visibility });
74
+ return pageSafely(pager, { path: '/DataStores', mode: 'local', top, cursor, deadline, maxBytes: PAGE_BYTES,
75
+ identity: { integration_flow, visibility, overdue_only },
76
+ filter: row => exact(row) && (!overdue_only || number(row.NumberOfOverdueMessages) > 0), project: projectCpiDataStore });
77
+ }
78
+ export async function readCpiVariables(client, { top = 100, cursor, integration_flow, visibility, deadline } = {}) {
79
+ const { pager } = service(client);
80
+ return pageSafely(pager, { path: '/Variables', mode: 'local', top, cursor, deadline, maxBytes: PAGE_BYTES,
81
+ identity: { integration_flow, visibility }, filter: matches({ integration_flow, visibility }), project: projectCpiVariable });
82
+ }
83
+ export async function readCpiDataStoreEntries(client, { data_store, integration_flow, type = '', top = 100, cursor, deadline } = {}) {
84
+ const { pager, request } = service(client);
85
+ const path = `/DataStores(DataStoreName=${key(data_store)},IntegrationFlow=${key(integration_flow)},Type=${key(type)})`;
86
+ // Validate the collection binding before either the parent-metadata or entry request.
87
+ const entries = await pageSafely(pager, { path: `${path}/Entries`, mode: 'local', top, cursor, deadline,
88
+ maxBytes: PAGE_BYTES, project: projectEntry });
89
+ let store = null;
90
+ let storeResult;
91
+ try {
92
+ const raw = await request('GET', path, { unwrap: false, deadline });
93
+ const row = raw?.d ?? raw;
94
+ if (!row || Array.isArray(row) || row.DataStoreName !== data_store || row.IntegrationFlow !== integration_flow || row.Type !== type) {
95
+ throw new IntegrationError('Unexpected data store identity');
96
+ }
97
+ store = projectCpiDataStore(row);
98
+ storeResult = { result: 'ok' };
99
+ } catch (error) { storeResult = sourceFailure(error); }
100
+ return { ...entries, store, source_results: { entries: entries.source_result ?? { result: 'ok' }, store: storeResult } };
101
+ }
102
+ export async function readCpiJmsQueues(client, { top = 100, cursor, name, deadline } = {}) {
103
+ const { pager } = service(client);
104
+ return pageSafely(pager, { path: '/Queues', mode: 'local', top, cursor, deadline, maxBytes: PAGE_BYTES,
105
+ identity: { name }, filter: name === undefined ? undefined : row => row.Name === name, project: projectCpiJmsQueue });
106
+ }
107
+ export async function readCpiJmsQueue(client, { name, deadline } = {}) {
108
+ const page = await readCpiJmsQueues(client, { name, top: 2, deadline });
109
+ return { name, found: page.items.length === 1 ? true : page.complete && page.items.length === 0 ? false : null,
110
+ queue: page.items.length === 1 ? page.items[0] : null, complete: page.complete,
111
+ ...(page.items.length > 1 ? { identity_ambiguous: true } : {}), ...(page.source_result ? { source_result: page.source_result } : {}),
112
+ stop_reason: page.stop_reason };
113
+ }
114
+ async function brokerSource(request, expand, deadline) {
115
+ try {
116
+ const raw = await request('GET', `/JmsBrokers(${key('Broker1')})`, { query: { $expand: expand }, unwrap: false, deadline });
117
+ if (Buffer.byteLength(JSON.stringify(raw)) > SOURCE_BYTES) throw new IntegrationError('Response exceeds safe size limit');
118
+ const broker = raw?.d ?? raw;
119
+ if (!broker || typeof broker !== 'object' || Array.isArray(broker) || !Object.hasOwn(broker, expand)) throw new IntegrationError('Missing broker metadata');
120
+ const collection = decodeCollection(broker[expand]);
121
+ return { broker, raw: broker[expand], rows: collection.items,
122
+ complete: !collection.next && (collection.total === null || collection.total === collection.items.length), source_result: { result: 'ok' } };
123
+ } catch (error) { return { rows: [], complete: false, source_result: sourceFailure(error) }; }
124
+ }
125
+ export async function readCpiJmsBrokerStatus(client, { deadline } = {}) {
126
+ const { pager, request } = service(client);
127
+ const [states, inactive] = await Promise.all(['QueueStates', 'InactiveQueues'].map(expand => brokerSource(request, expand, deadline)));
128
+ const brokerRow = states.broker ?? inactive.broker;
129
+ const broker = brokerRow ? Object.fromEntries([
130
+ ...BROKER_NUMBERS.map(field => [field, number(brokerRow[field])]), ...BROKER_FLAGS.map(field => [field, normalizeCpiFlag(brokerRow[field])])
131
+ ]) : null;
132
+ const summarize = async (source, path, filter, project) => {
133
+ if (source.raw === undefined) return emptyPage(source.source_result);
134
+ const page = await pageSafely(pager, { path, raw: source.raw, mode: 'local', top: 200, maxBytes: 24 * 1024, deadline,
135
+ sourceComplete: source.complete, filter, project });
136
+ // Broker status is a single bounded snapshot, not a resumable list endpoint.
137
+ return page.next_cursor ? { ...page, next_cursor: null, complete: false, stop_reason: 'source_limit' } : page;
138
+ };
139
+ const queueState = row => { const State = number(row.State); return { ...selectText(row, ['Name']), State,
140
+ state: State === 0 ? 'healthy' : State === 1 ? 'warning' : State === 2 ? 'critical' : 'unknown' }; };
141
+ const unhealthy = row => number(row.State) === 1 || number(row.State) === 2;
142
+ const [unhealthyQueues, unknownQueues, stoppedQueues] = await Promise.all([
143
+ summarize(states, '/JmsBrokers/QueueStates', unhealthy, queueState),
144
+ summarize(states, '/JmsBrokers/UnknownQueueStates', row => ![0, 1, 2].includes(number(row.State)), queueState),
145
+ summarize(inactive, '/JmsBrokers/InactiveQueues', undefined, row => ({ ...selectText(row, ['Name']), stopped: true }))
146
+ ]);
147
+ return { broker, queue_count: states.complete ? states.rows.length : null,
148
+ unhealthy_queue_count: states.complete ? states.rows.filter(unhealthy).length : null,
149
+ unknown_queue_count: states.complete ? states.rows.filter(row => ![0, 1, 2].includes(number(row.State))).length : null,
150
+ stopped_queue_count: inactive.complete ? inactive.rows.length : null,
151
+ unhealthy_queues: unhealthyQueues, unknown_queues: unknownQueues, stopped_queues: stoppedQueues,
152
+ complete: states.complete && inactive.complete && unhealthyQueues.complete && unknownQueues.complete && stoppedQueues.complete,
153
+ source_results: { queue_states: states.source_result, inactive_queues: inactive.source_result } };
154
+ }
155
+
156
+ export function registerCpiAsyncMonitoringTools(register, client) {
157
+ register('cpi_list_data_stores', 'List CPI data stores',
158
+ 'List bounded data store metadata with exact local iFlow/visibility filters and store-reported backlog counts. No query options or message bodies are read.',
159
+ { ...collectionInputs, ...exactFilters, overdue_only: z.boolean().default(false) }, READ_ONLY_ANNOTATIONS,
160
+ args => readCpiDataStores(client, args));
161
+ register('cpi_list_data_store_entries', 'List data store entries',
162
+ 'List bounded entry metadata and separate store-reported backlog counts. Empty integration_flow selects a global store. MessageId links are unverified; no payloads are read.',
163
+ { data_store: z.string().min(1).max(200), integration_flow: z.string().max(200), type: z.string().max(40).default(''), ...collectionInputs },
164
+ READ_ONLY_ANNOTATIONS, args => readCpiDataStoreEntries(client, args));
165
+ register('cpi_list_variables', 'List CPI variables',
166
+ 'List bounded variable metadata with exact local iFlow/visibility filters. No values or OData query options are read.',
167
+ { ...collectionInputs, ...exactFilters }, READ_ONLY_ANNOTATIONS, args => readCpiVariables(client, args));
168
+ register('cpi_list_jms_queues', 'List JMS queues',
169
+ 'List bounded JMS queue metadata from /Queues. Unrecognized or missing flags remain unknown; queue message bodies are never read.',
170
+ collectionInputs, READ_ONLY_ANNOTATIONS, args => readCpiJmsQueues(client, args));
171
+ register('cpi_get_jms_queue', 'Get JMS queue metadata',
172
+ 'Find one exact queue name in bounded /Queues metadata. No message bodies are read.',
173
+ { name: z.string().min(1).max(200) }, READ_ONLY_ANNOTATIONS, args => readCpiJmsQueue(client, args));
174
+ register('cpi_get_jms_broker_status', 'Get JMS broker status',
175
+ 'Read broker capacity/limit flags and independently retrieve warning/critical queue states and stopped queue identities. Missing access remains explicit partial evidence.',
176
+ {}, READ_ONLY_ANNOTATIONS, args => readCpiJmsBrokerStatus(client, args));
177
+ }
@@ -0,0 +1,92 @@
1
+ // Private original archive/configuration reads. Never return this object directly from a tool.
2
+ import { entity, key, pick } from './integration-common.mjs';
3
+ import { createCollectionPager, decodeCollection } from './integration-collections.mjs';
4
+ import { bundleDigest, readBundle, parseProperties, strFromU8 } from './iflow-bundle.mjs';
5
+ import { stateDigest } from './integration-state.mjs';
6
+ import { IntegrationError } from './integration-http.mjs';
7
+
8
+ export const CPI_BASE = '/api/v1';
9
+ export const CPI_PACKAGE_ARTIFACT_SETS = Object.freeze({
10
+ iflows: 'IntegrationDesigntimeArtifacts',
11
+ value_mappings: 'ValueMappingDesigntimeArtifacts',
12
+ script_collections: 'ScriptCollectionDesigntimeArtifacts',
13
+ message_mappings: 'MessageMappingDesigntimeArtifacts'
14
+ });
15
+ export const cpiArtifactPath = (id, version = 'active') => `/IntegrationDesigntimeArtifacts(Id=${key(id)},Version=${key(version)})`;
16
+
17
+ export async function readCpiContent(client, id, version = 'active', { deadline } = {}) {
18
+ const path = cpiArtifactPath(id, version);
19
+ const artifact = entity(await client.request('GET', `${CPI_BASE}${path}`, { deadline }));
20
+ if (!artifact || artifact.Id !== id || typeof artifact.Version !== 'string') throw new IntegrationError('CPI artifact identity/version is unavailable');
21
+ delete artifact.ArtifactContent;
22
+ const bytes = await client.request('GET', `${CPI_BASE}${path}/$value`, {
23
+ responseType: 'bytes', accept: 'application/zip, application/octet-stream', deadline
24
+ });
25
+ const files = readBundle(bytes);
26
+ const configurations = await readCpiConfigurations(client, id, version, { deadline });
27
+ const names = new Set(configurations.map(entry => entry.ParameterKey));
28
+ const defaults = files.get('src/main/resources/parameters.prop');
29
+ for (const name of Object.keys(defaults ? parseProperties(strFromU8(defaults)) : {})) names.add(name);
30
+ const bundle_sha256 = bundleDigest(files);
31
+ return { artifact, bytes, files, configurations, bundle_sha256, state_sha256: stateDigest(bundle_sha256, configurations),
32
+ parameter_names: [...names].sort(), selector: version, resolved_version: artifact.Version };
33
+ }
34
+
35
+ export async function readCpiConfigurations(client, id, version = 'active', { deadline } = {}) {
36
+ try {
37
+ const path = cpiArtifactPath(id, version);
38
+ const pager = createCollectionPager(client, { root: CPI_BASE });
39
+ const raw = await client.request('GET', `${CPI_BASE}${path}/Configurations`, { unwrap: false, deadline });
40
+ const mode = decodeCollection(raw).next ? 'odata' : 'local';
41
+ const configurations = [];
42
+ const names = new Set();
43
+ let cursor;
44
+ for (let pageNumber = 0; pageNumber < 10; pageNumber += 1) {
45
+ const page = await pager.page({ path: `${path}/Configurations`, top: 200, cursor, deadline,
46
+ raw, mode, project: entry => pick(entry, ['ParameterKey', 'ParameterValue', 'DataType']) });
47
+ for (const entry of page.items) {
48
+ if (typeof entry.ParameterKey !== 'string' || typeof entry.ParameterValue !== 'string' || typeof entry.DataType !== 'string' || names.has(entry.ParameterKey)) {
49
+ throw new IntegrationError('CPI configuration response is incomplete or ambiguous');
50
+ }
51
+ names.add(entry.ParameterKey);
52
+ configurations.push(entry);
53
+ }
54
+ if (page.complete) return configurations;
55
+ if (!page.next_cursor) break;
56
+ cursor = page.next_cursor;
57
+ }
58
+ throw new IntegrationError('CPI configurations could not be read completely within the source limit');
59
+ } catch (error) {
60
+ // Configurations are private; a failed tenant response can echo their values.
61
+ const evidence = cpiReleaseFailure(error);
62
+ throw new IntegrationError(`CPI configuration read is incomplete or failed${evidence.status ? ` (HTTP ${evidence.status})` : ''}; values withheld`,
63
+ { ...evidence, ...(error?.code === 'INTEGRATION_DEADLINE' ? { code: error.code } : {}) });
64
+ }
65
+ }
66
+
67
+ /** Operational failures intentionally omit arbitrary tenant text, which can echo configuration/source. */
68
+ export function cpiReleaseFailure(error) {
69
+ const status = Number(error?.status);
70
+ return { result: status === 401 ? 'unauthenticated' : status === 403 ? 'forbidden' : [404, 501].includes(status) ? 'unavailable' : 'error',
71
+ ...(Number.isInteger(status) && status >= 100 && status <= 599 ? { status } : {}),
72
+ ...(error?.code === 'INTEGRATION_DEADLINE' ? { stop_reason: 'deadline' } : {}) };
73
+ }
74
+
75
+ export async function writeCpiConfigurations(client, id, configurations, { deadline } = {}) {
76
+ // Reserve space for both exact progress and the enclosing mutation outcome before any write.
77
+ if (Buffer.byteLength(JSON.stringify(configurations.map(entry => entry.ParameterKey))) > 24000) {
78
+ throw new IntegrationError('CPI configuration parameter names exceed the safe progress-report limit');
79
+ }
80
+ const updated = [];
81
+ for (const parameter of configurations) {
82
+ try {
83
+ await client.request('PUT', `${CPI_BASE}${cpiArtifactPath(id)}/$links/Configurations(${key(parameter.ParameterKey)})`, {
84
+ json: { ParameterValue: parameter.ParameterValue, DataType: parameter.DataType }, expect: [200, 202, 204], responseType: 'none', deadline
85
+ });
86
+ updated.push(parameter.ParameterKey);
87
+ } catch (error) {
88
+ return { updated, failed: { key: parameter.ParameterKey, ...cpiReleaseFailure(error) } };
89
+ }
90
+ }
91
+ return { updated, failed: null };
92
+ }
@@ -0,0 +1,225 @@
1
+ // Payload-free Cloud Integration message monitoring and incident navigation.
2
+ import { z } from 'zod';
3
+ import { READ_ONLY_ANNOTATIONS, ERROR_TEXT_CAP, capText, entity, isoDate, isoToOdataDateTime, key, pick, redactSecrets, timeWindow, top as topSchema } from './integration-common.mjs';
4
+ import { IntegrationError, odataDate, odataKey } from './integration-http.mjs';
5
+ import { createCollectionPager, cursorSchema, decodeCollection } from './integration-collections.mjs';
6
+ import { redactText } from './redact.mjs';
7
+ import { registerCpiAsyncMonitoringTools } from './cpi-async-monitoring.mjs';
8
+
9
+ export const MESSAGE_STATUSES = Object.freeze(['COMPLETED', 'PROCESSING', 'RETRY', 'ESCALATED', 'FAILED', 'CANCELLED', 'DISCARDED', 'ABANDONED']);
10
+ export const messageGuid = z.string().regex(/^[A-Za-z0-9_-]{10,64}$/);
11
+ export const mplFilterInputs = {
12
+ iflow_id: z.string().regex(/^[A-Za-z][A-Za-z0-9_.-]{0,99}$/).optional(),
13
+ correlation_id: z.string().min(1).max(200).optional(),
14
+ application_message_id: z.string().min(1).max(200).optional(),
15
+ custom_header: z.object({ name: z.string().min(1).max(200), value: z.string().max(2000) }).strict().optional()
16
+ };
17
+ export const mplListInputs = {
18
+ ...mplFilterInputs, status: z.enum(MESSAGE_STATUSES).optional(), from: isoDate.optional(), to: isoDate.optional(),
19
+ top: topSchema(100, 20), cursor: cursorSchema
20
+ };
21
+ const pageInputs = { top: topSchema(200, 100), cursor: cursorSchema };
22
+ const scalarPick = (row, names) => Object.fromEntries(Object.entries(pick(row, names)).filter(([, value]) => value === null || ['string', 'number', 'boolean'].includes(typeof value)));
23
+ const dates = (row, names) => Object.fromEntries(Object.entries(scalarPick(row, names)).map(([name, value]) => [name, odataDate(value)]));
24
+ const time = value => Date.parse(odataDate(value)) || 0;
25
+ const emptyPage = () => ({ items: [], returned: 0, total: null, complete: true, next_cursor: null, stop_reason: 'end' });
26
+ const safeDiagnostic = value => redactText(value).replace(/https?:\/\/[^\s<>"']+/g, text => {
27
+ try { const url = new URL(text); return `${url.origin}${url.pathname}`; } catch { return '[url withheld]'; }
28
+ });
29
+ const scopeOf = (tool, args) => JSON.stringify([tool, args.iflow_id ?? null, args.status ?? null, args.correlation_id ?? null, args.application_message_id ?? null, args.custom_header?.name ?? null, args.custom_header?.value ?? null]);
30
+
31
+ /** One exact filter contract for both MPL collections and count sources. */
32
+ export function messageLogQuery(args, window) {
33
+ return {
34
+ $filter: [
35
+ `LogStart ge datetime'${isoToOdataDateTime(window.from)}'`,
36
+ `LogStart lt datetime'${isoToOdataDateTime(window.to)}'`,
37
+ args.iflow_id !== undefined && `IntegrationArtifact/Id eq ${odataKey(args.iflow_id)}`,
38
+ args.status !== undefined && `Status eq ${odataKey(args.status)}`,
39
+ args.correlation_id !== undefined && `CorrelationId eq ${odataKey(args.correlation_id)}`,
40
+ args.application_message_id !== undefined && `ApplicationMessageId eq ${odataKey(args.application_message_id)}`
41
+ ].filter(Boolean).join(' and '),
42
+ ...(args.custom_header ? { filterCustomHeaderProperties: `${odataKey(args.custom_header.name)} eq ${odataKey(args.custom_header.value)}` } : {})
43
+ };
44
+ }
45
+
46
+ /** Allowlist unknown observed status strings, never payload/navigation bodies. */
47
+ export function projectMessageLog(row) {
48
+ const log = {
49
+ ...scalarPick(row, ['MessageGuid', 'CorrelationId', 'ApplicationMessageId', 'IntegrationFlowName', 'Status', 'CustomStatus', 'Sender', 'Receiver']),
50
+ ...dates(row, ['LogStart', 'LogEnd'])
51
+ };
52
+ if (row.IntegrationArtifact && typeof row.IntegrationArtifact === 'object') log.IntegrationArtifact = scalarPick(row.IntegrationArtifact, ['Id', 'Type']);
53
+ for (const name of ['Sender', 'Receiver']) if (typeof log[name] === 'string' && /^https?:\/\//i.test(log[name])) log[name] = safeDiagnostic(log[name]);
54
+ return log;
55
+ }
56
+ export const projectMessageRun = row => ({
57
+ ...scalarPick(row, ['Id', 'OverallState', 'LogLevel']),
58
+ ...dates(row, ['RunStart', 'RunStop'])
59
+ });
60
+ export function projectMessageStep(row) {
61
+ const error = capText(safeDiagnostic(row.Error ?? ''), 1024);
62
+ return { ...scalarPick(row, ['StepId', 'ModelStepId', 'Activity', 'Status']), Error: error.text || null, error_truncated: error.truncated, ...dates(row, ['StepStart', 'StepStop']) };
63
+ }
64
+
65
+ /** Authenticate the cursor before recovering the frozen default window. */
66
+ export function messageLogWindow(pager, args, scope) {
67
+ if ((args.from === undefined) !== (args.to === undefined)) throw new IntegrationError('from and to must be supplied together');
68
+ const context = pager.cursorContext(args.cursor, { scope });
69
+ const supplied = args.from === undefined ? null : timeWindow(args.from, args.to);
70
+ if (context) {
71
+ const window = timeWindow(context.window?.from, context.window?.to);
72
+ if (supplied && (supplied.from !== window.from || supplied.to !== window.to)) throw new IntegrationError('The monitoring window changed; restart the listing');
73
+ return window;
74
+ }
75
+ const now = Date.now();
76
+ return supplied ?? timeWindow(new Date(now - 86400000).toISOString(), new Date(now).toISOString());
77
+ }
78
+ export async function listMessageLogs(pager, args, { scope = scopeOf('mpl', args), maxBytes = 94 * 1024, deadline } = {}) {
79
+ const window = messageLogWindow(pager, args, scope);
80
+ const page = await pager.page({ path: '/MessageProcessingLogs', query: { ...messageLogQuery(args, window), $inlinecount: 'allpages' },
81
+ top: args.top, cursor: args.cursor, scope, context: { window }, orderBy: 'LogStart desc', identity: 'MessageGuid', consistency: 'best_effort',
82
+ project: projectMessageLog, maxBytes, deadline });
83
+ return { ...page, window };
84
+ }
85
+
86
+ /** Shared named MPL read; callers supply the service-root-pinned GET and pager. */
87
+ export async function getMessageLogSeed(read, guid) {
88
+ const row = entity(await read(`/MessageProcessingLogs(${key(guid)})`));
89
+ if (!row || typeof row !== 'object' || Array.isArray(row) || row.MessageGuid !== guid) throw new IntegrationError('Message not found or invalid MPL metadata');
90
+ if (Buffer.byteLength(JSON.stringify(projectMessageLog(row), null, 2)) > 4096) throw new IntegrationError('MPL identity metadata exceeds the safe size limit');
91
+ return row;
92
+ }
93
+
94
+ export async function getMessageLog(read, pager, guid) {
95
+ const path = `/MessageProcessingLogs(${key(guid)})`;
96
+ const log = projectMessageLog(await getMessageLogSeed(read, guid));
97
+ const text = await read(`${path}/ErrorInformation/$value`, { responseType: 'text', accept: '*/*', expect: [200, 204, 404] });
98
+ const error = capText(safeDiagnostic(text ?? ''), ERROR_TEXT_CAP);
99
+ const headersPage = await pager.page({ path: `${path}/CustomHeaderProperties`, mode: 'local', top: 200, sourceComplete: false,
100
+ maxBytes: 60 * 1024, project: row => redactSecrets(scalarPick(row, ['Name', 'Value'])) });
101
+ return { log, error_information: error.text || null, error_information_truncated: error.truncated,
102
+ custom_header_properties: headersPage.items, custom_header_properties_complete: headersPage.complete, custom_header_properties_truncated: headersPage.stop_reason !== 'end' };
103
+ }
104
+
105
+ const sourceFailure = error => ({ result: error?.status === 401 ? 'unauthenticated' : error?.status === 403 ? 'forbidden' : [404, 501].includes(error?.status) ? 'unavailable' : 'error', ...(Number.isInteger(error?.status) ? { status: error.status } : {}), ...(error?.code === 'INTEGRATION_DEADLINE' ? { stop_reason: 'deadline' } : {}) });
106
+
107
+ /** Exact tenant counts, independently checked with no more than three in flight. */
108
+ export async function getMessageStatistics(client, args, { deadline = Date.now() + 30000 } = {}) {
109
+ const window = timeWindow(args.from, args.to);
110
+ const labels = ['total', ...MESSAGE_STATUSES];
111
+ const counts = {};
112
+ const checks = {};
113
+ let next = 0;
114
+ await Promise.all(Array.from({ length: 3 }, async () => {
115
+ while (next < labels.length) {
116
+ const label = labels[next++];
117
+ try {
118
+ const text = await client.request('GET', '/api/v1/MessageProcessingLogs/$count', { query: messageLogQuery({ ...args, status: label === 'total' ? undefined : label }, window), responseType: 'text', deadline });
119
+ if (typeof text !== 'string' || !/^\d+$/.test(text.trim()) || !Number.isSafeInteger(Number(text.trim()))) throw new IntegrationError('Invalid MPL count response');
120
+ counts[label] = Number(text.trim());
121
+ checks[label] = { result: 'ok' };
122
+ } catch (error) { counts[label] = null; checks[label] = sourceFailure(error); }
123
+ }
124
+ }));
125
+ const allCounts = labels.every(label => counts[label] !== null);
126
+ const classified = allCounts ? MESSAGE_STATUSES.reduce((sum, label) => sum + counts[label], 0) : null;
127
+ const consistent = allCounts && Number.isSafeInteger(classified) && classified <= counts.total;
128
+ const complete = allCounts && consistent;
129
+ return { window, total: counts.total, count_by_status: Object.fromEntries(MESSAGE_STATUSES.map(label => [label, counts[label]])),
130
+ unclassified: complete ? counts.total - classified : null, checks, complete,
131
+ coverage: !allCounts ? 'partial' : !consistent ? 'inconsistent' : counts.total === 0 ? 'no_traffic' : 'complete',
132
+ failure_rate_statuses: ['FAILED', 'ESCALATED', 'ABANDONED'],
133
+ failure_rate: complete && counts.total > 0 ? (counts.FAILED + counts.ESCALATED + counts.ABANDONED) / counts.total : null };
134
+ }
135
+
136
+ export function registerCpiMonitoringTools(register, client) {
137
+ const cpi = (path, options) => client.request('GET', `/api/v1${path}`, options);
138
+ const pager = createCollectionPager(client, { root: '/api/v1', request: (_method, path, options) => cpi(path, options) });
139
+ const getSeed = guid => getMessageLogSeed(cpi, guid);
140
+ async function readRuns(guid) {
141
+ const raw = await cpi(`/MessageProcessingLogs(${key(guid)})/Runs`, { unwrap: false });
142
+ const decoded = decodeCollection(raw);
143
+ return { raw, ...decoded, complete: !decoded.next && decoded.total !== null && decoded.total === decoded.items.length };
144
+ }
145
+ register('cpi_list_message_logs', 'List message processing logs',
146
+ 'List bounded MPL metadata in a fixed UTC [from,to) LogStart window of at most 31 days. Supply both timestamps or neither (frozen last 24 hours). Continuations retain that window; traversal is best effort.',
147
+ mplListInputs, READ_ONLY_ANNOTATIONS, args => listMessageLogs(pager, args));
148
+
149
+ register('cpi_get_message_statistics', 'Get message statistics',
150
+ 'Count total and eight known MPL statuses for the exact explicit UTC [from,to) window, at most 31 days. Partial/inconsistent counts have no overall failure rate; zero messages means no traffic.',
151
+ { ...mplFilterInputs, from: isoDate, to: isoDate }, READ_ONLY_ANNOTATIONS, args => getMessageStatistics(client, args));
152
+
153
+ register('cpi_get_message_log', 'Get message processing log',
154
+ 'Read allowlisted MPL metadata, bounded error text and custom header properties. Payload attachments are never read; credential-shaped header values are withheld.',
155
+ { message_guid: messageGuid }, READ_ONLY_ANNOTATIONS, ({ message_guid }) => getMessageLog(cpi, pager, message_guid));
156
+
157
+ register('cpi_list_message_log_runs', 'List message processing runs',
158
+ 'Page bounded run identities and status metadata for one existing message. Missing messages and messages with no runs are distinct. Run navigation completeness remains explicit.',
159
+ { message_guid: messageGuid, ...pageInputs }, READ_ONLY_ANNOTATIONS, async ({ message_guid, ...args }) => {
160
+ const scope = `runs:${message_guid}`;
161
+ pager.validateCursor(args.cursor, { scope });
162
+ await getSeed(message_guid);
163
+ const page = await pager.page({ ...args, path: `/MessageProcessingLogs(${key(message_guid)})/Runs`, mode: 'local', sourceComplete: false, scope,
164
+ sort: (a, b) => time(b.RunStart) - time(a.RunStart), project: projectMessageRun });
165
+ const noRuns = page.returned === 0 && !page.source_result && (await readRuns(message_guid)).items.length === 0;
166
+ return { ...page, message_guid, ...(noRuns ? { outcome: 'no_runs' } : {}) };
167
+ });
168
+
169
+ register('cpi_get_message_log_steps', 'Get message processing steps',
170
+ 'Page run-step metadata ordered by start time. An explicit run must belong to the message; omission freezes the latest observed run across pages. Error text is bounded; payloads are never read.',
171
+ { message_guid: messageGuid, run_id: z.string().min(1).max(200).optional(), ...pageInputs }, READ_ONLY_ANNOTATIONS, async ({ message_guid, run_id, ...args }) => {
172
+ const scope = JSON.stringify(['steps', message_guid, run_id ?? null]);
173
+ const context = pager.cursorContext(args.cursor, { scope });
174
+ await getSeed(message_guid);
175
+ const runs = await readRuns(message_guid);
176
+ if (!runs.items.length) return { ...emptyPage(), complete: runs.complete, stop_reason: runs.complete ? 'end' : 'source_limit', outcome: 'no_runs', run: null, message_guid };
177
+ const selectedId = context?.run_id ?? run_id;
178
+ const selected = selectedId === undefined ? runs.items.reduce((best, row) => time(row.RunStart) > time(best.RunStart) ? row : best) : runs.items.find(row => String(row.Id) === selectedId);
179
+ if (!selected) throw new IntegrationError(runs.complete ? 'run_id does not belong to this message' : 'run_id was not found in the bounded message runs; membership is unverified');
180
+ if (!['string', 'number'].includes(typeof selected.Id) || !String(selected.Id).length || String(selected.Id).length > 200) throw new IntegrationError('Invalid message run identity');
181
+ const run = projectMessageRun(selected);
182
+ const metadataBytes = Buffer.byteLength(JSON.stringify({ run, message_guid }, null, 2)) + 1024;
183
+ if (metadataBytes > 94 * 1024) return { ...emptyPage(), complete: false, stop_reason: 'output_limit', run: null, run_metadata_truncated: true, message_guid };
184
+ const page = await pager.page({ ...args, path: `/MessageProcessingLogRuns(${key(selected.Id)})/RunSteps`, mode: 'local', sourceComplete: false,
185
+ scope, context: { run_id: String(selected.Id) }, maxBytes: 95 * 1024 - metadataBytes,
186
+ sort: (a, b) => time(a.StepStart) - time(b.StepStart), project: projectMessageStep });
187
+ return { ...page, run, message_guid, runs_complete: runs.complete };
188
+ });
189
+
190
+ register('cpi_trace_message', 'Trace correlated messages',
191
+ 'Follow only the seed message’s exact nonempty CorrelationId inside an explicit UTC [from,to) window. Return bounded message/artifact identities and allowlisted run metadata when present; never infer business-ID or APIM relationships.',
192
+ { message_guid: messageGuid, from: isoDate, to: isoDate, top: topSchema(100, 20), cursor: cursorSchema }, READ_ONLY_ANNOTATIONS,
193
+ async ({ message_guid, ...args }) => {
194
+ const scope = `trace:${message_guid}`;
195
+ // Invalid or changed cursors must fail before even the seed request.
196
+ const window = messageLogWindow(pager, args, scope);
197
+ const context = pager.cursorContext(args.cursor, { scope });
198
+ const seed = projectMessageLog(await getSeed(message_guid));
199
+ const correlation = seed.CorrelationId;
200
+ if (context?.correlation_id !== undefined && context.correlation_id !== correlation) return { ...emptyPage(), complete: false, stop_reason: 'source_changed', window, seed };
201
+ if (typeof correlation !== 'string' || !correlation.trim()) return { ...emptyPage(), items: [seed], returned: 1, total: 1, stop_reason: 'no_correlation_id', window, seed };
202
+ const page = await pager.page({ path: '/MessageProcessingLogs', query: { ...messageLogQuery({ correlation_id: correlation }, window), $inlinecount: 'allpages' },
203
+ top: args.top, cursor: args.cursor, scope, context: { window, correlation_id: correlation }, orderBy: 'LogStart desc', consistency: 'best_effort', identity: 'MessageGuid',
204
+ project: projectMessageLog, maxBytes: 90 * 1024 });
205
+ const run_checks = [];
206
+ // Run identities are independently bounded; unreadable runs never hide healthy message evidence.
207
+ const items = [];
208
+ for (const item of page.items) {
209
+ try {
210
+ const runs = await readRuns(item.MessageGuid);
211
+ items.push({ ...item, runs: runs.items.slice(0, 10).map(projectMessageRun), runs_complete: runs.complete && runs.items.length <= 10 });
212
+ run_checks.push({ message_guid: item.MessageGuid, result: 'ok' });
213
+ } catch (error) { items.push({ ...item, runs: [], runs_complete: false }); run_checks.push({ message_guid: item.MessageGuid, ...sourceFailure(error) }); }
214
+ }
215
+ // Reserve a fixed run budget per page instead of letting ancillary metadata exceed the output envelope.
216
+ if (Buffer.byteLength(JSON.stringify({ ...page, items, seed, run_checks }, null, 2)) > 95 * 1024) {
217
+ return { ...page, complete: false, stop_reason: page.stop_reason === 'end' ? 'source_limit' : page.stop_reason, related_messages_complete: page.complete, seed, run_checks: [], runs_omitted: true, window };
218
+ }
219
+ const runsComplete = items.every(item => item.runs_complete);
220
+ return { ...page, complete: page.complete && runsComplete, stop_reason: page.stop_reason === 'end' && !runsComplete ? 'source_limit' : page.stop_reason,
221
+ related_messages_complete: page.complete, items, seed, run_checks, window };
222
+ });
223
+
224
+ registerCpiAsyncMonitoringTools(register, client);
225
+ }
@@ -0,0 +1,129 @@
1
+ // On-demand management signals, not a background monitor or receiver-side health proof.
2
+ import { z } from 'zod';
3
+ import { READ_ONLY_ANNOTATIONS, isoDate, timeWindow } from './integration-common.mjs';
4
+ import { createCollectionPager } from './integration-collections.mjs';
5
+ import { getMessageStatistics } from './cpi-monitoring.mjs';
6
+ import { readCpiDataStores, readCpiJmsQueues, readCpiJmsBrokerStatus } from './cpi-async-monitoring.mjs';
7
+ import { readCertificateHealth } from './cpi-security.mjs';
8
+
9
+ const pageFailure = error => ({ result: error?.status === 401 ? 'unauthenticated' : error?.status === 403 ? 'forbidden' : [404, 501].includes(error?.status) ? 'unavailable' : 'error',
10
+ status: Number.isInteger(error?.status) ? error.status : null,
11
+ stop_reason: error?.code === 'INTEGRATION_DEADLINE' ? 'deadline' : 'source_limit' });
12
+
13
+ async function observePages(read, inspect, deadline) {
14
+ let cursor;
15
+ let observed = 0;
16
+ let pages = 0;
17
+ try {
18
+ for (;;) {
19
+ if (Date.now() >= deadline) return { result: 'error', complete: false, observed, stop_reason: 'deadline' };
20
+ const page = await read(cursor);
21
+ for (const item of page.items ?? []) { inspect(item); observed += 1; }
22
+ pages += 1;
23
+ if (page.source_result && page.source_result.result !== 'ok') return { ...page.source_result, complete: false, observed, stop_reason: page.stop_reason };
24
+ if (!page.next_cursor) return { result: 'ok', complete: page.complete, observed, stop_reason: page.stop_reason };
25
+ if (pages >= 20 || observed >= 2000) return { result: 'ok', complete: false, observed, stop_reason: 'source_limit' };
26
+ cursor = page.next_cursor;
27
+ }
28
+ } catch (error) { return { ...pageFailure(error), complete: false, observed }; }
29
+ }
30
+
31
+ export async function getOperationalSummary(client, args, { summary = {}, deadline = Date.now() + 30000 } = {}) {
32
+ const window = timeWindow(args.from, args.to);
33
+ const evaluated_at = new Date().toISOString();
34
+ const findings = [];
35
+ let findings_truncated = false;
36
+ const add = (kind, identity, detail) => {
37
+ if (findings.length >= 100) { findings_truncated = true; return; }
38
+ findings.push({ kind, identity: String(identity ?? '').slice(0, 255), ...detail });
39
+ };
40
+ let degraded = false;
41
+ let unknown = false;
42
+ const targets = args.iflow_ids ? new Set(args.iflow_ids) : null;
43
+ const runtimeIds = new Set();
44
+ const runtimePager = createCollectionPager(client, { root: '/api/v1' });
45
+ const counts = { runtime_errors: 0, queues_warning: 0, queues_critical: 0, queues_stopped: 0, queues_unknown: 0, overdue_stores: 0,
46
+ overdue_messages: 0, certificates_expired: 0, certificates_not_yet_valid: 0, certificates_expiring: 0, certificates_unknown: 0 };
47
+ const sources = {};
48
+ // Independent reads share one deadline; an inaccessible source never hides healthy-source evidence.
49
+ await Promise.all([
50
+ (async () => {
51
+ sources.runtime = await observePages(cursor => runtimePager.page({ path: '/IntegrationRuntimeArtifacts', top: 500, cursor, orderBy: 'Id', deadline,
52
+ project: row => Object.fromEntries(['Id', 'Version', 'Status'].map(name => [name, typeof row[name] === 'string' ? row[name].slice(0, 200) : null])) }), row => {
53
+ if (targets && !targets.has(row.Id)) return;
54
+ runtimeIds.add(row.Id);
55
+ if (row.Status === 'ERROR') { degraded = true; counts.runtime_errors += 1; add('runtime_error', row.Id, { version: row.Version ?? null, status: row.Status }); }
56
+ else if (!['STARTED', 'STARTING', 'STOPPING'].includes(row.Status)) unknown = true;
57
+ }, deadline);
58
+ if (targets && [...targets].some(id => !runtimeIds.has(id))) unknown = true;
59
+ })(),
60
+ (async () => {
61
+ sources.stores = await observePages(cursor => readCpiDataStores(client, { top: 200, cursor, deadline }), row => {
62
+ if (targets && row.IntegrationFlow !== '' && !targets.has(row.IntegrationFlow)) return;
63
+ if (row.NumberOfOverdueMessages === null) unknown = true;
64
+ else if (row.NumberOfOverdueMessages > 0) {
65
+ degraded = true; counts.overdue_stores += 1; counts.overdue_messages += row.NumberOfOverdueMessages;
66
+ add('overdue_store', row.DataStoreName, { integration_flow: row.IntegrationFlow, messages: row.NumberOfMessages, overdue: row.NumberOfOverdueMessages });
67
+ }
68
+ }, deadline);
69
+ })(),
70
+ (async () => {
71
+ sources.queues = await observePages(cursor => readCpiJmsQueues(client, { top: 200, cursor, deadline }), row => {
72
+ if (row.State === 1 || row.State === 2) { degraded = true; counts[row.State === 1 ? 'queues_warning' : 'queues_critical'] += 1; add('queue_state', row.Name, { state: row.state }); }
73
+ if (row.Active === 0) { degraded = true; counts.queues_stopped += 1; add('stopped_queue', row.Name, { active: 0 }); }
74
+ if (row.state === 'unknown' || row.Active === null) { unknown = true; counts.queues_unknown += 1; }
75
+ }, deadline);
76
+ })(),
77
+ (async () => {
78
+ sources.certificates = await observePages(cursor => readCertificateHealth(client, { top: 200, cursor, warning_days: args.certificate_warning_days ?? 30, deadline, evaluated_at }), row => {
79
+ const validity = row.validity;
80
+ if (['expired', 'not_yet_valid', 'expiring'].includes(validity)) {
81
+ degraded = true; counts[`certificates_${validity}`] += 1;
82
+ add('certificate_validity', row.alias ?? row.Alias, { validity, days_remaining: row.days_remaining ?? null });
83
+ } else if (validity !== 'valid') { unknown = true; counts.certificates_unknown += 1; }
84
+ }, deadline);
85
+ })(),
86
+ (async () => {
87
+ try {
88
+ const broker = await readCpiJmsBrokerStatus(client, { deadline });
89
+ sources.broker = { source_results: broker.source_results, complete: broker.complete,
90
+ broker: broker.broker, warning_queue_count: broker.unhealthy_queue_count, stopped_queue_count: broker.stopped_queue_count };
91
+ if (Object.values(broker.source_results ?? {}).some(source => source.result !== 'ok') || broker.complete !== true) unknown = true;
92
+ for (const flag of ['CapacityWarning', 'CapacityError', 'IsQueuesHigh', 'IsMessageSpoolHigh', 'IsTransactedSessionsHigh', 'IsConsumersHigh', 'IsProducersHigh']) {
93
+ if (broker.broker?.[flag] === 1) { degraded = true; add('broker_limit', 'Broker1', { flag }); }
94
+ else if (broker.broker?.[flag] !== 0) unknown = true;
95
+ }
96
+ for (const queue of broker.unhealthy_queues?.items ?? []) {
97
+ if ([1, 2].includes(queue.State)) { degraded = true; add('broker_queue_state', queue.Name, { state: queue.State }); }
98
+ }
99
+ for (const queue of broker.stopped_queues?.items ?? []) { degraded = true; add('broker_stopped_queue', queue.Name, {}); }
100
+ } catch (error) { sources.broker = { ...pageFailure(error), complete: false }; unknown = true; }
101
+ })(),
102
+ (async () => {
103
+ const statistics = [];
104
+ for (const iflow_id of args.iflow_ids ?? [undefined]) {
105
+ const result = await getMessageStatistics(client, { ...window, ...(iflow_id ? { iflow_id } : {}) }, { deadline });
106
+ statistics.push({ ...(iflow_id ? { iflow_id } : {}), ...result });
107
+ for (const status of ['FAILED', 'ESCALATED', 'ABANDONED', 'RETRY']) {
108
+ const count = result.count_by_status[status];
109
+ if (count > 0) { degraded = true; add('message_status', iflow_id ?? 'tenant', { status, count }); }
110
+ }
111
+ if (!result.complete || result.total === 0 || result.unclassified > 0) unknown = true;
112
+ }
113
+ sources.messages = { complete: statistics.every(result => result.complete), items: statistics };
114
+ })()
115
+ ]);
116
+ for (const name of ['runtime', 'stores', 'queues', 'certificates']) if (sources[name]?.complete !== true || sources[name]?.result !== 'ok') unknown = true;
117
+ return { environment: summary.environment ?? 'default', hosts: { cpi: summary.cpi?.host ?? new URL(client.origin).host },
118
+ evaluated_at, window, iflow_ids: args.iflow_ids ?? null, health_scope: 'observed_management_signals',
119
+ health: degraded ? 'degraded' : unknown ? 'unknown' : 'no_issues_observed', counts, findings, findings_truncated, sources,
120
+ continuous_monitoring: 'unverified', receiver_side_processing: 'unverified' };
121
+ }
122
+
123
+ export function registerCpiOperationalTools(register, client, { summary = {} } = {}) {
124
+ register('cpi_get_operational_summary', 'Get CPI operational management snapshot',
125
+ 'Read an on-demand bounded snapshot of runtime, exact message counts, JMS limits/stopped queues, overdue stores and certificate validity. No traffic or missing sources is unknown unless an issue already establishes degraded. This is not continuous monitoring or receiver-side health proof.',
126
+ { from: isoDate, to: isoDate, iflow_ids: z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9_.-]{0,99}$/)).min(1).max(20).optional(),
127
+ certificate_warning_days: z.number().int().min(1).max(365).default(30) }, READ_ONLY_ANNOTATIONS,
128
+ args => getOperationalSummary(client, args, { summary }));
129
+ }