@persistica/flux-mesh 0.0.1

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/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@persistica/flux-mesh",
3
+ "version": "0.0.1",
4
+ "description": "Persistica Flux Mesh",
5
+ "type": "module",
6
+ "keywords": [],
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "types": "./index.d.ts",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/rujorgensen/persistica.git"
14
+ },
15
+ "author": "pixl.prophet",
16
+ "license": "ISC",
17
+ "bugs": {
18
+ "url": "https://github.com/rujorgensen/persistica/issues"
19
+ },
20
+ "homepage": "https://github.com/rujorgensen/persistica#readme",
21
+ "dependencies": {}
22
+ }
package/project.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "persistica-flux-mesh",
3
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/flux/mesh/src",
5
+ "projectType": "library",
6
+ "tags": [
7
+ "type:npm-package",
8
+ "scope:any"
9
+ ],
10
+ "targets": {
11
+ "bun-serve": {
12
+ "executor": "nx:run-commands",
13
+ "options": {
14
+ "command": "bun --watch run ./packages/flux/mesh/src/main.ts"
15
+ }
16
+ },
17
+ "build": {
18
+ "executor": "nx:run-commands",
19
+ "options": {
20
+ "command": "bun run {projectRoot}/build.js",
21
+ "args": [
22
+ "--projectRoot={projectRoot}"
23
+ ]
24
+ }
25
+ },
26
+ "serve": {
27
+ "executor": "@nx/js:node",
28
+ "defaultConfiguration": "development",
29
+ "dependsOn": [
30
+ "build"
31
+ ],
32
+ "options": {
33
+ "buildTarget": "persistica-flux-mesh:build",
34
+ "runBuildTargetDependencies": false
35
+ },
36
+ "configurations": {
37
+ "development": {
38
+ "buildTarget": "persistica-flux-mesh:build:development"
39
+ },
40
+ "production": {
41
+ "buildTarget": "persistica-flux-mesh:build:production"
42
+ }
43
+ }
44
+ },
45
+ "nx-release-publish": {
46
+ "options": {
47
+ "packageRoot": "dist/{projectRoot}"
48
+ }
49
+ },
50
+ "npm-publish": {
51
+ "executor": "nx:run-commands",
52
+ "options": {
53
+ "commands": [
54
+ "npm login",
55
+ "npm publish --access public"
56
+ ]
57
+ },
58
+ "dependsOn": [
59
+ "build"
60
+ ]
61
+ },
62
+ "bun-test": {
63
+ "executor": "nx:run-commands",
64
+ "options": {
65
+ "command": "bun test {projectRoot}/ --env-file=.env.e2e"
66
+ },
67
+ "dsa": "npm publish --access public --registry=https://registry.npmjs.org --scope=@persistica --workspace-root dist/{projectRoot}"
68
+ }
69
+ },
70
+ "release": {
71
+ "version": {
72
+ "currentVersionResolver": "git-tag",
73
+ "fallbackCurrentVersionResolver": "disk",
74
+ "preserveLocalDependencyProtocols": false,
75
+ "manifestRootsToUpdate": [
76
+ "dist/{projectRoot}"
77
+ ]
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,79 @@
1
+ import type {
2
+ TProcessAddress,
3
+ } from '@flux/shared/types';
4
+ import { GlobalRPCClient2 } from '../routing/rpc/core/global-rpc-client.class';
5
+ import {
6
+ type TRPCResponseCallbackFunction,
7
+ RPCClient,
8
+ } from '@flux/shared/ws';
9
+
10
+ export class WebRTCClient extends RPCClient<
11
+ 'createOffer' | 'acceptOffer' | 'acceptAnswer' | 'answerAcceptedByInitiator'
12
+ > {
13
+ constructor(
14
+ private readonly _originProcessAddress: TProcessAddress,
15
+ private readonly _sendToRPCServer: (
16
+ data: any, // ! Use 'unknown' or Bun.BufferSource (does not work)
17
+ compress?: boolean | undefined
18
+ ) => number,
19
+
20
+ // Handle the response from the RPC server
21
+ private readonly _handleResponseMessage: (
22
+ fn: TRPCResponseCallbackFunction,
23
+ ) => void
24
+ ) {
25
+ super(_sendToRPCServer, _handleResponseMessage);
26
+ }
27
+
28
+ public createOffer(): Promise<unknown> {
29
+ // TODO TYPE OFFER
30
+ return super.call(this._originProcessAddress, 'createOffer');
31
+ }
32
+
33
+ public acceptOfferAndCreateAnswer(
34
+ offer: unknown // TODO TYPE OFFER
35
+ ): Promise<unknown> {
36
+ // TODO TYOPE answer
37
+ return super.call(this._originProcessAddress, 'acceptOffer', offer);
38
+ }
39
+
40
+ public acceptAnswer(
41
+ answer: unknown // TODO TYPE answer
42
+ ): Promise<boolean> {
43
+ return super.call(this._originProcessAddress, 'acceptAnswer', answer);
44
+ }
45
+
46
+ public answerWasAccepted(answerAccepted: boolean): Promise<boolean> {
47
+ return super.call(
48
+ this._originProcessAddress,
49
+ 'answerAcceptedByInitiator',
50
+ answerAccepted
51
+ );
52
+ }
53
+ }
54
+
55
+ export class GlobalWebRTCClient extends GlobalRPCClient2<
56
+ 'createOffer' | 'acceptOffer' | 'acceptAnswer' | 'answerAcceptedByInitiator'
57
+ > {
58
+ public createOffer(): Promise<unknown> {
59
+ // TODO TYPE OFFER
60
+ return super.call('createOffer');
61
+ }
62
+
63
+ public acceptOfferAndCreateAnswer(
64
+ offer: unknown // TODO TYPE OFFER
65
+ ): Promise<unknown> {
66
+ // TODO TYOPE answer
67
+ return super.call('acceptOffer', offer);
68
+ }
69
+
70
+ public acceptAnswer(
71
+ answer: unknown // TODO TYPE answer
72
+ ): Promise<boolean> {
73
+ return super.call('acceptAnswer', answer);
74
+ }
75
+
76
+ public answerWasAccepted(answerAccepted: boolean): Promise<boolean> {
77
+ return super.call('answerAcceptedByInitiator', answerAccepted);
78
+ }
79
+ }
@@ -0,0 +1,74 @@
1
+ import type * as Bun from 'bun';
2
+ import * as nodeURL from 'node:url';
3
+ import { PicoLogger } from '@utils/pico-logger';
4
+ import { generateToken } from '../../auth/auth';
5
+ import {
6
+ type TNetworkId_S,
7
+ validateNetworkIdOrThrow,
8
+ } from '@flux/shared/types';
9
+ import {
10
+ type TFluxClientUID,
11
+ validateMachineUID,
12
+ } from '@flux/shared/utils';
13
+
14
+ /**
15
+ * This route is used to authorize a network authority.
16
+ *
17
+ * @returns { Promise<Response> }
18
+ */
19
+ export const authorizeNetworkAuthority = async (
20
+ request: Bun.BunRequest,
21
+ ): Promise<Response> => {
22
+ const urlWithParsedQuery: nodeURL.UrlWithParsedQuery = nodeURL.parse(request.url, true);
23
+
24
+ // Find the network authority to authenticate with
25
+ const networkIdString: string | string[] | undefined = urlWithParsedQuery.query['networkId'];
26
+
27
+ try {
28
+ validateNetworkIdOrThrow(networkIdString ?? '');
29
+ } catch (error) {
30
+ return new Response(
31
+ error instanceof Error ? error.message : 'Unknown error validating network ID',
32
+ {
33
+ status: 500,
34
+ },
35
+ );
36
+ }
37
+ const networkId: TNetworkId_S = networkIdString as TNetworkId_S;
38
+ const text = await request.text();
39
+
40
+ PicoLogger.log(`Received password:${text}${networkId}`, 'authorize');
41
+
42
+ console.error('TODOD: CHECK PASSWORD');
43
+
44
+ const cookies = request.cookies;
45
+
46
+ // Set a cookie with various options
47
+ cookies.set('user_id', '12345', {
48
+ maxAge: 60 * 60 * 24 * 7, // 1 week
49
+ httpOnly: true,
50
+ secure: true,
51
+ path: '/',
52
+ });
53
+
54
+ const machineUIDString: string | string[] | undefined = urlWithParsedQuery.query['machineUID'];
55
+ let machineUID: TFluxClientUID | undefined;
56
+ if (machineUIDString && validateMachineUID(machineUIDString)) {
57
+ machineUID = machineUIDString;
58
+ }
59
+
60
+ return new Response(
61
+ generateToken({
62
+ networkId,
63
+ isAuthority: true,
64
+ machineUID: machineUID,
65
+ }),
66
+ {
67
+ headers: {
68
+ 'Content-Type': 'text/plain',
69
+ 'Access-Control-Allow-Origin': '*',
70
+ 'Set-Cookie': 'sessionId=abc123; HttpOnly; Max-Age=3600',
71
+ },
72
+ }
73
+ );
74
+ };
@@ -0,0 +1,169 @@
1
+ import type { BunRequest } from 'bun';
2
+ import * as nodeURL from 'node:url';
3
+ import {
4
+ type TAddress,
5
+ type TNetworkId_S,
6
+ GlobalRPCTimeoutError,
7
+ UnknownClientError,
8
+ validateAgentUIDOrThrow,
9
+ validateNetworkIdOrThrow,
10
+ } from '@flux/shared/types';
11
+ import { generateToken } from '../../auth/auth';
12
+ import type { GlobalRPCClient } from '../../routing/rpc/core/global-rpc-client.class';
13
+ import type { NetworkAuthorityManager } from '../../register/register-network-authority.class';
14
+ import { retry } from '@flux/shared/utils';
15
+ import {
16
+ type TFluxClientUID,
17
+ validateMachineUID,
18
+ } from '@flux/shared/utils';
19
+
20
+ /**
21
+ *
22
+ * @param { BunRequest } request
23
+ * @param { NetworkAuthorityManager } networkAuthorityManager
24
+ * @param { GlobalRPCClient<'authorize'> } globalRPCClient
25
+ *
26
+ * @returns { void }
27
+ */
28
+ export const authorizeNetworkAgent = async (
29
+ request: BunRequest,
30
+ networkAuthorityManager: NetworkAuthorityManager,
31
+ globalRPCClient: GlobalRPCClient<'authorize'>,
32
+ ) => {
33
+ const urlWithParsedQuery: nodeURL.UrlWithParsedQuery = nodeURL.parse(request.url, true);
34
+
35
+ // Find the network authority to authenticate with
36
+ const networkIdString: string | string[] | undefined = urlWithParsedQuery.query['networkId'];
37
+
38
+ try {
39
+ validateNetworkIdOrThrow(networkIdString ?? '');
40
+ } catch (error) {
41
+ return new Response(
42
+ error instanceof Error ? error.message : 'Unknown error validating network ID',
43
+ {
44
+ status: 500,
45
+ },
46
+ );
47
+ }
48
+ const networkId: TNetworkId_S = networkIdString as TNetworkId_S;
49
+
50
+ if (!networkId) {
51
+ return new Response('Missing networkId', {
52
+ status: 500,
53
+ headers: {
54
+ // 'Access-Control-Allow-Origin': '*',
55
+ },
56
+ });
57
+ }
58
+
59
+ const text = await request.text();
60
+
61
+ const contentType = request.headers
62
+ .get('x-flux-content-type')
63
+ ?.toLowerCase();
64
+
65
+ try {
66
+ let networkAuthorityAddress: TAddress;
67
+
68
+ const authorizedJWT: string = await retry<string>(
69
+ async () => {
70
+ networkAuthorityAddress = await networkAuthorityManager
71
+ .resolveNetworkAuthorityAddressOrThrow(
72
+ networkId,
73
+ );
74
+
75
+ return globalRPCClient.call(
76
+ networkAuthorityAddress,
77
+ 'authorize',
78
+ `${contentType === 'application/json' ? 'json' : 'text'}:${text}`
79
+ );
80
+ },
81
+ (error: unknown) => {
82
+ if (
83
+ (error instanceof UnknownClientError) ||
84
+ (error instanceof GlobalRPCTimeoutError)
85
+ ) {
86
+ networkAuthorityManager.removeUnresponsiveClient(
87
+ networkId,
88
+ networkAuthorityAddress,
89
+ );
90
+
91
+ return true;
92
+ }
93
+
94
+ return false;
95
+ },
96
+ {
97
+ retries: 10,
98
+ delayMs: 50,
99
+ onRetry: (
100
+ attempt: number,
101
+ retries: number,
102
+ ) => {
103
+ console.log(`[authorizeNetworkAgent] Retrying... (attempt: ${attempt} of ${retries})`);
104
+ },
105
+ },
106
+ );
107
+
108
+ // const authorizedJWT: string = await globalRPCClient.call(
109
+ // networkAuthorityAddress,
110
+ // 'authorize',
111
+ // `${contentType === 'application/json' ? 'json' : 'text'}:${text}`
112
+ // );
113
+
114
+ if (authorizedJWT) {
115
+ const requestedAgentUidString: string | string[] | undefined = urlWithParsedQuery.query['requestedAgentUid'];
116
+
117
+ if (requestedAgentUidString) {
118
+ try {
119
+ validateAgentUIDOrThrow(requestedAgentUidString);
120
+ } catch (error) {
121
+ return new Response(
122
+ error instanceof Error ? error.message : 'The requested agent ID is not valid',
123
+ {
124
+ status: 500,
125
+ },
126
+ );
127
+ }
128
+ }
129
+
130
+ const cookies = request.cookies;
131
+
132
+ // Set a cookie with various options
133
+ cookies.set('claim', authorizedJWT, {
134
+ maxAge: 60 * 60 * 24 * 7, // 1 week
135
+ httpOnly: true,
136
+ secure: true,
137
+ path: '/',
138
+ });
139
+
140
+ const machineUIDString: string | string[] | undefined = urlWithParsedQuery.query['machineUID'];
141
+ let machineUID: TFluxClientUID | undefined;
142
+ if (machineUIDString && validateMachineUID(machineUIDString)) {
143
+ machineUID = machineUIDString;
144
+ }
145
+
146
+ return new Response(
147
+ generateToken({
148
+ networkId,
149
+ claim: authorizedJWT,
150
+ agentUID: requestedAgentUidString,
151
+ machineUID: machineUID,
152
+ }),
153
+ {
154
+ headers: {
155
+ 'Content-Type': 'text/plain',
156
+ 'Access-Control-Allow-Origin': '*',
157
+ // 'Set-Cookie': `sessionId=abc123; HttpOnly; Max-Age=3600; signed=true; Path=/;`,
158
+ },
159
+ }
160
+ );
161
+ }
162
+ } catch (error) {
163
+ console.error('Error authorizing:', error);
164
+ }
165
+
166
+ return new Response('Unauthorized', {
167
+ status: 500,
168
+ });
169
+ };
@@ -0,0 +1,9 @@
1
+ export const OPTIONS_RESPONSE = () =>
2
+ new Response(null, {
3
+ status: 204,
4
+ headers: {
5
+ 'Access-Control-Allow-Origin': '*',
6
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
7
+ 'Access-Control-Allow-Headers': 'Content-Type, x-flux-content-type, Set-Cookie',
8
+ },
9
+ });
@@ -0,0 +1,29 @@
1
+ import jwt from 'jsonwebtoken';
2
+
3
+ const secret = 'your-very-secure-secret'; // keep this secret safe!
4
+
5
+ export const generateToken = (
6
+ payload: object,
7
+ expiresIn = 60_000 //! TODOD CHANGE TO LOW VALUE AGAINGA.. FOR DEVE.. Live for 60 seconds
8
+ ): string => {
9
+ return jwt.sign(payload, secret, { expiresIn });
10
+ };
11
+
12
+ export const verifyTokenOrThrow = (
13
+ token: unknown,
14
+ // callback?: VerifyCallback<JwtPayload | string>,
15
+ ): object => {
16
+ if (typeof token !== 'string') {
17
+ throw new Error('Token must be a string');
18
+ }
19
+
20
+ try {
21
+ const decoded = jwt.verify(token, secret);
22
+
23
+ return decoded as any;
24
+ } catch (err) {
25
+ console.error('Invalid or expired token', (<any>err).message);
26
+
27
+ throw new Error('Invalid or expired token');
28
+ }
29
+ };
@@ -0,0 +1,209 @@
1
+ import {
2
+ type TAddress,
3
+ type TChannelName,
4
+ type TNetworkId_S,
5
+ AUTHORITY_ON_CREATE_CHANNEL,
6
+ AUTHORITY_ON_EMPTY_CHANNEL,
7
+ } from '@flux/shared/types';
8
+ import { NetworkChannelHash } from '@flux/mesh/store/redis/network-channel';
9
+ import {
10
+ type RedisConnection,
11
+ getMeshRedisConnection,
12
+ } from '../../routing/redis/redis-connection.class';
13
+ import type { GlobalChannelPubsub } from '../../routing/global-channel/global-channel-pubsub.class';
14
+
15
+ // ! TODO Hardcoded for now, take from network config in the future
16
+ const MAX_CHANNEL_MEMBERS = 25;
17
+
18
+ interface IUsageCache {
19
+ networkId: TNetworkId_S;
20
+ usage: number;
21
+ };
22
+ export class NetworkChannelManager {
23
+ private readonly channelUsageCount: Map<TChannelName, IUsageCache> = new Map();
24
+ private readonly redisConnection: RedisConnection = getMeshRedisConnection();
25
+ private readonly networkChannelHash: NetworkChannelHash = new NetworkChannelHash(
26
+ this.redisConnection,
27
+ );
28
+
29
+ constructor(
30
+ private readonly _globalChannelPubsub: GlobalChannelPubsub,
31
+ ) {
32
+ setInterval(() => {
33
+ for (const [channelName, usage] of this.channelUsageCount) {
34
+
35
+ if (usage.usage > 0) {
36
+ this.networkChannelHash
37
+ .incrementUsage(
38
+ usage.networkId,
39
+ channelName,
40
+ usage.usage,
41
+ );
42
+ }
43
+
44
+ this.channelUsageCount.delete(channelName);
45
+ }
46
+ }, 5_000);
47
+ }
48
+
49
+ /**
50
+ * Checks if a channel can have more members.
51
+ *
52
+ * ! TODO Don't query the database repeatedly, implement local synced cache.
53
+ *
54
+ * @param { TNetworkId_S } networkId
55
+ * @param { TChannelName } channelName
56
+ *
57
+ * @returns { Promise<boolean> }
58
+ */
59
+ public async canHaveMembers(
60
+ networkId: TNetworkId_S,
61
+ channelName: TChannelName,
62
+ ): Promise<boolean> {
63
+ const count: number = await this.networkChannelHash.readNetworkMemberCount(
64
+ networkId,
65
+ channelName,
66
+ );
67
+
68
+ return Promise.resolve(count < MAX_CHANNEL_MEMBERS);
69
+ }
70
+
71
+ /**
72
+ * Joins a network channel.
73
+ *
74
+ * @param { TNetworkId_S } networkId
75
+ * @param { TChannelName } channelName
76
+ * @param { TAddress } clientAddress
77
+ *
78
+ * @returns { void }
79
+ */
80
+ public joinNetworkChannel(
81
+ networkId: TNetworkId_S,
82
+ channelName: TChannelName,
83
+ clientAddress: TAddress,
84
+ ): void {
85
+ this.createNetworkChannelIfNotExist(
86
+ networkId,
87
+ channelName,
88
+ );
89
+
90
+ this.networkChannelHash.joinNetworkChannel(
91
+ networkId,
92
+ channelName,
93
+ clientAddress,
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Leaves a network channel.
99
+ *
100
+ * @param { TNetworkId_S } networkId
101
+ * @param { TChannelName } channelName
102
+ * @param { TAddress } clientAddress
103
+ *
104
+ * @returns { Promise<void> }
105
+ */
106
+ public async leaveNetworkChannel(
107
+ networkId: TNetworkId_S,
108
+ channelName: TChannelName,
109
+ clientAddress: TAddress,
110
+ ): Promise<void> {
111
+ const membersLeft: number = await this.networkChannelHash
112
+ .leaveNetworkChannel(
113
+ networkId,
114
+ channelName,
115
+ clientAddress,
116
+ );
117
+
118
+ if (membersLeft === 0) {
119
+ this._globalChannelPubsub
120
+ .publish(
121
+ `~/networks/${networkId}/channel-empty`,
122
+ `${AUTHORITY_ON_EMPTY_CHANNEL}:${channelName}`,
123
+ );
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Leaves all network channels.
129
+ *
130
+ * @param { TNetworkId_S } networkId
131
+ * @param { TAddress } clientAddress
132
+ * @param { Set<TChannelName> } channelNames
133
+ *
134
+ * @returns { void }
135
+ */
136
+ public leaveAllNetworkChannels(
137
+ networkId: TNetworkId_S,
138
+ clientAddress: TAddress,
139
+ channelNames: Set<TChannelName>,
140
+ ): void {
141
+ this.networkChannelHash.leaveAllNetworkChannels(
142
+ networkId,
143
+ clientAddress,
144
+ channelNames,
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Creates a network channel if it does not exist.
150
+ *
151
+ * @param { TNetworkId_S } networkId
152
+ * @param { TChannelName } channelName
153
+ *
154
+ * @returns { void }
155
+ */
156
+ private async createNetworkChannelIfNotExist(
157
+ networkId: TNetworkId_S,
158
+ channelName: TChannelName,
159
+ ): Promise<void> {
160
+ const wasCreated: boolean = await this.networkChannelHash
161
+ .createNetworkChannelIfNotExist(
162
+ networkId,
163
+ channelName,
164
+ );
165
+
166
+ if (wasCreated) {
167
+ this._globalChannelPubsub
168
+ .publish(
169
+ `~/networks/${networkId}/channel-created`,
170
+ `${AUTHORITY_ON_CREATE_CHANNEL}:${channelName}`,
171
+ );
172
+ }
173
+ }
174
+
175
+ // ****************************************************************************
176
+ // *** Update
177
+ // ****************************************************************************
178
+
179
+ /**
180
+ * Increases the usage count of a channel.
181
+ *
182
+ * @param { TNetworkId_S } networkId
183
+ * @param { TChannelName } channelName
184
+ * @param { number } usage
185
+ *
186
+ * @returns { void }
187
+ */
188
+ public increaseUsageCount(
189
+ networkId: TNetworkId_S,
190
+ channelName: TChannelName,
191
+ usage: number,
192
+ ): void {
193
+ const channelUsageCache: {
194
+ networkId: TNetworkId_S;
195
+ usage: number;
196
+ } | undefined = this.channelUsageCount.get(channelName);
197
+
198
+ this.channelUsageCount.set(
199
+ channelName,
200
+ channelUsageCache ? {
201
+ ...channelUsageCache,
202
+ usage: channelUsageCache.usage + usage,
203
+ } : {
204
+ networkId,
205
+ usage: usage,
206
+ },
207
+ );
208
+ }
209
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export {
2
+ FluxMeshServer,
3
+ } from './main';