jsgar 4.5.3 → 4.6.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.
Files changed (3) hide show
  1. package/dist/gar.umd.js +188 -8
  2. package/gar.js +181 -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,159 @@
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
+ // Skip AF_UNIX for SSL — OpenSSL's non-blocking handshake over AF_UNIX
96
+ // produces sporadic failures.
97
+ const lower = inetEndpoint.toLowerCase();
98
+ if (lower.startsWith('wss://') || lower.startsWith('https://')) return inetEndpoint;
99
+ if (!(await _garLoadNodeModules())) return inetEndpoint;
100
+ let parsed;
101
+ try { parsed = new URL(inetEndpoint); } catch { return inetEndpoint; }
102
+ const host = parsed.hostname;
103
+ const port = parsed.port;
104
+ if (!host || !port) return inetEndpoint;
105
+ let ip;
106
+ try {
107
+ if (_garNetMod.isIP(host)) {
108
+ ip = host;
109
+ } else {
110
+ const r = await _garDnsMod.promises.lookup(host, { family: 4 });
111
+ ip = r.address;
112
+ }
113
+ } catch { return inetEndpoint; }
114
+ if (ip.startsWith('127.') || _garLocalIPv4Set().has(ip)) {
115
+ return `unix:gar.endpoint.${port}`;
116
+ }
117
+ return inetEndpoint;
118
+ }
119
+
120
+ // Returns the abstract-namespace AF_UNIX path (a string starting with \u0000)
121
+ // for the given GAR WebSocket endpoint if (a) it resolves to a local IPv4 and
122
+ // (b) the abstract listener at the matching name is reachable. Otherwise null.
123
+ async function _garUnixAbstractPathForEndpoint(wsEndpoint) {
124
+ if (_garAfUnixDisabled()) return null;
125
+ if (!(await _garLoadNodeModules())) return null;
126
+ let parsed;
127
+ try {
128
+ parsed = new URL(wsEndpoint);
129
+ } catch {
130
+ return null;
131
+ }
132
+ const host = parsed.hostname;
133
+ const port = parsed.port;
134
+ if (!host || !port) return null;
135
+
136
+ let ip;
137
+ try {
138
+ if (_garNetMod.isIP(host)) {
139
+ ip = host;
140
+ } else {
141
+ const r = await _garDnsMod.promises.lookup(host, { family: 4 });
142
+ ip = r.address;
143
+ }
144
+ } catch {
145
+ return null;
146
+ }
147
+
148
+ // Loopback short-circuit; otherwise check against the host's interface IPs.
149
+ // (Less precise than RTM_GETROUTE used by the C++ side, but covers the
150
+ // practical "is this destination on this machine?" question for our use.)
151
+ if (!ip.startsWith('127.')) {
152
+ if (!_garLocalIPv4Set().has(ip)) return null;
153
+ }
154
+
155
+ const abstractPath = '\u0000gar.endpoint.' + port;
156
+ // Reachability probe: AF_UNIX local connect either succeeds immediately or
157
+ // fails immediately with ECONNREFUSED/ENOENT.
158
+ const reachable = await new Promise((resolve) => {
159
+ let done = false;
160
+ const finish = (ok) => {
161
+ if (done) return;
162
+ done = true;
163
+ try { sock.destroy(); } catch { /* ignore */ }
164
+ resolve(ok);
165
+ };
166
+ const sock = _garNetMod.createConnection({ path: abstractPath });
167
+ sock.once('connect', () => finish(true));
168
+ sock.once('error', () => finish(false));
169
+ setTimeout(() => finish(false), 500);
170
+ });
171
+ return reachable ? abstractPath : null;
172
+ }
173
+
174
+ function _garMakeUnixCreateConnectionFn(abstractPath, useSSL, sniHost, allowSelfSigned) {
175
+ return () => {
176
+ const unixSocket = _garNetMod.createConnection({ path: abstractPath });
177
+ if (!useSSL) return unixSocket;
178
+ return _garTlsMod.connect({
179
+ socket: unixSocket,
180
+ servername: sniHost,
181
+ rejectUnauthorized: !allowSelfSigned,
182
+ });
183
+ };
184
+ }
185
+
33
186
  class GARClient {
34
187
  /**
35
188
  * Initialize the GAR client.
@@ -169,13 +322,37 @@
169
322
  const WS = await this._ensureWebSocketCtor();
170
323
  const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
171
324
 
172
- this.log('INFO', `Connecting to WebSocket server at ${this.wsEndpoint}`);
325
+ // Prefer AF_UNIX abstract-namespace transport for local destinations.
326
+ // Falls back to TCP automatically (next reconnect iteration) if the
327
+ // listener disappears between probe and real connect.
328
+ // Skip AF_UNIX for SSL — OpenSSL's non-blocking handshake over
329
+ // AF_UNIX produces sporadic failures.
330
+ const useSSL = this.wsEndpoint.toLowerCase().startsWith('wss://');
331
+ let unixAbstractPath = null;
332
+ let sniHost = null;
333
+ if (isNode && !useSSL) {
334
+ unixAbstractPath = await _garUnixAbstractPathForEndpoint(this.wsEndpoint);
335
+ if (unixAbstractPath) {
336
+ try { sniHost = new URL(this.wsEndpoint).hostname; } catch { /* ignore */ }
337
+ }
338
+ }
339
+
340
+ this.log('INFO', `Connecting to WebSocket server at ${this.wsEndpoint}${unixAbstractPath ? ' (via AF_UNIX)' : ''}`);
173
341
 
174
342
  const connectionPromise = new Promise((resolve, reject) => {
175
343
  let websocket;
176
- if (this.allowSelfSignedCertificate && isNode) {
344
+ const wsOpts = {};
345
+ if (this.allowSelfSignedCertificate && isNode && useSSL) {
177
346
  // Node.js 'ws' supports options for TLS; browsers do not.
178
- websocket = new WS(this.wsEndpoint, ['gar-protocol'], { rejectUnauthorized: false });
347
+ wsOpts.rejectUnauthorized = false;
348
+ }
349
+ if (unixAbstractPath) {
350
+ wsOpts.createConnection = _garMakeUnixCreateConnectionFn(
351
+ unixAbstractPath, useSSL, sniHost, this.allowSelfSignedCertificate
352
+ );
353
+ }
354
+ if (Object.keys(wsOpts).length > 0) {
355
+ websocket = new WS(this.wsEndpoint, ['gar-protocol'], wsOpts);
179
356
  } else {
180
357
  websocket = new WS(this.wsEndpoint, ['gar-protocol']);
181
358
  }
@@ -1517,6 +1694,9 @@
1517
1694
  }
1518
1695
  }
1519
1696
 
1520
- return GARClient;
1697
+ exports.default = GARClient;
1698
+ exports.optimalEndpoint = optimalEndpoint;
1699
+
1700
+ Object.defineProperty(exports, '__esModule', { value: true });
1521
1701
 
1522
1702
  }));
package/gar.js CHANGED
@@ -24,6 +24,159 @@
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
+ // Skip AF_UNIX for SSL — OpenSSL's non-blocking handshake over AF_UNIX
90
+ // produces sporadic failures.
91
+ const lower = inetEndpoint.toLowerCase();
92
+ if (lower.startsWith('wss://') || lower.startsWith('https://')) return inetEndpoint;
93
+ if (!(await _garLoadNodeModules())) return inetEndpoint;
94
+ let parsed;
95
+ try { parsed = new URL(inetEndpoint); } catch { return inetEndpoint; }
96
+ const host = parsed.hostname;
97
+ const port = parsed.port;
98
+ if (!host || !port) return inetEndpoint;
99
+ let ip;
100
+ try {
101
+ if (_garNetMod.isIP(host)) {
102
+ ip = host;
103
+ } else {
104
+ const r = await _garDnsMod.promises.lookup(host, { family: 4 });
105
+ ip = r.address;
106
+ }
107
+ } catch { return inetEndpoint; }
108
+ if (ip.startsWith('127.') || _garLocalIPv4Set().has(ip)) {
109
+ return `unix:gar.endpoint.${port}`;
110
+ }
111
+ return inetEndpoint;
112
+ }
113
+
114
+ // Returns the abstract-namespace AF_UNIX path (a string starting with \u0000)
115
+ // for the given GAR WebSocket endpoint if (a) it resolves to a local IPv4 and
116
+ // (b) the abstract listener at the matching name is reachable. Otherwise null.
117
+ async function _garUnixAbstractPathForEndpoint(wsEndpoint) {
118
+ if (_garAfUnixDisabled()) return null;
119
+ if (!(await _garLoadNodeModules())) return null;
120
+ let parsed;
121
+ try {
122
+ parsed = new URL(wsEndpoint);
123
+ } catch {
124
+ return null;
125
+ }
126
+ const host = parsed.hostname;
127
+ const port = parsed.port;
128
+ if (!host || !port) return null;
129
+
130
+ let ip;
131
+ try {
132
+ if (_garNetMod.isIP(host)) {
133
+ ip = host;
134
+ } else {
135
+ const r = await _garDnsMod.promises.lookup(host, { family: 4 });
136
+ ip = r.address;
137
+ }
138
+ } catch {
139
+ return null;
140
+ }
141
+
142
+ // Loopback short-circuit; otherwise check against the host's interface IPs.
143
+ // (Less precise than RTM_GETROUTE used by the C++ side, but covers the
144
+ // practical "is this destination on this machine?" question for our use.)
145
+ if (!ip.startsWith('127.')) {
146
+ if (!_garLocalIPv4Set().has(ip)) return null;
147
+ }
148
+
149
+ const abstractPath = '\u0000gar.endpoint.' + port;
150
+ // Reachability probe: AF_UNIX local connect either succeeds immediately or
151
+ // fails immediately with ECONNREFUSED/ENOENT.
152
+ const reachable = await new Promise((resolve) => {
153
+ let done = false;
154
+ const finish = (ok) => {
155
+ if (done) return;
156
+ done = true;
157
+ try { sock.destroy(); } catch { /* ignore */ }
158
+ resolve(ok);
159
+ };
160
+ const sock = _garNetMod.createConnection({ path: abstractPath });
161
+ sock.once('connect', () => finish(true));
162
+ sock.once('error', () => finish(false));
163
+ setTimeout(() => finish(false), 500);
164
+ });
165
+ return reachable ? abstractPath : null;
166
+ }
167
+
168
+ function _garMakeUnixCreateConnectionFn(abstractPath, useSSL, sniHost, allowSelfSigned) {
169
+ return () => {
170
+ const unixSocket = _garNetMod.createConnection({ path: abstractPath });
171
+ if (!useSSL) return unixSocket;
172
+ return _garTlsMod.connect({
173
+ socket: unixSocket,
174
+ servername: sniHost,
175
+ rejectUnauthorized: !allowSelfSigned,
176
+ });
177
+ };
178
+ }
179
+
27
180
  class GARClient {
28
181
  /**
29
182
  * Initialize the GAR client.
@@ -163,13 +316,37 @@ class GARClient {
163
316
  const WS = await this._ensureWebSocketCtor();
164
317
  const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
165
318
 
166
- this.log('INFO', `Connecting to WebSocket server at ${this.wsEndpoint}`);
319
+ // Prefer AF_UNIX abstract-namespace transport for local destinations.
320
+ // Falls back to TCP automatically (next reconnect iteration) if the
321
+ // listener disappears between probe and real connect.
322
+ // Skip AF_UNIX for SSL — OpenSSL's non-blocking handshake over
323
+ // AF_UNIX produces sporadic failures.
324
+ const useSSL = this.wsEndpoint.toLowerCase().startsWith('wss://');
325
+ let unixAbstractPath = null;
326
+ let sniHost = null;
327
+ if (isNode && !useSSL) {
328
+ unixAbstractPath = await _garUnixAbstractPathForEndpoint(this.wsEndpoint);
329
+ if (unixAbstractPath) {
330
+ try { sniHost = new URL(this.wsEndpoint).hostname; } catch { /* ignore */ }
331
+ }
332
+ }
333
+
334
+ this.log('INFO', `Connecting to WebSocket server at ${this.wsEndpoint}${unixAbstractPath ? ' (via AF_UNIX)' : ''}`);
167
335
 
168
336
  const connectionPromise = new Promise((resolve, reject) => {
169
337
  let websocket;
170
- if (this.allowSelfSignedCertificate && isNode) {
338
+ const wsOpts = {};
339
+ if (this.allowSelfSignedCertificate && isNode && useSSL) {
171
340
  // Node.js 'ws' supports options for TLS; browsers do not.
172
- 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);
173
350
  } else {
174
351
  websocket = new WS(this.wsEndpoint, ['gar-protocol']);
175
352
  }
@@ -1512,3 +1689,4 @@ class GARClient {
1512
1689
  }
1513
1690
 
1514
1691
  export default GARClient;
1692
+ 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.2",
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
  }