@serve.zone/dcrouter 6.3.0 → 6.4.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/dist_serve/bundle.js +559 -452
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +22 -0
- package/dist_ts/classes.dcrouter.js +42 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/remoteingress.handler.d.ts +8 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +107 -0
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +56 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +133 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +45 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +107 -0
- package/dist_ts/remoteingress/index.d.ts +2 -0
- package/dist_ts/remoteingress/index.js +3 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/remoteingress.d.ts +24 -0
- package/dist_ts_interfaces/data/remoteingress.js +2 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/remoteingress.d.ts +89 -0
- package/dist_ts_interfaces/requests/remoteingress.js +3 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +19 -0
- package/dist_ts_web/appstate.js +124 -2
- package/dist_ts_web/elements/index.d.ts +1 -0
- package/dist_ts_web/elements/index.js +2 -1
- package/dist_ts_web/elements/ops-dashboard.js +6 -1
- package/dist_ts_web/elements/ops-view-remoteingress.d.ts +20 -0
- package/dist_ts_web/elements/ops-view-remoteingress.js +317 -0
- package/dist_ts_web/router.d.ts +1 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +3 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +66 -0
- package/ts/index.ts +3 -0
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/index.ts +2 -1
- package/ts/opsserver/handlers/remoteingress.handler.ts +163 -0
- package/ts/plugins.ts +3 -1
- package/ts/remoteingress/classes.remoteingress-manager.ts +160 -0
- package/ts/remoteingress/classes.tunnel-manager.ts +126 -0
- package/ts/remoteingress/index.ts +2 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +180 -1
- package/ts_web/elements/index.ts +1 -0
- package/ts_web/elements/ops-dashboard.ts +5 -0
- package/ts_web/elements/ops-view-remoteingress.ts +290 -0
- package/ts_web/router.ts +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
export class RemoteIngressHandler {
|
|
6
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
7
|
+
|
|
8
|
+
constructor(private opsServerRef: OpsServer) {
|
|
9
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
10
|
+
this.registerHandlers();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private registerHandlers(): void {
|
|
14
|
+
// Get all remote ingress edges
|
|
15
|
+
this.typedrouter.addTypedHandler(
|
|
16
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>(
|
|
17
|
+
'getRemoteIngresses',
|
|
18
|
+
async (dataArg, toolsArg) => {
|
|
19
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
20
|
+
if (!manager) {
|
|
21
|
+
return { edges: [] };
|
|
22
|
+
}
|
|
23
|
+
// Return edges without secrets
|
|
24
|
+
const edges = manager.getAllEdges().map((e) => ({
|
|
25
|
+
...e,
|
|
26
|
+
secret: '********', // Never expose secrets via API
|
|
27
|
+
}));
|
|
28
|
+
return { edges };
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Create a new remote ingress edge
|
|
34
|
+
this.typedrouter.addTypedHandler(
|
|
35
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>(
|
|
36
|
+
'createRemoteIngress',
|
|
37
|
+
async (dataArg, toolsArg) => {
|
|
38
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
39
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
40
|
+
|
|
41
|
+
if (!manager) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
edge: null as any,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const edge = await manager.createEdge(
|
|
49
|
+
dataArg.name,
|
|
50
|
+
dataArg.listenPorts,
|
|
51
|
+
dataArg.tags,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Sync allowed edges with the hub
|
|
55
|
+
if (tunnelManager) {
|
|
56
|
+
await tunnelManager.syncAllowedEdges();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { success: true, edge };
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Delete a remote ingress edge
|
|
65
|
+
this.typedrouter.addTypedHandler(
|
|
66
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>(
|
|
67
|
+
'deleteRemoteIngress',
|
|
68
|
+
async (dataArg, toolsArg) => {
|
|
69
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
70
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
71
|
+
|
|
72
|
+
if (!manager) {
|
|
73
|
+
return { success: false, message: 'RemoteIngress not configured' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const deleted = await manager.deleteEdge(dataArg.id);
|
|
77
|
+
if (deleted && tunnelManager) {
|
|
78
|
+
await tunnelManager.syncAllowedEdges();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: deleted,
|
|
83
|
+
message: deleted ? undefined : 'Edge not found',
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Update a remote ingress edge
|
|
90
|
+
this.typedrouter.addTypedHandler(
|
|
91
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>(
|
|
92
|
+
'updateRemoteIngress',
|
|
93
|
+
async (dataArg, toolsArg) => {
|
|
94
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
95
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
96
|
+
|
|
97
|
+
if (!manager) {
|
|
98
|
+
return { success: false, edge: null as any };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const edge = await manager.updateEdge(dataArg.id, {
|
|
102
|
+
name: dataArg.name,
|
|
103
|
+
listenPorts: dataArg.listenPorts,
|
|
104
|
+
enabled: dataArg.enabled,
|
|
105
|
+
tags: dataArg.tags,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!edge) {
|
|
109
|
+
return { success: false, edge: null as any };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sync allowed edges if enabled status changed
|
|
113
|
+
if (tunnelManager && dataArg.enabled !== undefined) {
|
|
114
|
+
await tunnelManager.syncAllowedEdges();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { success: true, edge: { ...edge, secret: '********' } };
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Regenerate secret for an edge
|
|
123
|
+
this.typedrouter.addTypedHandler(
|
|
124
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
|
|
125
|
+
'regenerateRemoteIngressSecret',
|
|
126
|
+
async (dataArg, toolsArg) => {
|
|
127
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
128
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
129
|
+
|
|
130
|
+
if (!manager) {
|
|
131
|
+
return { success: false, secret: '' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const secret = await manager.regenerateSecret(dataArg.id);
|
|
135
|
+
if (!secret) {
|
|
136
|
+
return { success: false, secret: '' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Sync allowed edges since secret changed
|
|
140
|
+
if (tunnelManager) {
|
|
141
|
+
await tunnelManager.syncAllowedEdges();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { success: true, secret };
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Get runtime status of all edges
|
|
150
|
+
this.typedrouter.addTypedHandler(
|
|
151
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>(
|
|
152
|
+
'getRemoteIngressStatus',
|
|
153
|
+
async (dataArg, toolsArg) => {
|
|
154
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
155
|
+
if (!tunnelManager) {
|
|
156
|
+
return { statuses: [] };
|
|
157
|
+
}
|
|
158
|
+
return { statuses: tunnelManager.getEdgeStatuses() };
|
|
159
|
+
},
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
package/ts/plugins.ts
CHANGED
|
@@ -23,9 +23,11 @@ export {
|
|
|
23
23
|
|
|
24
24
|
// @serve.zone scope
|
|
25
25
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
26
|
+
import * as remoteingress from '@serve.zone/remoteingress';
|
|
26
27
|
|
|
27
28
|
export {
|
|
28
|
-
servezoneInterfaces
|
|
29
|
+
servezoneInterfaces,
|
|
30
|
+
remoteingress,
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// @api.global scope
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { StorageManager } from '../storage/classes.storagemanager.js';
|
|
3
|
+
import type { IRemoteIngress } from '../../ts_interfaces/data/remoteingress.js';
|
|
4
|
+
|
|
5
|
+
const STORAGE_PREFIX = '/remote-ingress/';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Manages CRUD for remote ingress edge registrations.
|
|
9
|
+
* Persists edge configs via StorageManager and provides
|
|
10
|
+
* the allowed edges list for the Rust hub.
|
|
11
|
+
*/
|
|
12
|
+
export class RemoteIngressManager {
|
|
13
|
+
private storageManager: StorageManager;
|
|
14
|
+
private edges: Map<string, IRemoteIngress> = new Map();
|
|
15
|
+
|
|
16
|
+
constructor(storageManager: StorageManager) {
|
|
17
|
+
this.storageManager = storageManager;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load all edge registrations from storage into memory.
|
|
22
|
+
*/
|
|
23
|
+
public async initialize(): Promise<void> {
|
|
24
|
+
const keys = await this.storageManager.list(STORAGE_PREFIX);
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
|
|
27
|
+
if (edge) {
|
|
28
|
+
this.edges.set(edge.id, edge);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a new edge registration.
|
|
35
|
+
*/
|
|
36
|
+
public async createEdge(
|
|
37
|
+
name: string,
|
|
38
|
+
listenPorts: number[],
|
|
39
|
+
tags?: string[],
|
|
40
|
+
): Promise<IRemoteIngress> {
|
|
41
|
+
const id = plugins.uuid.v4();
|
|
42
|
+
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
|
|
45
|
+
const edge: IRemoteIngress = {
|
|
46
|
+
id,
|
|
47
|
+
name,
|
|
48
|
+
secret,
|
|
49
|
+
listenPorts,
|
|
50
|
+
enabled: true,
|
|
51
|
+
tags: tags || [],
|
|
52
|
+
createdAt: now,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
|
57
|
+
this.edges.set(id, edge);
|
|
58
|
+
return edge;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get an edge by ID.
|
|
63
|
+
*/
|
|
64
|
+
public getEdge(id: string): IRemoteIngress | undefined {
|
|
65
|
+
return this.edges.get(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get all edge registrations.
|
|
70
|
+
*/
|
|
71
|
+
public getAllEdges(): IRemoteIngress[] {
|
|
72
|
+
return Array.from(this.edges.values());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Update an edge registration.
|
|
77
|
+
*/
|
|
78
|
+
public async updateEdge(
|
|
79
|
+
id: string,
|
|
80
|
+
updates: {
|
|
81
|
+
name?: string;
|
|
82
|
+
listenPorts?: number[];
|
|
83
|
+
enabled?: boolean;
|
|
84
|
+
tags?: string[];
|
|
85
|
+
},
|
|
86
|
+
): Promise<IRemoteIngress | null> {
|
|
87
|
+
const edge = this.edges.get(id);
|
|
88
|
+
if (!edge) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (updates.name !== undefined) edge.name = updates.name;
|
|
93
|
+
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
|
94
|
+
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
|
95
|
+
if (updates.tags !== undefined) edge.tags = updates.tags;
|
|
96
|
+
edge.updatedAt = Date.now();
|
|
97
|
+
|
|
98
|
+
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
|
99
|
+
this.edges.set(id, edge);
|
|
100
|
+
return edge;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Delete an edge registration.
|
|
105
|
+
*/
|
|
106
|
+
public async deleteEdge(id: string): Promise<boolean> {
|
|
107
|
+
if (!this.edges.has(id)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
await this.storageManager.delete(`${STORAGE_PREFIX}${id}`);
|
|
111
|
+
this.edges.delete(id);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Regenerate the secret for an edge.
|
|
117
|
+
*/
|
|
118
|
+
public async regenerateSecret(id: string): Promise<string | null> {
|
|
119
|
+
const edge = this.edges.get(id);
|
|
120
|
+
if (!edge) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
edge.secret = plugins.crypto.randomBytes(32).toString('hex');
|
|
125
|
+
edge.updatedAt = Date.now();
|
|
126
|
+
|
|
127
|
+
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
|
128
|
+
this.edges.set(id, edge);
|
|
129
|
+
return edge.secret;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Verify an edge's secret using constant-time comparison.
|
|
134
|
+
*/
|
|
135
|
+
public verifySecret(id: string, secret: string): boolean {
|
|
136
|
+
const edge = this.edges.get(id);
|
|
137
|
+
if (!edge) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const expected = Buffer.from(edge.secret);
|
|
141
|
+
const provided = Buffer.from(secret);
|
|
142
|
+
if (expected.length !== provided.length) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return plugins.crypto.timingSafeEqual(expected, provided);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the list of allowed edges (enabled only) for the Rust hub.
|
|
150
|
+
*/
|
|
151
|
+
public getAllowedEdges(): Array<{ id: string; secret: string }> {
|
|
152
|
+
const result: Array<{ id: string; secret: string }> = [];
|
|
153
|
+
for (const edge of this.edges.values()) {
|
|
154
|
+
if (edge.enabled) {
|
|
155
|
+
result.push({ id: edge.id, secret: edge.secret });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IRemoteIngressStatus } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
|
+
import type { RemoteIngressManager } from './classes.remoteingress-manager.js';
|
|
4
|
+
|
|
5
|
+
export interface ITunnelManagerConfig {
|
|
6
|
+
tunnelPort?: number;
|
|
7
|
+
targetHost?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manages the RemoteIngressHub instance and tracks connected edge statuses.
|
|
12
|
+
*/
|
|
13
|
+
export class TunnelManager {
|
|
14
|
+
private hub: InstanceType<typeof plugins.remoteingress.RemoteIngressHub>;
|
|
15
|
+
private manager: RemoteIngressManager;
|
|
16
|
+
private config: ITunnelManagerConfig;
|
|
17
|
+
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
|
18
|
+
|
|
19
|
+
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
|
20
|
+
this.manager = manager;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.hub = new plugins.remoteingress.RemoteIngressHub();
|
|
23
|
+
|
|
24
|
+
// Listen for edge connect/disconnect events
|
|
25
|
+
this.hub.on('edgeConnected', (data: { edgeId: string }) => {
|
|
26
|
+
const existing = this.edgeStatuses.get(data.edgeId);
|
|
27
|
+
this.edgeStatuses.set(data.edgeId, {
|
|
28
|
+
edgeId: data.edgeId,
|
|
29
|
+
connected: true,
|
|
30
|
+
publicIp: existing?.publicIp ?? null,
|
|
31
|
+
activeTunnels: 0,
|
|
32
|
+
lastHeartbeat: Date.now(),
|
|
33
|
+
connectedAt: Date.now(),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
|
|
38
|
+
const existing = this.edgeStatuses.get(data.edgeId);
|
|
39
|
+
if (existing) {
|
|
40
|
+
existing.connected = false;
|
|
41
|
+
existing.activeTunnels = 0;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.hub.on('streamOpened', (data: { edgeId: string; streamId: number }) => {
|
|
46
|
+
const existing = this.edgeStatuses.get(data.edgeId);
|
|
47
|
+
if (existing) {
|
|
48
|
+
existing.activeTunnels++;
|
|
49
|
+
existing.lastHeartbeat = Date.now();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.hub.on('streamClosed', (data: { edgeId: string; streamId: number }) => {
|
|
54
|
+
const existing = this.edgeStatuses.get(data.edgeId);
|
|
55
|
+
if (existing && existing.activeTunnels > 0) {
|
|
56
|
+
existing.activeTunnels--;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Start the tunnel hub and load allowed edges.
|
|
63
|
+
*/
|
|
64
|
+
public async start(): Promise<void> {
|
|
65
|
+
await this.hub.start({
|
|
66
|
+
tunnelPort: this.config.tunnelPort ?? 8443,
|
|
67
|
+
targetHost: this.config.targetHost ?? '127.0.0.1',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Send allowed edges to the hub
|
|
71
|
+
await this.syncAllowedEdges();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stop the tunnel hub.
|
|
76
|
+
*/
|
|
77
|
+
public async stop(): Promise<void> {
|
|
78
|
+
await this.hub.stop();
|
|
79
|
+
this.edgeStatuses.clear();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sync allowed edges from the manager to the hub.
|
|
84
|
+
* Call this after creating/deleting/updating edges.
|
|
85
|
+
*/
|
|
86
|
+
public async syncAllowedEdges(): Promise<void> {
|
|
87
|
+
const edges = this.manager.getAllowedEdges();
|
|
88
|
+
await this.hub.updateAllowedEdges(edges);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get runtime statuses for all known edges.
|
|
93
|
+
*/
|
|
94
|
+
public getEdgeStatuses(): IRemoteIngressStatus[] {
|
|
95
|
+
return Array.from(this.edgeStatuses.values());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get status for a specific edge.
|
|
100
|
+
*/
|
|
101
|
+
public getEdgeStatus(edgeId: string): IRemoteIngressStatus | undefined {
|
|
102
|
+
return this.edgeStatuses.get(edgeId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the count of connected edges.
|
|
107
|
+
*/
|
|
108
|
+
public getConnectedCount(): number {
|
|
109
|
+
let count = 0;
|
|
110
|
+
for (const status of this.edgeStatuses.values()) {
|
|
111
|
+
if (status.connected) count++;
|
|
112
|
+
}
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the total number of active tunnels across all edges.
|
|
118
|
+
*/
|
|
119
|
+
public getTotalActiveTunnels(): number {
|
|
120
|
+
let total = 0;
|
|
121
|
+
for (const status of this.edgeStatuses.values()) {
|
|
122
|
+
total += status.activeTunnels;
|
|
123
|
+
}
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
}
|
package/ts_web/appstate.ts
CHANGED
|
@@ -116,7 +116,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|
|
116
116
|
// Determine initial view from URL path
|
|
117
117
|
const getInitialView = (): string => {
|
|
118
118
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
|
119
|
-
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
|
|
119
|
+
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'];
|
|
120
120
|
const segments = path.split('/').filter(Boolean);
|
|
121
121
|
const view = segments[0];
|
|
122
122
|
return validViews.includes(view) ? view : 'overview';
|
|
@@ -192,6 +192,34 @@ export const certificateStatePart = await appState.getStatePart<ICertificateStat
|
|
|
192
192
|
'soft'
|
|
193
193
|
);
|
|
194
194
|
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// Remote Ingress State
|
|
197
|
+
// ============================================================================
|
|
198
|
+
|
|
199
|
+
export interface IRemoteIngressState {
|
|
200
|
+
edges: interfaces.data.IRemoteIngress[];
|
|
201
|
+
statuses: interfaces.data.IRemoteIngressStatus[];
|
|
202
|
+
selectedEdgeId: string | null;
|
|
203
|
+
newEdgeSecret: string | null;
|
|
204
|
+
isLoading: boolean;
|
|
205
|
+
error: string | null;
|
|
206
|
+
lastUpdated: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngressState>(
|
|
210
|
+
'remoteIngress',
|
|
211
|
+
{
|
|
212
|
+
edges: [],
|
|
213
|
+
statuses: [],
|
|
214
|
+
selectedEdgeId: null,
|
|
215
|
+
newEdgeSecret: null,
|
|
216
|
+
isLoading: false,
|
|
217
|
+
error: null,
|
|
218
|
+
lastUpdated: 0,
|
|
219
|
+
},
|
|
220
|
+
'soft'
|
|
221
|
+
);
|
|
222
|
+
|
|
195
223
|
// Actions for state management
|
|
196
224
|
interface IActionContext {
|
|
197
225
|
identity: interfaces.data.IIdentity | null;
|
|
@@ -378,6 +406,13 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|
|
378
406
|
}, 100);
|
|
379
407
|
}
|
|
380
408
|
|
|
409
|
+
// If switching to remoteingress view, ensure we fetch edge data
|
|
410
|
+
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
|
411
|
+
setTimeout(() => {
|
|
412
|
+
remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
413
|
+
}, 100);
|
|
414
|
+
}
|
|
415
|
+
|
|
381
416
|
return {
|
|
382
417
|
...currentState,
|
|
383
418
|
activeView: viewName,
|
|
@@ -745,6 +780,150 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
|
|
745
780
|
}
|
|
746
781
|
);
|
|
747
782
|
|
|
783
|
+
// ============================================================================
|
|
784
|
+
// Remote Ingress Actions
|
|
785
|
+
// ============================================================================
|
|
786
|
+
|
|
787
|
+
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
|
|
788
|
+
const context = getActionContext();
|
|
789
|
+
const currentState = statePartArg.getState();
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
793
|
+
interfaces.requests.IReq_GetRemoteIngresses
|
|
794
|
+
>('/typedrequest', 'getRemoteIngresses');
|
|
795
|
+
|
|
796
|
+
const statusRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
797
|
+
interfaces.requests.IReq_GetRemoteIngressStatus
|
|
798
|
+
>('/typedrequest', 'getRemoteIngressStatus');
|
|
799
|
+
|
|
800
|
+
const [edgesResponse, statusResponse] = await Promise.all([
|
|
801
|
+
edgesRequest.fire({ identity: context.identity }),
|
|
802
|
+
statusRequest.fire({ identity: context.identity }),
|
|
803
|
+
]);
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
...currentState,
|
|
807
|
+
edges: edgesResponse.edges,
|
|
808
|
+
statuses: statusResponse.statuses,
|
|
809
|
+
isLoading: false,
|
|
810
|
+
error: null,
|
|
811
|
+
lastUpdated: Date.now(),
|
|
812
|
+
};
|
|
813
|
+
} catch (error) {
|
|
814
|
+
return {
|
|
815
|
+
...currentState,
|
|
816
|
+
isLoading: false,
|
|
817
|
+
error: error instanceof Error ? error.message : 'Failed to fetch remote ingress data',
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|
823
|
+
name: string;
|
|
824
|
+
listenPorts: number[];
|
|
825
|
+
tags?: string[];
|
|
826
|
+
}>(async (statePartArg, dataArg) => {
|
|
827
|
+
const context = getActionContext();
|
|
828
|
+
const currentState = statePartArg.getState();
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
832
|
+
interfaces.requests.IReq_CreateRemoteIngress
|
|
833
|
+
>('/typedrequest', 'createRemoteIngress');
|
|
834
|
+
|
|
835
|
+
const response = await request.fire({
|
|
836
|
+
identity: context.identity,
|
|
837
|
+
name: dataArg.name,
|
|
838
|
+
listenPorts: dataArg.listenPorts,
|
|
839
|
+
tags: dataArg.tags,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
if (response.success) {
|
|
843
|
+
// Refresh the list and store the new secret for display
|
|
844
|
+
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
845
|
+
return {
|
|
846
|
+
...statePartArg.getState(),
|
|
847
|
+
newEdgeSecret: response.edge.secret,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return currentState;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
return {
|
|
854
|
+
...currentState,
|
|
855
|
+
error: error instanceof Error ? error.message : 'Failed to create edge',
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
|
|
861
|
+
async (statePartArg, edgeId) => {
|
|
862
|
+
const context = getActionContext();
|
|
863
|
+
const currentState = statePartArg.getState();
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
867
|
+
interfaces.requests.IReq_DeleteRemoteIngress
|
|
868
|
+
>('/typedrequest', 'deleteRemoteIngress');
|
|
869
|
+
|
|
870
|
+
await request.fire({
|
|
871
|
+
identity: context.identity,
|
|
872
|
+
id: edgeId,
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
876
|
+
return statePartArg.getState();
|
|
877
|
+
} catch (error) {
|
|
878
|
+
return {
|
|
879
|
+
...currentState,
|
|
880
|
+
error: error instanceof Error ? error.message : 'Failed to delete edge',
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
|
887
|
+
async (statePartArg, edgeId) => {
|
|
888
|
+
const context = getActionContext();
|
|
889
|
+
const currentState = statePartArg.getState();
|
|
890
|
+
|
|
891
|
+
try {
|
|
892
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
893
|
+
interfaces.requests.IReq_RegenerateRemoteIngressSecret
|
|
894
|
+
>('/typedrequest', 'regenerateRemoteIngressSecret');
|
|
895
|
+
|
|
896
|
+
const response = await request.fire({
|
|
897
|
+
identity: context.identity,
|
|
898
|
+
id: edgeId,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
if (response.success) {
|
|
902
|
+
return {
|
|
903
|
+
...currentState,
|
|
904
|
+
newEdgeSecret: response.secret,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return currentState;
|
|
909
|
+
} catch (error) {
|
|
910
|
+
return {
|
|
911
|
+
...currentState,
|
|
912
|
+
error: error instanceof Error ? error.message : 'Failed to regenerate secret',
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
|
|
919
|
+
async (statePartArg) => {
|
|
920
|
+
return {
|
|
921
|
+
...statePartArg.getState(),
|
|
922
|
+
newEdgeSecret: null,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
|
|
748
927
|
// Combined refresh action for efficient polling
|
|
749
928
|
async function dispatchCombinedRefreshAction() {
|
|
750
929
|
const context = getActionContext();
|
package/ts_web/elements/index.ts
CHANGED