sap-ai-dev-toolkit 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/agents/abap-developer.agent.md +3 -3
- package/.github/agents/abap-runtime-debugger.agent.md +3 -3
- package/.github/agents/hana-cloud-hdi-specialist.agent.md +1 -1
- package/.github/agents/rap-service-developer.agent.md +1 -1
- package/.github/agents/sap-solution-architect.agent.md +1 -1
- package/.github/skills/abap-debugging/SKILL.md +3 -3
- package/.github/skills/abap-development/SKILL.md +1 -1
- package/.github/skills/abap-runtime-analysis/SKILL.md +3 -3
- package/.github/skills/abap-testing-quality/SKILL.md +1 -1
- package/.github/skills/cds-development/SKILL.md +1 -1
- package/.github/skills/clean-core-extensibility/SKILL.md +1 -1
- package/.github/skills/hana-cloud-inspection/SKILL.md +1 -1
- package/.github/skills/hana-cloud-native-development/SKILL.md +2 -2
- package/.github/skills/hana-cloud-validation/SKILL.md +1 -1
- package/.github/skills/rap-development/SKILL.md +1 -1
- package/.github/skills/rap-service-delivery/SKILL.md +1 -1
- package/.github/skills/sap-sdlc-orchestration/SKILL.md +1 -1
- package/.github/skills/sap-standard-api-analysis/SKILL.md +1 -1
- package/.github/skills/sap-transport-release/SKILL.md +1 -1
- package/README.md +6 -6
- package/inventory.md +179 -0
- package/package.json +2 -2
- package/scripts/build-vsp.mjs +2 -1
- package/scripts/postinstall.mjs +14 -16
- package/src/bas-destination-relay.mjs +25 -3
- package/src/bas-discovery.mjs +9 -3
- package/src/binary.mjs +101 -17
- package/src/cf-destination.mjs +55 -1
- package/src/credentials-store.mjs +0 -10
- package/src/engineering-tools.mjs +32 -10
- package/src/hana-tools.mjs +9 -2
- package/src/launcher.mjs +8 -11
- package/src/mcp-config.mjs +19 -1
- package/src/mcp-proxy.mjs +230 -76
- package/src/redact.mjs +28 -0
- package/src/setup.mjs +30 -1
- package/tools.md +1 -1
- package/test/bas-destination-relay.test.mjs +0 -415
- package/test/cf-connectivity.test.mjs +0 -287
- package/test/cf-destination.test.mjs +0 -307
- package/test/cf-runtime.test.mjs +0 -527
- package/test/copilot-assets.test.mjs +0 -63
- package/test/copilot-content.test.mjs +0 -249
- package/test/credentials-store.test.mjs +0 -187
- package/test/discovery.test.mjs +0 -282
- package/test/fixtures/fake-vsp.mjs +0 -266
- package/test/hana-config.test.mjs +0 -168
- package/test/hana-inspector-stdio.test.mjs +0 -44
- package/test/hana-tools.test.mjs +0 -206
- package/test/launcher.test.mjs +0 -401
- package/test/live-s4h.test.mjs +0 -204
- package/test/mcp-config-cf.test.mjs +0 -475
- package/test/mcp-proxy.test.mjs +0 -604
- package/test/pty.mjs +0 -8
- package/test/setup-cf.test.mjs +0 -489
- package/test/setup.test.mjs +0 -417
- package/test/terminal-ui.test.mjs +0 -47
package/test/discovery.test.mjs
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { createServer } from 'node:http';
|
|
4
|
-
import { normalizeDestination, destinationUrl, discoverDestinations, fetchDestinationList, redact, sanitizeChildEnv, slugifyDestination } from '../src/bas-discovery.mjs';
|
|
5
|
-
|
|
6
|
-
test('normalizes destination metadata case-insensitively', () => {
|
|
7
|
-
const destination = normalizeDestination({
|
|
8
|
-
Name: 'DEV-ABAP',
|
|
9
|
-
Properties: { 'WebIDEEnabled': 'true', 'HTML5.DynamicDestination': true, 'WebIDEUsage': 'dev_abap', 'SAP-Client': '200', Authentication: 'PrincipalPropagation', URL: 'https://backend.example' }
|
|
10
|
-
});
|
|
11
|
-
assert.deepEqual(destination, {
|
|
12
|
-
name: 'DEV-ABAP', authentication: 'PrincipalPropagation', client: '200',
|
|
13
|
-
url: 'http://DEV-ABAP.dest', backendUrl: 'https://backend.example', proxyType: null, rawKeys: ['Name', 'Properties']
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('returns every named destination regardless of ADT probe and preserves allowlists', async () => {
|
|
18
|
-
const body = { DESTINATIONS: [
|
|
19
|
-
{ Name: 'z-system', WebIDEEnabled: true, 'HTML5.DynamicDestination': false, WebIDEUsage: 'ui5' },
|
|
20
|
-
{ Name: 'b-system', WebIDEEnabled: false, 'HTML5.DynamicDestination': false, WebIDEUsage: 'ui5' },
|
|
21
|
-
{ Name: 'a-system', WebIDEEnabled: false, 'HTML5.DynamicDestination': false, WebIDEUsage: 'ui5' }
|
|
22
|
-
] };
|
|
23
|
-
const probeImpl = async url => ({ status: url.includes('a-system') ? 403 : url.includes('b-system') ? 200 : 404 });
|
|
24
|
-
const allowlisted = await discoverDestinations({
|
|
25
|
-
body,
|
|
26
|
-
env: { SAP_AI_DEV_TOOLKIT_DESTINATION: 'a-system,b-system' },
|
|
27
|
-
probeImpl
|
|
28
|
-
});
|
|
29
|
-
assert.deepEqual(allowlisted.map(item => [item.name, item.client, item.probe.status]), [
|
|
30
|
-
['a-system', '001', 'auth-required'],
|
|
31
|
-
['b-system', '001', 'available']
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
const all = await discoverDestinations({ body, env: {}, probeImpl });
|
|
35
|
-
assert.deepEqual(all.map(item => [item.name, item.probe.status]), [
|
|
36
|
-
['a-system', 'auth-required'],
|
|
37
|
-
['b-system', 'available'],
|
|
38
|
-
['z-system', 'not-found']
|
|
39
|
-
]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('accepts the previous destination environment variable during upgrades', async () => {
|
|
43
|
-
const destinations = await discoverDestinations({
|
|
44
|
-
body: [{ Name: 'legacy-system' }, { Name: 'other-system' }],
|
|
45
|
-
env: { BAS_VSP_DESTINATION: 'legacy-system' },
|
|
46
|
-
skipProbe: true
|
|
47
|
-
});
|
|
48
|
-
assert.deepEqual(destinations.map(destination => destination.name), ['legacy-system']);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('probes destinations concurrently while preserving order', async () => {
|
|
52
|
-
const body = [{ Name: 'c-system' }, { Name: 'a-system' }, { Name: 'b-system' }];
|
|
53
|
-
const started = [];
|
|
54
|
-
const release = [];
|
|
55
|
-
const probeImpl = url => new Promise(resolve => {
|
|
56
|
-
started.push(url);
|
|
57
|
-
release.push(() => resolve({ status: 200 }));
|
|
58
|
-
});
|
|
59
|
-
const discovery = discoverDestinations({ body, env: {}, probeImpl });
|
|
60
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
61
|
-
assert.equal(started.length, 3, 'all probes must be in flight at once');
|
|
62
|
-
assert.deepEqual(started, [
|
|
63
|
-
'http://a-system.dest/sap/bc/adt/discovery',
|
|
64
|
-
'http://b-system.dest/sap/bc/adt/discovery',
|
|
65
|
-
'http://c-system.dest/sap/bc/adt/discovery'
|
|
66
|
-
], 'probes start in sorted destination order');
|
|
67
|
-
for (const resolve of release) resolve();
|
|
68
|
-
const destinations = await discovery;
|
|
69
|
-
assert.deepEqual(destinations.map(destination => [destination.name, destination.probe.status]), [
|
|
70
|
-
['a-system', 'available'],
|
|
71
|
-
['b-system', 'available'],
|
|
72
|
-
['c-system', 'available']
|
|
73
|
-
]);
|
|
74
|
-
});
|
|
75
|
-
test('parses sample-style destination records and enables every ADT heartbeat response', async () => {
|
|
76
|
-
const body = [
|
|
77
|
-
{
|
|
78
|
-
Type: 'HTTP',
|
|
79
|
-
'HTML5.DynamicDestination': 'true',
|
|
80
|
-
Authentication: 'PrincipalPropagation',
|
|
81
|
-
'sap-client': '200',
|
|
82
|
-
Name: 'DGT_200',
|
|
83
|
-
WebIDEUsage: 'odata_abap,ui5_execute_abap,dev_abap',
|
|
84
|
-
WebIDEEnabled: 'true'
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
Name: 'S4D_100',
|
|
88
|
-
Type: 'HTTP',
|
|
89
|
-
Authentication: 'PrincipalPropagation',
|
|
90
|
-
'HTML5.DynamicDestination': 'true',
|
|
91
|
-
'sap-client': '100',
|
|
92
|
-
WebIDEUsage: 'odata_abap',
|
|
93
|
-
WebIDEEnabled: 'true'
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
Name: 'SUP_BACKEND',
|
|
97
|
-
Type: 'HTTP',
|
|
98
|
-
Authentication: 'OAuth2ClientCredentials',
|
|
99
|
-
'HTML5.DynamicDestination': 'true',
|
|
100
|
-
WebIDEUsage: 'true',
|
|
101
|
-
WebIDEEnabled: 'true'
|
|
102
|
-
}
|
|
103
|
-
];
|
|
104
|
-
const probed = [];
|
|
105
|
-
const destinations = await discoverDestinations({
|
|
106
|
-
body,
|
|
107
|
-
env: {},
|
|
108
|
-
probeImpl: async url => {
|
|
109
|
-
probed.push(url);
|
|
110
|
-
if (url.includes('DGT_200.dest')) return { status: 200 };
|
|
111
|
-
if (url.includes('S4D_100.dest')) return { status: 403 };
|
|
112
|
-
return { status: 404 };
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
assert.deepEqual(probed, [
|
|
117
|
-
'http://DGT_200.dest/sap/bc/adt/discovery',
|
|
118
|
-
'http://S4D_100.dest/sap/bc/adt/discovery',
|
|
119
|
-
'http://SUP_BACKEND.dest/sap/bc/adt/discovery'
|
|
120
|
-
]);
|
|
121
|
-
assert.deepEqual(destinations.map(({ name, client, authentication, probe }) => [
|
|
122
|
-
name, client, authentication, probe.status, probe.available
|
|
123
|
-
]), [
|
|
124
|
-
['DGT_200', '200', 'PrincipalPropagation', 'available', true],
|
|
125
|
-
['S4D_100', '100', 'PrincipalPropagation', 'auth-required', true],
|
|
126
|
-
['SUP_BACKEND', '001', 'OAuth2ClientCredentials', 'not-found', false]
|
|
127
|
-
]);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('keeps T4D destinations when their ADT probes fail', async () => {
|
|
131
|
-
const destinations = await discoverDestinations({
|
|
132
|
-
body: [
|
|
133
|
-
{ Name: 'T4D_100', 'sap-client': '100' },
|
|
134
|
-
{ Name: 'T4D_200', 'sap-client': '200' }
|
|
135
|
-
],
|
|
136
|
-
env: {},
|
|
137
|
-
probeImpl: async url => ({ status: url.includes('T4D_100') ? 503 : 404 })
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
assert.deepEqual(destinations.map(({ name, client, probe }) => [
|
|
141
|
-
name, client, probe.status, probe.httpStatus, probe.available
|
|
142
|
-
]), [
|
|
143
|
-
['T4D_100', '100', 'unavailable', 503, false],
|
|
144
|
-
['T4D_200', '200', 'not-found', 404, false]
|
|
145
|
-
]);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
test('rejects invalid destination URL names and creates deterministic collision slugs', () => {
|
|
150
|
-
assert.equal(destinationUrl('A-1_2'), 'http://A-1_2.dest');
|
|
151
|
-
assert.throws(() => destinationUrl('backend.example/path'), /Invalid BAS destination name/);
|
|
152
|
-
const used = new Map();
|
|
153
|
-
assert.deepEqual(['Dev System', 'dev-system', 'DEV_SYSTEM'].map(name => slugifyDestination(name, used)), ['dev-system', 'dev-system-2', 'dev-system-3']);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('sanitizes child identity material and preserves caller proxy safety settings', () => {
|
|
157
|
-
const env = sanitizeChildEnv({ HTTP_PROXY: 'http://proxy:8887', NO_PROXY: 'localhost,.dest,127.0.0.1', Authorization: 'Bearer secret', Cookie: 'SAP_SESSION=secret', SAP_USER: 'alice', SAP_PASSWORD: 'secret', SAP_READ_ONLY: 'true' });
|
|
158
|
-
assert.equal(env.SAP_PROXY_CONTEXTID_GUARD, 'true');
|
|
159
|
-
assert.equal(env.HTTP_PROXY, 'http://proxy:8887');
|
|
160
|
-
assert.equal(env.NO_PROXY, 'localhost,127.0.0.1');
|
|
161
|
-
assert.equal(env.SAP_READ_ONLY, 'true');
|
|
162
|
-
assert.equal(env.Authorization, undefined);
|
|
163
|
-
assert.equal(env.Cookie, undefined);
|
|
164
|
-
assert.equal(env.SAP_USER, undefined);
|
|
165
|
-
assert.equal(env.SAP_PASSWORD, undefined);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('probes .dest only through configured HTTP proxy and accepts proxy-auth responses', async () => {
|
|
169
|
-
const requests = [];
|
|
170
|
-
const proxy = createServer((request, response) => {
|
|
171
|
-
requests.push({ host: request.headers.host, path: request.url });
|
|
172
|
-
response.writeHead(401);
|
|
173
|
-
response.end();
|
|
174
|
-
});
|
|
175
|
-
await new Promise(resolve => proxy.listen(0, '127.0.0.1', resolve));
|
|
176
|
-
const port = proxy.address().port;
|
|
177
|
-
const found = await discoverDestinations({
|
|
178
|
-
body: [{ Name: 'proxy-system' }],
|
|
179
|
-
env: { HTTP_PROXY: `http://127.0.0.1:${port}` }
|
|
180
|
-
});
|
|
181
|
-
await new Promise(resolve => proxy.close(resolve));
|
|
182
|
-
assert.equal(found[0].probe.status, 'auth-required');
|
|
183
|
-
assert.deepEqual(requests, [{ host: 'proxy-system.dest', path: 'http://proxy-system.dest/sap/bc/adt/discovery' }]);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('uses the documented branded HTTP proxy for destination probes and respects explicit direct mode', async t => {
|
|
187
|
-
const requests = [];
|
|
188
|
-
const proxy = createServer((request, response) => {
|
|
189
|
-
requests.push({ host: request.headers.host, path: request.url });
|
|
190
|
-
response.writeHead(200);
|
|
191
|
-
response.end('<ok/>');
|
|
192
|
-
});
|
|
193
|
-
await new Promise(resolve => proxy.listen(0, '127.0.0.1', resolve));
|
|
194
|
-
t.after(() => new Promise(resolve => proxy.close(resolve)));
|
|
195
|
-
const proxyUrl = `http://127.0.0.1:${proxy.address().port}`;
|
|
196
|
-
const found = await discoverDestinations({
|
|
197
|
-
body: [{ Name: 'branded-proxy-system' }],
|
|
198
|
-
env: { HTTP_PROXY: 'http://127.0.0.1:1', SAP_AI_DEV_TOOLKIT_HTTP_PROXY: proxyUrl }
|
|
199
|
-
});
|
|
200
|
-
assert.equal(found[0].probe.status, 'available');
|
|
201
|
-
assert.deepEqual(requests, [{ host: 'branded-proxy-system.dest', path: 'http://branded-proxy-system.dest/sap/bc/adt/discovery' }]);
|
|
202
|
-
|
|
203
|
-
const direct = await discoverDestinations({
|
|
204
|
-
body: [{ Name: 'direct-system' }],
|
|
205
|
-
env: { HTTP_PROXY: proxyUrl, SAP_AI_DEV_TOOLKIT_HTTP_PROXY: '' },
|
|
206
|
-
probeImpl: async (_url, options) => {
|
|
207
|
-
assert.equal(options.proxyUrl, '', 'explicit empty branded proxy must bypass the configured proxy');
|
|
208
|
-
return { status: 200 };
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
assert.equal(direct[0].probe.status, 'available');
|
|
212
|
-
assert.equal(requests.length, 1);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test('fetches BAS destination lists through configured HTTP and HTTPS proxies', async t => {
|
|
216
|
-
const proxyRequests = [];
|
|
217
|
-
const directRequests = [];
|
|
218
|
-
const target = createServer((request, response) => {
|
|
219
|
-
directRequests.push({ method: request.method, host: request.headers.host, path: request.url });
|
|
220
|
-
response.writeHead(200, { 'content-type': 'application/json' });
|
|
221
|
-
response.end(JSON.stringify([{ Name: 'direct-system' }]));
|
|
222
|
-
});
|
|
223
|
-
await new Promise(resolve => target.listen(0, '127.0.0.1', resolve));
|
|
224
|
-
const proxy = createServer((request, response) => {
|
|
225
|
-
proxyRequests.push({ method: request.method, host: request.headers.host, path: request.url });
|
|
226
|
-
response.writeHead(200, { 'content-type': 'application/json' });
|
|
227
|
-
response.end(JSON.stringify([{ Name: 'proxied-system' }]));
|
|
228
|
-
});
|
|
229
|
-
proxy.on('connect', (request, socket) => {
|
|
230
|
-
proxyRequests.push({ method: request.method, host: request.url });
|
|
231
|
-
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
232
|
-
});
|
|
233
|
-
await new Promise(resolve => proxy.listen(0, '127.0.0.1', resolve));
|
|
234
|
-
t.after(async () => {
|
|
235
|
-
await Promise.all([target, proxy].map(server => new Promise(resolve => server.close(resolve))));
|
|
236
|
-
});
|
|
237
|
-
const proxyUrl = `http://127.0.0.1:${proxy.address().port}`;
|
|
238
|
-
const env = { http_proxy: proxyUrl, https_proxy: proxyUrl, no_proxy: '' };
|
|
239
|
-
const body = await fetchDestinationList('http://bas.example', { env, timeoutMs: 1000 });
|
|
240
|
-
assert.deepEqual(body, [{ Name: 'proxied-system' }]);
|
|
241
|
-
await assert.rejects(fetchDestinationList('https://bas.example', { env, timeoutMs: 1000 }));
|
|
242
|
-
assert.deepEqual(proxyRequests, [
|
|
243
|
-
{ method: 'GET', host: 'bas.example', path: 'http://bas.example/api/listDestinations' },
|
|
244
|
-
{ method: 'CONNECT', host: 'bas.example:443' }
|
|
245
|
-
]);
|
|
246
|
-
const directBody = await fetchDestinationList(`http://127.0.0.1:${target.address().port}`, {
|
|
247
|
-
env: { HTTP_PROXY: proxyUrl, NO_PROXY: '127.0.0.1' },
|
|
248
|
-
timeoutMs: 1000
|
|
249
|
-
});
|
|
250
|
-
assert.deepEqual(directBody, [{ Name: 'direct-system' }]);
|
|
251
|
-
assert.deepEqual(directRequests, [{
|
|
252
|
-
method: 'GET',
|
|
253
|
-
host: `127.0.0.1:${target.address().port}`,
|
|
254
|
-
path: '/api/listDestinations'
|
|
255
|
-
}]);
|
|
256
|
-
assert.equal(proxyRequests.length, 2);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test('branded HTTP proxy takes precedence over HTTPS_PROXY for secure BAS discovery', async () => {
|
|
260
|
-
let capturedDispatcher = 'not-called';
|
|
261
|
-
const body = await fetchDestinationList('https://bas.example', {
|
|
262
|
-
env: {
|
|
263
|
-
SAP_AI_DEV_TOOLKIT_HTTP_PROXY: '',
|
|
264
|
-
HTTPS_PROXY: 'http://127.0.0.1:1',
|
|
265
|
-
https_proxy: 'http://127.0.0.1:1'
|
|
266
|
-
},
|
|
267
|
-
fetchImpl: async (_url, options) => {
|
|
268
|
-
capturedDispatcher = options.dispatcher;
|
|
269
|
-
return new Response(JSON.stringify([{ Name: 'secure-system' }]), {
|
|
270
|
-
status: 200,
|
|
271
|
-
headers: { 'content-type': 'application/json' }
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
assert.deepEqual(body, [{ Name: 'secure-system' }]);
|
|
276
|
-
assert.equal(capturedDispatcher, undefined, 'explicit branded empty proxy must disable inherited HTTPS_PROXY too');
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test('redacts credential-bearing diagnostics without exposing payloads', () => {
|
|
280
|
-
const value = redact({ name: 'dev', password: 'secret', authorization: 'Bearer abc', nested: { cookie: 'x', ok: 'visible' } });
|
|
281
|
-
assert.deepEqual(value, { name: 'dev', password: '[redacted]', authorization: '[redacted]', nested: { cookie: '[redacted]', ok: 'visible' } });
|
|
282
|
-
});
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { appendFileSync } from 'node:fs';
|
|
3
|
-
import { request as httpRequest } from 'node:http';
|
|
4
|
-
|
|
5
|
-
const args = process.argv.slice(2);
|
|
6
|
-
const mode = args[args.indexOf('--mode') + 1] || 'focused';
|
|
7
|
-
const url = args[args.indexOf('--url') + 1] || 'unknown';
|
|
8
|
-
const destination = url.replace(/^https?:\/\//, '').replace(/\.dest$/, '');
|
|
9
|
-
const logPath = process.env.FAKE_LOG;
|
|
10
|
-
function log(entry) { if (logPath) appendFileSync(logPath, `${JSON.stringify({ destination, ...entry })}\n`); }
|
|
11
|
-
function reply(id, result) { process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`); }
|
|
12
|
-
function requestADTThroughProxy(targetUrl, proxyUrl, { method = 'GET', body = '', bypassProxyForLoopback = false } = {}) {
|
|
13
|
-
const target = new URL(targetUrl);
|
|
14
|
-
const proxy = new URL(proxyUrl);
|
|
15
|
-
if (target.protocol !== 'http:') throw new Error('fixture ADT request supports HTTP only');
|
|
16
|
-
const headers = { host: target.host, accept: 'application/xml,text/xml,*/*' };
|
|
17
|
-
if (body) headers['content-type'] = 'application/xml';
|
|
18
|
-
if (process.env.SAP_USER && process.env.SAP_PASSWORD) {
|
|
19
|
-
headers.authorization = `Basic ${Buffer.from(`${process.env.SAP_USER}:${process.env.SAP_PASSWORD}`).toString('base64')}`;
|
|
20
|
-
}
|
|
21
|
-
const loopback = bypassProxyForLoopback && ['127.0.0.1', 'localhost'].includes(target.hostname);
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
const requestOptions = loopback ? {
|
|
24
|
-
hostname: target.hostname,
|
|
25
|
-
port: Number(target.port),
|
|
26
|
-
method,
|
|
27
|
-
path: `${target.pathname}${target.search}`,
|
|
28
|
-
headers
|
|
29
|
-
} : {
|
|
30
|
-
hostname: proxy.hostname,
|
|
31
|
-
port: Number(proxy.port),
|
|
32
|
-
method,
|
|
33
|
-
path: target.href,
|
|
34
|
-
headers
|
|
35
|
-
};
|
|
36
|
-
const request = httpRequest(requestOptions, response => {
|
|
37
|
-
let body = '';
|
|
38
|
-
response.setEncoding('utf8');
|
|
39
|
-
response.on('data', chunk => { body += chunk; });
|
|
40
|
-
response.on('end', () => resolve({ status: response.statusCode || 0, body }));
|
|
41
|
-
});
|
|
42
|
-
request.setTimeout(5_000, () => request.destroy(new Error('fixture ADT request timed out')));
|
|
43
|
-
request.on('error', reject);
|
|
44
|
-
if (body) request.write(body);
|
|
45
|
-
request.end();
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
const toolNames = [
|
|
49
|
-
'GetSource',
|
|
50
|
-
'WriteSource',
|
|
51
|
-
'EditSource',
|
|
52
|
-
'SearchObject',
|
|
53
|
-
'GrepObjects',
|
|
54
|
-
'GrepPackages',
|
|
55
|
-
'FindDefinition',
|
|
56
|
-
'FindReferences',
|
|
57
|
-
'GetContext',
|
|
58
|
-
'SyntaxCheck',
|
|
59
|
-
'Activate',
|
|
60
|
-
'ActivatePackage',
|
|
61
|
-
'CompareSource',
|
|
62
|
-
'GetClassInfo',
|
|
63
|
-
'CreatePackage',
|
|
64
|
-
'CreateTable',
|
|
65
|
-
'GetTable',
|
|
66
|
-
'GetTableContents',
|
|
67
|
-
'GetTransport',
|
|
68
|
-
'ListTransports',
|
|
69
|
-
'GetTransportInfo',
|
|
70
|
-
'GetUserTransports',
|
|
71
|
-
'RunQuery',
|
|
72
|
-
'GetPackage',
|
|
73
|
-
'GetFunctionGroup',
|
|
74
|
-
'GetCDSDependencies',
|
|
75
|
-
'GetCDSImpactAnalysis',
|
|
76
|
-
'GetAPIReleaseState',
|
|
77
|
-
'GetCDSElementInfo',
|
|
78
|
-
'GetMessages',
|
|
79
|
-
'GetFeatures',
|
|
80
|
-
'PrettyPrint',
|
|
81
|
-
'GetSystemInfo',
|
|
82
|
-
'GetInstalledComponents',
|
|
83
|
-
'RunUnitTests',
|
|
84
|
-
'RunATCCheck',
|
|
85
|
-
'GetInactiveObjects',
|
|
86
|
-
'DeleteObject',
|
|
87
|
-
'DebuggerAttach',
|
|
88
|
-
'ListSQLTraces',
|
|
89
|
-
'CreateTransport',
|
|
90
|
-
'ReleaseTransport',
|
|
91
|
-
'DeleteTransport',
|
|
92
|
-
'AnalyzeABAPCode',
|
|
93
|
-
'AnalyzeCallGraph',
|
|
94
|
-
'CodeCompletion',
|
|
95
|
-
'GetAbapHelp',
|
|
96
|
-
'GetCallGraph',
|
|
97
|
-
'GetCalleesOf',
|
|
98
|
-
'GetCallersOf',
|
|
99
|
-
'GetCodeCoverage',
|
|
100
|
-
'GetConnectionInfo',
|
|
101
|
-
'GetObjectStructure',
|
|
102
|
-
'GetTypeHierarchy',
|
|
103
|
-
'GetTypeInfo',
|
|
104
|
-
'GrepObject',
|
|
105
|
-
'GrepPackage',
|
|
106
|
-
'CallRFC',
|
|
107
|
-
'DebuggerDetach',
|
|
108
|
-
'DebuggerGetStack',
|
|
109
|
-
'DebuggerGetVariables',
|
|
110
|
-
'DebuggerListen',
|
|
111
|
-
'DebuggerStep',
|
|
112
|
-
'DeleteBreakpoint',
|
|
113
|
-
'GetBreakpoints',
|
|
114
|
-
'GetDump',
|
|
115
|
-
'GetSQLTraceState',
|
|
116
|
-
'GetTrace',
|
|
117
|
-
'ListDumps',
|
|
118
|
-
'SetBreakpoint',
|
|
119
|
-
'CloneObject',
|
|
120
|
-
'CreateAndActivateProgram',
|
|
121
|
-
'CreateClassWithTests',
|
|
122
|
-
'CreateObject',
|
|
123
|
-
'CreateTestInclude',
|
|
124
|
-
'ExecuteABAP',
|
|
125
|
-
'GetClass',
|
|
126
|
-
'GetClassComponents',
|
|
127
|
-
'GetClassInclude',
|
|
128
|
-
'GetFunction',
|
|
129
|
-
'GetInclude',
|
|
130
|
-
'GetInterface',
|
|
131
|
-
'GetProgram',
|
|
132
|
-
'GetStructure',
|
|
133
|
-
'GetTransaction',
|
|
134
|
-
'LockObject',
|
|
135
|
-
'MoveObject',
|
|
136
|
-
'RecoverFailedCreate',
|
|
137
|
-
'RenameObject',
|
|
138
|
-
'SaveToFile',
|
|
139
|
-
'UnlockObject',
|
|
140
|
-
'UpdateClassInclude',
|
|
141
|
-
'UpdateSource',
|
|
142
|
-
'WriteClass',
|
|
143
|
-
'WriteProgram',
|
|
144
|
-
'ListDependencies',
|
|
145
|
-
'PublishServiceBinding',
|
|
146
|
-
'UnpublishServiceBinding'
|
|
147
|
-
];
|
|
148
|
-
if (mode === 'expert') toolNames.push('ActivateMultiple', 'SAP');
|
|
149
|
-
|
|
150
|
-
function toolDefinition(name) {
|
|
151
|
-
const schemas = {
|
|
152
|
-
RunQuery: {
|
|
153
|
-
type: 'object',
|
|
154
|
-
properties: {
|
|
155
|
-
sql_query: { type: 'string' },
|
|
156
|
-
max_rows: { type: 'number' },
|
|
157
|
-
all_rows: { type: 'boolean' }
|
|
158
|
-
},
|
|
159
|
-
required: ['sql_query']
|
|
160
|
-
},
|
|
161
|
-
GetAPIReleaseState: { type: 'object', properties: { object_uri: { type: 'string' } }, required: ['object_uri'] },
|
|
162
|
-
PrettyPrint: { type: 'object', properties: { source: { type: 'string' } }, required: ['source'] },
|
|
163
|
-
GetTransport: { type: 'object', properties: { transport: { type: 'string' } }, required: ['transport'] },
|
|
164
|
-
GetTransportInfo: {
|
|
165
|
-
type: 'object',
|
|
166
|
-
properties: { object_url: { type: 'string' }, dev_class: { type: 'string' } },
|
|
167
|
-
required: ['object_url', 'dev_class']
|
|
168
|
-
},
|
|
169
|
-
GetUserTransports: {
|
|
170
|
-
type: 'object',
|
|
171
|
-
properties: { user_name: { type: 'string' } },
|
|
172
|
-
required: []
|
|
173
|
-
},
|
|
174
|
-
ActivateMultiple: {
|
|
175
|
-
type: 'object',
|
|
176
|
-
properties: {
|
|
177
|
-
objects: {
|
|
178
|
-
type: 'array',
|
|
179
|
-
items: {
|
|
180
|
-
anyOf: [
|
|
181
|
-
{
|
|
182
|
-
type: 'object',
|
|
183
|
-
properties: { url: { type: 'string' }, name: { type: 'string' } },
|
|
184
|
-
required: ['url', 'name']
|
|
185
|
-
},
|
|
186
|
-
{ type: 'string' }
|
|
187
|
-
]
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
required: ['objects']
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
return { name, description: name, inputSchema: schemas[name] || { type: 'object', properties: {}, required: [] } };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let buffer = '';
|
|
198
|
-
process.stdin.setEncoding('utf8');
|
|
199
|
-
process.stdin.on('data', async chunk => {
|
|
200
|
-
buffer += chunk;
|
|
201
|
-
let end;
|
|
202
|
-
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
203
|
-
const line = buffer.slice(0, end).trim();
|
|
204
|
-
buffer = buffer.slice(end + 1);
|
|
205
|
-
if (!line) continue;
|
|
206
|
-
const message = JSON.parse(line);
|
|
207
|
-
if (message.method === 'initialize') {
|
|
208
|
-
log({ event: 'initialize', argv: args, env: { guard: process.env.SAP_PROXY_CONTEXTID_GUARD, authorization: process.env.Authorization, cookie: process.env.Cookie, user: process.env.SAP_USER, password: process.env.SAP_PASSWORD, verbose: process.env.SAP_VERBOSE, httpProxy: process.env.HTTP_PROXY, httpsProxy: process.env.HTTPS_PROXY, noProxy: process.env.NO_PROXY, allowTransportableEdits: process.env.SAP_ALLOW_TRANSPORTABLE_EDITS } });
|
|
209
|
-
if (destination === 'broken') process.exit(2);
|
|
210
|
-
reply(message.id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: destination, version: 'fixture' } });
|
|
211
|
-
} else if (message.method === 'tools/list') {
|
|
212
|
-
reply(message.id, message.params?.cursor
|
|
213
|
-
? { tools: toolNames.slice(15).map(toolDefinition) }
|
|
214
|
-
: { tools: toolNames.slice(0, 15).map(toolDefinition), nextCursor: 'page-2' });
|
|
215
|
-
} else if (message.method === 'tools/call') {
|
|
216
|
-
const name = message.params?.name;
|
|
217
|
-
log({ event: 'call', name, arguments: message.params?.arguments });
|
|
218
|
-
if (message.params?.arguments?.emitNotification) {
|
|
219
|
-
process.stdout.write(`${JSON.stringify({
|
|
220
|
-
jsonrpc: '2.0',
|
|
221
|
-
method: 'notifications/message',
|
|
222
|
-
params: { level: 'info', logger: 'fake-vsp', data: 'fixture log notification' }
|
|
223
|
-
})}\n`);
|
|
224
|
-
}
|
|
225
|
-
if (message.params?.arguments?.emitListChanged) {
|
|
226
|
-
process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' })}\n`);
|
|
227
|
-
}
|
|
228
|
-
const transportTools = [
|
|
229
|
-
'ListTransports', 'GetTransport', 'GetUserTransports', 'GetTransportInfo',
|
|
230
|
-
'CreateTransport', 'ReleaseTransport', 'DeleteTransport'
|
|
231
|
-
];
|
|
232
|
-
const disabledTransportOperation = transportTools.includes(name)
|
|
233
|
-
&& (!args.includes('--enable-transports')
|
|
234
|
-
|| (['CreateTransport', 'ReleaseTransport', 'DeleteTransport'].includes(name) && args.includes('--transport-read-only')));
|
|
235
|
-
if (disabledTransportOperation) {
|
|
236
|
-
reply(message.id, { content: [{ type: 'text', text: 'transport operation is not enabled' }], isError: true });
|
|
237
|
-
} else if (message.params?.arguments?.object === 'RPC_ERROR') {
|
|
238
|
-
process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, error: { code: -32042, message: 'backend connection refused; password=must-not-log' } })}\n`);
|
|
239
|
-
} else if (message.params?.arguments?.object === 'TOOL_ERROR') {
|
|
240
|
-
reply(message.id, { content: [{ type: 'text', text: 'backend timeout; token=must-not-log' }], isError: true });
|
|
241
|
-
} else if (process.env.FAKE_CF_ADT_TEST === 'true' && name === 'GetSystemInfo') {
|
|
242
|
-
try {
|
|
243
|
-
const target = new URL('/sap/bc/adt/discovery', url);
|
|
244
|
-
const proxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
|
245
|
-
const adt = await requestADTThroughProxy(target.href, proxy);
|
|
246
|
-
reply(message.id, { content: [{ type: 'text', text: adt.body }], isError: false });
|
|
247
|
-
} catch {
|
|
248
|
-
reply(message.id, { content: [{ type: 'text', text: 'fixture ADT request failed' }], isError: true });
|
|
249
|
-
}
|
|
250
|
-
} else if (process.env.FAKE_RELAY_CSRF_TEST === 'true' && name === 'GetSystemInfo') {
|
|
251
|
-
try {
|
|
252
|
-
const target = new URL('/sap/bc/adt/datapreview/freestyle', url);
|
|
253
|
-
const proxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
|
254
|
-
const adt = await requestADTThroughProxy(target.href, proxy, { method: 'POST', body: '<probe/>', bypassProxyForLoopback: true });
|
|
255
|
-
reply(message.id, { content: [{ type: 'text', text: `fixture ADT HTTP ${adt.status}: ${adt.body}` }], isError: adt.status < 200 || adt.status >= 300 });
|
|
256
|
-
} catch {
|
|
257
|
-
reply(message.id, { content: [{ type: 'text', text: 'fixture ADT write failed' }], isError: true });
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
reply(message.id, { content: [{ type: 'text', text: `${destination}:${name}` }], isError: false });
|
|
261
|
-
}
|
|
262
|
-
} else if (message.id !== undefined) reply(message.id, {});
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
process.stdin.on('end', () => { log({ event: 'end' }); });
|
|
266
|
-
process.on('SIGTERM', () => { log({ event: 'term' }); process.exit(0); });
|