@mcp-abap-adt/connection 0.1.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/bin/sap-abap-auth.js +600 -0
  4. package/dist/config/sapConfig.d.ts +43 -0
  5. package/dist/config/sapConfig.d.ts.map +1 -0
  6. package/dist/config/sapConfig.js +202 -0
  7. package/dist/connection/AbapConnection.d.ts +22 -0
  8. package/dist/connection/AbapConnection.d.ts.map +1 -0
  9. package/dist/connection/AbapConnection.js +2 -0
  10. package/dist/connection/AbstractAbapConnection.d.ts +115 -0
  11. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -0
  12. package/dist/connection/AbstractAbapConnection.js +716 -0
  13. package/dist/connection/BaseAbapConnection.d.ts +17 -0
  14. package/dist/connection/BaseAbapConnection.d.ts.map +1 -0
  15. package/dist/connection/BaseAbapConnection.js +68 -0
  16. package/dist/connection/JwtAbapConnection.d.ts +33 -0
  17. package/dist/connection/JwtAbapConnection.d.ts.map +1 -0
  18. package/dist/connection/JwtAbapConnection.js +305 -0
  19. package/dist/connection/connectionFactory.d.ts +5 -0
  20. package/dist/connection/connectionFactory.d.ts.map +1 -0
  21. package/dist/connection/connectionFactory.js +15 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +29 -0
  25. package/dist/logger.d.ts +67 -0
  26. package/dist/logger.d.ts.map +1 -0
  27. package/dist/logger.js +2 -0
  28. package/dist/utils/FileSessionStorage.d.ts +73 -0
  29. package/dist/utils/FileSessionStorage.d.ts.map +1 -0
  30. package/dist/utils/FileSessionStorage.js +191 -0
  31. package/dist/utils/timeouts.d.ts +8 -0
  32. package/dist/utils/timeouts.d.ts.map +1 -0
  33. package/dist/utils/timeouts.js +21 -0
  34. package/dist/utils/tokenRefresh.d.ts +17 -0
  35. package/dist/utils/tokenRefresh.d.ts.map +1 -0
  36. package/dist/utils/tokenRefresh.js +53 -0
  37. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mario-andreschak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @mcp-abap-adt/connection
2
+
3
+ ABAP connection layer for MCP ABAP ADT server.
4
+
5
+ ## Installation
6
+
7
+ ### From Git Repository
8
+
9
+ ```bash
10
+ # Clone this repository
11
+ git clone https://github.com/fr0ster/mcp-abap-connection.git
12
+ cd mcp-abap-connection
13
+
14
+ # Install dependencies
15
+ # If used as submodule in monorepo, use: npm run install:local
16
+ # Otherwise, regular npm install works fine
17
+ npm install
18
+
19
+ # Build
20
+ npm run build
21
+ ```
22
+
23
+ ### From Tarball Archive
24
+
25
+ ```bash
26
+ # Generate archive (in package directory)
27
+ npm run pack
28
+ # Creates: mcp-abap-adt-connection-0.1.0.tgz
29
+
30
+ # Install from archive (in consuming project)
31
+ npm install ./path/to/mcp-abap-adt-connection-0.1.0.tgz
32
+ ```
33
+
34
+ Or in `package.json`:
35
+ ```json
36
+ {
37
+ "dependencies": {
38
+ "@mcp-abap-adt/connection": "file:./archives/mcp-abap-adt-connection-0.1.0.tgz"
39
+ }
40
+ }
41
+ ```
42
+
43
+ **Note:** This package is completely self-contained and has no dependencies on other `@mcp-abap-adt/*` packages.
44
+
45
+ ## Usage
46
+
47
+ ```typescript
48
+ import { createAbapConnection } from '@mcp-abap-adt/connection';
49
+
50
+ // Basic authentication
51
+ const connection = createAbapConnection({
52
+ url: 'https://your-sap-system.com',
53
+ authType: 'basic',
54
+ username: 'user',
55
+ password: 'pass',
56
+ client: '100'
57
+ });
58
+
59
+ // JWT authentication
60
+ const connection = createAbapConnection({
61
+ url: 'https://your-sap-system.com',
62
+ authType: 'jwt',
63
+ jwtToken: 'your-jwt-token',
64
+ refreshToken: 'your-refresh-token',
65
+ uaaUrl: 'https://uaa-url.com',
66
+ uaaClientId: 'client-id',
67
+ uaaClientSecret: 'client-secret'
68
+ });
69
+
70
+ // Make ADT request
71
+ const response = await connection.makeAdtRequest({
72
+ url: '/sap/bc/adt/oo/classes',
73
+ method: 'GET',
74
+ timeout: 30000
75
+ });
76
+ ```
77
+
78
+ ## Development
79
+
80
+ This package is developed independently. For development setup, see the repository's documentation.
@@ -0,0 +1,600 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const axios = require("axios");
5
+ const { program } = require("commander");
6
+ const express = require("express");
7
+ const open = require("open").default;
8
+ const http = require("http");
9
+
10
+ /**
11
+ * Get .env file path
12
+ * @param {string} customPath Optional custom path
13
+ * @returns {string} Path to .env file
14
+ */
15
+ function getEnvFilePath(customPath) {
16
+ if (customPath) {
17
+ return path.resolve(process.cwd(), customPath);
18
+ }
19
+ return path.resolve(process.cwd(), ".env");
20
+ }
21
+
22
+ // Browser selection via --browser option (chrome, edge, firefox, system, none)
23
+ const BROWSER_MAP = {
24
+ chrome: "chrome",
25
+ edge: "msedge",
26
+ firefox: "firefox",
27
+ system: undefined, // system default
28
+ none: null, // no browser, manual URL copy
29
+ };
30
+
31
+ /**
32
+ * Reads a JSON service key file
33
+ * @param {string} filePath Path to the service key file
34
+ * @returns {object} Service key data object
35
+ */
36
+ function readServiceKey(filePath) {
37
+ try {
38
+ const fullPath = path.resolve(process.cwd(), filePath);
39
+ if (!fs.existsSync(fullPath)) {
40
+ console.error(`File not found: ${fullPath}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ const fileContent = fs.readFileSync(fullPath, "utf8");
45
+ return JSON.parse(fileContent);
46
+ } catch (error) {
47
+ console.error(`Error reading service key: ${error.message}`);
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Reads existing .env file and parses it
54
+ * @param {string} envFilePath Path to .env file
55
+ * @returns {Object} Parsed .env values
56
+ */
57
+ function readEnvFile(envFilePath) {
58
+ try {
59
+ if (!fs.existsSync(envFilePath)) {
60
+ return {};
61
+ }
62
+ const content = fs.readFileSync(envFilePath, "utf8");
63
+ const env = {};
64
+ content.split("\n").forEach((line) => {
65
+ line = line.trim();
66
+ if (line && !line.startsWith("#")) {
67
+ const [key, ...valueParts] = line.split("=");
68
+ if (key && valueParts.length > 0) {
69
+ env[key.trim()] = valueParts.join("=").trim();
70
+ }
71
+ }
72
+ });
73
+ return env;
74
+ } catch (error) {
75
+ console.error(`Error reading .env file: ${error.message}`);
76
+ return {};
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Attempts to refresh JWT token using refresh token
82
+ * @param {string} refreshToken Refresh token from .env
83
+ * @param {string} uaaUrl UAA URL from .env
84
+ * @param {string} clientId UAA client ID from .env
85
+ * @param {string} clientSecret UAA client secret from .env
86
+ * @returns {Promise<{accessToken: string, refreshToken: string}|null>} New tokens or null if failed
87
+ */
88
+ async function tryRefreshToken(refreshToken, uaaUrl, clientId, clientSecret) {
89
+ try {
90
+ console.log("🔄 Attempting to refresh existing JWT token...");
91
+ const tokenUrl = `${uaaUrl}/oauth/token`;
92
+
93
+ const params = new URLSearchParams();
94
+ params.append("grant_type", "refresh_token");
95
+ params.append("refresh_token", refreshToken);
96
+
97
+ const authString = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
98
+
99
+ const response = await axios({
100
+ method: "post",
101
+ url: tokenUrl,
102
+ headers: {
103
+ Authorization: `Basic ${authString}`,
104
+ "Content-Type": "application/x-www-form-urlencoded",
105
+ },
106
+ data: params.toString(),
107
+ timeout: 10000, // 10 second timeout
108
+ });
109
+
110
+ if (response.data && response.data.access_token) {
111
+ console.log("✅ Token refreshed successfully!");
112
+ return {
113
+ accessToken: response.data.access_token,
114
+ refreshToken: response.data.refresh_token || refreshToken,
115
+ };
116
+ }
117
+ return null;
118
+ } catch (error) {
119
+ console.log(`⚠️ Token refresh failed: ${error.message}`);
120
+ console.log("📝 Falling back to browser authentication...");
121
+ return null;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Updates the .env file with new values
127
+ * @param {Object} updates Object with updated values
128
+ * @param {string} envFilePath Path to .env file
129
+ */
130
+ function updateEnvFile(updates, envFilePath) {
131
+ try {
132
+ // Always remove the old .env file if it exists
133
+ if (fs.existsSync(envFilePath)) {
134
+ fs.unlinkSync(envFilePath);
135
+ }
136
+ let lines = [];
137
+ if (updates.SAP_AUTH_TYPE === "jwt") {
138
+ // jwt: write only relevant params
139
+ const jwtAllowed = [
140
+ "SAP_URL",
141
+ "SAP_CLIENT",
142
+ "SAP_LANGUAGE",
143
+ "TLS_REJECT_UNAUTHORIZED",
144
+ "SAP_AUTH_TYPE",
145
+ "SAP_JWT_TOKEN",
146
+ "SAP_REFRESH_TOKEN",
147
+ "SAP_UAA_URL",
148
+ "SAP_UAA_CLIENT_ID",
149
+ "SAP_UAA_CLIENT_SECRET",
150
+ ];
151
+ jwtAllowed.forEach((key) => {
152
+ if (updates[key]) lines.push(`${key}=${updates[key]}`);
153
+ });
154
+ lines.push("");
155
+ lines.push("# For JWT authentication");
156
+ lines.push("# SAP_USERNAME=your_username");
157
+ lines.push("# SAP_PASSWORD=your_password");
158
+ } else {
159
+ // basic: write only relevant params
160
+ const basicAllowed = [
161
+ "SAP_URL",
162
+ "SAP_CLIENT",
163
+ "SAP_LANGUAGE",
164
+ "TLS_REJECT_UNAUTHORIZED",
165
+ "SAP_AUTH_TYPE",
166
+ "SAP_USERNAME",
167
+ "SAP_PASSWORD",
168
+ ];
169
+ basicAllowed.forEach((key) => {
170
+ if (updates[key]) lines.push(`${key}=${updates[key]}`);
171
+ });
172
+ lines.push("");
173
+ lines.push("# For JWT authentication (not used for basic)");
174
+ lines.push("# SAP_JWT_TOKEN=your_jwt_token_here");
175
+ }
176
+ fs.writeFileSync(envFilePath, lines.join("\n") + "\n", "utf8");
177
+ console.log(".env file created successfully.");
178
+ } catch (error) {
179
+ console.error(`Error updating .env file: ${error.message}`);
180
+ process.exit(1);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Builds the JWT (OAuth2) authentication URL
186
+ * @param {Object} serviceKey SAP BTP service key object
187
+ * @param {number} port Redirect URL port
188
+ * @returns {string} Authentication URL
189
+ */
190
+ function getJwtAuthorizationUrl(serviceKey, port = 3001) {
191
+ // Use serviceKey.uaa.url (OAuth endpoint) for OAuth2 authorization URL (correct for BTP ABAP)
192
+ const oauthUrl = serviceKey.uaa?.url;
193
+ const clientid = serviceKey.uaa?.clientid;
194
+ const redirectUri = `http://localhost:${port}/callback`;
195
+ return `${oauthUrl}/oauth/authorize?client_id=${encodeURIComponent(
196
+ clientid
197
+ )}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}`;
198
+ }
199
+
200
+ /**
201
+ * Starts a local server to intercept the authentication response
202
+ * @param {Object} serviceKey SAP BTP service key object
203
+ * @param {string} browser Browser to open
204
+ * @param {string} flow Flow type: jwt (OAuth2)
205
+ * @returns {Promise<{accessToken: string, refreshToken?: string}>} Promise that resolves to tokens
206
+ */
207
+ async function startAuthServer(serviceKey, browser = undefined, flow = "jwt") {
208
+ return new Promise((resolve, reject) => {
209
+ const app = express();
210
+ const server = http.createServer(app);
211
+ const PORT = 3001;
212
+ let serverInstance = null;
213
+
214
+ // Choose the authorization URL
215
+ const authorizationUrl = getJwtAuthorizationUrl(serviceKey, PORT);
216
+
217
+ // JWT OAuth2 flow (get code, exchange for token)
218
+ app.get("/callback", async (req, res) => {
219
+ try {
220
+ const { code } = req.query;
221
+ if (!code) {
222
+ res.status(400).send("Error: Authorization code missing");
223
+ return reject(new Error("Authorization code missing"));
224
+ }
225
+ console.log("Authorization code received");
226
+ res.send(`<!DOCTYPE html>
227
+ <html lang="en">
228
+ <head>
229
+ <meta charset="UTF-8">
230
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
231
+ <title>SAP BTP Authentication</title>
232
+ <style>
233
+ body {
234
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
235
+ text-align: center;
236
+ margin: 0;
237
+ padding: 50px 20px;
238
+ background: linear-gradient(135deg, #0070f3 0%, #00d4ff 100%);
239
+ color: white;
240
+ min-height: 100vh;
241
+ display: flex;
242
+ flex-direction: column;
243
+ justify-content: center;
244
+ align-items: center;
245
+ }
246
+ .container {
247
+ background: rgba(255, 255, 255, 0.1);
248
+ border-radius: 20px;
249
+ padding: 40px;
250
+ backdrop-filter: blur(10px);
251
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
252
+ max-width: 500px;
253
+ width: 100%;
254
+ }
255
+ .success-icon {
256
+ font-size: 4rem;
257
+ margin-bottom: 20px;
258
+ color: #4ade80;
259
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
260
+ }
261
+ h1 {
262
+ margin: 0 0 20px 0;
263
+ font-size: 2rem;
264
+ font-weight: 300;
265
+ }
266
+ p {
267
+ margin: 0;
268
+ font-size: 1.1rem;
269
+ opacity: 0.9;
270
+ line-height: 1.5;
271
+ }
272
+ .sap-logo {
273
+ margin-top: 30px;
274
+ font-weight: bold;
275
+ opacity: 0.7;
276
+ font-size: 0.9rem;
277
+ }
278
+ </style>
279
+ </head>
280
+ <body>
281
+ <div class="container">
282
+ <div class="success-icon">✓</div>
283
+ <h1>Authentication Successful!</h1>
284
+ <p>You have successfully authenticated with SAP BTP.</p>
285
+ <p>You can now close this browser window.</p>
286
+ <div class="sap-logo">SAP Business Technology Platform</div>
287
+ </div>
288
+ </body>
289
+ </html>`);
290
+ try {
291
+ const tokens = await exchangeCodeForToken(serviceKey, code);
292
+ server.close(() => {
293
+ console.log("Authentication server stopped");
294
+ });
295
+ resolve(tokens);
296
+ } catch (error) {
297
+ reject(error);
298
+ }
299
+ } catch (error) {
300
+ console.error("Error handling callback:", error);
301
+ res.status(500).send("Error processing authentication");
302
+ reject(error);
303
+ }
304
+ });
305
+
306
+ serverInstance = server.listen(PORT, () => {
307
+ console.log(`Authentication server started on port ${PORT}`);
308
+
309
+ const browserApp = BROWSER_MAP[browser];
310
+ if (!browser || browser === "none" || browserApp === null) {
311
+ console.log(
312
+ "\nBrowser not specified. Please manually open the following URL:"
313
+ );
314
+ console.log("");
315
+ console.log(`🔗 ${authorizationUrl}`);
316
+ console.log("");
317
+ console.log(
318
+ "Copy and paste this URL into your browser to authenticate.\n"
319
+ );
320
+ } else {
321
+ console.log("Opening browser for authentication...");
322
+ if (browserApp) {
323
+ open(authorizationUrl, { app: { name: browserApp } });
324
+ } else {
325
+ open(authorizationUrl);
326
+ }
327
+ }
328
+ });
329
+
330
+ setTimeout(() => {
331
+ if (serverInstance) {
332
+ serverInstance.close();
333
+ reject(new Error("Authentication timeout. Process aborted."));
334
+ }
335
+ }, 5 * 60 * 1000);
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Exchanges the authorization code for tokens
341
+ * @param {Object} serviceKey SAP BTP service key object
342
+ * @param {string} code Authorization code
343
+ * @returns {Promise<{accessToken: string, refreshToken?: string}>} Promise that resolves to tokens
344
+ */
345
+ async function exchangeCodeForToken(serviceKey, code) {
346
+ try {
347
+ const { url, clientid, clientsecret } = serviceKey.uaa;
348
+ const tokenUrl = `${url}/oauth/token`;
349
+ const redirectUri = "http://localhost:3001/callback";
350
+
351
+ const params = new URLSearchParams();
352
+ params.append("grant_type", "authorization_code");
353
+ params.append("code", code);
354
+ params.append("redirect_uri", redirectUri);
355
+
356
+ const authString = Buffer.from(`${clientid}:${clientsecret}`).toString(
357
+ "base64"
358
+ );
359
+
360
+ const response = await axios({
361
+ method: "post",
362
+ url: tokenUrl,
363
+ headers: {
364
+ Authorization: `Basic ${authString}`,
365
+ "Content-Type": "application/x-www-form-urlencoded",
366
+ },
367
+ data: params.toString(),
368
+ });
369
+
370
+ if (response.data && response.data.access_token) {
371
+ console.log("OAuth token received successfully.");
372
+ return {
373
+ accessToken: response.data.access_token,
374
+ refreshToken: response.data.refresh_token
375
+ };
376
+ } else {
377
+ throw new Error("Response does not contain access_token");
378
+ }
379
+ } catch (error) {
380
+ if (error.response) {
381
+ console.error(
382
+ `API error (${error.response.status}): ${JSON.stringify(
383
+ error.response.data
384
+ )}`
385
+ );
386
+ } else {
387
+ console.error(`Error obtaining OAuth token: ${error.message}`);
388
+ }
389
+ throw error;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Refreshes the access token using refresh token
395
+ * @param {Object} serviceKey SAP BTP service key object
396
+ * @param {string} refreshToken Refresh token
397
+ * @returns {Promise<{accessToken: string, refreshToken?: string}>} Promise that resolves to new tokens
398
+ */
399
+ async function refreshJwtToken(serviceKey, refreshToken) {
400
+ try {
401
+ const { url, clientid, clientsecret } = serviceKey.uaa;
402
+ const tokenUrl = `${url}/oauth/token`;
403
+
404
+ const params = new URLSearchParams();
405
+ params.append("grant_type", "refresh_token");
406
+ params.append("refresh_token", refreshToken);
407
+
408
+ const authString = Buffer.from(`${clientid}:${clientsecret}`).toString(
409
+ "base64"
410
+ );
411
+
412
+ const response = await axios({
413
+ method: "post",
414
+ url: tokenUrl,
415
+ headers: {
416
+ Authorization: `Basic ${authString}`,
417
+ "Content-Type": "application/x-www-form-urlencoded",
418
+ },
419
+ data: params.toString(),
420
+ });
421
+
422
+ if (response.data && response.data.access_token) {
423
+ console.log("Access token refreshed successfully.");
424
+ return {
425
+ accessToken: response.data.access_token,
426
+ refreshToken: response.data.refresh_token || refreshToken // Use new refresh token if provided, otherwise keep old one
427
+ };
428
+ } else {
429
+ throw new Error("Response does not contain access_token");
430
+ }
431
+ } catch (error) {
432
+ if (error.response) {
433
+ console.error(
434
+ `API error (${error.response.status}): ${JSON.stringify(
435
+ error.response.data
436
+ )}`
437
+ );
438
+ } else {
439
+ console.error(`Error refreshing OAuth token: ${error.message}`);
440
+ }
441
+ throw error;
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Main program function
447
+ */
448
+ async function main() {
449
+ program
450
+ .name("sap-abap-auth")
451
+ .description(
452
+ "CLI utility for authentication in SAP BTP ABAP Environment (Steampunk) via browser. Creates .env file with connection configuration."
453
+ )
454
+ .version("0.1.0")
455
+ .helpOption("-h, --help", "Show help for all commands and options");
456
+
457
+ program
458
+ .command("auth")
459
+ .description(
460
+ "Authenticate in SAP BTP ABAP Environment (Steampunk) via browser and update .env file (JWT)"
461
+ )
462
+ .requiredOption(
463
+ "-k, --key <path>",
464
+ "Path to the service key file in JSON format"
465
+ )
466
+ .option(
467
+ "-b, --browser <browser>",
468
+ "Browser to open (chrome, edge, firefox, system, none). Use 'none' or omit to display URL for manual copy."
469
+ )
470
+ .option(
471
+ "-o, --output <path>",
472
+ "Path to output .env file (default: .env in current directory)"
473
+ )
474
+ .option(
475
+ "-f, --force",
476
+ "Force browser authentication even if valid tokens exist in .env"
477
+ )
478
+ .helpOption("-h, --help", "Show help for the auth command")
479
+ .action(async (options) => {
480
+ try {
481
+ if (!options.key) {
482
+ console.error(
483
+ "Service key file (--key) is required for authentication. Please provide a valid service key JSON file."
484
+ );
485
+ process.exit(1);
486
+ }
487
+ console.log("Starting authentication process...");
488
+ const serviceKey = readServiceKey(options.key);
489
+ console.log("Service key read successfully.");
490
+
491
+ // Validate required fields in service key
492
+ const abapUrl =
493
+ serviceKey.url || serviceKey.abap?.url || serviceKey.sap_url;
494
+ if (!abapUrl) {
495
+ console.error(
496
+ "SAP_URL is missing in the service key. Please check your service key JSON file."
497
+ );
498
+ process.exit(1);
499
+ }
500
+
501
+ let tokens = null;
502
+
503
+ // Try to refresh existing token if not forced
504
+ if (!options.force) {
505
+ const envFilePath = getEnvFilePath(options.output);
506
+ const existingEnv = readEnvFile(envFilePath);
507
+
508
+ // Check if we have all necessary data for token refresh
509
+ if (
510
+ existingEnv.SAP_REFRESH_TOKEN &&
511
+ existingEnv.SAP_UAA_URL &&
512
+ existingEnv.SAP_UAA_CLIENT_ID &&
513
+ existingEnv.SAP_UAA_CLIENT_SECRET
514
+ ) {
515
+ tokens = await tryRefreshToken(
516
+ existingEnv.SAP_REFRESH_TOKEN,
517
+ existingEnv.SAP_UAA_URL,
518
+ existingEnv.SAP_UAA_CLIENT_ID,
519
+ existingEnv.SAP_UAA_CLIENT_SECRET
520
+ );
521
+ } else if (existingEnv.SAP_REFRESH_TOKEN) {
522
+ console.log("⚠️ Refresh token found in .env but missing UAA credentials");
523
+ console.log("📝 Falling back to browser authentication...");
524
+ }
525
+ } else {
526
+ console.log("🔒 Force mode enabled - skipping token refresh");
527
+ }
528
+
529
+ // Fallback to browser authentication if refresh failed or was skipped
530
+ if (!tokens) {
531
+ console.log("🌐 Starting browser authentication...");
532
+ tokens = await startAuthServer(serviceKey, options.browser, "jwt");
533
+ if (!tokens || !tokens.accessToken) {
534
+ console.error("JWT token was not obtained. Authentication failed.");
535
+ process.exit(1);
536
+ }
537
+ }
538
+
539
+ // Collect all relevant parameters from service key
540
+ const envUpdates = {
541
+ SAP_URL: abapUrl,
542
+ TLS_REJECT_UNAUTHORIZED: "0",
543
+ SAP_AUTH_TYPE: "jwt",
544
+ SAP_JWT_TOKEN: tokens.accessToken,
545
+ };
546
+
547
+ // Add refresh token if available
548
+ if (tokens.refreshToken) {
549
+ envUpdates.SAP_REFRESH_TOKEN = tokens.refreshToken;
550
+ }
551
+
552
+ // Add UAA credentials for token refresh
553
+ if (serviceKey.uaa?.url) {
554
+ envUpdates.SAP_UAA_URL = serviceKey.uaa.url;
555
+ }
556
+ if (serviceKey.uaa?.clientid) {
557
+ envUpdates.SAP_UAA_CLIENT_ID = serviceKey.uaa.clientid;
558
+ }
559
+ if (serviceKey.uaa?.clientsecret) {
560
+ envUpdates.SAP_UAA_CLIENT_SECRET = serviceKey.uaa.clientsecret;
561
+ }
562
+ // Optional: client
563
+ const abapClient =
564
+ serviceKey.client || serviceKey.abap?.client || serviceKey.sap_client;
565
+ if (abapClient) {
566
+ envUpdates.SAP_CLIENT = abapClient;
567
+ }
568
+ // Optional: language
569
+ if (serviceKey.language) {
570
+ envUpdates.SAP_LANGUAGE = serviceKey.language;
571
+ } else if (serviceKey.abap && serviceKey.abap.language) {
572
+ envUpdates.SAP_LANGUAGE = serviceKey.abap.language;
573
+ }
574
+
575
+ // Use custom output path if provided
576
+ const envFilePath = getEnvFilePath(options.output);
577
+ updateEnvFile(envUpdates, envFilePath);
578
+ console.log("Authentication completed successfully!");
579
+ process.exit(0);
580
+ } catch (error) {
581
+ console.error(`Error during authentication: ${error.message}`);
582
+ process.exit(1);
583
+ }
584
+ });
585
+
586
+ // Parse and handle command-line arguments
587
+ program.parse(process.argv);
588
+
589
+ // If no arguments were provided, show help
590
+ if (process.argv.length === 2) {
591
+ program.help();
592
+ }
593
+ }
594
+
595
+ // Execute the main function
596
+ main().catch((error) => {
597
+ console.error(`Unexpected error: ${error.message}`);
598
+ process.exit(1);
599
+ });
600
+