@abapify/adt-auth 0.4.34 → 0.4.36
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 +21 -286
- package/dist/{env-B1kvodv4.d.mts → env-BQ8Uy_8m.d.mts} +2 -2
- package/dist/env-BQ8Uy_8m.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/plugins/basic.mjs +4 -4
- package/dist/plugins/basic.mjs.map +1 -1
- package/dist/plugins/service-key.mjs +1 -1
- package/dist/{service-key-upymvzHr.mjs → service-key-DEYaMW4g.mjs} +34 -17
- package/dist/service-key-DEYaMW4g.mjs.map +1 -0
- package/dist/service-key.d.mts.map +1 -1
- package/dist/service-key.mjs +1 -1
- package/dist/utils/env.d.mts +1 -1
- package/package.json +2 -2
- package/dist/env-B1kvodv4.d.mts.map +0 -1
- package/dist/service-key-upymvzHr.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,307 +1,42 @@
|
|
|
1
1
|
# @abapify/adt-auth
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@abapify/adt-auth)
|
|
4
|
+
[](https://github.com/abapify/adt-cli/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
Authentication package for SAP ADT systems
|
|
6
|
+
Authentication package for SAP ADT systems - supports multiple auth methods.
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
- ✅ **Multiple Auth Methods**: Basic, SLC, OAuth (extensible)
|
|
10
|
-
- ✅ **Secure Storage**: File-based with proper permissions
|
|
11
|
-
- ✅ **Type-Safe**: Full TypeScript support
|
|
12
|
-
- ✅ **Testable**: Easy to mock and test
|
|
13
|
-
- ✅ **Reusable**: Can be used by any Node.js tool
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
8
|
+
## Install
|
|
16
9
|
|
|
17
10
|
```bash
|
|
18
11
|
npm install @abapify/adt-auth
|
|
19
|
-
# or
|
|
20
|
-
bun add @abapify/adt-auth
|
|
21
12
|
```
|
|
22
13
|
|
|
23
14
|
## Usage
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
import { AuthManager, BasicAuthMethod } from '@abapify/adt-auth';
|
|
29
|
-
|
|
30
|
-
const authManager = new AuthManager();
|
|
31
|
-
authManager.registerMethod(new BasicAuthMethod());
|
|
32
|
-
|
|
33
|
-
// Login
|
|
34
|
-
const session = await authManager.login(
|
|
35
|
-
{
|
|
36
|
-
method: 'basic',
|
|
37
|
-
baseUrl: 'https://sap.example.com',
|
|
38
|
-
username: 'user',
|
|
39
|
-
password: 'pass',
|
|
40
|
-
client: '100',
|
|
41
|
-
},
|
|
42
|
-
'TRL',
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
console.log('✅ Logged in!', session.sid);
|
|
46
|
-
|
|
47
|
-
// Later: get credentials
|
|
48
|
-
const credentials = authManager.getCredentials('TRL');
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### SAP Secure Login Client (SLC)
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
import { AuthManager, SlcAuthMethod } from '@abapify/adt-auth';
|
|
55
|
-
|
|
56
|
-
const authManager = new AuthManager();
|
|
57
|
-
authManager.registerMethod(new SlcAuthMethod());
|
|
58
|
-
|
|
59
|
-
// Login (SLC handles authentication via proxy)
|
|
60
|
-
const session = await authManager.login(
|
|
61
|
-
{
|
|
62
|
-
method: 'slc',
|
|
63
|
-
baseUrl: 'https://sap.example.com',
|
|
64
|
-
slcProxy: {
|
|
65
|
-
host: 'localhost',
|
|
66
|
-
port: 3128,
|
|
67
|
-
},
|
|
68
|
-
client: '200',
|
|
69
|
-
},
|
|
70
|
-
'TRL',
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
console.log('✅ Logged in via SLC!');
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### Multiple Systems
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
// Login to multiple systems
|
|
80
|
-
await authManager.login(config1, 'TRL');
|
|
81
|
-
await authManager.login(config2, 'QAS');
|
|
82
|
-
await authManager.login(config3, 'PRD');
|
|
83
|
-
|
|
84
|
-
// List all systems
|
|
85
|
-
const sids = authManager.listSids();
|
|
86
|
-
console.log('Available systems:', sids);
|
|
87
|
-
|
|
88
|
-
// Set default
|
|
89
|
-
authManager.setDefaultSid('TRL');
|
|
16
|
+
```ts
|
|
17
|
+
import { AuthManager } from '@abapify/adt-auth';
|
|
90
18
|
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Main orchestrator for authentication.
|
|
100
|
-
|
|
101
|
-
#### Methods
|
|
102
|
-
|
|
103
|
-
- `registerMethod(method: AuthMethod)` - Register an auth method
|
|
104
|
-
- `login(config: AuthConfig, sid: string)` - Login and create session
|
|
105
|
-
- `getSession(sid?: string)` - Get session (uses default if sid not provided)
|
|
106
|
-
- `getCredentials(sid?: string)` - Get credentials for a session
|
|
107
|
-
- `testSession(sid: string)` - Test if session is still valid
|
|
108
|
-
- `logout(sid: string)` - Delete session
|
|
109
|
-
- `listSids()` - List all available SIDs
|
|
110
|
-
- `setDefaultSid(sid: string)` - Set default SID
|
|
111
|
-
- `getDefaultSid()` - Get default SID
|
|
112
|
-
|
|
113
|
-
### Auth Methods
|
|
114
|
-
|
|
115
|
-
#### BasicAuthMethod
|
|
116
|
-
|
|
117
|
-
Username/password authentication.
|
|
118
|
-
|
|
119
|
-
**Config:**
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
{
|
|
123
|
-
method: 'basic',
|
|
124
|
-
baseUrl: string,
|
|
125
|
-
username: string,
|
|
126
|
-
password: string,
|
|
127
|
-
client?: string,
|
|
128
|
-
language?: string,
|
|
129
|
-
insecure?: boolean,
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
#### SlcAuthMethod
|
|
134
|
-
|
|
135
|
-
SAP Secure Login Client Web Adapter.
|
|
136
|
-
|
|
137
|
-
**Config:**
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
{
|
|
141
|
-
method: 'slc',
|
|
142
|
-
baseUrl: string,
|
|
143
|
-
slcProxy: {
|
|
144
|
-
host: string,
|
|
145
|
-
port: number,
|
|
19
|
+
const auth = new AuthManager();
|
|
20
|
+
const session = await auth.login('DEV', {
|
|
21
|
+
plugin: '@abapify/adt-auth/plugins/basic',
|
|
22
|
+
options: {
|
|
23
|
+
url: 'https://sap.example.com',
|
|
24
|
+
username: 'DEVELOPER',
|
|
25
|
+
password,
|
|
146
26
|
},
|
|
147
|
-
client?: string,
|
|
148
|
-
language?: string,
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
**Prerequisites:**
|
|
153
|
-
|
|
154
|
-
- SAP Secure Login Client installed and running
|
|
155
|
-
- Web Adapter profile configured and active
|
|
156
|
-
|
|
157
|
-
## Storage
|
|
158
|
-
|
|
159
|
-
Sessions are stored in `~/.adt/sessions/` with file permissions `0600` (owner read/write only).
|
|
160
|
-
|
|
161
|
-
### File Structure
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
~/.adt/
|
|
165
|
-
└── sessions/
|
|
166
|
-
├── TRL.json
|
|
167
|
-
├── QAS.json
|
|
168
|
-
└── PRD.json
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Session Format
|
|
172
|
-
|
|
173
|
-
```json
|
|
174
|
-
{
|
|
175
|
-
"sid": "TRL",
|
|
176
|
-
"credentials": {
|
|
177
|
-
"method": "basic",
|
|
178
|
-
"baseUrl": "https://sap.example.com",
|
|
179
|
-
"username": "user",
|
|
180
|
-
"password": "encrypted",
|
|
181
|
-
"client": "100"
|
|
182
|
-
},
|
|
183
|
-
"createdAt": "2025-01-01T00:00:00.000Z",
|
|
184
|
-
"lastUsed": "2025-01-01T00:00:00.000Z"
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Integration with adt-client
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
import { createAdtClient } from '@abapify/adt-client';
|
|
192
|
-
import { AuthManager, BasicAuthMethod } from '@abapify/adt-auth';
|
|
193
|
-
|
|
194
|
-
// Get credentials
|
|
195
|
-
const authManager = new AuthManager();
|
|
196
|
-
authManager.registerMethod(new BasicAuthMethod());
|
|
197
|
-
const credentials = authManager.getCredentials('TRL');
|
|
198
|
-
|
|
199
|
-
if (!credentials) {
|
|
200
|
-
throw new Error('Not authenticated. Run: adt login');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Create ADT client
|
|
204
|
-
const client = createAdtClient({
|
|
205
|
-
baseUrl: credentials.baseUrl,
|
|
206
|
-
client: credentials.client,
|
|
207
|
-
|
|
208
|
-
// Method-specific config
|
|
209
|
-
...(credentials.method === 'basic' && {
|
|
210
|
-
username: credentials.username,
|
|
211
|
-
password: credentials.password,
|
|
212
|
-
}),
|
|
213
|
-
|
|
214
|
-
...(credentials.method === 'slc' && {
|
|
215
|
-
proxy: credentials.slcProxy,
|
|
216
|
-
}),
|
|
217
27
|
});
|
|
218
|
-
|
|
219
|
-
// Use client
|
|
220
|
-
const info =
|
|
221
|
-
await client.adt.core.http.systeminformation.getSystemInformation();
|
|
222
|
-
console.log('System:', info);
|
|
223
28
|
```
|
|
224
29
|
|
|
225
|
-
##
|
|
226
|
-
|
|
227
|
-
Auth plugins provide authentication methods. AuthManager is generic and delegates to plugins.
|
|
228
|
-
|
|
229
|
-
### Plugin Contract
|
|
230
|
-
|
|
231
|
-
All auth plugins MUST:
|
|
232
|
-
|
|
233
|
-
1. **Export default** - The plugin must be the default export
|
|
234
|
-
2. **Implement AuthPlugin interface** - Must have `authenticate(options): Promise<AuthPluginResult>`
|
|
235
|
-
3. **Return AuthPluginResult** - Standard format
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
// Plugin implementation
|
|
239
|
-
import type {
|
|
240
|
-
AuthPlugin,
|
|
241
|
-
AuthPluginResult,
|
|
242
|
-
AuthPluginOptions,
|
|
243
|
-
} from '@abapify/adt-auth';
|
|
244
|
-
|
|
245
|
-
const authPlugin: AuthPlugin = {
|
|
246
|
-
async authenticate(options: AuthPluginOptions): Promise<AuthPluginResult> {
|
|
247
|
-
// Your auth logic...
|
|
248
|
-
return {
|
|
249
|
-
method: 'cookie',
|
|
250
|
-
credentials: {
|
|
251
|
-
cookies: 'cookie-string',
|
|
252
|
-
expiresAt: new Date(Date.now() + 8 * 60 * 60 * 1000),
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
export default authPlugin;
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Available Plugins
|
|
262
|
-
|
|
263
|
-
- **@abapify/adt-puppeteer** - Browser-based SSO authentication
|
|
264
|
-
|
|
265
|
-
### Credential Refresh
|
|
266
|
-
|
|
267
|
-
When sessions expire, AuthManager automatically refreshes using the stored plugin:
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
// AuthManager loads plugin dynamically
|
|
271
|
-
const pluginModule = await import(session.auth.plugin);
|
|
272
|
-
const result = await pluginModule.default.authenticate(options);
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
## Security
|
|
276
|
-
|
|
277
|
-
### Credential Storage
|
|
278
|
-
|
|
279
|
-
- **File permissions**: `0600` (owner read/write only)
|
|
280
|
-
- **Directory permissions**: `0700` (owner only)
|
|
281
|
-
- **Password encryption**: TODO (currently plain text)
|
|
282
|
-
|
|
283
|
-
### SLC Security
|
|
284
|
-
|
|
285
|
-
- No credentials stored (SLC handles authentication)
|
|
286
|
-
- Only proxy configuration stored
|
|
287
|
-
- Certificate-based authentication via SLC
|
|
288
|
-
|
|
289
|
-
## Development
|
|
290
|
-
|
|
291
|
-
```bash
|
|
292
|
-
# Install dependencies
|
|
293
|
-
bun install
|
|
30
|
+
## Features
|
|
294
31
|
|
|
295
|
-
|
|
296
|
-
|
|
32
|
+
- Plugin-based credential acquisition (`basic`, `service-key`, browser SSO)
|
|
33
|
+
- File-backed session storage per destination
|
|
34
|
+
- BTP service-key helpers (`readServiceKey`, `resolveServiceKeyFromEnv`)
|
|
297
35
|
|
|
298
|
-
|
|
299
|
-
bun test
|
|
36
|
+
## Documentation
|
|
300
37
|
|
|
301
|
-
|
|
302
|
-
bun run typecheck
|
|
303
|
-
```
|
|
38
|
+
**[adt-cli.netlify.app/sdk/packages/adt-auth](https://adt-cli.netlify.app/sdk/packages/adt-auth)**
|
|
304
39
|
|
|
305
|
-
|
|
40
|
+
---
|
|
306
41
|
|
|
307
|
-
|
|
42
|
+
Part of [**abapify**](https://github.com/abapify/adt-cli) — open, typed tooling for SAP ABAP Development Tools (ADT): CLI, MCP server, and SDK.
|
|
@@ -51,7 +51,7 @@ interface Destination {
|
|
|
51
51
|
options: AuthPluginOptions;
|
|
52
52
|
}
|
|
53
53
|
declare class AuthManager {
|
|
54
|
-
private storage;
|
|
54
|
+
private readonly storage;
|
|
55
55
|
constructor(storage?: FileStorage);
|
|
56
56
|
/**
|
|
57
57
|
* Login using a destination configuration
|
|
@@ -191,4 +191,4 @@ declare function readServiceKey(filePath: string): BTPServiceKey;
|
|
|
191
191
|
*/
|
|
192
192
|
declare function resolveServiceKeyFromEnv(): Destination | null;
|
|
193
193
|
export { UAACredentials as a, FileStorage as c, ServiceKeyPluginOptions as i, resolveServiceKeyFromEnv as n, AuthManager as o, BTPServiceKey as r, Destination as s, readServiceKey as t };
|
|
194
|
-
//# sourceMappingURL=env-
|
|
194
|
+
//# sourceMappingURL=env-BQ8Uy_8m.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-BQ8Uy_8m.d.mts","names":[],"sources":["../src/storage/file-storage.ts","../src/auth-manager.ts","../src/types/service-key.ts","../src/utils/env.ts"],"mappings":";cAuBa;mBACM;mBACA;mBACA;EAEL,YAAA;;;;EAcZ,KAAK,SAAS;;;;EASd,KAAK,cAAc;;;;EAoBnB,OAAO;;;;EAUP;;;;EAaA,OAAO;;;;EAWP,cAAc;;;;EASd;;;;EAQA;UAUQ;UAIA;UAMA;UAWA;;;;;UCpIO;;EAEf;;EAEA,SAAS;;cAGE;mBACM;EAEL,YAAA,UAAU;;;;;;;;;EAgBhB,MAAM,aAAa,aAAa,cAAc,QAAQ;;;;UAgCpD;;;;EA0CR,YAAY,SAAS;;;;EAOrB,WAAW,eAAe;;;;EAW1B,cAAc;;;;EAOd;;;;EAWA,cAAc;;;;EAOd;;;;EAOA;;;;EAWA,UAAU,SAAS;;;;;;EAab,mBACJ,SAAS,aACT;IAAY,OAAO;MAClB,QAAQ;;;;EAyFX,gBAAgB,SAAS;;;;EAWzB,aACE,SAAS;IACN;IAAkB;;;UC1SR;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf,KAAK;EACL;EACA;EACA;EACA,WAAW;EACX,UAAU,eAAe;EACzB,SAAS;EACT;;UAYe;EACf;EACA;EACA;;EAEA,eAAe,gBAAgB;;EAE/B;;EAEA;;EAEA;GACC;;;;;;;;;;iBCrDa,eAAe,mBAAmB;;;;;;;;;;;iBA6BlC,4BAA4B"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as AuthPluginResult, c as BasicCredentials, d as CookieCredentials, f as Credentials, i as AuthPluginOptions, l as ConnectionTestResult, n as AuthMethod, o as AuthSession, r as AuthPlugin, s as BasicAuthResult, t as AuthConfig, u as CookieAuthResult } from "./types-DRCpFrpT.mjs";
|
|
2
|
-
import { a as UAACredentials, c as FileStorage, i as ServiceKeyPluginOptions, n as resolveServiceKeyFromEnv, o as AuthManager, r as BTPServiceKey, s as Destination, t as readServiceKey } from "./env-
|
|
2
|
+
import { a as UAACredentials, c as FileStorage, i as ServiceKeyPluginOptions, n as resolveServiceKeyFromEnv, o as AuthManager, r as BTPServiceKey, s as Destination, t as readServiceKey } from "./env-BQ8Uy_8m.mjs";
|
|
3
3
|
import authPlugin from "./basic.mjs";
|
|
4
4
|
import authPlugin$1 from "./service-key.mjs";
|
|
5
5
|
export { type AuthConfig, AuthManager, type AuthMethod, type AuthPlugin, type AuthPluginOptions, type AuthPluginResult, type AuthSession, type BTPServiceKey, type BasicAuthResult, type BasicCredentials, type ConnectionTestResult, type CookieAuthResult, type CookieCredentials, type Credentials, type Destination, FileStorage, type ServiceKeyPluginOptions, type UAACredentials, authPlugin as basicAuthPlugin, readServiceKey, resolveServiceKeyFromEnv, authPlugin$1 as serviceKeyAuthPlugin };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import authPlugin from "./plugins/basic.mjs";
|
|
2
|
-
import { t as authPlugin$1 } from "./service-key-
|
|
2
|
+
import { t as authPlugin$1 } from "./service-key-DEYaMW4g.mjs";
|
|
3
3
|
import { readServiceKey, resolveServiceKeyFromEnv } from "./utils/env.mjs";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/storage/file-storage.ts","../src/auth-manager.ts"],"sourcesContent":["/**\n * File-based session storage\n *\n * Stores auth sessions in ~/.adt/sessions/\n * Stores config in ~/.adt/config.json\n */\n\nimport {\n readFileSync,\n writeFileSync,\n existsSync,\n mkdirSync,\n readdirSync,\n unlinkSync,\n} from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { homedir } from 'node:os';\nimport type { AuthSession } from '../types';\n\ninterface Config {\n defaultSid?: string;\n}\n\nexport class FileStorage {\n private readonly baseDir: string;\n private readonly storageDir: string;\n private readonly configPath: string;\n\n constructor(baseDir?: string) {\n this.baseDir = baseDir || join(homedir(), '.adt');\n this.storageDir = join(this.baseDir, 'sessions');\n this.configPath = join(this.baseDir, 'config.json');\n this.ensureStorageDir();\n }\n\n // ===========================================================================\n // Session Storage\n // ===========================================================================\n\n /**\n * Save session to file\n */\n save(session: AuthSession): void {\n const filePath = this.getSessionPath(session.sid);\n const data = JSON.stringify(session, null, 2);\n writeFileSync(filePath, data, { mode: 0o600 }); // Owner read/write only\n }\n\n /**\n * Load session from file\n */\n load(sid: string): AuthSession | null {\n const filePath = this.getSessionPath(sid);\n if (!existsSync(filePath)) {\n return null;\n }\n\n try {\n const data = readFileSync(filePath, 'utf8');\n return JSON.parse(data) as AuthSession;\n } catch (error) {\n throw new Error(\n `Failed to load session ${sid}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n /**\n * Delete session file\n */\n delete(sid: string): void {\n const filePath = this.getSessionPath(sid);\n if (existsSync(filePath)) {\n unlinkSync(filePath);\n }\n }\n\n /**\n * List all available SIDs\n */\n list(): string[] {\n if (!existsSync(this.storageDir)) {\n return [];\n }\n\n return readdirSync(this.storageDir)\n .filter((file) => file.endsWith('.json'))\n .map((file) => file.replace('.json', ''));\n }\n\n /**\n * Check if session exists\n */\n exists(sid: string): boolean {\n return existsSync(this.getSessionPath(sid));\n }\n\n // ===========================================================================\n // Default SID Management\n // ===========================================================================\n\n /**\n * Set default SID\n */\n setDefaultSid(sid: string): void {\n const config = this.loadConfig();\n config.defaultSid = sid;\n this.saveConfig(config);\n }\n\n /**\n * Get default SID\n */\n getDefaultSid(): string | null {\n const config = this.loadConfig();\n return config.defaultSid ?? null;\n }\n\n /**\n * Clear default SID\n */\n clearDefaultSid(): void {\n const config = this.loadConfig();\n delete config.defaultSid;\n this.saveConfig(config);\n }\n\n // ===========================================================================\n // Private Helpers\n // ===========================================================================\n\n private getSessionPath(sid: string): string {\n return join(this.storageDir, `${sid}.json`);\n }\n\n private ensureStorageDir(): void {\n if (!existsSync(this.storageDir)) {\n mkdirSync(this.storageDir, { recursive: true, mode: 0o700 }); // Owner only\n }\n }\n\n private loadConfig(): Config {\n if (!existsSync(this.configPath)) {\n return {};\n }\n try {\n return JSON.parse(readFileSync(this.configPath, 'utf8'));\n } catch {\n return {};\n }\n }\n\n private saveConfig(config: Config): void {\n mkdirSync(dirname(this.configPath), { recursive: true });\n writeFileSync(this.configPath, JSON.stringify(config, null, 2), {\n mode: 0o600,\n });\n }\n}\n","/**\n * Authentication Manager\n *\n * Single source of truth for authentication across all consumers:\n * - CLI\n * - MCP Server\n * - Other tools\n */\n\nimport type {\n AuthSession,\n CookieCredentials,\n AuthPlugin,\n AuthPluginOptions,\n AuthPluginResult,\n} from './types';\nimport { FileStorage } from './storage/file-storage';\n\n/**\n * Destination configuration (from adt.config.ts)\n */\nexport interface Destination {\n /** Plugin package to use (e.g., '@abapify/adt-puppeteer', '@abapify/adt-auth/basic') */\n type: string;\n /** Plugin-specific options */\n options: AuthPluginOptions;\n}\n\nexport class AuthManager {\n private storage: FileStorage;\n\n constructor(storage?: FileStorage) {\n this.storage = storage || new FileStorage();\n }\n\n // ===========================================================================\n // Login - Main entry point\n // ===========================================================================\n\n /**\n * Login using a destination configuration\n *\n * Dynamically loads the plugin specified in destination.type and authenticates.\n *\n * @param sid - System ID (e.g., 'TRL')\n * @param destination - Destination config with plugin type and options\n */\n async login(sid: string, destination: Destination): Promise<AuthSession> {\n // Dynamic import of the auth plugin\n const pluginModule = (await import(destination.type)) as {\n default?: AuthPlugin;\n };\n\n if (!pluginModule.default?.authenticate) {\n throw new Error(\n `Plugin ${destination.type} does not have a default export with authenticate method`,\n );\n }\n\n // Authenticate using the plugin\n const result = await pluginModule.default.authenticate(destination.options);\n\n // Build session based on result type\n const session = this.buildSession(sid, destination, result);\n\n // Save session\n this.saveSession(session);\n\n // Set as default if first system\n if (this.listSids().length === 1) {\n this.setDefaultSid(sid);\n }\n\n return session;\n }\n\n /**\n * Build session from plugin result\n */\n private buildSession(\n sid: string,\n destination: Destination,\n result: AuthPluginResult,\n ): AuthSession {\n if (result.method === 'cookie') {\n return {\n sid: sid.toUpperCase(),\n host: destination.options.url,\n client: destination.options.client,\n auth: {\n method: 'cookie',\n plugin: destination.type,\n pluginOptions: destination.options, // Store full plugin options for refresh fallback\n credentials: {\n cookies: result.credentials.cookies,\n expiresAt: result.credentials.expiresAt.toISOString(),\n },\n },\n };\n } else {\n return {\n sid: sid.toUpperCase(),\n host: destination.options.url,\n client: destination.options.client,\n auth: {\n method: 'basic',\n plugin: destination.type,\n pluginOptions: destination.options, // Store full plugin options for consistency\n credentials: result.credentials,\n },\n };\n }\n }\n\n // ===========================================================================\n // Session Management\n // ===========================================================================\n\n /**\n * Save a session (after successful authentication)\n */\n saveSession(session: AuthSession): void {\n this.storage.save(session);\n }\n\n /**\n * Get session by SID\n */\n getSession(sid?: string): AuthSession | null {\n const targetSid = sid || this.getDefaultSid();\n if (!targetSid) {\n return null;\n }\n return this.storage.load(targetSid);\n }\n\n /**\n * Delete session (logout)\n */\n deleteSession(sid: string): void {\n this.storage.delete(sid);\n }\n\n /**\n * List all available SIDs\n */\n listSids(): string[] {\n return this.storage.list();\n }\n\n // ===========================================================================\n // Default SID Management\n // ===========================================================================\n\n /**\n * Set default SID (persisted to config)\n */\n setDefaultSid(sid: string): void {\n this.storage.setDefaultSid(sid);\n }\n\n /**\n * Get default SID\n */\n getDefaultSid(): string | null {\n return this.storage.getDefaultSid() || (this.storage.list()[0] ?? null);\n }\n\n /**\n * Clear default SID\n */\n clearDefaultSid(): void {\n this.storage.clearDefaultSid();\n }\n\n // ===========================================================================\n // Credential Helpers\n // ===========================================================================\n\n /**\n * Check if session credentials are expired\n */\n isExpired(session: AuthSession): boolean {\n if (session.auth.method !== 'cookie') {\n return false; // Basic auth doesn't expire\n }\n const creds = session.auth.credentials as CookieCredentials;\n return new Date(creds.expiresAt) < new Date();\n }\n\n /**\n * Refresh credentials using the auth plugin\n *\n * @returns Updated session with new credentials, or null if refresh not supported/failed\n */\n async refreshCredentials(\n session: AuthSession,\n options?: { log?: (message: string) => void },\n ): Promise<AuthSession | null> {\n const log = options?.log ?? console.log;\n\n if (!session.auth.plugin) {\n return null; // No plugin = can't refresh\n }\n\n // Dynamic import of the auth plugin (expects default export)\n const pluginModule = (await import(session.auth.plugin)) as {\n default?: AuthPlugin;\n };\n\n if (!pluginModule.default) {\n throw new Error(\n `Plugin ${session.auth.plugin} does not have a default export`,\n );\n }\n\n const plugin = pluginModule.default;\n\n // Try plugin's refresh method first (preferred for session-based auth)\n if (plugin.refresh) {\n const sessionWithLog: AuthSession = {\n ...session,\n auth: {\n ...session.auth,\n pluginOptions: {\n url: session.host,\n client: session.client,\n ...session.auth.pluginOptions,\n log,\n },\n },\n };\n const result = await plugin.refresh(sessionWithLog);\n\n if (result) {\n // Refresh succeeded - build and save updated session\n // Preserve original plugin options (including userDataDir)\n const destination: Destination = {\n type: session.auth.plugin,\n options: session.auth.pluginOptions || {\n url: session.host,\n client: session.client,\n },\n };\n\n const updatedSession = this.buildSession(\n session.sid,\n destination,\n result,\n );\n this.saveSession(updatedSession);\n\n return updatedSession;\n }\n\n // Refresh failed - fall through to interactive authenticate\n log(\n '⚠️ Silent refresh failed - falling back to interactive authentication...',\n );\n }\n\n // Fallback: Call authenticate (full re-auth with browser interaction)\n // Use stored plugin options to preserve settings like userDataDir\n const baseOptions: AuthPluginOptions = session.auth.pluginOptions || {\n url: session.host,\n client: session.client,\n };\n const runtimeOptions = { ...baseOptions, log };\n\n const result = await plugin.authenticate(runtimeOptions);\n\n // Build destination for buildSession\n const destination: Destination = {\n type: session.auth.plugin,\n options: baseOptions,\n };\n\n // Build and save updated session\n const updatedSession = this.buildSession(session.sid, destination, result);\n this.saveSession(updatedSession);\n\n return updatedSession;\n }\n\n /**\n * Get cookie header for HTTP client (convenience method)\n */\n getCookieHeader(session: AuthSession): string | null {\n if (session.auth.method !== 'cookie') {\n return null;\n }\n const creds = session.auth.credentials as CookieCredentials;\n return creds.cookies;\n }\n\n /**\n * Get basic auth credentials (convenience method)\n */\n getBasicAuth(\n session: AuthSession,\n ): { username: string; password: string } | null {\n if (session.auth.method !== 'basic') {\n return null;\n }\n return session.auth.credentials as { username: string; password: string };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/storage/file-storage.ts","../src/auth-manager.ts"],"sourcesContent":["/**\n * File-based session storage\n *\n * Stores auth sessions in ~/.adt/sessions/\n * Stores config in ~/.adt/config.json\n */\n\nimport {\n readFileSync,\n writeFileSync,\n existsSync,\n mkdirSync,\n readdirSync,\n unlinkSync,\n} from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { homedir } from 'node:os';\nimport type { AuthSession } from '../types';\n\ninterface Config {\n defaultSid?: string;\n}\n\nexport class FileStorage {\n private readonly baseDir: string;\n private readonly storageDir: string;\n private readonly configPath: string;\n\n constructor(baseDir?: string) {\n this.baseDir = baseDir || join(homedir(), '.adt');\n this.storageDir = join(this.baseDir, 'sessions');\n this.configPath = join(this.baseDir, 'config.json');\n this.ensureStorageDir();\n }\n\n // ===========================================================================\n // Session Storage\n // ===========================================================================\n\n /**\n * Save session to file\n */\n save(session: AuthSession): void {\n const filePath = this.getSessionPath(session.sid);\n const data = JSON.stringify(session, null, 2);\n writeFileSync(filePath, data, { mode: 0o600 }); // Owner read/write only\n }\n\n /**\n * Load session from file\n */\n load(sid: string): AuthSession | null {\n const filePath = this.getSessionPath(sid);\n if (!existsSync(filePath)) {\n return null;\n }\n\n try {\n const data = readFileSync(filePath, 'utf8');\n return JSON.parse(data) as AuthSession;\n } catch (error) {\n throw new Error(\n `Failed to load session ${sid}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n /**\n * Delete session file\n */\n delete(sid: string): void {\n const filePath = this.getSessionPath(sid);\n if (existsSync(filePath)) {\n unlinkSync(filePath);\n }\n }\n\n /**\n * List all available SIDs\n */\n list(): string[] {\n if (!existsSync(this.storageDir)) {\n return [];\n }\n\n return readdirSync(this.storageDir)\n .filter((file) => file.endsWith('.json'))\n .map((file) => file.replace('.json', ''));\n }\n\n /**\n * Check if session exists\n */\n exists(sid: string): boolean {\n return existsSync(this.getSessionPath(sid));\n }\n\n // ===========================================================================\n // Default SID Management\n // ===========================================================================\n\n /**\n * Set default SID\n */\n setDefaultSid(sid: string): void {\n const config = this.loadConfig();\n config.defaultSid = sid;\n this.saveConfig(config);\n }\n\n /**\n * Get default SID\n */\n getDefaultSid(): string | null {\n const config = this.loadConfig();\n return config.defaultSid ?? null;\n }\n\n /**\n * Clear default SID\n */\n clearDefaultSid(): void {\n const config = this.loadConfig();\n delete config.defaultSid;\n this.saveConfig(config);\n }\n\n // ===========================================================================\n // Private Helpers\n // ===========================================================================\n\n private getSessionPath(sid: string): string {\n return join(this.storageDir, `${sid}.json`);\n }\n\n private ensureStorageDir(): void {\n if (!existsSync(this.storageDir)) {\n mkdirSync(this.storageDir, { recursive: true, mode: 0o700 }); // Owner only\n }\n }\n\n private loadConfig(): Config {\n if (!existsSync(this.configPath)) {\n return {};\n }\n try {\n return JSON.parse(readFileSync(this.configPath, 'utf8'));\n } catch {\n return {};\n }\n }\n\n private saveConfig(config: Config): void {\n mkdirSync(dirname(this.configPath), { recursive: true });\n writeFileSync(this.configPath, JSON.stringify(config, null, 2), {\n mode: 0o600,\n });\n }\n}\n","/**\n * Authentication Manager\n *\n * Single source of truth for authentication across all consumers:\n * - CLI\n * - MCP Server\n * - Other tools\n */\n\nimport type {\n AuthSession,\n CookieCredentials,\n AuthPlugin,\n AuthPluginOptions,\n AuthPluginResult,\n} from './types';\nimport { FileStorage } from './storage/file-storage';\n\n/**\n * Destination configuration (from adt.config.ts)\n */\nexport interface Destination {\n /** Plugin package to use (e.g., '@abapify/adt-puppeteer', '@abapify/adt-auth/basic') */\n type: string;\n /** Plugin-specific options */\n options: AuthPluginOptions;\n}\n\nexport class AuthManager {\n private readonly storage: FileStorage;\n\n constructor(storage?: FileStorage) {\n this.storage = storage || new FileStorage();\n }\n\n // ===========================================================================\n // Login - Main entry point\n // ===========================================================================\n\n /**\n * Login using a destination configuration\n *\n * Dynamically loads the plugin specified in destination.type and authenticates.\n *\n * @param sid - System ID (e.g., 'TRL')\n * @param destination - Destination config with plugin type and options\n */\n async login(sid: string, destination: Destination): Promise<AuthSession> {\n // Dynamic import of the auth plugin\n const pluginModule = (await import(destination.type)) as {\n default?: AuthPlugin;\n };\n\n if (!pluginModule.default?.authenticate) {\n throw new Error(\n `Plugin ${destination.type} does not have a default export with authenticate method`,\n );\n }\n\n // Authenticate using the plugin\n const result = await pluginModule.default.authenticate(destination.options);\n\n // Build session based on result type\n const session = this.buildSession(sid, destination, result);\n\n // Save session\n this.saveSession(session);\n\n // Set as default if first system\n if (this.listSids().length === 1) {\n this.setDefaultSid(sid);\n }\n\n return session;\n }\n\n /**\n * Build session from plugin result\n */\n private buildSession(\n sid: string,\n destination: Destination,\n result: AuthPluginResult,\n ): AuthSession {\n if (result.method === 'cookie') {\n return {\n sid: sid.toUpperCase(),\n host: destination.options.url,\n client: destination.options.client,\n auth: {\n method: 'cookie',\n plugin: destination.type,\n pluginOptions: destination.options, // Store full plugin options for refresh fallback\n credentials: {\n cookies: result.credentials.cookies,\n expiresAt: result.credentials.expiresAt.toISOString(),\n },\n },\n };\n } else {\n return {\n sid: sid.toUpperCase(),\n host: destination.options.url,\n client: destination.options.client,\n auth: {\n method: 'basic',\n plugin: destination.type,\n pluginOptions: destination.options, // Store full plugin options for consistency\n credentials: result.credentials,\n },\n };\n }\n }\n\n // ===========================================================================\n // Session Management\n // ===========================================================================\n\n /**\n * Save a session (after successful authentication)\n */\n saveSession(session: AuthSession): void {\n this.storage.save(session);\n }\n\n /**\n * Get session by SID\n */\n getSession(sid?: string): AuthSession | null {\n const targetSid = sid || this.getDefaultSid();\n if (!targetSid) {\n return null;\n }\n return this.storage.load(targetSid);\n }\n\n /**\n * Delete session (logout)\n */\n deleteSession(sid: string): void {\n this.storage.delete(sid);\n }\n\n /**\n * List all available SIDs\n */\n listSids(): string[] {\n return this.storage.list();\n }\n\n // ===========================================================================\n // Default SID Management\n // ===========================================================================\n\n /**\n * Set default SID (persisted to config)\n */\n setDefaultSid(sid: string): void {\n this.storage.setDefaultSid(sid);\n }\n\n /**\n * Get default SID\n */\n getDefaultSid(): string | null {\n return this.storage.getDefaultSid() || (this.storage.list()[0] ?? null);\n }\n\n /**\n * Clear default SID\n */\n clearDefaultSid(): void {\n this.storage.clearDefaultSid();\n }\n\n // ===========================================================================\n // Credential Helpers\n // ===========================================================================\n\n /**\n * Check if session credentials are expired\n */\n isExpired(session: AuthSession): boolean {\n if (session.auth.method !== 'cookie') {\n return false; // Basic auth doesn't expire\n }\n const creds = session.auth.credentials as CookieCredentials;\n return new Date(creds.expiresAt) < new Date();\n }\n\n /**\n * Refresh credentials using the auth plugin\n *\n * @returns Updated session with new credentials, or null if refresh not supported/failed\n */\n async refreshCredentials(\n session: AuthSession,\n options?: { log?: (message: string) => void },\n ): Promise<AuthSession | null> {\n const log = options?.log ?? console.log;\n\n if (!session.auth.plugin) {\n return null; // No plugin = can't refresh\n }\n\n // Dynamic import of the auth plugin (expects default export)\n const pluginModule = (await import(session.auth.plugin)) as {\n default?: AuthPlugin;\n };\n\n if (!pluginModule.default) {\n throw new Error(\n `Plugin ${session.auth.plugin} does not have a default export`,\n );\n }\n\n const plugin = pluginModule.default;\n\n // Try plugin's refresh method first (preferred for session-based auth)\n if (plugin.refresh) {\n const sessionWithLog: AuthSession = {\n ...session,\n auth: {\n ...session.auth,\n pluginOptions: {\n url: session.host,\n client: session.client,\n ...session.auth.pluginOptions,\n log,\n },\n },\n };\n const result = await plugin.refresh(sessionWithLog);\n\n if (result) {\n // Refresh succeeded - build and save updated session\n // Preserve original plugin options (including userDataDir)\n const destination: Destination = {\n type: session.auth.plugin,\n options: session.auth.pluginOptions || {\n url: session.host,\n client: session.client,\n },\n };\n\n const updatedSession = this.buildSession(\n session.sid,\n destination,\n result,\n );\n this.saveSession(updatedSession);\n\n return updatedSession;\n }\n\n // Refresh failed - fall through to interactive authenticate\n log(\n '⚠️ Silent refresh failed - falling back to interactive authentication...',\n );\n }\n\n // Fallback: Call authenticate (full re-auth with browser interaction)\n // Use stored plugin options to preserve settings like userDataDir\n const baseOptions: AuthPluginOptions = session.auth.pluginOptions || {\n url: session.host,\n client: session.client,\n };\n const runtimeOptions = { ...baseOptions, log };\n\n const result = await plugin.authenticate(runtimeOptions);\n\n // Build destination for buildSession\n const destination: Destination = {\n type: session.auth.plugin,\n options: baseOptions,\n };\n\n // Build and save updated session\n const updatedSession = this.buildSession(session.sid, destination, result);\n this.saveSession(updatedSession);\n\n return updatedSession;\n }\n\n /**\n * Get cookie header for HTTP client (convenience method)\n */\n getCookieHeader(session: AuthSession): string | null {\n if (session.auth.method !== 'cookie') {\n return null;\n }\n const creds = session.auth.credentials as CookieCredentials;\n return creds.cookies;\n }\n\n /**\n * Get basic auth credentials (convenience method)\n */\n getBasicAuth(\n session: AuthSession,\n ): { username: string; password: string } | null {\n if (session.auth.method !== 'basic') {\n return null;\n }\n return session.auth.credentials as { username: string; password: string };\n }\n}\n"],"mappings":"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"}
|
package/dist/plugins/basic.mjs
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Basic auth plugin - uses provided credentials
|
|
4
4
|
*/
|
|
5
|
-
const authPlugin = {
|
|
5
|
+
const authPlugin = { authenticate(options) {
|
|
6
6
|
const { username, password } = options;
|
|
7
|
-
if (!username || !password)
|
|
8
|
-
return {
|
|
7
|
+
if (!username || !password) return Promise.reject(/* @__PURE__ */ new Error("Basic auth requires username and password"));
|
|
8
|
+
return Promise.resolve({
|
|
9
9
|
method: "basic",
|
|
10
10
|
credentials: {
|
|
11
11
|
username,
|
|
12
12
|
password
|
|
13
13
|
}
|
|
14
|
-
};
|
|
14
|
+
});
|
|
15
15
|
} };
|
|
16
16
|
|
|
17
17
|
export { authPlugin as default };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"basic.mjs","names":[],"sources":["../../src/plugins/basic.ts"],"sourcesContent":["/**\n * Built-in Basic Auth Plugin\n *\n * Simple username/password authentication\n * No external dependencies\n */\n\nimport type { AuthPlugin, AuthPluginOptions, BasicAuthResult } from '../types';\n\nexport interface BasicAuthOptions extends AuthPluginOptions {\n username: string;\n password: string;\n}\n\n/**\n * Basic auth plugin - uses provided credentials\n */\nconst authPlugin: AuthPlugin = {\n
|
|
1
|
+
{"version":3,"file":"basic.mjs","names":[],"sources":["../../src/plugins/basic.ts"],"sourcesContent":["/**\n * Built-in Basic Auth Plugin\n *\n * Simple username/password authentication\n * No external dependencies\n */\n\nimport type { AuthPlugin, AuthPluginOptions, BasicAuthResult } from '../types';\n\nexport interface BasicAuthOptions extends AuthPluginOptions {\n username: string;\n password: string;\n}\n\n/**\n * Basic auth plugin - uses provided credentials\n */\nconst authPlugin: AuthPlugin = {\n authenticate(options: BasicAuthOptions): Promise<BasicAuthResult> {\n const { username, password } = options;\n\n if (!username || !password) {\n return Promise.reject(\n new Error('Basic auth requires username and password'),\n );\n }\n\n return Promise.resolve({\n method: 'basic',\n credentials: {\n username,\n password,\n },\n });\n },\n};\n\nexport default authPlugin;\n"],"mappings":";;;;AAiBA,MAAM,aAAyB,EAC7B,aAAa,SAAqD;CAChE,MAAM,EAAE,UAAU,aAAa;CAE/B,IAAI,CAAC,YAAY,CAAC,UAChB,OAAO,QAAQ,uBACb,IAAI,MAAM,2CAA2C,CACvD;CAGF,OAAO,QAAQ,QAAQ;EACrB,QAAQ;EACR,aAAa;GACX;GACA;EACF;CACF,CAAC;AACH,EACF"}
|
|
@@ -34,7 +34,10 @@ const MAX_PORT_RETRIES = 5;
|
|
|
34
34
|
* Tries platform-appropriate commands: pbcopy (macOS), clip.exe (WSL), xclip/xsel (Linux).
|
|
35
35
|
*/
|
|
36
36
|
async function copyToClipboard(text) {
|
|
37
|
-
|
|
37
|
+
let candidates;
|
|
38
|
+
if (process.platform === "darwin") candidates = [["pbcopy"]];
|
|
39
|
+
else if (process.platform === "win32") candidates = [["clip"]];
|
|
40
|
+
else candidates = [
|
|
38
41
|
["clip.exe"],
|
|
39
42
|
[
|
|
40
43
|
"xclip",
|
|
@@ -175,20 +178,38 @@ async function performPkceFlow(serviceKey, openBrowser, port, timeoutMs, redirec
|
|
|
175
178
|
});
|
|
176
179
|
});
|
|
177
180
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (openBrowser)
|
|
184
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the browser opener: use the provided callback, or dynamically
|
|
183
|
+
* import the `open` package. Throws when neither is available.
|
|
184
|
+
*/
|
|
185
|
+
async function resolveBrowserOpener(openBrowser) {
|
|
186
|
+
if (openBrowser) return openBrowser;
|
|
187
|
+
try {
|
|
185
188
|
const openFn = (await import("open")).default;
|
|
186
|
-
|
|
189
|
+
return async (url) => {
|
|
187
190
|
await openFn(url);
|
|
188
191
|
};
|
|
189
192
|
} catch {
|
|
190
193
|
throw new Error("No openBrowser callback provided and \"open\" package not available. Install \"open\" or pass an openBrowser function in plugin options.");
|
|
191
194
|
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Classify a failed PKCE attempt. EADDRINUSE is retried on the next port;
|
|
198
|
+
* when an explicit redirectUri is set, retrying on a different port is
|
|
199
|
+
* pointless because the OAuth callback will still go to the original port
|
|
200
|
+
* encoded in the redirect URI — so that combination is fatal. Everything
|
|
201
|
+
* else is rethrown unchanged.
|
|
202
|
+
*/
|
|
203
|
+
function classifyAttemptError(err, port, hasExplicitRedirect, log) {
|
|
204
|
+
if (!(err instanceof Error && "code" in err && err.code === "EADDRINUSE")) throw err;
|
|
205
|
+
if (hasExplicitRedirect) throw new Error(`Port ${port} is in use and redirectUri is explicitly set — cannot retry on a different port`, { cause: err });
|
|
206
|
+
log(`⚠️ Port ${port} in use, trying ${port + 1}...`);
|
|
207
|
+
}
|
|
208
|
+
const authPlugin = { async authenticate(options) {
|
|
209
|
+
const { serviceKey, openBrowser, callbackPort, timeoutMs, redirectUri } = options;
|
|
210
|
+
if (!serviceKey) throw new Error("service-key plugin requires a serviceKey option");
|
|
211
|
+
const parsed = ServiceKeyParser.parse(serviceKey);
|
|
212
|
+
const browserOpener = await resolveBrowserOpener(openBrowser);
|
|
192
213
|
const startPort = typeof callbackPort === "number" ? callbackPort : DEFAULT_PORT;
|
|
193
214
|
const timeout = typeof timeoutMs === "number" ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
194
215
|
const log = options.log ?? console.log;
|
|
@@ -216,17 +237,13 @@ const authPlugin = { async authenticate(options) {
|
|
|
216
237
|
}
|
|
217
238
|
};
|
|
218
239
|
} catch (err) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
log(`⚠️ Port ${port} in use, trying ${port + 1}...`);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
throw err;
|
|
240
|
+
classifyAttemptError(err, port, hasExplicitRedirect, log);
|
|
241
|
+
lastError = err;
|
|
242
|
+
continue;
|
|
226
243
|
}
|
|
227
244
|
}
|
|
228
245
|
throw new Error(`All ports ${startPort}-${startPort + MAX_PORT_RETRIES - 1} are in use. Free a port or set callbackPort in plugin options.`, { cause: lastError });
|
|
229
246
|
} };
|
|
230
247
|
|
|
231
248
|
export { authPlugin as t };
|
|
232
|
-
//# sourceMappingURL=service-key-
|
|
249
|
+
//# sourceMappingURL=service-key-DEYaMW4g.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-key-DEYaMW4g.mjs","names":["parseUrl"],"sources":["../src/utils/pkce.ts","../src/plugins/service-key.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\n\n/**\n * Generates a cryptographically secure code verifier for PKCE flow\n */\nexport function generateCodeVerifier(): string {\n return randomBytes(32).toString('base64url');\n}\n\n/**\n * Generates a code challenge from the verifier using SHA256\n */\nexport function generateCodeChallenge(verifier: string): string {\n return createHash('sha256').update(verifier).digest('base64url');\n}\n\n/**\n * Generates a random state parameter for OAuth flow\n */\nexport function generateState(): string {\n return randomBytes(16).toString('hex');\n}\n","import { createServer, type Server } from 'node:http';\nimport { spawn } from 'node:child_process';\nimport { parse as parseUrl } from 'node:url';\nimport type { AuthPlugin, AuthPluginOptions, CookieAuthResult } from '../types';\nimport {\n ServiceKeyParser,\n type BTPServiceKey,\n type ServiceKeyPluginOptions,\n} from '../types/service-key';\nimport {\n generateCodeVerifier,\n generateCodeChallenge,\n generateState,\n} from '../utils/pkce';\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_REDIRECT_PATH = '/callback';\nconst DEFAULT_TIMEOUT_MS = 300_000; // 5 minutes\nconst MAX_PORT_RETRIES = 5; // Try up to 5 consecutive ports on EADDRINUSE\n\n/**\n * Copy text to system clipboard (best-effort, never throws).\n * Tries platform-appropriate commands: pbcopy (macOS), clip.exe (WSL), xclip/xsel (Linux).\n */\nasync function copyToClipboard(text: string): Promise<void> {\n let candidates: string[][];\n if (process.platform === 'darwin') {\n candidates = [['pbcopy']];\n } else if (process.platform === 'win32') {\n candidates = [['clip']];\n } else {\n candidates = [\n ['clip.exe'],\n ['xclip', '-selection', 'clipboard'],\n ['xsel', '--clipboard'],\n ];\n }\n\n for (const [cmd, ...args] of candidates) {\n try {\n await new Promise<void>((resolve, reject) => {\n const child = spawn(cmd, args, {\n stdio: ['pipe', 'ignore', 'ignore'],\n });\n child.on('error', reject);\n child.on('close', (code) =>\n code === 0\n ? resolve()\n : reject(new Error(`${cmd} exited with code ${code}`)),\n );\n child.stdin.write(text);\n child.stdin.end();\n });\n return;\n } catch {\n continue;\n }\n }\n throw new Error('No clipboard utility available');\n}\n\ninterface OAuthTokenResponse {\n access_token: string;\n token_type: string;\n expires_in: number;\n scope?: string;\n refresh_token?: string;\n}\n\n/**\n * Exchange an authorization code for tokens using the PKCE flow.\n */\nasync function exchangeCodeForToken(\n serviceKey: BTPServiceKey,\n code: string,\n codeVerifier: string,\n redirectUri: string,\n): Promise<OAuthTokenResponse> {\n const { clientid, clientsecret, url: uaaUrl } = serviceKey.uaa;\n const credentials = Buffer.from(`${clientid}:${clientsecret}`).toString(\n 'base64',\n );\n\n const response = await fetch(`${uaaUrl}/oauth/token`, {\n method: 'POST',\n headers: {\n Authorization: `Basic ${credentials}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`Token exchange failed (${response.status}): ${text}`);\n }\n\n const data = (await response.json()) as OAuthTokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token response missing access_token');\n }\n\n return data;\n}\n\n/**\n * Run the OAuth PKCE authorization code flow:\n * 1. Start local callback server\n * 2. Build XSUAA authorize URL with PKCE\n * 3. Open browser for user login\n * 4. Receive auth code via redirect\n * 5. Exchange code for user token\n */\nasync function performPkceFlow(\n serviceKey: BTPServiceKey,\n openBrowser: (url: string) => Promise<void>,\n port: number,\n timeoutMs: number,\n redirectUri?: string,\n): Promise<OAuthTokenResponse> {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n const resolvedRedirectUri =\n redirectUri ?? `http://localhost:${port}${DEFAULT_REDIRECT_PATH}`;\n const callbackPath = (() => {\n try {\n const pathname = new URL(resolvedRedirectUri).pathname;\n return pathname && pathname.length > 0 ? pathname : '/';\n } catch {\n return DEFAULT_REDIRECT_PATH;\n }\n })();\n\n // Build XSUAA authorize URL\n const authUrl = new URL(`${serviceKey.uaa.url}/oauth/authorize`);\n authUrl.searchParams.set('client_id', serviceKey.uaa.clientid);\n authUrl.searchParams.set('response_type', 'code');\n authUrl.searchParams.set('redirect_uri', resolvedRedirectUri);\n authUrl.searchParams.set('code_challenge', codeChallenge);\n authUrl.searchParams.set('code_challenge_method', 'S256');\n authUrl.searchParams.set('state', state);\n authUrl.searchParams.set('scope', 'openid');\n\n return new Promise<OAuthTokenResponse>((resolve, reject) => {\n // eslint-disable-next-line prefer-const -- server must be declared before timeout but assigned after\n let server: Server;\n\n const timeout = setTimeout(() => {\n server?.close();\n reject(new Error('Authentication timed out'));\n }, timeoutMs);\n\n server = createServer(async (req, res) => {\n try {\n const url = parseUrl(req.url || '', true);\n\n if (url.pathname !== callbackPath) {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n return;\n }\n\n const {\n code,\n state: returnedState,\n error,\n error_description,\n } = url.query;\n\n if (error) {\n const errorSuffix = error_description\n ? ` - ${error_description}`\n : '';\n throw new Error(`OAuth error: ${error}${errorSuffix}`);\n }\n\n if (returnedState !== state) {\n throw new Error('State mismatch - possible CSRF attack');\n }\n\n if (!code) {\n throw new Error('No authorization code received');\n }\n\n const tokenData = await exchangeCodeForToken(\n serviceKey,\n code as string,\n codeVerifier,\n resolvedRedirectUri,\n );\n\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(\n '<html><body style=\"font-family:Arial;text-align:center;padding:50px\">' +\n '<h2>Authentication successful!</h2>' +\n '<p>You can close this window and return to the terminal.</p>' +\n '<script>setTimeout(()=>window.close(),2000)</script>' +\n '</body></html>',\n );\n\n clearTimeout(timeout);\n setTimeout(() => {\n server.close();\n resolve(tokenData);\n }, 500);\n } catch (err) {\n // Escape error message before embedding into HTML response to prevent\n // reflected XSS / XSS-through-exception. The error message may contain\n // data derived from the (attacker-controllable) OAuth callback URL\n // (e.g. error_description) and must never be treated as trusted.\n const rawMessage = err instanceof Error ? err.message : String(err);\n const escaped = rawMessage.replaceAll(/[&<>\"']/g, (c) => {\n switch (c) {\n case '&':\n return '&';\n case '<':\n return '<';\n case '>':\n return '>';\n case '\"':\n return '"';\n default:\n return ''';\n }\n });\n res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(\n '<html><body style=\"font-family:Arial;text-align:center;padding:50px\">' +\n `<h2>Authentication failed</h2><p>${escaped}</p>` +\n '</body></html>',\n );\n\n clearTimeout(timeout);\n server.close(() => reject(err));\n }\n });\n\n server.on('error', (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n\n server.listen(port, () => {\n openBrowser(authUrl.toString()).catch((err) => {\n clearTimeout(timeout);\n server.close();\n reject(\n new Error('Could not open browser for authentication', {\n cause: err,\n }),\n );\n });\n });\n });\n}\n\n/**\n * Resolve the browser opener: use the provided callback, or dynamically\n * import the `open` package. Throws when neither is available.\n */\nasync function resolveBrowserOpener(\n openBrowser: ((url: string) => Promise<void>) | undefined,\n): Promise<(url: string) => Promise<void>> {\n if (openBrowser) {\n return openBrowser;\n }\n try {\n const openModule = await import('open');\n const openFn = openModule.default;\n return async (url: string) => {\n await openFn(url);\n };\n } catch {\n throw new Error(\n 'No openBrowser callback provided and \"open\" package not available. ' +\n 'Install \"open\" or pass an openBrowser function in plugin options.',\n );\n }\n}\n\n/**\n * Classify a failed PKCE attempt. EADDRINUSE is retried on the next port;\n * when an explicit redirectUri is set, retrying on a different port is\n * pointless because the OAuth callback will still go to the original port\n * encoded in the redirect URI — so that combination is fatal. Everything\n * else is rethrown unchanged.\n */\nfunction classifyAttemptError(\n err: unknown,\n port: number,\n hasExplicitRedirect: boolean,\n log: (msg: string) => void,\n): void {\n const isAddrInUse =\n err instanceof Error &&\n 'code' in err &&\n (err as NodeJS.ErrnoException).code === 'EADDRINUSE';\n if (!isAddrInUse) {\n throw err; // Non-port error — rethrow immediately\n }\n if (hasExplicitRedirect) {\n throw new Error(\n `Port ${port} is in use and redirectUri is explicitly set — cannot retry on a different port`,\n { cause: err },\n );\n }\n log(`⚠️ Port ${port} in use, trying ${port + 1}...`);\n}\n\nconst authPlugin: AuthPlugin = {\n async authenticate(options: AuthPluginOptions): Promise<CookieAuthResult> {\n const { serviceKey, openBrowser, callbackPort, timeoutMs, redirectUri } =\n options as ServiceKeyPluginOptions;\n\n if (!serviceKey) {\n throw new Error('service-key plugin requires a serviceKey option');\n }\n\n const parsed = ServiceKeyParser.parse(serviceKey);\n\n // Resolve openBrowser: use provided callback, or dynamically import 'open'\n const browserOpener = await resolveBrowserOpener(openBrowser);\n\n const startPort =\n typeof callbackPort === 'number' ? callbackPort : DEFAULT_PORT;\n const timeout =\n typeof timeoutMs === 'number' ? timeoutMs : DEFAULT_TIMEOUT_MS;\n\n // Extract log from options (passed by AuthManager during refresh, or by CLI)\n const log = (options as { log?: (msg: string) => void }).log ?? console.log;\n\n // Wrap browser opener: always print the URL and try clipboard copy before opening\n const wrappedBrowserOpener = async (url: string) => {\n log(`🔗 Open this URL to authenticate:\\n ${url}`);\n\n try {\n await copyToClipboard(url);\n log('📋 URL copied to clipboard');\n } catch {\n // Clipboard not available — URL is already printed above\n }\n\n await browserOpener(url);\n };\n\n // Try ports starting from startPort, retrying on EADDRINUSE\n const hasExplicitRedirect = typeof redirectUri === 'string';\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {\n const port = startPort + attempt;\n // When redirectUri is explicitly set, use it as-is (don't rewrite for new port)\n const effectiveRedirectUri = hasExplicitRedirect\n ? redirectUri\n : undefined;\n\n try {\n const tokenData = await performPkceFlow(\n parsed,\n wrappedBrowserOpener,\n port,\n timeout,\n effectiveRedirectUri,\n );\n\n const expiresAt = new Date(Date.now() + tokenData.expires_in * 1000);\n\n return {\n method: 'cookie',\n credentials: {\n cookies: `Authorization: Bearer ${tokenData.access_token}`,\n expiresAt,\n },\n };\n } catch (err) {\n classifyAttemptError(err, port, hasExplicitRedirect, log);\n lastError = err;\n continue;\n }\n }\n\n throw new Error(\n `All ports ${startPort}-${startPort + MAX_PORT_RETRIES - 1} are in use. ` +\n `Free a port or set callbackPort in plugin options.`,\n { cause: lastError },\n );\n },\n};\n\nexport default authPlugin;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-key.d.mts","names":[],"sources":["../src/plugins/service-key.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"service-key.d.mts","names":[],"sources":["../src/plugins/service-key.ts"],"mappings":";cA6TM,YAAY"}
|
package/dist/service-key.mjs
CHANGED
package/dist/utils/env.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as resolveServiceKeyFromEnv, t as readServiceKey } from "../env-
|
|
1
|
+
import { n as resolveServiceKeyFromEnv, t as readServiceKey } from "../env-BQ8Uy_8m.mjs";
|
|
2
2
|
export { readServiceKey, resolveServiceKeyFromEnv };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.4.
|
|
6
|
+
"version": "0.4.36",
|
|
7
7
|
"description": "Authentication package for SAP ADT systems - supports multiple auth methods",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"types": "./dist/index.d.mts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@abapify/logger": "0.4.
|
|
24
|
+
"@abapify/logger": "0.4.36",
|
|
25
25
|
"open": "^11.0.4",
|
|
26
26
|
"proxy-agent": "^6.4.0"
|
|
27
27
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"env-B1kvodv4.d.mts","names":[],"sources":["../src/storage/file-storage.ts","../src/auth-manager.ts","../src/types/service-key.ts","../src/utils/env.ts"],"mappings":";cAuBa;mBACM;mBACA;mBACA;EAEL,YAAA;;;;EAcZ,KAAK,SAAS;;;;EASd,KAAK,cAAc;;;;EAoBnB,OAAO;;;;EAUP;;;;EAaA,OAAO;;;;EAWP,cAAc;;;;EASd;;;;EAQA;UAUQ;UAIA;UAMA;UAWA;;;;;UCpIO;;EAEf;;EAEA,SAAS;;cAGE;UACH;EAEI,YAAA,UAAU;;;;;;;;;EAgBhB,MAAM,aAAa,aAAa,cAAc,QAAQ;;;;UAgCpD;;;;EA0CR,YAAY,SAAS;;;;EAOrB,WAAW,eAAe;;;;EAW1B,cAAc;;;;EAOd;;;;EAWA,cAAc;;;;EAOd;;;;EAOA;;;;EAWA,UAAU,SAAS;;;;;;EAab,mBACJ,SAAS,aACT;IAAY,OAAO;MAClB,QAAQ;;;;EAyFX,gBAAgB,SAAS;;;;EAWzB,aACE,SAAS;IACN;IAAkB;;;UC1SR;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf,KAAK;EACL;EACA;EACA;EACA,WAAW;EACX,UAAU,eAAe;EACzB,SAAS;EACT;;UAYe;EACf;EACA;EACA;;EAEA,eAAe,gBAAgB;;EAE/B;;EAEA;;EAEA;GACC;;;;;;;;;;iBCrDa,eAAe,mBAAmB;;;;;;;;;;;iBA6BlC,4BAA4B"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"service-key-upymvzHr.mjs","names":["parseUrl"],"sources":["../src/utils/pkce.ts","../src/plugins/service-key.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\n\n/**\n * Generates a cryptographically secure code verifier for PKCE flow\n */\nexport function generateCodeVerifier(): string {\n return randomBytes(32).toString('base64url');\n}\n\n/**\n * Generates a code challenge from the verifier using SHA256\n */\nexport function generateCodeChallenge(verifier: string): string {\n return createHash('sha256').update(verifier).digest('base64url');\n}\n\n/**\n * Generates a random state parameter for OAuth flow\n */\nexport function generateState(): string {\n return randomBytes(16).toString('hex');\n}\n","import { createServer, type Server } from 'node:http';\nimport { spawn } from 'node:child_process';\nimport { parse as parseUrl } from 'node:url';\nimport type { AuthPlugin, AuthPluginOptions, CookieAuthResult } from '../types';\nimport {\n ServiceKeyParser,\n type BTPServiceKey,\n type ServiceKeyPluginOptions,\n} from '../types/service-key';\nimport {\n generateCodeVerifier,\n generateCodeChallenge,\n generateState,\n} from '../utils/pkce';\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_REDIRECT_PATH = '/callback';\nconst DEFAULT_TIMEOUT_MS = 300_000; // 5 minutes\nconst MAX_PORT_RETRIES = 5; // Try up to 5 consecutive ports on EADDRINUSE\n\n/**\n * Copy text to system clipboard (best-effort, never throws).\n * Tries platform-appropriate commands: pbcopy (macOS), clip.exe (WSL), xclip/xsel (Linux).\n */\nasync function copyToClipboard(text: string): Promise<void> {\n const candidates =\n process.platform === 'darwin'\n ? [['pbcopy']]\n : process.platform === 'win32'\n ? [['clip']]\n : [\n ['clip.exe'],\n ['xclip', '-selection', 'clipboard'],\n ['xsel', '--clipboard'],\n ];\n\n for (const [cmd, ...args] of candidates) {\n try {\n await new Promise<void>((resolve, reject) => {\n const child = spawn(cmd, args, {\n stdio: ['pipe', 'ignore', 'ignore'],\n });\n child.on('error', reject);\n child.on('close', (code) =>\n code === 0\n ? resolve()\n : reject(new Error(`${cmd} exited with code ${code}`)),\n );\n child.stdin.write(text);\n child.stdin.end();\n });\n return;\n } catch {\n continue;\n }\n }\n throw new Error('No clipboard utility available');\n}\n\ninterface OAuthTokenResponse {\n access_token: string;\n token_type: string;\n expires_in: number;\n scope?: string;\n refresh_token?: string;\n}\n\n/**\n * Exchange an authorization code for tokens using the PKCE flow.\n */\nasync function exchangeCodeForToken(\n serviceKey: BTPServiceKey,\n code: string,\n codeVerifier: string,\n redirectUri: string,\n): Promise<OAuthTokenResponse> {\n const { clientid, clientsecret, url: uaaUrl } = serviceKey.uaa;\n const credentials = Buffer.from(`${clientid}:${clientsecret}`).toString(\n 'base64',\n );\n\n const response = await fetch(`${uaaUrl}/oauth/token`, {\n method: 'POST',\n headers: {\n Authorization: `Basic ${credentials}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`Token exchange failed (${response.status}): ${text}`);\n }\n\n const data = (await response.json()) as OAuthTokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token response missing access_token');\n }\n\n return data;\n}\n\n/**\n * Run the OAuth PKCE authorization code flow:\n * 1. Start local callback server\n * 2. Build XSUAA authorize URL with PKCE\n * 3. Open browser for user login\n * 4. Receive auth code via redirect\n * 5. Exchange code for user token\n */\nasync function performPkceFlow(\n serviceKey: BTPServiceKey,\n openBrowser: (url: string) => Promise<void>,\n port: number,\n timeoutMs: number,\n redirectUri?: string,\n): Promise<OAuthTokenResponse> {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n const resolvedRedirectUri =\n redirectUri ?? `http://localhost:${port}${DEFAULT_REDIRECT_PATH}`;\n const callbackPath = (() => {\n try {\n const pathname = new URL(resolvedRedirectUri).pathname;\n return pathname && pathname.length > 0 ? pathname : '/';\n } catch {\n return DEFAULT_REDIRECT_PATH;\n }\n })();\n\n // Build XSUAA authorize URL\n const authUrl = new URL(`${serviceKey.uaa.url}/oauth/authorize`);\n authUrl.searchParams.set('client_id', serviceKey.uaa.clientid);\n authUrl.searchParams.set('response_type', 'code');\n authUrl.searchParams.set('redirect_uri', resolvedRedirectUri);\n authUrl.searchParams.set('code_challenge', codeChallenge);\n authUrl.searchParams.set('code_challenge_method', 'S256');\n authUrl.searchParams.set('state', state);\n authUrl.searchParams.set('scope', 'openid');\n\n return new Promise<OAuthTokenResponse>((resolve, reject) => {\n // eslint-disable-next-line prefer-const -- server must be declared before timeout but assigned after\n let server: Server;\n\n const timeout = setTimeout(() => {\n server?.close();\n reject(new Error('Authentication timed out'));\n }, timeoutMs);\n\n server = createServer(async (req, res) => {\n try {\n const url = parseUrl(req.url || '', true);\n\n if (url.pathname !== callbackPath) {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n return;\n }\n\n const {\n code,\n state: returnedState,\n error,\n error_description,\n } = url.query;\n\n if (error) {\n const errorSuffix = error_description\n ? ` - ${error_description}`\n : '';\n throw new Error(`OAuth error: ${error}${errorSuffix}`);\n }\n\n if (returnedState !== state) {\n throw new Error('State mismatch - possible CSRF attack');\n }\n\n if (!code) {\n throw new Error('No authorization code received');\n }\n\n const tokenData = await exchangeCodeForToken(\n serviceKey,\n code as string,\n codeVerifier,\n resolvedRedirectUri,\n );\n\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(\n '<html><body style=\"font-family:Arial;text-align:center;padding:50px\">' +\n '<h2>Authentication successful!</h2>' +\n '<p>You can close this window and return to the terminal.</p>' +\n '<script>setTimeout(()=>window.close(),2000)</script>' +\n '</body></html>',\n );\n\n clearTimeout(timeout);\n setTimeout(() => {\n server.close();\n resolve(tokenData);\n }, 500);\n } catch (err) {\n // Escape error message before embedding into HTML response to prevent\n // reflected XSS / XSS-through-exception. The error message may contain\n // data derived from the (attacker-controllable) OAuth callback URL\n // (e.g. error_description) and must never be treated as trusted.\n const rawMessage = err instanceof Error ? err.message : String(err);\n const escaped = rawMessage.replaceAll(/[&<>\"']/g, (c) => {\n switch (c) {\n case '&':\n return '&';\n case '<':\n return '<';\n case '>':\n return '>';\n case '\"':\n return '"';\n default:\n return ''';\n }\n });\n res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(\n '<html><body style=\"font-family:Arial;text-align:center;padding:50px\">' +\n `<h2>Authentication failed</h2><p>${escaped}</p>` +\n '</body></html>',\n );\n\n clearTimeout(timeout);\n server.close(() => reject(err));\n }\n });\n\n server.on('error', (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n\n server.listen(port, () => {\n openBrowser(authUrl.toString()).catch((err) => {\n clearTimeout(timeout);\n server.close();\n reject(\n new Error('Could not open browser for authentication', {\n cause: err,\n }),\n );\n });\n });\n });\n}\n\nconst authPlugin: AuthPlugin = {\n async authenticate(options: AuthPluginOptions): Promise<CookieAuthResult> {\n const { serviceKey, openBrowser, callbackPort, timeoutMs, redirectUri } =\n options as ServiceKeyPluginOptions;\n\n if (!serviceKey) {\n throw new Error('service-key plugin requires a serviceKey option');\n }\n\n const parsed = ServiceKeyParser.parse(serviceKey);\n\n // Resolve openBrowser: use provided callback, or dynamically import 'open'\n let browserOpener: (url: string) => Promise<void>;\n if (openBrowser) {\n browserOpener = openBrowser;\n } else {\n // Fallback: try dynamic import of 'open' package\n try {\n const openModule = await import('open');\n const openFn = openModule.default;\n browserOpener = async (url: string) => {\n await openFn(url);\n };\n } catch {\n throw new Error(\n 'No openBrowser callback provided and \"open\" package not available. ' +\n 'Install \"open\" or pass an openBrowser function in plugin options.',\n );\n }\n }\n\n const startPort =\n typeof callbackPort === 'number' ? callbackPort : DEFAULT_PORT;\n const timeout =\n typeof timeoutMs === 'number' ? timeoutMs : DEFAULT_TIMEOUT_MS;\n\n // Extract log from options (passed by AuthManager during refresh, or by CLI)\n const log = (options as { log?: (msg: string) => void }).log ?? console.log;\n\n // Wrap browser opener: always print the URL and try clipboard copy before opening\n const wrappedBrowserOpener = async (url: string) => {\n log(`🔗 Open this URL to authenticate:\\n ${url}`);\n\n try {\n await copyToClipboard(url);\n log('📋 URL copied to clipboard');\n } catch {\n // Clipboard not available — URL is already printed above\n }\n\n await browserOpener(url);\n };\n\n // Try ports starting from startPort, retrying on EADDRINUSE\n const hasExplicitRedirect = typeof redirectUri === 'string';\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {\n const port = startPort + attempt;\n // When redirectUri is explicitly set, use it as-is (don't rewrite for new port)\n const effectiveRedirectUri = hasExplicitRedirect\n ? redirectUri\n : undefined;\n\n try {\n const tokenData = await performPkceFlow(\n parsed,\n wrappedBrowserOpener,\n port,\n timeout,\n effectiveRedirectUri,\n );\n\n const expiresAt = new Date(Date.now() + tokenData.expires_in * 1000);\n\n return {\n method: 'cookie',\n credentials: {\n cookies: `Authorization: Bearer ${tokenData.access_token}`,\n expiresAt,\n },\n };\n } catch (err) {\n if (\n err instanceof Error &&\n 'code' in err &&\n (err as NodeJS.ErrnoException).code === 'EADDRINUSE'\n ) {\n // When an explicit redirectUri is set, retrying on a different port\n // is pointless because the OAuth callback will still go to the\n // original port encoded in the redirect URI.\n if (hasExplicitRedirect) {\n throw new Error(\n `Port ${port} is in use and redirectUri is explicitly set — cannot retry on a different port`,\n { cause: err },\n );\n }\n lastError = err;\n log(`⚠️ Port ${port} in use, trying ${port + 1}...`);\n continue;\n }\n throw err; // Non-port error — rethrow immediately\n }\n }\n\n throw new Error(\n `All ports ${startPort}-${startPort + MAX_PORT_RETRIES - 1} are in use. ` +\n `Free a port or set callbackPort in plugin options.`,\n { cause: lastError },\n );\n },\n};\n\nexport default authPlugin;\n"],"mappings":"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"}
|