spectrawl 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrawl",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "The unified web layer for AI agents. Search, browse, authenticate, act — one tool, self-hosted, free.",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
package/src/act/index.js CHANGED
@@ -53,22 +53,9 @@ class ActEngine {
53
53
  }
54
54
  }
55
55
 
56
- // Get auth for this platform/account
57
56
  const account = params.account
58
- if (account) {
59
- const cookies = await this.auth.getCookies(platform, account)
60
- if (!cookies) {
61
- return {
62
- success: false,
63
- error: 'auth_missing',
64
- detail: `No auth found for ${platform}/${account}.`,
65
- suggestion: `Run: spectrawl login ${platform} --account ${account}`
66
- }
67
- }
68
- params._cookies = cookies
69
- }
70
57
 
71
- // Check rate limits
58
+ // Check rate limits FIRST (no point checking auth if rate limited)
72
59
  const rateCheck = this.rateLimiter.check(platform, action, params)
73
60
  if (!rateCheck.allowed) {
74
61
  return {
@@ -94,6 +81,20 @@ class ActEngine {
94
81
  }
95
82
  }
96
83
 
84
+ // Get auth for this platform/account
85
+ if (account) {
86
+ const cookies = await this.auth.getCookies(platform, account)
87
+ if (!cookies) {
88
+ return {
89
+ success: false,
90
+ error: 'auth_missing',
91
+ detail: `No auth found for ${platform}/${account}.`,
92
+ suggestion: `Run: spectrawl login ${platform} --account ${account}`
93
+ }
94
+ }
95
+ params._cookies = cookies
96
+ }
97
+
97
98
  try {
98
99
  const result = await adapter.execute(action, params, {
99
100
  auth: this.auth,
package/src/cli.js CHANGED
@@ -22,6 +22,8 @@ async function main() {
22
22
  return mcp()
23
23
  case 'install-stealth':
24
24
  return installStealth()
25
+ case 'proxy':
26
+ return proxy()
25
27
  case 'version':
26
28
  console.log('spectrawl v0.1.0')
27
29
  return
@@ -116,6 +118,40 @@ async function mcp() {
116
118
  server.start()
117
119
  }
118
120
 
121
+ async function proxy() {
122
+ const { ProxyServer } = require('./proxy')
123
+ const { loadConfig } = require('./config')
124
+ const config = loadConfig()
125
+
126
+ const port = getFlag('--port') || config.proxy?.localPort || 8080
127
+ const upstreams = config.proxy?.upstreams || []
128
+
129
+ if (upstreams.length === 0) {
130
+ console.log('No upstream proxies configured.')
131
+ console.log('Add them to spectrawl.json:')
132
+ console.log(JSON.stringify({
133
+ proxy: {
134
+ localPort: 8080,
135
+ strategy: 'round-robin',
136
+ upstreams: [
137
+ { url: 'http://user:pass@proxy1.example.com:8080' },
138
+ { url: 'http://user:pass@proxy2.example.com:8080' }
139
+ ]
140
+ }
141
+ }, null, 2))
142
+ return
143
+ }
144
+
145
+ const server = new ProxyServer({
146
+ port: parseInt(port),
147
+ upstreams,
148
+ strategy: config.proxy?.strategy || 'round-robin',
149
+ maxFailures: config.proxy?.maxFailures || 5
150
+ })
151
+
152
+ server.start()
153
+ }
154
+
119
155
  async function installStealth() {
120
156
  const { install, isInstalled } = require('./browse/install-stealth')
121
157
  if (isInstalled()) {
@@ -138,6 +174,7 @@ Commands:
138
174
  serve [--port N] Start HTTP server
139
175
  mcp Start MCP server (stdio)
140
176
  install-stealth Download Camoufox anti-detect browser
177
+ proxy [--port N] Start rotating proxy server
141
178
  version Show version
142
179
 
143
180
  Examples:
@@ -0,0 +1,234 @@
1
+ const http = require('http')
2
+ const net = require('net')
3
+ const { URL } = require('url')
4
+
5
+ /**
6
+ * Spectrawl Proxy Server — rotating residential proxy gateway.
7
+ *
8
+ * One local endpoint (localhost:8080) that rotates through upstream
9
+ * residential proxies. Any tool on the server points here instead
10
+ * of configuring ProxyCheap/BrightData individually.
11
+ *
12
+ * Supports HTTP and HTTPS (CONNECT tunnel).
13
+ */
14
+ class ProxyServer {
15
+ constructor(config = {}) {
16
+ this.port = config.port || 8080
17
+ this.upstreams = (config.upstreams || []).map(u => ({
18
+ ...u,
19
+ failures: 0,
20
+ lastFailure: 0,
21
+ requests: 0
22
+ }))
23
+ this.strategy = config.strategy || 'round-robin' // round-robin | random | least-used
24
+ this.maxFailures = config.maxFailures || 5
25
+ this.failureCooldown = config.failureCooldown || 60000 // 1 min
26
+ this.server = null
27
+ this._index = 0
28
+ this._stats = { total: 0, success: 0, failed: 0, started: null }
29
+ }
30
+
31
+ /**
32
+ * Pick next upstream proxy.
33
+ */
34
+ _nextUpstream() {
35
+ const now = Date.now()
36
+ const healthy = this.upstreams.filter(u =>
37
+ u.failures < this.maxFailures || (now - u.lastFailure) > this.failureCooldown
38
+ )
39
+
40
+ if (healthy.length === 0) {
41
+ // Reset all if everything is dead
42
+ this.upstreams.forEach(u => { u.failures = 0 })
43
+ return this.upstreams[0] || null
44
+ }
45
+
46
+ switch (this.strategy) {
47
+ case 'random':
48
+ return healthy[Math.floor(Math.random() * healthy.length)]
49
+
50
+ case 'least-used':
51
+ return healthy.sort((a, b) => a.requests - b.requests)[0]
52
+
53
+ case 'round-robin':
54
+ default:
55
+ this._index = (this._index + 1) % healthy.length
56
+ return healthy[this._index]
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Parse upstream proxy URL into components.
62
+ */
63
+ _parseUpstream(upstream) {
64
+ if (!upstream) return null
65
+ const url = upstream.url || `http://${upstream.host}:${upstream.port}`
66
+ const parsed = new URL(url)
67
+ return {
68
+ host: parsed.hostname,
69
+ port: parseInt(parsed.port) || 80,
70
+ auth: upstream.username && upstream.password
71
+ ? Buffer.from(`${upstream.username}:${upstream.password}`).toString('base64')
72
+ : parsed.username && parsed.password
73
+ ? Buffer.from(`${parsed.username}:${parsed.password}`).toString('base64')
74
+ : null
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Handle HTTP requests (non-CONNECT).
80
+ */
81
+ _handleRequest(clientReq, clientRes) {
82
+ this._stats.total++
83
+ const upstream = this._nextUpstream()
84
+
85
+ if (!upstream) {
86
+ clientRes.writeHead(502, { 'Content-Type': 'application/json' })
87
+ clientRes.end(JSON.stringify({ error: 'No upstream proxies configured' }))
88
+ return
89
+ }
90
+
91
+ upstream.requests++
92
+ const proxy = this._parseUpstream(upstream)
93
+
94
+ const opts = {
95
+ hostname: proxy.host,
96
+ port: proxy.port,
97
+ path: clientReq.url,
98
+ method: clientReq.method,
99
+ headers: { ...clientReq.headers }
100
+ }
101
+
102
+ if (proxy.auth) {
103
+ opts.headers['Proxy-Authorization'] = `Basic ${proxy.auth}`
104
+ }
105
+
106
+ const proxyReq = http.request(opts, (proxyRes) => {
107
+ this._stats.success++
108
+ clientRes.writeHead(proxyRes.statusCode, proxyRes.headers)
109
+ proxyRes.pipe(clientRes)
110
+ })
111
+
112
+ proxyReq.on('error', (err) => {
113
+ this._stats.failed++
114
+ upstream.failures++
115
+ upstream.lastFailure = Date.now()
116
+ console.log(`Proxy upstream ${upstream.url || upstream.host} failed: ${err.message}`)
117
+ clientRes.writeHead(502, { 'Content-Type': 'application/json' })
118
+ clientRes.end(JSON.stringify({ error: 'Upstream proxy failed', detail: err.message }))
119
+ })
120
+
121
+ clientReq.pipe(proxyReq)
122
+ }
123
+
124
+ /**
125
+ * Handle HTTPS CONNECT tunnels.
126
+ */
127
+ _handleConnect(clientReq, clientSocket, head) {
128
+ this._stats.total++
129
+ const upstream = this._nextUpstream()
130
+
131
+ if (!upstream) {
132
+ clientSocket.write('HTTP/1.1 502 No upstream proxy\r\n\r\n')
133
+ clientSocket.destroy()
134
+ return
135
+ }
136
+
137
+ upstream.requests++
138
+ const proxy = this._parseUpstream(upstream)
139
+ const [targetHost, targetPort] = clientReq.url.split(':')
140
+
141
+ // Connect to upstream proxy
142
+ const proxySocket = net.connect(proxy.port, proxy.host, () => {
143
+ // Send CONNECT to upstream
144
+ let connectReq = `CONNECT ${clientReq.url} HTTP/1.1\r\nHost: ${clientReq.url}\r\n`
145
+ if (proxy.auth) {
146
+ connectReq += `Proxy-Authorization: Basic ${proxy.auth}\r\n`
147
+ }
148
+ connectReq += '\r\n'
149
+
150
+ proxySocket.write(connectReq)
151
+ })
152
+
153
+ proxySocket.once('data', (chunk) => {
154
+ const response = chunk.toString()
155
+ if (response.includes('200')) {
156
+ this._stats.success++
157
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
158
+ proxySocket.write(head)
159
+ proxySocket.pipe(clientSocket)
160
+ clientSocket.pipe(proxySocket)
161
+ } else {
162
+ this._stats.failed++
163
+ upstream.failures++
164
+ upstream.lastFailure = Date.now()
165
+ clientSocket.write('HTTP/1.1 502 Upstream Rejected\r\n\r\n')
166
+ clientSocket.destroy()
167
+ proxySocket.destroy()
168
+ }
169
+ })
170
+
171
+ proxySocket.on('error', (err) => {
172
+ this._stats.failed++
173
+ upstream.failures++
174
+ upstream.lastFailure = Date.now()
175
+ clientSocket.write('HTTP/1.1 502 Upstream Error\r\n\r\n')
176
+ clientSocket.destroy()
177
+ })
178
+
179
+ clientSocket.on('error', () => proxySocket.destroy())
180
+ }
181
+
182
+ /**
183
+ * Start the proxy server.
184
+ */
185
+ start() {
186
+ this.server = http.createServer((req, res) => {
187
+ // Health endpoint
188
+ if (req.url === '/__health') {
189
+ res.writeHead(200, { 'Content-Type': 'application/json' })
190
+ res.end(JSON.stringify(this.stats()))
191
+ return
192
+ }
193
+ this._handleRequest(req, res)
194
+ })
195
+
196
+ this.server.on('connect', (req, socket, head) => {
197
+ this._handleConnect(req, socket, head)
198
+ })
199
+
200
+ this.server.listen(this.port, () => {
201
+ this._stats.started = new Date().toISOString()
202
+ console.log(`🔀 Spectrawl proxy running on http://localhost:${this.port}`)
203
+ console.log(` Upstreams: ${this.upstreams.length}`)
204
+ console.log(` Strategy: ${this.strategy}`)
205
+ console.log(` Health: http://localhost:${this.port}/__health`)
206
+ })
207
+
208
+ return this.server
209
+ }
210
+
211
+ /**
212
+ * Get proxy stats.
213
+ */
214
+ stats() {
215
+ return {
216
+ ...this._stats,
217
+ upstreams: this.upstreams.map(u => ({
218
+ url: u.url || `${u.host}:${u.port}`,
219
+ requests: u.requests,
220
+ failures: u.failures,
221
+ healthy: u.failures < this.maxFailures
222
+ }))
223
+ }
224
+ }
225
+
226
+ stop() {
227
+ if (this.server) {
228
+ this.server.close()
229
+ this.server = null
230
+ }
231
+ }
232
+ }
233
+
234
+ module.exports = { ProxyServer }