@serve.zone/dcrouter 13.19.0 → 13.20.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.
@@ -15,16 +15,90 @@ import {
15
15
 
16
16
  // TLS dropdown options shared by create and edit dialogs
17
17
  const tlsModeOptions = [
18
- { key: 'none', option: '(none — no TLS)' },
19
- { key: 'passthrough', option: 'Passthrough' },
20
- { key: 'terminate', option: 'Terminate' },
21
- { key: 'terminate-and-reencrypt', option: 'Terminate & Re-encrypt' },
18
+ { key: 'none', option: '(none — plain TCP/HTTP, use for SSH)' },
19
+ { key: 'passthrough', option: 'Passthrough (TLS only)' },
20
+ { key: 'terminate', option: 'Terminate TLS' },
21
+ { key: 'terminate-and-reencrypt', option: 'Terminate & Re-encrypt TLS' },
22
22
  ];
23
23
  const tlsCertOptions = [
24
24
  { key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' },
25
25
  { key: 'custom', option: 'Custom certificate' },
26
26
  ];
27
27
 
28
+ function getDropdownKey(value: any): string {
29
+ return typeof value === 'string' ? value : value?.key || '';
30
+ }
31
+
32
+ function parseTargetPort(value: any): number | undefined {
33
+ const parsed = typeof value === 'number'
34
+ ? value
35
+ : typeof value === 'string'
36
+ ? parseInt(value.trim(), 10)
37
+ : Number.NaN;
38
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
39
+ return undefined;
40
+ }
41
+ return parsed;
42
+ }
43
+
44
+ function getRouteTargetInputs(formEl: any) {
45
+ const textInputs = Array.from(formEl.querySelectorAll('dees-input-text')) as any[];
46
+ const checkboxInputs = Array.from(formEl.querySelectorAll('dees-input-checkbox')) as any[];
47
+ return {
48
+ hostInput: textInputs.find((input) => input.key === 'targetHost'),
49
+ portInput: textInputs.find((input) => input.key === 'targetPort'),
50
+ preservePortInput: checkboxInputs.find((input) => input.key === 'preserveMatchPort'),
51
+ };
52
+ }
53
+
54
+ function setupTargetInputState(formEl: any) {
55
+ const updateState = async () => {
56
+ const data = await formEl.collectFormData();
57
+ const contentEl = formEl.closest('.content') || formEl.parentElement;
58
+ const usesNetworkTarget = !!getDropdownKey(data.networkTargetRef);
59
+ const preserveMatchPort = !usesNetworkTarget && Boolean(data.preserveMatchPort);
60
+ const { hostInput, portInput, preservePortInput } = getRouteTargetInputs(formEl);
61
+ const hostDescription = usesNetworkTarget
62
+ ? 'Controlled by the selected network target'
63
+ : 'Used when no network target is selected';
64
+ const portDescription = usesNetworkTarget
65
+ ? 'Controlled by the selected network target'
66
+ : preserveMatchPort
67
+ ? 'Forwarded to the backend on the same port the client matched'
68
+ : 'Used when no network target is selected';
69
+
70
+ if (hostInput) {
71
+ hostInput.disabled = usesNetworkTarget;
72
+ hostInput.required = !usesNetworkTarget;
73
+ hostInput.description = hostDescription;
74
+ }
75
+ if (portInput) {
76
+ portInput.disabled = usesNetworkTarget || preserveMatchPort;
77
+ portInput.required = !usesNetworkTarget && !preserveMatchPort;
78
+ portInput.description = portDescription;
79
+ }
80
+ if (preservePortInput) {
81
+ preservePortInput.disabled = usesNetworkTarget;
82
+ preservePortInput.description = usesNetworkTarget
83
+ ? 'Unavailable when a network target is selected'
84
+ : 'Forward to the backend using the same port that matched this route';
85
+ if (usesNetworkTarget) {
86
+ preservePortInput.value = false;
87
+ }
88
+ }
89
+
90
+ const remoteIngressGroup = contentEl?.querySelector('.remoteIngressGroup') as HTMLElement | null;
91
+ if (remoteIngressGroup) {
92
+ remoteIngressGroup.style.display = Boolean(data.remoteIngressEnabled) ? 'flex' : 'none';
93
+ }
94
+
95
+ await formEl.updateRequiredStatus?.();
96
+ };
97
+
98
+ formEl.changeSubject.subscribe(() => updateState());
99
+ updateState();
100
+ }
101
+
28
102
  /**
29
103
  * Toggle TLS form field visibility based on selected TLS mode and certificate type.
30
104
  */
@@ -411,10 +485,13 @@ export class OpsViewRoutes extends DeesElement {
411
485
  ? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
412
486
  : [];
413
487
  const firstTarget = route.action.targets?.[0];
488
+ const currentPreserveMatchPort = firstTarget?.port === 'preserve';
414
489
  const currentTargetHost = firstTarget
415
490
  ? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
416
491
  : '';
417
- const currentTargetPort = firstTarget?.port != null ? String(firstTarget.port) : '';
492
+ const currentTargetPort = typeof firstTarget?.port === 'number' ? String(firstTarget.port) : '';
493
+ const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
494
+ const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
418
495
 
419
496
  // Compute current TLS state for pre-population
420
497
  const currentTls = (route.action as any).tls;
@@ -439,6 +516,11 @@ export class OpsViewRoutes extends DeesElement {
439
516
  <dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown>
440
517
  <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
441
518
  <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
519
+ <dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${currentPreserveMatchPort}></dees-input-checkbox>
520
+ <dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${currentRemoteIngressEnabled}></dees-input-checkbox>
521
+ <div class="remoteIngressGroup" style="display: ${currentRemoteIngressEnabled ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
522
+ <dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'} .value=${currentEdgeFilter}></dees-input-list>
523
+ </div>
442
524
  <dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions.find((o) => o.key === currentTlsMode) || tlsModeOptions[0]}></dees-input-dropdown>
443
525
  <div class="tlsCertificateGroup" style="display: ${needsCert ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
444
526
  <dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions.find((o) => o.key === currentTlsCert) || tlsCertOptions[0]}></dees-input-dropdown>
@@ -470,6 +552,24 @@ export class OpsViewRoutes extends DeesElement {
470
552
  : [];
471
553
  const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
472
554
 
555
+ const profileKey = getDropdownKey(formData.sourceProfileRef);
556
+ const targetKey = getDropdownKey(formData.networkTargetRef);
557
+ const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
558
+ const targetPort = preserveMatchPort
559
+ ? 'preserve'
560
+ : parseTargetPort(formData.targetPort)
561
+ ?? (targetKey ? parseTargetPort(currentTargetPort) ?? ports[0] : undefined);
562
+
563
+ if (targetPort === undefined) {
564
+ alert('Target Port must be a valid port number when no network target is selected.');
565
+ return;
566
+ }
567
+
568
+ const remoteIngressEnabled = Boolean(formData.remoteIngressEnabled);
569
+ const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
570
+ ? formData.remoteIngressEdgeFilter.filter(Boolean)
571
+ : [];
572
+
473
573
  const updatedRoute: any = {
474
574
  name: formData.name,
475
575
  match: {
@@ -480,11 +580,17 @@ export class OpsViewRoutes extends DeesElement {
480
580
  type: 'forward',
481
581
  targets: [
482
582
  {
483
- host: formData.targetHost || 'localhost',
484
- port: parseInt(formData.targetPort, 10) || 443,
583
+ host: formData.targetHost || currentTargetHost || 'localhost',
584
+ port: targetPort,
485
585
  },
486
586
  ],
487
587
  },
588
+ remoteIngress: remoteIngressEnabled
589
+ ? {
590
+ enabled: true,
591
+ ...(remoteIngressEdgeFilter.length > 0 ? { edgeFilter: remoteIngressEdgeFilter } : {}),
592
+ }
593
+ : null,
488
594
  ...(priority != null && !isNaN(priority) ? { priority } : {}),
489
595
  };
490
596
 
@@ -508,15 +614,17 @@ export class OpsViewRoutes extends DeesElement {
508
614
  }
509
615
 
510
616
  const metadata: any = {};
511
- const profileRefValue = formData.sourceProfileRef as any;
512
- const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
513
617
  if (profileKey) {
514
618
  metadata.sourceProfileRef = profileKey;
619
+ } else if (merged.metadata?.sourceProfileRef) {
620
+ metadata.sourceProfileRef = '';
621
+ metadata.sourceProfileName = '';
515
622
  }
516
- const targetRefValue = formData.networkTargetRef as any;
517
- const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
518
623
  if (targetKey) {
519
624
  metadata.networkTargetRef = targetKey;
625
+ } else if (merged.metadata?.networkTargetRef) {
626
+ metadata.networkTargetRef = '';
627
+ metadata.networkTargetName = '';
520
628
  }
521
629
 
522
630
  await appstate.routeManagementStatePart.dispatchAction(
@@ -537,6 +645,7 @@ export class OpsViewRoutes extends DeesElement {
537
645
  if (editForm) {
538
646
  await editForm.updateComplete;
539
647
  setupTlsVisibility(editForm);
648
+ setupTargetInputState(editForm);
540
649
  }
541
650
  }
542
651
 
@@ -573,6 +682,11 @@ export class OpsViewRoutes extends DeesElement {
573
682
  <dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown>
574
683
  <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
575
684
  <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
685
+ <dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${false}></dees-input-checkbox>
686
+ <dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${false}></dees-input-checkbox>
687
+ <div class="remoteIngressGroup" style="display: none; flex-direction: column; gap: 16px;">
688
+ <dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'}></dees-input-list>
689
+ </div>
576
690
  <dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions[0]}></dees-input-dropdown>
577
691
  <div class="tlsCertificateGroup" style="display: none; flex-direction: column; gap: 16px;">
578
692
  <dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions[0]}></dees-input-dropdown>
@@ -604,6 +718,24 @@ export class OpsViewRoutes extends DeesElement {
604
718
  : [];
605
719
  const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
606
720
 
721
+ const profileKey = getDropdownKey(formData.sourceProfileRef);
722
+ const targetKey = getDropdownKey(formData.networkTargetRef);
723
+ const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
724
+ const targetPort = preserveMatchPort
725
+ ? 'preserve'
726
+ : parseTargetPort(formData.targetPort)
727
+ ?? (targetKey ? ports[0] : undefined);
728
+
729
+ if (targetPort === undefined) {
730
+ alert('Target Port must be a valid port number when no network target is selected.');
731
+ return;
732
+ }
733
+
734
+ const remoteIngressEnabled = Boolean(formData.remoteIngressEnabled);
735
+ const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
736
+ ? formData.remoteIngressEdgeFilter.filter(Boolean)
737
+ : [];
738
+
607
739
  const route: any = {
608
740
  name: formData.name,
609
741
  match: {
@@ -615,10 +747,18 @@ export class OpsViewRoutes extends DeesElement {
615
747
  targets: [
616
748
  {
617
749
  host: formData.targetHost || 'localhost',
618
- port: parseInt(formData.targetPort, 10) || 443,
750
+ port: targetPort,
619
751
  },
620
752
  ],
621
753
  },
754
+ ...(remoteIngressEnabled
755
+ ? {
756
+ remoteIngress: {
757
+ enabled: true,
758
+ ...(remoteIngressEdgeFilter.length > 0 ? { edgeFilter: remoteIngressEdgeFilter } : {}),
759
+ },
760
+ }
761
+ : {}),
622
762
  ...(priority != null && !isNaN(priority) ? { priority } : {}),
623
763
  };
624
764
 
@@ -641,13 +781,9 @@ export class OpsViewRoutes extends DeesElement {
641
781
 
642
782
  // Build metadata if profile/target selected
643
783
  const metadata: any = {};
644
- const profileRefValue = formData.sourceProfileRef as any;
645
- const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
646
784
  if (profileKey) {
647
785
  metadata.sourceProfileRef = profileKey;
648
786
  }
649
- const targetRefValue = formData.networkTargetRef as any;
650
- const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
651
787
  if (targetKey) {
652
788
  metadata.networkTargetRef = targetKey;
653
789
  }
@@ -669,6 +805,7 @@ export class OpsViewRoutes extends DeesElement {
669
805
  if (createForm) {
670
806
  await createForm.updateComplete;
671
807
  setupTlsVisibility(createForm);
808
+ setupTargetInputState(createForm);
672
809
  }
673
810
  }
674
811