@abapify/browser-auth 0.4.33 → 0.4.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,28 +1,11 @@
1
1
  # @abapify/browser-auth
2
2
 
3
- [![version](https://img.shields.io/github/package-json/v/abapify/adt-cli?filename=packages/browser-auth/package.json)](https://github.com/abapify/adt-cli/pkgs/npm/%40abapify%2Fbrowser-auth)
3
+ [![npm](https://img.shields.io/npm/v/%40abapify%2Fbrowser-auth)](https://www.npmjs.com/package/@abapify/browser-auth)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/abapify/adt-cli/blob/main/LICENSE)
4
5
 
5
- Core browser-based SSO authentication logic for SAP ADT systems. This package provides the shared authentication flow used by browser-specific adapters like Playwright and Puppeteer.
6
+ Browser-based SSO authentication core - shared logic for Playwright/Puppeteer plugins.
6
7
 
7
- > **Note:** This is a low-level package. Most users should use `@abapify/adt-playwright` or `@abapify/adt-puppeteer` instead.
8
-
9
- ## Architecture
10
-
11
- ```
12
- @abapify/browser-auth (this package)
13
- ├── BrowserAdapter interface
14
- ├── Event-driven auth flow
15
- ├── Cookie utilities
16
- └── Pattern matching
17
- ↑
18
- @abapify/adt-playwright ─────┐
19
- └── Playwright adapter │
20
- ├── Implement BrowserAdapter
21
- @abapify/adt-puppeteer ──────┘
22
- └── Puppeteer adapter
23
- ```
24
-
25
- ## Installation
8
+ ## Install
26
9
 
27
10
  ```bash
28
11
  npm install @abapify/browser-auth
@@ -30,243 +13,25 @@ npm install @abapify/browser-auth
30
13
 
31
14
  ## Usage
32
15
 
33
- This package is primarily used by browser adapter implementations. Direct usage:
16
+ ```ts
17
+ import { authenticate, toCookieHeader } from '@abapify/browser-auth';
34
18
 
35
- ```typescript
36
- import {
37
- authenticate,
38
- testCredentials,
39
- toCookieHeader,
40
- } from '@abapify/browser-auth';
41
- import type { BrowserAdapter } from '@abapify/browser-auth';
42
-
43
- // Create your browser adapter (see BrowserAdapter interface)
44
- const adapter: BrowserAdapter = createMyAdapter();
45
-
46
- // Authenticate
47
19
  const credentials = await authenticate(adapter, {
48
- url: 'https://sap-system.example.com',
49
- headless: false,
50
- userDataDir: true,
51
- requiredCookies: ['SAP_SESSIONID_*', 'sap-usercontext'],
20
+ url: 'https://sap.example.com',
52
21
  });
53
-
54
- // Test if credentials are still valid
55
- const result = await testCredentials(credentials);
56
- console.log(result.valid); // true/false
57
-
58
- // Convert to Cookie header string
59
- const cookieHeader = toCookieHeader(credentials);
60
- // "SAP_SESSIONID_XXX=abc123; sap-usercontext=..."
61
- ```
62
-
63
- ## API
64
-
65
- ### `authenticate(adapter, options)`
66
-
67
- Performs browser-based SSO authentication.
68
-
69
- ```typescript
70
- async function authenticate(
71
- adapter: BrowserAdapter,
72
- options: BrowserAuthOptions,
73
- ): Promise<BrowserCredentials>;
74
- ```
75
-
76
- **Parameters:**
77
-
78
- - `adapter` - Browser adapter implementing `BrowserAdapter` interface
79
- - `options` - Authentication options
80
-
81
- **Options:**
82
-
83
- ```typescript
84
- interface BrowserAuthOptions {
85
- url: string; // SAP system URL
86
- headless?: boolean; // Hide browser (default: false)
87
- timeout?: number; // Login timeout in ms (default: 300000)
88
- userAgent?: string; // Custom user agent
89
- requiredCookies?: string[]; // Cookie patterns to wait for
90
- userDataDir?: string | boolean; // Session persistence path
91
- ignoreHTTPSErrors?: boolean; // Ignore SSL errors (default: true)
92
- }
93
- ```
94
-
95
- **Returns:** `BrowserCredentials` with captured cookies
96
-
97
- ### `testCredentials(credentials)`
98
-
99
- Tests if credentials are still valid by making a request to the SAP system.
100
-
101
- ```typescript
102
- async function testCredentials(
103
- credentials: BrowserCredentials,
104
- ): Promise<TestResult>;
105
- ```
106
-
107
- ### `toCookieHeader(credentials)`
108
-
109
- Converts credentials to a Cookie header string.
110
-
111
- ```typescript
112
- function toCookieHeader(credentials: BrowserCredentials): string;
113
- ```
114
-
115
- ### `toHeaders(credentials)`
116
-
117
- Creates headers object with Cookie and User-Agent.
118
-
119
- ```typescript
120
- function toHeaders(credentials: BrowserCredentials): Record<string, string>;
121
- ```
122
-
123
- ## BrowserAdapter Interface
124
-
125
- To create a new browser adapter, implement this interface:
126
-
127
- ```typescript
128
- interface BrowserAdapter {
129
- /** Launch browser and create context */
130
- launch(options: {
131
- headless: boolean;
132
- userDataDir?: string;
133
- ignoreHTTPSErrors?: boolean;
134
- userAgent?: string;
135
- }): Promise<void>;
136
-
137
- /** Create a new page */
138
- newPage(): Promise<void>;
139
-
140
- /** Navigate to URL */
141
- goto(url: string, options?: { timeout?: number }): Promise<void>;
142
-
143
- /** Get all cookies */
144
- getCookies(): Promise<CookieData[]>;
145
-
146
- /** Get user agent string */
147
- getUserAgent(): Promise<string>;
148
-
149
- /** Close the page */
150
- closePage(): Promise<void>;
151
-
152
- /** Close browser context */
153
- close(): Promise<void>;
154
-
155
- /** Register response event handler */
156
- onResponse(handler: (event: ResponseEvent) => void): void;
157
-
158
- /** Register page close handler */
159
- onPageClose(handler: () => void): void;
160
- }
161
- ```
162
-
163
- ## Cookie Pattern Matching
164
-
165
- The `requiredCookies` option supports wildcard patterns:
166
-
167
- ```typescript
168
- // Wait for any SAP session cookie
169
- requiredCookies: ['SAP_SESSIONID_*'];
170
-
171
- // Wait for specific cookies
172
- requiredCookies: ['SAP_SESSIONID_TRL_200', 'sap-usercontext'];
173
-
174
- // Multiple patterns
175
- requiredCookies: ['SAP_SESSIONID_*', 'MYSAPSSO2', 'sap-usercontext'];
22
+ const cookie = toCookieHeader(credentials);
176
23
  ```
177
24
 
178
- ### Utility Functions
25
+ ## Features
179
26
 
180
- ```typescript
181
- import { matchesCookiePattern, cookieMatchesAny } from '@abapify/browser-auth';
27
+ - Shared browser-SSO flow for Playwright and Puppeteer adapters
28
+ - `BrowserAdapter` interface for new browser engines
29
+ - Cookie filtering and header helpers
182
30
 
183
- // Check single pattern
184
- matchesCookiePattern('SAP_SESSIONID_TRL_200', 'SAP_SESSIONID_*'); // true
31
+ ## Documentation
185
32
 
186
- // Check multiple patterns
187
- cookieMatchesAny('SAP_SESSIONID_TRL_200', ['MYSAPSSO2', 'SAP_SESSIONID_*']); // true
188
- ```
189
-
190
- ## Types
191
-
192
- ```typescript
193
- interface CookieData {
194
- name: string;
195
- value: string;
196
- domain: string;
197
- path: string;
198
- expires?: number;
199
- httpOnly?: boolean;
200
- secure?: boolean;
201
- sameSite?: 'Strict' | 'Lax' | 'None';
202
- }
203
-
204
- interface BrowserCredentials {
205
- baseUrl: string;
206
- cookies: CookieData[];
207
- authenticatedAt: Date;
208
- userAgent?: string;
209
- }
210
-
211
- interface TestResult {
212
- valid: boolean;
213
- error?: string;
214
- responseTime?: number;
215
- }
216
- ```
217
-
218
- ## Creating a New Adapter
219
-
220
- Example: Creating a Selenium adapter
221
-
222
- ```typescript
223
- import type {
224
- BrowserAdapter,
225
- CookieData,
226
- ResponseEvent,
227
- } from '@abapify/browser-auth';
228
- import { Builder, Browser } from 'selenium-webdriver';
229
-
230
- export function createSeleniumAdapter(): BrowserAdapter {
231
- let driver: WebDriver | null = null;
232
- const responseHandlers: ((event: ResponseEvent) => void)[] = [];
233
-
234
- return {
235
- async launch(options) {
236
- driver = await new Builder().forBrowser(Browser.CHROME).build();
237
- },
238
-
239
- async newPage() {
240
- // Selenium uses single page model
241
- },
242
-
243
- async goto(url, options) {
244
- await driver?.get(url);
245
- },
246
-
247
- async getCookies(): Promise<CookieData[]> {
248
- const cookies = await driver?.manage().getCookies();
249
- return (
250
- cookies?.map((c) => ({
251
- name: c.name,
252
- value: c.value,
253
- domain: c.domain || '',
254
- path: c.path || '/',
255
- expires: c.expiry?.getTime(),
256
- httpOnly: c.httpOnly,
257
- secure: c.secure,
258
- })) || []
259
- );
260
- },
261
-
262
- // ... implement remaining methods
263
- };
264
- }
265
- ```
33
+ **[adt-cli.netlify.app/sdk/packages/browser-auth](https://adt-cli.netlify.app/sdk/packages/browser-auth)**
266
34
 
267
- ## Related Packages
35
+ ---
268
36
 
269
- - [`@abapify/adt-playwright`](../adt-playwright) - Playwright adapter (recommended)
270
- - [`@abapify/adt-puppeteer`](../adt-puppeteer) - Puppeteer adapter
271
- - [`@abapify/adt-auth`](../adt-auth) - Authentication manager
272
- - [`@abapify/adt-config`](../adt-config) - Configuration utilities
37
+ Part of [**abapify**](https://github.com/abapify/adt-cli) — open, typed tooling for SAP ABAP Development Tools (ADT): CLI, MCP server, and SDK.
package/dist/index.mjs CHANGED
@@ -21,7 +21,7 @@ function resolveUserDataDir(userDataDir) {
21
21
  */
22
22
  function matchesCookiePattern(cookieName, pattern) {
23
23
  if (!pattern.includes("*")) return cookieName === pattern;
24
- const regexPattern = pattern.replaceAll(/\*/g, ".*");
24
+ const regexPattern = pattern.replaceAll("*", ".*");
25
25
  return new RegExp(`^${regexPattern}$`).test(cookieName);
26
26
  }
27
27
  /**
@@ -1 +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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"}
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('*', '.*');\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.33",
3
+ "version": "0.4.36",
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.33"
20
+ "@abapify/adt-config": "0.4.36"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^22.10.2"