@abapify/adt-auth 0.4.0 → 0.4.3
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 +10 -10
- package/dist/basic.d.mts +13 -0
- package/dist/basic.d.mts.map +1 -0
- package/dist/basic.mjs +3 -0
- package/dist/env-Co2ABx6K.d.mts +202 -0
- package/dist/env-Co2ABx6K.d.mts.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +291 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugins/basic.mjs +19 -0
- package/dist/plugins/basic.mjs.map +1 -0
- package/dist/plugins/service-key.mjs +3 -0
- package/dist/service-key-2AKKsYxf.mjs +234 -0
- package/dist/service-key-2AKKsYxf.mjs.map +1 -0
- package/dist/service-key-eMRV2aSt.mjs +20 -0
- package/dist/service-key-eMRV2aSt.mjs.map +1 -0
- package/dist/service-key.d.mts +6 -0
- package/dist/service-key.d.mts.map +1 -0
- package/dist/service-key.mjs +3 -0
- package/dist/types-C6-wI7SQ.d.mts +107 -0
- package/dist/types-C6-wI7SQ.d.mts.map +1 -0
- package/dist/utils/env.d.mts +2 -0
- package/dist/utils/env.mjs +48 -0
- package/dist/utils/env.mjs.map +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -39,13 +39,13 @@ const session = await authManager.login(
|
|
|
39
39
|
password: 'pass',
|
|
40
40
|
client: '100',
|
|
41
41
|
},
|
|
42
|
-
'
|
|
42
|
+
'TRL',
|
|
43
43
|
);
|
|
44
44
|
|
|
45
45
|
console.log('✅ Logged in!', session.sid);
|
|
46
46
|
|
|
47
47
|
// Later: get credentials
|
|
48
|
-
const credentials = authManager.getCredentials('
|
|
48
|
+
const credentials = authManager.getCredentials('TRL');
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
### SAP Secure Login Client (SLC)
|
|
@@ -67,7 +67,7 @@ const session = await authManager.login(
|
|
|
67
67
|
},
|
|
68
68
|
client: '200',
|
|
69
69
|
},
|
|
70
|
-
'
|
|
70
|
+
'TRL',
|
|
71
71
|
);
|
|
72
72
|
|
|
73
73
|
console.log('✅ Logged in via SLC!');
|
|
@@ -77,8 +77,8 @@ console.log('✅ Logged in via SLC!');
|
|
|
77
77
|
|
|
78
78
|
```typescript
|
|
79
79
|
// Login to multiple systems
|
|
80
|
-
await authManager.login(config1, '
|
|
81
|
-
await authManager.login(config2, '
|
|
80
|
+
await authManager.login(config1, 'TRL');
|
|
81
|
+
await authManager.login(config2, 'QAS');
|
|
82
82
|
await authManager.login(config3, 'PRD');
|
|
83
83
|
|
|
84
84
|
// List all systems
|
|
@@ -86,7 +86,7 @@ const sids = authManager.listSids();
|
|
|
86
86
|
console.log('Available systems:', sids);
|
|
87
87
|
|
|
88
88
|
// Set default
|
|
89
|
-
authManager.setDefaultSid('
|
|
89
|
+
authManager.setDefaultSid('TRL');
|
|
90
90
|
|
|
91
91
|
// Get default system credentials
|
|
92
92
|
const creds = authManager.getCredentials(); // Uses default SID
|
|
@@ -163,8 +163,8 @@ Sessions are stored in `~/.adt/sessions/` with file permissions `0600` (owner re
|
|
|
163
163
|
```
|
|
164
164
|
~/.adt/
|
|
165
165
|
└── sessions/
|
|
166
|
-
├──
|
|
167
|
-
├──
|
|
166
|
+
├── TRL.json
|
|
167
|
+
├── QAS.json
|
|
168
168
|
└── PRD.json
|
|
169
169
|
```
|
|
170
170
|
|
|
@@ -172,7 +172,7 @@ Sessions are stored in `~/.adt/sessions/` with file permissions `0600` (owner re
|
|
|
172
172
|
|
|
173
173
|
```json
|
|
174
174
|
{
|
|
175
|
-
"sid": "
|
|
175
|
+
"sid": "TRL",
|
|
176
176
|
"credentials": {
|
|
177
177
|
"method": "basic",
|
|
178
178
|
"baseUrl": "https://sap.example.com",
|
|
@@ -194,7 +194,7 @@ import { AuthManager, BasicAuthMethod } from '@abapify/adt-auth';
|
|
|
194
194
|
// Get credentials
|
|
195
195
|
const authManager = new AuthManager();
|
|
196
196
|
authManager.registerMethod(new BasicAuthMethod());
|
|
197
|
-
const credentials = authManager.getCredentials('
|
|
197
|
+
const credentials = authManager.getCredentials('TRL');
|
|
198
198
|
|
|
199
199
|
if (!credentials) {
|
|
200
200
|
throw new Error('Not authenticated. Run: adt login');
|
package/dist/basic.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { i as AuthPluginOptions, r as AuthPlugin } from "./types-C6-wI7SQ.mjs";
|
|
2
|
+
//#region src/plugins/basic.d.ts
|
|
3
|
+
interface BasicAuthOptions extends AuthPluginOptions {
|
|
4
|
+
username: string;
|
|
5
|
+
password: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Basic auth plugin - uses provided credentials
|
|
9
|
+
*/
|
|
10
|
+
declare const authPlugin: AuthPlugin;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { BasicAuthOptions, authPlugin as default };
|
|
13
|
+
//# sourceMappingURL=basic.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"basic.d.mts","names":[],"sources":["../src/plugins/basic.ts"],"mappings":";;UASiB,yBAAyB;EACxC;EACA;;;;;cAMI,YAAY"}
|
package/dist/basic.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { i as AuthPluginOptions, o as AuthSession } from "./types-C6-wI7SQ.mjs";
|
|
2
|
+
//#region src/storage/file-storage.d.ts
|
|
3
|
+
declare class FileStorage {
|
|
4
|
+
private readonly baseDir;
|
|
5
|
+
private readonly storageDir;
|
|
6
|
+
private readonly configPath;
|
|
7
|
+
constructor(baseDir?: string);
|
|
8
|
+
/**
|
|
9
|
+
* Save session to file
|
|
10
|
+
*/
|
|
11
|
+
save(session: AuthSession): void;
|
|
12
|
+
/**
|
|
13
|
+
* Load session from file
|
|
14
|
+
*/
|
|
15
|
+
load(sid: string): AuthSession | null;
|
|
16
|
+
/**
|
|
17
|
+
* Delete session file
|
|
18
|
+
*/
|
|
19
|
+
delete(sid: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* List all available SIDs
|
|
22
|
+
*/
|
|
23
|
+
list(): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Check if session exists
|
|
26
|
+
*/
|
|
27
|
+
exists(sid: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Set default SID
|
|
30
|
+
*/
|
|
31
|
+
setDefaultSid(sid: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Get default SID
|
|
34
|
+
*/
|
|
35
|
+
getDefaultSid(): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Clear default SID
|
|
38
|
+
*/
|
|
39
|
+
clearDefaultSid(): void;
|
|
40
|
+
private getSessionPath;
|
|
41
|
+
private ensureStorageDir;
|
|
42
|
+
private loadConfig;
|
|
43
|
+
private saveConfig;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/auth-manager.d.ts
|
|
47
|
+
/**
|
|
48
|
+
* Destination configuration (from adt.config.ts)
|
|
49
|
+
*/
|
|
50
|
+
interface Destination {
|
|
51
|
+
/** Plugin package to use (e.g., '@abapify/adt-puppeteer', '@abapify/adt-auth/basic') */
|
|
52
|
+
type: string;
|
|
53
|
+
/** Plugin-specific options */
|
|
54
|
+
options: AuthPluginOptions;
|
|
55
|
+
}
|
|
56
|
+
declare class AuthManager {
|
|
57
|
+
private storage;
|
|
58
|
+
constructor(storage?: FileStorage);
|
|
59
|
+
/**
|
|
60
|
+
* Login using a destination configuration
|
|
61
|
+
*
|
|
62
|
+
* Dynamically loads the plugin specified in destination.type and authenticates.
|
|
63
|
+
*
|
|
64
|
+
* @param sid - System ID (e.g., 'TRL')
|
|
65
|
+
* @param destination - Destination config with plugin type and options
|
|
66
|
+
*/
|
|
67
|
+
login(sid: string, destination: Destination): Promise<AuthSession>;
|
|
68
|
+
/**
|
|
69
|
+
* Build session from plugin result
|
|
70
|
+
*/
|
|
71
|
+
private buildSession;
|
|
72
|
+
/**
|
|
73
|
+
* Save a session (after successful authentication)
|
|
74
|
+
*/
|
|
75
|
+
saveSession(session: AuthSession): void;
|
|
76
|
+
/**
|
|
77
|
+
* Get session by SID
|
|
78
|
+
*/
|
|
79
|
+
getSession(sid?: string): AuthSession | null;
|
|
80
|
+
/**
|
|
81
|
+
* Delete session (logout)
|
|
82
|
+
*/
|
|
83
|
+
deleteSession(sid: string): void;
|
|
84
|
+
/**
|
|
85
|
+
* List all available SIDs
|
|
86
|
+
*/
|
|
87
|
+
listSids(): string[];
|
|
88
|
+
/**
|
|
89
|
+
* Set default SID (persisted to config)
|
|
90
|
+
*/
|
|
91
|
+
setDefaultSid(sid: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* Get default SID
|
|
94
|
+
*/
|
|
95
|
+
getDefaultSid(): string | null;
|
|
96
|
+
/**
|
|
97
|
+
* Clear default SID
|
|
98
|
+
*/
|
|
99
|
+
clearDefaultSid(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Check if session credentials are expired
|
|
102
|
+
*/
|
|
103
|
+
isExpired(session: AuthSession): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Refresh credentials using the auth plugin
|
|
106
|
+
*
|
|
107
|
+
* @returns Updated session with new credentials, or null if refresh not supported/failed
|
|
108
|
+
*/
|
|
109
|
+
refreshCredentials(session: AuthSession, options?: {
|
|
110
|
+
log?: (message: string) => void;
|
|
111
|
+
}): Promise<AuthSession | null>;
|
|
112
|
+
/**
|
|
113
|
+
* Get cookie header for HTTP client (convenience method)
|
|
114
|
+
*/
|
|
115
|
+
getCookieHeader(session: AuthSession): string | null;
|
|
116
|
+
/**
|
|
117
|
+
* Get basic auth credentials (convenience method)
|
|
118
|
+
*/
|
|
119
|
+
getBasicAuth(session: AuthSession): {
|
|
120
|
+
username: string;
|
|
121
|
+
password: string;
|
|
122
|
+
} | null;
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/types/service-key.d.ts
|
|
126
|
+
interface UAACredentials {
|
|
127
|
+
tenantmode: string;
|
|
128
|
+
sburl: string;
|
|
129
|
+
subaccountid: string;
|
|
130
|
+
'credential-type': string;
|
|
131
|
+
clientid: string;
|
|
132
|
+
xsappname: string;
|
|
133
|
+
clientsecret: string;
|
|
134
|
+
serviceInstanceId: string;
|
|
135
|
+
url: string;
|
|
136
|
+
uaadomain: string;
|
|
137
|
+
verificationkey: string;
|
|
138
|
+
apiurl: string;
|
|
139
|
+
identityzone: string;
|
|
140
|
+
identityzoneid: string;
|
|
141
|
+
tenantid: string;
|
|
142
|
+
zoneid: string;
|
|
143
|
+
}
|
|
144
|
+
interface Catalog {
|
|
145
|
+
path: string;
|
|
146
|
+
type: string;
|
|
147
|
+
}
|
|
148
|
+
interface Binding {
|
|
149
|
+
env: string;
|
|
150
|
+
version: string;
|
|
151
|
+
type: string;
|
|
152
|
+
id: string;
|
|
153
|
+
}
|
|
154
|
+
interface BTPServiceKey {
|
|
155
|
+
uaa: UAACredentials;
|
|
156
|
+
url: string;
|
|
157
|
+
'sap.cloud.service': string;
|
|
158
|
+
systemid: string;
|
|
159
|
+
endpoints: Record<string, string>;
|
|
160
|
+
catalogs: Record<string, Catalog>;
|
|
161
|
+
binding: Binding;
|
|
162
|
+
preserve_host_header: boolean;
|
|
163
|
+
}
|
|
164
|
+
interface ServiceKeyPluginOptions {
|
|
165
|
+
url: string;
|
|
166
|
+
client?: string;
|
|
167
|
+
serviceKey: string | object;
|
|
168
|
+
/** Callback to open a URL in the user's browser. Falls back to dynamic import of 'open'. */
|
|
169
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
170
|
+
/** Port for the local OAuth callback server (default: 3000) */
|
|
171
|
+
callbackPort?: number;
|
|
172
|
+
/** Timeout for the auth flow in ms (default: 300000 = 5 min) */
|
|
173
|
+
timeoutMs?: number;
|
|
174
|
+
/** Full redirect URI for the OAuth callback (e.g. in tunnelled/forwarded environments). Defaults to http://localhost:<port>/callback */
|
|
175
|
+
redirectUri?: string;
|
|
176
|
+
[key: string]: unknown;
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/utils/env.d.ts
|
|
180
|
+
/**
|
|
181
|
+
* Read a BTP service key from a file path.
|
|
182
|
+
*
|
|
183
|
+
* @param filePath - Path to a JSON file containing the BTP service key
|
|
184
|
+
* @returns Parsed and validated `BTPServiceKey`
|
|
185
|
+
* @throws Error if the input looks like raw JSON (a file path is required)
|
|
186
|
+
* @throws Error if the file does not exist
|
|
187
|
+
*/
|
|
188
|
+
declare function readServiceKey(filePath: string): BTPServiceKey;
|
|
189
|
+
/**
|
|
190
|
+
* Resolves a service key destination from the `ADT_SERVICE_KEY` environment
|
|
191
|
+
* variable.
|
|
192
|
+
*
|
|
193
|
+
* The value can be a path to a JSON file or a raw JSON string (for backward
|
|
194
|
+
* compatibility with CI pipelines that store the key as a secret JSON value).
|
|
195
|
+
*
|
|
196
|
+
* @returns A `Destination` object for the service-key plugin, or `null` if
|
|
197
|
+
* `ADT_SERVICE_KEY` is not set.
|
|
198
|
+
*/
|
|
199
|
+
declare function resolveServiceKeyFromEnv(): Destination | null;
|
|
200
|
+
//#endregion
|
|
201
|
+
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 };
|
|
202
|
+
//# sourceMappingURL=env-Co2ABx6K.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-Co2ABx6K.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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
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-C6-wI7SQ.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-Co2ABx6K.mjs";
|
|
3
|
+
import authPlugin from "./basic.mjs";
|
|
4
|
+
import authPlugin$1 from "./service-key.mjs";
|
|
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
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import authPlugin from "./plugins/basic.mjs";
|
|
2
|
+
import { t as authPlugin$1 } from "./service-key-2AKKsYxf.mjs";
|
|
3
|
+
import { readServiceKey, resolveServiceKeyFromEnv } from "./utils/env.mjs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
|
|
8
|
+
//#region src/storage/file-storage.ts
|
|
9
|
+
/**
|
|
10
|
+
* File-based session storage
|
|
11
|
+
*
|
|
12
|
+
* Stores auth sessions in ~/.adt/sessions/
|
|
13
|
+
* Stores config in ~/.adt/config.json
|
|
14
|
+
*/
|
|
15
|
+
var FileStorage = class {
|
|
16
|
+
baseDir;
|
|
17
|
+
storageDir;
|
|
18
|
+
configPath;
|
|
19
|
+
constructor(baseDir) {
|
|
20
|
+
this.baseDir = baseDir || join(homedir(), ".adt");
|
|
21
|
+
this.storageDir = join(this.baseDir, "sessions");
|
|
22
|
+
this.configPath = join(this.baseDir, "config.json");
|
|
23
|
+
this.ensureStorageDir();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Save session to file
|
|
27
|
+
*/
|
|
28
|
+
save(session) {
|
|
29
|
+
const filePath = this.getSessionPath(session.sid);
|
|
30
|
+
const data = JSON.stringify(session, null, 2);
|
|
31
|
+
writeFileSync(filePath, data, { mode: 384 });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load session from file
|
|
35
|
+
*/
|
|
36
|
+
load(sid) {
|
|
37
|
+
const filePath = this.getSessionPath(sid);
|
|
38
|
+
if (!existsSync(filePath)) return null;
|
|
39
|
+
try {
|
|
40
|
+
const data = readFileSync(filePath, "utf8");
|
|
41
|
+
return JSON.parse(data);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Failed to load session ${sid}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Delete session file
|
|
48
|
+
*/
|
|
49
|
+
delete(sid) {
|
|
50
|
+
const filePath = this.getSessionPath(sid);
|
|
51
|
+
if (existsSync(filePath)) unlinkSync(filePath);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* List all available SIDs
|
|
55
|
+
*/
|
|
56
|
+
list() {
|
|
57
|
+
if (!existsSync(this.storageDir)) return [];
|
|
58
|
+
return readdirSync(this.storageDir).filter((file) => file.endsWith(".json")).map((file) => file.replace(".json", ""));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if session exists
|
|
62
|
+
*/
|
|
63
|
+
exists(sid) {
|
|
64
|
+
return existsSync(this.getSessionPath(sid));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Set default SID
|
|
68
|
+
*/
|
|
69
|
+
setDefaultSid(sid) {
|
|
70
|
+
const config = this.loadConfig();
|
|
71
|
+
config.defaultSid = sid;
|
|
72
|
+
this.saveConfig(config);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get default SID
|
|
76
|
+
*/
|
|
77
|
+
getDefaultSid() {
|
|
78
|
+
return this.loadConfig().defaultSid ?? null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Clear default SID
|
|
82
|
+
*/
|
|
83
|
+
clearDefaultSid() {
|
|
84
|
+
const config = this.loadConfig();
|
|
85
|
+
delete config.defaultSid;
|
|
86
|
+
this.saveConfig(config);
|
|
87
|
+
}
|
|
88
|
+
getSessionPath(sid) {
|
|
89
|
+
return join(this.storageDir, `${sid}.json`);
|
|
90
|
+
}
|
|
91
|
+
ensureStorageDir() {
|
|
92
|
+
if (!existsSync(this.storageDir)) mkdirSync(this.storageDir, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
mode: 448
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
loadConfig() {
|
|
98
|
+
if (!existsSync(this.configPath)) return {};
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(readFileSync(this.configPath, "utf8"));
|
|
101
|
+
} catch {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
saveConfig(config) {
|
|
106
|
+
mkdirSync(dirname(this.configPath), { recursive: true });
|
|
107
|
+
writeFileSync(this.configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/auth-manager.ts
|
|
113
|
+
var AuthManager = class {
|
|
114
|
+
storage;
|
|
115
|
+
constructor(storage) {
|
|
116
|
+
this.storage = storage || new FileStorage();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Login using a destination configuration
|
|
120
|
+
*
|
|
121
|
+
* Dynamically loads the plugin specified in destination.type and authenticates.
|
|
122
|
+
*
|
|
123
|
+
* @param sid - System ID (e.g., 'TRL')
|
|
124
|
+
* @param destination - Destination config with plugin type and options
|
|
125
|
+
*/
|
|
126
|
+
async login(sid, destination) {
|
|
127
|
+
const pluginModule = await import(destination.type);
|
|
128
|
+
if (!pluginModule.default?.authenticate) throw new Error(`Plugin ${destination.type} does not have a default export with authenticate method`);
|
|
129
|
+
const result = await pluginModule.default.authenticate(destination.options);
|
|
130
|
+
const session = this.buildSession(sid, destination, result);
|
|
131
|
+
this.saveSession(session);
|
|
132
|
+
if (this.listSids().length === 1) this.setDefaultSid(sid);
|
|
133
|
+
return session;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build session from plugin result
|
|
137
|
+
*/
|
|
138
|
+
buildSession(sid, destination, result) {
|
|
139
|
+
if (result.method === "cookie") return {
|
|
140
|
+
sid: sid.toUpperCase(),
|
|
141
|
+
host: destination.options.url,
|
|
142
|
+
client: destination.options.client,
|
|
143
|
+
auth: {
|
|
144
|
+
method: "cookie",
|
|
145
|
+
plugin: destination.type,
|
|
146
|
+
pluginOptions: destination.options,
|
|
147
|
+
credentials: {
|
|
148
|
+
cookies: result.credentials.cookies,
|
|
149
|
+
expiresAt: result.credentials.expiresAt.toISOString()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
else return {
|
|
154
|
+
sid: sid.toUpperCase(),
|
|
155
|
+
host: destination.options.url,
|
|
156
|
+
client: destination.options.client,
|
|
157
|
+
auth: {
|
|
158
|
+
method: "basic",
|
|
159
|
+
plugin: destination.type,
|
|
160
|
+
pluginOptions: destination.options,
|
|
161
|
+
credentials: result.credentials
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Save a session (after successful authentication)
|
|
167
|
+
*/
|
|
168
|
+
saveSession(session) {
|
|
169
|
+
this.storage.save(session);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get session by SID
|
|
173
|
+
*/
|
|
174
|
+
getSession(sid) {
|
|
175
|
+
const targetSid = sid || this.getDefaultSid();
|
|
176
|
+
if (!targetSid) return null;
|
|
177
|
+
return this.storage.load(targetSid);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Delete session (logout)
|
|
181
|
+
*/
|
|
182
|
+
deleteSession(sid) {
|
|
183
|
+
this.storage.delete(sid);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* List all available SIDs
|
|
187
|
+
*/
|
|
188
|
+
listSids() {
|
|
189
|
+
return this.storage.list();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Set default SID (persisted to config)
|
|
193
|
+
*/
|
|
194
|
+
setDefaultSid(sid) {
|
|
195
|
+
this.storage.setDefaultSid(sid);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get default SID
|
|
199
|
+
*/
|
|
200
|
+
getDefaultSid() {
|
|
201
|
+
return this.storage.getDefaultSid() || (this.storage.list()[0] ?? null);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Clear default SID
|
|
205
|
+
*/
|
|
206
|
+
clearDefaultSid() {
|
|
207
|
+
this.storage.clearDefaultSid();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Check if session credentials are expired
|
|
211
|
+
*/
|
|
212
|
+
isExpired(session) {
|
|
213
|
+
if (session.auth.method !== "cookie") return false;
|
|
214
|
+
const creds = session.auth.credentials;
|
|
215
|
+
return new Date(creds.expiresAt) < /* @__PURE__ */ new Date();
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Refresh credentials using the auth plugin
|
|
219
|
+
*
|
|
220
|
+
* @returns Updated session with new credentials, or null if refresh not supported/failed
|
|
221
|
+
*/
|
|
222
|
+
async refreshCredentials(session, options) {
|
|
223
|
+
const log = options?.log ?? console.log;
|
|
224
|
+
if (!session.auth.plugin) return null;
|
|
225
|
+
const pluginModule = await import(session.auth.plugin);
|
|
226
|
+
if (!pluginModule.default) throw new Error(`Plugin ${session.auth.plugin} does not have a default export`);
|
|
227
|
+
const plugin = pluginModule.default;
|
|
228
|
+
if (plugin.refresh) {
|
|
229
|
+
const sessionWithLog = {
|
|
230
|
+
...session,
|
|
231
|
+
auth: {
|
|
232
|
+
...session.auth,
|
|
233
|
+
pluginOptions: {
|
|
234
|
+
url: session.host,
|
|
235
|
+
client: session.client,
|
|
236
|
+
...session.auth.pluginOptions,
|
|
237
|
+
log
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const result = await plugin.refresh(sessionWithLog);
|
|
242
|
+
if (result) {
|
|
243
|
+
const destination = {
|
|
244
|
+
type: session.auth.plugin,
|
|
245
|
+
options: session.auth.pluginOptions || {
|
|
246
|
+
url: session.host,
|
|
247
|
+
client: session.client
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const updatedSession = this.buildSession(session.sid, destination, result);
|
|
251
|
+
this.saveSession(updatedSession);
|
|
252
|
+
return updatedSession;
|
|
253
|
+
}
|
|
254
|
+
log("⚠️ Silent refresh failed - falling back to interactive authentication...");
|
|
255
|
+
}
|
|
256
|
+
const baseOptions = session.auth.pluginOptions || {
|
|
257
|
+
url: session.host,
|
|
258
|
+
client: session.client
|
|
259
|
+
};
|
|
260
|
+
const runtimeOptions = {
|
|
261
|
+
...baseOptions,
|
|
262
|
+
log
|
|
263
|
+
};
|
|
264
|
+
const result = await plugin.authenticate(runtimeOptions);
|
|
265
|
+
const destination = {
|
|
266
|
+
type: session.auth.plugin,
|
|
267
|
+
options: baseOptions
|
|
268
|
+
};
|
|
269
|
+
const updatedSession = this.buildSession(session.sid, destination, result);
|
|
270
|
+
this.saveSession(updatedSession);
|
|
271
|
+
return updatedSession;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get cookie header for HTTP client (convenience method)
|
|
275
|
+
*/
|
|
276
|
+
getCookieHeader(session) {
|
|
277
|
+
if (session.auth.method !== "cookie") return null;
|
|
278
|
+
return session.auth.credentials.cookies;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get basic auth credentials (convenience method)
|
|
282
|
+
*/
|
|
283
|
+
getBasicAuth(session) {
|
|
284
|
+
if (session.auth.method !== "basic") return null;
|
|
285
|
+
return session.auth.credentials;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
export { AuthManager, FileStorage, authPlugin as basicAuthPlugin, readServiceKey, resolveServiceKeyFromEnv, authPlugin$1 as serviceKeyAuthPlugin };
|
|
291
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/plugins/basic.ts
|
|
2
|
+
/**
|
|
3
|
+
* Basic auth plugin - uses provided credentials
|
|
4
|
+
*/
|
|
5
|
+
const authPlugin = { async authenticate(options) {
|
|
6
|
+
const { username, password } = options;
|
|
7
|
+
if (!username || !password) throw new Error("Basic auth requires username and password");
|
|
8
|
+
return {
|
|
9
|
+
method: "basic",
|
|
10
|
+
credentials: {
|
|
11
|
+
username,
|
|
12
|
+
password
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
} };
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { authPlugin as default };
|
|
19
|
+
//# sourceMappingURL=basic.mjs.map
|
|
@@ -0,0 +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 async authenticate(options: BasicAuthOptions): Promise<BasicAuthResult> {\n const { username, password } = options;\n\n if (!username || !password) {\n throw new Error('Basic auth requires username and password');\n }\n\n return {\n method: 'basic',\n credentials: {\n username,\n password,\n },\n };\n },\n};\n\nexport default authPlugin;\n"],"mappings":";;;;AAiBA,MAAM,aAAyB,EAC7B,MAAM,aAAa,SAAqD;CACtE,MAAM,EAAE,UAAU,aAAa;CAE/B,IAAI,CAAC,YAAY,CAAC,UAChB,MAAM,IAAI,MAAM,2CAA2C;CAG7D,OAAO;EACL,QAAQ;EACR,aAAa;GACX;GACA;EACF;CACF;AACF,EACF"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { t as ServiceKeyParser } from "./service-key-eMRV2aSt.mjs";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { parse } from "node:url";
|
|
5
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
//#region src/utils/pkce.ts
|
|
8
|
+
/**
|
|
9
|
+
* Generates a cryptographically secure code verifier for PKCE flow
|
|
10
|
+
*/
|
|
11
|
+
function generateCodeVerifier() {
|
|
12
|
+
return randomBytes(32).toString("base64url");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Generates a code challenge from the verifier using SHA256
|
|
16
|
+
*/
|
|
17
|
+
function generateCodeChallenge(verifier) {
|
|
18
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generates a random state parameter for OAuth flow
|
|
22
|
+
*/
|
|
23
|
+
function generateState() {
|
|
24
|
+
return randomBytes(16).toString("hex");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/plugins/service-key.ts
|
|
29
|
+
const DEFAULT_PORT = 3e3;
|
|
30
|
+
const DEFAULT_REDIRECT_PATH = "/callback";
|
|
31
|
+
const DEFAULT_TIMEOUT_MS = 3e5;
|
|
32
|
+
const MAX_PORT_RETRIES = 5;
|
|
33
|
+
/**
|
|
34
|
+
* Copy text to system clipboard (best-effort, never throws).
|
|
35
|
+
* Tries platform-appropriate commands: pbcopy (macOS), clip.exe (WSL), xclip/xsel (Linux).
|
|
36
|
+
*/
|
|
37
|
+
async function copyToClipboard(text) {
|
|
38
|
+
const candidates = process.platform === "darwin" ? [["pbcopy"]] : process.platform === "win32" ? [["clip"]] : [
|
|
39
|
+
["clip.exe"],
|
|
40
|
+
[
|
|
41
|
+
"xclip",
|
|
42
|
+
"-selection",
|
|
43
|
+
"clipboard"
|
|
44
|
+
],
|
|
45
|
+
["xsel", "--clipboard"]
|
|
46
|
+
];
|
|
47
|
+
for (const [cmd, ...args] of candidates) try {
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn(cmd, args, { stdio: [
|
|
50
|
+
"pipe",
|
|
51
|
+
"ignore",
|
|
52
|
+
"ignore"
|
|
53
|
+
] });
|
|
54
|
+
child.on("error", reject);
|
|
55
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`${cmd} exited with code ${code}`)));
|
|
56
|
+
child.stdin.write(text);
|
|
57
|
+
child.stdin.end();
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
} catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
throw new Error("No clipboard utility available");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Exchange an authorization code for tokens using the PKCE flow.
|
|
67
|
+
*/
|
|
68
|
+
async function exchangeCodeForToken(serviceKey, code, codeVerifier, redirectUri) {
|
|
69
|
+
const { clientid, clientsecret, url: uaaUrl } = serviceKey.uaa;
|
|
70
|
+
const credentials = Buffer.from(`${clientid}:${clientsecret}`).toString("base64");
|
|
71
|
+
const response = await fetch(`${uaaUrl}/oauth/token`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Basic ${credentials}`,
|
|
75
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
76
|
+
},
|
|
77
|
+
body: new URLSearchParams({
|
|
78
|
+
grant_type: "authorization_code",
|
|
79
|
+
code,
|
|
80
|
+
redirect_uri: redirectUri,
|
|
81
|
+
code_verifier: codeVerifier
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
throw new Error(`Token exchange failed (${response.status}): ${text}`);
|
|
87
|
+
}
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
if (!data.access_token) throw new Error("Token response missing access_token");
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Run the OAuth PKCE authorization code flow:
|
|
94
|
+
* 1. Start local callback server
|
|
95
|
+
* 2. Build XSUAA authorize URL with PKCE
|
|
96
|
+
* 3. Open browser for user login
|
|
97
|
+
* 4. Receive auth code via redirect
|
|
98
|
+
* 5. Exchange code for user token
|
|
99
|
+
*/
|
|
100
|
+
async function performPkceFlow(serviceKey, openBrowser, port, timeoutMs, redirectUri) {
|
|
101
|
+
const codeVerifier = generateCodeVerifier();
|
|
102
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
103
|
+
const state = generateState();
|
|
104
|
+
const resolvedRedirectUri = redirectUri ?? `http://localhost:${port}${DEFAULT_REDIRECT_PATH}`;
|
|
105
|
+
const callbackPath = (() => {
|
|
106
|
+
try {
|
|
107
|
+
const pathname = new URL(resolvedRedirectUri).pathname;
|
|
108
|
+
return pathname && pathname.length > 0 ? pathname : "/";
|
|
109
|
+
} catch {
|
|
110
|
+
return DEFAULT_REDIRECT_PATH;
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
const authUrl = new URL(`${serviceKey.uaa.url}/oauth/authorize`);
|
|
114
|
+
authUrl.searchParams.set("client_id", serviceKey.uaa.clientid);
|
|
115
|
+
authUrl.searchParams.set("response_type", "code");
|
|
116
|
+
authUrl.searchParams.set("redirect_uri", resolvedRedirectUri);
|
|
117
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
118
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
119
|
+
authUrl.searchParams.set("state", state);
|
|
120
|
+
authUrl.searchParams.set("scope", "openid");
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
let server;
|
|
123
|
+
const timeout = setTimeout(() => {
|
|
124
|
+
server?.close();
|
|
125
|
+
reject(/* @__PURE__ */ new Error("Authentication timed out"));
|
|
126
|
+
}, timeoutMs);
|
|
127
|
+
server = createServer(async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const url = parse(req.url || "", true);
|
|
130
|
+
if (url.pathname !== callbackPath) {
|
|
131
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
132
|
+
res.end("Not Found");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const { code, state: returnedState, error, error_description } = url.query;
|
|
136
|
+
if (error) {
|
|
137
|
+
const errorSuffix = error_description ? ` - ${error_description}` : "";
|
|
138
|
+
throw new Error(`OAuth error: ${error}${errorSuffix}`);
|
|
139
|
+
}
|
|
140
|
+
if (returnedState !== state) throw new Error("State mismatch - possible CSRF attack");
|
|
141
|
+
if (!code) throw new Error("No authorization code received");
|
|
142
|
+
const tokenData = await exchangeCodeForToken(serviceKey, code, codeVerifier, resolvedRedirectUri);
|
|
143
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
144
|
+
res.end("<html><body style=\"font-family:Arial;text-align:center;padding:50px\"><h2>Authentication successful!</h2><p>You can close this window and return to the terminal.</p><script>setTimeout(()=>window.close(),2000)<\/script></body></html>");
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
server.close();
|
|
148
|
+
resolve(tokenData);
|
|
149
|
+
}, 500);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const escaped = (err instanceof Error ? err.message : String(err)).replaceAll(/[&<>"']/g, (c) => {
|
|
152
|
+
switch (c) {
|
|
153
|
+
case "&": return "&";
|
|
154
|
+
case "<": return "<";
|
|
155
|
+
case ">": return ">";
|
|
156
|
+
case "\"": return """;
|
|
157
|
+
default: return "'";
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
161
|
+
res.end(`<html><body style="font-family:Arial;text-align:center;padding:50px"><h2>Authentication failed</h2><p>${escaped}</p></body></html>`);
|
|
162
|
+
clearTimeout(timeout);
|
|
163
|
+
server.close(() => reject(err));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
server.on("error", (err) => {
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
reject(err);
|
|
169
|
+
});
|
|
170
|
+
server.listen(port, () => {
|
|
171
|
+
openBrowser(authUrl.toString()).catch((err) => {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
server.close();
|
|
174
|
+
reject(new Error("Could not open browser for authentication", { cause: err }));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const authPlugin = { async authenticate(options) {
|
|
180
|
+
const { serviceKey, openBrowser, callbackPort, timeoutMs, redirectUri } = options;
|
|
181
|
+
if (!serviceKey) throw new Error("service-key plugin requires a serviceKey option");
|
|
182
|
+
const parsed = ServiceKeyParser.parse(serviceKey);
|
|
183
|
+
let browserOpener;
|
|
184
|
+
if (openBrowser) browserOpener = openBrowser;
|
|
185
|
+
else try {
|
|
186
|
+
const openFn = (await import("open")).default;
|
|
187
|
+
browserOpener = async (url) => {
|
|
188
|
+
await openFn(url);
|
|
189
|
+
};
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error("No openBrowser callback provided and \"open\" package not available. Install \"open\" or pass an openBrowser function in plugin options.");
|
|
192
|
+
}
|
|
193
|
+
const startPort = typeof callbackPort === "number" ? callbackPort : DEFAULT_PORT;
|
|
194
|
+
const timeout = typeof timeoutMs === "number" ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
195
|
+
const log = options.log ?? console.log;
|
|
196
|
+
const wrappedBrowserOpener = async (url) => {
|
|
197
|
+
log(`🔗 Open this URL to authenticate:\n ${url}`);
|
|
198
|
+
try {
|
|
199
|
+
await copyToClipboard(url);
|
|
200
|
+
log("📋 URL copied to clipboard");
|
|
201
|
+
} catch {}
|
|
202
|
+
await browserOpener(url);
|
|
203
|
+
};
|
|
204
|
+
const hasExplicitRedirect = typeof redirectUri === "string";
|
|
205
|
+
let lastError;
|
|
206
|
+
for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
|
|
207
|
+
const port = startPort + attempt;
|
|
208
|
+
const effectiveRedirectUri = hasExplicitRedirect ? redirectUri : void 0;
|
|
209
|
+
try {
|
|
210
|
+
const tokenData = await performPkceFlow(parsed, wrappedBrowserOpener, port, timeout, effectiveRedirectUri);
|
|
211
|
+
const expiresAt = new Date(Date.now() + tokenData.expires_in * 1e3);
|
|
212
|
+
return {
|
|
213
|
+
method: "cookie",
|
|
214
|
+
credentials: {
|
|
215
|
+
cookies: `Authorization: Bearer ${tokenData.access_token}`,
|
|
216
|
+
expiresAt
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (err instanceof Error && "code" in err && err.code === "EADDRINUSE") {
|
|
221
|
+
if (hasExplicitRedirect) throw new Error(`Port ${port} is in use and redirectUri is explicitly set — cannot retry on a different port`, { cause: err });
|
|
222
|
+
lastError = err;
|
|
223
|
+
log(`⚠️ Port ${port} in use, trying ${port + 1}...`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
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 });
|
|
230
|
+
} };
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
export { authPlugin as t };
|
|
234
|
+
//# sourceMappingURL=service-key-2AKKsYxf.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-key-2AKKsYxf.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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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/types/service-key.ts
|
|
2
|
+
var ServiceKeyParser = class {
|
|
3
|
+
static parse(serviceKeyJson) {
|
|
4
|
+
let parsed;
|
|
5
|
+
if (typeof serviceKeyJson === "string") try {
|
|
6
|
+
parsed = JSON.parse(serviceKeyJson);
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new Error("Invalid JSON format in service key", { cause: error });
|
|
9
|
+
}
|
|
10
|
+
else parsed = serviceKeyJson;
|
|
11
|
+
if (!parsed || typeof parsed !== "object") throw new Error("Invalid service key format");
|
|
12
|
+
const serviceKey = parsed;
|
|
13
|
+
if (!serviceKey["uaa"] || !serviceKey["url"] || !serviceKey["systemid"]) throw new Error("Missing required fields in service key");
|
|
14
|
+
return serviceKey;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { ServiceKeyParser as t };
|
|
20
|
+
//# sourceMappingURL=service-key-eMRV2aSt.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-key-eMRV2aSt.mjs","names":[],"sources":["../src/types/service-key.ts"],"sourcesContent":["// BTP Service Key types\n\nexport interface UAACredentials {\n tenantmode: string;\n sburl: string;\n subaccountid: string;\n 'credential-type': string;\n clientid: string;\n xsappname: string;\n clientsecret: string;\n serviceInstanceId: string;\n url: string;\n uaadomain: string;\n verificationkey: string;\n apiurl: string;\n identityzone: string;\n identityzoneid: string;\n tenantid: string;\n zoneid: string;\n}\n\nexport interface Catalog {\n path: string;\n type: string;\n}\n\nexport interface Binding {\n env: string;\n version: string;\n type: string;\n id: string;\n}\n\nexport interface BTPServiceKey {\n uaa: UAACredentials;\n url: string;\n 'sap.cloud.service': string;\n systemid: string;\n endpoints: Record<string, string>;\n catalogs: Record<string, Catalog>;\n binding: Binding;\n preserve_host_header: boolean;\n}\n\nexport interface OAuthToken {\n access_token: string;\n token_type: string;\n expires_in: number;\n scope: string;\n refresh_token?: string;\n expires_at: Date;\n}\n\nexport interface ServiceKeyPluginOptions {\n url: string;\n client?: string;\n serviceKey: string | object;\n /** Callback to open a URL in the user's browser. Falls back to dynamic import of 'open'. */\n openBrowser?: (url: string) => Promise<void>;\n /** Port for the local OAuth callback server (default: 3000) */\n callbackPort?: number;\n /** Timeout for the auth flow in ms (default: 300000 = 5 min) */\n timeoutMs?: number;\n /** Full redirect URI for the OAuth callback (e.g. in tunnelled/forwarded environments). Defaults to http://localhost:<port>/callback */\n redirectUri?: string;\n [key: string]: unknown;\n}\n\nexport class ServiceKeyParser {\n static parse(serviceKeyJson: string | object): BTPServiceKey {\n let parsed: unknown;\n\n if (typeof serviceKeyJson === 'string') {\n try {\n parsed = JSON.parse(serviceKeyJson);\n } catch (error) {\n throw new Error('Invalid JSON format in service key', { cause: error });\n }\n } else {\n parsed = serviceKeyJson;\n }\n\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('Invalid service key format');\n }\n\n const serviceKey = parsed as Record<string, unknown>;\n\n if (!serviceKey['uaa'] || !serviceKey['url'] || !serviceKey['systemid']) {\n throw new Error('Missing required fields in service key');\n }\n\n return serviceKey as unknown as BTPServiceKey;\n }\n}\n"],"mappings":";AAoEA,IAAa,mBAAb,MAA8B;CAC5B,OAAO,MAAM,gBAAgD;EAC3D,IAAI;EAEJ,IAAI,OAAO,mBAAmB,UAC5B,IAAI;GACF,SAAS,KAAK,MAAM,cAAc;EACpC,SAAS,OAAO;GACd,MAAM,IAAI,MAAM,sCAAsC,EAAE,OAAO,MAAM,CAAC;EACxE;OAEA,SAAS;EAGX,IAAI,CAAC,UAAU,OAAO,WAAW,UAC/B,MAAM,IAAI,MAAM,4BAA4B;EAG9C,MAAM,aAAa;EAEnB,IAAI,CAAC,WAAW,UAAU,CAAC,WAAW,UAAU,CAAC,WAAW,aAC1D,MAAM,IAAI,MAAM,wCAAwC;EAG1D,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-key.d.mts","names":[],"sources":["../src/plugins/service-key.ts"],"mappings":";;cAsQM,YAAY"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Core types for ADT authentication
|
|
4
|
+
*
|
|
5
|
+
* Session format is the single source of truth for all consumers
|
|
6
|
+
* (CLI, MCP server, other tools)
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Authentication method types
|
|
10
|
+
* - basic: username/password
|
|
11
|
+
* - cookie: session cookies (from browser SSO, etc.)
|
|
12
|
+
*/
|
|
13
|
+
type AuthMethod = 'basic' | 'cookie';
|
|
14
|
+
/**
|
|
15
|
+
* Basic authentication credentials
|
|
16
|
+
*/
|
|
17
|
+
interface BasicCredentials {
|
|
18
|
+
username: string;
|
|
19
|
+
password: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Cookie-based authentication credentials
|
|
23
|
+
*/
|
|
24
|
+
interface CookieCredentials {
|
|
25
|
+
cookies: string;
|
|
26
|
+
expiresAt: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Union of all credential types
|
|
30
|
+
*/
|
|
31
|
+
type Credentials = BasicCredentials | CookieCredentials;
|
|
32
|
+
/**
|
|
33
|
+
* Auth configuration in session file
|
|
34
|
+
*
|
|
35
|
+
* - `method`: What the HTTP client uses ("cookie" | "basic")
|
|
36
|
+
* - `plugin`: Package to dynamically import for refresh (optional)
|
|
37
|
+
* - `pluginOptions`: Original plugin options (for refresh fallback)
|
|
38
|
+
* - `credentials`: Method-specific credentials
|
|
39
|
+
*/
|
|
40
|
+
interface AuthConfig {
|
|
41
|
+
method: AuthMethod;
|
|
42
|
+
plugin?: string;
|
|
43
|
+
pluginOptions?: AuthPluginOptions;
|
|
44
|
+
credentials: Credentials;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Authentication session stored in ~/.adt/sessions/<SID>.json
|
|
48
|
+
*
|
|
49
|
+
* This is the canonical format used by all consumers:
|
|
50
|
+
* - CLI
|
|
51
|
+
* - MCP Server
|
|
52
|
+
* - Other tools
|
|
53
|
+
*/
|
|
54
|
+
interface AuthSession {
|
|
55
|
+
sid: string;
|
|
56
|
+
host: string;
|
|
57
|
+
client?: string;
|
|
58
|
+
auth: AuthConfig;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Auth plugin interface for credential providers
|
|
62
|
+
*
|
|
63
|
+
* Plugins are dynamically imported when:
|
|
64
|
+
* - Initial authentication (login)
|
|
65
|
+
* - Credential refresh (when expired)
|
|
66
|
+
*/
|
|
67
|
+
interface AuthPlugin {
|
|
68
|
+
/** Authenticate and return credentials */
|
|
69
|
+
authenticate(options: AuthPluginOptions): Promise<AuthPluginResult>;
|
|
70
|
+
/** Optional: Refresh existing credentials (for session-based auth) */
|
|
71
|
+
refresh?(session: AuthSession): Promise<AuthPluginResult | null>;
|
|
72
|
+
}
|
|
73
|
+
interface AuthPluginOptions {
|
|
74
|
+
url: string;
|
|
75
|
+
client?: string;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
/** Cookie-based auth result (from browser SSO plugins) */
|
|
79
|
+
interface CookieAuthResult {
|
|
80
|
+
method: 'cookie';
|
|
81
|
+
credentials: {
|
|
82
|
+
cookies: string;
|
|
83
|
+
expiresAt: Date;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Basic auth result (from basic auth plugin) */
|
|
87
|
+
interface BasicAuthResult {
|
|
88
|
+
method: 'basic';
|
|
89
|
+
credentials: {
|
|
90
|
+
username: string;
|
|
91
|
+
password: string;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Union of all plugin results */
|
|
95
|
+
type AuthPluginResult = CookieAuthResult | BasicAuthResult;
|
|
96
|
+
/**
|
|
97
|
+
* Connection test result
|
|
98
|
+
*/
|
|
99
|
+
interface ConnectionTestResult {
|
|
100
|
+
success: boolean;
|
|
101
|
+
error?: string;
|
|
102
|
+
statusCode?: number;
|
|
103
|
+
responseTime?: number;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { AuthPluginResult as a, BasicCredentials as c, CookieCredentials as d, Credentials as f, AuthPluginOptions as i, ConnectionTestResult as l, AuthMethod as n, AuthSession as o, AuthPlugin as r, BasicAuthResult as s, AuthConfig as t, CookieAuthResult as u };
|
|
107
|
+
//# sourceMappingURL=types-C6-wI7SQ.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-C6-wI7SQ.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;KAgBY;;;;UAKK;EACf;EACA;;;;;UAMe;EACf;EACA;;;;;KAMU,cAAc,mBAAmB;;;;;;;;;UAc5B;EACf,QAAQ;EACR;EACA,gBAAgB;EAChB,aAAa;;;;;;;;;;UAeE;EACf;EACA;EACA;EACA,MAAM;;;;;;;;;UAcS;;EAEf,aAAa,SAAS,oBAAoB,QAAQ;;EAGlD,SAAS,SAAS,cAAc,QAAQ;;UAGzB;EACf;EACA;GACC;;;UAIc;EACf;EACA;IACE;IACA,WAAW;;;;UAKE;EACf;EACA;IACE;IACA;;;;KAKQ,mBAAmB,mBAAmB;;;;UASjC;EACf;EACA;EACA;EACA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { t as ServiceKeyParser } from "../service-key-eMRV2aSt.mjs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/env.ts
|
|
5
|
+
/**
|
|
6
|
+
* Read a BTP service key from a file path.
|
|
7
|
+
*
|
|
8
|
+
* @param filePath - Path to a JSON file containing the BTP service key
|
|
9
|
+
* @returns Parsed and validated `BTPServiceKey`
|
|
10
|
+
* @throws Error if the input looks like raw JSON (a file path is required)
|
|
11
|
+
* @throws Error if the file does not exist
|
|
12
|
+
*/
|
|
13
|
+
function readServiceKey(filePath) {
|
|
14
|
+
const trimmed = filePath.trim();
|
|
15
|
+
if (trimmed.startsWith("{")) throw new Error("Expected a path to a JSON file, but received a raw JSON string.\nWrite the service key JSON to a file and pass the file path instead.\nExample: /path/to/service-key.json");
|
|
16
|
+
if (!existsSync(trimmed)) throw new Error(`Service key file not found: ${trimmed}`);
|
|
17
|
+
const raw = readFileSync(trimmed, "utf-8");
|
|
18
|
+
return ServiceKeyParser.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a service key destination from the `ADT_SERVICE_KEY` environment
|
|
22
|
+
* variable.
|
|
23
|
+
*
|
|
24
|
+
* The value can be a path to a JSON file or a raw JSON string (for backward
|
|
25
|
+
* compatibility with CI pipelines that store the key as a secret JSON value).
|
|
26
|
+
*
|
|
27
|
+
* @returns A `Destination` object for the service-key plugin, or `null` if
|
|
28
|
+
* `ADT_SERVICE_KEY` is not set.
|
|
29
|
+
*/
|
|
30
|
+
function resolveServiceKeyFromEnv() {
|
|
31
|
+
const raw = process.env["ADT_SERVICE_KEY"];
|
|
32
|
+
if (!raw) return null;
|
|
33
|
+
const trimmed = raw.trim();
|
|
34
|
+
let serviceKey;
|
|
35
|
+
if (trimmed.startsWith("{")) serviceKey = ServiceKeyParser.parse(trimmed);
|
|
36
|
+
else serviceKey = readServiceKey(trimmed);
|
|
37
|
+
return {
|
|
38
|
+
type: "@abapify/adt-auth/plugins/service-key",
|
|
39
|
+
options: {
|
|
40
|
+
url: serviceKey.url,
|
|
41
|
+
serviceKey
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { readServiceKey, resolveServiceKeyFromEnv };
|
|
48
|
+
//# sourceMappingURL=env.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.mjs","names":[],"sources":["../../src/utils/env.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'node:fs';\nimport type { Destination } from '../auth-manager';\nimport { ServiceKeyParser, type BTPServiceKey } from '../types/service-key';\n\n/**\n * Read a BTP service key from a file path.\n *\n * @param filePath - Path to a JSON file containing the BTP service key\n * @returns Parsed and validated `BTPServiceKey`\n * @throws Error if the input looks like raw JSON (a file path is required)\n * @throws Error if the file does not exist\n */\nexport function readServiceKey(filePath: string): BTPServiceKey {\n const trimmed = filePath.trim();\n\n if (trimmed.startsWith('{')) {\n throw new Error(\n 'Expected a path to a JSON file, but received a raw JSON string.\\n' +\n 'Write the service key JSON to a file and pass the file path instead.\\n' +\n 'Example: /path/to/service-key.json',\n );\n }\n\n if (!existsSync(trimmed)) {\n throw new Error(`Service key file not found: ${trimmed}`);\n }\n\n const raw = readFileSync(trimmed, 'utf-8');\n return ServiceKeyParser.parse(raw);\n}\n\n/**\n * Resolves a service key destination from the `ADT_SERVICE_KEY` environment\n * variable.\n *\n * The value can be a path to a JSON file or a raw JSON string (for backward\n * compatibility with CI pipelines that store the key as a secret JSON value).\n *\n * @returns A `Destination` object for the service-key plugin, or `null` if\n * `ADT_SERVICE_KEY` is not set.\n */\nexport function resolveServiceKeyFromEnv(): Destination | null {\n const raw = process.env['ADT_SERVICE_KEY'];\n\n if (!raw) {\n return null;\n }\n\n const trimmed = raw.trim();\n\n // Accept both a raw JSON string and a file path so that CI environments that\n // store the key as a secret JSON value continue to work without changes.\n let serviceKey: BTPServiceKey;\n if (trimmed.startsWith('{')) {\n serviceKey = ServiceKeyParser.parse(trimmed);\n } else {\n serviceKey = readServiceKey(trimmed);\n }\n\n return {\n type: '@abapify/adt-auth/plugins/service-key',\n options: {\n url: serviceKey.url,\n serviceKey,\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,eAAe,UAAiC;CAC9D,MAAM,UAAU,SAAS,KAAK;CAE9B,IAAI,QAAQ,WAAW,GAAG,GACxB,MAAM,IAAI,MACR,2KAGF;CAGF,IAAI,CAAC,WAAW,OAAO,GACrB,MAAM,IAAI,MAAM,+BAA+B,SAAS;CAG1D,MAAM,MAAM,aAAa,SAAS,OAAO;CACzC,OAAO,iBAAiB,MAAM,GAAG;AACnC;;;;;;;;;;;AAYA,SAAgB,2BAA+C;CAC7D,MAAM,MAAM,QAAQ,IAAI;CAExB,IAAI,CAAC,KACH,OAAO;CAGT,MAAM,UAAU,IAAI,KAAK;CAIzB,IAAI;CACJ,IAAI,QAAQ,WAAW,GAAG,GACxB,aAAa,iBAAiB,MAAM,OAAO;MAE3C,aAAa,eAAe,OAAO;CAGrC,OAAO;EACL,MAAM;EACN,SAAS;GACP,KAAK,WAAW;GAChB;EACF;CACF;AACF"}
|
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.3",
|
|
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.3",
|
|
25
25
|
"proxy-agent": "^6.4.0"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"slc",
|
|
33
33
|
"oauth"
|
|
34
34
|
],
|
|
35
|
-
"author": "
|
|
35
|
+
"author": "abapify",
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|