@serve.zone/dcrouter 13.44.1 → 14.0.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.
Files changed (33) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +1882 -1453
  3. package/dist_ts/00_commitinfo_data.js +2 -2
  4. package/dist_ts/acme/manager.acme-config.d.ts +1 -14
  5. package/dist_ts/acme/manager.acme-config.js +4 -65
  6. package/dist_ts/classes.dcrouter.d.ts +7 -2
  7. package/dist_ts/classes.dcrouter.js +105 -27
  8. package/dist_ts/config/classes.api-token-manager.js +3 -3
  9. package/dist_ts/db/documents/classes.acme-config.doc.d.ts +1 -3
  10. package/dist_ts/db/documents/classes.acme-config.doc.js +2 -4
  11. package/dist_ts/dns/manager.dns.d.ts +0 -13
  12. package/dist_ts/dns/manager.dns.js +1 -81
  13. package/dist_ts/opsserver/handlers/certificate.handler.d.ts +0 -9
  14. package/dist_ts/opsserver/handlers/certificate.handler.js +1 -40
  15. package/dist_ts/opsserver/handlers/config.handler.js +11 -12
  16. package/dist_ts/opsserver/handlers/email-settings.handler.js +2 -2
  17. package/dist_ts_interfaces/data/acme-config.d.ts +1 -3
  18. package/dist_ts_interfaces/requests/certificate.d.ts +0 -12
  19. package/dist_ts_migrations/index.js +2 -2
  20. package/dist_ts_web/00_commitinfo_data.js +2 -2
  21. package/dist_ts_web/elements/network/ops-view-routes.js +118 -142
  22. package/package.json +4 -4
  23. package/ts/00_commitinfo_data.ts +1 -1
  24. package/ts/acme/manager.acme-config.ts +3 -77
  25. package/ts/classes.dcrouter.ts +120 -28
  26. package/ts/config/classes.api-token-manager.ts +2 -2
  27. package/ts/db/documents/classes.acme-config.doc.ts +1 -3
  28. package/ts/dns/manager.dns.ts +0 -103
  29. package/ts/opsserver/handlers/certificate.handler.ts +0 -47
  30. package/ts/opsserver/handlers/config.handler.ts +10 -11
  31. package/ts/opsserver/handlers/email-settings.handler.ts +1 -1
  32. package/ts_web/00_commitinfo_data.ts +1 -1
  33. package/ts_web/elements/network/ops-view-routes.ts +124 -146
@@ -2,6 +2,12 @@ import * as appstate from '../../appstate.js';
2
2
  import * as interfaces from '../../../dist_ts_interfaces/index.js';
3
3
  import { viewHostCss } from '../shared/css.js';
4
4
  import { type IStatsTile } from '@design.estate/dees-catalog';
5
+ import type {
6
+ IRoutePathClassOption as ISzRoutePathClassOption,
7
+ IRouteSourcePolicyPreset as ISzRouteSourcePolicyPreset,
8
+ ISourceProfileOption as ISzSourceProfileOption,
9
+ SzInputRouteSourcePolicy,
10
+ } from '@serve.zone/catalog';
5
11
 
6
12
  import {
7
13
  DeesElement,
@@ -24,8 +30,8 @@ const tlsCertOptions = [
24
30
  { key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' },
25
31
  { key: 'custom', option: 'Custom certificate' },
26
32
  ];
27
- const maxSourceBindingRows = 16;
28
33
  const giteaSourcePolicyProfileNames = ['TRUSTED NETWORKS', 'AI CRAWLERS', 'PUBLIC'] as const;
34
+ type TSzRouteSecurity = NonNullable<ISzSourceProfileOption['security']>;
29
35
 
30
36
  function rateLimit(maxRequests: number): interfaces.data.IRouteSecurity['rateLimit'] {
31
37
  return { enabled: true, maxRequests, window: 60, keyBy: 'ip' };
@@ -35,36 +41,6 @@ function getDropdownKey(value: any): string {
35
41
  return typeof value === 'string' ? value : value?.key || '';
36
42
  }
37
43
 
38
- function getSourceBindingRefsFromFormData(formData: Record<string, any>): string[] {
39
- const refs: string[] = [];
40
- for (let index = 0; index < maxSourceBindingRows; index++) {
41
- const ref = getDropdownKey(formData[`sourceBindingProfileRef${index}`]);
42
- if (ref && !refs.includes(ref)) {
43
- refs.push(ref);
44
- }
45
- }
46
- return refs;
47
- }
48
-
49
- function buildSourceBindingsMetadata(
50
- profileRefs: string[],
51
- existingSourceBindings?: interfaces.data.IRouteSourceBinding[],
52
- ): interfaces.data.IRouteSourceBinding[] {
53
- return profileRefs.map((sourceProfileRef) => {
54
- const existingBinding = existingSourceBindings?.find((binding) => binding.sourceProfileRef === sourceProfileRef);
55
- return existingBinding
56
- ? {
57
- ...existingBinding,
58
- sourceProfileRef,
59
- onExceeded: existingBinding.onExceeded || { type: '429' as const },
60
- }
61
- : {
62
- sourceProfileRef,
63
- onExceeded: { type: '429' as const },
64
- };
65
- });
66
- }
67
-
68
44
  function getGiteaPresetProfileRefs(profiles: interfaces.data.ISourceProfile[]): {
69
45
  refs: string[];
70
46
  missingNames: string[];
@@ -116,70 +92,111 @@ function buildGiteaSourceBindingsMetadata(profileRefs: string[]): interfaces.dat
116
92
  ];
117
93
  }
118
94
 
119
- function getGiteaPresetSourceBindings(profiles: interfaces.data.ISourceProfile[]): interfaces.data.IRouteSourceBinding[] | null {
95
+ function getGiteaSourcePolicyPresets(profiles: interfaces.data.ISourceProfile[]): ISzRouteSourcePolicyPreset[] {
120
96
  const { refs, missingNames } = getGiteaPresetProfileRefs(profiles);
121
97
  if (missingNames.length > 0) {
122
- alert(`Gitea source-policy preset needs these seeded profiles: ${missingNames.join(', ')}`);
123
- return null;
124
- }
125
- if (!validateSourceBindingSelection(refs, profiles)) {
126
- return null;
98
+ return [];
127
99
  }
128
- return buildGiteaSourceBindingsMetadata(refs);
100
+ return [
101
+ {
102
+ key: 'gitea-bot-protection',
103
+ label: 'Gitea bot protection',
104
+ description: 'TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC with path-class rate limits.',
105
+ bindings: buildGiteaSourceBindingsMetadata(refs),
106
+ },
107
+ ];
129
108
  }
130
109
 
131
- function metadataUsesPathPolicies(metadata?: interfaces.data.IRouteMetadata): boolean {
132
- return Boolean(metadata?.sourceBindings?.some((binding) => binding.pathPolicies?.length));
110
+ function normalizeSecurityListEntries(entries: unknown): string[] {
111
+ if (!Array.isArray(entries)) {
112
+ return [];
113
+ }
114
+ return entries
115
+ .map((entry) => {
116
+ if (typeof entry === 'string') return entry.trim();
117
+ if (entry && typeof entry === 'object' && 'ip' in entry) {
118
+ const ip = (entry as Record<string, unknown>).ip;
119
+ return typeof ip === 'string' ? ip.trim() : '';
120
+ }
121
+ return '';
122
+ })
123
+ .filter(Boolean);
133
124
  }
134
125
 
135
126
  function sourceProfileMatchesAll(profile: interfaces.data.ISourceProfile): boolean {
136
- return (profile.security?.ipAllowList || []).some((entry) => {
137
- const source = typeof entry === 'string' ? entry : entry.ip;
127
+ return normalizeSecurityListEntries(profile.security?.ipAllowList).some((source) => {
138
128
  return ['*', '0.0.0.0/0', '::/0'].includes(source.trim());
139
129
  });
140
130
  }
141
131
 
142
132
  function sourceProfileHasSourceMatches(profile: interfaces.data.ISourceProfile): boolean {
143
- return (profile.security?.ipAllowList || []).some((entry) => {
144
- const source = typeof entry === 'string' ? entry : entry.ip;
145
- return source.trim().length > 0;
146
- });
133
+ return normalizeSecurityListEntries(profile.security?.ipAllowList).length > 0;
147
134
  }
148
135
 
149
- function validateSourceBindingSelection(
150
- profileRefs: string[],
151
- profiles: interfaces.data.ISourceProfile[],
152
- ): boolean {
153
- if (profileRefs.length === 0) {
154
- return true;
155
- }
156
-
157
- const selectedProfiles = profileRefs
158
- .map((profileRef) => profiles.find((profile) => profile.id === profileRef))
159
- .filter(Boolean) as interfaces.data.ISourceProfile[];
136
+ function normalizeCatalogRateLimit(
137
+ rateLimitValue: interfaces.data.IRouteSecurity['rateLimit'] | undefined,
138
+ ): TSzRouteSecurity['rateLimit'] | undefined {
139
+ if (!rateLimitValue) return undefined;
140
+ return {
141
+ enabled: Boolean(rateLimitValue.enabled),
142
+ maxRequests: Number(rateLimitValue.maxRequests) || 0,
143
+ window: Number(rateLimitValue.window) || 0,
144
+ ...(rateLimitValue.keyBy ? { keyBy: String(rateLimitValue.keyBy) } : {}),
145
+ };
146
+ }
160
147
 
161
- if (selectedProfiles.length !== profileRefs.length) {
162
- alert('One or more selected source profiles could not be found. Refresh profiles and try again.');
163
- return false;
164
- }
148
+ function getSourceProfileOptions(profiles: interfaces.data.ISourceProfile[]): ISzSourceProfileOption[] {
149
+ return profiles.map((profile) => {
150
+ const ipAllowList = normalizeSecurityListEntries(profile.security?.ipAllowList);
151
+ const ipBlockList = normalizeSecurityListEntries(profile.security?.ipBlockList);
152
+ const rateLimitValue = normalizeCatalogRateLimit(profile.security?.rateLimit);
153
+ const security: TSzRouteSecurity = {
154
+ ...(ipAllowList.length ? { ipAllowList } : {}),
155
+ ...(ipBlockList.length ? { ipBlockList } : {}),
156
+ ...(typeof profile.security?.maxConnections === 'number' ? { maxConnections: profile.security.maxConnections } : {}),
157
+ ...(rateLimitValue ? { rateLimit: rateLimitValue } : {}),
158
+ };
159
+ return {
160
+ id: profile.id,
161
+ name: profile.name,
162
+ description: profile.description,
163
+ security,
164
+ hasSourceMatches: sourceProfileHasSourceMatches(profile),
165
+ matchesAllSources: sourceProfileMatchesAll(profile),
166
+ };
167
+ });
168
+ }
165
169
 
166
- const profilesWithoutMatches = selectedProfiles.filter((profile) => !sourceProfileHasSourceMatches(profile));
167
- if (profilesWithoutMatches.length > 0) {
168
- alert(`Source profiles need IP/CIDR match entries before use: ${profilesWithoutMatches.map((profile) => profile.name).join(', ')}`);
169
- return false;
170
- }
170
+ function getRoutePathClassOptions(): ISzRoutePathClassOption[] {
171
+ return interfaces.data.routePathClasses.map((pathClass) => ({
172
+ key: pathClass,
173
+ label: interfaces.data.giteaRoutePathClassLabels[pathClass],
174
+ defaultPatterns: interfaces.data.giteaRoutePathClassPatterns[pathClass],
175
+ }));
176
+ }
171
177
 
172
- if (selectedProfiles.slice(0, -1).some((profile) => sourceProfileMatchesAll(profile))) {
173
- alert('Wildcard source profiles must be last. Earlier wildcard profiles would shadow all following profiles.');
174
- return false;
175
- }
178
+ function getSourcePolicyInfoText(profiles: interfaces.data.ISourceProfile[]): string {
179
+ const { missingNames } = getGiteaPresetProfileRefs(profiles);
180
+ const presetText = missingNames.length > 0
181
+ ? `Gitea preset hidden until these source profiles exist: ${missingNames.join(', ')}.`
182
+ : 'Use the Gitea preset as a starting point, then edit the generated bindings before saving.';
183
+ return `First matching source profile wins. Leave empty for no route-level source access control. ${presetText}`;
184
+ }
176
185
 
177
- const fallbackProfile = selectedProfiles[selectedProfiles.length - 1];
178
- if (sourceProfileMatchesAll(fallbackProfile) && fallbackProfile.security?.rateLimit?.enabled !== true) {
179
- return confirm(`The wildcard profile "${fallbackProfile.name}" has no enabled rate limit. Save anyway?`);
186
+ function validateSourcePolicyInput(form: Element): boolean {
187
+ const sourcePolicyInput = form.querySelector('sz-input-route-source-policy') as SzInputRouteSourcePolicy | null;
188
+ if (!sourcePolicyInput || sourcePolicyInput.isValid()) {
189
+ return true;
180
190
  }
191
+ alert(sourcePolicyInput.getValidationMessages().join('\n'));
192
+ return false;
193
+ }
181
194
 
182
- return true;
195
+ function getSourceBindingsFromFormData(formData: Record<string, unknown>): interfaces.data.IRouteSourceBinding[] {
196
+ const sourceBindings = formData.sourceBindings;
197
+ return Array.isArray(sourceBindings)
198
+ ? sourceBindings as interfaces.data.IRouteSourceBinding[]
199
+ : [];
183
200
  }
184
201
 
185
202
  function parseTargetPort(value: any): number | undefined {
@@ -620,13 +637,6 @@ export class OpsViewRoutes extends DeesElement {
620
637
  const profiles = this.profilesTargetsState.profiles;
621
638
  const targets = this.profilesTargetsState.targets;
622
639
 
623
- const profileOptions = [
624
- { key: '', option: '(none — inline security)' },
625
- ...profiles.map((p) => ({
626
- key: p.id,
627
- option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
628
- })),
629
- ];
630
640
  const targetOptions = [
631
641
  { key: '', option: '(none — inline target)' },
632
642
  ...targets.map((t) => ({
@@ -651,7 +661,10 @@ export class OpsViewRoutes extends DeesElement {
651
661
  const currentVpnOnly = route.vpnOnly === true;
652
662
  const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
653
663
  const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
654
- const currentSourceBindingRefs = this.getSourceBindingRefs(merged.metadata);
664
+ const sourceProfileOptions = getSourceProfileOptions(profiles);
665
+ const pathClassOptions = getRoutePathClassOptions();
666
+ const sourcePolicyPresets = getGiteaSourcePolicyPresets(profiles);
667
+ const sourcePolicyInfoText = getSourcePolicyInfoText(profiles);
655
668
 
656
669
  // Compute current TLS state for pre-population
657
670
  const currentTls = (route.action as any).tls;
@@ -672,24 +685,15 @@ export class OpsViewRoutes extends DeesElement {
672
685
  <dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .value=${currentPorts} .required=${true}></dees-input-text>
673
686
  <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
674
687
  <dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
675
- <div class="sourcePolicyGroup" style="display: flex; flex-direction: column; gap: 12px; padding: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;">
676
- <strong>Source Bindings</strong>
677
- <small>First matching source profile wins. Leave all rows empty to remove route-level source access control.</small>
678
- <dees-input-checkbox
679
- .key=${'useGiteaTemplate'}
680
- .label=${'Apply Gitea bot protection template on save'}
681
- .description=${'Replaces these rows with TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and path-class limits.'}
682
- .value=${false}
683
- ></dees-input-checkbox>
684
- ${Array.from({ length: maxSourceBindingRows }, (_item, index) => html`
685
- <dees-input-dropdown
686
- .key=${`sourceBindingProfileRef${index}`}
687
- .label=${`Binding ${index + 1}`}
688
- .options=${profileOptions}
689
- .selectedOption=${profileOptions.find((o) => o.key === (currentSourceBindingRefs[index] || '')) || profileOptions[0]}
690
- ></dees-input-dropdown>
691
- `)}
692
- </div>
688
+ <sz-input-route-source-policy
689
+ .key=${'sourceBindings'}
690
+ .label=${'Source Policy'}
691
+ .infoText=${sourcePolicyInfoText}
692
+ .sourceProfiles=${sourceProfileOptions}
693
+ .pathClassOptions=${pathClassOptions}
694
+ .presets=${sourcePolicyPresets}
695
+ .value=${merged.metadata?.sourceBindings || []}
696
+ ></sz-input-route-source-policy>
693
697
  <dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown>
694
698
  <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
695
699
  <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
@@ -723,6 +727,7 @@ export class OpsViewRoutes extends DeesElement {
723
727
  if (!form) return;
724
728
  const formData = await form.collectFormData();
725
729
  if (!formData.name || !formData.ports) return;
730
+ if (!validateSourcePolicyInput(form)) return;
726
731
 
727
732
  const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
728
733
  const domains: string[] = Array.isArray(formData.domains)
@@ -730,11 +735,7 @@ export class OpsViewRoutes extends DeesElement {
730
735
  : [];
731
736
  const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
732
737
 
733
- const useGiteaTemplate = Boolean(formData.useGiteaTemplate);
734
- const sourceBindingRefs = useGiteaTemplate
735
- ? []
736
- : getSourceBindingRefsFromFormData(formData);
737
- if (!useGiteaTemplate && !validateSourceBindingSelection(sourceBindingRefs, profiles)) return;
738
+ const sourceBindings = getSourceBindingsFromFormData(formData);
738
739
  const targetKey = getDropdownKey(formData.networkTargetRef);
739
740
  const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
740
741
  const targetPort = preserveMatchPort
@@ -798,12 +799,8 @@ export class OpsViewRoutes extends DeesElement {
798
799
  }
799
800
 
800
801
  const metadata: any = {};
801
- if (useGiteaTemplate) {
802
- const sourceBindings = getGiteaPresetSourceBindings(profiles);
803
- if (!sourceBindings) return;
802
+ if (sourceBindings.length > 0) {
804
803
  metadata.sourceBindings = sourceBindings;
805
- } else if (sourceBindingRefs.length > 0) {
806
- metadata.sourceBindings = buildSourceBindingsMetadata(sourceBindingRefs, merged.metadata?.sourceBindings);
807
804
  } else if (merged.metadata?.sourceBindings) {
808
805
  metadata.sourceBindings = [];
809
806
  }
@@ -841,14 +838,11 @@ export class OpsViewRoutes extends DeesElement {
841
838
  const profiles = this.profilesTargetsState.profiles;
842
839
  const targets = this.profilesTargetsState.targets;
843
840
 
844
- // Build dropdown options for profiles and targets
845
- const profileOptions = [
846
- { key: '', option: '(none — inline security)' },
847
- ...profiles.map((p) => ({
848
- key: p.id,
849
- option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
850
- })),
851
- ];
841
+ // Build dropdown options for targets and source policy metadata
842
+ const sourceProfileOptions = getSourceProfileOptions(profiles);
843
+ const pathClassOptions = getRoutePathClassOptions();
844
+ const sourcePolicyPresets = getGiteaSourcePolicyPresets(profiles);
845
+ const sourcePolicyInfoText = getSourcePolicyInfoText(profiles);
852
846
  const targetOptions = [
853
847
  { key: '', option: '(none — inline target)' },
854
848
  ...targets.map((t) => ({
@@ -865,24 +859,15 @@ export class OpsViewRoutes extends DeesElement {
865
859
  <dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .required=${true}></dees-input-text>
866
860
  <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
867
861
  <dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'}></dees-input-text>
868
- <div class="sourcePolicyGroup" style="display: flex; flex-direction: column; gap: 12px; padding: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;">
869
- <strong>Source Bindings</strong>
870
- <small>First matching source profile wins. Leave all rows empty for no route-level source access control.</small>
871
- <dees-input-checkbox
872
- .key=${'useGiteaTemplate'}
873
- .label=${'Apply Gitea bot protection template on save'}
874
- .description=${'Writes TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and path-class limits.'}
875
- .value=${false}
876
- ></dees-input-checkbox>
877
- ${Array.from({ length: maxSourceBindingRows }, (_item, index) => html`
878
- <dees-input-dropdown
879
- .key=${`sourceBindingProfileRef${index}`}
880
- .label=${`Binding ${index + 1}`}
881
- .options=${profileOptions}
882
- .selectedOption=${profileOptions[0]}
883
- ></dees-input-dropdown>
884
- `)}
885
- </div>
862
+ <sz-input-route-source-policy
863
+ .key=${'sourceBindings'}
864
+ .label=${'Source Policy'}
865
+ .infoText=${sourcePolicyInfoText}
866
+ .sourceProfiles=${sourceProfileOptions}
867
+ .pathClassOptions=${pathClassOptions}
868
+ .presets=${sourcePolicyPresets}
869
+ .value=${[]}
870
+ ></sz-input-route-source-policy>
886
871
  <dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown>
887
872
  <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
888
873
  <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
@@ -916,6 +901,7 @@ export class OpsViewRoutes extends DeesElement {
916
901
  if (!form) return;
917
902
  const formData = await form.collectFormData();
918
903
  if (!formData.name || !formData.ports) return;
904
+ if (!validateSourcePolicyInput(form)) return;
919
905
 
920
906
  const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
921
907
  const domains: string[] = Array.isArray(formData.domains)
@@ -923,11 +909,7 @@ export class OpsViewRoutes extends DeesElement {
923
909
  : [];
924
910
  const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
925
911
 
926
- const useGiteaTemplate = Boolean(formData.useGiteaTemplate);
927
- const sourceBindingRefs = useGiteaTemplate
928
- ? []
929
- : getSourceBindingRefsFromFormData(formData);
930
- if (!useGiteaTemplate && !validateSourceBindingSelection(sourceBindingRefs, profiles)) return;
912
+ const sourceBindings = getSourceBindingsFromFormData(formData);
931
913
  const targetKey = getDropdownKey(formData.networkTargetRef);
932
914
  const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
933
915
  const targetPort = preserveMatchPort
@@ -992,12 +974,8 @@ export class OpsViewRoutes extends DeesElement {
992
974
 
993
975
  // Build metadata if profile/target selected
994
976
  const metadata: any = {};
995
- if (useGiteaTemplate) {
996
- const sourceBindings = getGiteaPresetSourceBindings(profiles);
997
- if (!sourceBindings) return;
977
+ if (sourceBindings.length > 0) {
998
978
  metadata.sourceBindings = sourceBindings;
999
- } else if (sourceBindingRefs.length > 0) {
1000
- metadata.sourceBindings = buildSourceBindingsMetadata(sourceBindingRefs);
1001
979
  }
1002
980
  if (targetKey) {
1003
981
  metadata.networkTargetRef = targetKey;