wreq-js 0.2.0 → 1.0.0

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/dist/wreq-js.js CHANGED
@@ -1,12 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WebSocket = void 0;
3
+ exports.RequestError = exports.WebSocket = exports.Session = exports.Response = exports.Headers = void 0;
4
+ exports.fetch = fetch;
5
+ exports.createSession = createSession;
6
+ exports.withSession = withSession;
4
7
  exports.request = request;
5
8
  exports.getProfiles = getProfiles;
6
9
  exports.get = get;
7
10
  exports.post = post;
8
11
  exports.websocket = websocket;
12
+ const node_crypto_1 = require("node:crypto");
13
+ const node_http_1 = require("node:http");
9
14
  const types_1 = require("./types");
15
+ Object.defineProperty(exports, "RequestError", { enumerable: true, get: function () { return types_1.RequestError; } });
10
16
  let nativeBinding;
11
17
  let cachedProfiles;
12
18
  function loadNativeBinding() {
@@ -55,44 +61,550 @@ const websocketFinalizer = typeof FinalizationRegistry === "function"
55
61
  void nativeBinding.websocketClose(connection).catch(() => undefined);
56
62
  })
57
63
  : undefined;
64
+ const DEFAULT_BROWSER = "chrome_142";
65
+ function generateSessionId() {
66
+ const cryptoGlobal = globalThis.crypto;
67
+ if (cryptoGlobal?.randomUUID) {
68
+ return cryptoGlobal.randomUUID();
69
+ }
70
+ return (0, node_crypto_1.randomBytes)(16).toString("hex");
71
+ }
72
+ function normalizeSessionOptions(options) {
73
+ const sessionId = options?.sessionId ?? generateSessionId();
74
+ const defaults = {
75
+ browser: options?.browser ?? DEFAULT_BROWSER,
76
+ };
77
+ if (options?.proxy !== undefined) {
78
+ defaults.proxy = options.proxy;
79
+ }
80
+ if (options?.timeout !== undefined) {
81
+ defaults.timeout = options.timeout;
82
+ }
83
+ return { sessionId, defaults };
84
+ }
85
+ function isIterable(value) {
86
+ return Boolean(value) && typeof value[Symbol.iterator] === "function";
87
+ }
88
+ function isPlainObject(value) {
89
+ if (typeof value !== "object" || value === null) {
90
+ return false;
91
+ }
92
+ const proto = Object.getPrototypeOf(value);
93
+ return proto === Object.prototype || proto === null;
94
+ }
95
+ function coerceHeaderValue(value) {
96
+ return String(value);
97
+ }
98
+ class Headers {
99
+ constructor(init) {
100
+ this.store = new Map();
101
+ if (init) {
102
+ this.applyInit(init);
103
+ }
104
+ }
105
+ applyInit(init) {
106
+ if (init instanceof Headers) {
107
+ for (const [name, value] of init) {
108
+ this.append(name, value);
109
+ }
110
+ return;
111
+ }
112
+ if (Array.isArray(init) || isIterable(init)) {
113
+ for (const tuple of init) {
114
+ if (!tuple) {
115
+ continue;
116
+ }
117
+ const [name, value] = tuple;
118
+ this.append(name, value);
119
+ }
120
+ return;
121
+ }
122
+ if (isPlainObject(init)) {
123
+ for (const [name, value] of Object.entries(init)) {
124
+ if (value === undefined || value === null) {
125
+ continue;
126
+ }
127
+ this.set(name, coerceHeaderValue(value));
128
+ }
129
+ }
130
+ }
131
+ normalizeName(name) {
132
+ if (typeof name !== "string") {
133
+ throw new TypeError("Header name must be a string");
134
+ }
135
+ const trimmed = name.trim();
136
+ if (!trimmed) {
137
+ throw new TypeError("Header name must not be empty");
138
+ }
139
+ return { key: trimmed.toLowerCase(), display: trimmed };
140
+ }
141
+ assertValue(value) {
142
+ if (value === undefined || value === null) {
143
+ throw new TypeError("Header value must not be null or undefined");
144
+ }
145
+ return coerceHeaderValue(value);
146
+ }
147
+ append(name, value) {
148
+ const normalized = this.normalizeName(name);
149
+ const existing = this.store.get(normalized.key);
150
+ const coercedValue = this.assertValue(value);
151
+ if (existing) {
152
+ existing.values.push(coercedValue);
153
+ return;
154
+ }
155
+ this.store.set(normalized.key, {
156
+ name: normalized.display,
157
+ values: [coercedValue],
158
+ });
159
+ }
160
+ set(name, value) {
161
+ const normalized = this.normalizeName(name);
162
+ const coercedValue = this.assertValue(value);
163
+ this.store.set(normalized.key, {
164
+ name: normalized.display,
165
+ values: [coercedValue],
166
+ });
167
+ }
168
+ get(name) {
169
+ const normalized = this.normalizeName(name);
170
+ const entry = this.store.get(normalized.key);
171
+ return entry ? entry.values.join(", ") : null;
172
+ }
173
+ has(name) {
174
+ const normalized = this.normalizeName(name);
175
+ return this.store.has(normalized.key);
176
+ }
177
+ delete(name) {
178
+ const normalized = this.normalizeName(name);
179
+ this.store.delete(normalized.key);
180
+ }
181
+ entries() {
182
+ return this[Symbol.iterator]();
183
+ }
184
+ *keys() {
185
+ for (const [name] of this) {
186
+ yield name;
187
+ }
188
+ }
189
+ *values() {
190
+ for (const [, value] of this) {
191
+ yield value;
192
+ }
193
+ }
194
+ forEach(callback, thisArg) {
195
+ for (const [name, value] of this) {
196
+ callback.call(thisArg, value, name, this);
197
+ }
198
+ }
199
+ [Symbol.iterator]() {
200
+ const generator = function* (store) {
201
+ for (const entry of store.values()) {
202
+ yield [entry.name, entry.values.join(", ")];
203
+ }
204
+ };
205
+ return generator(this.store);
206
+ }
207
+ toObject() {
208
+ const result = {};
209
+ for (const [name, value] of this) {
210
+ result[name] = value;
211
+ }
212
+ return result;
213
+ }
214
+ }
215
+ exports.Headers = Headers;
216
+ function cloneNativeResponse(payload) {
217
+ return {
218
+ status: payload.status,
219
+ headers: { ...payload.headers },
220
+ body: payload.body,
221
+ cookies: { ...payload.cookies },
222
+ url: payload.url,
223
+ };
224
+ }
225
+ class Response {
226
+ constructor(payload, requestUrl) {
227
+ this.type = "basic";
228
+ this.bodyUsed = false;
229
+ this.payload = cloneNativeResponse(payload);
230
+ this.requestUrl = requestUrl;
231
+ this.status = payload.status;
232
+ this.statusText = node_http_1.STATUS_CODES[payload.status] ?? "";
233
+ this.ok = this.status >= 200 && this.status < 300;
234
+ this.headers = new Headers(payload.headers);
235
+ this.url = payload.url;
236
+ this.redirected = this.url !== requestUrl;
237
+ this.cookies = { ...payload.cookies };
238
+ this.body = payload.body;
239
+ }
240
+ async json() {
241
+ const text = await this.text();
242
+ return JSON.parse(text);
243
+ }
244
+ async text() {
245
+ this.assertBodyAvailable();
246
+ this.bodyUsed = true;
247
+ return this.body;
248
+ }
249
+ clone() {
250
+ if (this.bodyUsed) {
251
+ throw new TypeError("Cannot clone a Response whose body is already used");
252
+ }
253
+ return new Response(cloneNativeResponse(this.payload), this.requestUrl);
254
+ }
255
+ assertBodyAvailable() {
256
+ if (this.bodyUsed) {
257
+ throw new TypeError("Response body is already used");
258
+ }
259
+ }
260
+ }
261
+ exports.Response = Response;
262
+ class Session {
263
+ constructor(id, defaults) {
264
+ this.disposed = false;
265
+ this.id = id;
266
+ this.defaults = defaults;
267
+ }
268
+ get closed() {
269
+ return this.disposed;
270
+ }
271
+ ensureActive() {
272
+ if (this.disposed) {
273
+ throw new types_1.RequestError("Session has been closed");
274
+ }
275
+ }
276
+ enforceBrowser(browser) {
277
+ const resolved = browser ?? this.defaults.browser;
278
+ if (resolved !== this.defaults.browser) {
279
+ throw new types_1.RequestError("Session browser cannot be changed after creation");
280
+ }
281
+ return resolved;
282
+ }
283
+ enforceProxy(proxy) {
284
+ if (proxy === undefined) {
285
+ return this.defaults.proxy;
286
+ }
287
+ if ((this.defaults.proxy ?? null) !== (proxy ?? null)) {
288
+ throw new types_1.RequestError("Session proxy cannot be changed after creation");
289
+ }
290
+ return proxy;
291
+ }
292
+ async fetch(input, init) {
293
+ this.ensureActive();
294
+ const config = {
295
+ ...(init ?? {}),
296
+ session: this,
297
+ cookieMode: "session",
298
+ };
299
+ config.browser = this.enforceBrowser(config.browser);
300
+ const proxy = this.enforceProxy(config.proxy);
301
+ if (proxy !== undefined || config.proxy !== undefined) {
302
+ if (proxy === undefined) {
303
+ delete config.proxy;
304
+ }
305
+ else {
306
+ config.proxy = proxy;
307
+ }
308
+ }
309
+ if (config.timeout === undefined && this.defaults.timeout !== undefined) {
310
+ config.timeout = this.defaults.timeout;
311
+ }
312
+ return fetch(input, config);
313
+ }
314
+ async clearCookies() {
315
+ this.ensureActive();
316
+ try {
317
+ nativeBinding.clearSession(this.id);
318
+ }
319
+ catch (error) {
320
+ throw new types_1.RequestError(String(error));
321
+ }
322
+ }
323
+ async close() {
324
+ if (this.disposed) {
325
+ return;
326
+ }
327
+ this.disposed = true;
328
+ try {
329
+ nativeBinding.dropSession(this.id);
330
+ }
331
+ catch (error) {
332
+ throw new types_1.RequestError(String(error));
333
+ }
334
+ }
335
+ }
336
+ exports.Session = Session;
337
+ function resolveSessionContext(config) {
338
+ const requestedMode = config.cookieMode ?? "ephemeral";
339
+ const sessionCandidate = config.session;
340
+ const providedSessionId = typeof config.sessionId === "string" ? config.sessionId.trim() : undefined;
341
+ if (sessionCandidate && providedSessionId) {
342
+ throw new types_1.RequestError("Provide either `session` or `sessionId`, not both.");
343
+ }
344
+ if (sessionCandidate) {
345
+ if (!(sessionCandidate instanceof Session)) {
346
+ throw new types_1.RequestError("`session` must be created via createSession()");
347
+ }
348
+ if (sessionCandidate.closed) {
349
+ throw new types_1.RequestError("Session has been closed");
350
+ }
351
+ return {
352
+ sessionId: sessionCandidate.id,
353
+ cookieMode: "session",
354
+ dropAfterRequest: false,
355
+ };
356
+ }
357
+ if (providedSessionId) {
358
+ if (!providedSessionId) {
359
+ throw new types_1.RequestError("sessionId must not be empty");
360
+ }
361
+ if (requestedMode === "ephemeral") {
362
+ throw new types_1.RequestError("cookieMode 'ephemeral' cannot be combined with sessionId");
363
+ }
364
+ return {
365
+ sessionId: providedSessionId,
366
+ cookieMode: "session",
367
+ dropAfterRequest: false,
368
+ };
369
+ }
370
+ if (requestedMode === "session") {
371
+ throw new types_1.RequestError("cookieMode 'session' requires a session or sessionId");
372
+ }
373
+ return {
374
+ sessionId: generateSessionId(),
375
+ cookieMode: "ephemeral",
376
+ dropAfterRequest: true,
377
+ };
378
+ }
379
+ function createAbortError(reason) {
380
+ const fallbackMessage = typeof reason === "string" ? reason : "The operation was aborted";
381
+ if (typeof DOMException !== "undefined" && reason instanceof DOMException) {
382
+ return reason.name === "AbortError" ? reason : new DOMException(reason.message || fallbackMessage, "AbortError");
383
+ }
384
+ if (reason instanceof Error) {
385
+ reason.name = "AbortError";
386
+ return reason;
387
+ }
388
+ if (typeof DOMException !== "undefined") {
389
+ return new DOMException(fallbackMessage, "AbortError");
390
+ }
391
+ const error = new Error(fallbackMessage);
392
+ error.name = "AbortError";
393
+ return error;
394
+ }
395
+ function isAbortError(error) {
396
+ return Boolean(error) && typeof error.name === "string" && error.name === "AbortError";
397
+ }
398
+ function setupAbort(signal) {
399
+ if (!signal) {
400
+ return null;
401
+ }
402
+ if (signal.aborted) {
403
+ throw createAbortError(signal.reason);
404
+ }
405
+ let onAbort;
406
+ const promise = new Promise((_, reject) => {
407
+ onAbort = () => {
408
+ reject(createAbortError(signal.reason));
409
+ };
410
+ signal.addEventListener("abort", onAbort, { once: true });
411
+ });
412
+ const cleanup = () => {
413
+ if (onAbort) {
414
+ signal.removeEventListener("abort", onAbort);
415
+ onAbort = undefined;
416
+ }
417
+ };
418
+ return { promise, cleanup };
419
+ }
420
+ function normalizeUrlInput(input) {
421
+ const value = typeof input === "string" ? input : input.toString();
422
+ if (!value) {
423
+ throw new types_1.RequestError("URL is required");
424
+ }
425
+ try {
426
+ return new URL(value).toString();
427
+ }
428
+ catch {
429
+ throw new types_1.RequestError(`Invalid URL: ${value}`);
430
+ }
431
+ }
432
+ function validateRedirectMode(mode) {
433
+ if (!mode || mode === "follow") {
434
+ return;
435
+ }
436
+ throw new types_1.RequestError(`Redirect mode '${mode}' is not supported`);
437
+ }
438
+ function serializeBody(body) {
439
+ if (body === null || body === undefined) {
440
+ return undefined;
441
+ }
442
+ if (typeof body === "string") {
443
+ return body;
444
+ }
445
+ if (Buffer.isBuffer(body)) {
446
+ return body.toString();
447
+ }
448
+ if (body instanceof URLSearchParams) {
449
+ return body.toString();
450
+ }
451
+ if (body instanceof ArrayBuffer) {
452
+ return Buffer.from(body).toString();
453
+ }
454
+ if (ArrayBuffer.isView(body)) {
455
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString();
456
+ }
457
+ throw new TypeError("Unsupported body type; expected string, Buffer, ArrayBuffer, or URLSearchParams");
458
+ }
459
+ const SUPPORTED_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"];
460
+ function ensureMethod(method) {
461
+ const normalized = method?.trim().toUpperCase();
462
+ return normalized && normalized.length > 0 ? normalized : "GET";
463
+ }
464
+ function assertSupportedMethod(method) {
465
+ if (!SUPPORTED_METHODS.includes(method)) {
466
+ throw new types_1.RequestError(`Unsupported HTTP method: ${method}`);
467
+ }
468
+ }
469
+ function ensureBodyAllowed(method, body) {
470
+ if (!body) {
471
+ return;
472
+ }
473
+ if (method === "GET" || method === "HEAD") {
474
+ throw new types_1.RequestError(`Request with ${method} method cannot have a body`);
475
+ }
476
+ }
477
+ function validateBrowserProfile(browser) {
478
+ if (!browser) {
479
+ return;
480
+ }
481
+ const profiles = getProfiles();
482
+ if (!profiles.includes(browser)) {
483
+ throw new types_1.RequestError(`Invalid browser profile: ${browser}. Available profiles: ${profiles.join(", ")}`);
484
+ }
485
+ }
486
+ async function dispatchRequest(options, requestUrl, signal) {
487
+ const abortHandler = setupAbort(signal);
488
+ const nativePromise = nativeBinding.request(options);
489
+ const pending = abortHandler ? Promise.race([nativePromise, abortHandler.promise]) : nativePromise;
490
+ let payload;
491
+ try {
492
+ payload = (await pending);
493
+ }
494
+ catch (error) {
495
+ if (isAbortError(error)) {
496
+ throw error;
497
+ }
498
+ if (error instanceof types_1.RequestError) {
499
+ throw error;
500
+ }
501
+ throw new types_1.RequestError(String(error));
502
+ }
503
+ finally {
504
+ abortHandler?.cleanup();
505
+ }
506
+ return new Response(payload, requestUrl);
507
+ }
58
508
  /**
59
- * Make an HTTP request with browser impersonation
60
- *
61
- * @param options - Request options
62
- * @returns Promise that resolves to the response
63
- *
64
- * @example
65
- * ```typescript
66
- * import { request } from 'wreq-js';
509
+ * Fetch-compatible entry point that adds browser impersonation controls.
67
510
  *
68
- * const response = await request({
69
- * url: 'https://example.com/api',
70
- * browser: 'chrome_137',
71
- * headers: {
72
- * 'Custom-Header': 'value'
73
- * }
74
- * });
75
- *
76
- * console.log(response.status); // 200
77
- * console.log(response.body); // Response body
78
- * ```
511
+ * @param input - Request URL (string or URL instance)
512
+ * @param init - Fetch-compatible init options
79
513
  */
80
- async function request(options) {
81
- if (!options.url) {
82
- throw new types_1.RequestError("URL is required");
514
+ async function fetch(input, init) {
515
+ const url = normalizeUrlInput(input);
516
+ const config = init ?? {};
517
+ const sessionContext = resolveSessionContext(config);
518
+ validateRedirectMode(config.redirect);
519
+ validateBrowserProfile(config.browser);
520
+ const headers = new Headers(config.headers);
521
+ const method = ensureMethod(config.method);
522
+ assertSupportedMethod(method);
523
+ const body = serializeBody(config.body ?? null);
524
+ ensureBodyAllowed(method, body);
525
+ const headerRecord = headers.toObject();
526
+ const hasHeaders = Object.keys(headerRecord).length > 0;
527
+ const requestOptions = {
528
+ url,
529
+ method,
530
+ ...(config.browser && { browser: config.browser }),
531
+ ...(hasHeaders && { headers: headerRecord }),
532
+ ...(body !== undefined && { body }),
533
+ ...(config.proxy !== undefined && { proxy: config.proxy }),
534
+ ...(config.timeout !== undefined && { timeout: config.timeout }),
535
+ sessionId: sessionContext.sessionId,
536
+ ephemeral: sessionContext.dropAfterRequest,
537
+ };
538
+ try {
539
+ return await dispatchRequest(requestOptions, url, config.signal ?? null);
83
540
  }
84
- if (options.browser) {
85
- const profiles = getProfiles();
86
- if (!profiles.includes(options.browser)) {
87
- throw new types_1.RequestError(`Invalid browser profile: ${options.browser}. Available profiles: ${profiles.join(", ")}`);
541
+ finally {
542
+ if (sessionContext.dropAfterRequest) {
543
+ try {
544
+ nativeBinding.dropSession(sessionContext.sessionId);
545
+ }
546
+ catch {
547
+ // ignore cleanup errors for ephemeral sessions
548
+ }
88
549
  }
89
550
  }
551
+ }
552
+ async function createSession(options) {
553
+ const { sessionId, defaults } = normalizeSessionOptions(options);
554
+ validateBrowserProfile(defaults.browser);
555
+ let createdId;
90
556
  try {
91
- return await nativeBinding.request(options);
557
+ createdId = nativeBinding.createSession({
558
+ sessionId,
559
+ browser: defaults.browser,
560
+ ...(defaults.proxy !== undefined && { proxy: defaults.proxy }),
561
+ });
92
562
  }
93
563
  catch (error) {
94
564
  throw new types_1.RequestError(String(error));
95
565
  }
566
+ return new Session(createdId, defaults);
567
+ }
568
+ async function withSession(fn, options) {
569
+ const session = await createSession(options);
570
+ try {
571
+ return await fn(session);
572
+ }
573
+ finally {
574
+ await session.close();
575
+ }
576
+ }
577
+ /**
578
+ * @deprecated Use {@link fetch} instead.
579
+ */
580
+ async function request(options) {
581
+ if (!options.url) {
582
+ throw new types_1.RequestError("URL is required");
583
+ }
584
+ const { url, ...rest } = options;
585
+ const init = {};
586
+ if (rest.method !== undefined) {
587
+ init.method = rest.method;
588
+ }
589
+ if (rest.headers !== undefined) {
590
+ init.headers = rest.headers;
591
+ }
592
+ if (rest.body !== undefined) {
593
+ init.body = rest.body;
594
+ }
595
+ if (rest.browser !== undefined) {
596
+ init.browser = rest.browser;
597
+ }
598
+ if (rest.proxy !== undefined) {
599
+ init.proxy = rest.proxy;
600
+ }
601
+ if (rest.timeout !== undefined) {
602
+ init.timeout = rest.timeout;
603
+ }
604
+ if (rest.sessionId !== undefined) {
605
+ init.sessionId = rest.sessionId;
606
+ }
607
+ return fetch(url, init);
96
608
  }
97
609
  /**
98
610
  * Get list of available browser profiles
@@ -114,43 +626,21 @@ function getProfiles() {
114
626
  return cachedProfiles;
115
627
  }
116
628
  /**
117
- * Convenience function for GET requests
118
- *
119
- * @param url - URL to request
120
- * @param options - Additional request options
121
- * @returns Promise that resolves to the response
122
- *
123
- * @example
124
- * ```typescript
125
- * import { get } from 'wreq-js';
126
- *
127
- * const response = await get('https://example.com/api');
128
- * ```
629
+ * Convenience helper for GET requests using {@link fetch}.
129
630
  */
130
- async function get(url, options) {
131
- return request({ ...options, url, method: "GET" });
631
+ async function get(url, init) {
632
+ return fetch(url, { ...(init ?? {}), method: "GET" });
132
633
  }
133
634
  /**
134
- * Convenience function for POST requests
135
- *
136
- * @param url - URL to request
137
- * @param body - Request body
138
- * @param options - Additional request options
139
- * @returns Promise that resolves to the response
140
- *
141
- * @example
142
- * ```typescript
143
- * import { post } from 'wreq-js';
144
- *
145
- * const response = await post(
146
- * 'https://example.com/api',
147
- * JSON.stringify({ foo: 'bar' }),
148
- * { headers: { 'Content-Type': 'application/json' } }
149
- * );
150
- * ```
635
+ * Convenience helper for POST requests using {@link fetch}.
151
636
  */
152
- async function post(url, body, options) {
153
- return request({ ...options, url, method: "POST", ...(body !== undefined && { body }) });
637
+ async function post(url, body, init) {
638
+ const config = {
639
+ ...(init ?? {}),
640
+ method: "POST",
641
+ ...(body !== undefined ? { body } : {}),
642
+ };
643
+ return fetch(url, config);
154
644
  }
155
645
  /**
156
646
  * WebSocket connection class
@@ -161,7 +651,7 @@ async function post(url, body, options) {
161
651
  *
162
652
  * const ws = await websocket({
163
653
  * url: 'wss://echo.websocket.org',
164
- * browser: 'chrome_137',
654
+ * browser: 'chrome_142',
165
655
  * onMessage: (data) => {
166
656
  * console.log('Received:', data);
167
657
  * },
@@ -246,7 +736,7 @@ async function websocket(options) {
246
736
  try {
247
737
  const connection = await nativeBinding.websocketConnect({
248
738
  url: options.url,
249
- browser: options.browser || "chrome_137",
739
+ browser: options.browser || DEFAULT_BROWSER,
250
740
  headers: options.headers || {},
251
741
  ...(options.proxy !== undefined && { proxy: options.proxy }),
252
742
  onMessage: options.onMessage,
@@ -260,11 +750,17 @@ async function websocket(options) {
260
750
  }
261
751
  }
262
752
  exports.default = {
753
+ fetch,
263
754
  request,
264
755
  get,
265
756
  post,
266
757
  getProfiles,
758
+ createSession,
759
+ withSession,
267
760
  websocket,
268
761
  WebSocket,
762
+ Headers,
763
+ Response,
764
+ Session,
269
765
  };
270
766
  //# sourceMappingURL=wreq-js.js.map