@serve.zone/dcrouter 16.4.0 → 16.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +852 -852
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -1
- package/dist_ts/classes.dcrouter.js +20 -4
- package/dist_ts/db/documents/classes.cached.email.js +2 -2
- package/dist_ts/db/documents/classes.dns-record.doc.d.ts +4 -0
- package/dist_ts/db/documents/classes.dns-record.doc.js +33 -2
- package/dist_ts/db/documents/classes.email-domain.doc.d.ts +5 -1
- package/dist_ts/db/documents/classes.email-domain.doc.js +26 -2
- package/dist_ts/dns/manager.dns.d.ts +6 -0
- package/dist_ts/dns/manager.dns.js +81 -32
- package/dist_ts/dns/providers/cloudflare.provider.d.ts +2 -0
- package/dist_ts/dns/providers/cloudflare.provider.js +45 -28
- package/dist_ts/email/classes.email-domain.manager.d.ts +43 -38
- package/dist_ts/email/classes.email-domain.manager.js +391 -324
- package/dist_ts/email/classes.mail-dns-sync.d.ts +58 -17
- package/dist_ts/email/classes.mail-dns-sync.js +857 -141
- package/dist_ts/email/classes.mail-edge-eligibility.d.ts +55 -0
- package/dist_ts/email/classes.mail-edge-eligibility.js +168 -0
- package/dist_ts/email/classes.mail-egress-coordinator.d.ts +3 -1
- package/dist_ts/email/classes.mail-egress-coordinator.js +43 -22
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +3 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +58 -12
- package/dist_ts/email/index.d.ts +1 -0
- package/dist_ts/email/index.js +2 -1
- package/dist_ts/opsserver/handlers/email-domain.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/email-domain.handler.js +138 -22
- package/dist_ts/remoteingress/classes.hub-lifecycle.js +2 -1
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +6 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +40 -2
- package/dist_ts_interfaces/data/dns-record.d.ts +6 -0
- package/dist_ts_interfaces/data/email-domain.d.ts +81 -0
- package/dist_ts_interfaces/requests/email-domains.d.ts +25 -17
- package/dist_ts_migrations/index.js +185 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/email-domains.d.ts +16 -0
- package/dist_ts_web/appstate/email-domains.js +140 -19
- package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +1 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +36 -13
- package/package.json +7 -7
- package/readme.md +13 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +22 -3
- package/ts/db/documents/classes.cached.email.ts +1 -0
- package/ts/db/documents/classes.dns-record.doc.ts +20 -0
- package/ts/db/documents/classes.email-domain.doc.ts +14 -0
- package/ts/dns/manager.dns.ts +89 -29
- package/ts/dns/providers/cloudflare.provider.ts +46 -27
- package/ts/email/classes.email-domain.manager.ts +416 -368
- package/ts/email/classes.mail-dns-sync.ts +913 -146
- package/ts/email/classes.mail-edge-eligibility.ts +205 -0
- package/ts/email/classes.mail-egress-coordinator.ts +46 -23
- package/ts/email/classes.workapp-mail-manager.ts +69 -13
- package/ts/email/index.ts +1 -0
- package/ts/opsserver/handlers/email-domain.handler.ts +154 -21
- package/ts/remoteingress/classes.hub-lifecycle.ts +1 -0
- package/ts/remoteingress/classes.tunnel-manager.ts +53 -7
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/email-domains.ts +187 -18
- package/ts_web/elements/email/ops-view-email-domains.ts +44 -12
|
@@ -3,6 +3,91 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
4
|
import { requireOpsAuth } from '../helpers/auth.js';
|
|
5
5
|
|
|
6
|
+
const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
|
|
7
|
+
return typeof valueArg === 'object' && valueArg !== null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const normalizeActionErrors = (
|
|
11
|
+
valueArg: unknown,
|
|
12
|
+
): interfaces.requests.IEmailDomainActionError[] | undefined => {
|
|
13
|
+
if (!Array.isArray(valueArg)) return undefined;
|
|
14
|
+
|
|
15
|
+
const errors: interfaces.requests.IEmailDomainActionError[] = [];
|
|
16
|
+
for (const entry of valueArg) {
|
|
17
|
+
if (!isRecord(entry) || typeof entry.message !== 'string') continue;
|
|
18
|
+
errors.push({
|
|
19
|
+
code: typeof entry.code === 'string' ? entry.code : 'EMAIL_DOMAIN_ACTION_FAILED',
|
|
20
|
+
message: entry.message,
|
|
21
|
+
recordKey: typeof entry.recordKey === 'string' ? entry.recordKey : undefined,
|
|
22
|
+
retryable: typeof entry.retryable === 'boolean' ? entry.retryable : undefined,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return errors.length > 0 ? errors : undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const normalizeEmailDomainActionResult = (
|
|
29
|
+
valueArg: unknown,
|
|
30
|
+
defaultSuccess = true,
|
|
31
|
+
): interfaces.requests.IEmailDomainActionResult => {
|
|
32
|
+
const value = isRecord(valueArg) ? valueArg : {};
|
|
33
|
+
const directDomain = isRecord(value.domain)
|
|
34
|
+
? value.domain
|
|
35
|
+
: typeof value.id === 'string' && typeof value.domain === 'string'
|
|
36
|
+
? value
|
|
37
|
+
: undefined;
|
|
38
|
+
const reconciliation = isRecord(directDomain?.reconciliation)
|
|
39
|
+
? directDomain.reconciliation
|
|
40
|
+
: undefined;
|
|
41
|
+
const rawLifecycle = value.lifecycleStatus ?? reconciliation?.lifecycleStatus;
|
|
42
|
+
const lifecycleStatus =
|
|
43
|
+
rawLifecycle === 'active' ||
|
|
44
|
+
rawLifecycle === 'pending' ||
|
|
45
|
+
rawLifecycle === 'failed' ||
|
|
46
|
+
rawLifecycle === 'deleting'
|
|
47
|
+
? rawLifecycle
|
|
48
|
+
: undefined;
|
|
49
|
+
const reconciliationCapability = isRecord(reconciliation?.capability)
|
|
50
|
+
? reconciliation.capability.egressIdentityProof
|
|
51
|
+
: undefined;
|
|
52
|
+
const rawCapability = value.capability ?? reconciliationCapability;
|
|
53
|
+
const capability =
|
|
54
|
+
rawCapability === 'supported' || rawCapability === 'unsupported'
|
|
55
|
+
? rawCapability
|
|
56
|
+
: undefined;
|
|
57
|
+
const errors = normalizeActionErrors(value.errors ?? reconciliation?.errors);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
success: typeof value.success === 'boolean'
|
|
61
|
+
? value.success
|
|
62
|
+
: lifecycleStatus === 'failed'
|
|
63
|
+
? false
|
|
64
|
+
: defaultSuccess,
|
|
65
|
+
message: typeof value.message === 'string' ? value.message : errors?.[0]?.message,
|
|
66
|
+
lifecycleStatus,
|
|
67
|
+
code: typeof value.code === 'string' ? value.code : errors?.[0]?.code,
|
|
68
|
+
errors,
|
|
69
|
+
retryAt: typeof value.retryAt === 'string'
|
|
70
|
+
? value.retryAt
|
|
71
|
+
: typeof reconciliation?.retryAt === 'string'
|
|
72
|
+
? reconciliation.retryAt
|
|
73
|
+
: undefined,
|
|
74
|
+
capability,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const normalizeEmailDomainActionFailure = (
|
|
79
|
+
errorArg: unknown,
|
|
80
|
+
fallbackMessageArg: string,
|
|
81
|
+
): interfaces.requests.IEmailDomainActionResult => {
|
|
82
|
+
const result = normalizeEmailDomainActionResult(errorArg, false);
|
|
83
|
+
const errorMessage = errorArg instanceof Error ? errorArg.message : undefined;
|
|
84
|
+
return {
|
|
85
|
+
...result,
|
|
86
|
+
success: false,
|
|
87
|
+
message: result.message || errorMessage || result.errors?.[0]?.message || fallbackMessageArg,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
6
91
|
/**
|
|
7
92
|
* CRUD + DNS provisioning handler for email domains.
|
|
8
93
|
*
|
|
@@ -31,6 +116,23 @@ export class EmailDomainHandler {
|
|
|
31
116
|
return this.opsServerRef.dcRouterRef.emailDomainManager;
|
|
32
117
|
}
|
|
33
118
|
|
|
119
|
+
private managerUnavailable(): interfaces.requests.IEmailDomainActionResult {
|
|
120
|
+
const message = 'EmailDomainManager not initialized';
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
message,
|
|
124
|
+
code: 'EMAIL_DOMAIN_MANAGER_UNAVAILABLE',
|
|
125
|
+
capability: 'unsupported',
|
|
126
|
+
errors: [
|
|
127
|
+
{
|
|
128
|
+
code: 'EMAIL_DOMAIN_MANAGER_UNAVAILABLE',
|
|
129
|
+
message,
|
|
130
|
+
retryable: true,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
34
136
|
private registerHandlers(): void {
|
|
35
137
|
// List all email domains
|
|
36
138
|
this.typedrouter.addTypedHandler(
|
|
@@ -63,10 +165,10 @@ export class EmailDomainHandler {
|
|
|
63
165
|
async (dataArg) => {
|
|
64
166
|
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
65
167
|
if (!this.manager) {
|
|
66
|
-
return
|
|
168
|
+
return this.managerUnavailable();
|
|
67
169
|
}
|
|
68
170
|
try {
|
|
69
|
-
const
|
|
171
|
+
const rawResult: unknown = await this.manager.createEmailDomain({
|
|
70
172
|
linkedDomainId: dataArg.linkedDomainId,
|
|
71
173
|
subdomain: dataArg.subdomain,
|
|
72
174
|
dkimSelector: dataArg.dkimSelector,
|
|
@@ -75,9 +177,18 @@ export class EmailDomainHandler {
|
|
|
75
177
|
rotationIntervalDays: dataArg.rotationIntervalDays,
|
|
76
178
|
remoteIngress: dataArg.remoteIngress,
|
|
77
179
|
});
|
|
78
|
-
|
|
180
|
+
const result = normalizeEmailDomainActionResult(rawResult);
|
|
181
|
+
if (!result.success) return result;
|
|
182
|
+
|
|
183
|
+
const resultRecord = isRecord(rawResult) ? rawResult : undefined;
|
|
184
|
+
const domain = isRecord(resultRecord?.domain)
|
|
185
|
+
? resultRecord.domain as unknown as interfaces.data.IEmailDomain
|
|
186
|
+
: typeof resultRecord?.id === 'string'
|
|
187
|
+
? rawResult as interfaces.data.IEmailDomain
|
|
188
|
+
: undefined;
|
|
189
|
+
return { ...result, domain };
|
|
79
190
|
} catch (err: unknown) {
|
|
80
|
-
return
|
|
191
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to create email domain');
|
|
81
192
|
}
|
|
82
193
|
},
|
|
83
194
|
),
|
|
@@ -90,18 +201,18 @@ export class EmailDomainHandler {
|
|
|
90
201
|
async (dataArg) => {
|
|
91
202
|
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
92
203
|
if (!this.manager) {
|
|
93
|
-
return
|
|
204
|
+
return this.managerUnavailable();
|
|
94
205
|
}
|
|
95
206
|
try {
|
|
96
|
-
await this.manager.updateEmailDomain(dataArg.id, {
|
|
207
|
+
const rawResult: unknown = await this.manager.updateEmailDomain(dataArg.id, {
|
|
97
208
|
rotateKeys: dataArg.rotateKeys,
|
|
98
209
|
rotationIntervalDays: dataArg.rotationIntervalDays,
|
|
99
210
|
rateLimits: dataArg.rateLimits,
|
|
100
211
|
remoteIngress: dataArg.remoteIngress,
|
|
101
212
|
});
|
|
102
|
-
return
|
|
213
|
+
return normalizeEmailDomainActionResult(rawResult);
|
|
103
214
|
} catch (err: unknown) {
|
|
104
|
-
return
|
|
215
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to update email domain');
|
|
105
216
|
}
|
|
106
217
|
},
|
|
107
218
|
),
|
|
@@ -114,13 +225,13 @@ export class EmailDomainHandler {
|
|
|
114
225
|
async (dataArg) => {
|
|
115
226
|
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
116
227
|
if (!this.manager) {
|
|
117
|
-
return
|
|
228
|
+
return this.managerUnavailable();
|
|
118
229
|
}
|
|
119
230
|
try {
|
|
120
|
-
await this.manager.deleteEmailDomain(dataArg.id);
|
|
121
|
-
return
|
|
231
|
+
const rawResult: unknown = await this.manager.deleteEmailDomain(dataArg.id);
|
|
232
|
+
return normalizeEmailDomainActionResult(rawResult);
|
|
122
233
|
} catch (err: unknown) {
|
|
123
|
-
return
|
|
234
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to delete email domain');
|
|
124
235
|
}
|
|
125
236
|
},
|
|
126
237
|
),
|
|
@@ -133,14 +244,27 @@ export class EmailDomainHandler {
|
|
|
133
244
|
async (dataArg) => {
|
|
134
245
|
await this.requireAuth(dataArg, 'email-domains:read' as any);
|
|
135
246
|
if (!this.manager) {
|
|
136
|
-
return
|
|
247
|
+
return this.managerUnavailable();
|
|
137
248
|
}
|
|
138
249
|
try {
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
250
|
+
const rawResult: unknown = await this.manager.validateDns(dataArg.id);
|
|
251
|
+
const result = normalizeEmailDomainActionResult(rawResult);
|
|
252
|
+
if (!result.success) return result;
|
|
253
|
+
|
|
254
|
+
const resultRecord = isRecord(rawResult) ? rawResult : undefined;
|
|
255
|
+
const records = (
|
|
256
|
+
Array.isArray(rawResult)
|
|
257
|
+
? rawResult
|
|
258
|
+
: Array.isArray(resultRecord?.records)
|
|
259
|
+
? resultRecord.records
|
|
260
|
+
: undefined
|
|
261
|
+
) as interfaces.data.IEmailDnsRecord[] | undefined;
|
|
262
|
+
const domain = isRecord(resultRecord?.domain)
|
|
263
|
+
? resultRecord.domain as unknown as interfaces.data.IEmailDomain
|
|
264
|
+
: await this.manager.getById(dataArg.id) ?? undefined;
|
|
265
|
+
return { ...result, domain, records };
|
|
142
266
|
} catch (err: unknown) {
|
|
143
|
-
return
|
|
267
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to validate email domain DNS');
|
|
144
268
|
}
|
|
145
269
|
},
|
|
146
270
|
),
|
|
@@ -165,13 +289,22 @@ export class EmailDomainHandler {
|
|
|
165
289
|
async (dataArg) => {
|
|
166
290
|
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
167
291
|
if (!this.manager) {
|
|
168
|
-
return
|
|
292
|
+
return this.managerUnavailable();
|
|
169
293
|
}
|
|
170
294
|
try {
|
|
171
|
-
const
|
|
172
|
-
|
|
295
|
+
const rawResult: unknown = await this.manager.provisionDnsRecords(dataArg.id);
|
|
296
|
+
const result = normalizeEmailDomainActionResult(rawResult);
|
|
297
|
+
if (!result.success) return result;
|
|
298
|
+
|
|
299
|
+
const resultRecord = isRecord(rawResult) ? rawResult : undefined;
|
|
300
|
+
const provisioned = typeof rawResult === 'number'
|
|
301
|
+
? rawResult
|
|
302
|
+
: typeof resultRecord?.provisioned === 'number'
|
|
303
|
+
? resultRecord.provisioned
|
|
304
|
+
: undefined;
|
|
305
|
+
return { ...result, provisioned };
|
|
173
306
|
} catch (err: unknown) {
|
|
174
|
-
return
|
|
307
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to provision email domain DNS');
|
|
175
308
|
}
|
|
176
309
|
},
|
|
177
310
|
),
|
|
@@ -223,6 +223,7 @@ export class RemoteIngressHubLifecycle {
|
|
|
223
223
|
tls: tlsConfig,
|
|
224
224
|
performance: manager.getHubPerformanceConfig(),
|
|
225
225
|
});
|
|
226
|
+
tunnelManager.setOnTopologyChanged((reason) => this.dcRouterRef.mailDnsSync.requestSync(reason));
|
|
226
227
|
try {
|
|
227
228
|
await tunnelManager.start();
|
|
228
229
|
} catch (err) {
|
|
@@ -20,8 +20,14 @@ export type TRemoteIngressEgressProxyRequest = Omit<plugins.remoteingress.IEgres
|
|
|
20
20
|
edgeFilter?: string[];
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
export type TRemoteIngressHubStatus = Awaited<ReturnType<
|
|
24
|
+
InstanceType<typeof plugins.remoteingress.RemoteIngressHub>['getStatus']
|
|
25
|
+
>>;
|
|
26
|
+
type TRemoteIngressHubEdgeStatus = TRemoteIngressHubStatus['connectedEdges'][number];
|
|
27
|
+
|
|
23
28
|
/** Cooldown applied to an edge after a failed egress open before it is deprioritized. */
|
|
24
29
|
const egressFailureCooldownMs = 30_000;
|
|
30
|
+
const egressStatusFreshnessMs = 45_000;
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
33
|
* Manages the RemoteIngressHub instance and tracks connected edge statuses.
|
|
@@ -38,6 +44,7 @@ export class TunnelManager {
|
|
|
38
44
|
private egressProxyEdgeByProxyId: Map<string, string> = new Map();
|
|
39
45
|
private activeEgressProxiesByEdge: Map<string, number> = new Map();
|
|
40
46
|
private egressFailureCooldownUntil: Map<string, number> = new Map();
|
|
47
|
+
private onTopologyChanged?: (reason: string) => void;
|
|
41
48
|
|
|
42
49
|
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
|
43
50
|
this.manager = manager;
|
|
@@ -54,10 +61,12 @@ export class TunnelManager {
|
|
|
54
61
|
lastHeartbeat: Date.now(),
|
|
55
62
|
connectedAt: Date.now(),
|
|
56
63
|
});
|
|
64
|
+
this.onTopologyChanged?.(`RemoteIngress edge connected: ${data.edgeId}`);
|
|
57
65
|
});
|
|
58
66
|
|
|
59
67
|
this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
|
|
60
68
|
this.edgeStatuses.delete(data.edgeId);
|
|
69
|
+
this.onTopologyChanged?.(`RemoteIngress edge disconnected: ${data.edgeId}`);
|
|
61
70
|
});
|
|
62
71
|
|
|
63
72
|
this.hub.on('streamSummary', (data: {
|
|
@@ -94,6 +103,14 @@ export class TunnelManager {
|
|
|
94
103
|
this.egressFailureCooldownUntil.set(data.edgeId, Date.now() + egressFailureCooldownMs);
|
|
95
104
|
}
|
|
96
105
|
});
|
|
106
|
+
|
|
107
|
+
this.hub.on('egressIdentityChanged', (data) => {
|
|
108
|
+
this.onTopologyChanged?.(`RemoteIngress egress identity changed: ${data.edgeId}`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public setOnTopologyChanged(callback: (reason: string) => void): void {
|
|
113
|
+
this.onTopologyChanged = callback;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
/**
|
|
@@ -165,6 +182,7 @@ export class TunnelManager {
|
|
|
165
182
|
if (this.stopped) return;
|
|
166
183
|
if (!hubStatus || !hubStatus.connectedEdges) return;
|
|
167
184
|
|
|
185
|
+
const before = this.getTopologySignature();
|
|
168
186
|
const rustEdgeIds = new Set<string>();
|
|
169
187
|
|
|
170
188
|
for (const rustEdge of hubStatus.connectedEdges) {
|
|
@@ -199,6 +217,10 @@ export class TunnelManager {
|
|
|
199
217
|
this.edgeStatuses.delete(edgeId);
|
|
200
218
|
}
|
|
201
219
|
}
|
|
220
|
+
const after = this.getTopologySignature();
|
|
221
|
+
if (before !== after) {
|
|
222
|
+
this.onTopologyChanged?.('RemoteIngress mail egress topology changed');
|
|
223
|
+
}
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
/**
|
|
@@ -260,33 +282,43 @@ export class TunnelManager {
|
|
|
260
282
|
);
|
|
261
283
|
}
|
|
262
284
|
|
|
285
|
+
/** Authoritative hub state used by the source-bound mail identity adapter. */
|
|
286
|
+
public async getAuthoritativeHubStatus(): Promise<TRemoteIngressHubStatus | undefined> {
|
|
287
|
+
if (this.stopped) return undefined;
|
|
288
|
+
return await this.hub.getStatus();
|
|
289
|
+
}
|
|
290
|
+
|
|
263
291
|
/**
|
|
264
292
|
* Eligibility (connected + egressEnabled + egressTcpV1 + native QUIC), scoped by
|
|
265
293
|
* edgeId/edgeFilter, capped by per-edge maxConcurrentStreams, ordered deterministically:
|
|
266
294
|
* not-in-cooldown first, then fewest active tunnels, then edgeId.
|
|
267
295
|
*/
|
|
268
296
|
private selectEgressCandidates(
|
|
269
|
-
connectedEdges:
|
|
297
|
+
connectedEdges: TRemoteIngressHubEdgeStatus[],
|
|
270
298
|
request: TRemoteIngressEgressProxyRequest,
|
|
271
299
|
): Array<{ edgeId: string; activeStreams: number }> {
|
|
272
300
|
const eligible = connectedEdges.filter((edge: any) => {
|
|
273
301
|
const hasEgressCapability = Array.isArray(edge.capabilities) && edge.capabilities.includes('egressTcpV1');
|
|
274
302
|
const usesQuic = edge.transportMode === 'quic'
|
|
275
303
|
|| (edge.transportMode === 'quicWithFallback' && edge.fallbackUsed === false);
|
|
276
|
-
|
|
304
|
+
const observed = this.edgeStatuses.get(edge.edgeId);
|
|
305
|
+
const fresh = observed?.connected
|
|
306
|
+
&& observed.lastHeartbeat !== null
|
|
307
|
+
&& Date.now() - observed.lastHeartbeat <= egressStatusFreshnessMs;
|
|
308
|
+
return edge.egressEnabled === true && hasEgressCapability && usesQuic && fresh;
|
|
277
309
|
});
|
|
278
310
|
|
|
279
311
|
let scoped = eligible;
|
|
280
312
|
if (request.edgeId) {
|
|
281
|
-
scoped = eligible.filter((edge
|
|
313
|
+
scoped = eligible.filter((edge) => edge.edgeId === request.edgeId);
|
|
282
314
|
} else if (request.edgeFilter && request.edgeFilter.length > 0) {
|
|
283
315
|
const allowedIds = new Set(
|
|
284
316
|
this.manager.resolveEdgesByFilter(request.edgeFilter).map((edge) => edge.id),
|
|
285
317
|
);
|
|
286
|
-
scoped = eligible.filter((edge
|
|
318
|
+
scoped = eligible.filter((edge) => allowedIds.has(edge.edgeId));
|
|
287
319
|
}
|
|
288
320
|
|
|
289
|
-
const underCap = scoped.filter((edge
|
|
321
|
+
const underCap = scoped.filter((edge) => {
|
|
290
322
|
const cap = this.manager.getEdge(edge.edgeId)?.egress?.maxConcurrentStreams;
|
|
291
323
|
if (!cap) return true;
|
|
292
324
|
return (this.activeEgressProxiesByEdge.get(edge.edgeId) || 0) < cap;
|
|
@@ -295,7 +327,7 @@ export class TunnelManager {
|
|
|
295
327
|
const now = Date.now();
|
|
296
328
|
const inCooldown = (edgeId: string) => (this.egressFailureCooldownUntil.get(edgeId) || 0) > now;
|
|
297
329
|
return underCap
|
|
298
|
-
.map((edge
|
|
330
|
+
.map((edge) => ({ edgeId: edge.edgeId, activeStreams: Number(edge.activeStreams) || 0 }))
|
|
299
331
|
.sort((a, b) => {
|
|
300
332
|
const cooldownDelta = Number(inCooldown(a.edgeId)) - Number(inCooldown(b.edgeId));
|
|
301
333
|
if (cooldownDelta !== 0) return cooldownDelta;
|
|
@@ -355,7 +387,7 @@ export class TunnelManager {
|
|
|
355
387
|
return await this.hub.stopEgressTcpProxy(proxyId);
|
|
356
388
|
}
|
|
357
389
|
|
|
358
|
-
private applyRustStatus(status: IRemoteIngressStatus, rustEdge:
|
|
390
|
+
private applyRustStatus(status: IRemoteIngressStatus, rustEdge: TRemoteIngressHubEdgeStatus): void {
|
|
359
391
|
status.transportMode = rustEdge.transportMode;
|
|
360
392
|
status.fallbackUsed = rustEdge.fallbackUsed;
|
|
361
393
|
status.capabilities = rustEdge.capabilities;
|
|
@@ -367,6 +399,20 @@ export class TunnelManager {
|
|
|
367
399
|
status.udp = rustEdge.udp;
|
|
368
400
|
}
|
|
369
401
|
|
|
402
|
+
private getTopologySignature(): string {
|
|
403
|
+
return [...this.edgeStatuses.values()]
|
|
404
|
+
.map((status) => [
|
|
405
|
+
status.edgeId,
|
|
406
|
+
status.connected,
|
|
407
|
+
status.egressEnabled,
|
|
408
|
+
status.transportMode,
|
|
409
|
+
status.fallbackUsed,
|
|
410
|
+
[...(status.capabilities || [])].sort().join(','),
|
|
411
|
+
].join('|'))
|
|
412
|
+
.sort()
|
|
413
|
+
.join(';');
|
|
414
|
+
}
|
|
415
|
+
|
|
370
416
|
/**
|
|
371
417
|
* Get runtime statuses for all known edges.
|
|
372
418
|
*/
|