@abapify/browser-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 +3 -3
- package/dist/index.d.mts +183 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +157 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ The `requiredCookies` option supports wildcard patterns:
|
|
|
169
169
|
requiredCookies: ['SAP_SESSIONID_*'];
|
|
170
170
|
|
|
171
171
|
// Wait for specific cookies
|
|
172
|
-
requiredCookies: ['
|
|
172
|
+
requiredCookies: ['SAP_SESSIONID_TRL_200', 'sap-usercontext'];
|
|
173
173
|
|
|
174
174
|
// Multiple patterns
|
|
175
175
|
requiredCookies: ['SAP_SESSIONID_*', 'MYSAPSSO2', 'sap-usercontext'];
|
|
@@ -181,10 +181,10 @@ requiredCookies: ['SAP_SESSIONID_*', 'MYSAPSSO2', 'sap-usercontext'];
|
|
|
181
181
|
import { matchesCookiePattern, cookieMatchesAny } from '@abapify/browser-auth';
|
|
182
182
|
|
|
183
183
|
// Check single pattern
|
|
184
|
-
matchesCookiePattern('
|
|
184
|
+
matchesCookiePattern('SAP_SESSIONID_TRL_200', 'SAP_SESSIONID_*'); // true
|
|
185
185
|
|
|
186
186
|
// Check multiple patterns
|
|
187
|
-
cookieMatchesAny('
|
|
187
|
+
cookieMatchesAny('SAP_SESSIONID_TRL_200', ['MYSAPSSO2', 'SAP_SESSIONID_*']); // true
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
## Types
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Browser Auth Core Types
|
|
4
|
+
*
|
|
5
|
+
* Shared types for browser-based SSO authentication.
|
|
6
|
+
* Used by both Playwright and Puppeteer adapters.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Cookie data structure (browser-agnostic)
|
|
10
|
+
*/
|
|
11
|
+
interface CookieData {
|
|
12
|
+
name: string;
|
|
13
|
+
value: string;
|
|
14
|
+
domain: string;
|
|
15
|
+
path: string;
|
|
16
|
+
expires?: number;
|
|
17
|
+
httpOnly?: boolean;
|
|
18
|
+
secure?: boolean;
|
|
19
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Browser authentication credentials
|
|
23
|
+
*/
|
|
24
|
+
interface BrowserCredentials {
|
|
25
|
+
/** SAP system base URL */
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
/** Cookies from authenticated session */
|
|
28
|
+
cookies: CookieData[];
|
|
29
|
+
/** When the session was created */
|
|
30
|
+
authenticatedAt: Date;
|
|
31
|
+
/** User agent used during authentication */
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Browser auth configuration options
|
|
36
|
+
*/
|
|
37
|
+
interface BrowserAuthOptions {
|
|
38
|
+
/** SAP system URL (used for cookie domain matching) */
|
|
39
|
+
url: string;
|
|
40
|
+
/**
|
|
41
|
+
* URL to open for SSO login (default: {url}/sap/bc/adt/core/http/systeminformation).
|
|
42
|
+
* Use this when the base SAP URL requires basic auth but SSO is available
|
|
43
|
+
* via a different path (e.g., /fiori, /sap/bc/ui5_ui5/ui2/ushell/shells/abap/FioriLaunchpad.html).
|
|
44
|
+
*/
|
|
45
|
+
loginUrl?: string;
|
|
46
|
+
/** Show browser window during login (default: false) */
|
|
47
|
+
headless?: boolean;
|
|
48
|
+
/** Timeout for login in ms (default: 300000 = 5 minutes) */
|
|
49
|
+
timeout?: number;
|
|
50
|
+
/** Custom user agent */
|
|
51
|
+
userAgent?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Cookie names/patterns to wait for before completing auth.
|
|
54
|
+
* Supports wildcards: 'SAP_SESSIONID_*'
|
|
55
|
+
* Auth completes when ALL patterns are matched.
|
|
56
|
+
*/
|
|
57
|
+
requiredCookies?: string[];
|
|
58
|
+
/** Path to persistent browser profile */
|
|
59
|
+
userDataDir?: string | boolean;
|
|
60
|
+
/** Ignore HTTPS errors (self-signed certificates) */
|
|
61
|
+
ignoreHTTPSErrors?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Response event data from browser adapter
|
|
65
|
+
*/
|
|
66
|
+
interface ResponseEvent {
|
|
67
|
+
url: string;
|
|
68
|
+
status: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Browser adapter interface - implemented by Playwright/Puppeteer wrappers
|
|
72
|
+
*/
|
|
73
|
+
interface BrowserAdapter {
|
|
74
|
+
/**
|
|
75
|
+
* Launch browser and create context
|
|
76
|
+
*/
|
|
77
|
+
launch(options: {
|
|
78
|
+
headless: boolean;
|
|
79
|
+
userDataDir?: string;
|
|
80
|
+
ignoreHTTPSErrors?: boolean;
|
|
81
|
+
userAgent?: string;
|
|
82
|
+
}): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Create a new page
|
|
85
|
+
*/
|
|
86
|
+
newPage(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Navigate to URL
|
|
89
|
+
*/
|
|
90
|
+
goto(url: string, options?: {
|
|
91
|
+
timeout?: number;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Get all cookies for a domain
|
|
95
|
+
*/
|
|
96
|
+
getCookies(): Promise<CookieData[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Clear cookies matching a domain pattern
|
|
99
|
+
*/
|
|
100
|
+
clearCookies(domain: string): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Get user agent string
|
|
103
|
+
*/
|
|
104
|
+
getUserAgent(): Promise<string>;
|
|
105
|
+
/**
|
|
106
|
+
* Close the page
|
|
107
|
+
*/
|
|
108
|
+
closePage(): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Close browser context
|
|
111
|
+
*/
|
|
112
|
+
close(): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Register event handler for responses
|
|
115
|
+
*/
|
|
116
|
+
onResponse(handler: (event: ResponseEvent) => void): void;
|
|
117
|
+
/**
|
|
118
|
+
* Register event handler for page close
|
|
119
|
+
*/
|
|
120
|
+
onPageClose(handler: () => void): void;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Test result for credential validation
|
|
124
|
+
*/
|
|
125
|
+
interface TestResult {
|
|
126
|
+
valid: boolean;
|
|
127
|
+
error?: string;
|
|
128
|
+
responseTime?: number;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/auth-core.d.ts
|
|
132
|
+
interface AuthenticateOptions extends BrowserAuthOptions {
|
|
133
|
+
/** Callback for logging */
|
|
134
|
+
log?: (message: string) => void;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Authenticate using browser-based SSO
|
|
138
|
+
*
|
|
139
|
+
* This is the main entry point - it orchestrates the auth flow using the provided adapter.
|
|
140
|
+
* The adapter handles browser-specific operations, this function handles the logic.
|
|
141
|
+
*/
|
|
142
|
+
declare function authenticate(adapter: BrowserAdapter, options: AuthenticateOptions): Promise<BrowserCredentials>;
|
|
143
|
+
/**
|
|
144
|
+
* Test if credentials are still valid
|
|
145
|
+
*/
|
|
146
|
+
declare function testCredentials(credentials: BrowserCredentials, options?: {
|
|
147
|
+
timeout?: number;
|
|
148
|
+
}): Promise<{
|
|
149
|
+
valid: boolean;
|
|
150
|
+
error?: string;
|
|
151
|
+
responseTime?: number;
|
|
152
|
+
}>;
|
|
153
|
+
/**
|
|
154
|
+
* Convert credentials to HTTP cookie header
|
|
155
|
+
*/
|
|
156
|
+
declare function toCookieHeader(credentials: BrowserCredentials): string;
|
|
157
|
+
/**
|
|
158
|
+
* Convert credentials to HTTP headers object
|
|
159
|
+
*/
|
|
160
|
+
declare function toHeaders(credentials: BrowserCredentials): Record<string, string>;
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/utils.d.ts
|
|
163
|
+
/**
|
|
164
|
+
* Browser Auth Utilities
|
|
165
|
+
*
|
|
166
|
+
* Cookie matching and path resolution utilities.
|
|
167
|
+
*/
|
|
168
|
+
/**
|
|
169
|
+
* Resolve userDataDir configuration to an absolute path
|
|
170
|
+
*/
|
|
171
|
+
declare function resolveUserDataDir(userDataDir?: string | boolean): string | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* Check if a cookie name matches a pattern (supports * wildcard)
|
|
174
|
+
* @example matchesCookiePattern('SAP_SESSIONID_TRL_200', 'SAP_SESSIONID_*') // true
|
|
175
|
+
*/
|
|
176
|
+
declare function matchesCookiePattern(cookieName: string, pattern: string): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Check if a cookie matches any of the required patterns
|
|
179
|
+
*/
|
|
180
|
+
declare function cookieMatchesAny(cookieName: string, patterns: string[]): boolean;
|
|
181
|
+
//#endregion
|
|
182
|
+
export { type AuthenticateOptions, type BrowserAdapter, type BrowserAuthOptions, type BrowserCredentials, type CookieData, type ResponseEvent, type TestResult, authenticate, cookieMatchesAny, matchesCookiePattern, resolveUserDataDir, testCredentials, toCookieHeader, toHeaders };
|
|
183
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/auth-core.ts","../src/utils.ts"],"mappings":";;;;;;;;;;UAUiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;UAMe;;EAEf;;EAEA,SAAS;;EAET,iBAAiB;;EAEjB;;;;;UAMe;;EAEf;;;;;;EAMA;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;EAEA;;EAEA;;;;;UAMe;EACf;EACA;;;;;UAMe;;;;EAIf,OAAO;IACL;IACA;IACA;IACA;MACE;;;;EAKJ,WAAW;;;;EAKX,KAAK,aAAa;IAAY;MAAqB;;;;EAKnD,cAAc,QAAQ;;;;EAKtB,aAAa,iBAAiB;;;;EAK9B,gBAAgB;;;;EAKhB,aAAa;;;;EAKb,SAAS;;;;EAKT,WAAW,UAAU,OAAO;;;;EAK5B,YAAY;;;;;UAMG;EACf;EACA;EACA;;;;UCtHe,4BAA4B;;EAE3C,OAAO;;;;;;;;iBASa,aACpB,SAAS,gBACT,SAAS,sBACR,QAAQ;;;;iBAgJW,gBACpB,aAAa,oBACb;EAAY;IACX;EAAU;EAAgB;EAAgB;;;;;iBA+C7B,eAAe,aAAa;;;;iBAO5B,UACd,aAAa,qBACZ;;;;;;;;;;;iBChOa,mBACd;;;;;iBAec,qBACd,oBACA;;;;iBAYc,iBACd,oBACA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/utils.ts
|
|
5
|
+
/**
|
|
6
|
+
* Browser Auth Utilities
|
|
7
|
+
*
|
|
8
|
+
* Cookie matching and path resolution utilities.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_USER_DATA_DIR = join(homedir(), ".adt", "browser-profile");
|
|
11
|
+
/**
|
|
12
|
+
* Resolve userDataDir configuration to an absolute path
|
|
13
|
+
*/
|
|
14
|
+
function resolveUserDataDir(userDataDir) {
|
|
15
|
+
if (userDataDir === true) return DEFAULT_USER_DATA_DIR;
|
|
16
|
+
if (typeof userDataDir === "string") return userDataDir;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if a cookie name matches a pattern (supports * wildcard)
|
|
20
|
+
* @example matchesCookiePattern('SAP_SESSIONID_TRL_200', 'SAP_SESSIONID_*') // true
|
|
21
|
+
*/
|
|
22
|
+
function matchesCookiePattern(cookieName, pattern) {
|
|
23
|
+
if (!pattern.includes("*")) return cookieName === pattern;
|
|
24
|
+
const regexPattern = pattern.replaceAll(/\*/g, ".*");
|
|
25
|
+
return new RegExp(`^${regexPattern}$`).test(cookieName);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if a cookie matches any of the required patterns
|
|
29
|
+
*/
|
|
30
|
+
function cookieMatchesAny(cookieName, patterns) {
|
|
31
|
+
return patterns.some((pattern) => matchesCookiePattern(cookieName, pattern));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/auth-core.ts
|
|
36
|
+
const DEFAULT_TIMEOUT = 3e5;
|
|
37
|
+
const SYSTEM_INFO_PATH = "/sap/bc/adt/core/http/systeminformation";
|
|
38
|
+
/**
|
|
39
|
+
* Authenticate using browser-based SSO
|
|
40
|
+
*
|
|
41
|
+
* This is the main entry point - it orchestrates the auth flow using the provided adapter.
|
|
42
|
+
* The adapter handles browser-specific operations, this function handles the logic.
|
|
43
|
+
*/
|
|
44
|
+
async function authenticate(adapter, options) {
|
|
45
|
+
const { url, loginUrl, headless = false, timeout = DEFAULT_TIMEOUT, userAgent, requiredCookies, userDataDir, ignoreHTTPSErrors = true, log = console.log } = options;
|
|
46
|
+
const targetUrl = loginUrl || new URL(SYSTEM_INFO_PATH, url).toString();
|
|
47
|
+
const sapHost = new URL(url).hostname;
|
|
48
|
+
const profileDir = resolveUserDataDir(userDataDir);
|
|
49
|
+
if (profileDir) log(`๐ Using persistent browser profile: ${profileDir}`);
|
|
50
|
+
try {
|
|
51
|
+
await adapter.launch({
|
|
52
|
+
headless,
|
|
53
|
+
userDataDir: profileDir,
|
|
54
|
+
ignoreHTTPSErrors,
|
|
55
|
+
userAgent
|
|
56
|
+
});
|
|
57
|
+
log("๐ Opening browser...");
|
|
58
|
+
await adapter.newPage();
|
|
59
|
+
log("๐งน Clearing old SAP cookies...");
|
|
60
|
+
await adapter.clearCookies(sapHost);
|
|
61
|
+
log("๐ Complete SSO login if prompted...");
|
|
62
|
+
const cookiesToWait = requiredCookies && requiredCookies.length > 0 ? requiredCookies : ["SAP_SESSIONID_*"];
|
|
63
|
+
log(`โณ Waiting for cookies: ${cookiesToWait.join(", ")}`);
|
|
64
|
+
const sapCookies = await new Promise((resolve, reject) => {
|
|
65
|
+
const timeoutId = setTimeout(() => {
|
|
66
|
+
reject(/* @__PURE__ */ new Error("Authentication timeout - SSO not completed"));
|
|
67
|
+
}, timeout);
|
|
68
|
+
adapter.onPageClose(() => {
|
|
69
|
+
clearTimeout(timeoutId);
|
|
70
|
+
reject(/* @__PURE__ */ new Error("Authentication cancelled - browser was closed"));
|
|
71
|
+
});
|
|
72
|
+
adapter.onResponse(async (event) => {
|
|
73
|
+
const eventHost = new URL(event.url).hostname;
|
|
74
|
+
if ((eventHost === sapHost || eventHost.endsWith("." + sapHost) || sapHost.endsWith("." + eventHost)) && event.status === 200) {
|
|
75
|
+
const matchedCookies = (await adapter.getCookies()).filter((c) => c.domain.includes(sapHost) || sapHost.includes(c.domain.replace(/^\./, ""))).filter((c) => cookieMatchesAny(c.name, cookiesToWait));
|
|
76
|
+
if (cookiesToWait.every((pattern) => matchedCookies.some((c) => matchesCookiePattern(c.name, pattern)))) {
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
log("โ
Authentication complete!");
|
|
79
|
+
log(`๐ช Found required cookies: ${matchedCookies.map((c) => c.name).join(", ")}`);
|
|
80
|
+
resolve(matchedCookies);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
adapter.goto(targetUrl, { timeout: 3e4 }).catch(() => {});
|
|
85
|
+
});
|
|
86
|
+
await adapter.closePage();
|
|
87
|
+
if (requiredCookies && requiredCookies.length > 0) {
|
|
88
|
+
const missingPatterns = requiredCookies.filter((pattern) => !sapCookies.some((c) => matchesCookiePattern(c.name, pattern)));
|
|
89
|
+
if (missingPatterns.length > 0) throw new Error(`Missing required cookies: ${missingPatterns.join(", ")}`);
|
|
90
|
+
}
|
|
91
|
+
if (sapCookies.length === 0) throw new Error("No SAP cookies captured - authentication may have failed");
|
|
92
|
+
const cookieNames = sapCookies.map((c) => c.name).join(", ");
|
|
93
|
+
log(`๐ช Captured ${sapCookies.length} cookies: ${cookieNames}`);
|
|
94
|
+
const userAgentString = userAgent || await adapter.getUserAgent();
|
|
95
|
+
return {
|
|
96
|
+
baseUrl: url,
|
|
97
|
+
cookies: sapCookies,
|
|
98
|
+
authenticatedAt: /* @__PURE__ */ new Date(),
|
|
99
|
+
userAgent: userAgentString
|
|
100
|
+
};
|
|
101
|
+
} finally {
|
|
102
|
+
await adapter.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Test if credentials are still valid
|
|
107
|
+
*/
|
|
108
|
+
async function testCredentials(credentials, options) {
|
|
109
|
+
const startTime = Date.now();
|
|
110
|
+
const testUrl = new URL(SYSTEM_INFO_PATH, credentials.baseUrl);
|
|
111
|
+
try {
|
|
112
|
+
const cookieHeader = credentials.cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
113
|
+
const response = await fetch(testUrl.toString(), {
|
|
114
|
+
headers: {
|
|
115
|
+
Cookie: cookieHeader,
|
|
116
|
+
Accept: "application/xml"
|
|
117
|
+
},
|
|
118
|
+
signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
|
|
119
|
+
});
|
|
120
|
+
const text = await response.text();
|
|
121
|
+
if (text.includes("SAMLRequest") || text.includes("SAMLResponse")) return {
|
|
122
|
+
valid: false,
|
|
123
|
+
error: "Session expired - SAML redirect detected",
|
|
124
|
+
responseTime: Date.now() - startTime
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
valid: response.ok,
|
|
128
|
+
error: response.ok ? void 0 : `HTTP ${response.status}`,
|
|
129
|
+
responseTime: Date.now() - startTime
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return {
|
|
133
|
+
valid: false,
|
|
134
|
+
error: error instanceof Error ? error.message : String(error),
|
|
135
|
+
responseTime: Date.now() - startTime
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Convert credentials to HTTP cookie header
|
|
141
|
+
*/
|
|
142
|
+
function toCookieHeader(credentials) {
|
|
143
|
+
return credentials.cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Convert credentials to HTTP headers object
|
|
147
|
+
*/
|
|
148
|
+
function toHeaders(credentials) {
|
|
149
|
+
return {
|
|
150
|
+
Cookie: toCookieHeader(credentials),
|
|
151
|
+
...credentials.userAgent ? { "User-Agent": credentials.userAgent } : {}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
//#endregion
|
|
156
|
+
export { authenticate, cookieMatchesAny, matchesCookiePattern, resolveUserDataDir, testCredentials, toCookieHeader, toHeaders };
|
|
157
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/utils.ts","../src/auth-core.ts"],"sourcesContent":["/**\n * Browser Auth Utilities\n *\n * Cookie matching and path resolution utilities.\n */\n\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nconst DEFAULT_USER_DATA_DIR = join(homedir(), '.adt', 'browser-profile');\n\n/**\n * Resolve userDataDir configuration to an absolute path\n */\nexport function resolveUserDataDir(\n userDataDir?: string | boolean,\n): string | undefined {\n if (userDataDir === true) {\n return DEFAULT_USER_DATA_DIR;\n }\n if (typeof userDataDir === 'string') {\n return userDataDir;\n }\n return undefined;\n}\n\n/**\n * Check if a cookie name matches a pattern (supports * wildcard)\n * @example matchesCookiePattern('SAP_SESSIONID_TRL_200', 'SAP_SESSIONID_*') // true\n */\nexport function matchesCookiePattern(\n cookieName: string,\n pattern: string,\n): boolean {\n if (!pattern.includes('*')) {\n return cookieName === pattern;\n }\n const regexPattern = pattern.replaceAll(/\\*/g, '.*');\n return new RegExp(`^${regexPattern}$`).test(cookieName);\n}\n\n/**\n * Check if a cookie matches any of the required patterns\n */\nexport function cookieMatchesAny(\n cookieName: string,\n patterns: string[],\n): boolean {\n return patterns.some((pattern) => matchesCookiePattern(cookieName, pattern));\n}\n","/**\n * Browser Auth Core\n *\n * Event-driven authentication logic shared by Playwright and Puppeteer adapters.\n * This module contains NO browser-specific code.\n */\n\nimport type {\n BrowserAdapter,\n BrowserAuthOptions,\n BrowserCredentials,\n} from './types';\nimport {\n matchesCookiePattern,\n cookieMatchesAny,\n resolveUserDataDir,\n} from './utils';\n\nconst DEFAULT_TIMEOUT = 300_000; // 5 minutes\nconst SYSTEM_INFO_PATH = '/sap/bc/adt/core/http/systeminformation';\n\nexport interface AuthenticateOptions extends BrowserAuthOptions {\n /** Callback for logging */\n log?: (message: string) => void;\n}\n\n/**\n * Authenticate using browser-based SSO\n *\n * This is the main entry point - it orchestrates the auth flow using the provided adapter.\n * The adapter handles browser-specific operations, this function handles the logic.\n */\nexport async function authenticate(\n adapter: BrowserAdapter,\n options: AuthenticateOptions,\n): Promise<BrowserCredentials> {\n const {\n url,\n loginUrl,\n headless = false,\n timeout = DEFAULT_TIMEOUT,\n userAgent,\n requiredCookies,\n userDataDir,\n ignoreHTTPSErrors = true,\n log = console.log,\n } = options;\n\n // URL to navigate to for SSO login\n const targetUrl = loginUrl || new URL(SYSTEM_INFO_PATH, url).toString();\n const sapHost = new URL(url).hostname;\n const profileDir = resolveUserDataDir(userDataDir);\n\n if (profileDir) {\n log(`๐ Using persistent browser profile: ${profileDir}`);\n }\n\n try {\n // Step 1: Launch browser\n await adapter.launch({\n headless,\n userDataDir: profileDir,\n ignoreHTTPSErrors,\n userAgent,\n });\n\n log('๐ Opening browser...');\n await adapter.newPage();\n\n // Step 2: Clear old SAP cookies to ensure we capture fresh ones\n // This is critical for re-authentication - old cookies may be stale\n log('๐งน Clearing old SAP cookies...');\n await adapter.clearCookies(sapHost);\n\n // Step 3: Wait for authentication (required cookies appear after SSO)\n log('๐ Complete SSO login if prompted...');\n\n const cookiesToWait =\n requiredCookies && requiredCookies.length > 0\n ? requiredCookies\n : ['SAP_SESSIONID_*']; // Default: wait for session cookie\n\n log(`โณ Waiting for cookies: ${cookiesToWait.join(', ')}`);\n\n const sapCookies = await new Promise<\n Awaited<ReturnType<typeof adapter.getCookies>>\n >((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n reject(new Error('Authentication timeout - SSO not completed'));\n }, timeout);\n\n adapter.onPageClose(() => {\n clearTimeout(timeoutId);\n reject(new Error('Authentication cancelled - browser was closed'));\n });\n\n // Check for required cookies on every response from SAP domain\n adapter.onResponse(async (event) => {\n // Check responses from SAP domain (not just targetUrl, since SSO may redirect)\n const eventHost = new URL(event.url).hostname;\n const isSapDomain =\n eventHost === sapHost ||\n eventHost.endsWith('.' + sapHost) ||\n sapHost.endsWith('.' + eventHost);\n\n if (isSapDomain && event.status === 200) {\n // Got 200 from SAP domain - check if required cookies are set\n const allCookies = await adapter.getCookies();\n const domainCookies = allCookies.filter(\n (c) =>\n c.domain.includes(sapHost) ||\n sapHost.includes(c.domain.replace(/^\\./, '')),\n );\n const matchedCookies = domainCookies.filter((c) =>\n cookieMatchesAny(c.name, cookiesToWait),\n );\n const allFound = cookiesToWait.every((pattern) =>\n matchedCookies.some((c) => matchesCookiePattern(c.name, pattern)),\n );\n\n if (allFound) {\n clearTimeout(timeoutId);\n log('โ
Authentication complete!');\n log(\n `๐ช Found required cookies: ${matchedCookies.map((c) => c.name).join(', ')}`,\n );\n resolve(matchedCookies);\n }\n // If cookies not found yet, keep waiting for more responses\n }\n });\n\n // Navigate - events are already set up; navigation errors are expected\n // (auth flow handles results through response event listeners, not navigation result)\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n adapter.goto(targetUrl, { timeout: 30000 }).catch(() => {});\n });\n\n await adapter.closePage();\n\n // Validate cookies\n if (requiredCookies && requiredCookies.length > 0) {\n const missingPatterns = requiredCookies.filter(\n (pattern) =>\n !sapCookies.some((c) => matchesCookiePattern(c.name, pattern)),\n );\n if (missingPatterns.length > 0) {\n throw new Error(\n `Missing required cookies: ${missingPatterns.join(', ')}`,\n );\n }\n }\n\n if (sapCookies.length === 0) {\n throw new Error(\n 'No SAP cookies captured - authentication may have failed',\n );\n }\n\n const cookieNames = sapCookies.map((c) => c.name).join(', ');\n log(`๐ช Captured ${sapCookies.length} cookies: ${cookieNames}`);\n\n // Get user agent\n const userAgentString = userAgent || (await adapter.getUserAgent());\n\n return {\n baseUrl: url,\n cookies: sapCookies,\n authenticatedAt: new Date(),\n userAgent: userAgentString,\n };\n } finally {\n await adapter.close();\n }\n}\n\n/**\n * Test if credentials are still valid\n */\nexport async function testCredentials(\n credentials: BrowserCredentials,\n options?: { timeout?: number },\n): Promise<{ valid: boolean; error?: string; responseTime?: number }> {\n const startTime = Date.now();\n const testUrl = new URL(SYSTEM_INFO_PATH, credentials.baseUrl);\n\n try {\n const cookieHeader = credentials.cookies\n .map((c) => `${c.name}=${c.value}`)\n .join('; ');\n\n const response = await fetch(testUrl.toString(), {\n headers: {\n Cookie: cookieHeader,\n Accept: 'application/xml',\n },\n signal: options?.timeout\n ? AbortSignal.timeout(options.timeout)\n : undefined,\n });\n\n const text = await response.text();\n\n // Check for SAML redirect (session expired)\n if (text.includes('SAMLRequest') || text.includes('SAMLResponse')) {\n return {\n valid: false,\n error: 'Session expired - SAML redirect detected',\n responseTime: Date.now() - startTime,\n };\n }\n\n return {\n valid: response.ok,\n error: response.ok ? undefined : `HTTP ${response.status}`,\n responseTime: Date.now() - startTime,\n };\n } catch (error) {\n return {\n valid: false,\n error: error instanceof Error ? error.message : String(error),\n responseTime: Date.now() - startTime,\n };\n }\n}\n\n/**\n * Convert credentials to HTTP cookie header\n */\nexport function toCookieHeader(credentials: BrowserCredentials): string {\n return credentials.cookies.map((c) => `${c.name}=${c.value}`).join('; ');\n}\n\n/**\n * Convert credentials to HTTP headers object\n */\nexport function toHeaders(\n credentials: BrowserCredentials,\n): Record<string, string> {\n return {\n Cookie: toCookieHeader(credentials),\n ...(credentials.userAgent ? { 'User-Agent': credentials.userAgent } : {}),\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abapify/browser-auth",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Browser-based SSO authentication core - shared logic for Playwright/Puppeteer plugins",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@abapify/adt-config": "0.4.
|
|
20
|
+
"@abapify/adt-config": "0.4.3"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^22.10.2"
|