@aidra/pmx-io 0.0.0-stage → 9.13.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/README.md +69 -2
- package/bin/pmx-io-node.js +143 -0
- package/index.d.ts +381 -0
- package/index.js +27 -0
- package/index.mjs +47 -0
- package/lib/actions.js +120 -0
- package/lib/activate.js +402 -0
- package/lib/application.js +106 -0
- package/lib/bun.js +155 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +17 -0
- package/lib/debug.js +48 -0
- package/lib/esm-hooks.mjs +111 -0
- package/lib/exit.js +79 -0
- package/lib/features/metrics.js +409 -0
- package/lib/features/notify.js +358 -0
- package/lib/features/profiling.js +208 -0
- package/lib/hooks.js +151 -0
- package/lib/inbound.js +174 -0
- package/lib/integrations.js +372 -0
- package/lib/itg/amqplib.js +169 -0
- package/lib/itg/bullmq.js +141 -0
- package/lib/itg/common.js +657 -0
- package/lib/itg/dbcomment.js +114 -0
- package/lib/itg/index.js +103 -0
- package/lib/itg/ioredis.js +212 -0
- package/lib/itg/kafkajs.js +205 -0
- package/lib/itg/mongodb.js +322 -0
- package/lib/itg/mysql2.js +205 -0
- package/lib/itg/nats.js +531 -0
- package/lib/itg/noderedis.js +236 -0
- package/lib/itg/pg.js +253 -0
- package/lib/itg/pgqueue.js +305 -0
- package/lib/itg/postgres.js +252 -0
- package/lib/itg/redis-common.js +265 -0
- package/lib/itg/sql.js +302 -0
- package/lib/metrics/service.js +286 -0
- package/lib/metrics/types.js +342 -0
- package/lib/modules.js +260 -0
- package/lib/node-options.js +62 -0
- package/lib/otel/bridge.js +797 -0
- package/lib/otel/env.js +114 -0
- package/lib/outbound.js +362 -0
- package/lib/pmx.js +757 -0
- package/lib/relay/hub.js +279 -0
- package/lib/relay/transport.js +253 -0
- package/lib/standalone/activate.js +307 -0
- package/lib/standalone/capture.js +55 -0
- package/lib/standalone/container.js +909 -0
- package/lib/standalone/transport.js +1023 -0
- package/lib/tracing/context.js +90 -0
- package/lib/tracing/control.js +124 -0
- package/lib/tracing/exporter.js +115 -0
- package/lib/tracing/index.js +242 -0
- package/lib/tracing/logs.js +176 -0
- package/lib/tracing/options.js +105 -0
- package/lib/tracing/paths.js +34 -0
- package/lib/tracing/tracer.js +850 -0
- package/lib/tracing/w3c.js +415 -0
- package/lib/transport/ipc.js +97 -0
- package/lib/transport/ws.js +511 -0
- package/lib/util.js +109 -0
- package/package.json +48 -4
- package/preload.cjs +26 -0
- package/preload.mjs +25 -0
- package/standalone.cjs +16 -0
- package/standalone.mjs +17 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// HTTP/1.1 requests and an RFC 6455 WebSocket client over net/tls, for the standalone transport
|
|
5
|
+
// (80 NIO-264, PYI-155 conformance). No dependency and no use of http/https/fetch, so the
|
|
6
|
+
// library's own requests are never seen by its outbound instrumentation (NIO-222) and loading
|
|
7
|
+
// it costs nothing at start-up. Every socket is unref'd: the transport never keeps the app alive.
|
|
8
|
+
//
|
|
9
|
+
// * handshake: 101 with `Upgrade: websocket`, `Connection` containing `upgrade`, the correct
|
|
10
|
+
// Sec-WebSocket-Accept, and no extension or subprotocol the client did not ask for;
|
|
11
|
+
// * every client frame masked with a fresh key; one text frame per message;
|
|
12
|
+
// * incoming fragmented messages reassembled; text validated as UTF-8 (else close 1007);
|
|
13
|
+
// a masked server frame, a reserved bit, an unknown opcode, or a control frame longer than
|
|
14
|
+
// 125 bytes or fragmented closes with 1002; messages over 16 MiB close with 1009;
|
|
15
|
+
// * pings answered with the same payload; close handshake with a 2 s timeout;
|
|
16
|
+
// * proxy (opt-in): CONNECT tunnel with Basic auth for https and for the WebSocket; plain
|
|
17
|
+
// http requests are sent to the proxy in absolute form. TLS to prodman always uses
|
|
18
|
+
// SNI = prodman's host and the runtime's CA store (NODE_EXTRA_CA_CERTS applies).
|
|
19
|
+
|
|
20
|
+
const net = require('net')
|
|
21
|
+
const crypto = require('crypto')
|
|
22
|
+
const { EventEmitter } = require('events')
|
|
23
|
+
|
|
24
|
+
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
25
|
+
const MAX_MESSAGE = 16 * 1024 * 1024
|
|
26
|
+
const CLOSE_TIMEOUT = 2000
|
|
27
|
+
|
|
28
|
+
function parseUrl (u) {
|
|
29
|
+
const url = u instanceof URL ? u : new URL(u)
|
|
30
|
+
const secure = url.protocol === 'https:' || url.protocol === 'wss:'
|
|
31
|
+
return {
|
|
32
|
+
url,
|
|
33
|
+
secure,
|
|
34
|
+
host: url.hostname.replace(/^\[|\]$/g, ''),
|
|
35
|
+
port: Number(url.port) || (secure ? 443 : 80),
|
|
36
|
+
hostHeader: url.host,
|
|
37
|
+
path: (url.pathname || '/') + (url.search || '')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function proxyAuth (proxy) {
|
|
42
|
+
if (!proxy || !proxy.username) return null
|
|
43
|
+
const user = decodeURIComponent(proxy.username)
|
|
44
|
+
const pass = decodeURIComponent(proxy.password || '')
|
|
45
|
+
return 'Basic ' + Buffer.from(user + ':' + pass).toString('base64')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseHead (buf) {
|
|
49
|
+
const end = buf.indexOf('\r\n\r\n')
|
|
50
|
+
if (end === -1) return null
|
|
51
|
+
const head = buf.slice(0, end).toString('latin1')
|
|
52
|
+
const [statusLine, ...lines] = head.split('\r\n')
|
|
53
|
+
const status = Number(statusLine.split(' ')[1])
|
|
54
|
+
const headers = {}
|
|
55
|
+
for (const l of lines) {
|
|
56
|
+
const i = l.indexOf(':')
|
|
57
|
+
if (i > 0) {
|
|
58
|
+
const k = l.slice(0, i).trim().toLowerCase()
|
|
59
|
+
const v = l.slice(i + 1).trim()
|
|
60
|
+
headers[k] = headers[k] !== undefined ? headers[k] + ', ' + v : v
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { status, headers, rest: buf.slice(end + 4) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function withTimeout (sock, ms, onTimeout) {
|
|
67
|
+
if (!ms) return () => {}
|
|
68
|
+
const t = setTimeout(() => {
|
|
69
|
+
onTimeout()
|
|
70
|
+
sock.destroy(new Error('timeout'))
|
|
71
|
+
}, ms)
|
|
72
|
+
if (t.unref) t.unref()
|
|
73
|
+
return () => clearTimeout(t)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A TCP (or TLS) socket to `target` ({host, port, secure}), through `proxy` (a URL) if given.
|
|
78
|
+
* `opts.timeout` (ms) bounds the whole set-up — TCP connect, proxy CONNECT and TLS handshake —
|
|
79
|
+
* as pmx-io's deadline does (PYI-155): a peer that accepts and then stays silent can never
|
|
80
|
+
* leave the transport waiting for ever.
|
|
81
|
+
*/
|
|
82
|
+
function openSocket (target, opts) {
|
|
83
|
+
opts = opts || {}
|
|
84
|
+
const proxy = opts.proxy ? (opts.proxy instanceof URL ? opts.proxy : new URL(opts.proxy)) : null
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
let done = false
|
|
87
|
+
let current = null
|
|
88
|
+
let timer = null
|
|
89
|
+
const fail = (err) => {
|
|
90
|
+
if (done) return
|
|
91
|
+
done = true
|
|
92
|
+
if (timer) clearTimeout(timer)
|
|
93
|
+
if (current) {
|
|
94
|
+
try {
|
|
95
|
+
current.destroy()
|
|
96
|
+
} catch (_) {}
|
|
97
|
+
}
|
|
98
|
+
reject(err)
|
|
99
|
+
}
|
|
100
|
+
const succeed = (s) => {
|
|
101
|
+
if (done) return
|
|
102
|
+
done = true
|
|
103
|
+
if (timer) clearTimeout(timer)
|
|
104
|
+
resolve(s)
|
|
105
|
+
}
|
|
106
|
+
if (opts.timeout > 0) {
|
|
107
|
+
timer = setTimeout(() => fail(new Error('connect timeout')), opts.timeout)
|
|
108
|
+
if (timer.unref) timer.unref()
|
|
109
|
+
}
|
|
110
|
+
const secureUp = (raw) => {
|
|
111
|
+
if (!target.secure) {
|
|
112
|
+
succeed(raw)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const tls = require('tls')
|
|
116
|
+
const s = tls.connect({ socket: raw, servername: net.isIP(target.host) ? undefined : target.host, host: target.host })
|
|
117
|
+
current = s
|
|
118
|
+
if (s.unref) s.unref()
|
|
119
|
+
s.once('secureConnect', () => succeed(s))
|
|
120
|
+
s.on('error', fail)
|
|
121
|
+
}
|
|
122
|
+
if (!proxy) {
|
|
123
|
+
const s = net.connect({ host: target.host, port: target.port })
|
|
124
|
+
current = s
|
|
125
|
+
if (s.unref) s.unref()
|
|
126
|
+
s.setNoDelay(true)
|
|
127
|
+
s.on('error', fail)
|
|
128
|
+
s.once('connect', () => secureUp(s))
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const pPort = Number(proxy.port) || 80
|
|
132
|
+
const s = net.connect({ host: proxy.hostname.replace(/^\[|\]$/g, ''), port: pPort })
|
|
133
|
+
current = s
|
|
134
|
+
if (s.unref) s.unref()
|
|
135
|
+
s.setNoDelay(true)
|
|
136
|
+
s.on('error', fail)
|
|
137
|
+
s.once('close', () => fail(new Error('proxy closed the connection')))
|
|
138
|
+
s.once('connect', () => {
|
|
139
|
+
const authority = (net.isIPv6(target.host) ? `[${target.host}]` : target.host) + ':' + target.port
|
|
140
|
+
const lines = [`CONNECT ${authority} HTTP/1.1`, `Host: ${authority}`]
|
|
141
|
+
const auth = proxyAuth(proxy)
|
|
142
|
+
if (auth) lines.push(`Proxy-Authorization: ${auth}`)
|
|
143
|
+
s.write(lines.join('\r\n') + '\r\n\r\n')
|
|
144
|
+
let buf = Buffer.alloc(0)
|
|
145
|
+
const onData = (chunk) => {
|
|
146
|
+
buf = Buffer.concat([buf, chunk])
|
|
147
|
+
const h = parseHead(buf)
|
|
148
|
+
if (!h) {
|
|
149
|
+
if (buf.length > 65536) fail(new Error('proxy response too large'))
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
s.removeListener('data', onData)
|
|
153
|
+
if (h.status !== 200) {
|
|
154
|
+
fail(new Error(`proxy CONNECT failed: ${h.status}`))
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (h.rest.length) s.unshift(h.rest)
|
|
158
|
+
secureUp(s)
|
|
159
|
+
}
|
|
160
|
+
s.on('data', onData)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function decodeChunked (buf) {
|
|
166
|
+
const parts = []
|
|
167
|
+
let i = 0
|
|
168
|
+
for (;;) {
|
|
169
|
+
const nl = buf.indexOf('\r\n', i)
|
|
170
|
+
if (nl === -1) return null
|
|
171
|
+
const size = parseInt(buf.slice(i, nl).toString('latin1').split(';')[0].trim(), 16)
|
|
172
|
+
if (!Number.isFinite(size)) return null
|
|
173
|
+
if (size === 0) return Buffer.concat(parts)
|
|
174
|
+
if (buf.length < nl + 2 + size + 2) return null
|
|
175
|
+
parts.push(buf.slice(nl + 2, nl + 2 + size))
|
|
176
|
+
i = nl + 2 + size + 2
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A minimal HTTP/1.1 request (Connection: close). Resolves { status, headers, body }.
|
|
182
|
+
* @param {{timeout?: number, proxy?: string|URL}} [opts]
|
|
183
|
+
*/
|
|
184
|
+
function httpRequest (method, url, body, headers, opts) {
|
|
185
|
+
opts = opts || {}
|
|
186
|
+
const t = parseUrl(url)
|
|
187
|
+
const proxy = opts.proxy ? (opts.proxy instanceof URL ? opts.proxy : new URL(opts.proxy)) : null
|
|
188
|
+
const viaAbsolute = proxy && !t.secure
|
|
189
|
+
const sockTarget = viaAbsolute ? { host: proxy.hostname.replace(/^\[|\]$/g, ''), port: Number(proxy.port) || 80, secure: false } : t
|
|
190
|
+
const payload = body === null || body === undefined ? null : Buffer.isBuffer(body) ? body : Buffer.from(String(body), 'utf8')
|
|
191
|
+
const deadline = opts.timeout ? Date.now() + opts.timeout : 0
|
|
192
|
+
return openSocket(sockTarget, { proxy: viaAbsolute ? null : proxy, timeout: opts.timeout }).then((sock) => new Promise((resolve, reject) => {
|
|
193
|
+
let settled = false
|
|
194
|
+
const finish = (err, res) => {
|
|
195
|
+
if (settled) return
|
|
196
|
+
settled = true
|
|
197
|
+
clear()
|
|
198
|
+
try {
|
|
199
|
+
sock.destroy()
|
|
200
|
+
} catch (_) {}
|
|
201
|
+
if (err) reject(err)
|
|
202
|
+
else resolve(res)
|
|
203
|
+
}
|
|
204
|
+
const clear = withTimeout(sock, deadline ? Math.max(1, deadline - Date.now()) : 0, () => finish(new Error('timeout')))
|
|
205
|
+
const lines = [`${method} ${viaAbsolute ? t.url.href : t.path} HTTP/1.1`, `Host: ${t.hostHeader}`, 'Connection: close']
|
|
206
|
+
if (viaAbsolute) {
|
|
207
|
+
const auth = proxyAuth(proxy)
|
|
208
|
+
if (auth) lines.push(`Proxy-Authorization: ${auth}`)
|
|
209
|
+
}
|
|
210
|
+
for (const k of Object.keys(headers || {})) lines.push(`${k}: ${headers[k]}`)
|
|
211
|
+
if (payload) lines.push(`Content-Length: ${payload.length}`)
|
|
212
|
+
sock.write(lines.join('\r\n') + '\r\n\r\n')
|
|
213
|
+
if (payload) sock.write(payload)
|
|
214
|
+
const chunks = []
|
|
215
|
+
let total = 0
|
|
216
|
+
const tryParse = (final) => {
|
|
217
|
+
const buf = Buffer.concat(chunks)
|
|
218
|
+
const h = parseHead(buf)
|
|
219
|
+
if (!h) {
|
|
220
|
+
if (final) finish(new Error('incomplete response'))
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
const len = h.headers['content-length']
|
|
224
|
+
if (len !== undefined) {
|
|
225
|
+
const n = Number(len)
|
|
226
|
+
if (h.rest.length >= n) return finish(null, { status: h.status, headers: h.headers, body: h.rest.slice(0, n) })
|
|
227
|
+
} else if (/chunked/i.test(h.headers['transfer-encoding'] || '')) {
|
|
228
|
+
const b = decodeChunked(h.rest)
|
|
229
|
+
if (b) return finish(null, { status: h.status, headers: h.headers, body: b })
|
|
230
|
+
}
|
|
231
|
+
if (final) finish(null, { status: h.status, headers: h.headers, body: h.rest })
|
|
232
|
+
}
|
|
233
|
+
sock.on('data', (c) => {
|
|
234
|
+
chunks.push(c)
|
|
235
|
+
total += c.length
|
|
236
|
+
if (total > MAX_MESSAGE) return finish(new Error('response too large'))
|
|
237
|
+
tryParse(false)
|
|
238
|
+
})
|
|
239
|
+
sock.on('end', () => tryParse(true))
|
|
240
|
+
sock.on('close', () => tryParse(true))
|
|
241
|
+
sock.on('error', (err) => finish(err))
|
|
242
|
+
}))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
let utf8
|
|
248
|
+
function validUtf8 (buf) {
|
|
249
|
+
if (!utf8) utf8 = new TextDecoder('utf-8', { fatal: true })
|
|
250
|
+
try {
|
|
251
|
+
return utf8.decode(buf)
|
|
252
|
+
} catch (_) {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
class WebSocketClient extends EventEmitter {
|
|
258
|
+
/**
|
|
259
|
+
* @param {string} url ws:// or wss://
|
|
260
|
+
* @param {object} headers extra upgrade headers
|
|
261
|
+
* @param {{proxy?: string|URL, timeout?: number}} [options]
|
|
262
|
+
*/
|
|
263
|
+
constructor (url, headers, options) {
|
|
264
|
+
super()
|
|
265
|
+
this.url = new URL(url)
|
|
266
|
+
this.headers = headers || {}
|
|
267
|
+
this.options = options || {}
|
|
268
|
+
this.readyState = 0 // 0 connecting, 1 open, 2 closing, 3 closed
|
|
269
|
+
this._buf = Buffer.alloc(0)
|
|
270
|
+
this._fragments = null
|
|
271
|
+
this._fragBytes = 0
|
|
272
|
+
this._socket = null
|
|
273
|
+
this._closeTimer = null
|
|
274
|
+
this.closeCode = null
|
|
275
|
+
process.nextTick(() => this._connect())
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_connect () {
|
|
279
|
+
const t = parseUrl(this.url)
|
|
280
|
+
openSocket(t, { proxy: this.options.proxy, timeout: this.options.timeout || 15000 }).then((socket) => {
|
|
281
|
+
if (this.readyState === 3) {
|
|
282
|
+
socket.destroy()
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
this._socket = socket
|
|
286
|
+
if (socket.unref) socket.unref()
|
|
287
|
+
this._key = crypto.randomBytes(16).toString('base64')
|
|
288
|
+
const lines = [
|
|
289
|
+
`GET ${t.path} HTTP/1.1`,
|
|
290
|
+
`Host: ${t.hostHeader}`,
|
|
291
|
+
'Upgrade: websocket',
|
|
292
|
+
'Connection: Upgrade',
|
|
293
|
+
`Sec-WebSocket-Key: ${this._key}`,
|
|
294
|
+
'Sec-WebSocket-Version: 13'
|
|
295
|
+
]
|
|
296
|
+
for (const k of Object.keys(this.headers)) lines.push(`${k}: ${this.headers[k]}`)
|
|
297
|
+
socket.write(lines.join('\r\n') + '\r\n\r\n')
|
|
298
|
+
socket.on('data', (chunk) => this._onData(chunk))
|
|
299
|
+
socket.on('error', (err) => this.emit('error', err))
|
|
300
|
+
socket.on('close', () => this._onClose())
|
|
301
|
+
socket.on('drain', () => this.emit('drain'))
|
|
302
|
+
this._upgradeTimer = setTimeout(() => {
|
|
303
|
+
if (this.readyState === 0) this._abort(new Error('upgrade timeout'))
|
|
304
|
+
}, this.options.timeout || 15000)
|
|
305
|
+
if (this._upgradeTimer.unref) this._upgradeTimer.unref()
|
|
306
|
+
}, (err) => {
|
|
307
|
+
this.emit('error', err)
|
|
308
|
+
this._onClose()
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_abort (err) {
|
|
313
|
+
if (err) this.emit('error', err)
|
|
314
|
+
this.terminate()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_onClose () {
|
|
318
|
+
if (this.readyState === 3) return
|
|
319
|
+
this.readyState = 3
|
|
320
|
+
clearTimeout(this._closeTimer)
|
|
321
|
+
clearTimeout(this._upgradeTimer)
|
|
322
|
+
this.emit('close', this.closeCode)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_onData (chunk) {
|
|
326
|
+
this._buf = this._buf.length ? Buffer.concat([this._buf, chunk]) : chunk
|
|
327
|
+
if (this.readyState === 0) {
|
|
328
|
+
const h = parseHead(this._buf)
|
|
329
|
+
if (!h) {
|
|
330
|
+
if (this._buf.length > 65536) this._abort(new Error('upgrade response too large'))
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
this._buf = h.rest
|
|
334
|
+
clearTimeout(this._upgradeTimer)
|
|
335
|
+
const expected = crypto.createHash('sha1').update(this._key + GUID).digest('base64')
|
|
336
|
+
const upgrade = String(h.headers.upgrade || '').toLowerCase()
|
|
337
|
+
const connection = String(h.headers.connection || '').toLowerCase()
|
|
338
|
+
let why = null
|
|
339
|
+
if (h.status !== 101) why = `Unexpected server response: ${h.status}`
|
|
340
|
+
else if (upgrade !== 'websocket') why = 'missing Upgrade: websocket'
|
|
341
|
+
else if (!connection.split(',').map((s) => s.trim()).includes('upgrade')) why = 'missing Connection: upgrade'
|
|
342
|
+
else if (h.headers['sec-websocket-accept'] !== expected) why = 'bad Sec-WebSocket-Accept'
|
|
343
|
+
else if (h.headers['sec-websocket-extensions'] !== undefined) why = 'unrequested extension'
|
|
344
|
+
else if (h.headers['sec-websocket-protocol'] !== undefined) why = 'unrequested subprotocol'
|
|
345
|
+
if (why) return this._abort(new Error(why))
|
|
346
|
+
this.readyState = 1
|
|
347
|
+
this.emit('open')
|
|
348
|
+
}
|
|
349
|
+
this._parseFrames()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
_protocolError (code) {
|
|
353
|
+
this.close(code)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
_parseFrames () {
|
|
357
|
+
while (this.readyState === 1 || this.readyState === 2) {
|
|
358
|
+
const b = this._buf
|
|
359
|
+
if (b.length < 2) return
|
|
360
|
+
const fin = (b[0] & 0x80) !== 0
|
|
361
|
+
const rsv = b[0] & 0x70
|
|
362
|
+
const opcode = b[0] & 0x0f
|
|
363
|
+
const masked = (b[1] & 0x80) !== 0
|
|
364
|
+
let len = b[1] & 0x7f
|
|
365
|
+
let off = 2
|
|
366
|
+
if (rsv) return this._protocolError(1002)
|
|
367
|
+
if (masked) return this._protocolError(1002)
|
|
368
|
+
if (![0x0, 0x1, 0x2, 0x8, 0x9, 0xa].includes(opcode)) return this._protocolError(1002)
|
|
369
|
+
const control = opcode >= 0x8
|
|
370
|
+
if (control && (!fin || len > 125)) return this._protocolError(1002)
|
|
371
|
+
if (len === 126) {
|
|
372
|
+
if (b.length < 4) return
|
|
373
|
+
len = b.readUInt16BE(2)
|
|
374
|
+
off = 4
|
|
375
|
+
} else if (len === 127) {
|
|
376
|
+
if (b.length < 10) return
|
|
377
|
+
const big = b.readBigUInt64BE(2)
|
|
378
|
+
if (big > BigInt(MAX_MESSAGE)) return this._protocolError(1009)
|
|
379
|
+
len = Number(big)
|
|
380
|
+
off = 10
|
|
381
|
+
}
|
|
382
|
+
if (len > MAX_MESSAGE || this._fragBytes + len > MAX_MESSAGE) return this._protocolError(1009)
|
|
383
|
+
if (b.length < off + len) return
|
|
384
|
+
const payload = b.slice(off, off + len)
|
|
385
|
+
this._buf = b.slice(off + len)
|
|
386
|
+
this._onFrame(fin, opcode, payload)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
_onFrame (fin, opcode, payload) {
|
|
391
|
+
switch (opcode) {
|
|
392
|
+
case 0x0:
|
|
393
|
+
if (!this._fragments) return this._protocolError(1002)
|
|
394
|
+
this._fragments.parts.push(payload)
|
|
395
|
+
this._fragBytes += payload.length
|
|
396
|
+
if (fin) {
|
|
397
|
+
const { op, parts } = this._fragments
|
|
398
|
+
this._fragments = null
|
|
399
|
+
this._fragBytes = 0
|
|
400
|
+
this._deliver(op, Buffer.concat(parts))
|
|
401
|
+
}
|
|
402
|
+
return
|
|
403
|
+
case 0x1:
|
|
404
|
+
case 0x2:
|
|
405
|
+
if (this._fragments) return this._protocolError(1002)
|
|
406
|
+
if (!fin) {
|
|
407
|
+
this._fragments = { op: opcode, parts: [payload] }
|
|
408
|
+
this._fragBytes = payload.length
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
this._deliver(opcode, payload)
|
|
412
|
+
return
|
|
413
|
+
case 0x8: {
|
|
414
|
+
if (payload.length === 1) return this._protocolError(1002)
|
|
415
|
+
if (payload.length > 2 && validUtf8(payload.slice(2)) === null) return this._protocolError(1007)
|
|
416
|
+
const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005
|
|
417
|
+
if (this.closeCode === null) this.closeCode = code
|
|
418
|
+
if (this.readyState === 1) {
|
|
419
|
+
this._writeFrame(0x8, payload.length >= 2 ? payload.slice(0, 2) : Buffer.alloc(0))
|
|
420
|
+
this.readyState = 2
|
|
421
|
+
}
|
|
422
|
+
if (this._socket) this._socket.end()
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
case 0x9:
|
|
426
|
+
this._writeFrame(0xa, payload)
|
|
427
|
+
this.emit('ping', payload)
|
|
428
|
+
return
|
|
429
|
+
case 0xa:
|
|
430
|
+
this.emit('pong', payload)
|
|
431
|
+
return
|
|
432
|
+
default:
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_deliver (opcode, data) {
|
|
437
|
+
if (opcode === 0x1) {
|
|
438
|
+
const text = validUtf8(data)
|
|
439
|
+
if (text === null) return this._protocolError(1007)
|
|
440
|
+
this.emit('message', text)
|
|
441
|
+
} else {
|
|
442
|
+
this.emit('message', data)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
_writeFrame (opcode, payload, cb) {
|
|
447
|
+
if (!this._socket || this._socket.destroyed) return false
|
|
448
|
+
const len = payload.length
|
|
449
|
+
let header
|
|
450
|
+
if (len < 126) {
|
|
451
|
+
header = Buffer.alloc(2)
|
|
452
|
+
header[1] = 0x80 | len
|
|
453
|
+
} else if (len < 65536) {
|
|
454
|
+
header = Buffer.alloc(4)
|
|
455
|
+
header[1] = 0x80 | 126
|
|
456
|
+
header.writeUInt16BE(len, 2)
|
|
457
|
+
} else {
|
|
458
|
+
header = Buffer.alloc(10)
|
|
459
|
+
header[1] = 0x80 | 127
|
|
460
|
+
header.writeBigUInt64BE(BigInt(len), 2)
|
|
461
|
+
}
|
|
462
|
+
header[0] = 0x80 | opcode
|
|
463
|
+
const mask = crypto.randomBytes(4)
|
|
464
|
+
const masked = Buffer.allocUnsafe(len)
|
|
465
|
+
for (let i = 0; i < len; i++) masked[i] = payload[i] ^ mask[i & 3]
|
|
466
|
+
return this._socket.write(Buffer.concat([header, mask, masked]), cb)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
send (text, cb) {
|
|
470
|
+
if (this.readyState !== 1) return false
|
|
471
|
+
this._writeFrame(0x1, Buffer.from(String(text), 'utf8'), cb)
|
|
472
|
+
return true
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Bytes queued in the socket but not yet written. */
|
|
476
|
+
bufferedAmount () {
|
|
477
|
+
return this._socket ? this._socket.writableLength : 0
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
ping () {
|
|
481
|
+
if (this.readyState !== 1) return false
|
|
482
|
+
return this._writeFrame(0x9, Buffer.alloc(0))
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
close (code, reason) {
|
|
486
|
+
if (this.readyState >= 2) {
|
|
487
|
+
if (this.readyState === 0) this.terminate()
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
if (this.readyState === 0) {
|
|
491
|
+
this.terminate()
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
const r = Buffer.from(String(reason || ''), 'utf8').slice(0, 123)
|
|
495
|
+
const p = Buffer.alloc(2 + r.length)
|
|
496
|
+
p.writeUInt16BE(code || 1000, 0)
|
|
497
|
+
r.copy(p, 2)
|
|
498
|
+
if (this.closeCode === null) this.closeCode = code || 1000
|
|
499
|
+
this._writeFrame(0x8, p)
|
|
500
|
+
this.readyState = 2
|
|
501
|
+
this._closeTimer = setTimeout(() => this.terminate(), CLOSE_TIMEOUT)
|
|
502
|
+
if (this._closeTimer.unref) this._closeTimer.unref()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
terminate () {
|
|
506
|
+
if (this._socket) this._socket.destroy()
|
|
507
|
+
this._onClose()
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
module.exports = { WebSocketClient, httpRequest, openSocket, parseUrl, MAX_MESSAGE }
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
|
|
7
|
+
const PKG_DIR = path.resolve(__dirname, '..')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The CommonJS Module class. `module.constructor` is the same object as require('module')
|
|
11
|
+
* without loading the public module facade, which on Node 18 pulls in the ES-module loader,
|
|
12
|
+
* readline and fs/promises internals at activation (NIO-024).
|
|
13
|
+
*/
|
|
14
|
+
function cjsModule () {
|
|
15
|
+
const M = module.constructor
|
|
16
|
+
if (typeof M === 'function' && M.prototype && typeof M.prototype.require === 'function' && typeof M._load === 'function') return M
|
|
17
|
+
return require('module')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function round2 (n) {
|
|
21
|
+
return Math.round(n * 100) / 100
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isPlainObject (v) {
|
|
25
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Random lowercase base-36 identifier of up to 16 characters (80 A.7). */
|
|
29
|
+
function shortUuid () {
|
|
30
|
+
let s = ''
|
|
31
|
+
while (s.length < 16) s += Math.random().toString(36).slice(2)
|
|
32
|
+
return s.slice(0, 16)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Recursive autocast of env-provided module configuration (80 A.8.3). */
|
|
36
|
+
function autocast (v) {
|
|
37
|
+
if (Array.isArray(v)) return v.map(autocast)
|
|
38
|
+
if (isPlainObject(v)) {
|
|
39
|
+
const out = {}
|
|
40
|
+
for (const k of Object.keys(v)) out[k] = autocast(v[k])
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
if (typeof v !== 'string') return v
|
|
44
|
+
if (v === 'true') return true
|
|
45
|
+
if (v === 'false') return false
|
|
46
|
+
if (v === 'null') return null
|
|
47
|
+
if (v === 'undefined') return undefined
|
|
48
|
+
if (v === 'NaN') return NaN
|
|
49
|
+
if (v.trim() !== '' && !isNaN(Number(v))) return Number(v)
|
|
50
|
+
return v
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The app's main script: require.main's file, else the real path of argv[1] (80 A.8.4). */
|
|
54
|
+
function mainScript () {
|
|
55
|
+
try {
|
|
56
|
+
if (require.main && require.main.filename) return require.main.filename
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
const arg = process.argv[1]
|
|
59
|
+
if (!arg) return null
|
|
60
|
+
try {
|
|
61
|
+
return fs.realpathSync(arg)
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return path.resolve(arg)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** package.json next to the main script, or 1/2 directories above (3 levels only). */
|
|
68
|
+
function findPackageJson () {
|
|
69
|
+
const script = mainScript()
|
|
70
|
+
if (!script) return null
|
|
71
|
+
let dir = path.dirname(script)
|
|
72
|
+
for (let i = 0; i < 3; i++) {
|
|
73
|
+
const candidate = path.join(dir, 'package.json')
|
|
74
|
+
if (fs.existsSync(candidate)) return candidate
|
|
75
|
+
const parent = path.dirname(dir)
|
|
76
|
+
if (parent === dir) break
|
|
77
|
+
dir = parent
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readPackageJsonSync () {
|
|
83
|
+
const file = findPackageJson()
|
|
84
|
+
if (!file) return null
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
87
|
+
} catch (_) {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** JSON deep copy; returns undefined if the value cannot be serialised. Throws on cycles. */
|
|
93
|
+
function jsonCopy (v) {
|
|
94
|
+
const s = JSON.stringify(v)
|
|
95
|
+
return s === undefined ? undefined : JSON.parse(s)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
cjsModule,
|
|
100
|
+
PKG_DIR,
|
|
101
|
+
round2,
|
|
102
|
+
isPlainObject,
|
|
103
|
+
shortUuid,
|
|
104
|
+
autocast,
|
|
105
|
+
mainScript,
|
|
106
|
+
findPackageJson,
|
|
107
|
+
readPackageJsonSync,
|
|
108
|
+
jsonCopy
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aidra/pmx-io",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "9.13.0",
|
|
4
|
+
"description": "In-process instrumentation for Node.js and Bun apps run by pmx, or reporting to prodman on their own (@pm2/io compatible)",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/westonpark/pmx.git",
|
|
9
|
+
"directory": "packages/node-io"
|
|
10
|
+
},
|
|
11
|
+
"private": false,
|
|
12
|
+
"main": "./index.js",
|
|
13
|
+
"module": "./index.mjs",
|
|
14
|
+
"types": "./index.d.ts",
|
|
15
|
+
"bin": {
|
|
16
|
+
"pmx-io-node": "bin/pmx-io-node.js"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./index.d.ts",
|
|
21
|
+
"import": "./index.mjs",
|
|
22
|
+
"require": "./index.js",
|
|
23
|
+
"default": "./index.js"
|
|
24
|
+
},
|
|
25
|
+
"./preload.cjs": "./preload.cjs",
|
|
26
|
+
"./preload.mjs": "./preload.mjs",
|
|
27
|
+
"./standalone": "./standalone.cjs",
|
|
28
|
+
"./standalone.cjs": "./standalone.cjs",
|
|
29
|
+
"./standalone.mjs": "./standalone.mjs",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"index.js",
|
|
34
|
+
"index.mjs",
|
|
35
|
+
"index.d.ts",
|
|
36
|
+
"preload.cjs",
|
|
37
|
+
"preload.mjs",
|
|
38
|
+
"standalone.cjs",
|
|
39
|
+
"standalone.mjs",
|
|
40
|
+
"bin/",
|
|
41
|
+
"lib/"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "sh ../../scripts/test-sandbox.sh node --test test/*.test.js"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {}
|
|
50
|
+
}
|
package/preload.cjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// CommonJS preload, injected by the pmx daemon with `--require` (Node) or `--preload` (Bun).
|
|
5
|
+
// Activates only under pmx (80 NIO-020); any failure leaves the app untouched (NIO-023).
|
|
6
|
+
// The cheap environment checks come first so that an inert preload loads nothing else.
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const env = process.env
|
|
10
|
+
// Spawn duty of the daemon carried by its injected preload (20 DMN-240/253, 40 §2.1 item 1 /
|
|
11
|
+
// §2.2 item 2; cleanroom Q-NIO-031): source maps are enabled before the main module loads
|
|
12
|
+
// unless `disable_source_map_support` is true. `source_map_support` (forced) is the default.
|
|
13
|
+
if (env.PMX_IO === '1' && env.disable_source_map_support !== 'true' &&
|
|
14
|
+
typeof process.setSourceMapsEnabled === 'function') {
|
|
15
|
+
process.setSourceMapsEnabled(true)
|
|
16
|
+
}
|
|
17
|
+
// the root under the daemon (NIO-020), or a descendant / worker thread of one (NIO-291/293)
|
|
18
|
+
if ((env.PMX_IO === '1' || (!env.PMX_IO && env.PMX_IO_HUB && env.PMX_IO_STANDALONE !== '1')) &&
|
|
19
|
+
env.pmx !== 'false' && env.automation !== 'false') {
|
|
20
|
+
require('./lib/activate.js').activate()
|
|
21
|
+
}
|
|
22
|
+
} catch (err) {
|
|
23
|
+
try {
|
|
24
|
+
process.stderr.write('[pmx] instrumentation disabled: ' + (err && err.message) + '\n')
|
|
25
|
+
} catch (_) {}
|
|
26
|
+
}
|