cruss-agent 1.0.5 → 1.0.7

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 (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +41 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cruss-agent",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Local API relay for Cruss — test localhost APIs from cruss-ten.vercel.app",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -6,14 +6,24 @@ const https = require('https')
6
6
  const url = require('url')
7
7
 
8
8
  const PORT = 9119
9
- const VERSION = '1.0.5'
10
-
9
+ const VERSION = '1.0.7'
10
+
11
+ // ── CORS ─────────────────────────────────────────────────────────────────
12
+ // Access-Control-Allow-Private-Network is required by Chrome 104+ (PNA policy).
13
+ // When an HTTPS page (e.g. cruss-ten.vercel.app) makes a non-simple request
14
+ // (POST with Content-Type: application/json) to localhost, Chrome first sends
15
+ // an OPTIONS preflight with "Access-Control-Request-Private-Network: true".
16
+ // The agent MUST respond with "Access-Control-Allow-Private-Network: true"
17
+ // or Chrome silently blocks the actual request. GET /health works without it
18
+ // because simple GET requests bypass the PNA preflight — which is exactly
19
+ // why health checks passed but POST /proxy was never received.
11
20
  function corsHeaders() {
12
21
  return {
13
- 'Access-Control-Allow-Origin': '*',
14
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
15
- 'Access-Control-Allow-Headers': '*',
16
- 'Access-Control-Max-Age': '86400',
22
+ 'Access-Control-Allow-Origin': '*',
23
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
24
+ 'Access-Control-Allow-Headers': '*',
25
+ 'Access-Control-Allow-Private-Network': 'true',
26
+ 'Access-Control-Max-Age': '86400',
17
27
  }
18
28
  }
19
29
 
@@ -31,10 +41,6 @@ const HOP_BY_HOP = new Set([
31
41
  'te', 'trailers', 'transfer-encoding', 'upgrade',
32
42
  ])
33
43
 
34
- // Returns all addresses to try for a given hostname.
35
- // On macOS, python3 -m http.server binds to :: (IPv6 only).
36
- // Node resolves localhost -> 127.0.0.1 (IPv4) -> ECONNREFUSED.
37
- // Trying both 127.0.0.1 and ::1 guarantees one works.
38
44
  function loopbackCandidates(hostname) {
39
45
  if (hostname === 'localhost') return ['127.0.0.1', '::1']
40
46
  if (hostname === '127.0.0.1') return ['127.0.0.1', '::1']
@@ -45,14 +51,16 @@ function loopbackCandidates(hostname) {
45
51
  function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
46
52
  return new Promise((resolve, reject) => {
47
53
  const lib = isHttps ? https : http
54
+ const resolvedPort = port || (isHttps ? 443 : 80)
48
55
  const options = {
49
56
  hostname,
50
- port: port || (isHttps ? 443 : 80),
57
+ port: resolvedPort,
51
58
  path: path || '/',
52
59
  method: method || 'GET',
53
60
  headers: outHeaders,
54
61
  timeout: 30000,
55
62
  }
63
+ console.log(` → trying ${hostname}:${resolvedPort}${path || '/'}`)
56
64
  const t0 = Date.now()
57
65
  const req = lib.request(options, res => {
58
66
  const chunks = []
@@ -64,10 +72,11 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
64
72
  const respHeaders = {}
65
73
  for (const [k, v] of Object.entries(res.headers || {})) {
66
74
  if (!['access-control-allow-origin','access-control-allow-headers',
67
- 'access-control-allow-methods'].includes(k.toLowerCase())) {
75
+ 'access-control-allow-methods','access-control-allow-private-network'].includes(k.toLowerCase())) {
68
76
  respHeaders[k] = Array.isArray(v) ? v.join(', ') : v
69
77
  }
70
78
  }
79
+ console.log(` ✓ ${res.statusCode} ${res.statusMessage} (${elapsed}ms)`)
71
80
  resolve({
72
81
  status: res.statusCode, statusText: res.statusMessage,
73
82
  headers: respHeaders, body: bodyStr,
@@ -77,7 +86,10 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
77
86
  res.on('error', reject)
78
87
  })
79
88
  req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out (30s)')) })
80
- req.on('error', reject)
89
+ req.on('error', (err) => {
90
+ console.log(` ✗ ${hostname}:${resolvedPort} — ${err.code || err.message}`)
91
+ reject(err)
92
+ })
81
93
  const hasBody = body && !['GET','HEAD','OPTIONS'].includes((method||'GET').toUpperCase())
82
94
  if (hasBody) req.write(typeof body === 'string' ? body : JSON.stringify(body))
83
95
  req.end()
@@ -87,15 +99,14 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
87
99
  async function proxyRequest(targetUrl, method, reqHeaders, body) {
88
100
  const parsed = url.parse(targetUrl)
89
101
  const isHttps = parsed.protocol === 'https:'
90
-
91
102
  const outHeaders = {}
92
103
  for (const [k, v] of Object.entries(reqHeaders || {})) {
93
104
  if (!HOP_BY_HOP.has(k.toLowerCase())) outHeaders[k] = v
94
105
  }
95
-
96
106
  const candidates = loopbackCandidates(parsed.hostname)
107
+ console.log(`\n [${new Date().toISOString()}] ${method} ${targetUrl}`)
108
+ console.log(` candidates: ${candidates.join(', ')}`)
97
109
  let lastErr
98
-
99
110
  for (const host of candidates) {
100
111
  try {
101
112
  return await proxyOnce(host, parsed.port, parsed.path, method, outHeaders, body, isHttps)
@@ -104,10 +115,9 @@ async function proxyRequest(targetUrl, method, reqHeaders, body) {
104
115
  if (err.code !== 'ECONNREFUSED' && err.code !== 'EADDRNOTAVAIL') throw err
105
116
  }
106
117
  }
107
-
118
+ const port = parsed.port || (isHttps ? 443 : 80)
108
119
  throw new Error(
109
- `Connection refused on all addresses (${candidates.join(', ')}) port ${parsed.port || (isHttps ? 443 : 80)}. ` +
110
- `Is your server running?`
120
+ `Connection refused on all addresses (${candidates.join(', ')}) port ${port}. Is your server running on port ${port}?`
111
121
  )
112
122
  }
113
123
 
@@ -115,8 +125,14 @@ const server = http.createServer(async (req, res) => {
115
125
  const cors = corsHeaders()
116
126
  const reqPath = url.parse(req.url).pathname
117
127
 
128
+ // Log ALL incoming requests so we can see preflights
129
+ console.log(` [${req.method}] ${reqPath} — origin: ${req.headers['origin'] || '(none)'} pna: ${req.headers['access-control-request-private-network'] || 'no'}`)
130
+
118
131
  if (req.method === 'OPTIONS') {
119
- res.writeHead(204, cors); res.end(); return
132
+ console.log(` ← preflight OK`)
133
+ res.writeHead(204, cors)
134
+ res.end()
135
+ return
120
136
  }
121
137
 
122
138
  function send(status, body) {
@@ -134,19 +150,18 @@ const server = http.createServer(async (req, res) => {
134
150
  try {
135
151
  payload = JSON.parse(await readBody(req))
136
152
  } catch {
137
- send(400, { error: 'Request body must be valid JSON' }); return
153
+ send(400, { error: 'Request body must be valid JSON' })
154
+ return
138
155
  }
139
-
140
156
  const { method, url: targetUrl, headers, body } = payload || {}
141
-
142
157
  if (!targetUrl || typeof targetUrl !== 'string') { send(400, { error: 'url is required' }); return }
143
158
  if (!method || typeof method !== 'string') { send(400, { error: 'method is required' }); return }
144
-
145
159
  try {
146
160
  const result = await proxyRequest(targetUrl, method, headers || {}, body)
147
161
  send(200, { data: result })
148
162
  } catch (err) {
149
163
  const msg = err instanceof Error ? err.message : String(err)
164
+ console.log(` ✗ proxy failed: ${msg}`)
150
165
  send(200, { data: { status: 0, statusText: 'Network Error', headers: {}, body: null, error: msg, responseTime: 0, size: 0 } })
151
166
  }
152
167
  return
@@ -168,13 +183,13 @@ server.listen(PORT, '0.0.0.0', () => {
168
183
  console.log(' ║ Ctrl+C to stop ║')
169
184
  console.log(' ╚═══════════════════════════════════════╝')
170
185
  console.log('')
171
- console.log(' Waiting for requests...')
186
+ console.log(' All requests logged below.')
172
187
  console.log('')
173
188
  })
174
189
 
175
190
  server.on('error', err => {
176
191
  if (err.code === 'EADDRINUSE') {
177
- console.error(`\n Port ${PORT} is already in use. Run: kill $(lsof -ti:${PORT})`)
192
+ console.error(`\n Port ${PORT} already in use. Run: kill $(lsof -ti:${PORT})`)
178
193
  } else {
179
194
  console.error('\n Agent failed to start:', err.message)
180
195
  }