@serve.zone/dcrouter 15.11.0 → 16.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.
Files changed (73) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +34 -34
  3. package/dist_ts/00_commitinfo_data.js +2 -2
  4. package/dist_ts/classes.dcrouter.d.ts +10 -3
  5. package/dist_ts/classes.dcrouter.js +39 -43
  6. package/dist_ts/db/documents/classes.cached.email.d.ts +32 -0
  7. package/dist_ts/db/documents/classes.cached.email.js +67 -3
  8. package/dist_ts/db/documents/classes.email-domain.doc.d.ts +2 -1
  9. package/dist_ts/db/documents/classes.email-domain.doc.js +8 -2
  10. package/dist_ts/db/documents/classes.email-server-settings.doc.js +2 -2
  11. package/dist_ts/db/documents/classes.remote-ingress-edge.doc.d.ts +3 -0
  12. package/dist_ts/db/documents/classes.remote-ingress-edge.doc.js +20 -2
  13. package/dist_ts/dns/classes.dns-server-runtime.js +3 -1
  14. package/dist_ts/email/classes.accepted-email-spool.d.ts +5 -1
  15. package/dist_ts/email/classes.accepted-email-spool.js +9 -1
  16. package/dist_ts/email/classes.email-domain.manager.d.ts +17 -1
  17. package/dist_ts/email/classes.email-domain.manager.js +105 -18
  18. package/dist_ts/email/classes.email-route-builder.js +5 -2
  19. package/dist_ts/email/classes.email-settings.manager.js +3 -3
  20. package/dist_ts/email/classes.mail-dns-sync.d.ts +27 -0
  21. package/dist_ts/email/classes.mail-dns-sync.js +196 -0
  22. package/dist_ts/email/classes.mail-egress-coordinator.d.ts +37 -0
  23. package/dist_ts/email/classes.mail-egress-coordinator.js +113 -0
  24. package/dist_ts/email/index.d.ts +2 -0
  25. package/dist_ts/email/index.js +3 -1
  26. package/dist_ts/opsserver/handlers/config.handler.js +4 -2
  27. package/dist_ts/opsserver/handlers/email-domain.handler.js +3 -1
  28. package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +20 -3
  29. package/dist_ts/opsserver/handlers/email-ops.handler.js +141 -19
  30. package/dist_ts/opsserver/handlers/email-settings.handler.js +11 -6
  31. package/dist_ts/opsserver/handlers/remoteingress.handler.js +16 -7
  32. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +33 -1
  33. package/dist_ts/remoteingress/classes.remoteingress-manager.js +94 -2
  34. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +18 -0
  35. package/dist_ts/remoteingress/classes.tunnel-manager.js +114 -12
  36. package/dist_ts/remoteingress/edge-derivation-routes.d.ts +10 -0
  37. package/dist_ts/remoteingress/edge-derivation-routes.js +28 -0
  38. package/dist_ts/remoteingress/index.d.ts +1 -0
  39. package/dist_ts/remoteingress/index.js +2 -1
  40. package/dist_ts_interfaces/data/email-domain.d.ts +11 -1
  41. package/dist_ts_interfaces/data/email-settings.d.ts +11 -0
  42. package/dist_ts_interfaces/data/remoteingress.d.ts +10 -0
  43. package/dist_ts_interfaces/data/remoteingress.js +5 -2
  44. package/dist_ts_interfaces/requests/email-domains.d.ts +5 -1
  45. package/dist_ts_interfaces/requests/email-ops.d.ts +4 -0
  46. package/dist_ts_interfaces/requests/remoteingress.d.ts +11 -0
  47. package/dist_ts_migrations/index.js +91 -9
  48. package/dist_ts_web/00_commitinfo_data.js +2 -2
  49. package/package.json +4 -4
  50. package/ts/00_commitinfo_data.ts +1 -1
  51. package/ts/classes.dcrouter.ts +41 -48
  52. package/ts/db/documents/classes.cached.email.ts +72 -1
  53. package/ts/db/documents/classes.email-domain.doc.ts +4 -0
  54. package/ts/db/documents/classes.email-server-settings.doc.ts +1 -1
  55. package/ts/db/documents/classes.remote-ingress-edge.doc.ts +9 -0
  56. package/ts/dns/classes.dns-server-runtime.ts +2 -0
  57. package/ts/email/classes.accepted-email-spool.ts +12 -1
  58. package/ts/email/classes.email-domain.manager.ts +123 -18
  59. package/ts/email/classes.email-route-builder.ts +4 -1
  60. package/ts/email/classes.email-settings.manager.ts +2 -2
  61. package/ts/email/classes.mail-dns-sync.ts +218 -0
  62. package/ts/email/classes.mail-egress-coordinator.ts +125 -0
  63. package/ts/email/index.ts +2 -0
  64. package/ts/opsserver/handlers/config.handler.ts +3 -1
  65. package/ts/opsserver/handlers/email-domain.handler.ts +2 -0
  66. package/ts/opsserver/handlers/email-ops.handler.ts +156 -18
  67. package/ts/opsserver/handlers/email-settings.handler.ts +10 -5
  68. package/ts/opsserver/handlers/remoteingress.handler.ts +15 -5
  69. package/ts/remoteingress/classes.remoteingress-manager.ts +111 -0
  70. package/ts/remoteingress/classes.tunnel-manager.ts +125 -10
  71. package/ts/remoteingress/edge-derivation-routes.ts +29 -0
  72. package/ts/remoteingress/index.ts +1 -0
  73. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { logger } from '../logger.js';
3
3
  import type { IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../../ts_interfaces/data/remoteingress.js';
4
+ import type { IEmailOutboundEgressStatus } from '../../ts_interfaces/data/email-settings.js';
4
5
  import type { RemoteIngressManager } from './classes.remoteingress-manager.js';
5
6
 
6
7
  export interface ITunnelManagerConfig {
@@ -15,8 +16,13 @@ export interface ITunnelManagerConfig {
15
16
 
16
17
  export type TRemoteIngressEgressProxyRequest = Omit<plugins.remoteingress.IEgressTcpProxyRequest, 'edgeId'> & {
17
18
  edgeId?: string;
19
+ /** Restrict selection to edges whose id or tags match any entry. */
20
+ edgeFilter?: string[];
18
21
  };
19
22
 
23
+ /** Cooldown applied to an edge after a failed egress open before it is deprioritized. */
24
+ const egressFailureCooldownMs = 30_000;
25
+
20
26
  /**
21
27
  * Manages the RemoteIngressHub instance and tracks connected edge statuses.
22
28
  */
@@ -29,6 +35,9 @@ export class TunnelManager {
29
35
  private syncChain: Promise<void> = Promise.resolve();
30
36
  private reconcileChain: Promise<void> = Promise.resolve();
31
37
  private stopped = true;
38
+ private egressProxyEdgeByProxyId: Map<string, string> = new Map();
39
+ private activeEgressProxiesByEdge: Map<string, number> = new Map();
40
+ private egressFailureCooldownUntil: Map<string, number> = new Map();
32
41
 
33
42
  constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
34
43
  this.manager = manager;
@@ -80,6 +89,10 @@ export class TunnelManager {
80
89
  const message = `RemoteIngress egress ${data.outcome}: edge=${data.edgeId} proxy=${data.proxyId} target=${data.logicalHost}:${data.port}${data.resolvedIp ? ` resolvedIp=${data.resolvedIp}` : ''}${data.error ? ` error=${data.error}` : ''}`;
81
90
  const isExpectedLifecycleEvent = data.outcome === 'connected' || data.outcome === 'closed';
82
91
  logger.log(isExpectedLifecycleEvent ? 'info' : 'warn', message);
92
+ if (!isExpectedLifecycleEvent) {
93
+ // Connection-level failures steer subsequent selections to other edges.
94
+ this.egressFailureCooldownUntil.set(data.edgeId, Date.now() + egressFailureCooldownMs);
95
+ }
83
96
  });
84
97
  }
85
98
 
@@ -137,6 +150,9 @@ export class TunnelManager {
137
150
  this.hub.removeAllListeners();
138
151
  await this.hub.stop();
139
152
  this.edgeStatuses.clear();
153
+ this.egressProxyEdgeByProxyId.clear();
154
+ this.activeEgressProxiesByEdge.clear();
155
+ this.egressFailureCooldownUntil.clear();
140
156
  }
141
157
 
142
158
  /**
@@ -212,31 +228,130 @@ export class TunnelManager {
212
228
  throw new Error('RemoteIngress hub is not running');
213
229
  }
214
230
  const connectedEdges = Array.isArray(status?.connectedEdges) ? status.connectedEdges : [];
215
- const eligibleEdges = connectedEdges.filter((edge: any) => {
231
+ const candidates = this.selectEgressCandidates(connectedEdges, request);
232
+
233
+ if (candidates.length === 0) {
234
+ throw new Error(`No eligible RemoteIngress QUIC egress edge is connected${this.describeEgressScope(request)}`);
235
+ }
236
+
237
+ const { edgeId: _ignored, edgeFilter: _alsoIgnored, ...proxyRequest } = request;
238
+ let lastError: Error | undefined;
239
+ for (const candidate of candidates) {
240
+ try {
241
+ const proxy = await this.hub.startEgressTcpProxy({
242
+ ...proxyRequest,
243
+ edgeId: candidate.edgeId,
244
+ });
245
+ this.egressProxyEdgeByProxyId.set(proxy.proxyId, candidate.edgeId);
246
+ this.activeEgressProxiesByEdge.set(
247
+ candidate.edgeId,
248
+ (this.activeEgressProxiesByEdge.get(candidate.edgeId) || 0) + 1,
249
+ );
250
+ return proxy;
251
+ } catch (error: unknown) {
252
+ lastError = error as Error;
253
+ this.egressFailureCooldownUntil.set(candidate.edgeId, Date.now() + egressFailureCooldownMs);
254
+ logger.log('warn', `RemoteIngress egress open failed on edge ${candidate.edgeId}, trying next candidate: ${lastError.message}`);
255
+ }
256
+ }
257
+
258
+ throw new Error(
259
+ `All eligible RemoteIngress egress edges failed${this.describeEgressScope(request)}: ${lastError?.message || 'unknown error'}`,
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Eligibility (connected + egressEnabled + egressTcpV1 + native QUIC), scoped by
265
+ * edgeId/edgeFilter, capped by per-edge maxConcurrentStreams, ordered deterministically:
266
+ * not-in-cooldown first, then fewest active tunnels, then edgeId.
267
+ */
268
+ private selectEgressCandidates(
269
+ connectedEdges: any[],
270
+ request: TRemoteIngressEgressProxyRequest,
271
+ ): Array<{ edgeId: string; activeStreams: number }> {
272
+ const eligible = connectedEdges.filter((edge: any) => {
216
273
  const hasEgressCapability = Array.isArray(edge.capabilities) && edge.capabilities.includes('egressTcpV1');
217
274
  const usesQuic = edge.transportMode === 'quic'
218
275
  || (edge.transportMode === 'quicWithFallback' && edge.fallbackUsed === false);
219
276
  return edge.egressEnabled === true && hasEgressCapability && usesQuic;
220
277
  });
221
- const selectedEdge = request.edgeId
222
- ? eligibleEdges.find((edge: any) => edge.edgeId === request.edgeId)
223
- : eligibleEdges[0];
224
278
 
225
- if (!selectedEdge) {
226
- const edgeHint = request.edgeId ? ` for edge ${request.edgeId}` : '';
227
- throw new Error(`No eligible RemoteIngress QUIC egress edge is connected${edgeHint}`);
279
+ let scoped = eligible;
280
+ if (request.edgeId) {
281
+ scoped = eligible.filter((edge: any) => edge.edgeId === request.edgeId);
282
+ } else if (request.edgeFilter && request.edgeFilter.length > 0) {
283
+ const allowedIds = new Set(
284
+ this.manager.resolveEdgesByFilter(request.edgeFilter).map((edge) => edge.id),
285
+ );
286
+ scoped = eligible.filter((edge: any) => allowedIds.has(edge.edgeId));
228
287
  }
229
288
 
230
- return await this.hub.startEgressTcpProxy({
231
- ...request,
232
- edgeId: selectedEdge.edgeId,
289
+ const underCap = scoped.filter((edge: any) => {
290
+ const cap = this.manager.getEdge(edge.edgeId)?.egress?.maxConcurrentStreams;
291
+ if (!cap) return true;
292
+ return (this.activeEgressProxiesByEdge.get(edge.edgeId) || 0) < cap;
233
293
  });
294
+
295
+ const now = Date.now();
296
+ const inCooldown = (edgeId: string) => (this.egressFailureCooldownUntil.get(edgeId) || 0) > now;
297
+ return underCap
298
+ .map((edge: any) => ({ edgeId: edge.edgeId as string, activeStreams: Number(edge.activeStreams) || 0 }))
299
+ .sort((a, b) => {
300
+ const cooldownDelta = Number(inCooldown(a.edgeId)) - Number(inCooldown(b.edgeId));
301
+ if (cooldownDelta !== 0) return cooldownDelta;
302
+ if (a.activeStreams !== b.activeStreams) return a.activeStreams - b.activeStreams;
303
+ return a.edgeId.localeCompare(b.edgeId);
304
+ });
305
+ }
306
+
307
+ private describeEgressScope(request: TRemoteIngressEgressProxyRequest): string {
308
+ if (request.edgeId) return ` (pinned edge: ${request.edgeId})`;
309
+ if (request.edgeFilter && request.edgeFilter.length > 0) return ` (filter: ${request.edgeFilter.join(', ')})`;
310
+ return '';
311
+ }
312
+
313
+ /**
314
+ * Readiness of the outbound egress path for a given edge scope —
315
+ * consumed by the ops API and the email-setup preflight.
316
+ */
317
+ public getEgressReadiness(edgeFilter: string[]): IEmailOutboundEgressStatus {
318
+ const allowedIds = new Set(
319
+ this.manager.resolveEdgesByFilter(edgeFilter).map((edge) => edge.id),
320
+ );
321
+ let eligibleEdgeCount = 0;
322
+ for (const status of this.edgeStatuses.values()) {
323
+ if (!status.connected) continue;
324
+ if (!allowedIds.has(status.edgeId)) continue;
325
+ const hasEgressCapability = status.capabilities?.includes('egressTcpV1') ?? false;
326
+ const usesQuic = status.transportMode === 'quic'
327
+ || (status.transportMode === 'quicWithFallback' && status.fallbackUsed === false);
328
+ if (status.egressEnabled === true && hasEgressCapability && usesQuic) {
329
+ eligibleEdgeCount++;
330
+ }
331
+ }
332
+ if (eligibleEdgeCount > 0) {
333
+ return { ready: true, eligibleEdgeCount };
334
+ }
335
+ const reason = allowedIds.size === 0
336
+ ? `no enabled edge matches filter [${edgeFilter.join(', ')}]`
337
+ : 'no connected matching edge with native QUIC egress capability';
338
+ return { ready: false, eligibleEdgeCount: 0, reason };
234
339
  }
235
340
 
236
341
  public async stopEgressTcpProxy(proxyId: string): Promise<boolean> {
237
342
  if (this.stopped) {
238
343
  return false;
239
344
  }
345
+ const edgeId = this.egressProxyEdgeByProxyId.get(proxyId);
346
+ if (edgeId) {
347
+ this.egressProxyEdgeByProxyId.delete(proxyId);
348
+ const current = this.activeEgressProxiesByEdge.get(edgeId) || 0;
349
+ if (current <= 1) {
350
+ this.activeEgressProxiesByEdge.delete(edgeId);
351
+ } else {
352
+ this.activeEgressProxiesByEdge.set(edgeId, current - 1);
353
+ }
354
+ }
240
355
  return await this.hub.stopEgressTcpProxy(proxyId);
241
356
  }
242
357
 
@@ -0,0 +1,29 @@
1
+ import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
2
+ import { REMOTE_INGRESS_DNS_TAG } from '../../ts_interfaces/data/remoteingress.js';
3
+
4
+ /**
5
+ * Synthetic routes that exist ONLY for RemoteIngress edge port derivation and
6
+ * must never reach SmartProxy (smartdns owns host-side 53; a SmartProxy listener
7
+ * on 53 would collide). Forwards public TCP+UDP 53 on dns-tagged edges to the
8
+ * hub's authoritative DNS server.
9
+ */
10
+ export function buildEdgeOnlyDerivationRoutes(options: { dnsNsDomains?: string[] }): IDcRouterRouteConfig[] {
11
+ if (!options.dnsNsDomains?.length) {
12
+ return [];
13
+ }
14
+ return [{
15
+ name: 'dns-53-edge-forwarding',
16
+ match: {
17
+ ports: [53],
18
+ transport: 'all',
19
+ },
20
+ action: {
21
+ type: 'forward',
22
+ targets: [{ host: '127.0.0.1', port: 53 }],
23
+ },
24
+ remoteIngress: {
25
+ enabled: true,
26
+ edgeFilter: [REMOTE_INGRESS_DNS_TAG],
27
+ },
28
+ } as IDcRouterRouteConfig];
29
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './classes.remoteingress-manager.js';
2
2
  export * from './classes.tunnel-manager.js';
3
3
  export * from './classes.hub-lifecycle.js';
4
+ export * from './edge-derivation-routes.js';
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '15.11.0',
6
+ version: '16.0.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }