@emircansahin/ghostfetch 0.1.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.
@@ -0,0 +1,167 @@
1
+ import { NoProxyAvailableError } from './errors';
2
+ /** Failures within this window (ms) from different requests count as 1. */
3
+ const DEDUP_WINDOW = 1000;
4
+ /** How often to poll for an available proxy when waiting (ms). */
5
+ const WAIT_POLL_INTERVAL = 2000;
6
+ const DEFAULT_BAN = {
7
+ maxFailures: 3,
8
+ duration: 60 * 60 * 1000, // 1 hour
9
+ };
10
+ export class ProxyManager {
11
+ constructor(proxies, banConfig) {
12
+ this.proxies = [];
13
+ this.banMap = new Map();
14
+ this.countryMap = new Map(); // proxy → country code
15
+ this.proxies = [...proxies];
16
+ this.banConfig = banConfig === false ? false : { ...DEFAULT_BAN, ...banConfig };
17
+ }
18
+ /**
19
+ * Get a random non-banned proxy.
20
+ * Supports exclude (skip last failed proxy) and country filter.
21
+ * Returns null if none available.
22
+ */
23
+ getProxy(opts) {
24
+ // Backwards compat: allow passing just exclude string
25
+ const { exclude, country } = typeof opts === 'string' || opts === null || opts === undefined
26
+ ? { exclude: opts ?? undefined, country: undefined }
27
+ : opts;
28
+ let available = this.getAvailableProxies();
29
+ // Filter by country if requested
30
+ if (country) {
31
+ const upper = country.toUpperCase();
32
+ available = available.filter((p) => this.countryMap.get(p) === upper);
33
+ }
34
+ const candidates = exclude
35
+ ? available.filter((p) => p !== exclude)
36
+ : available;
37
+ // If excluding leaves nothing but there are available proxies, fall back
38
+ const pool = candidates.length > 0 ? candidates : available;
39
+ if (pool.length === 0)
40
+ return null;
41
+ return pool[Math.floor(Math.random() * pool.length)];
42
+ }
43
+ /**
44
+ * Wait until a proxy becomes available (bans expire or list is refreshed).
45
+ * Resolves with the proxy string. Supports country filter.
46
+ *
47
+ * Automatically calculates timeout from the earliest ban expiry.
48
+ * If no ban will ever expire (shouldn't happen), times out after 5 minutes.
49
+ */
50
+ waitForProxy(opts) {
51
+ const immediate = this.getProxy(opts);
52
+ if (immediate)
53
+ return Promise.resolve(immediate);
54
+ // Calculate max wait from earliest ban expiry + buffer
55
+ const maxWait = this.getEarliestBanExpiry() ?? 5 * 60 * 1000;
56
+ return new Promise((resolve, reject) => {
57
+ const timeout = setTimeout(() => {
58
+ clearInterval(interval);
59
+ reject(new NoProxyAvailableError());
60
+ }, maxWait);
61
+ const interval = setInterval(() => {
62
+ const proxy = this.getProxy(opts);
63
+ if (proxy) {
64
+ clearTimeout(timeout);
65
+ clearInterval(interval);
66
+ resolve(proxy);
67
+ }
68
+ }, WAIT_POLL_INTERVAL);
69
+ });
70
+ }
71
+ /** Get ms until the earliest ban expires, or null if no active bans. */
72
+ getEarliestBanExpiry() {
73
+ if (this.banConfig === false)
74
+ return null;
75
+ const now = Date.now();
76
+ let earliest = Infinity;
77
+ for (const [, entry] of this.banMap) {
78
+ if (!entry.bannedAt)
79
+ continue;
80
+ const expiresAt = entry.bannedAt + this.banConfig.duration;
81
+ const remaining = expiresAt - now;
82
+ if (remaining > 0 && remaining < earliest) {
83
+ earliest = remaining;
84
+ }
85
+ }
86
+ // Add 1s buffer so the ban is definitely expired when we check
87
+ return earliest === Infinity ? null : earliest + 1000;
88
+ }
89
+ /** Get all currently available (non-banned) proxies. */
90
+ getAvailableProxies() {
91
+ if (this.banConfig === false)
92
+ return [...this.proxies];
93
+ const now = Date.now();
94
+ const duration = this.banConfig.duration;
95
+ return this.proxies.filter((proxy) => {
96
+ const ban = this.banMap.get(proxy);
97
+ if (!ban)
98
+ return true;
99
+ if (!ban.bannedAt)
100
+ return true;
101
+ if (now - ban.bannedAt >= duration) {
102
+ this.banMap.delete(proxy);
103
+ return true;
104
+ }
105
+ return false;
106
+ });
107
+ }
108
+ /**
109
+ * Report a proxy failure. Returns true if the proxy got banned.
110
+ *
111
+ * Concurrent dedup: if the last failure was within DEDUP_WINDOW ms,
112
+ * this call is ignored (multiple parallel requests failing at the same
113
+ * moment count as a single failure).
114
+ */
115
+ reportFailure(proxy) {
116
+ if (this.banConfig === false)
117
+ return false;
118
+ const now = Date.now();
119
+ const entry = this.banMap.get(proxy);
120
+ if (entry && (now - entry.lastFailure) < DEDUP_WINDOW) {
121
+ // Check if actually still banned (not expired)
122
+ return entry.bannedAt > 0 && (now - entry.bannedAt) < this.banConfig.duration;
123
+ }
124
+ const failCount = (entry?.failCount ?? 0) + 1;
125
+ if (failCount >= this.banConfig.maxFailures) {
126
+ this.banMap.set(proxy, { bannedAt: now, failCount, lastFailure: now });
127
+ return true;
128
+ }
129
+ this.banMap.set(proxy, { bannedAt: 0, failCount, lastFailure: now });
130
+ return false;
131
+ }
132
+ /** Report a proxy success — resets its fail count. */
133
+ reportSuccess(proxy) {
134
+ this.banMap.delete(proxy);
135
+ }
136
+ /** Replace the proxy list and clear all bans + country data. */
137
+ replaceProxies(proxies) {
138
+ this.proxies = [...proxies];
139
+ this.banMap.clear();
140
+ this.countryMap.clear();
141
+ }
142
+ /** Set country for a proxy. */
143
+ setCountry(proxy, country) {
144
+ this.countryMap.set(proxy, country.toUpperCase());
145
+ }
146
+ /** Get country for a proxy (or undefined if not resolved). */
147
+ getCountry(proxy) {
148
+ return this.countryMap.get(proxy);
149
+ }
150
+ /** Get all proxies for a specific country. */
151
+ getProxiesByCountry(country) {
152
+ const upper = country.toUpperCase();
153
+ return this.proxies.filter((p) => this.countryMap.get(p) === upper);
154
+ }
155
+ /** Get total proxy count. */
156
+ get total() {
157
+ return this.proxies.length;
158
+ }
159
+ /** Get available (non-banned) proxy count. */
160
+ get available() {
161
+ return this.getAvailableProxies().length;
162
+ }
163
+ /** Get banned proxy count. */
164
+ get banned() {
165
+ return this.total - this.available;
166
+ }
167
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { ErrorType, GhostFetchResponse, Interceptor, InterceptorAction } from './types';
2
+ /**
3
+ * Classify an error as proxy, server, or ambiguous.
4
+ *
5
+ * - proxy: request definitely never reached the server (DNS fail, connection refused, etc.)
6
+ * - server: an HTTP response was received — the proxy worked fine
7
+ * - ambiguous: could be either (timeout, connection reset) — proxy should NOT be penalized
8
+ */
9
+ export declare function classifyError(error: unknown): ErrorType;
10
+ /**
11
+ * Check if a successful response is actually a Cloudflare JS challenge.
12
+ */
13
+ export declare function isCloudflareChallenge(response: GhostFetchResponse): boolean;
14
+ export interface InterceptorResult {
15
+ /** Whether an interceptor matched this URL */
16
+ matched: boolean;
17
+ /** The action returned by check(), or null if not matched */
18
+ action: InterceptorAction;
19
+ /** The interceptor that matched (if any) */
20
+ interceptor?: Interceptor;
21
+ }
22
+ /**
23
+ * Run interceptors against a response.
24
+ *
25
+ * First interceptor whose `match` returns true takes ownership.
26
+ * Its `check` result determines the action. Default status handling
27
+ * is bypassed whenever an interceptor matches (even if check returns null).
28
+ */
29
+ export declare function checkInterceptors(url: string, response: GhostFetchResponse, interceptors: Interceptor[]): InterceptorResult;
30
+ /**
31
+ * Check if a response status code should trigger a default retry.
32
+ * Only called when no interceptor matched the URL.
33
+ * Returns the error type if retry should happen, or null if response is fine.
34
+ */
35
+ export declare function checkDefaultRetryStatus(status: number): ErrorType | null;
36
+ //# sourceMappingURL=classifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../src/classifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAoDxF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAiCvD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAK3E;AAED,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,6DAA6D;IAC7D,MAAM,EAAE,iBAAiB,CAAC;IAC1B,4CAA4C;IAC5C,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,kBAAkB,EAC5B,YAAY,EAAE,WAAW,EAAE,GAC1B,iBAAiB,CASnB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAExE"}
@@ -0,0 +1,68 @@
1
+ import { GhostFetchConfig, GhostFetchResponse, HealthCheckResult, Interceptor, RequestOptions, HttpMethod } from './types';
2
+ export declare class GhostFetch {
3
+ private cycleTLS;
4
+ private initPromise;
5
+ private proxyManager;
6
+ private interceptors;
7
+ private config;
8
+ private retryDefaults;
9
+ private refreshTimer;
10
+ private refreshing;
11
+ private healthCheckPromise;
12
+ constructor(config?: GhostFetchConfig);
13
+ /**
14
+ * Wait until the initial health check is complete.
15
+ * Returns health check results with proxy details and country info.
16
+ * Requests automatically wait for this internally.
17
+ *
18
+ * @example
19
+ * const result = await client.ready();
20
+ * console.log(result);
21
+ * // {
22
+ * // total: 10, healthy: 8, dead: 2,
23
+ * // countries: { US: 3, DE: 5 },
24
+ * // proxies: { 'http://...@host:8001': 'US', 'http://...@host:8002': 'DE', ... }
25
+ * // }
26
+ */
27
+ ready(): Promise<HealthCheckResult>;
28
+ /** Lazy-initialize CycleTLS instance. */
29
+ private ensureClient;
30
+ /** Add a custom interceptor for site-specific error handling. */
31
+ addInterceptor(interceptor: Interceptor): void;
32
+ /** Remove an interceptor by name. */
33
+ removeInterceptor(name: string): void;
34
+ /** Manually refresh the proxy list via the onProxyRefresh callback. */
35
+ refreshProxies(): Promise<void>;
36
+ /** Get proxy manager stats. */
37
+ get stats(): {
38
+ totalProxies: number;
39
+ availableProxies: number;
40
+ bannedProxies: number;
41
+ };
42
+ get(url: string, options?: RequestOptions): Promise<GhostFetchResponse>;
43
+ post(url: string, options?: RequestOptions): Promise<GhostFetchResponse>;
44
+ put(url: string, options?: RequestOptions): Promise<GhostFetchResponse>;
45
+ delete(url: string, options?: RequestOptions): Promise<GhostFetchResponse>;
46
+ patch(url: string, options?: RequestOptions): Promise<GhostFetchResponse>;
47
+ head(url: string, options?: RequestOptions): Promise<GhostFetchResponse>;
48
+ request(method: HttpMethod, url: string, options?: RequestOptions): Promise<GhostFetchResponse>;
49
+ /**
50
+ * Pick a proxy based on forceProxy and country settings.
51
+ * - If forceProxy: wait until a proxy is available (blocks)
52
+ * - If !forceProxy: return null if none available (proceed without proxy)
53
+ */
54
+ private pickProxy;
55
+ /** Handle an interceptor action (retry/ban/skip). Returns 'return' to send response, or { error } to continue retry loop. */
56
+ private handleInterceptorAction;
57
+ private executeRequest;
58
+ /**
59
+ * Health check + country resolution for proxies.
60
+ * Each proxy gets up to 3 attempts (0s, +15s, +60s) to reach ipinfo.io.
61
+ * Healthy proxies are added to the pool with their country code.
62
+ * Failed proxies are discarded.
63
+ */
64
+ private healthCheckProxies;
65
+ /** Gracefully shut down the CycleTLS instance and clear timers. */
66
+ destroy(): Promise<void>;
67
+ }
68
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,cAAc,EAEd,UAAU,EACX,MAAM,SAAS,CAAC;AAQjB,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,kBAAkB,CAA2C;gBAEzD,MAAM,GAAE,gBAAqB;IAoBzC;;;;;;;;;;;;;OAaG;IACG,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAKzC,yCAAyC;YAC3B,YAAY;IAa1B,iEAAiE;IACjE,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAI9C,qCAAqC;IACrC,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIrC,uEAAuE;IACjE,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBrC,+BAA+B;IAC/B,IAAI,KAAK;;;;MAMR;IAIK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIvE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIxE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIvE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAI1E,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIzE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAMxE,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAoHrG;;;;OAIG;YACW,SAAS;IA+BvB,6HAA6H;IAC7H,OAAO,CAAC,uBAAuB;YAqCjB,cAAc;IAyD5B;;;;;OAKG;YACW,kBAAkB;IA6DhC,mEAAmE;IAC7D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAgB/B"}
@@ -0,0 +1,21 @@
1
+ import { ErrorType, GhostFetchError } from './types';
2
+ export declare class GhostFetchRequestError extends Error implements GhostFetchError {
3
+ readonly type: ErrorType;
4
+ readonly status?: number;
5
+ readonly body?: string;
6
+ readonly proxy?: string;
7
+ readonly cause?: unknown;
8
+ constructor(opts: GhostFetchError);
9
+ }
10
+ export declare class CloudflareJSChallengeError extends GhostFetchRequestError {
11
+ constructor(url: string, proxy?: string);
12
+ }
13
+ export declare class NoProxyAvailableError extends Error {
14
+ constructor();
15
+ }
16
+ export declare class MaxRetriesExceededError extends Error {
17
+ readonly lastError: GhostFetchRequestError;
18
+ readonly attempts: number;
19
+ constructor(attempts: number, lastError: GhostFetchRequestError);
20
+ }
21
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAErD,qBAAa,sBAAuB,SAAQ,KAAM,YAAW,eAAe;IAC1E,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,IAAI,EAAE,eAAe;CASlC;AAED,qBAAa,0BAA2B,SAAQ,sBAAsB;gBACxD,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;CAQxC;AAED,qBAAa,qBAAsB,SAAQ,KAAK;;CAK/C;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,SAAS,EAAE,sBAAsB,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,sBAAsB;CAMhE"}
@@ -0,0 +1,5 @@
1
+ export { GhostFetch } from './client';
2
+ export { ProxyManager } from './proxy-manager';
3
+ export { GhostFetchRequestError, CloudflareJSChallengeError, NoProxyAvailableError, MaxRetriesExceededError, } from './errors';
4
+ export type { GhostFetchConfig, GhostFetchResponse, GhostFetchError, HealthCheckResult, Interceptor, InterceptorAction, RequestInterceptor, RequestOptions, RetryConfig, BanConfig, ErrorType, HttpMethod, } from './types';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EACL,sBAAsB,EACtB,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,WAAW,EACX,SAAS,EACT,SAAS,EACT,UAAU,GACX,MAAM,SAAS,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { BanConfig } from './types';
2
+ export interface GetProxyOptions {
3
+ exclude?: string | null;
4
+ country?: string;
5
+ }
6
+ export declare class ProxyManager {
7
+ private proxies;
8
+ private banMap;
9
+ private banConfig;
10
+ private countryMap;
11
+ constructor(proxies: string[], banConfig?: BanConfig | false);
12
+ /**
13
+ * Get a random non-banned proxy.
14
+ * Supports exclude (skip last failed proxy) and country filter.
15
+ * Returns null if none available.
16
+ */
17
+ getProxy(opts?: GetProxyOptions | string | null): string | null;
18
+ /**
19
+ * Wait until a proxy becomes available (bans expire or list is refreshed).
20
+ * Resolves with the proxy string. Supports country filter.
21
+ *
22
+ * Automatically calculates timeout from the earliest ban expiry.
23
+ * If no ban will ever expire (shouldn't happen), times out after 5 minutes.
24
+ */
25
+ waitForProxy(opts?: GetProxyOptions): Promise<string>;
26
+ /** Get ms until the earliest ban expires, or null if no active bans. */
27
+ private getEarliestBanExpiry;
28
+ /** Get all currently available (non-banned) proxies. */
29
+ getAvailableProxies(): string[];
30
+ /**
31
+ * Report a proxy failure. Returns true if the proxy got banned.
32
+ *
33
+ * Concurrent dedup: if the last failure was within DEDUP_WINDOW ms,
34
+ * this call is ignored (multiple parallel requests failing at the same
35
+ * moment count as a single failure).
36
+ */
37
+ reportFailure(proxy: string): boolean;
38
+ /** Report a proxy success — resets its fail count. */
39
+ reportSuccess(proxy: string): void;
40
+ /** Replace the proxy list and clear all bans + country data. */
41
+ replaceProxies(proxies: string[]): void;
42
+ /** Set country for a proxy. */
43
+ setCountry(proxy: string, country: string): void;
44
+ /** Get country for a proxy (or undefined if not resolved). */
45
+ getCountry(proxy: string): string | undefined;
46
+ /** Get all proxies for a specific country. */
47
+ getProxiesByCountry(country: string): string[];
48
+ /** Get total proxy count. */
49
+ get total(): number;
50
+ /** Get available (non-banned) proxy count. */
51
+ get available(): number;
52
+ /** Get banned proxy count. */
53
+ get banned(): number;
54
+ }
55
+ //# sourceMappingURL=proxy-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy-manager.d.ts","sourceRoot":"","sources":["../../src/proxy-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAoBpC,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,UAAU,CAA6B;gBAEnC,OAAO,EAAE,MAAM,EAAE,EAAE,SAAS,CAAC,EAAE,SAAS,GAAG,KAAK;IAK5D;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI;IAyB/D;;;;;;OAMG;IACH,YAAY,CAAC,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBrD,wEAAwE;IACxE,OAAO,CAAC,oBAAoB;IAmB5B,wDAAwD;IACxD,mBAAmB,IAAI,MAAM,EAAE;IAiB/B;;;;;;OAMG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAsBrC,sDAAsD;IACtD,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIlC,gEAAgE;IAChE,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI;IAMvC,+BAA+B;IAC/B,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAIhD,8DAA8D;IAC9D,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C,8CAA8C;IAC9C,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAK9C,6BAA6B;IAC7B,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,8CAA8C;IAC9C,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,8BAA8B;IAC9B,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF"}
@@ -0,0 +1,141 @@
1
+ export interface GhostFetchConfig {
2
+ /** List of proxy URLs in format http://user:pass@host:port */
3
+ proxies?: string[];
4
+ /** Request timeout in milliseconds (default: 30000) */
5
+ timeout?: number;
6
+ /** Retry configuration */
7
+ retry?: RetryConfig;
8
+ /** Proxy ban configuration. Set to false to disable banning entirely. */
9
+ ban?: BanConfig | false;
10
+ /**
11
+ * If true, requests will wait until a proxy becomes available when all are
12
+ * banned. If false (default), requests proceed without a proxy when none
13
+ * are available.
14
+ */
15
+ forceProxy?: boolean;
16
+ /**
17
+ * Called periodically to refresh the proxy list.
18
+ * When called, all bans are cleared and the returned list replaces the current one.
19
+ */
20
+ onProxyRefresh?: () => Promise<string[]> | string[];
21
+ /** Interval in ms to call onProxyRefresh (default: 3600000 = 1 hour) */
22
+ proxyRefreshInterval?: number;
23
+ /** Default headers for all requests */
24
+ headers?: Record<string, string>;
25
+ /** CycleTLS JA3 fingerprint (optional — CycleTLS picks a realistic default) */
26
+ ja3?: string;
27
+ /** User-Agent string (optional — CycleTLS picks a realistic default) */
28
+ userAgent?: string;
29
+ }
30
+ export interface RetryConfig {
31
+ /**
32
+ * Delay before each retry in ms. Array length = number of retries.
33
+ *
34
+ * @example [5000, 15000, 30000] → 3 retries: wait 5s, 15s, 30s
35
+ * @default [1000, 2000, 4000]
36
+ */
37
+ delays?: number[];
38
+ }
39
+ export interface BanConfig {
40
+ /** Number of consecutive failures before banning a proxy (default: 3) */
41
+ maxFailures?: number;
42
+ /** Ban duration in ms (default: 3600000 = 1 hour) */
43
+ duration?: number;
44
+ }
45
+ /**
46
+ * Interceptor action returned by check():
47
+ * - 'retry' — retry with different proxy, current proxy is not penalized
48
+ * - 'ban' — retry with different proxy AND penalize current proxy (fail counter +1)
49
+ * - 'skip' — return response as-is, no retry, bypass all default handling
50
+ * - null — interceptor doesn't care, fall through to default behavior
51
+ */
52
+ export type InterceptorAction = 'retry' | 'ban' | 'skip' | null;
53
+ export interface Interceptor {
54
+ /** Name for debugging purposes */
55
+ name?: string;
56
+ /** Return true if this interceptor applies to the given URL */
57
+ match: (url: string) => boolean;
58
+ /**
59
+ * Inspect the HTTP response and decide what to do.
60
+ *
61
+ * @returns
62
+ * - 'retry' — retry with different proxy (proxy is fine)
63
+ * - 'ban' — retry + penalize this proxy
64
+ * - 'skip' — return response directly, no retry, no default handling
65
+ * - null — interceptor doesn't care, default behavior applies
66
+ */
67
+ check: (response: GhostFetchResponse) => InterceptorAction;
68
+ }
69
+ /**
70
+ * Per-request interceptor — same as Interceptor but without `match` and `name`
71
+ * since it applies to the specific request URL.
72
+ */
73
+ export interface RequestInterceptor {
74
+ check: (response: GhostFetchResponse) => InterceptorAction;
75
+ }
76
+ export type ErrorType = 'proxy' | 'server' | 'ambiguous';
77
+ export interface GhostFetchResponse {
78
+ /** HTTP status code */
79
+ status: number;
80
+ /** Response headers */
81
+ headers: Record<string, string>;
82
+ /** Response body as string */
83
+ body: string;
84
+ /** Final URL (after redirects) */
85
+ url: string;
86
+ }
87
+ export interface GhostFetchError {
88
+ /** Error type classification: proxy, server, or ambiguous */
89
+ type: ErrorType;
90
+ /** Error message */
91
+ message: string;
92
+ /** HTTP status code (if response was received) */
93
+ status?: number;
94
+ /** Response body (if response was received) */
95
+ body?: string;
96
+ /** The proxy URL that was used */
97
+ proxy?: string;
98
+ /** Original error */
99
+ cause?: unknown;
100
+ }
101
+ export interface RequestOptions {
102
+ /** Additional headers for this request */
103
+ headers?: Record<string, string>;
104
+ /** Override timeout for this request */
105
+ timeout?: number;
106
+ /** Request body (for POST, PUT, PATCH) */
107
+ body?: string | Record<string, unknown>;
108
+ /** Force a specific proxy for this request */
109
+ proxy?: string;
110
+ /** Override retry config for this request */
111
+ retry?: RetryConfig;
112
+ /**
113
+ * Override forceProxy for this request.
114
+ * If true, wait until a proxy is available. If false, proceed without proxy.
115
+ * Defaults to instance-level forceProxy (which defaults to false).
116
+ */
117
+ forceProxy?: boolean;
118
+ /**
119
+ * Per-request interceptor. Takes priority over instance-level interceptors.
120
+ * No `match` needed — it applies to this request's URL automatically.
121
+ */
122
+ interceptor?: RequestInterceptor;
123
+ /**
124
+ * Require a proxy from this country (ISO 3166-1 alpha-2, e.g. 'US', 'DE').
125
+ */
126
+ country?: string;
127
+ }
128
+ export interface HealthCheckResult {
129
+ /** Total proxies that were tested */
130
+ total: number;
131
+ /** Number of healthy proxies added to the pool */
132
+ healthy: number;
133
+ /** Number of dead proxies that were discarded */
134
+ dead: number;
135
+ /** Country distribution (e.g. { US: 3, DE: 5 }) */
136
+ countries: Record<string, number>;
137
+ /** Per-proxy detail: proxy → country or null if no country resolved */
138
+ proxies: Record<string, string | null>;
139
+ }
140
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
141
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@emircansahin/ghostfetch",
3
+ "version": "0.1.0",
4
+ "description": "Resilient HTTP client with CycleTLS, proxy rotation, smart error classification, and per-site interceptors",
5
+ "main": "./dist/cjs/index.js",
6
+ "module": "./dist/esm/index.js",
7
+ "types": "./dist/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/esm/index.js",
11
+ "require": "./dist/cjs/index.js",
12
+ "types": "./dist/types/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > dist/esm/package.json && tsc -p tsconfig.cjs.json",
21
+ "build:esm": "tsc -p tsconfig.esm.json",
22
+ "build:cjs": "tsc -p tsconfig.cjs.json",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest"
25
+ },
26
+ "keywords": [
27
+ "fetch",
28
+ "proxy",
29
+ "cycletls",
30
+ "cloudflare",
31
+ "bypass",
32
+ "rotation",
33
+ "resilient",
34
+ "scraping"
35
+ ],
36
+ "author": "Emircan Sahin",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "cycletls": "^1.0.26",
40
+ "form-data": "^4.0.5"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "typescript": "^5.5.0",
45
+ "vitest": "^2.0.0"
46
+ }
47
+ }