jsgar 4.5.3 → 4.6.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 (3) hide show
  1. package/dist/gar.umd.js +182 -8
  2. package/gar.js +175 -3
  3. package/package.json +2 -2
package/dist/gar.umd.js CHANGED
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
- typeof define === 'function' && define.amd ? define(factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.GARClient = factory());
5
- })(this, (function () { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GARClient = {}));
5
+ })(this, (function (exports) { 'use strict';
6
6
 
7
7
  /**
8
8
  * A client implementation for the Generic Active Records (GAR) protocol using WebSockets.
@@ -30,6 +30,155 @@
30
30
  // - In Node.js, dynamically imports 'ws' and assigns globalThis.WebSocket
31
31
  // All client code references the constructor via helper methods, not a top-level binding.
32
32
 
33
+ // AF_UNIX local-transport optimization (mirrors ipc/gar.cpp). When the endpoint
34
+ // resolves to an IP bound to a local interface, the GAR server is also listening
35
+ // on an abstract-namespace AF_UNIX socket at "\0gar.endpoint.<port>". Routing the
36
+ // WebSocket through that path bypasses the kernel TCP loopback stack — avoiding
37
+ // EDR/Falcon-style instrumentation overhead — while preserving the wire protocol.
38
+ //
39
+ // Browser environments have no raw socket access, so this is Node.js-only. The
40
+ // 'ws' library lets us inject a pre-connected net.Socket (or TLS-wrapped) via
41
+ // the createConnection option in the constructor.
42
+ //
43
+ // Set TRS_DISABLE_AF_UNIX=1 (also accepts true/on/yes, case-insensitive) to opt
44
+ // out and force the regular TCP path. Useful when capturing GAR traffic with
45
+ // tcpdump/wireshark on the loopback interface, which only sees TCP.
46
+
47
+ function _garAfUnixDisabled() {
48
+ const v = (typeof process !== 'undefined' && process.env)
49
+ ? process.env.TRS_DISABLE_AF_UNIX : undefined;
50
+ if (!v) return false;
51
+ return ['1', 'true', 'on', 'yes'].includes(v.toLowerCase());
52
+ }
53
+
54
+ let _garNetMod = null;
55
+ let _garTlsMod = null;
56
+ let _garOsMod = null;
57
+ let _garDnsMod = null;
58
+
59
+ async function _garLoadNodeModules() {
60
+ if (_garNetMod) return true;
61
+ const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
62
+ if (!isNode) return false;
63
+ try {
64
+ const { createRequire } = await import('node:module');
65
+ const req = createRequire(process.cwd() + '/');
66
+ _garNetMod = req('net');
67
+ _garTlsMod = req('tls');
68
+ _garOsMod = req('os');
69
+ _garDnsMod = req('dns');
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function _garLocalIPv4Set() {
77
+ const set = new Set(['127.0.0.1']);
78
+ const ifaces = _garOsMod.networkInterfaces();
79
+ for (const name of Object.keys(ifaces)) {
80
+ for (const addr of ifaces[name] || []) {
81
+ if (addr.family === 'IPv4' || addr.family === 4) set.add(addr.address);
82
+ }
83
+ }
84
+ return set;
85
+ }
86
+
87
+ // Returns the optimal GAR endpoint string for |inetEndpoint|: if the URL's
88
+ // host resolves locally on this machine, returns the AF_UNIX form
89
+ // "unix:gar.endpoint.<port>"; otherwise echoes the input. Mirrors
90
+ // `trsutil --optimal-endpoint`. Async because DNS lookup in Node is async-only.
91
+ // Respects TRS_DISABLE_AF_UNIX. Does not probe the AF_UNIX listener — caller
92
+ // (or jsgar's connect path) handles fallback if it's unreachable.
93
+ async function optimalEndpoint(inetEndpoint) {
94
+ if (_garAfUnixDisabled()) return inetEndpoint;
95
+ if (!(await _garLoadNodeModules())) return inetEndpoint;
96
+ let parsed;
97
+ try { parsed = new URL(inetEndpoint); } catch { return inetEndpoint; }
98
+ const host = parsed.hostname;
99
+ const port = parsed.port;
100
+ if (!host || !port) return inetEndpoint;
101
+ let ip;
102
+ try {
103
+ if (_garNetMod.isIP(host)) {
104
+ ip = host;
105
+ } else {
106
+ const r = await _garDnsMod.promises.lookup(host, { family: 4 });
107
+ ip = r.address;
108
+ }
109
+ } catch { return inetEndpoint; }
110
+ if (ip.startsWith('127.') || _garLocalIPv4Set().has(ip)) {
111
+ return `unix:gar.endpoint.${port}`;
112
+ }
113
+ return inetEndpoint;
114
+ }
115
+
116
+ // Returns the abstract-namespace AF_UNIX path (a string starting with \u0000)
117
+ // for the given GAR WebSocket endpoint if (a) it resolves to a local IPv4 and
118
+ // (b) the abstract listener at the matching name is reachable. Otherwise null.
119
+ async function _garUnixAbstractPathForEndpoint(wsEndpoint) {
120
+ if (_garAfUnixDisabled()) return null;
121
+ if (!(await _garLoadNodeModules())) return null;
122
+ let parsed;
123
+ try {
124
+ parsed = new URL(wsEndpoint);
125
+ } catch {
126
+ return null;
127
+ }
128
+ const host = parsed.hostname;
129
+ const port = parsed.port;
130
+ if (!host || !port) return null;
131
+
132
+ let ip;
133
+ try {
134
+ if (_garNetMod.isIP(host)) {
135
+ ip = host;
136
+ } else {
137
+ const r = await _garDnsMod.promises.lookup(host, { family: 4 });
138
+ ip = r.address;
139
+ }
140
+ } catch {
141
+ return null;
142
+ }
143
+
144
+ // Loopback short-circuit; otherwise check against the host's interface IPs.
145
+ // (Less precise than RTM_GETROUTE used by the C++ side, but covers the
146
+ // practical "is this destination on this machine?" question for our use.)
147
+ if (!ip.startsWith('127.')) {
148
+ if (!_garLocalIPv4Set().has(ip)) return null;
149
+ }
150
+
151
+ const abstractPath = '\u0000gar.endpoint.' + port;
152
+ // Reachability probe: AF_UNIX local connect either succeeds immediately or
153
+ // fails immediately with ECONNREFUSED/ENOENT.
154
+ const reachable = await new Promise((resolve) => {
155
+ let done = false;
156
+ const finish = (ok) => {
157
+ if (done) return;
158
+ done = true;
159
+ try { sock.destroy(); } catch { /* ignore */ }
160
+ resolve(ok);
161
+ };
162
+ const sock = _garNetMod.createConnection({ path: abstractPath });
163
+ sock.once('connect', () => finish(true));
164
+ sock.once('error', () => finish(false));
165
+ setTimeout(() => finish(false), 500);
166
+ });
167
+ return reachable ? abstractPath : null;
168
+ }
169
+
170
+ function _garMakeUnixCreateConnectionFn(abstractPath, useSSL, sniHost, allowSelfSigned) {
171
+ return () => {
172
+ const unixSocket = _garNetMod.createConnection({ path: abstractPath });
173
+ if (!useSSL) return unixSocket;
174
+ return _garTlsMod.connect({
175
+ socket: unixSocket,
176
+ servername: sniHost,
177
+ rejectUnauthorized: !allowSelfSigned,
178
+ });
179
+ };
180
+ }
181
+
33
182
  class GARClient {
34
183
  /**
35
184
  * Initialize the GAR client.
@@ -169,13 +318,35 @@
169
318
  const WS = await this._ensureWebSocketCtor();
170
319
  const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
171
320
 
172
- this.log('INFO', `Connecting to WebSocket server at ${this.wsEndpoint}`);
321
+ // Prefer AF_UNIX abstract-namespace transport for local destinations.
322
+ // Falls back to TCP automatically (next reconnect iteration) if the
323
+ // listener disappears between probe and real connect.
324
+ let unixAbstractPath = null;
325
+ let sniHost = null;
326
+ if (isNode) {
327
+ unixAbstractPath = await _garUnixAbstractPathForEndpoint(this.wsEndpoint);
328
+ if (unixAbstractPath) {
329
+ try { sniHost = new URL(this.wsEndpoint).hostname; } catch { /* ignore */ }
330
+ }
331
+ }
332
+
333
+ this.log('INFO', `Connecting to WebSocket server at ${this.wsEndpoint}${unixAbstractPath ? ' (via AF_UNIX)' : ''}`);
173
334
 
174
335
  const connectionPromise = new Promise((resolve, reject) => {
175
336
  let websocket;
176
- if (this.allowSelfSignedCertificate && isNode) {
337
+ const useSSL = this.wsEndpoint.toLowerCase().startsWith('wss://');
338
+ const wsOpts = {};
339
+ if (this.allowSelfSignedCertificate && isNode && useSSL) {
177
340
  // Node.js 'ws' supports options for TLS; browsers do not.
178
- websocket = new WS(this.wsEndpoint, ['gar-protocol'], { rejectUnauthorized: false });
341
+ wsOpts.rejectUnauthorized = false;
342
+ }
343
+ if (unixAbstractPath) {
344
+ wsOpts.createConnection = _garMakeUnixCreateConnectionFn(
345
+ unixAbstractPath, useSSL, sniHost, this.allowSelfSignedCertificate
346
+ );
347
+ }
348
+ if (Object.keys(wsOpts).length > 0) {
349
+ websocket = new WS(this.wsEndpoint, ['gar-protocol'], wsOpts);
179
350
  } else {
180
351
  websocket = new WS(this.wsEndpoint, ['gar-protocol']);
181
352
  }
@@ -1517,6 +1688,9 @@
1517
1688
  }
1518
1689
  }
1519
1690
 
1520
- return GARClient;
1691
+ exports.default = GARClient;
1692
+ exports.optimalEndpoint = optimalEndpoint;
1693
+
1694
+ Object.defineProperty(exports, '__esModule', { value: true });
1521
1695
 
1522
1696
  }));
package/gar.js CHANGED
@@ -24,6 +24,155 @@
24
24
  // - In Node.js, dynamically imports 'ws' and assigns globalThis.WebSocket
25
25
  // All client code references the constructor via helper methods, not a top-level binding.
26
26
 
27
+ // AF_UNIX local-transport optimization (mirrors ipc/gar.cpp). When the endpoint
28
+ // resolves to an IP bound to a local interface, the GAR server is also listening
29
+ // on an abstract-namespace AF_UNIX socket at "\0gar.endpoint.<port>". Routing the
30
+ // WebSocket through that path bypasses the kernel TCP loopback stack — avoiding
31
+ // EDR/Falcon-style instrumentation overhead — while preserving the wire protocol.
32
+ //
33
+ // Browser environments have no raw socket access, so this is Node.js-only. The
34
+ // 'ws' library lets us inject a pre-connected net.Socket (or TLS-wrapped) via
35
+ // the createConnection option in the constructor.
36
+ //
37
+ // Set TRS_DISABLE_AF_UNIX=1 (also accepts true/on/yes, case-insensitive) to opt
38
+ // out and force the regular TCP path. Useful when capturing GAR traffic with
39
+ // tcpdump/wireshark on the loopback interface, which only sees TCP.
40
+
41
+ function _garAfUnixDisabled() {
42
+ const v = (typeof process !== 'undefined' && process.env)
43
+ ? process.env.TRS_DISABLE_AF_UNIX : undefined;
44
+ if (!v) return false;
45
+ return ['1', 'true', 'on', 'yes'].includes(v.toLowerCase());
46
+ }
47
+
48
+ let _garNetMod = null;
49
+ let _garTlsMod = null;
50
+ let _garOsMod = null;
51
+ let _garDnsMod = null;
52
+
53
+ async function _garLoadNodeModules() {
54
+ if (_garNetMod) return true;
55
+ const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
56
+ if (!isNode) return false;
57
+ try {
58
+ const { createRequire } = await import('node:module');
59
+ const req = createRequire(process.cwd() + '/');
60
+ _garNetMod = req('net');
61
+ _garTlsMod = req('tls');
62
+ _garOsMod = req('os');
63
+ _garDnsMod = req('dns');
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ function _garLocalIPv4Set() {
71
+ const set = new Set(['127.0.0.1']);
72
+ const ifaces = _garOsMod.networkInterfaces();
73
+ for (const name of Object.keys(ifaces)) {
74
+ for (const addr of ifaces[name] || []) {
75
+ if (addr.family === 'IPv4' || addr.family === 4) set.add(addr.address);
76
+ }
77
+ }
78
+ return set;
79
+ }
80
+
81
+ // Returns the optimal GAR endpoint string for |inetEndpoint|: if the URL's
82
+ // host resolves locally on this machine, returns the AF_UNIX form
83
+ // "unix:gar.endpoint.<port>"; otherwise echoes the input. Mirrors
84
+ // `trsutil --optimal-endpoint`. Async because DNS lookup in Node is async-only.
85
+ // Respects TRS_DISABLE_AF_UNIX. Does not probe the AF_UNIX listener — caller
86
+ // (or jsgar's connect path) handles fallback if it's unreachable.
87
+ async function optimalEndpoint(inetEndpoint) {
88
+ if (_garAfUnixDisabled()) return inetEndpoint;
89
+ if (!(await _garLoadNodeModules())) return inetEndpoint;
90
+ let parsed;
91
+ try { parsed = new URL(inetEndpoint); } catch { return inetEndpoint; }
92
+ const host = parsed.hostname;
93
+ const port = parsed.port;
94
+ if (!host || !port) return inetEndpoint;
95
+ let ip;
96
+ try {
97
+ if (_garNetMod.isIP(host)) {
98
+ ip = host;
99
+ } else {
100
+ const r = await _garDnsMod.promises.lookup(host, { family: 4 });
101
+ ip = r.address;
102
+ }
103
+ } catch { return inetEndpoint; }
104
+ if (ip.startsWith('127.') || _garLocalIPv4Set().has(ip)) {
105
+ return `unix:gar.endpoint.${port}`;
106
+ }
107
+ return inetEndpoint;
108
+ }
109
+
110
+ // Returns the abstract-namespace AF_UNIX path (a string starting with \u0000)
111
+ // for the given GAR WebSocket endpoint if (a) it resolves to a local IPv4 and
112
+ // (b) the abstract listener at the matching name is reachable. Otherwise null.
113
+ async function _garUnixAbstractPathForEndpoint(wsEndpoint) {
114
+ if (_garAfUnixDisabled()) return null;
115
+ if (!(await _garLoadNodeModules())) return null;
116
+ let parsed;
117
+ try {
118
+ parsed = new URL(wsEndpoint);
119
+ } catch {
120
+ return null;
121
+ }
122
+ const host = parsed.hostname;
123
+ const port = parsed.port;
124
+ if (!host || !port) return null;
125
+
126
+ let ip;
127
+ try {
128
+ if (_garNetMod.isIP(host)) {
129
+ ip = host;
130
+ } else {
131
+ const r = await _garDnsMod.promises.lookup(host, { family: 4 });
132
+ ip = r.address;
133
+ }
134
+ } catch {
135
+ return null;
136
+ }
137
+
138
+ // Loopback short-circuit; otherwise check against the host's interface IPs.
139
+ // (Less precise than RTM_GETROUTE used by the C++ side, but covers the
140
+ // practical "is this destination on this machine?" question for our use.)
141
+ if (!ip.startsWith('127.')) {
142
+ if (!_garLocalIPv4Set().has(ip)) return null;
143
+ }
144
+
145
+ const abstractPath = '\u0000gar.endpoint.' + port;
146
+ // Reachability probe: AF_UNIX local connect either succeeds immediately or
147
+ // fails immediately with ECONNREFUSED/ENOENT.
148
+ const reachable = await new Promise((resolve) => {
149
+ let done = false;
150
+ const finish = (ok) => {
151
+ if (done) return;
152
+ done = true;
153
+ try { sock.destroy(); } catch { /* ignore */ }
154
+ resolve(ok);
155
+ };
156
+ const sock = _garNetMod.createConnection({ path: abstractPath });
157
+ sock.once('connect', () => finish(true));
158
+ sock.once('error', () => finish(false));
159
+ setTimeout(() => finish(false), 500);
160
+ });
161
+ return reachable ? abstractPath : null;
162
+ }
163
+
164
+ function _garMakeUnixCreateConnectionFn(abstractPath, useSSL, sniHost, allowSelfSigned) {
165
+ return () => {
166
+ const unixSocket = _garNetMod.createConnection({ path: abstractPath });
167
+ if (!useSSL) return unixSocket;
168
+ return _garTlsMod.connect({
169
+ socket: unixSocket,
170
+ servername: sniHost,
171
+ rejectUnauthorized: !allowSelfSigned,
172
+ });
173
+ };
174
+ }
175
+
27
176
  class GARClient {
28
177
  /**
29
178
  * Initialize the GAR client.
@@ -163,13 +312,35 @@ class GARClient {
163
312
  const WS = await this._ensureWebSocketCtor();
164
313
  const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
165
314
 
166
- this.log('INFO', `Connecting to WebSocket server at ${this.wsEndpoint}`);
315
+ // Prefer AF_UNIX abstract-namespace transport for local destinations.
316
+ // Falls back to TCP automatically (next reconnect iteration) if the
317
+ // listener disappears between probe and real connect.
318
+ let unixAbstractPath = null;
319
+ let sniHost = null;
320
+ if (isNode) {
321
+ unixAbstractPath = await _garUnixAbstractPathForEndpoint(this.wsEndpoint);
322
+ if (unixAbstractPath) {
323
+ try { sniHost = new URL(this.wsEndpoint).hostname; } catch { /* ignore */ }
324
+ }
325
+ }
326
+
327
+ this.log('INFO', `Connecting to WebSocket server at ${this.wsEndpoint}${unixAbstractPath ? ' (via AF_UNIX)' : ''}`);
167
328
 
168
329
  const connectionPromise = new Promise((resolve, reject) => {
169
330
  let websocket;
170
- if (this.allowSelfSignedCertificate && isNode) {
331
+ const useSSL = this.wsEndpoint.toLowerCase().startsWith('wss://');
332
+ const wsOpts = {};
333
+ if (this.allowSelfSignedCertificate && isNode && useSSL) {
171
334
  // Node.js 'ws' supports options for TLS; browsers do not.
172
- websocket = new WS(this.wsEndpoint, ['gar-protocol'], { rejectUnauthorized: false });
335
+ wsOpts.rejectUnauthorized = false;
336
+ }
337
+ if (unixAbstractPath) {
338
+ wsOpts.createConnection = _garMakeUnixCreateConnectionFn(
339
+ unixAbstractPath, useSSL, sniHost, this.allowSelfSignedCertificate
340
+ );
341
+ }
342
+ if (Object.keys(wsOpts).length > 0) {
343
+ websocket = new WS(this.wsEndpoint, ['gar-protocol'], wsOpts);
173
344
  } else {
174
345
  websocket = new WS(this.wsEndpoint, ['gar-protocol']);
175
346
  }
@@ -1512,3 +1683,4 @@ class GARClient {
1512
1683
  }
1513
1684
 
1514
1685
  export default GARClient;
1686
+ export { optimalEndpoint };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsgar",
3
- "version": "4.5.3",
3
+ "version": "4.6.0",
4
4
  "description": "A Javascript client for the GAR protocol",
5
5
  "type": "module",
6
6
  "main": "dist/gar.umd.js",
@@ -36,7 +36,7 @@
36
36
  "@eslint/json": "^0.13.2",
37
37
  "@rollup/plugin-commonjs": "^24.0.0",
38
38
  "@rollup/plugin-node-resolve": "^15.0.0",
39
- "eslint": "^9.38.0",
39
+ "eslint": "^9.39.4",
40
40
  "globals": "^16.0.0",
41
41
  "rollup": "^3.0.0"
42
42
  }