@abapify/adt-auth 0.4.0 → 0.4.1

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.
@@ -0,0 +1,14 @@
1
+ import { i as AuthPluginOptions, r as AuthPlugin } from "./types-C2mcG0xY.mjs";
2
+
3
+ //#region src/plugins/basic.d.ts
4
+ interface BasicAuthOptions extends AuthPluginOptions {
5
+ username: string;
6
+ password: string;
7
+ }
8
+ /**
9
+ * Basic auth plugin - uses provided credentials
10
+ */
11
+ declare const authPlugin: AuthPlugin;
12
+ //#endregion
13
+ export { BasicAuthOptions, authPlugin as default };
14
+ //# sourceMappingURL=basic.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"basic.d.mts","names":[],"sources":["../src/plugins/basic.ts"],"mappings":";;;UASiB,gBAAA,SAAyB,iBAAA;EACxC,QAAA;EACA,QAAA;AAAA;;AACD;;cAKK,UAAA,EAAY,UAAA"}
package/dist/basic.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import authPlugin from "./plugins/basic.mjs";
2
+
3
+ export { authPlugin as default };
@@ -0,0 +1,203 @@
1
+ import { i as AuthPluginOptions, o as AuthSession } from "./types-C2mcG0xY.mjs";
2
+
3
+ //#region src/storage/file-storage.d.ts
4
+ declare class FileStorage {
5
+ private readonly baseDir;
6
+ private readonly storageDir;
7
+ private readonly configPath;
8
+ constructor(baseDir?: string);
9
+ /**
10
+ * Save session to file
11
+ */
12
+ save(session: AuthSession): void;
13
+ /**
14
+ * Load session from file
15
+ */
16
+ load(sid: string): AuthSession | null;
17
+ /**
18
+ * Delete session file
19
+ */
20
+ delete(sid: string): void;
21
+ /**
22
+ * List all available SIDs
23
+ */
24
+ list(): string[];
25
+ /**
26
+ * Check if session exists
27
+ */
28
+ exists(sid: string): boolean;
29
+ /**
30
+ * Set default SID
31
+ */
32
+ setDefaultSid(sid: string): void;
33
+ /**
34
+ * Get default SID
35
+ */
36
+ getDefaultSid(): string | null;
37
+ /**
38
+ * Clear default SID
39
+ */
40
+ clearDefaultSid(): void;
41
+ private getSessionPath;
42
+ private ensureStorageDir;
43
+ private loadConfig;
44
+ private saveConfig;
45
+ }
46
+ //#endregion
47
+ //#region src/auth-manager.d.ts
48
+ /**
49
+ * Destination configuration (from adt.config.ts)
50
+ */
51
+ interface Destination {
52
+ /** Plugin package to use (e.g., '@abapify/adt-puppeteer', '@abapify/adt-auth/basic') */
53
+ type: string;
54
+ /** Plugin-specific options */
55
+ options: AuthPluginOptions;
56
+ }
57
+ declare class AuthManager {
58
+ private storage;
59
+ constructor(storage?: FileStorage);
60
+ /**
61
+ * Login using a destination configuration
62
+ *
63
+ * Dynamically loads the plugin specified in destination.type and authenticates.
64
+ *
65
+ * @param sid - System ID (e.g., 'BHF', 'S0D')
66
+ * @param destination - Destination config with plugin type and options
67
+ */
68
+ login(sid: string, destination: Destination): Promise<AuthSession>;
69
+ /**
70
+ * Build session from plugin result
71
+ */
72
+ private buildSession;
73
+ /**
74
+ * Save a session (after successful authentication)
75
+ */
76
+ saveSession(session: AuthSession): void;
77
+ /**
78
+ * Get session by SID
79
+ */
80
+ getSession(sid?: string): AuthSession | null;
81
+ /**
82
+ * Delete session (logout)
83
+ */
84
+ deleteSession(sid: string): void;
85
+ /**
86
+ * List all available SIDs
87
+ */
88
+ listSids(): string[];
89
+ /**
90
+ * Set default SID (persisted to config)
91
+ */
92
+ setDefaultSid(sid: string): void;
93
+ /**
94
+ * Get default SID
95
+ */
96
+ getDefaultSid(): string | null;
97
+ /**
98
+ * Clear default SID
99
+ */
100
+ clearDefaultSid(): void;
101
+ /**
102
+ * Check if session credentials are expired
103
+ */
104
+ isExpired(session: AuthSession): boolean;
105
+ /**
106
+ * Refresh credentials using the auth plugin
107
+ *
108
+ * @returns Updated session with new credentials, or null if refresh not supported/failed
109
+ */
110
+ refreshCredentials(session: AuthSession, options?: {
111
+ log?: (message: string) => void;
112
+ }): Promise<AuthSession | null>;
113
+ /**
114
+ * Get cookie header for HTTP client (convenience method)
115
+ */
116
+ getCookieHeader(session: AuthSession): string | null;
117
+ /**
118
+ * Get basic auth credentials (convenience method)
119
+ */
120
+ getBasicAuth(session: AuthSession): {
121
+ username: string;
122
+ password: string;
123
+ } | null;
124
+ }
125
+ //#endregion
126
+ //#region src/types/service-key.d.ts
127
+ interface UAACredentials {
128
+ tenantmode: string;
129
+ sburl: string;
130
+ subaccountid: string;
131
+ 'credential-type': string;
132
+ clientid: string;
133
+ xsappname: string;
134
+ clientsecret: string;
135
+ serviceInstanceId: string;
136
+ url: string;
137
+ uaadomain: string;
138
+ verificationkey: string;
139
+ apiurl: string;
140
+ identityzone: string;
141
+ identityzoneid: string;
142
+ tenantid: string;
143
+ zoneid: string;
144
+ }
145
+ interface Catalog {
146
+ path: string;
147
+ type: string;
148
+ }
149
+ interface Binding {
150
+ env: string;
151
+ version: string;
152
+ type: string;
153
+ id: string;
154
+ }
155
+ interface BTPServiceKey {
156
+ uaa: UAACredentials;
157
+ url: string;
158
+ 'sap.cloud.service': string;
159
+ systemid: string;
160
+ endpoints: Record<string, string>;
161
+ catalogs: Record<string, Catalog>;
162
+ binding: Binding;
163
+ preserve_host_header: boolean;
164
+ }
165
+ interface ServiceKeyPluginOptions {
166
+ url: string;
167
+ client?: string;
168
+ serviceKey: string | object;
169
+ /** Callback to open a URL in the user's browser. Falls back to dynamic import of 'open'. */
170
+ openBrowser?: (url: string) => Promise<void>;
171
+ /** Port for the local OAuth callback server (default: 3000) */
172
+ callbackPort?: number;
173
+ /** Timeout for the auth flow in ms (default: 300000 = 5 min) */
174
+ timeoutMs?: number;
175
+ /** Full redirect URI for the OAuth callback (e.g. in tunnelled/forwarded environments). Defaults to http://localhost:<port>/callback */
176
+ redirectUri?: string;
177
+ [key: string]: unknown;
178
+ }
179
+ //#endregion
180
+ //#region src/utils/env.d.ts
181
+ /**
182
+ * Read a BTP service key from a file path.
183
+ *
184
+ * @param filePath - Path to a JSON file containing the BTP service key
185
+ * @returns Parsed and validated `BTPServiceKey`
186
+ * @throws Error if the input looks like raw JSON (a file path is required)
187
+ * @throws Error if the file does not exist
188
+ */
189
+ declare function readServiceKey(filePath: string): BTPServiceKey;
190
+ /**
191
+ * Resolves a service key destination from the `ADT_SERVICE_KEY` environment
192
+ * variable.
193
+ *
194
+ * The value can be a path to a JSON file or a raw JSON string (for backward
195
+ * compatibility with CI pipelines that store the key as a secret JSON value).
196
+ *
197
+ * @returns A `Destination` object for the service-key plugin, or `null` if
198
+ * `ADT_SERVICE_KEY` is not set.
199
+ */
200
+ declare function resolveServiceKeyFromEnv(): Destination | null;
201
+ //#endregion
202
+ 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 };
203
+ //# sourceMappingURL=env-CNa9YA_U.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-CNa9YA_U.d.mts","names":[],"sources":["../src/storage/file-storage.ts","../src/auth-manager.ts","../src/types/service-key.ts","../src/utils/env.ts"],"mappings":";;;cAuBa,WAAA;EAAA,iBACM,OAAA;EAAA,iBACA,UAAA;EAAA,iBACA,UAAA;cAEL,OAAA;EAcZ;;;EAAA,IAAA,CAAK,OAAA,EAAS,WAAA;EAST;;;EAAL,IAAA,CAAK,GAAA,WAAc,WAAA;EA8BnB;;;EAVA,MAAA,CAAO,GAAA;EAkCO;;;EAxBd,IAAA,CAAA;EAuDQ;;;EA1CR,MAAA,CAAO,GAAA;EA2DW;;;EAhDlB,aAAA,CAAc,GAAA;ECpFC;;;ED6Ff,aAAA,CAAA;EC3FA;;;EDmGA,eAAA,CAAA;EAAA,QAUQ,cAAA;EAAA,QAIA,gBAAA;EAAA,QAMA,UAAA;EAAA,QAWA,UAAA;AAAA;;;;;;UCpIO,WAAA;EDqBV;ECnBL,IAAA;ED4BK;EC1BL,OAAA,EAAS,iBAAA;AAAA;AAAA,cAGE,WAAA;EAAA,QACH,OAAA;cAEI,OAAA,GAAU,WAAA;ED+Df;;;;;;;;EC/CD,KAAA,CAAM,GAAA,UAAa,WAAA,EAAa,WAAA,GAAc,OAAA,CAAQ,WAAA;ED0G1C;;;EAAA,QC1EV,YAAA;;AA1DV;;EAoGE,WAAA,CAAY,OAAA,EAAS,WAAA;EAhGK;;;EAuG1B,UAAA,CAAW,GAAA,YAAe,WAAA;EAvGA;;AAG5B;EA+GE,aAAA,CAAc,GAAA;;;;EAOd,QAAA,CAAA;EAnGoD;;;EA8GpD,aAAA,CAAc,GAAA;EAuCH;;;EAhCX,aAAA,CAAA;EAuIW;;;EAhIX,eAAA,CAAA;;;;EAWA,SAAA,CAAU,OAAA,EAAS,WAAA;EAvIP;;;;;EAoJN,kBAAA,CACJ,OAAA,EAAS,WAAA,EACT,OAAA;IAAY,GAAA,IAAO,OAAA;EAAA,IAClB,OAAA,CAAQ,WAAA;EA7EC;;;EAsKZ,eAAA,CAAgB,OAAA,EAAS,WAAA;EApJzB;;;EA+JA,YAAA,CACE,OAAA,EAAS,WAAA;IACN,QAAA;IAAkB,QAAA;EAAA;AAAA;;;UC1SR,cAAA;EACf,UAAA;EACA,KAAA;EACA,YAAA;EACA,iBAAA;EACA,QAAA;EACA,SAAA;EACA,YAAA;EACA,iBAAA;EACA,GAAA;EACA,SAAA;EACA,eAAA;EACA,MAAA;EACA,YAAA;EACA,cAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UAGe,OAAA;EACf,IAAA;EACA,IAAA;AAAA;AAAA,UAGe,OAAA;EACf,GAAA;EACA,OAAA;EACA,IAAA;EACA,EAAA;AAAA;AAAA,UAGe,aAAA;EACf,GAAA,EAAK,cAAA;EACL,GAAA;EACA,mBAAA;EACA,QAAA;EACA,SAAA,EAAW,MAAA;EACX,QAAA,EAAU,MAAA,SAAe,OAAA;EACzB,OAAA,EAAS,OAAA;EACT,oBAAA;AAAA;AAAA,UAYe,uBAAA;EACf,GAAA;EACA,MAAA;EACA,UAAA;EDT4D;ECW5D,WAAA,IAAe,GAAA,aAAgB,OAAA;ED+DV;EC7DrB,YAAA;ED0HmB;ECxHnB,SAAA;EDwIW;ECtIX,WAAA;EAAA,CACC,GAAA;AAAA;;;;AF1CH;;;;;;;iBGXgB,cAAA,CAAe,QAAA,WAAmB,aAAA;;;;;;;;;;;iBA6BlC,wBAAA,CAAA,GAA4B,WAAA"}
@@ -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-C2mcG0xY.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-CNa9YA_U.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,289 @@
1
+ import authPlugin from "./plugins/basic.mjs";
2
+ import { t as authPlugin$1 } from "./service-key-D_fQRV_K.mjs";
3
+ import { readServiceKey, resolveServiceKeyFromEnv } from "./utils/env.mjs";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
5
+ import { dirname, join } from "path";
6
+ import { homedir } from "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
+ writeFileSync(this.getSessionPath(session.sid), JSON.stringify(session, null, 2), { mode: 384 });
30
+ }
31
+ /**
32
+ * Load session from file
33
+ */
34
+ load(sid) {
35
+ const filePath = this.getSessionPath(sid);
36
+ if (!existsSync(filePath)) return null;
37
+ try {
38
+ const data = readFileSync(filePath, "utf8");
39
+ return JSON.parse(data);
40
+ } catch (error) {
41
+ throw new Error(`Failed to load session ${sid}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
42
+ }
43
+ }
44
+ /**
45
+ * Delete session file
46
+ */
47
+ delete(sid) {
48
+ const filePath = this.getSessionPath(sid);
49
+ if (existsSync(filePath)) unlinkSync(filePath);
50
+ }
51
+ /**
52
+ * List all available SIDs
53
+ */
54
+ list() {
55
+ if (!existsSync(this.storageDir)) return [];
56
+ return readdirSync(this.storageDir).filter((file) => file.endsWith(".json")).map((file) => file.replace(".json", ""));
57
+ }
58
+ /**
59
+ * Check if session exists
60
+ */
61
+ exists(sid) {
62
+ return existsSync(this.getSessionPath(sid));
63
+ }
64
+ /**
65
+ * Set default SID
66
+ */
67
+ setDefaultSid(sid) {
68
+ const config = this.loadConfig();
69
+ config.defaultSid = sid;
70
+ this.saveConfig(config);
71
+ }
72
+ /**
73
+ * Get default SID
74
+ */
75
+ getDefaultSid() {
76
+ return this.loadConfig().defaultSid ?? null;
77
+ }
78
+ /**
79
+ * Clear default SID
80
+ */
81
+ clearDefaultSid() {
82
+ const config = this.loadConfig();
83
+ delete config.defaultSid;
84
+ this.saveConfig(config);
85
+ }
86
+ getSessionPath(sid) {
87
+ return join(this.storageDir, `${sid}.json`);
88
+ }
89
+ ensureStorageDir() {
90
+ if (!existsSync(this.storageDir)) mkdirSync(this.storageDir, {
91
+ recursive: true,
92
+ mode: 448
93
+ });
94
+ }
95
+ loadConfig() {
96
+ if (!existsSync(this.configPath)) return {};
97
+ try {
98
+ return JSON.parse(readFileSync(this.configPath, "utf8"));
99
+ } catch {
100
+ return {};
101
+ }
102
+ }
103
+ saveConfig(config) {
104
+ mkdirSync(dirname(this.configPath), { recursive: true });
105
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2), { mode: 384 });
106
+ }
107
+ };
108
+
109
+ //#endregion
110
+ //#region src/auth-manager.ts
111
+ var AuthManager = class {
112
+ storage;
113
+ constructor(storage) {
114
+ this.storage = storage || new FileStorage();
115
+ }
116
+ /**
117
+ * Login using a destination configuration
118
+ *
119
+ * Dynamically loads the plugin specified in destination.type and authenticates.
120
+ *
121
+ * @param sid - System ID (e.g., 'BHF', 'S0D')
122
+ * @param destination - Destination config with plugin type and options
123
+ */
124
+ async login(sid, destination) {
125
+ const pluginModule = await import(destination.type);
126
+ if (!pluginModule.default?.authenticate) throw new Error(`Plugin ${destination.type} does not have a default export with authenticate method`);
127
+ const result = await pluginModule.default.authenticate(destination.options);
128
+ const session = this.buildSession(sid, destination, result);
129
+ this.saveSession(session);
130
+ if (this.listSids().length === 1) this.setDefaultSid(sid);
131
+ return session;
132
+ }
133
+ /**
134
+ * Build session from plugin result
135
+ */
136
+ buildSession(sid, destination, result) {
137
+ if (result.method === "cookie") return {
138
+ sid: sid.toUpperCase(),
139
+ host: destination.options.url,
140
+ client: destination.options.client,
141
+ auth: {
142
+ method: "cookie",
143
+ plugin: destination.type,
144
+ pluginOptions: destination.options,
145
+ credentials: {
146
+ cookies: result.credentials.cookies,
147
+ expiresAt: result.credentials.expiresAt.toISOString()
148
+ }
149
+ }
150
+ };
151
+ else return {
152
+ sid: sid.toUpperCase(),
153
+ host: destination.options.url,
154
+ client: destination.options.client,
155
+ auth: {
156
+ method: "basic",
157
+ plugin: destination.type,
158
+ pluginOptions: destination.options,
159
+ credentials: result.credentials
160
+ }
161
+ };
162
+ }
163
+ /**
164
+ * Save a session (after successful authentication)
165
+ */
166
+ saveSession(session) {
167
+ this.storage.save(session);
168
+ }
169
+ /**
170
+ * Get session by SID
171
+ */
172
+ getSession(sid) {
173
+ const targetSid = sid || this.getDefaultSid();
174
+ if (!targetSid) return null;
175
+ return this.storage.load(targetSid);
176
+ }
177
+ /**
178
+ * Delete session (logout)
179
+ */
180
+ deleteSession(sid) {
181
+ this.storage.delete(sid);
182
+ }
183
+ /**
184
+ * List all available SIDs
185
+ */
186
+ listSids() {
187
+ return this.storage.list();
188
+ }
189
+ /**
190
+ * Set default SID (persisted to config)
191
+ */
192
+ setDefaultSid(sid) {
193
+ this.storage.setDefaultSid(sid);
194
+ }
195
+ /**
196
+ * Get default SID
197
+ */
198
+ getDefaultSid() {
199
+ return this.storage.getDefaultSid() || (this.storage.list()[0] ?? null);
200
+ }
201
+ /**
202
+ * Clear default SID
203
+ */
204
+ clearDefaultSid() {
205
+ this.storage.clearDefaultSid();
206
+ }
207
+ /**
208
+ * Check if session credentials are expired
209
+ */
210
+ isExpired(session) {
211
+ if (session.auth.method !== "cookie") return false;
212
+ const creds = session.auth.credentials;
213
+ return new Date(creds.expiresAt) < /* @__PURE__ */ new Date();
214
+ }
215
+ /**
216
+ * Refresh credentials using the auth plugin
217
+ *
218
+ * @returns Updated session with new credentials, or null if refresh not supported/failed
219
+ */
220
+ async refreshCredentials(session, options) {
221
+ const log = options?.log ?? console.log;
222
+ if (!session.auth.plugin) return null;
223
+ const pluginModule = await import(session.auth.plugin);
224
+ if (!pluginModule.default) throw new Error(`Plugin ${session.auth.plugin} does not have a default export`);
225
+ const plugin = pluginModule.default;
226
+ if (plugin.refresh) {
227
+ const sessionWithLog = {
228
+ ...session,
229
+ auth: {
230
+ ...session.auth,
231
+ pluginOptions: {
232
+ url: session.host,
233
+ client: session.client,
234
+ ...session.auth.pluginOptions,
235
+ log
236
+ }
237
+ }
238
+ };
239
+ const result = await plugin.refresh(sessionWithLog);
240
+ if (result) {
241
+ const destination = {
242
+ type: session.auth.plugin,
243
+ options: session.auth.pluginOptions || {
244
+ url: session.host,
245
+ client: session.client
246
+ }
247
+ };
248
+ const updatedSession = this.buildSession(session.sid, destination, result);
249
+ this.saveSession(updatedSession);
250
+ return updatedSession;
251
+ }
252
+ log("⚠️ Silent refresh failed - falling back to interactive authentication...");
253
+ }
254
+ const baseOptions = session.auth.pluginOptions || {
255
+ url: session.host,
256
+ client: session.client
257
+ };
258
+ const runtimeOptions = {
259
+ ...baseOptions,
260
+ log
261
+ };
262
+ const result = await plugin.authenticate(runtimeOptions);
263
+ const destination = {
264
+ type: session.auth.plugin,
265
+ options: baseOptions
266
+ };
267
+ const updatedSession = this.buildSession(session.sid, destination, result);
268
+ this.saveSession(updatedSession);
269
+ return updatedSession;
270
+ }
271
+ /**
272
+ * Get cookie header for HTTP client (convenience method)
273
+ */
274
+ getCookieHeader(session) {
275
+ if (session.auth.method !== "cookie") return null;
276
+ return session.auth.credentials.cookies;
277
+ }
278
+ /**
279
+ * Get basic auth credentials (convenience method)
280
+ */
281
+ getBasicAuth(session) {
282
+ if (session.auth.method !== "basic") return null;
283
+ return session.auth.credentials;
284
+ }
285
+ };
286
+
287
+ //#endregion
288
+ export { AuthManager, FileStorage, authPlugin as basicAuthPlugin, readServiceKey, resolveServiceKeyFromEnv, authPlugin$1 as serviceKeyAuthPlugin };
289
+ //# 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 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from '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., 'BHF', 'S0D')\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;AAE/B,KAAI,CAAC,YAAY,CAAC,SAChB,OAAM,IAAI,MAAM,4CAA4C;AAG9D,QAAO;EACL,QAAQ;EACR,aAAa;GACX;GACA;GACD;EACF;GAEJ"}
@@ -0,0 +1,3 @@
1
+ import { t as authPlugin } from "../service-key-D_fQRV_K.mjs";
2
+
3
+ export { authPlugin as default };
@@ -0,0 +1,234 @@
1
+ import { t as ServiceKeyParser } from "./service-key-DiAQtcf7.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 "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 "&amp;";
154
+ case "<": return "&lt;";
155
+ case ">": return "&gt;";
156
+ case "\"": return "&quot;";
157
+ default: return "&#39;";
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-D_fQRV_K.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-key-D_fQRV_K.mjs","names":["parseUrl"],"sources":["../src/utils/pkce.ts","../src/plugins/service-key.ts"],"sourcesContent":["import { createHash, randomBytes } from '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 '&amp;';\n case '<':\n return '&lt;';\n case '>':\n return '&gt;';\n case '\"':\n return '&quot;';\n default:\n return '&#39;';\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-DiAQtcf7.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-key-DiAQtcf7.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;AAEJ,MAAI,OAAO,mBAAmB,SAC5B,KAAI;AACF,YAAS,KAAK,MAAM,eAAe;WAC5B,OAAO;AACd,SAAM,IAAI,MAAM,sCAAsC,EAAE,OAAO,OAAO,CAAC;;MAGzE,UAAS;AAGX,MAAI,CAAC,UAAU,OAAO,WAAW,SAC/B,OAAM,IAAI,MAAM,6BAA6B;EAG/C,MAAM,aAAa;AAEnB,MAAI,CAAC,WAAW,UAAU,CAAC,WAAW,UAAU,CAAC,WAAW,YAC1D,OAAM,IAAI,MAAM,yCAAyC;AAG3D,SAAO"}
@@ -0,0 +1,7 @@
1
+ import { r as AuthPlugin } from "./types-C2mcG0xY.mjs";
2
+
3
+ //#region src/plugins/service-key.d.ts
4
+ declare const authPlugin: AuthPlugin;
5
+ //#endregion
6
+ export { authPlugin as default };
7
+ //# sourceMappingURL=service-key.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-key.d.mts","names":[],"sources":["../src/plugins/service-key.ts"],"mappings":";;;cAsQM,UAAA,EAAY,UAAA"}
@@ -0,0 +1,3 @@
1
+ import { t as authPlugin } from "./service-key-D_fQRV_K.mjs";
2
+
3
+ export { authPlugin as default };
@@ -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-C2mcG0xY.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-C2mcG0xY.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;AAgBA;;;;;AAKA;;;;;AAAA,KALY,UAAA;;;;UAKK,gBAAA;EACf,QAAA;EACA,QAAA;AAAA;;;AA4BF;UAtBiB,iBAAA;EACf,OAAA;EACA,SAAA;AAAA;;;;KAMU,WAAA,GAAc,gBAAA,GAAmB,iBAAA;;;;;;;;;UAc5B,UAAA;EACf,MAAA,EAAQ,UAAA;EACR,MAAA;EACA,aAAA,GAAgB,iBAAA;EAChB,WAAA,EAAa,WAAA;AAAA;;;;;;AAiCf;;;UAlBiB,WAAA;EACf,GAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA,EAAM,UAAA;AAAA;;;;;;;;UAcS,UAAA;EAKG;EAHlB,YAAA,CAAa,OAAA,EAAS,iBAAA,GAAoB,OAAA,CAAQ,gBAAA;EAGlB;EAAhC,OAAA,EAAS,OAAA,EAAS,WAAA,GAAc,OAAA,CAAQ,gBAAA;AAAA;AAAA,UAGzB,iBAAA;EACf,GAAA;EACA,MAAA;EAAA,CACC,GAAA;AAAA;;UAIc,gBAAA;EACf,MAAA;EACA,WAAA;IACE,OAAA;IACA,SAAA,EAAW,IAAA;EAAA;AAAA;;UAKE,eAAA;EACf,MAAA;EACA,WAAA;IACE,QAAA;IACA,QAAA;EAAA;AAAA;AAJJ;AAAA,KASY,gBAAA,GAAmB,gBAAA,GAAmB,eAAA;;;;UASjC,oBAAA;EACf,OAAA;EACA,KAAA;EACA,UAAA;EACA,YAAA;AAAA"}
@@ -0,0 +1,2 @@
1
+ import { n as resolveServiceKeyFromEnv, t as readServiceKey } from "../env-CNa9YA_U.mjs";
2
+ export { readServiceKey, resolveServiceKeyFromEnv };
@@ -0,0 +1,48 @@
1
+ import { t as ServiceKeyParser } from "../service-key-DiAQtcf7.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,MAAM;AAE/B,KAAI,QAAQ,WAAW,IAAI,CACzB,OAAM,IAAI,MACR,4KAGD;AAGH,KAAI,CAAC,WAAW,QAAQ,CACtB,OAAM,IAAI,MAAM,+BAA+B,UAAU;CAG3D,MAAM,MAAM,aAAa,SAAS,QAAQ;AAC1C,QAAO,iBAAiB,MAAM,IAAI;;;;;;;;;;;;AAapC,SAAgB,2BAA+C;CAC7D,MAAM,MAAM,QAAQ,IAAI;AAExB,KAAI,CAAC,IACH,QAAO;CAGT,MAAM,UAAU,IAAI,MAAM;CAI1B,IAAI;AACJ,KAAI,QAAQ,WAAW,IAAI,CACzB,cAAa,iBAAiB,MAAM,QAAQ;KAE5C,cAAa,eAAe,QAAQ;AAGtC,QAAO;EACL,MAAM;EACN,SAAS;GACP,KAAK,WAAW;GAChB;GACD;EACF"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.0",
6
+ "version": "0.4.1",
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.0",
24
+ "@abapify/logger": "0.4.1",
25
25
  "proxy-agent": "^6.4.0"
26
26
  },
27
27
  "keywords": [