@persistica/flux-agent 0.0.1 → 0.0.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.
- package/index.cjs +1074 -10
- package/package.json +5 -2
- package/src/index.d.ts +1 -1
- package/src/lib/channel/channel.type.d.ts +10 -0
- package/src/lib/connector/auth/register-client.auth.d.ts +18 -0
- package/src/lib/connector/flux-client-data.class.d.ts +11 -0
- package/src/lib/connector/low-level-com/web-rtc/ice-connection.d.ts +34 -0
- package/src/lib/connector/rpc/NOT-USED-network-authority.class.d.ts +4 -0
- package/src/lib/connector/state-machines/connect-to-remote-client.d.ts +6 -0
- package/src/lib/flux-agent.class.d.ts +43 -0
- package/src/lib/flux-interface.class.d.ts +12 -0
- package/src/lib/flux-remote-client.class.d.ts +10 -0
- package/src/lib/route-transport-coordinator.d.ts +8 -0
- package/src/lib/utils/obscuring/decrypt.utils.d.ts +13 -0
- package/src/lib/utils/obscuring/encyprt.utils.d.ts +12 -0
- package/src/lib/utils/obscuring/shared.utils.d.ts +9 -0
- package/src/lib/persistica-flux-agent-2.d.ts +0 -1
package/index.cjs
CHANGED
|
@@ -17,19 +17,1083 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
17
|
};
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
|
|
20
|
-
// packages/flux/agent
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
23
|
-
|
|
20
|
+
// packages/flux/agent/src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FluxAgent: () => FluxAgent
|
|
24
24
|
});
|
|
25
|
-
module.exports = __toCommonJS(
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// libs/flux/shared/types/src/lib/channel.type.ts
|
|
28
|
+
var validateChannelNameOrThrow = (channelName) => {
|
|
29
|
+
if (channelName.includes(":")) {
|
|
30
|
+
throw new Error("Channel name cannot contain :");
|
|
31
|
+
}
|
|
32
|
+
if (channelName.includes("/")) {
|
|
33
|
+
throw new Error("Channel name cannot contain /");
|
|
34
|
+
}
|
|
35
|
+
if (!/^[A-Za-z0-9-]+$/.test(channelName)) {
|
|
36
|
+
throw new Error("Channel name can only contain letters, numbers and dashes ('-')");
|
|
37
|
+
}
|
|
38
|
+
if (channelName.length > 100) {
|
|
39
|
+
throw new Error("Channel name cannot be longer than 100 characters");
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// libs/flux/shared/types/src/lib/auth/auth.fn.ts
|
|
45
|
+
var checkAuthTicketShape = (ticket) => {
|
|
46
|
+
return typeof ticket === "string";
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// libs/flux/shared/types/src/lib/low-level-ws.interfaces.ts
|
|
50
|
+
var RPC_REQUEST = "rpc-request";
|
|
51
|
+
var RPC_RESPONSE = "rpc-response";
|
|
52
|
+
var CONNECT_TO_CLIENT = "connect-to-client";
|
|
53
|
+
var NETWORK_CHANNEL_PUBLISH = "nc-pub";
|
|
54
|
+
var ON_NETWORK_CHANNEL_PUBLISH = "nc-on-pub";
|
|
55
|
+
var SUBSCRIBE_NETWORK_CHANNEL_NAME = "subscribe-to-nc-name";
|
|
56
|
+
var SUBSCRIBED_NETWORK_CHANNEL_NAME = "subscribed-to-nc-name";
|
|
57
|
+
var UNSUBSCRIBE_NETWORK_CHANNEL_NAME = "unsubscribe-nc-name";
|
|
58
|
+
var AUTHORITY_CHANNEL_SUBSCRIBE = "acs";
|
|
59
|
+
var AUTHORITY_DISCONNECT_AGENT = "ada";
|
|
60
|
+
|
|
61
|
+
// libs/flux/shared/types/src/lib/client.type.ts
|
|
62
|
+
var validateAgentUIDOrThrow = (clientOwnUID) => {
|
|
63
|
+
if (typeof clientOwnUID !== "string") {
|
|
64
|
+
throw new Error("UID must be a string");
|
|
65
|
+
}
|
|
66
|
+
if (!/^[A-Za-z0-9-]+$/.test(clientOwnUID)) {
|
|
67
|
+
throw new Error("UID can only contain letters, numbers and dashes ('-')");
|
|
68
|
+
}
|
|
69
|
+
if (clientOwnUID.length > 50) {
|
|
70
|
+
throw new Error("UID cannot be longer than 50 characters");
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// libs/flux/shared/ws/src/lib/rpc/rpc-server.class.ts
|
|
76
|
+
var RPCServer = class {
|
|
77
|
+
methods = /* @__PURE__ */ new Map();
|
|
78
|
+
/**
|
|
79
|
+
* Registers a method to be called by the client.
|
|
80
|
+
*
|
|
81
|
+
* @param { TMethodName } methodName
|
|
82
|
+
* @param { TCallback } fn
|
|
83
|
+
*
|
|
84
|
+
* @returns { void }
|
|
85
|
+
*/
|
|
86
|
+
registerMethod(methodName, fn) {
|
|
87
|
+
const existingMethod = this.methods.get(methodName);
|
|
88
|
+
if (existingMethod && existingMethod !== fn) {
|
|
89
|
+
throw new Error(`Method '${methodName}' already registered`);
|
|
90
|
+
}
|
|
91
|
+
this.methods.set(methodName, fn);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handles the request from the client.
|
|
95
|
+
*
|
|
96
|
+
* @param { RPCRequest } request
|
|
97
|
+
* @param { Function } sendToRPCClient
|
|
98
|
+
*
|
|
99
|
+
* @returns { void }
|
|
100
|
+
*/
|
|
101
|
+
async handleMessage(request, sendToRPCClient) {
|
|
102
|
+
const response = {
|
|
103
|
+
id: request.id,
|
|
104
|
+
rpcProcessAddress: request.originProcessAddress,
|
|
105
|
+
result: null
|
|
106
|
+
};
|
|
107
|
+
const method = this.methods.get(request.method);
|
|
108
|
+
if (method) {
|
|
109
|
+
try {
|
|
110
|
+
response.result = await method(...request.params);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
response.error = error.message;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
response.error = `Method '${request.method}' not found`;
|
|
116
|
+
}
|
|
117
|
+
sendToRPCClient(response);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// libs/flux/shared/ws/src/lib/ws-client.ts
|
|
122
|
+
var WebSocketClient = class extends RPCServer {
|
|
123
|
+
options;
|
|
124
|
+
eventListeners = {
|
|
125
|
+
open: [],
|
|
126
|
+
message: [],
|
|
127
|
+
close: [],
|
|
128
|
+
error: [],
|
|
129
|
+
connecting: []
|
|
130
|
+
};
|
|
131
|
+
reconnectAttempts = 0;
|
|
132
|
+
ws;
|
|
133
|
+
isOpen = false;
|
|
134
|
+
// Is the connection open
|
|
135
|
+
constructor(options) {
|
|
136
|
+
super();
|
|
137
|
+
this.options = {
|
|
138
|
+
autoReconnect: true,
|
|
139
|
+
reconnectDelay: 1e3,
|
|
140
|
+
heartbeatInterval: 6e4,
|
|
141
|
+
connectionTimeout: 5e3,
|
|
142
|
+
...options
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
*
|
|
147
|
+
* @returns { Promise<void> }
|
|
148
|
+
*/
|
|
149
|
+
connect() {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
this.emit("connecting", this.reconnectAttempts);
|
|
152
|
+
this.ws = new WebSocket(
|
|
153
|
+
this.options.url
|
|
154
|
+
);
|
|
155
|
+
const timeout = setTimeout(() => {
|
|
156
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
157
|
+
this.ws?.close();
|
|
158
|
+
reject(new Error("Connection timeout"));
|
|
159
|
+
}
|
|
160
|
+
}, this.options.connectionTimeout);
|
|
161
|
+
this.ws.onopen = () => {
|
|
162
|
+
clearTimeout(timeout);
|
|
163
|
+
this.reconnectAttempts = 0;
|
|
164
|
+
this.isOpen = true;
|
|
165
|
+
this.emit("open");
|
|
166
|
+
resolve();
|
|
167
|
+
};
|
|
168
|
+
this.ws.onmessage = (event) => {
|
|
169
|
+
this.emit("message", event.data);
|
|
170
|
+
};
|
|
171
|
+
this.ws.onclose = () => {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
if (this.isOpen) {
|
|
174
|
+
this.emit("close");
|
|
175
|
+
this.isOpen = false;
|
|
176
|
+
}
|
|
177
|
+
if (this.options.autoReconnect && (this.options.retries === void 0 || this.reconnectAttempts < this.options.retries)) {
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
this.reconnectAttempts++;
|
|
180
|
+
this.connect();
|
|
181
|
+
}, Math.min(this.options.reconnectDelay ?? Number.POSITIVE_INFINITY, this.options.reconnectDelay * (this.reconnectAttempts || 1)));
|
|
182
|
+
} else if (this.options.autoReconnect) {
|
|
183
|
+
this.emit("error", new Error("Connection failed: retries exhausted"));
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
this.ws.onerror = (event) => {
|
|
187
|
+
if (event?.message?.includes("Failed to connect")) {
|
|
188
|
+
} else {
|
|
189
|
+
console.log("\u274C Error", event.message);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
emit(event, data) {
|
|
195
|
+
for (const listener of this.eventListeners[event]) {
|
|
196
|
+
listener(data);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
on(event, listener) {
|
|
200
|
+
this.eventListeners[event].push(listener);
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Clears all subscribers.
|
|
205
|
+
*
|
|
206
|
+
* @returns { void }
|
|
207
|
+
*/
|
|
208
|
+
clearEventSubscribers() {
|
|
209
|
+
this.eventListeners.open = [];
|
|
210
|
+
this.eventListeners.message = [];
|
|
211
|
+
this.eventListeners.close = [];
|
|
212
|
+
this.eventListeners.connecting = [];
|
|
213
|
+
this.eventListeners.error = [];
|
|
214
|
+
}
|
|
215
|
+
send(message) {
|
|
216
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
217
|
+
this.ws.send(message);
|
|
218
|
+
} else {
|
|
219
|
+
console.warn("WebSocket not open.");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
close() {
|
|
223
|
+
this.options.autoReconnect = false;
|
|
224
|
+
this.ws?.close();
|
|
225
|
+
this.clearEventSubscribers();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var FluxWebSocketClientConnection = class extends WebSocketClient {
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// packages/flux/agent/src/lib/flux-remote-client.class.ts
|
|
232
|
+
var FluxRemoteClient = class {
|
|
233
|
+
constructor(iceConnection) {
|
|
234
|
+
this.iceConnection = iceConnection;
|
|
235
|
+
}
|
|
236
|
+
callProcedure() {
|
|
237
|
+
}
|
|
238
|
+
send(message) {
|
|
239
|
+
this.iceConnection.sendMessage(message);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// packages/flux/agent/src/lib/connector/low-level-com/web-rtc/ice-connection.ts
|
|
244
|
+
var peerConnectionConfig = {
|
|
245
|
+
iceServers: [
|
|
246
|
+
{ urls: "stun:stun.stunprotocol.org:3478" },
|
|
247
|
+
{ urls: "stun:stun2.1.google.com:19302" },
|
|
248
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
249
|
+
]
|
|
250
|
+
};
|
|
251
|
+
var ICEConnection = class {
|
|
252
|
+
constructor(_fluxWebSocketClientConnection, _stateChange) {
|
|
253
|
+
this._fluxWebSocketClientConnection = _fluxWebSocketClientConnection;
|
|
254
|
+
this._stateChange = _stateChange;
|
|
255
|
+
this.peerConnection.ondatachannel = (event) => {
|
|
256
|
+
console.log("*********** GOT DATAT SCHANNNEL ");
|
|
257
|
+
this.dataChannel = event.channel;
|
|
258
|
+
this.dataChannel.onopen = () => console.log("ReceiveChannel opened");
|
|
259
|
+
this.dataChannel.onmessage = (event2) => {
|
|
260
|
+
console.log("Receiving ICE event", event2);
|
|
261
|
+
};
|
|
262
|
+
this.dataChannel.onopen = () => {
|
|
263
|
+
console.log("Opened");
|
|
264
|
+
};
|
|
265
|
+
this.dataChannel.onclose = () => {
|
|
266
|
+
console.log("Opened");
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
this.peerConnection.onicecandidate = (event) => {
|
|
270
|
+
if (event.candidate) {
|
|
271
|
+
console.log("Send this ICE Candidate to the peer");
|
|
272
|
+
}
|
|
273
|
+
if (event.candidate === null) {
|
|
274
|
+
this.offerSDP = JSON.stringify(this.peerConnection.localDescription);
|
|
275
|
+
console.log("offerSDP:", this.offerSDP);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
this._fluxWebSocketClientConnection.registerMethod("createOffer", this.createOffer.bind(this));
|
|
279
|
+
this._fluxWebSocketClientConnection.registerMethod("acceptOffer", this.acceptOffer.bind(this));
|
|
280
|
+
this._fluxWebSocketClientConnection.registerMethod("acceptAnswer", this.acceptAnswer.bind(this));
|
|
281
|
+
this._fluxWebSocketClientConnection.registerMethod("answerAcceptedByInitiator", this.answerAcceptedByInitiator.bind(this));
|
|
282
|
+
}
|
|
283
|
+
//extends RPCServer<'createOffer' | 'acceptOffer' | 'acceptAnswer'> {
|
|
284
|
+
peerConnection = new RTCPeerConnection(peerConnectionConfig);
|
|
285
|
+
dataChannel;
|
|
286
|
+
// = this.peerConnection.createDataChannel('flux-channel');
|
|
287
|
+
offerSDP;
|
|
288
|
+
/**
|
|
289
|
+
* This offer must be sent to the other peer.
|
|
290
|
+
*/
|
|
291
|
+
async createOffer() {
|
|
292
|
+
console.log("creating offer and data channel");
|
|
293
|
+
this.dataChannel = this.peerConnection.createDataChannel("flux-channel");
|
|
294
|
+
this._stateChange("creating-offer");
|
|
295
|
+
const offer = await this.peerConnection.createOffer();
|
|
296
|
+
await this.peerConnection.setLocalDescription(offer);
|
|
297
|
+
console.log("Send this Offer to the peer:", offer);
|
|
298
|
+
return offer;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
*
|
|
302
|
+
* @param { RTCSessionDescriptionInit } offer
|
|
303
|
+
*/
|
|
304
|
+
async acceptOffer(offer) {
|
|
305
|
+
this._stateChange("setting-remote-offer");
|
|
306
|
+
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
|
307
|
+
const answer = await this.peerConnection.createAnswer();
|
|
308
|
+
this._stateChange("creating-answer");
|
|
309
|
+
await this.peerConnection.setLocalDescription(answer);
|
|
310
|
+
console.log("Send this Answer to the peer");
|
|
311
|
+
return answer;
|
|
312
|
+
}
|
|
313
|
+
async acceptAnswer(answer) {
|
|
314
|
+
this._stateChange("setting-remote-answer");
|
|
315
|
+
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
|
|
316
|
+
this._stateChange("connected");
|
|
317
|
+
console.log("the answer was accepted");
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* If remote (not initiator) the answer was accepted.
|
|
322
|
+
*
|
|
323
|
+
* @param answer
|
|
324
|
+
* @returns
|
|
325
|
+
*/
|
|
326
|
+
answerAcceptedByInitiator(answerAccepted) {
|
|
327
|
+
this._stateChange(answerAccepted ? "connected" : "failed");
|
|
328
|
+
}
|
|
329
|
+
sendMessage(message) {
|
|
330
|
+
console.log("Sending message over webRTC");
|
|
331
|
+
if (this.dataChannel && this.dataChannel.readyState === "open") {
|
|
332
|
+
this.dataChannel.send(message);
|
|
333
|
+
} else {
|
|
334
|
+
console.warn("Connection is not established yet.", this.dataChannel?.readyState);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// libs/flux/shared/connection/src/lib/agent/flux-agent-network.class.ts
|
|
340
|
+
var FluxAgentNetworkConnection = class {
|
|
341
|
+
constructor(_fluxWebSocketConnection, _fluxWebSocketClientConnection, _webRTCStateChange) {
|
|
342
|
+
this._fluxWebSocketConnection = _fluxWebSocketConnection;
|
|
343
|
+
this._fluxWebSocketClientConnection = _fluxWebSocketClientConnection;
|
|
344
|
+
this._webRTCStateChange = _webRTCStateChange;
|
|
345
|
+
if (typeof RTCPeerConnection !== "undefined") {
|
|
346
|
+
this.iceConnection = new ICEConnection(
|
|
347
|
+
this._fluxWebSocketClientConnection,
|
|
348
|
+
this._webRTCStateChange
|
|
349
|
+
);
|
|
350
|
+
console.log("RTCPeerConnection is available", !!this.iceConnection);
|
|
351
|
+
} else {
|
|
352
|
+
console.warn("\u2757WebRTC is not available in this environment.");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
iceConnection;
|
|
356
|
+
connectedChannelSet = /* @__PURE__ */ new Set();
|
|
357
|
+
/**
|
|
358
|
+
* Joins a channel on the network.
|
|
359
|
+
*
|
|
360
|
+
* @param { string } channelName
|
|
361
|
+
*/
|
|
362
|
+
async joinChannel(channelName) {
|
|
363
|
+
console.log(`Joining channel "${channelName}"`);
|
|
364
|
+
if (!validateChannelNameOrThrow(channelName)) {
|
|
365
|
+
throw new Error("This will not actually be thrown");
|
|
366
|
+
}
|
|
367
|
+
const fluxNetworkChannel = await this._fluxWebSocketConnection.joinChannel(
|
|
368
|
+
channelName
|
|
369
|
+
);
|
|
370
|
+
this.connectedChannelSet.add(fluxNetworkChannel.channelName);
|
|
371
|
+
return fluxNetworkChannel;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Listen to messages on this channel.
|
|
375
|
+
*
|
|
376
|
+
* @param { string } channelName
|
|
377
|
+
*
|
|
378
|
+
* @returns { Promise<void> }
|
|
379
|
+
*/
|
|
380
|
+
async leaveChannel(channelName) {
|
|
381
|
+
console.log(`Leaving channel "${channelName}"`);
|
|
382
|
+
if (!validateChannelNameOrThrow(channelName)) {
|
|
383
|
+
throw new Error("This will not actually be thrown");
|
|
384
|
+
}
|
|
385
|
+
await this._fluxWebSocketConnection.leaveChannel(
|
|
386
|
+
channelName
|
|
387
|
+
);
|
|
388
|
+
this.connectedChannelSet.delete(channelName);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Connects to a client.
|
|
392
|
+
*
|
|
393
|
+
* @param clientId
|
|
394
|
+
*
|
|
395
|
+
* @returns
|
|
396
|
+
*/
|
|
397
|
+
connectToAgent(clientId) {
|
|
398
|
+
this._fluxWebSocketConnection.connectToAgent(clientId);
|
|
399
|
+
return new FluxRemoteClient(this.iceConnection);
|
|
400
|
+
}
|
|
401
|
+
readConnectedChannels() {
|
|
402
|
+
return [...this.connectedChannelSet];
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// libs/flux/shared/connection/src/lib/flux-network-channel.class.ts
|
|
407
|
+
var FluxNetworkChannel = class {
|
|
408
|
+
constructor(channelName, _fluxWebSocketConnection) {
|
|
409
|
+
this.channelName = channelName;
|
|
410
|
+
this._fluxWebSocketConnection = _fluxWebSocketConnection;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Broadcasts a message to the channel.
|
|
414
|
+
*
|
|
415
|
+
* @param { string } channelName
|
|
416
|
+
*
|
|
417
|
+
* @returns { void }
|
|
418
|
+
*/
|
|
419
|
+
publish(message) {
|
|
420
|
+
this._fluxWebSocketConnection.publish(
|
|
421
|
+
this.channelName,
|
|
422
|
+
message
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Listen to messages on this channel.
|
|
427
|
+
*
|
|
428
|
+
* @param { TMessageCallback } fn
|
|
429
|
+
*
|
|
430
|
+
* @returns { void }
|
|
431
|
+
*/
|
|
432
|
+
onPublish(fn) {
|
|
433
|
+
this._fluxWebSocketConnection.onPublish(
|
|
434
|
+
this.channelName,
|
|
435
|
+
fn
|
|
436
|
+
// TODO
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// libs/flux/shared/types/src/lib/client-id.type.ts
|
|
442
|
+
var NANOID_LENGTH = 21;
|
|
443
|
+
var NANOID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-";
|
|
444
|
+
var isNanoId = (id) => {
|
|
445
|
+
if (typeof id !== "string") return false;
|
|
446
|
+
const regex = new RegExp(`^[${NANOID_ALPHABET}]{${NANOID_LENGTH}}$`);
|
|
447
|
+
return regex.test(id);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// libs/utils/logger/src/lib/utils-logger.ts
|
|
451
|
+
var hasOverlap = (a, b) => a && b ? [...a].some((item) => (Array.isArray(b) ? b : [b]).includes(item)) : false;
|
|
452
|
+
var PicoLogger = /* @__PURE__ */ (() => {
|
|
453
|
+
let allowScopes;
|
|
454
|
+
const configure = (configuration) => {
|
|
455
|
+
allowScopes = typeof configuration.allowScopes === "string" && configuration.allowScopes === "*" ? "*" : new Set(configuration.allowScopes);
|
|
456
|
+
};
|
|
457
|
+
const log = (message, scopes) => {
|
|
458
|
+
if (allowScopes !== "*" && !hasOverlap(allowScopes, scopes)) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
console.log(`[${scopes}] ${message}`);
|
|
462
|
+
};
|
|
463
|
+
const warn = (message, scopes) => {
|
|
464
|
+
if (allowScopes !== "*" && !hasOverlap(allowScopes, scopes)) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
console.warn(`[${scopes}] ${message}`);
|
|
468
|
+
};
|
|
469
|
+
const error = (message, scopes) => {
|
|
470
|
+
if (allowScopes !== "*" && !hasOverlap(allowScopes, scopes)) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
console.error(`[${scopes}] ${message}`);
|
|
474
|
+
};
|
|
475
|
+
return {
|
|
476
|
+
configure,
|
|
477
|
+
log,
|
|
478
|
+
warn,
|
|
479
|
+
error
|
|
480
|
+
};
|
|
481
|
+
})();
|
|
482
|
+
|
|
483
|
+
// libs/flux/shared/connection/src/lib/flux-ws-connection.ts
|
|
484
|
+
var createWSConnection = (id, ticket, stateManager, onReconnectCallback, options) => {
|
|
485
|
+
return new FluxWebSocketConnection(
|
|
486
|
+
id,
|
|
487
|
+
onReconnectCallback,
|
|
488
|
+
stateManager,
|
|
489
|
+
ticket,
|
|
490
|
+
options
|
|
491
|
+
);
|
|
492
|
+
};
|
|
493
|
+
var FluxWebSocketConnection = class {
|
|
494
|
+
constructor(fluxInstanceId, onReconnectCallback, stateManager, token, options) {
|
|
495
|
+
this.fluxInstanceId = fluxInstanceId;
|
|
496
|
+
this.onReconnectCallback = onReconnectCallback;
|
|
497
|
+
this.stateManager = stateManager;
|
|
498
|
+
this.token = token;
|
|
499
|
+
this.options = options;
|
|
500
|
+
this.options = {
|
|
501
|
+
// Defaults
|
|
502
|
+
retries: 1e4,
|
|
503
|
+
port: 5100,
|
|
504
|
+
// Override with provided options
|
|
505
|
+
...this.options
|
|
506
|
+
};
|
|
507
|
+
this.socket = new FluxWebSocketClientConnection(
|
|
508
|
+
{
|
|
509
|
+
url: `ws://localhost:${this.options.port}?token=${this.token}`,
|
|
510
|
+
autoReconnect: true,
|
|
511
|
+
reconnectDelay: 2e3,
|
|
512
|
+
retries: this.options.retries
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
socket;
|
|
517
|
+
callbacks = /* @__PURE__ */ new Set();
|
|
518
|
+
channelCallbacks = /* @__PURE__ */ new Map();
|
|
519
|
+
// Handles messages before the rest of the logic. Will not continue if there are interceptors
|
|
520
|
+
packageTypeInterceptorCallbacks = /* @__PURE__ */ new Map();
|
|
521
|
+
webSocketClient;
|
|
522
|
+
first = true;
|
|
523
|
+
/**
|
|
524
|
+
* Intercepts messages of a given package type.
|
|
525
|
+
*
|
|
526
|
+
* @param { string } packageType
|
|
527
|
+
* @param { TMessageCallback } fn
|
|
528
|
+
*
|
|
529
|
+
* @returns { void }
|
|
530
|
+
*/
|
|
531
|
+
interceptPackageTypeMessages(packageType, fn) {
|
|
532
|
+
const existingInterceptors = this.packageTypeInterceptorCallbacks.get(packageType);
|
|
533
|
+
if (existingInterceptors) {
|
|
534
|
+
existingInterceptors.add(fn);
|
|
535
|
+
} else {
|
|
536
|
+
this.packageTypeInterceptorCallbacks.set(packageType, /* @__PURE__ */ new Set([fn]));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Removes a package type interceptor.
|
|
541
|
+
*
|
|
542
|
+
* @param { string } packageType
|
|
543
|
+
* @param { TMessageCallback } fn
|
|
544
|
+
*
|
|
545
|
+
* @returns { void }
|
|
546
|
+
*/
|
|
547
|
+
removePackageTypeInterceptor(packageType, fn) {
|
|
548
|
+
const existingInterceptors = this.packageTypeInterceptorCallbacks.get(packageType);
|
|
549
|
+
if (existingInterceptors) {
|
|
550
|
+
existingInterceptors.delete(fn);
|
|
551
|
+
} else {
|
|
552
|
+
console.warn(`No interceptors found for package type "${packageType}"`);
|
|
553
|
+
}
|
|
554
|
+
if (existingInterceptors && existingInterceptors.size === 0) {
|
|
555
|
+
this.packageTypeInterceptorCallbacks.delete(packageType);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Connect to the Flux platform.
|
|
560
|
+
*
|
|
561
|
+
* @returns { Promise<FluxWebSocketClientConnection> }
|
|
562
|
+
*/
|
|
563
|
+
async connect() {
|
|
564
|
+
if (this.webSocketClient) {
|
|
565
|
+
return Promise.resolve(this.webSocketClient);
|
|
566
|
+
}
|
|
567
|
+
return new Promise((resolve) => {
|
|
568
|
+
this.interceptPackageTypeMessages(
|
|
569
|
+
"isReady",
|
|
570
|
+
() => {
|
|
571
|
+
this.stateManager.emitNetworkState("connected");
|
|
572
|
+
this.webSocketClient = this.socket;
|
|
573
|
+
resolve(this.socket);
|
|
574
|
+
if (this.first) {
|
|
575
|
+
this.first = false;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
this.onReconnectCallback();
|
|
579
|
+
}
|
|
580
|
+
);
|
|
581
|
+
this.socket.clearEventSubscribers();
|
|
582
|
+
this.socket.on("message", this.handleMessage.bind(this)).on("close", () => {
|
|
583
|
+
this.webSocketClient = void 0;
|
|
584
|
+
this.stateManager.emitNetworkState("disconnected");
|
|
585
|
+
PicoLogger.log("\u{1F50C}\u{1F534} Disconnected", this.fluxInstanceId);
|
|
586
|
+
}).on("connecting", (retryAttempt) => {
|
|
587
|
+
this.stateManager.emitNetworkState("disconnected");
|
|
588
|
+
if (retryAttempt > 0) {
|
|
589
|
+
PicoLogger.log(`\u{1F504} Connecting attempt: #${retryAttempt} of ${this.options?.retries ?? "none"}`, this.fluxInstanceId);
|
|
590
|
+
} else {
|
|
591
|
+
PicoLogger.log(`\u{1F504} Connecting: ${this.fluxInstanceId}`);
|
|
592
|
+
}
|
|
593
|
+
}).on("error", (error) => {
|
|
594
|
+
PicoLogger.log(`\u274C Error: "${error.message}".`, this.fluxInstanceId);
|
|
595
|
+
});
|
|
596
|
+
this.stateManager.emitNetworkState("connecting");
|
|
597
|
+
this.socket.connect();
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
disconnect() {
|
|
601
|
+
this.socket.close();
|
|
602
|
+
this.webSocketClient = void 0;
|
|
603
|
+
this.stateManager.emitNetworkState("disconnected");
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Registers an authority to the network.
|
|
607
|
+
*
|
|
608
|
+
* @param { TAuthorizeCallback } cb
|
|
609
|
+
* @param { TChannnelAuthCallback<M> } authorizeNetworkChannel
|
|
610
|
+
*
|
|
611
|
+
* @returns { Promise<void> }
|
|
612
|
+
*/
|
|
613
|
+
async registerAuthority(cb, authorizeNetworkChannel) {
|
|
614
|
+
const webSocketClient = await this.connect();
|
|
615
|
+
webSocketClient.registerMethod("authorize", (messageWithType) => {
|
|
616
|
+
const dataType = messageWithType.split(":")[0];
|
|
617
|
+
const message = messageWithType.substring(messageWithType.indexOf(":") + 1);
|
|
618
|
+
return cb(dataType === "json" ? JSON.parse(message) : message);
|
|
619
|
+
});
|
|
620
|
+
webSocketClient.registerMethod("authorizeNetworkChannel", authorizeNetworkChannel);
|
|
621
|
+
return Promise.resolve(void 0);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Connects to a network.
|
|
625
|
+
*
|
|
626
|
+
* @returns { Promise<FluxAgentNetworkConnection> }
|
|
627
|
+
*/
|
|
628
|
+
async connectToNetwork() {
|
|
629
|
+
const webSocketClient = await this.connect();
|
|
630
|
+
return Promise.resolve(new FluxAgentNetworkConnection(
|
|
631
|
+
this,
|
|
632
|
+
webSocketClient,
|
|
633
|
+
this.stateManager.emitWebRTCState.bind(this.stateManager)
|
|
634
|
+
));
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Join a channel.
|
|
638
|
+
*
|
|
639
|
+
* @param { TChannelName } channelName
|
|
640
|
+
*
|
|
641
|
+
* @returns { Promise<FluxNetworkChannel> }
|
|
642
|
+
*/
|
|
643
|
+
async joinChannel(channelName) {
|
|
644
|
+
if (this.webSocketClient) {
|
|
645
|
+
this.webSocketClient.send(`${SUBSCRIBE_NETWORK_CHANNEL_NAME}:${channelName}`);
|
|
646
|
+
return new Promise((resolve, reject) => {
|
|
647
|
+
const cb = (message) => {
|
|
648
|
+
this.removePackageTypeInterceptor(SUBSCRIBED_NETWORK_CHANNEL_NAME, cb);
|
|
649
|
+
const receivedChannelName = message.substring(message.indexOf(":") + 1);
|
|
650
|
+
if (channelName !== receivedChannelName) {
|
|
651
|
+
reject(new Error(`Channel name mismatch: "${channelName}" !== "${receivedChannelName}"`));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
resolve(new FluxNetworkChannel(channelName, this));
|
|
655
|
+
};
|
|
656
|
+
this.interceptPackageTypeMessages(
|
|
657
|
+
SUBSCRIBED_NETWORK_CHANNEL_NAME,
|
|
658
|
+
cb
|
|
659
|
+
);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return Promise.reject(new Error("Not connected"));
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
*
|
|
666
|
+
* @param { TChannelName } channelName
|
|
667
|
+
*
|
|
668
|
+
* @returns { Promise<void> }
|
|
669
|
+
*/
|
|
670
|
+
async leaveChannel(channelName) {
|
|
671
|
+
if (this.webSocketClient) {
|
|
672
|
+
this.webSocketClient.send(`${UNSUBSCRIBE_NETWORK_CHANNEL_NAME}:${channelName}`);
|
|
673
|
+
return Promise.resolve(void 0);
|
|
674
|
+
}
|
|
675
|
+
return Promise.reject(new Error("Not connected"));
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
*
|
|
679
|
+
* @param { TChannelName } channelName
|
|
680
|
+
* @param { string } message
|
|
681
|
+
*
|
|
682
|
+
* @returns { void }
|
|
683
|
+
*/
|
|
684
|
+
publish(channelName, message) {
|
|
685
|
+
if (this.webSocketClient) {
|
|
686
|
+
const messageString = typeof message === "string" ? message : `o:${JSON.stringify(message)}`;
|
|
687
|
+
this.webSocketClient.send(`${NETWORK_CHANNEL_PUBLISH}:${channelName}:${messageString}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Adds a callback to the list of callbacks for a given channel topic.
|
|
692
|
+
*
|
|
693
|
+
* @param { TChannelName } channelName
|
|
694
|
+
* @param { string } message
|
|
695
|
+
*
|
|
696
|
+
* @returns { void }
|
|
697
|
+
*/
|
|
698
|
+
onPublish(channelName, fn) {
|
|
699
|
+
const existingSubscriber = this.channelCallbacks.get(channelName);
|
|
700
|
+
if (existingSubscriber) {
|
|
701
|
+
existingSubscriber.add(fn);
|
|
702
|
+
} else {
|
|
703
|
+
const newSubscriber = /* @__PURE__ */ new Set();
|
|
704
|
+
newSubscriber.add(fn);
|
|
705
|
+
this.channelCallbacks.set(channelName, newSubscriber);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
*
|
|
710
|
+
* @param { string } channelName
|
|
711
|
+
*
|
|
712
|
+
* @returns { Promise<FluxNetworkChannel> }
|
|
713
|
+
*/
|
|
714
|
+
async connectToAgent(destinationClientId) {
|
|
715
|
+
if (this.webSocketClient) {
|
|
716
|
+
this.webSocketClient.send(`${CONNECT_TO_CLIENT}:${destinationClientId}`);
|
|
717
|
+
}
|
|
718
|
+
return Promise.resolve();
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
*
|
|
722
|
+
* @param { TMessageCallback } cb
|
|
723
|
+
*
|
|
724
|
+
* @returns { void }
|
|
725
|
+
*/
|
|
726
|
+
onMessage(cb) {
|
|
727
|
+
this.callbacks.add(cb);
|
|
728
|
+
}
|
|
729
|
+
// ****************************************************************************
|
|
730
|
+
// *** For Authorities
|
|
731
|
+
// ****************************************************************************
|
|
732
|
+
/**
|
|
733
|
+
* Subscribes to the channel changes.
|
|
734
|
+
*
|
|
735
|
+
* @returns { void }
|
|
736
|
+
*/
|
|
737
|
+
subscribeToChannelChanges() {
|
|
738
|
+
if (this.webSocketClient) {
|
|
739
|
+
this.webSocketClient.send(AUTHORITY_CHANNEL_SUBSCRIBE);
|
|
740
|
+
} else {
|
|
741
|
+
console.warn("WebSocket client is not connected");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Disconnects an agent from the network.
|
|
746
|
+
*
|
|
747
|
+
* @param { string } id
|
|
748
|
+
*
|
|
749
|
+
* @returns { void }
|
|
750
|
+
*/
|
|
751
|
+
disconnectAgent(id) {
|
|
752
|
+
if (!isNanoId(id)) {
|
|
753
|
+
throw new Error(`Invalid agent id: '${id}'`);
|
|
754
|
+
}
|
|
755
|
+
if (this.webSocketClient) {
|
|
756
|
+
this.webSocketClient.send(`${AUTHORITY_DISCONNECT_AGENT}:${id}`);
|
|
757
|
+
} else {
|
|
758
|
+
console.warn("WebSocket client is not connected");
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// ****************************************************************************
|
|
762
|
+
// *** Internal Helpers
|
|
763
|
+
// ****************************************************************************
|
|
764
|
+
handleMessage(message_) {
|
|
765
|
+
for (const cb of this.callbacks) {
|
|
766
|
+
cb(message_);
|
|
767
|
+
}
|
|
768
|
+
const packageType = message_.split(":")[0];
|
|
769
|
+
const msgInterceptors = this.packageTypeInterceptorCallbacks.get(packageType);
|
|
770
|
+
if (msgInterceptors && msgInterceptors.size > 0) {
|
|
771
|
+
const channelName = message_.substring(message_.indexOf(":") + 1);
|
|
772
|
+
for (const msgInterceptor of msgInterceptors) {
|
|
773
|
+
msgInterceptor(channelName);
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
switch (packageType) {
|
|
778
|
+
case ON_NETWORK_CHANNEL_PUBLISH: {
|
|
779
|
+
const firstColon = message_.indexOf(":");
|
|
780
|
+
const secondColon = message_.indexOf(":", firstColon + 1);
|
|
781
|
+
const channelName = message_.slice(firstColon + 1, secondColon);
|
|
782
|
+
if (validateChannelNameOrThrow(channelName)) {
|
|
783
|
+
const topicCallbacks = this.channelCallbacks.get(channelName);
|
|
784
|
+
if (topicCallbacks) {
|
|
785
|
+
let data = message_.slice(secondColon + 1);
|
|
786
|
+
if (data.startsWith("o:")) {
|
|
787
|
+
try {
|
|
788
|
+
data = JSON.parse(data.substring(2));
|
|
789
|
+
} catch {
|
|
790
|
+
data = data.substring(2);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
for (const cb of topicCallbacks) {
|
|
794
|
+
cb(data);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
case RPC_REQUEST: {
|
|
801
|
+
const payload = JSON.parse(message_.substring(message_.indexOf(":") + 1));
|
|
802
|
+
this.socket.handleMessage(payload, (data) => {
|
|
803
|
+
this.socket.send(`${RPC_RESPONSE}:${JSON.stringify(data)}`);
|
|
804
|
+
});
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
case RPC_RESPONSE: {
|
|
808
|
+
const payload = message_.substring(message_.indexOf(":") + 1);
|
|
809
|
+
PicoLogger.log("\u{1F50C} Unhandled type rpc response", "ws-client");
|
|
810
|
+
PicoLogger.log(`payload: ${payload}`, "ws-client");
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
default:
|
|
814
|
+
PicoLogger.log(`\u{1F50C} Unhandled type: "${message_}"`, "ws-client");
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
// packages/flux/agent/src/lib/flux-agent.class.ts
|
|
821
|
+
var import_nanoid = require("nanoid");
|
|
822
|
+
|
|
823
|
+
// packages/flux/agent/src/lib/utils/obscuring/shared.utils.ts
|
|
824
|
+
var ALGORITHM = { name: "AES-GCM", length: 256 };
|
|
825
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
826
|
+
async function deriveKey(password, salt) {
|
|
827
|
+
const passwordBuffer = new TextEncoder().encode(password);
|
|
828
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
829
|
+
"raw",
|
|
830
|
+
passwordBuffer,
|
|
831
|
+
{ name: "PBKDF2" },
|
|
832
|
+
false,
|
|
833
|
+
["deriveKey"]
|
|
834
|
+
);
|
|
835
|
+
return crypto.subtle.deriveKey(
|
|
836
|
+
{
|
|
837
|
+
name: "PBKDF2",
|
|
838
|
+
salt,
|
|
839
|
+
iterations: PBKDF2_ITERATIONS,
|
|
840
|
+
hash: "SHA-256"
|
|
841
|
+
},
|
|
842
|
+
keyMaterial,
|
|
843
|
+
ALGORITHM,
|
|
844
|
+
true,
|
|
845
|
+
["encrypt", "decrypt"]
|
|
846
|
+
);
|
|
30
847
|
}
|
|
31
|
-
|
|
848
|
+
|
|
849
|
+
// packages/flux/agent/src/lib/utils/obscuring/encyprt.utils.ts
|
|
850
|
+
var IV_LENGTH = 12;
|
|
851
|
+
var SALT_LENGTH = 16;
|
|
852
|
+
async function encrypt(data, password) {
|
|
853
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
854
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
855
|
+
const key = await deriveKey(password, salt);
|
|
856
|
+
const encoded = new TextEncoder().encode(data);
|
|
857
|
+
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
858
|
+
{ name: "AES-GCM", iv },
|
|
859
|
+
key,
|
|
860
|
+
encoded
|
|
861
|
+
);
|
|
862
|
+
return {
|
|
863
|
+
encrypted: btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer))),
|
|
864
|
+
// Base64
|
|
865
|
+
iv: btoa(String.fromCharCode(...iv)),
|
|
866
|
+
// Base64 IV
|
|
867
|
+
salt: btoa(String.fromCharCode(...salt))
|
|
868
|
+
// Base64 Salt
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// libs/flux/shared/utils/src/lib/state-manager.util.ts
|
|
873
|
+
var StateManager = class {
|
|
874
|
+
networkStateListeners = /* @__PURE__ */ new Set();
|
|
875
|
+
webRTCStateListeners = /* @__PURE__ */ new Set();
|
|
876
|
+
attachNetworkStateListener(fn) {
|
|
877
|
+
this.networkStateListeners.add(fn);
|
|
878
|
+
}
|
|
879
|
+
detachNetworkStateListener(fn) {
|
|
880
|
+
this.networkStateListeners.delete(fn);
|
|
881
|
+
}
|
|
882
|
+
attachWebRTCStateListener(fn) {
|
|
883
|
+
this.webRTCStateListeners.add(fn);
|
|
884
|
+
}
|
|
885
|
+
detachWebRTCStateListener(fn) {
|
|
886
|
+
this.webRTCStateListeners.delete(fn);
|
|
887
|
+
}
|
|
888
|
+
emitNetworkState(networkState) {
|
|
889
|
+
for (const fn of this.networkStateListeners) {
|
|
890
|
+
fn(networkState);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
emitWebRTCState(rtcState) {
|
|
894
|
+
for (const fn of this.webRTCStateListeners) {
|
|
895
|
+
fn(rtcState);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/*
|
|
899
|
+
public clear(
|
|
900
|
+
|
|
901
|
+
): void {
|
|
902
|
+
this.networkStateListeners.clear();
|
|
903
|
+
this.webRTCStateListeners.clear();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
public getNetworkStateListeners(
|
|
907
|
+
|
|
908
|
+
): Set<(networkState: TNetworkConnectionState) => void> {
|
|
909
|
+
return this.networkStateListeners;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
public getWebRTCStateListeners(
|
|
913
|
+
|
|
914
|
+
): Set<(rtcState: TRTCState) => void> {
|
|
915
|
+
return this.webRTCStateListeners;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
public getNetworkStateListenersCount(
|
|
919
|
+
|
|
920
|
+
): number {
|
|
921
|
+
return this.networkStateListeners.size;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
public getWebRTCStateListenersCount(
|
|
925
|
+
|
|
926
|
+
): number {
|
|
927
|
+
return this.webRTCStateListeners.size;
|
|
928
|
+
}
|
|
929
|
+
*/
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// libs/flux/shared/utils/src/lib/machine-id.util.ts
|
|
933
|
+
var validateMachineUID = (machineUID) => {
|
|
934
|
+
if (typeof machineUID !== "string") {
|
|
935
|
+
console.warn("Machine UID must be a string");
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
if (machineUID.length < 5) {
|
|
939
|
+
console.warn("Machine UID too short");
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
if (machineUID.length > 50) {
|
|
943
|
+
console.warn("Machine UID too long");
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
return true;
|
|
947
|
+
};
|
|
948
|
+
var getMachineUID = async () => {
|
|
949
|
+
if (typeof globalThis !== "object") {
|
|
950
|
+
throw new Error("Flux can only be used in an environment with globalThis");
|
|
951
|
+
}
|
|
952
|
+
if (globalThis.__flux_client_id !== null) {
|
|
953
|
+
console.log("[flux-client] Reusing existing id");
|
|
954
|
+
return globalThis.__flux_client_id;
|
|
955
|
+
}
|
|
956
|
+
globalThis.__flux_client_id = null;
|
|
957
|
+
return globalThis.__flux_client_id;
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// packages/flux/agent/src/lib/connector/auth/register-client.auth.ts
|
|
961
|
+
var authenticateAgentOrThrow = async (networkId, domain, unknownIdentificationPayload, clientInfo, password) => {
|
|
962
|
+
if (unknownIdentificationPayload === void 0 || unknownIdentificationPayload === null) {
|
|
963
|
+
throw new Error("Custom payload is required");
|
|
964
|
+
}
|
|
965
|
+
let customPayload;
|
|
966
|
+
if (typeof unknownIdentificationPayload === "string") {
|
|
967
|
+
customPayload = unknownIdentificationPayload;
|
|
968
|
+
}
|
|
969
|
+
if (typeof unknownIdentificationPayload === "object") {
|
|
970
|
+
try {
|
|
971
|
+
customPayload = JSON.stringify(unknownIdentificationPayload);
|
|
972
|
+
} catch {
|
|
973
|
+
throw new Error("Custom payload must be serializable");
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (customPayload === void 0) {
|
|
977
|
+
throw new Error("Custom payload must be a string or serializable object");
|
|
978
|
+
}
|
|
979
|
+
if (password !== void 0) {
|
|
980
|
+
customPayload = JSON.stringify(await encrypt(customPayload, password));
|
|
981
|
+
}
|
|
982
|
+
const url = new URL(`${domain}/auth/network-client`);
|
|
983
|
+
url.searchParams.set("networkId", networkId);
|
|
984
|
+
if (clientInfo.clientUId) {
|
|
985
|
+
url.searchParams.set("requestedAgentUid", clientInfo.clientUId);
|
|
986
|
+
}
|
|
987
|
+
if (clientInfo.machineUID && validateMachineUID(clientInfo.machineUID)) {
|
|
988
|
+
url.searchParams.set("machineUID", clientInfo.machineUID);
|
|
989
|
+
}
|
|
990
|
+
const response = await fetch(url, {
|
|
991
|
+
method: "POST",
|
|
992
|
+
headers: {
|
|
993
|
+
"Content-Type": "text/plain",
|
|
994
|
+
"x-flux-content-type": typeof unknownIdentificationPayload === "string" ? "text/plain" : "application/json",
|
|
995
|
+
"Accept": "text/plain"
|
|
996
|
+
},
|
|
997
|
+
body: customPayload
|
|
998
|
+
});
|
|
999
|
+
if (!response.ok) {
|
|
1000
|
+
throw new Error(`Auth failed: ${response.status}`);
|
|
1001
|
+
}
|
|
1002
|
+
const result = await response.text();
|
|
1003
|
+
if (!checkAuthTicketShape(result)) {
|
|
1004
|
+
throw new Error("Auth failed: unexpected response");
|
|
1005
|
+
}
|
|
1006
|
+
return result;
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// packages/flux/agent/src/lib/connector/flux-client-data.class.ts
|
|
1010
|
+
var FluxClientData = class {
|
|
1011
|
+
callbacks = /* @__PURE__ */ new Set();
|
|
1012
|
+
fluxWebSocketConnection;
|
|
1013
|
+
updateWsConnection(fluxWebSocketConnection) {
|
|
1014
|
+
this.fluxWebSocketConnection = fluxWebSocketConnection;
|
|
1015
|
+
for (const cb of this.callbacks) {
|
|
1016
|
+
this.fluxWebSocketConnection.onMessage(cb);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
onMessage(cb) {
|
|
1020
|
+
this.callbacks.add(cb);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
// packages/flux/agent/src/lib/flux-agent.class.ts
|
|
1025
|
+
globalThis.agentLoadCount ??= 0;
|
|
1026
|
+
globalThis.agentLoadCount++;
|
|
1027
|
+
console.log(`[flux-agent] Reloaded ${globalThis.agentLoadCount} time(s)`);
|
|
1028
|
+
var FluxAgent = class {
|
|
1029
|
+
constructor(networkId, options) {
|
|
1030
|
+
this.networkId = networkId;
|
|
1031
|
+
this.options = options;
|
|
1032
|
+
}
|
|
1033
|
+
id = (0, import_nanoid.nanoid)();
|
|
1034
|
+
fluxWebSocketConnection;
|
|
1035
|
+
fluxClientData = new FluxClientData();
|
|
1036
|
+
stateManager = new StateManager();
|
|
1037
|
+
/**
|
|
1038
|
+
* Connect to the network using provided identification.
|
|
1039
|
+
*
|
|
1040
|
+
* @param { unknown } identification
|
|
1041
|
+
* @param { string } [clientUId]
|
|
1042
|
+
*
|
|
1043
|
+
* @returns { Promise<FluxAgentNetworkConnection> }
|
|
1044
|
+
*/
|
|
1045
|
+
async connect(identification, clientUId) {
|
|
1046
|
+
this.stateManager.emitNetworkState("authorizing");
|
|
1047
|
+
if (clientUId && !validateAgentUIDOrThrow(clientUId)) {
|
|
1048
|
+
throw new Error("Will never be thrown");
|
|
1049
|
+
}
|
|
1050
|
+
const ticket = await authenticateAgentOrThrow(
|
|
1051
|
+
this.networkId,
|
|
1052
|
+
this.options?.domain ?? "http://localhost:5100",
|
|
1053
|
+
identification,
|
|
1054
|
+
{
|
|
1055
|
+
clientUId,
|
|
1056
|
+
machineUID: await getMachineUID() ?? void 0
|
|
1057
|
+
}
|
|
1058
|
+
);
|
|
1059
|
+
this.fluxWebSocketConnection = createWSConnection(
|
|
1060
|
+
this.id,
|
|
1061
|
+
ticket,
|
|
1062
|
+
this.stateManager,
|
|
1063
|
+
async () => {
|
|
1064
|
+
await this.connect(
|
|
1065
|
+
identification,
|
|
1066
|
+
clientUId
|
|
1067
|
+
);
|
|
1068
|
+
},
|
|
1069
|
+
this.options
|
|
1070
|
+
);
|
|
1071
|
+
this.fluxClientData.updateWsConnection(this.fluxWebSocketConnection);
|
|
1072
|
+
const fluxNetworkConnection = await this.fluxWebSocketConnection.connectToNetwork();
|
|
1073
|
+
return fluxNetworkConnection;
|
|
1074
|
+
}
|
|
1075
|
+
disconnect() {
|
|
1076
|
+
this.fluxWebSocketConnection?.disconnect();
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
*
|
|
1080
|
+
* @param fn
|
|
1081
|
+
*
|
|
1082
|
+
* @returns { void }
|
|
1083
|
+
*/
|
|
1084
|
+
onWebRTConnectionState = this.stateManager.attachWebRTCStateListener.bind(this.stateManager);
|
|
1085
|
+
/**
|
|
1086
|
+
*
|
|
1087
|
+
* @param fn
|
|
1088
|
+
*
|
|
1089
|
+
* @returns { void }
|
|
1090
|
+
*/
|
|
1091
|
+
onNetworkState = this.stateManager.attachNetworkStateListener.bind(this.stateManager);
|
|
1092
|
+
onMessage(cb) {
|
|
1093
|
+
this.fluxClientData.onMessage(cb);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
32
1096
|
// Annotate the CommonJS export names for ESM import in node:
|
|
33
1097
|
0 && (module.exports = {
|
|
34
|
-
|
|
1098
|
+
FluxAgent
|
|
35
1099
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persistica/flux-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Persistica Flux Agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [],
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "./index.cjs",
|
|
11
11
|
"types": "./index.d.ts",
|
|
12
|
-
"dependencies": {},
|
|
13
12
|
"repository": {
|
|
14
13
|
"type": "git",
|
|
15
14
|
"url": "git+https://github.com/rujorgensen/persistica.git"
|
|
@@ -20,5 +19,9 @@
|
|
|
20
19
|
"url": "https://github.com/rujorgensen/persistica/issues"
|
|
21
20
|
},
|
|
22
21
|
"homepage": "https://github.com/rujorgensen/persistica#readme",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"nanoid": "5.1.5"
|
|
24
|
+
},
|
|
25
|
+
"packageManager": "bun@1.2.20",
|
|
23
26
|
"module": "./index.js"
|
|
24
27
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { FluxAgent, } from './lib/flux-agent.class';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TChannelName } from '@flux/shared/types';
|
|
2
|
+
/**
|
|
3
|
+
* Callback function to authorize a channel connection.
|
|
4
|
+
*
|
|
5
|
+
* @param { TChannelName } channelTopic - the topic to subscribe to
|
|
6
|
+
* @param { unknown } identification - whatever identification the authority requires
|
|
7
|
+
*
|
|
8
|
+
* @returns { Promise<boolean> } - true if the channel connection is authorized, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
export type TChannnelAuthCallback<T> = (channelTopic: TChannelName, identification: T) => Promise<boolean>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type TAuthenticationTicket, type TAgentOwnUId, type TNetworkId_S } from '@flux/shared/types';
|
|
2
|
+
import { type TFluxClientUID } from '@flux/shared/utils';
|
|
3
|
+
/**
|
|
4
|
+
* Authenticates with the server and returns a ticket for connecting the websocket.
|
|
5
|
+
*
|
|
6
|
+
* This client -> flux server -> authority client -> flux server -> this client.
|
|
7
|
+
*
|
|
8
|
+
* @param { string } domain The domain of the authority server.
|
|
9
|
+
* @param { unknown } unknownIdentificationPayload The payload to send to the authority server.
|
|
10
|
+
* @param { string } [password]
|
|
11
|
+
* @param { TAgentOwnUId } [agentOwnUId]
|
|
12
|
+
*
|
|
13
|
+
* @returns { Promise<TAuthenticationTicket> }
|
|
14
|
+
*/
|
|
15
|
+
export declare const authenticateAgentOrThrow: (networkId: TNetworkId_S, domain: string, unknownIdentificationPayload: unknown, clientInfo: {
|
|
16
|
+
clientUId?: TAgentOwnUId;
|
|
17
|
+
machineUID?: TFluxClientUID;
|
|
18
|
+
}, password?: string) => Promise<TAuthenticationTicket>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects data from any source (currently WS or WebRTC)
|
|
3
|
+
*/
|
|
4
|
+
import type { TMessageCallback } from '@flux/shared/ws';
|
|
5
|
+
import type { FluxWebSocketConnection } from '@flux/shared/connection';
|
|
6
|
+
export declare class FluxClientData {
|
|
7
|
+
private readonly callbacks;
|
|
8
|
+
private fluxWebSocketConnection;
|
|
9
|
+
updateWsConnection(fluxWebSocketConnection: FluxWebSocketConnection): void;
|
|
10
|
+
onMessage(cb: TMessageCallback): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One device calls createOffer() and shares the generated offer with the second device.
|
|
3
|
+
* The second device calls acceptOffer(offer), generates an answer, and shares it back with the first device.
|
|
4
|
+
* Both devices exchange ICE candidates (logged in the console) to establish the P2P connection.
|
|
5
|
+
* Once connected, messages can be sent between devices.
|
|
6
|
+
*/
|
|
7
|
+
import type { TRTCState } from '@flux/shared/utils';
|
|
8
|
+
import type { FluxWebSocketClientConnection } from '@flux/shared/ws';
|
|
9
|
+
export declare class ICEConnection {
|
|
10
|
+
private readonly _fluxWebSocketClientConnection;
|
|
11
|
+
private readonly _stateChange;
|
|
12
|
+
private readonly peerConnection;
|
|
13
|
+
private dataChannel;
|
|
14
|
+
private offerSDP;
|
|
15
|
+
constructor(_fluxWebSocketClientConnection: FluxWebSocketClientConnection, _stateChange: (state: TRTCState) => void);
|
|
16
|
+
/**
|
|
17
|
+
* This offer must be sent to the other peer.
|
|
18
|
+
*/
|
|
19
|
+
private createOffer;
|
|
20
|
+
/**
|
|
21
|
+
*
|
|
22
|
+
* @param { RTCSessionDescriptionInit } offer
|
|
23
|
+
*/
|
|
24
|
+
private acceptOffer;
|
|
25
|
+
private acceptAnswer;
|
|
26
|
+
/**
|
|
27
|
+
* If remote (not initiator) the answer was accepted.
|
|
28
|
+
*
|
|
29
|
+
* @param answer
|
|
30
|
+
* @returns
|
|
31
|
+
*/
|
|
32
|
+
private answerAcceptedByInitiator;
|
|
33
|
+
sendMessage(message: string): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
var agentLoadCount: number | null | undefined;
|
|
3
|
+
}
|
|
4
|
+
import type { TMessageCallback } from '@flux/shared/ws';
|
|
5
|
+
import type { FluxAgentNetworkConnection } from '@flux/shared/connection';
|
|
6
|
+
export declare class FluxAgent {
|
|
7
|
+
private readonly networkId;
|
|
8
|
+
private readonly options?;
|
|
9
|
+
readonly id: string;
|
|
10
|
+
private fluxWebSocketConnection;
|
|
11
|
+
private readonly fluxClientData;
|
|
12
|
+
private readonly stateManager;
|
|
13
|
+
constructor(networkId: string, options?: {
|
|
14
|
+
domain?: string;
|
|
15
|
+
secretKey?: string;
|
|
16
|
+
retries?: number;
|
|
17
|
+
} | undefined);
|
|
18
|
+
/**
|
|
19
|
+
* Connect to the network using provided identification.
|
|
20
|
+
*
|
|
21
|
+
* @param { unknown } identification
|
|
22
|
+
* @param { string } [clientUId]
|
|
23
|
+
*
|
|
24
|
+
* @returns { Promise<FluxAgentNetworkConnection> }
|
|
25
|
+
*/
|
|
26
|
+
connect(identification: unknown, clientUId?: string): Promise<FluxAgentNetworkConnection>;
|
|
27
|
+
disconnect(): void;
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @param fn
|
|
31
|
+
*
|
|
32
|
+
* @returns { void }
|
|
33
|
+
*/
|
|
34
|
+
onWebRTConnectionState: (fn: (rtcState: import("@flux/shared/utils").TRTCState) => void) => void;
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param fn
|
|
38
|
+
*
|
|
39
|
+
* @returns { void }
|
|
40
|
+
*/
|
|
41
|
+
onNetworkState: (fn: (networkState: import("@flux/shared/utils").TNetworkConnectionState) => void) => void;
|
|
42
|
+
onMessage(cb: TMessageCallback): void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface to the flux platform:
|
|
3
|
+
*
|
|
4
|
+
* Requires socket connectivity to the Flux platform, and a verified network.
|
|
5
|
+
*/
|
|
6
|
+
import type { IPackageStatus } from '@flux/shared/types';
|
|
7
|
+
import type { FluxWebSocketConnection } from '../../../../../libs/flux/shared/connection/src/lib/flux-ws-connection';
|
|
8
|
+
export declare class FluxInterface {
|
|
9
|
+
private readonly socketConnection;
|
|
10
|
+
constructor(socketConnection: FluxWebSocketConnection);
|
|
11
|
+
messageReport(packageStatus: Omit<IPackageStatus, 'txNode'>): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connection to another known client
|
|
3
|
+
*/
|
|
4
|
+
import { ICEConnection } from './connector/low-level-com/web-rtc/ice-connection';
|
|
5
|
+
export declare class FluxRemoteClient {
|
|
6
|
+
private readonly iceConnection;
|
|
7
|
+
constructor(iceConnection: ICEConnection);
|
|
8
|
+
callProcedure(): void;
|
|
9
|
+
send(message: string): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface IEncryptedData {
|
|
2
|
+
encrypted: string;
|
|
3
|
+
iv: string;
|
|
4
|
+
salt: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Decrypts an AES-256-GCM encrypted JSON object using a password
|
|
8
|
+
* @param {IEncryptedData} encryptedData - The encrypted data object
|
|
9
|
+
* @param {string} password - The password
|
|
10
|
+
* @returns {Promise<Object>} - Decrypted JSON object
|
|
11
|
+
*/
|
|
12
|
+
export declare function decrypt(encryptedData: IEncryptedData, password: string): Promise<any>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypts a JSON object using AES-256-GCM with password-derived key
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} data - The data to encrypt
|
|
5
|
+
* @param {string} password - The password
|
|
6
|
+
* @returns {Promise<Object>} - Encrypted data (Base64 format)
|
|
7
|
+
*/
|
|
8
|
+
export declare function encrypt(data: string, password: string): Promise<{
|
|
9
|
+
encrypted: string;
|
|
10
|
+
iv: string;
|
|
11
|
+
salt: string;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a cryptographic key from a password using PBKDF2
|
|
3
|
+
*
|
|
4
|
+
* @param { string } password - User-provided password
|
|
5
|
+
* @param { Uint8Array } salt - Salt for key derivation
|
|
6
|
+
*
|
|
7
|
+
* @returns { Promise<CryptoKey> } - AES-256-GCM key
|
|
8
|
+
*/
|
|
9
|
+
export declare function deriveKey(password: string, salt: any): Promise<CryptoKey>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function persisticaFluxAgent2(): string;
|