@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.
Files changed (67) hide show
  1. package/README.md +69 -2
  2. package/bin/pmx-io-node.js +143 -0
  3. package/index.d.ts +381 -0
  4. package/index.js +27 -0
  5. package/index.mjs +47 -0
  6. package/lib/actions.js +120 -0
  7. package/lib/activate.js +402 -0
  8. package/lib/application.js +106 -0
  9. package/lib/bun.js +155 -0
  10. package/lib/config.js +67 -0
  11. package/lib/constants.js +17 -0
  12. package/lib/debug.js +48 -0
  13. package/lib/esm-hooks.mjs +111 -0
  14. package/lib/exit.js +79 -0
  15. package/lib/features/metrics.js +409 -0
  16. package/lib/features/notify.js +358 -0
  17. package/lib/features/profiling.js +208 -0
  18. package/lib/hooks.js +151 -0
  19. package/lib/inbound.js +174 -0
  20. package/lib/integrations.js +372 -0
  21. package/lib/itg/amqplib.js +169 -0
  22. package/lib/itg/bullmq.js +141 -0
  23. package/lib/itg/common.js +657 -0
  24. package/lib/itg/dbcomment.js +114 -0
  25. package/lib/itg/index.js +103 -0
  26. package/lib/itg/ioredis.js +212 -0
  27. package/lib/itg/kafkajs.js +205 -0
  28. package/lib/itg/mongodb.js +322 -0
  29. package/lib/itg/mysql2.js +205 -0
  30. package/lib/itg/nats.js +531 -0
  31. package/lib/itg/noderedis.js +236 -0
  32. package/lib/itg/pg.js +253 -0
  33. package/lib/itg/pgqueue.js +305 -0
  34. package/lib/itg/postgres.js +252 -0
  35. package/lib/itg/redis-common.js +265 -0
  36. package/lib/itg/sql.js +302 -0
  37. package/lib/metrics/service.js +286 -0
  38. package/lib/metrics/types.js +342 -0
  39. package/lib/modules.js +260 -0
  40. package/lib/node-options.js +62 -0
  41. package/lib/otel/bridge.js +797 -0
  42. package/lib/otel/env.js +114 -0
  43. package/lib/outbound.js +362 -0
  44. package/lib/pmx.js +757 -0
  45. package/lib/relay/hub.js +279 -0
  46. package/lib/relay/transport.js +253 -0
  47. package/lib/standalone/activate.js +307 -0
  48. package/lib/standalone/capture.js +55 -0
  49. package/lib/standalone/container.js +909 -0
  50. package/lib/standalone/transport.js +1023 -0
  51. package/lib/tracing/context.js +90 -0
  52. package/lib/tracing/control.js +124 -0
  53. package/lib/tracing/exporter.js +115 -0
  54. package/lib/tracing/index.js +242 -0
  55. package/lib/tracing/logs.js +176 -0
  56. package/lib/tracing/options.js +105 -0
  57. package/lib/tracing/paths.js +34 -0
  58. package/lib/tracing/tracer.js +850 -0
  59. package/lib/tracing/w3c.js +415 -0
  60. package/lib/transport/ipc.js +97 -0
  61. package/lib/transport/ws.js +511 -0
  62. package/lib/util.js +109 -0
  63. package/package.json +48 -4
  64. package/preload.cjs +26 -0
  65. package/preload.mjs +25 -0
  66. package/standalone.cjs +16 -0
  67. 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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }