@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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/bin/sap-abap-auth.js +600 -0
- package/dist/config/sapConfig.d.ts +43 -0
- package/dist/config/sapConfig.d.ts.map +1 -0
- package/dist/config/sapConfig.js +202 -0
- package/dist/connection/AbapConnection.d.ts +22 -0
- package/dist/connection/AbapConnection.d.ts.map +1 -0
- package/dist/connection/AbapConnection.js +2 -0
- package/dist/connection/AbstractAbapConnection.d.ts +115 -0
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -0
- package/dist/connection/AbstractAbapConnection.js +716 -0
- package/dist/connection/BaseAbapConnection.d.ts +17 -0
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -0
- package/dist/connection/BaseAbapConnection.js +68 -0
- package/dist/connection/JwtAbapConnection.d.ts +33 -0
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -0
- package/dist/connection/JwtAbapConnection.js +305 -0
- package/dist/connection/connectionFactory.d.ts +5 -0
- package/dist/connection/connectionFactory.d.ts.map +1 -0
- package/dist/connection/connectionFactory.js +15 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/logger.d.ts +67 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +2 -0
- package/dist/utils/FileSessionStorage.d.ts +73 -0
- package/dist/utils/FileSessionStorage.d.ts.map +1 -0
- package/dist/utils/FileSessionStorage.js +191 -0
- package/dist/utils/timeouts.d.ts +8 -0
- package/dist/utils/timeouts.d.ts.map +1 -0
- package/dist/utils/timeouts.js +21 -0
- package/dist/utils/tokenRefresh.d.ts +17 -0
- package/dist/utils/tokenRefresh.d.ts.map +1 -0
- package/dist/utils/tokenRefresh.js +53 -0
- 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
|
+
|