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 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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // abap-local-client entry point (CommonJS wrapper for ESM package)
3
+ (async () => {
4
+ await import('../sso-sap-client.mjs');
5
+ })().catch(err => {
6
+ console.error('Failed to start abap-local-client:', err.message);
7
+ process.exit(1);
8
+ });
package/bin/config.cjs ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // abap-local-config entry point (CommonJS wrapper for ESM package)
3
+ (async () => {
4
+ await import('../config-cli.mjs');
5
+ })().catch(err => {
6
+ console.error('Failed to start abap-local-config:', err.message);
7
+ process.exit(1);
8
+ });
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
+ }