jsgar 4.5.2 → 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.
- package/dist/gar.umd.js +188 -13
- package/gar.js +181 -8
- 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' ?
|
|
3
|
-
typeof define === 'function' && define.amd ? define(factory) :
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.GARClient =
|
|
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.
|
|
@@ -74,7 +223,7 @@
|
|
|
74
223
|
this.localKeyMap = new Map();
|
|
75
224
|
this.invalidatedKeyIds = new Set();
|
|
76
225
|
this.recordMap = new Map();
|
|
77
|
-
this._activeOwnership = { msg_ref: 0, ownership_action: 'None', skip_ownership_checks: false, client_key_id: 0 };
|
|
226
|
+
this._activeOwnership = { msg_ref: 0, ownership_action: 'None', skip_ownership_checks: false, require_existing: false, client_key_id: 0 };
|
|
78
227
|
|
|
79
228
|
this.running = false;
|
|
80
229
|
this.heartbeatIntervalId = null;
|
|
@@ -142,7 +291,7 @@
|
|
|
142
291
|
this.invalidatedKeyIds.clear();
|
|
143
292
|
|
|
144
293
|
// Active ownership state
|
|
145
|
-
this._activeOwnership = { msg_ref: 0, ownership_action: 'None', skip_ownership_checks: false, client_key_id: 0 };
|
|
294
|
+
this._activeOwnership = { msg_ref: 0, ownership_action: 'None', skip_ownership_checks: false, require_existing: false, client_key_id: 0 };
|
|
146
295
|
|
|
147
296
|
// Heartbeat grace period flags
|
|
148
297
|
this._initialGracePeriod = false;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1145,12 +1316,13 @@
|
|
|
1145
1316
|
* @param {number} [options.msgRef=0] - Message reference for correlating recoverable error responses.
|
|
1146
1317
|
* @param {string} [options.ownershipAction='None'] - 'None' (check only), 'Acquire', or 'Release'.
|
|
1147
1318
|
* @param {boolean} [options.skipOwnershipChecks=false] - Bypass ownership validation.
|
|
1319
|
+
* @param {boolean} [options.requireExisting=false] - Only write to existing keys; do not create new keys.
|
|
1148
1320
|
* @param {number} [options.clientKeyId=0] - The g::Client key ID for ownership. 0 uses the connection key.
|
|
1149
1321
|
*/
|
|
1150
|
-
updateActiveOwnership({ msgRef = 0, ownershipAction = 'None', skipOwnershipChecks = false, clientKeyId = 0 } = {}) {
|
|
1151
|
-
const newOwnership = { msg_ref: msgRef, ownership_action: ownershipAction, skip_ownership_checks: skipOwnershipChecks, client_key_id: clientKeyId };
|
|
1322
|
+
updateActiveOwnership({ msgRef = 0, ownershipAction = 'None', skipOwnershipChecks = false, requireExisting = false, clientKeyId = 0 } = {}) {
|
|
1323
|
+
const newOwnership = { msg_ref: msgRef, ownership_action: ownershipAction, skip_ownership_checks: skipOwnershipChecks, require_existing: requireExisting, client_key_id: clientKeyId };
|
|
1152
1324
|
const cur = this._activeOwnership;
|
|
1153
|
-
if (newOwnership.msg_ref !== cur.msg_ref || newOwnership.ownership_action !== cur.ownership_action || newOwnership.skip_ownership_checks !== cur.skip_ownership_checks || newOwnership.client_key_id !== cur.client_key_id) {
|
|
1325
|
+
if (newOwnership.msg_ref !== cur.msg_ref || newOwnership.ownership_action !== cur.ownership_action || newOwnership.skip_ownership_checks !== cur.skip_ownership_checks || newOwnership.require_existing !== cur.require_existing || newOwnership.client_key_id !== cur.client_key_id) {
|
|
1154
1326
|
this._activeOwnership = newOwnership;
|
|
1155
1327
|
this.sendMessage({ message_type: 'ActiveOwnership', value: newOwnership });
|
|
1156
1328
|
}
|
|
@@ -1516,6 +1688,9 @@
|
|
|
1516
1688
|
}
|
|
1517
1689
|
}
|
|
1518
1690
|
|
|
1519
|
-
|
|
1691
|
+
exports.default = GARClient;
|
|
1692
|
+
exports.optimalEndpoint = optimalEndpoint;
|
|
1693
|
+
|
|
1694
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
1520
1695
|
|
|
1521
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.
|
|
@@ -68,7 +217,7 @@ class GARClient {
|
|
|
68
217
|
this.localKeyMap = new Map();
|
|
69
218
|
this.invalidatedKeyIds = new Set();
|
|
70
219
|
this.recordMap = new Map();
|
|
71
|
-
this._activeOwnership = { msg_ref: 0, ownership_action: 'None', skip_ownership_checks: false, client_key_id: 0 };
|
|
220
|
+
this._activeOwnership = { msg_ref: 0, ownership_action: 'None', skip_ownership_checks: false, require_existing: false, client_key_id: 0 };
|
|
72
221
|
|
|
73
222
|
this.running = false;
|
|
74
223
|
this.heartbeatIntervalId = null;
|
|
@@ -136,7 +285,7 @@ class GARClient {
|
|
|
136
285
|
this.invalidatedKeyIds.clear();
|
|
137
286
|
|
|
138
287
|
// Active ownership state
|
|
139
|
-
this._activeOwnership = { msg_ref: 0, ownership_action: 'None', skip_ownership_checks: false, client_key_id: 0 };
|
|
288
|
+
this._activeOwnership = { msg_ref: 0, ownership_action: 'None', skip_ownership_checks: false, require_existing: false, client_key_id: 0 };
|
|
140
289
|
|
|
141
290
|
// Heartbeat grace period flags
|
|
142
291
|
this._initialGracePeriod = false;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1139,12 +1310,13 @@ class GARClient {
|
|
|
1139
1310
|
* @param {number} [options.msgRef=0] - Message reference for correlating recoverable error responses.
|
|
1140
1311
|
* @param {string} [options.ownershipAction='None'] - 'None' (check only), 'Acquire', or 'Release'.
|
|
1141
1312
|
* @param {boolean} [options.skipOwnershipChecks=false] - Bypass ownership validation.
|
|
1313
|
+
* @param {boolean} [options.requireExisting=false] - Only write to existing keys; do not create new keys.
|
|
1142
1314
|
* @param {number} [options.clientKeyId=0] - The g::Client key ID for ownership. 0 uses the connection key.
|
|
1143
1315
|
*/
|
|
1144
|
-
updateActiveOwnership({ msgRef = 0, ownershipAction = 'None', skipOwnershipChecks = false, clientKeyId = 0 } = {}) {
|
|
1145
|
-
const newOwnership = { msg_ref: msgRef, ownership_action: ownershipAction, skip_ownership_checks: skipOwnershipChecks, client_key_id: clientKeyId };
|
|
1316
|
+
updateActiveOwnership({ msgRef = 0, ownershipAction = 'None', skipOwnershipChecks = false, requireExisting = false, clientKeyId = 0 } = {}) {
|
|
1317
|
+
const newOwnership = { msg_ref: msgRef, ownership_action: ownershipAction, skip_ownership_checks: skipOwnershipChecks, require_existing: requireExisting, client_key_id: clientKeyId };
|
|
1146
1318
|
const cur = this._activeOwnership;
|
|
1147
|
-
if (newOwnership.msg_ref !== cur.msg_ref || newOwnership.ownership_action !== cur.ownership_action || newOwnership.skip_ownership_checks !== cur.skip_ownership_checks || newOwnership.client_key_id !== cur.client_key_id) {
|
|
1319
|
+
if (newOwnership.msg_ref !== cur.msg_ref || newOwnership.ownership_action !== cur.ownership_action || newOwnership.skip_ownership_checks !== cur.skip_ownership_checks || newOwnership.require_existing !== cur.require_existing || newOwnership.client_key_id !== cur.client_key_id) {
|
|
1148
1320
|
this._activeOwnership = newOwnership;
|
|
1149
1321
|
this.sendMessage({ message_type: 'ActiveOwnership', value: newOwnership });
|
|
1150
1322
|
}
|
|
@@ -1511,3 +1683,4 @@ class GARClient {
|
|
|
1511
1683
|
}
|
|
1512
1684
|
|
|
1513
1685
|
export default GARClient;
|
|
1686
|
+
export { optimalEndpoint };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgar",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
39
|
+
"eslint": "^9.39.4",
|
|
40
40
|
"globals": "^16.0.0",
|
|
41
41
|
"rollup": "^3.0.0"
|
|
42
42
|
}
|