websession-api-client 0.0.2 → 0.0.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.
Files changed (3) hide show
  1. package/index.js +437 -317
  2. package/package.json +1 -1
  3. package/readme.md +79 -79
package/index.js CHANGED
@@ -1,317 +1,437 @@
1
- //
2
- const puppeteer = require('puppeteer-extra');
3
- const StealthPlugin = require('puppeteer-extra-plugin-stealth');
4
-
5
- puppeteer.use(StealthPlugin());
6
-
7
- const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
8
-
9
- class ApiExtractor {
10
- constructor() {
11
- this.browser = null;
12
- this.page = null;
13
- this.TargetCookie = null;
14
- this.TargetCookieValue = null;
15
- this.TargetDomain = null;
16
-
17
- this.authPage = null;
18
- this.bearerToken = null;
19
- this.tokenExpiry = null;
20
- // this.init();
21
- }
22
-
23
- async init(options = {}) {
24
- console.log('starting browser...');
25
-
26
- const defaultArgs = [
27
- '--no-sandbox',
28
- '--disable-setuid-sandbox',
29
- '--disable-web-security',
30
- '--disable-features=VizDisplayCompositor',
31
- '--disable-background-timer-throttling',
32
- '--disable-backgrounding-occluded-windows',
33
- '--disable-renderer-backgrounding',
34
- '--disable-dev-shm-usage',
35
- '--no-first-run',
36
- '--no-default-browser-check',
37
- '--disable-gpu',
38
- ];
39
-
40
- const puppeteerConfig = {
41
- headless: true,
42
- ...options,
43
- args: options.args ? [...defaultArgs, ...options.args] : defaultArgs
44
- };
45
-
46
- this.browser = await puppeteer.launch(puppeteerConfig);
47
-
48
- this.page = await this.browser.newPage();
49
- await this.page.setUserAgent(DEFAULT_USER_AGENT);
50
-
51
- console.log('Browser initialized');
52
- }
53
-
54
- async getBearer(timeoutMs = 15000) {
55
- console.log('Getting fresh Bearer token...');
56
-
57
- if (!this.browser) {
58
- throw new Error('Browser not initialized. Call init() first.');
59
- }
60
-
61
- const tokenPage = await this.browser.newPage();
62
- await tokenPage.setUserAgent(DEFAULT_USER_AGENT);
63
- await tokenPage.setRequestInterception(true);
64
-
65
- let settled = false;
66
- let timeoutId;
67
-
68
- const tokenPromise = new Promise((resolve) => {
69
- const onRequest = (request) => {
70
- try {
71
- const authHeader = request.headers()['authorization'];
72
- if (!settled && authHeader && authHeader.startsWith('Bearer ') && authHeader.length > 100) {
73
- settled = true;
74
- const capturedToken = authHeader.substring(7);
75
- console.log('Fresh Bearer token captured:', capturedToken.substring(0, 10) + '...');
76
- clearTimeout(timeoutId);
77
- tokenPage.off('request', onRequest);
78
- resolve(capturedToken);
79
- }
80
- } finally {
81
- request.continue();
82
- }
83
- };
84
-
85
- tokenPage.on('request', onRequest);
86
-
87
- timeoutId = setTimeout(() => {
88
- if (settled) return;
89
- settled = true;
90
- tokenPage.off('request', onRequest);
91
- resolve(null);
92
- }, timeoutMs);
93
- });
94
- this.authPage = tokenPage;
95
- return { tokenPromise };
96
- }
97
-
98
- // CookieName is what the cookie is named in devtools, value is the value from devtools, and domain is the cookie domain
99
- async setAuth(cookieName, domain, cookieValue) {
100
- this.TargetDomain = domain;
101
- this.TargetCookie = cookieName;
102
- this.TargetCookieValue = cookieValue;
103
- console.log('Setting auth cookie and attempting token capture...');
104
- try {
105
- // Capture the bearer token from the first request after setting the cookie
106
- const { tokenPromise } = await this.getBearer();
107
-
108
- const domainparts = this.TargetDomain.split('.').slice(-2);
109
- const baseDomain = domainparts.join('.');
110
-
111
- await this.authPage.setCookie({
112
- name: this.TargetCookie,
113
- value: this.TargetCookieValue,
114
- domain: `.${baseDomain}`,
115
- });
116
-
117
- const navigationPromise = this.authPage.goto(`https://${this.TargetDomain}/`, {
118
- waitUntil: 'domcontentloaded',
119
- timeout: 15000
120
- }).catch((error) => {
121
- if (error && error.name === 'TimeoutError') {
122
- return null;
123
- }
124
- throw error;
125
- });
126
-
127
- const capturedToken = await tokenPromise;
128
- await navigationPromise;
129
-
130
- if (capturedToken) {
131
- this.bearerToken = capturedToken;
132
- this.tokenExpiry = Date.now() + (55 * 60 * 1000);
133
- console.log('Bearer token set successfully');
134
- await this.authPage.close();
135
- return true;
136
- }
137
-
138
- console.log('No Bearer token captured');
139
- await this.authPage.close();
140
- return false;
141
- } catch (error) {
142
- console.error('Bearer token extraction error:', error.message);
143
- return false;
144
- }
145
- }
146
-
147
- async callApi(url, options = {}) {
148
- const {
149
- auth = 'auto',
150
- transport = 'node',
151
- headers = {},
152
- json,
153
- ...fetchOptions
154
- } = options;
155
-
156
- const shouldUseAuth = auth === true || (auth === 'auto' && !!this.bearerToken);
157
-
158
- if (auth === true && !this.bearerToken) {
159
- throw new Error('This request requires auth, but no bearer token is set. Call setAuth() first.');
160
- }
161
-
162
- const finalHeaders = {
163
- 'Accept': 'application/json',
164
- 'User-Agent': DEFAULT_USER_AGENT,
165
- ...headers
166
- };
167
-
168
- if (shouldUseAuth) {
169
- finalHeaders.Authorization = `Bearer ${this.bearerToken}`;
170
- }
171
-
172
- // Convenience: pass { json: {...} } instead of manually stringifying body
173
- if (json !== undefined) {
174
- fetchOptions.body = JSON.stringify(json);
175
- const hasContentTypeHeader = Object.keys(finalHeaders).some(
176
- (key) => key.toLowerCase() === 'content-type'
177
- );
178
-
179
- if (!hasContentTypeHeader) {
180
- finalHeaders['Content-Type'] = 'application/json';
181
- }
182
- }
183
-
184
- if (transport === 'browser') {
185
- return this.callApiInBrowser(url, {
186
- ...fetchOptions,
187
- headers: finalHeaders
188
- });
189
- }
190
-
191
- try {
192
- const response = await fetch(url, {
193
- ...fetchOptions,
194
- headers: finalHeaders
195
- });
196
-
197
- if (!response.ok) {
198
- const errorText = await response.text().catch(() => '');
199
- console.error(`API call failed (${response.status} ${response.statusText})`, errorText);
200
- return null;
201
- }
202
-
203
- const contentType = response.headers.get('content-type') || '';
204
- if (contentType.includes('application/json')) {
205
- return await response.json();
206
- }
207
-
208
- return await response.text();
209
- } catch (error) {
210
- console.error('API call error:', error.message);
211
- return null;
212
- }
213
- }
214
-
215
- async callAuthApi(url, options = {}) {
216
- return this.callApi(url, { ...options, auth: true });
217
- }
218
-
219
- async callApiInBrowser(url, options = {}) {
220
- if (!this.page) {
221
- throw new Error('Browser page not initialized. Call init() first.');
222
- }
223
-
224
- const browserFetchOptions = { ...options };
225
-
226
- if (browserFetchOptions.body !== undefined && typeof browserFetchOptions.body !== 'string') {
227
- throw new Error('Browser transport only supports string bodies. Use `json` or a string `body`.');
228
- }
229
-
230
- if (!browserFetchOptions.credentials) {
231
- browserFetchOptions.credentials = 'include';
232
- }
233
-
234
- let currentOrigin = null;
235
- let targetOrigin = null;
236
-
237
- try {
238
- currentOrigin = new URL(this.page.url()).origin;
239
- } catch (error) {
240
- currentOrigin = null;
241
- }
242
-
243
- try {
244
- targetOrigin = new URL(url).origin;
245
- } catch (error) {
246
- targetOrigin = null;
247
- }
248
-
249
- // Reduce CORS issues by putting the browser page on the target origin first.
250
- if (targetOrigin && currentOrigin !== targetOrigin) {
251
- try {
252
- await this.page.goto(targetOrigin, {
253
- waitUntil: 'domcontentloaded',
254
- timeout: 15000
255
- });
256
- } catch (error) {
257
- if (!error || error.name !== 'TimeoutError') {
258
- throw error;
259
- }
260
- }
261
- }
262
-
263
- try {
264
- const response = await this.page.evaluate(async ({ requestUrl, requestOptions }) => {
265
- try {
266
- const res = await fetch(requestUrl, requestOptions);
267
- const headers = {};
268
-
269
- res.headers.forEach((value, key) => {
270
- headers[key] = value;
271
- });
272
-
273
- return {
274
- ok: res.ok,
275
- status: res.status,
276
- statusText: res.statusText,
277
- headers,
278
- text: await res.text()
279
- };
280
- } catch (error) {
281
- return {
282
- ok: false,
283
- status: 0,
284
- statusText: 'BrowserFetchError',
285
- headers: {},
286
- text: error && error.message ? error.message : String(error)
287
- };
288
- }
289
- }, {
290
- requestUrl: url,
291
- requestOptions: browserFetchOptions
292
- });
293
-
294
- if (!response.ok) {
295
- console.error(`API call failed (${response.status} ${response.statusText})`, response.text);
296
- return null;
297
- }
298
-
299
- const contentType = (response.headers && response.headers['content-type']) || '';
300
- if (contentType.includes('application/json')) {
301
- try {
302
- return JSON.parse(response.text);
303
- } catch (error) {
304
- console.error('API call error: failed to parse JSON response from browser transport');
305
- return null;
306
- }
307
- }
308
-
309
- return response.text;
310
- } catch (error) {
311
- console.error('API call error (browser transport):', error.message);
312
- return null;
313
- }
314
- }
315
- }
316
-
317
- module.exports = ApiExtractor;
1
+ const puppeteer = require('puppeteer-extra');
2
+ const StealthPlugin = require('puppeteer-extra-plugin-stealth');
3
+
4
+ puppeteer.use(StealthPlugin());
5
+
6
+ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
7
+
8
+ class ApiExtractor {
9
+ constructor() {
10
+ this.browser = null;
11
+ this.page = null;
12
+ this.TargetCookie = null;
13
+ this.TargetCookieValue = null;
14
+ this.TargetDomain = null;
15
+
16
+ this.authPage = null;
17
+ this.bearerToken = null;
18
+ this.tokenExpiry = null;
19
+ }
20
+
21
+ async init(options = {}) {
22
+ console.log('starting browser...');
23
+
24
+ const defaultArgs = [
25
+ '--no-sandbox',
26
+ '--disable-setuid-sandbox',
27
+ '--disable-web-security',
28
+ '--disable-features=VizDisplayCompositor',
29
+ '--disable-background-timer-throttling',
30
+ '--disable-backgrounding-occluded-windows',
31
+ '--disable-renderer-backgrounding',
32
+ '--disable-dev-shm-usage',
33
+ '--no-first-run',
34
+ '--no-default-browser-check',
35
+ '--disable-gpu',
36
+ ];
37
+
38
+ const puppeteerConfig = {
39
+ headless: true,
40
+ ...options,
41
+ args: options.args ? [...defaultArgs, ...options.args] : defaultArgs
42
+ };
43
+
44
+ this.browser = await puppeteer.launch(puppeteerConfig);
45
+
46
+ this.page = await this.browser.newPage();
47
+ await this.page.setUserAgent(DEFAULT_USER_AGENT);
48
+
49
+ console.log('Browser initialized');
50
+ }
51
+
52
+ async getBearer(timeoutMs = 15000) {
53
+ console.log('Getting fresh Bearer token...');
54
+
55
+ if (!this.browser) {
56
+ throw new Error('Browser not initialized. Call init() first.');
57
+ }
58
+
59
+ const tokenPage = await this.browser.newPage();
60
+ await tokenPage.setUserAgent(DEFAULT_USER_AGENT);
61
+ await tokenPage.setRequestInterception(true);
62
+
63
+ let settled = false;
64
+ let timeoutId;
65
+
66
+ const tokenPromise = new Promise((resolve) => {
67
+ const onRequest = (request) => {
68
+ try {
69
+ const authHeader = request.headers()['authorization'];
70
+ if (!settled && authHeader && authHeader.startsWith('Bearer ') && authHeader.length > 100) {
71
+ settled = true;
72
+ const capturedToken = authHeader.substring(7);
73
+ console.log('Fresh Bearer token captured:', capturedToken.substring(0, 10) + '...');
74
+ clearTimeout(timeoutId);
75
+ tokenPage.off('request', onRequest);
76
+ resolve(capturedToken);
77
+ }
78
+ } finally {
79
+ request.continue();
80
+ }
81
+ };
82
+
83
+ tokenPage.on('request', onRequest);
84
+
85
+ timeoutId = setTimeout(() => {
86
+ if (settled) return;
87
+ settled = true;
88
+ tokenPage.off('request', onRequest);
89
+ resolve(null);
90
+ }, timeoutMs);
91
+ });
92
+
93
+ this.authPage = tokenPage;
94
+ return { tokenPromise };
95
+ }
96
+
97
+ // CookieName is what the cookie is named in devtools, value is the value from devtools, and domain is the cookie domain
98
+ async setAuth(cookieName, domain, cookieValue) {
99
+ this.TargetDomain = domain;
100
+ this.TargetCookie = cookieName;
101
+ this.TargetCookieValue = cookieValue;
102
+ console.log('Setting auth cookie and attempting token capture...');
103
+ try {
104
+ const { tokenPromise } = await this.getBearer();
105
+
106
+ const domainparts = this.TargetDomain.split('.').slice(-2);
107
+ const baseDomain = domainparts.join('.');
108
+
109
+ await this.authPage.setCookie({
110
+ name: this.TargetCookie,
111
+ value: this.TargetCookieValue,
112
+ domain: `.${baseDomain}`,
113
+ });
114
+
115
+ const navigationPromise = this.authPage.goto(`https://${this.TargetDomain}/`, {
116
+ waitUntil: 'domcontentloaded',
117
+ timeout: 15000
118
+ }).catch((error) => {
119
+ if (error && error.name === 'TimeoutError') {
120
+ return null;
121
+ }
122
+ throw error;
123
+ });
124
+
125
+ const capturedToken = await tokenPromise;
126
+ await navigationPromise;
127
+
128
+ if (capturedToken) {
129
+ this.bearerToken = capturedToken;
130
+ this.tokenExpiry = Date.now() + (55 * 60 * 1000);
131
+ console.log('Bearer token set successfully');
132
+ await this.authPage.close();
133
+ return true;
134
+ }
135
+
136
+ console.log('No Bearer token captured');
137
+ await this.authPage.close();
138
+ return false;
139
+ } catch (error) {
140
+ console.error('Bearer token extraction error:', error.message);
141
+ return false;
142
+ }
143
+ }
144
+
145
+ async callApi(url, options = {}) {
146
+ const {
147
+ auth = 'auto',
148
+ transport = 'node',
149
+ headers = {},
150
+ json,
151
+ ...fetchOptions
152
+ } = options;
153
+
154
+ const shouldUseAuth = auth === true || (auth === 'auto' && !!this.bearerToken);
155
+
156
+ if (auth === true && !this.bearerToken) {
157
+ throw new Error('This request requires auth, but no bearer token is set. Call setAuth() first.');
158
+ }
159
+
160
+ const finalHeaders = {
161
+ 'Accept': 'application/json',
162
+ 'User-Agent': DEFAULT_USER_AGENT,
163
+ ...headers
164
+ };
165
+
166
+ if (shouldUseAuth) {
167
+ finalHeaders.Authorization = `Bearer ${this.bearerToken}`;
168
+ }
169
+
170
+ // Convenience: pass { json: {...} } instead of manually stringifying body
171
+ if (json !== undefined) {
172
+ fetchOptions.body = JSON.stringify(json);
173
+ const hasContentTypeHeader = Object.keys(finalHeaders).some(
174
+ (key) => key.toLowerCase() === 'content-type'
175
+ );
176
+ if (!hasContentTypeHeader) {
177
+ finalHeaders['Content-Type'] = 'application/json';
178
+ }
179
+ }
180
+
181
+ if (transport === 'browser') {
182
+ return this.callApiInBrowser(url, {
183
+ ...fetchOptions,
184
+ headers: finalHeaders
185
+ });
186
+ }
187
+
188
+ try {
189
+ const response = await fetch(url, {
190
+ ...fetchOptions,
191
+ headers: finalHeaders
192
+ });
193
+
194
+ if (!response.ok) {
195
+ const errorText = await response.text().catch(() => '');
196
+ console.error(`API call failed (${response.status} ${response.statusText})`, errorText);
197
+ return null;
198
+ }
199
+
200
+ const contentType = response.headers.get('content-type') || '';
201
+ if (contentType.includes('application/json')) {
202
+ return await response.json();
203
+ }
204
+
205
+ return await response.text();
206
+ } catch (error) {
207
+ console.error('API call error:', error.message);
208
+ return null;
209
+ }
210
+ }
211
+
212
+ async callAuthApi(url, options = {}) {
213
+ return this.callApi(url, { ...options, auth: true });
214
+ }
215
+
216
+ async callApiInBrowser(url, options = {}) {
217
+ if (!this.page) {
218
+ throw new Error('Browser page not initialized. Call init() first.');
219
+ }
220
+
221
+ const browserFetchOptions = { ...options };
222
+
223
+ if (browserFetchOptions.body !== undefined && typeof browserFetchOptions.body !== 'string') {
224
+ throw new Error('Browser transport only supports string bodies. Use `json` or a string `body`.');
225
+ }
226
+
227
+ if (!browserFetchOptions.credentials) {
228
+ browserFetchOptions.credentials = 'include';
229
+ }
230
+
231
+ let currentOrigin = null;
232
+ let targetOrigin = null;
233
+
234
+ try {
235
+ currentOrigin = new URL(this.page.url()).origin;
236
+ } catch (error) {
237
+ currentOrigin = null;
238
+ }
239
+
240
+ try {
241
+ targetOrigin = new URL(url).origin;
242
+ } catch (error) {
243
+ targetOrigin = null;
244
+ }
245
+
246
+ // Reduce CORS issues by putting the browser page on the target origin first.
247
+ if (targetOrigin && currentOrigin !== targetOrigin) {
248
+ try {
249
+ await this.page.goto(targetOrigin, {
250
+ waitUntil: 'domcontentloaded',
251
+ timeout: 15000
252
+ });
253
+ } catch (error) {
254
+ if (!error || error.name !== 'TimeoutError') {
255
+ throw error;
256
+ }
257
+ }
258
+ }
259
+
260
+ try {
261
+ const response = await this.page.evaluate(async ({ requestUrl, requestOptions }) => {
262
+ try {
263
+ const res = await fetch(requestUrl, requestOptions);
264
+ const headers = {};
265
+
266
+ res.headers.forEach((value, key) => {
267
+ headers[key] = value;
268
+ });
269
+
270
+ return {
271
+ ok: res.ok,
272
+ status: res.status,
273
+ statusText: res.statusText,
274
+ headers,
275
+ text: await res.text()
276
+ };
277
+ } catch (error) {
278
+ return {
279
+ ok: false,
280
+ status: 0,
281
+ statusText: 'BrowserFetchError',
282
+ headers: {},
283
+ text: error && error.message ? error.message : String(error)
284
+ };
285
+ }
286
+ }, {
287
+ requestUrl: url,
288
+ requestOptions: browserFetchOptions
289
+ });
290
+
291
+ if (!response.ok) {
292
+ console.error(`API call failed (${response.status} ${response.statusText})`, response.text);
293
+ return null;
294
+ }
295
+
296
+ const contentType = (response.headers && response.headers['content-type']) || '';
297
+ if (contentType.includes('application/json')) {
298
+ try {
299
+ return JSON.parse(response.text);
300
+ } catch (error) {
301
+ console.error('API call error: failed to parse JSON response from browser transport');
302
+ return null;
303
+ }
304
+ }
305
+
306
+ return response.text;
307
+ } catch (error) {
308
+ console.error('API call error (browser transport):', error.message);
309
+ return null;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Navigate to any webpage and capture matching network requests/responses.
315
+ *
316
+ * @param {string} targetUrl - The URL to navigate to.
317
+ * @param {object} [options]
318
+ * @param {function} [options.filter] - Called with a request summary object.
319
+ * Return true to capture that request. If omitted, all requests are captured.
320
+ * Summary shape: { url, method, headers, postData, resourceType }
321
+ * @param {boolean} [options.captureResponse] - Also capture response body (slower). Default false.
322
+ * @param {number} [options.timeout] - Max ms to wait for page + requests. Default 30000.
323
+ * @param {number} [options.waitAfterLoad] - Extra ms to wait after page load for late requests. Default 3000.
324
+ * @param {number} [options.maxCaptures] - Stop early after capturing this many. Default Infinity.
325
+ * @param {object[]} [options.cookies] - Cookies to set before navigating.
326
+ * Each: { name, value, domain, ... }
327
+ * @returns {Promise<object[]>} Array of captured request (and optionally response) objects.
328
+ */
329
+ async captureRequests(targetUrl, options = {}) {
330
+ const {
331
+ filter = null,
332
+ captureResponse = false,
333
+ timeout = 30000,
334
+ waitAfterLoad = 3000,
335
+ maxCaptures = Infinity,
336
+ cookies = []
337
+ } = options;
338
+
339
+ if (!this.browser) {
340
+ throw new Error('Browser not initialized. Call init() first.');
341
+ }
342
+
343
+ const page = await this.browser.newPage();
344
+ await page.setUserAgent(DEFAULT_USER_AGENT);
345
+
346
+ if (cookies.length > 0) {
347
+ await page.setCookie(...cookies);
348
+ }
349
+
350
+ const captured = [];
351
+ const responseBodyPromises = [];
352
+
353
+ // Use CDP for response body capture since request interception
354
+ // can interfere with page behavior on some sites.
355
+ const client = await page.createCDPSession();
356
+ await client.send('Network.enable');
357
+
358
+ // Map requestId -> request data for pairing requests with responses
359
+ const pendingRequests = new Map();
360
+
361
+ client.on('Network.requestWillBeSent', (event) => {
362
+ const summary = {
363
+ requestId: event.requestId,
364
+ url: event.request.url,
365
+ method: event.request.method,
366
+ headers: event.request.headers,
367
+ postData: event.request.postData || null,
368
+ resourceType: event.type
369
+ };
370
+
371
+ const shouldCapture = filter ? filter(summary) : true;
372
+ if (shouldCapture) {
373
+ pendingRequests.set(event.requestId, summary);
374
+ }
375
+ });
376
+
377
+ client.on('Network.responseReceived', async (event) => {
378
+ const reqData = pendingRequests.get(event.requestId);
379
+ if (!reqData) return;
380
+
381
+ const entry = {
382
+ request: {
383
+ url: reqData.url,
384
+ method: reqData.method,
385
+ headers: reqData.headers,
386
+ postData: reqData.postData,
387
+ resourceType: reqData.resourceType
388
+ },
389
+ response: {
390
+ status: event.response.status,
391
+ headers: event.response.headers,
392
+ mimeType: event.response.mimeType
393
+ }
394
+ };
395
+
396
+ if (captureResponse) {
397
+ const p = client.send('Network.getResponseBody', {
398
+ requestId: event.requestId
399
+ }).then(({ body, base64Encoded }) => {
400
+ entry.response.body = base64Encoded
401
+ ? Buffer.from(body, 'base64').toString('utf-8')
402
+ : body;
403
+ }).catch(() => {
404
+ entry.response.body = null;
405
+ });
406
+ responseBodyPromises.push(p);
407
+ }
408
+
409
+ captured.push(entry);
410
+ pendingRequests.delete(event.requestId);
411
+ });
412
+
413
+ try {
414
+ await page.goto(targetUrl, {
415
+ waitUntil: 'domcontentloaded',
416
+ timeout
417
+ }).catch((err) => {
418
+ if (err && err.name === 'TimeoutError') return null;
419
+ throw err;
420
+ });
421
+
422
+ // Wait for late-firing requests (XHR, fetch, GraphQL, etc.)
423
+ if (captured.length < maxCaptures) {
424
+ await new Promise((r) => setTimeout(r, waitAfterLoad));
425
+ }
426
+
427
+ await Promise.all(responseBodyPromises);
428
+ } finally {
429
+ await client.detach();
430
+ await page.close();
431
+ }
432
+
433
+ return captured;
434
+ }
435
+ }
436
+
437
+ module.exports = ApiExtractor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "websession-api-client",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",
package/readme.md CHANGED
@@ -1,79 +1,79 @@
1
- # websession-api-client
2
-
3
- Call APIs using a browser-captured bearer token, or call public APIs without auth.
4
-
5
- ## Usage
6
-
7
- ```js
8
- const ApiExtractor = require('./index');
9
-
10
- (async () => {
11
- const client = new ApiExtractor();
12
- await client.init();
13
-
14
- // 1) Public API (no auth header sent)
15
- const publicData = await client.callApi('https://api.example.com/public', {
16
- auth: false
17
- });
18
-
19
- // 2) Set auth (captures a bearer token from site traffic)
20
- await client.setAuth('cookie_name', 'app.example.com', 'cookie_value');
21
-
22
- // 3) Authenticated API (explicit)
23
- const privateData = await client.callApi('https://api.example.com/private', {
24
- auth: true
25
- });
26
-
27
- // 4) Authenticated API with helper
28
- const privateData2 = await client.callAuthApi('https://api.example.com/private');
29
-
30
- // 5) POST JSON (auth optional)
31
- const created = await client.callApi('https://api.example.com/items', {
32
- method: 'POST',
33
- auth: true,
34
- json: { name: 'test' }
35
- });
36
-
37
- // 6) Browser-session request (useful for sites protected by Cloudflare/anti-bot checks)
38
- const protectedData = await client.callApi('https://www.example.com/api/data.json', {
39
- auth: false,
40
- transport: 'browser'
41
- });
42
-
43
- console.log({ publicData, privateData, privateData2, created, protectedData });
44
- })();
45
- ```
46
-
47
- ## `callApi(url, options)`
48
-
49
- Supports normal `fetch` options plus:
50
-
51
- - `auth`: `true` | `false` | `'auto'` (default)
52
- - `true`: require bearer token and send `Authorization`
53
- - `false`: never send `Authorization`
54
- - `'auto'`: send `Authorization` only if a bearer token is available
55
- - `json`: object to JSON.stringify into `body` and auto-set `Content-Type: application/json`
56
- - `transport`: `'node'` (default) | `'browser'`
57
- - `'node'`: regular Node.js `fetch`
58
- - `'browser'`: runs `fetch` inside the Puppeteer page session (uses browser cookies/session)
59
-
60
- All other `fetch` options (`method`, `headers`, `body`, etc.) are passed through.
61
-
62
- ## Cloudflare / Bot Protection Note
63
-
64
- If a public endpoint returns an HTML challenge page (403) instead of JSON, that is usually a bot-protection challenge, not an auth issue.
65
-
66
- Use browser transport:
67
-
68
- ```js
69
- const data = await client.callApi('https://www.artstation.com/users/yourname/projects.json', {
70
- auth: false,
71
- transport: 'browser'
72
- });
73
- ```
74
-
75
- If the site still challenges headless Chrome, start Puppeteer in a visible window and complete the check once:
76
-
77
- ```js
78
- await client.init({ headless: false });
79
- ```
1
+ # websession-api-client
2
+
3
+ Call APIs using a browser-captured bearer token, or call public APIs without auth.
4
+
5
+ ## Usage
6
+
7
+ ```js
8
+ const ApiExtractor = require('./index');
9
+
10
+ (async () => {
11
+ const client = new ApiExtractor();
12
+ await client.init();
13
+
14
+ // 1) Public API (no auth header sent)
15
+ const publicData = await client.callApi('https://api.example.com/public', {
16
+ auth: false
17
+ });
18
+
19
+ // 2) Set auth (captures a bearer token from site traffic)
20
+ await client.setAuth('cookie_name', 'app.example.com', 'cookie_value');
21
+
22
+ // 3) Authenticated API (explicit)
23
+ const privateData = await client.callApi('https://api.example.com/private', {
24
+ auth: true
25
+ });
26
+
27
+ // 4) Authenticated API with helper
28
+ const privateData2 = await client.callAuthApi('https://api.example.com/private');
29
+
30
+ // 5) POST JSON (auth optional)
31
+ const created = await client.callApi('https://api.example.com/items', {
32
+ method: 'POST',
33
+ auth: true,
34
+ json: { name: 'test' }
35
+ });
36
+
37
+ // 6) Browser-session request (useful for sites protected by Cloudflare/anti-bot checks)
38
+ const protectedData = await client.callApi('https://www.example.com/api/data.json', {
39
+ auth: false,
40
+ transport: 'browser'
41
+ });
42
+
43
+ console.log({ publicData, privateData, privateData2, created, protectedData });
44
+ })();
45
+ ```
46
+
47
+ ## `callApi(url, options)`
48
+
49
+ Supports normal `fetch` options plus:
50
+
51
+ - `auth`: `true` | `false` | `'auto'` (default)
52
+ - `true`: require bearer token and send `Authorization`
53
+ - `false`: never send `Authorization`
54
+ - `'auto'`: send `Authorization` only if a bearer token is available
55
+ - `json`: object to JSON.stringify into `body` and auto-set `Content-Type: application/json`
56
+ - `transport`: `'node'` (default) | `'browser'`
57
+ - `'node'`: regular Node.js `fetch`
58
+ - `'browser'`: runs `fetch` inside the Puppeteer page session (uses browser cookies/session)
59
+
60
+ All other `fetch` options (`method`, `headers`, `body`, etc.) are passed through.
61
+
62
+ ## Cloudflare / Bot Protection Note
63
+
64
+ If a public endpoint returns an HTML challenge page (403) instead of JSON, that is usually a bot-protection challenge, not an auth issue.
65
+
66
+ Use browser transport:
67
+
68
+ ```js
69
+ const data = await client.callApi('https://www.artstation.com/users/yourname/projects.json', {
70
+ auth: false,
71
+ transport: 'browser'
72
+ });
73
+ ```
74
+
75
+ If the site still challenges headless Chrome, start Puppeteer in a visible window and complete the check once:
76
+
77
+ ```js
78
+ await client.init({ headless: false });
79
+ ```