sap-ai-dev-toolkit 0.9.9917 → 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.
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 +73 -14
  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 +52 -60
  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 +66 -44
  59. package/tools.md +193 -26
@@ -0,0 +1,122 @@
1
+ // Optional service credentials are host/setup inputs, never tool-call inputs.
2
+ // Keep this parser independent of the core CPI/APIM/Developer Hub OAuth shapes.
3
+ export const OPTIONAL_INPUTS = Object.freeze({
4
+ ctms: Object.freeze({ variable: 'CTMS_SERVICE_KEY', label: 'Cloud Transport Management' }),
5
+ ans: Object.freeze({ variable: 'ANS_CONFIG', label: 'Alert Notification' }),
6
+ event_mesh: Object.freeze({ variable: 'EVENT_MESH_CONFIG', label: 'Event Mesh' }),
7
+ aem: Object.freeze({ variable: 'AEM_CONFIG', label: 'Advanced Event Mesh cloud' }),
8
+ aem_semp: Object.freeze({ variable: 'AEM_SEMP_CONFIG', label: 'Advanced Event Mesh broker SEMP' }),
9
+ open_connectors: Object.freeze({ variable: 'OPEN_CONNECTORS_CONFIG', label: 'Open Connectors' }),
10
+ edge: Object.freeze({ variable: 'EDGE_CONFIG', label: 'Edge Integration Cell' })
11
+ });
12
+ export const OPTIONAL_COMPONENT_KEYS = Object.freeze(Object.keys(OPTIONAL_INPUTS));
13
+
14
+ const FIELDS = Object.freeze({
15
+ ctms: ['url', 'tokenUrl', 'clientId', 'clientSecret'],
16
+ ans: ['url', 'username', 'password'],
17
+ event_mesh: ['url', 'tokenUrl', 'clientId', 'clientSecret', 'probeQueueName'],
18
+ aem: ['apiToken'],
19
+ aem_semp: ['url', 'msgVpnName', 'username', 'password'],
20
+ open_connectors: ['url', 'userSecret', 'organizationSecret'],
21
+ edge: ['url', 'clientId', 'clientSecret']
22
+ });
23
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);
24
+ const SOURCES = new Set(['environment', 'stored-key', 'setup', 'configuration']);
25
+
26
+ function invalid(side, detail) {
27
+ // Only fixed field names and messages enter errors; never interpolate supplied values.
28
+ throw new Error(`${OPTIONAL_INPUTS[side].variable}: ${detail}`);
29
+ }
30
+
31
+ function endpoint(side, name, value) {
32
+ let parsed;
33
+ try {
34
+ parsed = new URL(value);
35
+ } catch {
36
+ invalid(side, `${name} must be a valid URL`);
37
+ }
38
+ if (parsed.username || parsed.password || value.includes('?') || value.includes('#')) {
39
+ invalid(side, `${name} must not contain credentials, a query, or a fragment`);
40
+ }
41
+ if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && LOOPBACK_HOSTS.has(parsed.hostname))) {
42
+ invalid(side, `${name} must use https`);
43
+ }
44
+ return parsed;
45
+ }
46
+
47
+ /** Resolve a typed optional record. Raw cTMS service keys require explicit host/setup permission. */
48
+ export function parseOptionalConfig(side, value, { source = 'configuration', allowRawCtms = false } = {}) {
49
+ if (!OPTIONAL_COMPONENT_KEYS.includes(side)) throw new Error('Unknown optional Integration Suite component');
50
+ let record = value;
51
+ if (typeof record === 'string') {
52
+ if (source === 'stored-key') invalid(side, 'stored configuration must be a normalized object');
53
+ try {
54
+ record = JSON.parse(record);
55
+ } catch {
56
+ invalid(side, 'configuration must be valid JSON');
57
+ }
58
+ }
59
+ if (!record || typeof record !== 'object' || Array.isArray(record)) invalid(side, 'configuration must be an object');
60
+ if (side === 'ctms' && allowRawCtms && ['environment', 'setup'].includes(source) && Object.hasOwn(record, 'uri')) {
61
+ record = { url: record.uri, tokenUrl: record.uaa?.url, clientId: record.uaa?.clientid, clientSecret: record.uaa?.clientsecret };
62
+ }
63
+ const fields = FIELDS[side];
64
+ if (Object.keys(record).some(key => !fields.includes(key))) invalid(side, 'configuration contains unsupported fields');
65
+ const normalized = {};
66
+ for (const field of fields) {
67
+ const value = record[field];
68
+ if (field === 'probeQueueName' && value === undefined) continue;
69
+ if (typeof value !== 'string' || !value.trim()) invalid(side, `${field} must be a nonempty string`);
70
+ if (/[\u0000-\u001f\u007f]/.test(value)) invalid(side, `${field} must not contain control characters`);
71
+ if (side === 'open_connectors' && ['userSecret', 'organizationSecret'].includes(field) &&
72
+ (value.length > 16384 || !/^[\x21-\x7e]+$/.test(value) || value.includes(','))) {
73
+ invalid(side, `${field} must be an unambiguous printable authorization token`);
74
+ }
75
+ // Credential bytes are significant; trim endpoint/identity inputs only.
76
+ normalized[field] = ['url', 'tokenUrl', 'probeQueueName', 'msgVpnName'].includes(field) ? value.trim() : value;
77
+ }
78
+ if (normalized.url) {
79
+ const url = endpoint(side, 'url', normalized.url);
80
+ if (side === 'edge' && url.pathname !== '/') invalid(side, 'url must be the API virtual-host origin');
81
+ if (side === 'open_connectors' && !/\/elements\/api-v2\/?$/.test(url.pathname)) {
82
+ invalid(side, 'url must include /elements/api-v2');
83
+ }
84
+ normalized.url = url.href.replace(/\/+$/, '');
85
+ }
86
+ if (normalized.tokenUrl) {
87
+ const token = endpoint(side, 'tokenUrl', normalized.tokenUrl);
88
+ if (side === 'ctms' && !/\/oauth\/token\/?$/.test(token.pathname)) {
89
+ token.pathname = `${token.pathname.replace(/\/+$/, '')}/oauth/token`;
90
+ }
91
+ // Event Mesh's binding supplies the complete endpoint, not an OAuth base URL.
92
+ normalized.tokenUrl = token.href;
93
+ }
94
+ return Object.freeze({ ...normalized, source: SOURCES.has(source) ? source : 'configuration' });
95
+ }
96
+
97
+ /** Environment presence wins even for empty/malformed values; errors disable only that side. */
98
+ export function resolveOptionalConfig(env = process.env, stored = {}) {
99
+ const resolved = {};
100
+ const errors = [];
101
+ for (const side of OPTIONAL_COMPONENT_KEYS) {
102
+ const { variable } = OPTIONAL_INPUTS[side];
103
+ const supplied = env[variable] !== undefined;
104
+ const source = supplied ? 'environment' : 'stored-key';
105
+ resolved[side] = null;
106
+ if (!supplied && !Object.hasOwn(stored || {}, side)) continue;
107
+ try {
108
+ resolved[side] = parseOptionalConfig(side, supplied ? env[variable] : stored[side], { source, allowRawCtms: supplied });
109
+ } catch (error) {
110
+ errors.push(Object.freeze({ component: side, source, variable, message: error.message }));
111
+ }
112
+ }
113
+ return Object.freeze({ ...resolved, optional_config_errors: Object.freeze(errors) });
114
+ }
115
+
116
+ /** Private-store projection: metadata is not part of the normalized credential record. */
117
+ export function optionalConfigRecord(side, config) {
118
+ if (!OPTIONAL_COMPONENT_KEYS.includes(side)) throw new Error('Unknown optional Integration Suite component');
119
+ const record = Object.fromEntries(FIELDS[side].filter(field => Object.hasOwn(config || {}, field)).map(field => [field, config[field]]));
120
+ const { source, ...normalized } = parseOptionalConfig(side, record, { source: 'stored-key' });
121
+ return normalized;
122
+ }
@@ -0,0 +1,153 @@
1
+ // Existing optional-service credentials only: no provisioning, SDK or tenant writes.
2
+ import { createServiceClient, createStaticServiceClient } from './integration-http.mjs';
3
+ import { registerAnsTools } from './integration-alerts.mjs';
4
+ import { registerCtmsTools } from './integration-transport.mjs';
5
+ import { registerMessagingTools, registerEventMeshTools, registerAemTools, registerAemSempTools } from './integration-messaging.mjs';
6
+ import { createOpenConnectorsClient, registerOpenConnectorsTools } from './open-connectors-tools.mjs';
7
+ import { registerEdgeMonitoringTools } from './edge-monitoring.mjs';
8
+
9
+ export const OPTIONAL_CLIENT_KEYS = Object.freeze(['ctms', 'ans', 'event_mesh', 'aem', 'aem_semp', 'open_connectors', 'edge']);
10
+ const PROBES = new WeakMap();
11
+ const SCOPES = new WeakMap();
12
+ const OBSERVED = [200, 204, 400, 401, 403, 404, 405, 429, 500, 501, 502, 503, 504, 529];
13
+ const basic = (username, password) => `Basic ${Buffer.from(`${username}:${password}`, 'utf8').toString('base64')}`;
14
+
15
+ function classify(status) {
16
+ if (status >= 200 && status < 300) return { result: 'ok', detail: 'Read-only API access verified; activation and entitlement remain unknown' };
17
+ if (status === 401) return { result: 'unauthenticated', detail: 'HTTP 401: authentication was not accepted' };
18
+ if (status === 403) return { result: 'forbidden', detail: 'HTTP 403: access denied; permission, activation and entitlement causes are not established' };
19
+ if (status === 404 || status === 501) return { result: 'unavailable', detail: `HTTP ${status}: endpoint unavailable; activation and entitlement remain unknown` };
20
+ if (status === 529) return { result: 'error', detail: 'HTTP 529: no SEMP session slot available; this is not an empty queue collection' };
21
+ return { result: 'error', detail: `HTTP ${status}` };
22
+ }
23
+
24
+ function collection(value, kind) {
25
+ if (kind === 'nodes') return Array.isArray(value?.nodes);
26
+ if (kind === 'ans') return Array.isArray(value?.results) && value?.metadata && typeof value.metadata === 'object';
27
+ if (kind === 'aem' || kind === 'semp') return Array.isArray(value?.data);
28
+ if (kind === 'instances') return Array.isArray(value);
29
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
30
+ }
31
+
32
+ function source(probe, status, result, phase) {
33
+ // Probe paths are fixed routes or encoded queue identities; never query URLs or response bodies.
34
+ return { method: 'GET', path: probe.path, status, result, ...(phase ? { phase } : {}) };
35
+ }
36
+
37
+ async function readProbe(client, probe, deadline) {
38
+ try {
39
+ const { status, data } = await client.request('GET', probe.path, {
40
+ query: probe.query, expect: OBSERVED, withStatus: true, responseType: 'text',
41
+ accept: probe.kind === 'metadata' ? 'application/xml, text/xml' : 'application/json', deadline
42
+ });
43
+ let classification = classify(status);
44
+ if (classification.result === 'ok') {
45
+ if (probe.kind === 'metadata') {
46
+ if (!/<(?:[\w.-]+:)?Edmx\b/i.test(String(data))) classification = { result: 'error', detail: 'Local metadata response is not an OData metadata document' };
47
+ } else {
48
+ let value;
49
+ try { value = JSON.parse(data); } catch { /* Classify without including the body. */ }
50
+ if (!collection(value, probe.kind)) classification = { result: 'error', detail: 'Response does not match the documented API collection or named-resource contract' };
51
+ }
52
+ }
53
+ return { area: probe.area, status, ...classification, source: source(probe, status, classification.result) };
54
+ } catch (error) {
55
+ const status = Number.isInteger(error?.status) ? error.status : null;
56
+ const phase = String(error?.phase ?? '').startsWith('token') ? 'token' : undefined;
57
+ const classification = error?.stop_reason === 'deadline'
58
+ ? { result: 'error', detail: 'Verification deadline exceeded', stop_reason: 'deadline' }
59
+ : status === null ? { result: 'error', detail: 'Request failed without an HTTP response' }
60
+ : status >= 200 && status < 300 ? { result: 'error', detail: 'Response processing failed' } : classify(status);
61
+ if (phase && status !== null) classification.detail = `Token request failed (HTTP ${status}); response body withheld`;
62
+ return { area: probe.area, status, ...classification, source: source(probe, status, classification.result, phase) };
63
+ }
64
+ }
65
+
66
+ /** Private auth closures and probe state never enter summaries or MCP output. */
67
+ export function createOptionalClients(config, { env = process.env, fetchImpl } = {}) {
68
+ const clients = {};
69
+ const options = { env, ...(fetchImpl ? { fetchImpl } : {}) };
70
+ const oauth = (label, side) => createServiceClient({ label, url: side.url, tokenUrl: side.tokenUrl, clientId: side.clientId, clientSecret: side.clientSecret, exposeErrorDetails: false, ...options });
71
+ const fixed = (label, side, authorization, extra = {}) => createStaticServiceClient({ label, url: side.url, authorization, ...options, ...extra });
72
+ for (const key of OPTIONAL_CLIENT_KEYS) {
73
+ const side = config?.[key];
74
+ if (!side) { clients[key] = null; continue; }
75
+ let client;
76
+ let probes;
77
+ if (key === 'ctms') {
78
+ client = oauth('cTMS', side);
79
+ probes = [{ area: 'Transport nodes', path: '/v2/nodes', kind: 'nodes' }];
80
+ } else if (key === 'ans') {
81
+ client = fixed('ANS', side, () => basic(side.username, side.password));
82
+ probes = ['subscription', 'condition', 'action'].map(name => ({ area: `Alert ${name} metadata`, path: `/cf/configuration/v1/${name}`, query: { page: 0, pageSize: 1 }, kind: 'ans' }));
83
+ } else if (key === 'event_mesh') {
84
+ client = oauth('Event Mesh', side);
85
+ probes = side.probeQueueName ? [{ area: 'Named queue management', path: `/hub/rest/api/v1/management/messaging/queues/${encodeURIComponent(side.probeQueueName)}`, kind: 'queue' }] : [];
86
+ } else if (key === 'aem') {
87
+ client = fixed('Advanced Event Mesh', { url: 'https://api.solace.cloud' }, () => `Bearer ${side.apiToken}`);
88
+ probes = [{ area: 'GA event broker services', path: '/api/v2/missionControl/eventBrokerServices', query: { pageNumber: 1, pageSize: 1, expand: 'allowedActions' }, kind: 'aem' }];
89
+ } else if (key === 'aem_semp') {
90
+ client = fixed('AEM SEMP', side, () => basic(side.username, side.password), { reuseCookies: true });
91
+ SCOPES.set(client, Object.freeze({ sempVpnName: side.msgVpnName }));
92
+ probes = [{ area: 'VPN queue metadata', path: `/SEMP/v2/monitor/msgVpns/${encodeURIComponent(side.msgVpnName)}/queues`, query: { count: 1 }, kind: 'semp' }];
93
+ } else if (key === 'open_connectors') {
94
+ client = createOpenConnectorsClient(side, options);
95
+ probes = [{ area: 'Connector instance identities', path: '/instances', query: { pageSize: 1 }, kind: 'instances' }];
96
+ } else if (key === 'edge') {
97
+ client = fixed('Edge Integration Cell', side, () => basic(side.clientId, side.clientSecret));
98
+ probes = [{ area: 'Local OData metadata', path: '/local/api/eic/v1/$metadata', kind: 'metadata' }];
99
+ }
100
+ clients[key] = client;
101
+ PROBES.set(client, probes);
102
+ }
103
+ return Object.freeze(clients);
104
+ }
105
+
106
+ /** Optional evidence is separate from configured core access and never proves entitlement. */
107
+ export async function verifyOptionalClients(clients = {}, { deadline = Date.now() + 30_000 } = {}) {
108
+ const entries = await Promise.all(OPTIONAL_CLIENT_KEYS.map(async key => {
109
+ const client = clients[key];
110
+ if (!client) return [key, { configured: false, signed_in: false, ok: false, result: 'untested', activation: 'unknown', entitlement: 'unknown', checks: [] }];
111
+ const probes = PROBES.get(client);
112
+ if (!probes?.length) return [key, {
113
+ configured: true, signed_in: false, ok: false, result: 'untested', activation: 'unknown', entitlement: 'unknown',
114
+ detail: key === 'event_mesh' ? 'Set probeQueueName to an existing queue to test management access; no token or API request was made' : 'No private probe definition exists for this client', checks: []
115
+ }];
116
+ const checks = await Promise.all(probes.map(probe => readProbe(client, probe, deadline)));
117
+ const firstFailure = checks.find(check => check.result !== 'ok');
118
+ const result = firstFailure?.result ?? 'ok';
119
+ const signedIn = checks.some(check => check.status !== null && check.result !== 'unauthenticated' && check.source.phase !== 'token');
120
+ return [key, {
121
+ configured: true, signed_in: signedIn, ok: result === 'ok', result, activation: 'unknown', entitlement: 'unknown',
122
+ ...(firstFailure ? { error: firstFailure.detail } : {}), checks
123
+ }];
124
+ }));
125
+ const reports = Object.fromEntries(entries);
126
+ const configured = Object.entries(reports).filter(([, side]) => side.configured);
127
+ return {
128
+ ...reports,
129
+ optional_completeness: {
130
+ scope: 'configured_optional_access', configured: configured.map(([key]) => key),
131
+ complete: configured.every(([, side]) => side.result === 'ok'),
132
+ result: configured.length === 0 ? 'not_configured' : configured.every(([, side]) => side.result === 'ok') ? 'ok' : 'partial',
133
+ unverified: configured.filter(([, side]) => side.result !== 'ok').map(([key]) => key)
134
+ }
135
+ };
136
+ }
137
+
138
+ /** Optional authority is separate per client; configuration does not enable other adapters. */
139
+ export function registerOptionalIntegrationTools(register, { ans, ctms, event_mesh, aem, aem_semp, open_connectors, edge } = {}) {
140
+ if (ans) registerAnsTools(register, ans);
141
+ if (ctms) registerCtmsTools(register, ctms);
142
+ registerMessagingTools(register, { event_mesh, aem, aem_semp }, SCOPES.get(aem_semp));
143
+ if (open_connectors) registerOpenConnectorsTools(register, open_connectors);
144
+ if (edge) registerEdgeMonitoringTools(register, edge);
145
+ }
146
+
147
+ export function optionalToolNames() {
148
+ const names = [];
149
+ registerAnsTools(name => names.push(name), null);
150
+ registerCtmsTools(name => names.push(name), null);
151
+ for (const registrar of [registerEventMeshTools, registerAemTools, registerAemSempTools, registerOpenConnectorsTools, registerEdgeMonitoringTools]) registrar(name => names.push(name), null);
152
+ return names;
153
+ }