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.
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 +48 -56
  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,181 @@
1
+ // Public documentation is contract evidence, not proof of activation, entitlement or access.
2
+ const DOC = 'https://github.com/SAP-docs/btp-integration-suite/blob/main/docs/';
3
+ const handoff = (prerequisites, steps, safety, verification) => ({ prerequisites, steps, safety, post_action_verification: verification });
4
+ const CATALOG = {
5
+ cpi: {
6
+ name: 'Cloud Integration', public_contract: 'established', prefix: 'cpi_',
7
+ authentication: 'OAuth client credentials for the CPI API; permissions depend on each operation',
8
+ setup_requirements: ['Existing CPI API service key via CPI_SERVICE_KEY or hidden setup input; API credential is distinct from runtime sender credentials'],
9
+ source_links: [DOC + 'ci/Development/message-processing-logs-827a2d7.md', DOC + 'ci/Development/backup-and-restore-your-integration-artifacts-bd280ef.md'],
10
+ native_handoff: [
11
+ handoff(['Exact artifact ID/version, dependency and backup review'], ['Design → package → Edit → Artifacts → Add → appropriate Value Mapping/Script Collection/Message Mapping Create or Upload', 'Use the design-workspace archive format; Save as Version / Version History and restore in the native editor'], 'Individual ZIP, mass-download ZIP and MTAR are different formats. Source creation is not deployment.', ['Read resulting artifact ID/version; separately approve deployment and verify runtime identity']),
12
+ handoff(['Exact queue and approved message identities'], ['Monitor → Manage Stores → Message Queues → inspect queue and Where-Used/access type/state', 'Actions → Usage → Start/Stop; retry only separately approved messages in an active non-exclusive queue'], 'Whole-queue Retry affects all messages and needs distinct approval; duplicate receiver effects remain possible.', ['Refresh queue metadata and correlate MPL; never consume message bodies for inspection']),
13
+ handoff(['Exact MPL/design identity, backup/dependency/lock/configure-only review'], ['Use the native monitoring action for supported cancellation; use native design deletion confirmation for the exact artifact'], 'No guessed cancellation/status write; do not promise processing stops, unlock another user or substitute queue deletion.', ['Verify named design absence or the observed processing state separately'])
14
+ ]
15
+ },
16
+ apim: {
17
+ name: 'API Management', public_contract: 'established', prefix: 'apim_',
18
+ authentication: 'API Management API-access OAuth client credentials; analytics, management and runtime authority are distinct',
19
+ setup_requirements: ['Existing APIM_SERVICE_KEY or hidden setup input; inspect operation-specific API roles'],
20
+ source_links: [DOC + 'apim/API-Management/APIM-Initial-Setup/accessing-api-management-apis-programmatically-24a2c37.md', 'https://api.sap.com/api/APIPortal_Analytics_CF/overview', DOC + 'apim/API-Management/api-revisions-58097ac.md'],
21
+ native_handoff: [handoff(['Exact proxy/retained revision; preserve current draft and review product associations'], ['API details → Revisions → select retained revision → Revert', 'Inspect the resulting new revision → Deploy; standalone Undeploy uses its native action'], 'Saved-bundle import is a new imported revision, not native Revert. Deletion is not undeployment.', ['Read named proxy state/version and static policy evidence; perform separately approved consumer smoke reads'])]
22
+ },
23
+ devhub: {
24
+ name: 'Developer Hub', public_contract: 'established', prefix: 'devhub_',
25
+ authentication: 'Developer Hub API-access OAuth service key; consumer API keys are separate private runtime credentials',
26
+ setup_requirements: ['Existing DEVHUB_SERVICE_KEY or hidden setup input; registered application/developer identities for consumer operations'],
27
+ source_links: [DOC + 'apim/API-Management/create-or-update-or-read-an-application-using-subscription-key-e2645b5.md'],
28
+ native_handoff: [handoff(['Exact consumer application/developer, owner approval and coordinated private credential delivery'], ['My Workspace → Subscriptions → Regenerate Key; coordinate client cutover', 'Admin Center → Manage Users → Registered Users → Revoke User with reason for portal access removal'], 'Regeneration changes key and secret and invalidates the old key. Portal revoke, unpublish and application deletion do not prove issued OAuth-token revocation.', ['Inspect application/subscription metadata; separately approve bounded runtime smoke reads'])]
29
+ },
30
+ ctms: {
31
+ name: 'Cloud Transport Management', public_contract: 'established', prefix: 'ctms_',
32
+ authentication: 'OAuth client credentials from CTMS_SERVICE_KEY uri and uaa fields',
33
+ setup_requirements: ['Existing cTMS service key; Content Agent assembly and source/target destinations/routes require administrator confirmation', 'Node access does not prove upload/import readiness or entitlement'],
34
+ source_links: ['https://github.com/SAP/jenkins-library/blob/master/src/com/sap/piper/integration/TransportManagementService.groovy', 'https://github.com/SAP-docs/btp-cloud-transport-management/blob/main/docs/70-integrations/extended-integration-scenarios-7e966f7.md', 'https://github.com/SAP-docs/btp-cloud-transport-management/blob/main/docs/80-ans-integration/migrating-to-sap-alert-notification-service-as-a-trusted-internal-service-0914d2c.md'],
35
+ contract_boundaries: [{ operation: 'Transport upload/import/history/log automation', public_contract: 'documented_reference_inaccessible', detail: 'Detailed TMS_v2 archive/result schemas require SAP-ID access; no speculative upload, import, reset, forward or delete tools' }],
36
+ native_handoff: [handoff(['Named source content/versions, target node/route, Content Agent and source/target destinations, independent target-bound server'], ['Authorized owner assembles CPI/APIM content with Content Agent into its supported transport format', 'Select source/export node and target route; submit the named request', 'Authorized operator imports the explicitly approved request set and tracks action/request logs'], 'Separate assembly/upload/import approvals. A ZIP or toolkit snapshot is not an MTAR. Error can mean partial import; resetting to Repeatable is not rollback. Use trusted internal ANS integration, not obsolete ALERT_NOTIFICATION_SERVICE destination (removal scheduled Q4 2026).', ['On target-bound server inspect exact content/versions/runtime/endpoints and separately approved consumer smoke reads'])]
37
+ },
38
+ ans: {
39
+ name: 'Alert Notification service', public_contract: 'established', prefix: 'ans_',
40
+ authentication: 'Technical-client Basic authentication; CONFIGURATION_READ for inspection, separately authorized CONFIGURATION_WRITE for rules',
41
+ setup_requirements: ['Existing ANS_CONFIG {url,username,password}; configure delivery actions natively', 'Rule inspection never proves notification delivery'],
42
+ source_links: ['https://github.com/SAP/alert-notification-node-client/blob/main/src/configuration-api/configuration-client.ts', 'https://github.com/SAP/alert-notification-node-client/blob/main/src/configuration-api/models.ts', 'https://github.com/SAP/alert-notification-node-client/blob/main/src/utils/common.ts'],
43
+ native_handoff: [handoff(['Existing service and technical client, exact subscription/condition/action identities'], ['Configure action recipients/credentials in native ANS', 'Inspect scoped event conditions, enabled subscriptions/actions and native delivery status'], 'No arbitrary action-property writer or implicit all-events subscription. CPI failures are not automatically ANS events.', ['Verify references and intentional scope; require approved synthetic notification delivery evidence'])]
44
+ },
45
+ event_mesh: {
46
+ name: 'Standalone Event Mesh (default plan)', public_contract: 'established', prefix: 'event_mesh_',
47
+ authentication: 'Management-binding OAuth client credentials; exact token endpoint is preserved',
48
+ setup_requirements: ['Existing EVENT_MESH_CONFIG {url,tokenUrl,clientId,clientSecret,probeQueueName?}', 'Set an existing fully qualified probeQueueName to test management access; otherwise no token/API probe is made'],
49
+ source_links: ['https://help.sap.com/docs/event-mesh/event-mesh/use-rest-apis-to-manage-queues-and-queue-subscriptions'],
50
+ native_handoff: [handoff(['Existing standalone default-plan service, namespace and queue identity'], ['Use native Event Mesh management to inspect namespace, queues and subscriptions'], 'Standalone default-plan APIs do not establish Suite-embedded Event Mesh parity; no message consumption or replay.', ['Read exact named queue management metadata when credentials and identity are supplied'])]
51
+ },
52
+ aem: {
53
+ name: 'Advanced Event Mesh cloud management', public_contract: 'established', prefix: 'aem_',
54
+ authentication: 'Static cloud API bearer token with minimum services:view; fixed US origin https://api.solace.cloud',
55
+ setup_requirements: ['Existing AEM_CONFIG {apiToken}; use GA Mission Control API only', 'Cloud API token is not a broker-local SEMP credential'],
56
+ source_links: ['https://help.pubsub.em.services.cloud.sap/Cloud/cloud-rest-api-lp.htm', 'https://api.solace.dev/cloud/openapi/mission-control.json'],
57
+ native_handoff: [handoff(['Exact broker service/operation identity, appropriate cloud role'], ['Inspect broker service and operation in the native cloud console'], 'No Beta availability endpoint, alternate region fallback or broker credential output.', ['Read GA broker service metadata and observed operation state'])]
58
+ },
59
+ aem_semp: {
60
+ name: 'Advanced Event Mesh broker-local SEMP', public_contract: 'established', prefix: null,
61
+ authentication: 'Broker-local Basic with VPN-scoped read-only management user; private session-cookie reuse',
62
+ setup_requirements: ['Existing AEM_SEMP_CONFIG {url,msgVpnName,username,password}; Basic must be enabled', 'HTTP 529 means no session slot, not empty queues; do not substitute a cloud API token'],
63
+ source_links: ['https://help.pubsub.em.services.cloud.sap/Admin/SEMP/SEMP-Security.htm'],
64
+ native_handoff: [handoff(['Exact broker origin and VPN, VPN-scoped read-only management access'], ['Inspect queue monitor state in the native broker console'], 'No SEMP proxy, message-body read or control/replay action.', ['Read configured VPN queue monitor metadata; authenticate only to the configured broker'])]
65
+ },
66
+ open_connectors: {
67
+ name: 'Open Connectors', public_contract: 'established', prefix: 'open_connectors_',
68
+ authentication: 'User <secret>, Organization <secret>; private Element token only for exact instance enabled-state action',
69
+ setup_requirements: ['Existing OPEN_CONNECTORS_CONFIG {url,userSecret,organizationSecret}; URL includes /elements/api-v2'],
70
+ source_links: ['https://help.openconnectors.ext.hana.ondemand.com/home/organization-secret-and-user-secret', 'https://help.openconnectors.ext.hana.ondemand.com/home/disabling-and-enabling-instances', 'https://help.openconnectors.ext.hana.ondemand.com/home/standardized-pagination'],
71
+ native_handoff: [handoff(['Exact instance/provider identity, administrator access for provider authorization'], ['Inspect the instance enabled state in the native console', 'Perform provider-specific OAuth/re-authentication in the native workflow'], 'Disable is DELETE /instances/enabled, not instance deletion. Acceptance does not establish provider health or effective state.', ['Read safe instance identity; inspect enabled state/provider authorization natively'])]
72
+ },
73
+ edge: {
74
+ name: 'Edge Integration Cell local APIs', public_contract: 'established', prefix: 'edge_',
75
+ authentication: 'Local Basic service-key clientId/clientSecret against the API virtual-host origin; preserve /local routes',
76
+ setup_requirements: ['Existing EDGE_CONFIG {url,clientId,clientSecret}; local API network reachability', 'A local 401 can mean missing /local; unsupported local endpoints can return 403; neither proves non-entitlement'],
77
+ source_links: [DOC + 'ISuite_Edge_Integration_Cell/local-odata-api-access-fe61332.md', DOC + 'ISuite_Edge_Integration_Cell/operations-cockpit-api-da1abc0.md'],
78
+ contract_boundaries: [{ operation: 'Component/pod/job field-level operational schemas', public_contract: 'not_established', detail: 'Published route inventory does not establish safe status-field projections; no raw-object passthrough or restart/Kubernetes tools' }],
79
+ native_handoff: [handoff(['Exact cell/runtime/message identities and native cockpit access'], ['Use Operations Cockpit to inspect component/pod/job diagnostics', 'Use local metadata/MPL APIs for supported read-only diagnosis'], 'No cloud OAuth substitution, Kubernetes administration or component restart.', ['Read local OData metadata and bounded message/run evidence where supported'])]
80
+ },
81
+ cloud_alm: {
82
+ name: 'SAP Cloud ALM continuous monitoring', public_contract: 'established',
83
+ authentication: 'Administrator-run Cloud ALM integration; dedicated CPI API credential, not a developer-credential grant',
84
+ setup_requirements: ['Dedicated CPI API credential and Landscape Management endpoint', 'Configure Integration & Exception Monitoring collection/categories and Health Monitoring', 'MonitoringDataRead, MonitoringArtifactsDeploy and HealthCheckMonitoringDataRead are separate setup requirements', 'Require collection and approved synthetic notification delivery evidence before continuous monitoring is verified'],
85
+ source_links: ['https://support.sap.com/en/alm/sap-cloud-alm/operations/expert-portal/integration-monitoring/calm-cpi.html'],
86
+ native_handoff: [handoff(['Cloud ALM administrator and dedicated integration credential'], ['Configure Landscape Management endpoint', 'Enable Integration & Exception Monitoring collection/categories and Health Monitoring', 'Configure native event thresholds and notifications'], 'CPI API access alone is not continuous collection/delivery; no scheduler or telemetry daemon is installed. APIM anomaly readiness is distinct from analytics polling.', ['Observe collection and approved synthetic notification delivery; otherwise needs_admin_confirmation'])]
87
+ },
88
+ suite_event_mesh: {
89
+ name: 'Suite-embedded Event Mesh', public_contract: 'not_established',
90
+ authentication: 'Native Suite activation/role requirements; standalone Event Mesh credentials are not evidence of access',
91
+ setup_requirements: ['Separate Suite capability activation and limits must be inspected by an administrator'],
92
+ source_links: ['https://help.sap.com/docs/integration-suite'],
93
+ native_handoff: [handoff(['Suite administrator and exact embedded capability/tenant'], ['Configure/Monitor → Event Mesh; inspect activation and configured limits'], 'No assumption of parity with standalone default-plan APIs.', ['Confirm embedded capability activation/limits in its native surface'])]
94
+ },
95
+ tpm: {
96
+ name: 'Trading Partner Management', public_contract: 'established',
97
+ authentication: 'Native TPM roles; B2bInterchangeRead for metadata, B2bInterchangeOperate for separately approved operations',
98
+ setup_requirements: ['Exact agreement/version and Default or Full activation mode; inspect generated runtime dependencies'],
99
+ source_links: [DOC + 'ISuite_Trading_Partner_Management/activating-a-trading-partner-agreement-baed0e3.md', DOC + 'ISuite_Trading_Partner_Management/monitor-interchanges-42c1199.md'],
100
+ native_handoff: [handoff(
101
+ ['Exact agreement/version, explicit Default versus Full approval, business-document identity and B2bInterchangeRead'],
102
+ ['Design → B2B Scenarios → Agreements → open exact agreement → Activate → choose Default or Full → confirm',
103
+ 'Default activates agreement/channels, not inactive security configurations, identifier groups or custom rules; Full activates all referenced objects',
104
+ 'Inspect Failed activation details and Partner Directory Data → Copy PID / View Data; use Monitor → Interchanges for document state'],
105
+ 'Company-side AS2 inbound decryption requires manual activation. Never edit generated Partner Directory parameters to simulate agreement management. Payload read is separate; retry/restart/cancel require distinct approval and B2bInterchangeOperate.',
106
+ ['Inspect activation/interchange results and separately verify resulting CPI runtime artifacts'])]
107
+ },
108
+ integration_advisor: {
109
+ name: 'Integration Advisor', public_contract: 'not_established',
110
+ authentication: 'Native Integration Advisor role/access; no established external MIG/MAG administration adapter',
111
+ setup_requirements: ['Exact MIG/MAG identity/version and supported runtime artifact format'],
112
+ source_links: [DOC + 'ISuite_Integration_Advisor/exporting-runtime-artifacts-5ab4cfe.md', DOC + 'ISuite_Integration_Advisor/consuming-integration-advisor-artifacts-in-integration-flows-a33a6c6.md'],
113
+ native_handoff: [handoff(
114
+ ['Named MIG/MAG and selected version, native validation/simulation and intended runtime format'],
115
+ ['Open MIG/MAG → select exact version → review validation/simulation', 'Additional Options → Export → SAP Cloud Integration Runtime Artifacts', 'Inspect generated XSD/XSL/scripts/examples and import supported CPI content separately'],
116
+ 'Generated runtime ZIP is not a transport MTAR or editable MIG/MAG source. Editing generated CPI output does not modify its source guideline. Do not mix old and newly exported namespace-dependent artifact files.',
117
+ ['Inspect source validation/export and resulting CPI artifact identity/version; separately authorize deployment'])]
118
+ },
119
+ integration_assessment: {
120
+ name: 'Integration Assessment', public_contract: 'documented_reference_inaccessible',
121
+ authentication: 'Existing Integration Assessment APIs/default credential; url is token-server URL, entities/management are resource origins',
122
+ setup_requirements: ['Existing Assessment API service key and access to User Actions → API Reference; exact resource schemas must be established before automation'],
123
+ source_links: [DOC + 'ISuite_Integration_Assessment/integration-assessment-apis-47847b5.md', DOC + 'ISuite_Integration_Assessment/creating-service-instance-and-service-key-to-enable-api-calling-749897f.md'],
124
+ native_handoff: [handoff(['Assessment administrator and existing APIs/default credential'], ['User Actions → API Reference → inspect the exact resource/schema', 'Use native assessment workflow for the selected identity'], 'Do not parse this key as CPI or guess an /applications endpoint.', ['Record exact accessible API contract and selected assessment outcome before proposing an adapter'])]
125
+ },
126
+ migration_assessment: {
127
+ name: 'Migration Assessment and tooling', public_contract: 'not_established',
128
+ authentication: 'Native assessment/tooling access; no guessed external API or reused PO credentials',
129
+ setup_requirements: ['Exact extraction/evaluation request and approved source-system access'],
130
+ source_links: [DOC + 'ISuite_Migration_Assessment/create-a-data-extraction-request-ce0ad0e.md', DOC + 'ISuite_Migration_Assessment/create-a-scenario-evaluation-request-435ec61.md'],
131
+ native_handoff: [handoff(['Selected source/request, native migration authorization'], ['Request → Data Extraction → inspect Log', 'Request → Scenario Evaluation → inspect dashboard → export/generate report', 'Run conversion in native migration tooling; inspect generated CPI content separately'], 'Assessment is not conversion/deployment; no PO-credential reuse or guessed migration API.', ['Inspect extraction/evaluation outcome and generated CPI artifact identities/content'])]
132
+ },
133
+ integration_cell_api_mcp: {
134
+ name: 'New Integration Cell API/MCP artifacts', public_contract: 'not_established',
135
+ authentication: 'Native Suite design/deploy/monitor roles; distinct from classic APIM and MCP invocation access',
136
+ setup_requirements: ['Exact new artifact kind/identity/version and its native supported workflow'],
137
+ source_links: [
138
+ DOC + 'ISuite_Integrations_APIs/create-an-api-artifact-using-a-target-url-or-an-openapi-specification-914f57e.md',
139
+ DOC + 'ISuite_Integrations_APIs/create-an-mcp-server-from-an-api-artifact-9f18140.md',
140
+ DOC + 'ISuite_Integrations_APIs/deploy-an-api-artifact-b70e7ec.md',
141
+ DOC + 'ISuite_Integrations_APIs/deploy-an-mcp-server-017ba42.md',
142
+ DOC + 'ISuite_Integrations_APIs/monitor-apis-and-mcp-servers-399b6c6.md'
143
+ ],
144
+ native_handoff: [handoff(
145
+ ['PI_Integration_Developer, activated API Management and provisioned Integration Cell, exact package/artifact/version and approved runtime/virtual host',
146
+ 'API-derived MCP requires a deployed OData/REST API with OAuth and OpenAPI 3.0.0–3.0.3'],
147
+ ['Design → Integrations and APIs → package → Edit → Artifacts → Add → API → Integration Cell → URL or Specification; review and save configuration',
148
+ 'For MCP: Artifacts → Add → MCP Server → API → select deployed source API → select intentional tools → Add; review authentication/authorization policies',
149
+ 'Approve exact artifact deployment separately → Deploy → confirm runtime profile and virtual host',
150
+ 'Monitor → Integrations and APIs → Runtime: Integration Cell → Manage Integration Content / Monitor Message Processing; inspect deployed endpoints and logs'],
151
+ 'No external administration contract is established. Classic APIM proxy tools and CPI metadata discovery are not this artifact lifecycle. Runtime profile is fixed at creation; deployed endpoint can differ from design-time virtual host. MCP invocation access does not authorize administration.',
152
+ ['Verify exact deployed artifact version, chosen runtime/virtual host and processing outcome natively; separately authorize any consumer invocation'])]
153
+ }
154
+ };
155
+
156
+ const RESULT = new Set(['ok', 'unauthenticated', 'forbidden', 'unavailable', 'untested', 'error']);
157
+ const SOURCES = new Set(['environment', 'service-key', 'stored-key', 'vcap-services']);
158
+
159
+ /** Always return the full catalog. Only registered tools can become supported actions. */
160
+ export function capabilityCatalog({ summary = {}, verification, toolNames = [], optional_config_errors = [] } = {}) {
161
+ const registered = [...new Set(Array.isArray(toolNames) ? toolNames.filter(name => typeof name === 'string' && /^[a-z][a-z0-9_]+$/.test(name)) : [])];
162
+ const catalog = {};
163
+ for (const [key, definition] of Object.entries(CATALOG)) {
164
+ const { prefix, ...metadata } = definition;
165
+ const configured = summary[key]?.configured === true || verification?.[key]?.configured === true;
166
+ const report = verification?.[key];
167
+ const access = RESULT.has(report?.result) ? report.result : 'untested';
168
+ const actions = registered.filter(name => key === 'aem_semp' ? name === 'aem_list_queues' : key === 'aem' ? name.startsWith('aem_') && name !== 'aem_list_queues' : prefix && name.startsWith(prefix));
169
+ const invalid = optional_config_errors.some(error => error?.component === key);
170
+ catalog[key] = {
171
+ ...structuredClone(metadata),
172
+ configured,
173
+ configuration: invalid ? 'invalid' : configured ? 'configured' : 'absent',
174
+ ...(SOURCES.has(summary[key]?.source) ? { configuration_source: summary[key].source } : {}),
175
+ access, activation: 'unknown', entitlement: 'unknown', common_subaccount: 'unverified',
176
+ supported_actions: actions,
177
+ native_access: 'unverified'
178
+ };
179
+ }
180
+ return catalog;
181
+ }
@@ -0,0 +1,203 @@
1
+ // Bounded collection traversal. Cursors are private to a client runtime and never authorize arbitrary URLs.
2
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import { IntegrationError } from './integration-http.mjs';
5
+
6
+ const OUTPUT_BYTES = 96 * 1024 - 1024;
7
+ const SOURCE_BYTES = 2 * 1024 * 1024;
8
+ const TTL = 15 * 60 * 1000;
9
+ const MAX_CONTINUATIONS = 256;
10
+ const runtimes = new WeakMap();
11
+ export const cursorSchema = z.string().min(1).max(16384).optional();
12
+ const bytes = value => Buffer.byteLength(JSON.stringify(value));
13
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('base64url');
14
+ const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object'
15
+ ? Object.fromEntries(Object.keys(value).sort().filter(key => value[key] !== undefined).map(key => [key, canonical(value[key])])) : value;
16
+
17
+ /** Preserve OData count/continuation plumbing before projecting individual rows. */
18
+ export function decodeCollection(raw) {
19
+ const source = raw?.d ?? raw;
20
+ const items = Array.isArray(source) ? source : source?.results ?? source?.value;
21
+ if (!Array.isArray(items)) throw new IntegrationError('Expected a collection response');
22
+ const count = source?.__count ?? source?.['@odata.count'] ?? source?.['@count'] ?? raw?.['@odata.count'] ?? raw?.['@count'];
23
+ const total = (typeof count === 'number' || typeof count === 'string' && /^\d+$/.test(count)) && Number.isSafeInteger(Number(count)) && Number(count) >= 0 ? Number(count) : null;
24
+ const next = source?.__next ?? source?.['@odata.nextLink'] ?? source?.['@nextLink'] ?? source?.['odata.nextLink'] ?? raw?.['@odata.nextLink'] ?? raw?.['@nextLink'] ?? null;
25
+ if (next !== null && typeof next !== 'string') throw new IntegrationError('Invalid collection continuation');
26
+ return { items, total, next };
27
+ }
28
+
29
+ export function createCollectionPager(client, { root, request } = {}) {
30
+ if (typeof root !== 'string' || !root.startsWith('/') || root.includes('?') || root.includes('#') || root.includes('..') || root.endsWith('/')) throw new IntegrationError('Invalid collection service root');
31
+ // Name enumeration constructs registrars with no client; defer client access until a real request.
32
+ function runtime() {
33
+ if (!client || typeof client.request !== 'function') throw new IntegrationError('Collection client is not configured');
34
+ let state = runtimes.get(client);
35
+ if (!state) { state = { key: randomBytes(32), id: randomBytes(16).toString('hex') }; runtimes.set(client, state); }
36
+ return state;
37
+ }
38
+ const send = (path, options) => request ? request('GET', path, options) : client.request('GET', `${root}${path}`, options);
39
+ const failCursor = () => { throw new IntegrationError('Invalid or expired collection cursor; restart the listing'); };
40
+ function encode(value, state) {
41
+ const payload = Buffer.from(JSON.stringify(value)).toString('base64url');
42
+ return `${payload}.${createHmac('sha256', state.key).update(payload).digest('base64url')}`;
43
+ }
44
+ function decode(cursor, binding, state) {
45
+ if (typeof cursor !== 'string' || cursor.length > 16384 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(cursor)) return failCursor();
46
+ const [payload, signature] = cursor.split('.');
47
+ const expected = createHmac('sha256', state.key).update(payload).digest();
48
+ const supplied = Buffer.from(signature, 'base64url');
49
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) return failCursor();
50
+ let value;
51
+ try { value = JSON.parse(Buffer.from(payload, 'base64url').toString()); } catch { return failCursor(); }
52
+ if (value.v !== 1 || value.runtime !== state.id || value.root !== root || binding !== undefined && value.binding !== binding || !Number.isFinite(value.expires) || Date.now() >= value.expires || !Number.isSafeInteger(value.offset) || value.offset < 0 || !Array.isArray(value.links) || value.links.length > MAX_CONTINUATIONS) return failCursor();
53
+ return value;
54
+ }
55
+ function relativeLink(link, path, query) {
56
+ const origin = client.origin;
57
+ if (typeof origin !== 'string') throw new IntegrationError('Collection client has no pinned origin');
58
+ const current = new URL(`${root}${path}`, origin);
59
+ for (const [key, value] of Object.entries(query)) if (value !== undefined) current.searchParams.set(key, String(value));
60
+ let target;
61
+ try { target = new URL(link, current); } catch { throw new IntegrationError('Invalid collection continuation URL'); }
62
+ if (target.origin !== origin || target.username || target.password || target.hash || !target.pathname.startsWith(`${root}/`) || decodeURIComponent(target.pathname).split('/').some(part => part === '..' || part === '.')) throw new IntegrationError('Collection continuation leaves the pinned service root');
63
+ if (target.pathname !== new URL(`${root}${path}`, origin).pathname) throw new IntegrationError('Collection continuation changes collection');
64
+ for (const key of target.searchParams.keys()) {
65
+ if (target.searchParams.getAll(key).length !== 1) throw new IntegrationError('Ambiguous collection continuation query');
66
+ if (key === '$skip' || key === '$top') {
67
+ const value = target.searchParams.get(key);
68
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || key === '$top' && (Number(value) < 1 || Number(value) > 501)) throw new IntegrationError('Unbounded collection continuation query');
69
+ }
70
+ }
71
+ // Continuations may change paging only, never filters, projections or ordering.
72
+ const paging = new Set(['$skip', '$skiptoken', '$top', '$inlinecount', '$count']);
73
+ for (const key of new Set([...current.searchParams.keys(), ...target.searchParams.keys()])) {
74
+ if (!paging.has(key) && current.searchParams.get(key) !== target.searchParams.get(key)) throw new IntegrationError('Collection continuation changes query identity');
75
+ }
76
+ return { path: target.pathname.slice(root.length), query: Object.fromEntries(target.searchParams) };
77
+ }
78
+
79
+ async function page({ path, query = {}, top = 100, cursor, project = value => value, mode = 'odata', orderBy, identity, consistency, raw, filter, sort, sourceComplete = true, endOfSource, fallback = true, deadline, scope, context, maxBytes = OUTPUT_BYTES, ...options }) {
80
+ if (typeof path !== 'string' || !path.startsWith('/') || path.startsWith('//') || /[?#\\]/.test(path) || path.split('/').some(part => part === '..' || part === '.')) throw new IntegrationError('Collection path must be relative to its service root');
81
+ if (!Number.isInteger(top) || top < 1 || top > 500 || !['odata', 'local'].includes(mode) || !Number.isInteger(maxBytes) || maxBytes < 1024 || maxBytes > OUTPUT_BYTES) throw new IntegrationError('Invalid collection page options');
82
+ const state = runtime();
83
+ const binding = digest(canonical({ root, path, query, orderBy, mode, scope, context, identity: typeof identity === 'function' ? String(identity) : identity, consistency, sourceComplete, endOfSource: endOfSource && String(endOfSource), filter: filter && String(filter), sort: sort && String(sort) }));
84
+ const initialSkip = Number(query.$skip ?? 0);
85
+ if (!Number.isSafeInteger(initialSkip) || initialSkip < 0) throw new IntegrationError('Invalid collection offset');
86
+ let resume = cursor ? decode(cursor, binding, state) : { v: 1, runtime: state.id, root, binding, scope, context, expires: Date.now() + TTL, offset: 0, links: [], skip: initialSkip };
87
+ if (resume.next) relativeLink(resume.next, path, { ...query, ...(orderBy ? { $orderby: orderBy } : {}) });
88
+ if (deadline !== undefined && Date.now() >= deadline) return result([], null, false, null, 'deadline');
89
+ const baseQuery = { ...query, ...(orderBy ? { $orderby: orderBy } : {}) };
90
+ let requestPath = path;
91
+ const localQuery = Object.fromEntries(Object.entries(query).filter(([key]) => !['$top', '$skip', '$orderby', '$inlinecount', '$count'].includes(key)));
92
+ let requestQuery = mode === 'local' || resume.local ? localQuery : { ...baseQuery, $top: top + 1, ...(resume.skip ? { $skip: resume.skip } : {}) };
93
+ if (resume.next) ({ path: requestPath, query: requestQuery } = relativeLink(resume.next, path, baseQuery));
94
+ if (resume.fetchQuery) requestQuery = resume.fetchQuery;
95
+ const currentFingerprint = digest(canonical({ path: requestPath, query: Object.fromEntries(Object.entries(requestQuery).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)])) }));
96
+ if (!cursor) resume.links = [currentFingerprint];
97
+ let local = mode === 'local' || resume.local;
98
+ let response;
99
+ try {
100
+ response = raw === undefined || cursor ? await send(requestPath, { ...options, query: requestQuery, unwrap: false, deadline }) : raw;
101
+ } catch (error) {
102
+ if (error.code === 'INTEGRATION_DEADLINE') return result([], null, false, null, 'deadline', { source_result: { result: 'error', stop_reason: 'deadline' } });
103
+ if (/safe size limit/.test(error.message)) return result([], null, false, null, 'source_limit', { source_result: { result: 'error', stop_reason: 'source_limit' } });
104
+ if (!local && fallback && (error.status === 400 || error.status === 501 || /HTTP (400|501)\b/.test(error.message))) {
105
+ try {
106
+ response = await send(path, { ...options, query: localQuery, unwrap: false, deadline });
107
+ local = true;
108
+ requestPath = path;
109
+ requestQuery = localQuery;
110
+ delete resume.next;
111
+ }
112
+ catch (failure) {
113
+ if (/safe size limit/.test(failure.message)) return result([], null, false, null, 'source_limit', { source_result: { result: 'error', stop_reason: 'source_limit' } });
114
+ if (failure.code === 'INTEGRATION_DEADLINE') return result([], null, false, null, 'deadline', { source_result: { result: 'error', stop_reason: 'deadline' } });
115
+ if (failure.status === 400 || failure.status === 501 || /HTTP (400|501)\b/.test(failure.message)) return result([], null, false, null, 'paging_unsupported', { source_result: { result: 'error', status: failure.status ?? Number(/HTTP (\d+)/.exec(failure.message)?.[1]) } });
116
+ throw failure;
117
+ }
118
+ } else throw error;
119
+ }
120
+ if (local && !resume.local) resume.offset += resume.skip;
121
+ if (bytes(response) > SOURCE_BYTES) return result([], null, false, null, 'source_limit');
122
+ const decoded = decodeCollection(response);
123
+ let rows = decoded.items;
124
+ const sourceDigest = local || resume.offset ? digest(rows) : null;
125
+ if (resume.sourceDigest && resume.sourceDigest !== sourceDigest) return result([], decoded.total, false, null, 'source_changed');
126
+ const sourceEnded = endOfSource?.(response) === true;
127
+ if (filter) rows = rows.filter(filter);
128
+ if (sort) rows = [...rows].sort(sort);
129
+ let nextLink = null;
130
+ if (decoded.next && !local) {
131
+ const validated = relativeLink(decoded.next, path, baseQuery);
132
+ nextLink = `${root}${validated.path}?${new URLSearchParams(validated.query)}`;
133
+ const fingerprint = digest(canonical({ path: validated.path, query: validated.query }));
134
+ if (resume.links.includes(fingerprint) || fingerprint === currentFingerprint) throw new IntegrationError('Repeated collection continuation; restart the listing');
135
+ }
136
+ const offset = resume.offset;
137
+ const selected = rows.slice(offset, offset + top);
138
+ const projected = selected.map(project);
139
+ let changed = false;
140
+ let ids;
141
+ if (consistency === 'best_effort') {
142
+ const identify = typeof identity === 'function' ? identity : row => row[identity ?? 'MessageGuid'];
143
+ ids = selected.map(identify).filter(value => value !== undefined && value !== null).map(String);
144
+ changed = new Set(ids).size !== ids.length || ids.some(id => resume.priorIds?.includes(id)) || resume.total !== undefined && resume.total !== null && decoded.total !== null && resume.total !== decoded.total;
145
+ }
146
+ let accepted = projected.length;
147
+ let envelope;
148
+ // Include the actual signed continuation in the byte budget. Never split a row or lose its resume position.
149
+ do {
150
+ const consumed = offset + accepted;
151
+ const buffered = consumed < rows.length;
152
+ const exhausted = !buffered && !nextLink && (local || (decoded.total !== null
153
+ ? (resume.skip + consumed) >= decoded.total
154
+ : sourceEnded || rows.length === 0));
155
+ const limited = !sourceComplete || local && (decoded.next !== null || decoded.total !== null && decoded.total > decoded.items.length);
156
+ let next = null;
157
+ let reason = changed ? 'source_changed' : accepted < projected.length ? 'output_limit' : limited && !buffered ? 'source_limit' : exhausted ? 'end' : 'more';
158
+ if (!rows.length && decoded.total !== null && decoded.total > (resume.skip ?? 0)) reason = 'source_changed';
159
+ if (!changed && reason !== 'source_changed' && accepted > 0 && (buffered || nextLink || !exhausted && !limited)) {
160
+ const nextState = { ...resume, total: decoded.total, priorIds: ids?.slice(0, accepted), local };
161
+ if (buffered && (local || nextLink)) {
162
+ nextState.offset = consumed;
163
+ nextState.sourceDigest = digest(decoded.items);
164
+ nextState.fetchQuery = requestQuery;
165
+ } else {
166
+ nextState.offset = 0;
167
+ delete nextState.sourceDigest;
168
+ delete nextState.fetchQuery;
169
+ nextState.skip = (resume.skip ?? 0) + consumed;
170
+ nextState.next = nextLink;
171
+ }
172
+ if (!buffered && nextLink) {
173
+ const validated = relativeLink(nextLink, path, baseQuery);
174
+ nextState.links = [...resume.links, digest(canonical(validated))];
175
+ }
176
+ if (nextState.links.length >= MAX_CONTINUATIONS) reason = 'source_limit';
177
+ else {
178
+ const encoded = encode(nextState, state);
179
+ if (encoded.length > 16384) reason = 'source_limit';
180
+ else next = encoded;
181
+ }
182
+ }
183
+ envelope = result(projected.slice(0, accepted), decoded.total, reason === 'end', next, reason, consistency ? { consistency } : {});
184
+ if (Buffer.byteLength(JSON.stringify(envelope, null, 2)) <= maxBytes) return envelope;
185
+ accepted -= 1;
186
+ } while (accepted >= 0);
187
+ return result([], decoded.total, false, null, 'output_limit', consistency ? { consistency } : {});
188
+ }
189
+ function cursorContext(cursor, { scope } = {}) {
190
+ if (!cursor) return undefined;
191
+ const value = decode(cursor, undefined, runtime());
192
+ if (value.scope !== scope) failCursor();
193
+ return value.context;
194
+ }
195
+ function validateCursor(cursor, options) {
196
+ cursorContext(cursor, options);
197
+ }
198
+ return Object.freeze({ page, validateCursor, cursorContext });
199
+ }
200
+
201
+ function result(items, total, complete, next_cursor, stop_reason, extra = {}) {
202
+ return { items, returned: items.length, total, complete, next_cursor, stop_reason, ...extra };
203
+ }