abap-local-client 1.0.0
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/README.md +22 -0
- package/basic-auth.mjs +197 -0
- package/bin/cli.cjs +8 -0
- package/bin/config.cjs +8 -0
- package/config-cli.mjs +325 -0
- package/config-manager.mjs +278 -0
- package/package.json +46 -0
- package/saml-login.mjs +372 -0
- package/sap-rfc-client.js +852 -0
- package/snc-auth.mjs +194 -0
- package/spnego-auth.mjs +362 -0
- package/sso-sap-client.mjs +687 -0
- package/x509-auth.mjs +395 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# abap-client-auth
|
|
2
|
+
|
|
3
|
+
SAP Local Client — WebSocket bridge with Basic, SPNEGO, X.509, SAML & SNC authentication.
|
|
4
|
+
|
|
5
|
+
## Authentication Methods
|
|
6
|
+
|
|
7
|
+
| Method | File | Description |
|
|
8
|
+
|--------|------|-------------|
|
|
9
|
+
| Basic Auth | `basic-auth.mjs` | Username/password via HTTP Basic header |
|
|
10
|
+
| SPNEGO | `spnego-auth.mjs` | Kerberos/SPNEGO ticket-based auth |
|
|
11
|
+
| X.509 | `x509-auth.mjs` | Client certificate auth |
|
|
12
|
+
| SAML | `saml-login.mjs` | Browser-based SAML SSO |
|
|
13
|
+
| SNC | `snc-auth.mjs` | Secure Network Communication for RFC |
|
|
14
|
+
| SSO Client | `sso-sap-client.mjs` | Main entry point combining all methods |
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
Uses `config-manager.mjs` for centralized configuration management.
|
|
19
|
+
|
|
20
|
+
## License
|
|
21
|
+
|
|
22
|
+
MIT
|
package/basic-auth.mjs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic HTTP Authentication Session
|
|
3
|
+
*
|
|
4
|
+
* Wraps an axios instance with HTTP Basic Auth pre-configured, managing
|
|
5
|
+
* CSRF tokens and cookies automatically — the same interface as
|
|
6
|
+
* SpnegoSession, X509Session, and SamlSession.
|
|
7
|
+
*
|
|
8
|
+
* This is the most common authentication scenario: username + password.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import https from 'https';
|
|
12
|
+
|
|
13
|
+
export class BasicSession {
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} axios - Axios instance
|
|
16
|
+
* @param {string} sapBaseUrl - SAP HTTPS base URL (e.g. "https://host:44301")
|
|
17
|
+
* @param {string} sapClient - SAP client/mandant
|
|
18
|
+
* @param {string} username - SAP username
|
|
19
|
+
* @param {string} password - SAP password
|
|
20
|
+
* @param {{ language?: string, rejectUnauthorized?: boolean, timeout?: number }} [opts]
|
|
21
|
+
*/
|
|
22
|
+
constructor(axios, sapBaseUrl, sapClient, username, password, opts = {}) {
|
|
23
|
+
this.axios = axios;
|
|
24
|
+
this.sapBaseUrl = sapBaseUrl;
|
|
25
|
+
this.sapClient = sapClient;
|
|
26
|
+
this.username = username;
|
|
27
|
+
this.password = password;
|
|
28
|
+
this.language = opts.language || 'EN';
|
|
29
|
+
|
|
30
|
+
this.csrfToken = null;
|
|
31
|
+
this.authenticated = false;
|
|
32
|
+
this.cookies = new Map(); // cookie jar: name → value
|
|
33
|
+
|
|
34
|
+
// Build the Basic auth header value once
|
|
35
|
+
this._basicAuth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
36
|
+
|
|
37
|
+
// Create a dedicated axios instance with pre-configured headers and auth
|
|
38
|
+
this._client = axios.create({
|
|
39
|
+
baseURL: sapBaseUrl,
|
|
40
|
+
timeout: opts.timeout || 60_000,
|
|
41
|
+
headers: {
|
|
42
|
+
'sap-client': sapClient,
|
|
43
|
+
'sap-language': this.language,
|
|
44
|
+
'Accept': 'application/xml,text/plain,*/*',
|
|
45
|
+
'X-sap-adt-sessiontype': 'stateful',
|
|
46
|
+
'Authorization': `Basic ${this._basicAuth}`,
|
|
47
|
+
},
|
|
48
|
+
httpsAgent: new https.Agent({
|
|
49
|
+
rejectUnauthorized: opts.rejectUnauthorized === true, // default false, like old client
|
|
50
|
+
keepAlive: true,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Response interceptor: capture cookies and CSRF token
|
|
55
|
+
this._client.interceptors.response.use(
|
|
56
|
+
(response) => {
|
|
57
|
+
// Harvest Set-Cookie headers
|
|
58
|
+
const setCookie = response.headers['set-cookie'];
|
|
59
|
+
if (setCookie) {
|
|
60
|
+
const list = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
61
|
+
for (const c of list) {
|
|
62
|
+
const pair = c.split(';')[0].trim();
|
|
63
|
+
if (!pair) continue;
|
|
64
|
+
const [name, ...rest] = pair.split('=');
|
|
65
|
+
this.cookies.set(name, rest.join('='));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Extract CSRF token
|
|
69
|
+
if (response.headers['x-csrf-token']) {
|
|
70
|
+
this.csrfToken = response.headers['x-csrf-token'];
|
|
71
|
+
}
|
|
72
|
+
return response;
|
|
73
|
+
},
|
|
74
|
+
(error) => {
|
|
75
|
+
// On 401/403, clear session (match old client behavior)
|
|
76
|
+
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
|
|
77
|
+
this.clearSession();
|
|
78
|
+
}
|
|
79
|
+
return Promise.reject(error);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Fetch CSRF token and validate the connection. */
|
|
85
|
+
async init() {
|
|
86
|
+
try {
|
|
87
|
+
const resp = await this._client({
|
|
88
|
+
method: 'GET',
|
|
89
|
+
url: '/sap/bc/adt/discovery',
|
|
90
|
+
headers: {
|
|
91
|
+
'Accept': 'application/atomsvc+xml',
|
|
92
|
+
'X-CSRF-Token': 'Fetch',
|
|
93
|
+
},
|
|
94
|
+
validateStatus: () => true,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (resp.status >= 200 && resp.status < 300) {
|
|
98
|
+
if (resp.headers['x-csrf-token']) {
|
|
99
|
+
this.csrfToken = resp.headers['x-csrf-token'];
|
|
100
|
+
}
|
|
101
|
+
this.authenticated = true;
|
|
102
|
+
return resp;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Discovery returned non-2xx — session invalid, but don't throw.
|
|
106
|
+
// Mark as not authenticated; first real call may still work.
|
|
107
|
+
console.warn(` ⚠️ Basic Auth init: HTTP ${resp.status}`);
|
|
108
|
+
return resp;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.warn(` ⚠️ Basic Auth init failed: ${err.message}`);
|
|
111
|
+
// Don't throw — session may still work on first real call.
|
|
112
|
+
// The error will surface naturally when get()/post()/put() are called.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Clear cookies and CSRF token on session error. */
|
|
117
|
+
clearSession() {
|
|
118
|
+
this.cookies.clear();
|
|
119
|
+
this.csrfToken = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Refresh CSRF token. */
|
|
123
|
+
async refreshCsrf() {
|
|
124
|
+
const resp = await this._client({
|
|
125
|
+
method: 'GET',
|
|
126
|
+
url: '/sap/bc/adt/discovery',
|
|
127
|
+
headers: {
|
|
128
|
+
'Accept': 'application/atomsvc+xml',
|
|
129
|
+
'X-CSRF-Token': 'Fetch',
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
if (resp.headers['x-csrf-token']) {
|
|
133
|
+
this.csrfToken = resp.headers['x-csrf-token'];
|
|
134
|
+
}
|
|
135
|
+
return this.csrfToken;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** GET request. */
|
|
139
|
+
async get(path, extraHeaders = {}) {
|
|
140
|
+
return this._client({
|
|
141
|
+
method: 'GET',
|
|
142
|
+
url: path,
|
|
143
|
+
headers: this._headers(path, extraHeaders),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** POST request (auto-injects CSRF token). */
|
|
148
|
+
async post(path, data, extraHeaders = {}) {
|
|
149
|
+
if (!this.csrfToken) await this.refreshCsrf();
|
|
150
|
+
return this._client({
|
|
151
|
+
method: 'POST',
|
|
152
|
+
url: path,
|
|
153
|
+
headers: this._headers(path, {
|
|
154
|
+
'Content-Type': 'application/xml',
|
|
155
|
+
'X-CSRF-Token': this.csrfToken,
|
|
156
|
+
...extraHeaders,
|
|
157
|
+
}),
|
|
158
|
+
data,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** PUT request (auto-injects CSRF token). */
|
|
163
|
+
async put(path, data, extraHeaders = {}) {
|
|
164
|
+
if (!this.csrfToken) await this.refreshCsrf();
|
|
165
|
+
return this._client({
|
|
166
|
+
method: 'PUT',
|
|
167
|
+
url: path,
|
|
168
|
+
headers: this._headers(path, {
|
|
169
|
+
'Content-Type': 'application/xml',
|
|
170
|
+
'X-CSRF-Token': this.csrfToken,
|
|
171
|
+
...extraHeaders,
|
|
172
|
+
}),
|
|
173
|
+
data,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build headers for a request.
|
|
179
|
+
* Merges extra headers; forces sap-client and sap-language from the session,
|
|
180
|
+
* and injects cookies from the cookie jar.
|
|
181
|
+
*/
|
|
182
|
+
_headers(_path, extra = {}) {
|
|
183
|
+
const h = {
|
|
184
|
+
'sap-client': this.sapClient,
|
|
185
|
+
'sap-language': this.language,
|
|
186
|
+
...extra,
|
|
187
|
+
};
|
|
188
|
+
// Inject cookies from jar (SAP session cookies)
|
|
189
|
+
if (this.cookies.size > 0) {
|
|
190
|
+
const cookieStr = Array.from(this.cookies.entries())
|
|
191
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
192
|
+
.join('; ');
|
|
193
|
+
h['Cookie'] = cookieStr;
|
|
194
|
+
}
|
|
195
|
+
return h;
|
|
196
|
+
}
|
|
197
|
+
}
|
package/bin/cli.cjs
ADDED
package/bin/config.cjs
ADDED
package/config-cli.mjs
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Configuration CLI — Manage the encrypted SAP system configuration.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node config-cli.mjs set-mcp-key <apiKey>
|
|
7
|
+
* node config-cli.mjs add-system <id> <authMode> [--key value]...
|
|
8
|
+
* node config-cli.mjs list-systems
|
|
9
|
+
* node config-cli.mjs remove-system <id>
|
|
10
|
+
* node config-cli.mjs enable-system <id>
|
|
11
|
+
* node config-cli.mjs disable-system <id>
|
|
12
|
+
* node config-cli.mjs show-config
|
|
13
|
+
* node config-cli.mjs arcanum-fallback <on|off>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as config from './config-manager.mjs';
|
|
17
|
+
import { createInterface } from 'readline';
|
|
18
|
+
|
|
19
|
+
function readStdin() {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
22
|
+
let data = '';
|
|
23
|
+
rl.on('line', (line) => data += line);
|
|
24
|
+
rl.on('close', () => resolve(data.trim()));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// Argument parsing
|
|
30
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
const command = args[0];
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
console.log(`
|
|
37
|
+
SAP Local Client — Configuration Manager
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
node config-cli.mjs set-mcp-key <apiKey>
|
|
41
|
+
Set the MCP Cloud API key (used for WebSocket auth).
|
|
42
|
+
If no argument, reads from stdin (hidden echo).
|
|
43
|
+
|
|
44
|
+
node config-cli.mjs set-ws-url <url>
|
|
45
|
+
Set the MCP Cloud WebSocket URL.
|
|
46
|
+
Examples:
|
|
47
|
+
node config-cli.mjs set-ws-url ws://localhost:8080
|
|
48
|
+
node config-cli.mjs set-ws-url wss://abap-mcp.mcps.b2rise.com/ws
|
|
49
|
+
|
|
50
|
+
node config-cli.mjs add-system <id> <authMode> [options...]
|
|
51
|
+
Add or update a SAP system.
|
|
52
|
+
authMode: basic | spnego | x509 | saml | snc
|
|
53
|
+
|
|
54
|
+
For basic:
|
|
55
|
+
--host <host> --port <port> --client <client>
|
|
56
|
+
--username <user> --password <pass> [--saprouter <string>] [--language <lang>]
|
|
57
|
+
|
|
58
|
+
For spnego:
|
|
59
|
+
--host <host> --port <port> --client <client>
|
|
60
|
+
[--service-principal <spn>] [--language <lang>]
|
|
61
|
+
|
|
62
|
+
For x509:
|
|
63
|
+
--host <host> --port <port> --client <client>
|
|
64
|
+
--ca-cert <pem> --ca-key <pem> --login-id <identifier>
|
|
65
|
+
[--language <lang>]
|
|
66
|
+
|
|
67
|
+
For saml:
|
|
68
|
+
--base-url <url> --client <client>
|
|
69
|
+
[--headless] [--timeout-ms <ms>] [--language <lang>]
|
|
70
|
+
|
|
71
|
+
For snc:
|
|
72
|
+
--host <host> --sysnr <sysnr> --client <client>
|
|
73
|
+
--snc-partner-name <name> [--snc-my-name <name>]
|
|
74
|
+
[--snc-qop <qop>] [--snc-lib <path>] [--username <user>]
|
|
75
|
+
[--saprouter <string>] [--language <lang>]
|
|
76
|
+
|
|
77
|
+
node config-cli.mjs list-systems
|
|
78
|
+
List all configured systems.
|
|
79
|
+
|
|
80
|
+
node config-cli.mjs remove-system <id>
|
|
81
|
+
Remove a system.
|
|
82
|
+
|
|
83
|
+
node config-cli.mjs enable-system <id>
|
|
84
|
+
node config-cli.mjs disable-system <id>
|
|
85
|
+
Enable or disable a system.
|
|
86
|
+
|
|
87
|
+
node config-cli.mjs show-config
|
|
88
|
+
Show the full configuration (API key and passwords REDACTED).
|
|
89
|
+
|
|
90
|
+
node config-cli.mjs arcanum-fallback <on|off>
|
|
91
|
+
Enable or disable Arcanum fallback for unknown system IDs.
|
|
92
|
+
|
|
93
|
+
Config file: ${config.getConfigFilePath()}
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseKeyValue(args) {
|
|
98
|
+
const map = {};
|
|
99
|
+
for (let i = 0; i < args.length; i++) {
|
|
100
|
+
if (args[i].startsWith('--')) {
|
|
101
|
+
const key = args[i].slice(2).replace(/-/g, '_');
|
|
102
|
+
const nextArg = args[i + 1];
|
|
103
|
+
// A standalone `--flag` with no value = boolean true
|
|
104
|
+
if (!nextArg || nextArg.startsWith('--')) {
|
|
105
|
+
map[key] = 'true';
|
|
106
|
+
} else {
|
|
107
|
+
map[key] = nextArg;
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return map;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function redact(str) {
|
|
116
|
+
if (!str || str.length <= 4) return '***';
|
|
117
|
+
return str.substring(0, 2) + '***' + str.substring(str.length - 2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
121
|
+
// Commands
|
|
122
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
123
|
+
|
|
124
|
+
switch (command) {
|
|
125
|
+
// ── set-mcp-key ─────────────────────────────────────────────────────────
|
|
126
|
+
case 'set-mcp-key': {
|
|
127
|
+
let apiKey = args[1];
|
|
128
|
+
if (!apiKey) {
|
|
129
|
+
console.log('Enter MCP API key (input will NOT be echoed):');
|
|
130
|
+
apiKey = await readStdin();
|
|
131
|
+
}
|
|
132
|
+
if (!apiKey) {
|
|
133
|
+
console.error('No API key provided.');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
config.setMcpApiKey(apiKey);
|
|
137
|
+
console.log('✅ MCP API key saved.');
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── add-system ─────────────────────────────────────────────────────────
|
|
142
|
+
case 'add-system': {
|
|
143
|
+
const systemId = args[1];
|
|
144
|
+
const authMode = args[2];
|
|
145
|
+
if (!systemId || !authMode) {
|
|
146
|
+
console.error('Usage: node config-cli.mjs add-system <id> <authMode> [options...]');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const validModes = ['basic', 'spnego', 'x509', 'saml', 'snc'];
|
|
151
|
+
if (!validModes.includes(authMode)) {
|
|
152
|
+
console.error(`Invalid auth mode: ${authMode}. Use: ${validModes.join(', ')}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const opts = parseKeyValue(args.slice(3));
|
|
157
|
+
|
|
158
|
+
// Build connection object based on auth mode
|
|
159
|
+
let connection = {};
|
|
160
|
+
switch (authMode) {
|
|
161
|
+
case 'basic':
|
|
162
|
+
connection = {
|
|
163
|
+
host: opts.host || 'localhost',
|
|
164
|
+
port: opts.port || '44301',
|
|
165
|
+
client: opts.client || '100',
|
|
166
|
+
username: opts.username || '',
|
|
167
|
+
password: opts.password || '',
|
|
168
|
+
saprouter: opts.saprouter || '',
|
|
169
|
+
language: opts.language || 'EN',
|
|
170
|
+
};
|
|
171
|
+
break;
|
|
172
|
+
case 'spnego':
|
|
173
|
+
connection = {
|
|
174
|
+
host: opts.host || '',
|
|
175
|
+
port: opts.port || '44301',
|
|
176
|
+
client: opts.client || '100',
|
|
177
|
+
servicePrincipal: opts.service_principal || '',
|
|
178
|
+
language: opts.language || 'EN',
|
|
179
|
+
};
|
|
180
|
+
break;
|
|
181
|
+
case 'x509':
|
|
182
|
+
connection = {
|
|
183
|
+
host: opts.host || '',
|
|
184
|
+
port: opts.port || '44301',
|
|
185
|
+
client: opts.client || '100',
|
|
186
|
+
caCertPem: opts.ca_cert || '',
|
|
187
|
+
caKeyPem: opts.ca_key || '',
|
|
188
|
+
loginIdentifier: opts.login_id || '',
|
|
189
|
+
validityMinutes: parseInt(opts.validity_minutes || '5', 10),
|
|
190
|
+
language: opts.language || 'EN',
|
|
191
|
+
};
|
|
192
|
+
break;
|
|
193
|
+
case 'saml':
|
|
194
|
+
connection = {
|
|
195
|
+
baseUrl: opts.base_url || '',
|
|
196
|
+
client: opts.client || '100',
|
|
197
|
+
headless: opts.headless === 'true',
|
|
198
|
+
loginTimeoutMs: parseInt(opts.timeout_ms || '600000', 10),
|
|
199
|
+
language: opts.language || 'EN',
|
|
200
|
+
};
|
|
201
|
+
break;
|
|
202
|
+
case 'snc':
|
|
203
|
+
connection = {
|
|
204
|
+
host: opts.host || '',
|
|
205
|
+
sysnr: opts.sysnr || '00',
|
|
206
|
+
client: opts.client || '100',
|
|
207
|
+
username: opts.username || '',
|
|
208
|
+
saprouter: opts.saprouter || '',
|
|
209
|
+
sncPartnerName: opts.snc_partner_name || '',
|
|
210
|
+
sncMyName: opts.snc_my_name || '',
|
|
211
|
+
sncQop: opts.snc_qop || '3',
|
|
212
|
+
sncLib: opts.snc_lib || '',
|
|
213
|
+
language: opts.language || 'EN',
|
|
214
|
+
};
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
config.addOrUpdateSystem(systemId, { authMode, connection, enabled: true });
|
|
219
|
+
console.log(`✅ System "${systemId}" (${authMode}) saved.`);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── list-systems ───────────────────────────────────────────────────────
|
|
224
|
+
case 'list-systems': {
|
|
225
|
+
const systems = config.listSystems();
|
|
226
|
+
if (systems.length === 0) {
|
|
227
|
+
console.log('No systems configured.');
|
|
228
|
+
} else {
|
|
229
|
+
console.log(`Configured systems (${systems.length}):\n`);
|
|
230
|
+
console.log(' ID AUTH MODE ENABLED');
|
|
231
|
+
console.log(' ────────────────── ─────────── ───────');
|
|
232
|
+
for (const s of systems) {
|
|
233
|
+
console.log(` ${s.systemId.padEnd(20)} ${s.authMode.padEnd(12)} ${s.enabled ? 'yes' : 'no'}`);
|
|
234
|
+
}
|
|
235
|
+
console.log('');
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── remove-system ──────────────────────────────────────────────────────
|
|
241
|
+
case 'remove-system': {
|
|
242
|
+
const systemId = args[1];
|
|
243
|
+
if (!systemId) {
|
|
244
|
+
console.error('Usage: node config-cli.mjs remove-system <id>');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
config.removeSystem(systemId);
|
|
248
|
+
console.log(`✅ System "${systemId}" removed.`);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── enable-system / disable-system ─────────────────────────────────────
|
|
253
|
+
case 'enable-system':
|
|
254
|
+
case 'disable-system': {
|
|
255
|
+
const systemId = args[1];
|
|
256
|
+
if (!systemId) {
|
|
257
|
+
console.error(`Usage: node config-cli.mjs ${command} <id>`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const enabled = command === 'enable-system';
|
|
261
|
+
config.setSystemEnabled(systemId, enabled);
|
|
262
|
+
console.log(`✅ System "${systemId}" ${enabled ? 'enabled' : 'disabled'}.`);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── show-config ───────────────────────────────────────────────────────
|
|
267
|
+
case 'show-config': {
|
|
268
|
+
const cfg = config.loadConfig();
|
|
269
|
+
console.log(`Config file: ${config.getConfigFilePath()}`);
|
|
270
|
+
console.log(`MCP API key: ${cfg.mcpApiKey ? redact(cfg.mcpApiKey) : '(not set)'}`);
|
|
271
|
+
console.log(`WS URL: ${cfg.wsUrl || 'ws://localhost:8080'}`);
|
|
272
|
+
console.log(`Arcanum: ${cfg.enableArcanumFallback ? 'ON' : 'OFF'}`);
|
|
273
|
+
console.log(`Reconnect: ${cfg.reconnectDelayMs}ms`);
|
|
274
|
+
console.log('');
|
|
275
|
+
|
|
276
|
+
const sysIds = Object.keys(cfg.systems);
|
|
277
|
+
if (sysIds.length === 0) {
|
|
278
|
+
console.log('No systems configured.');
|
|
279
|
+
} else {
|
|
280
|
+
for (const id of sysIds) {
|
|
281
|
+
const sys = cfg.systems[id];
|
|
282
|
+
const conn = { ...sys.connection };
|
|
283
|
+
// Redact sensitive fields
|
|
284
|
+
if (conn.password) conn.password = redact(conn.password);
|
|
285
|
+
if (conn.caCertPem) conn.caCertPem = conn.caCertPem.substring(0, 40) + '...';
|
|
286
|
+
if (conn.caKeyPem) conn.caKeyPem = '***REDACTED***';
|
|
287
|
+
console.log(`[${id}] auth: ${sys.authMode} enabled: ${sys.enabled !== false}`);
|
|
288
|
+
console.log(` ${JSON.stringify(conn)}`);
|
|
289
|
+
console.log('');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── arcanum-fallback ───────────────────────────────────────────────────
|
|
296
|
+
case 'arcanum-fallback': {
|
|
297
|
+
const value = args[1];
|
|
298
|
+
if (!value || (value !== 'on' && value !== 'off')) {
|
|
299
|
+
console.error('Usage: node config-cli.mjs arcanum-fallback <on|off>');
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
config.setArcanumFallback(value === 'on');
|
|
303
|
+
console.log(`✅ Arcanum fallback: ${value.toUpperCase()}`);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── set-ws-url ──────────────────────────────────────────────────────────
|
|
308
|
+
case 'set-ws-url': {
|
|
309
|
+
const url = args[1];
|
|
310
|
+
if (!url) {
|
|
311
|
+
console.error('Usage: node config-cli.mjs set-ws-url <url>');
|
|
312
|
+
console.error(' local: ws://localhost:8080');
|
|
313
|
+
console.error(' cloud: wss://abap-mcp.mcps.b2rise.com/ws');
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
config.setWsUrl(url);
|
|
317
|
+
console.log(`✅ WS URL: ${url}`);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── help / default ─────────────────────────────────────────────────────
|
|
322
|
+
default:
|
|
323
|
+
usage();
|
|
324
|
+
break;
|
|
325
|
+
}
|