@serve.zone/dcrouter 13.43.1 → 13.43.3

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.
@@ -24,10 +24,7 @@ const tlsCertOptions = [
24
24
  { key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' },
25
25
  { key: 'custom', option: 'Custom certificate' },
26
26
  ];
27
- const sourcePolicyPresetOptions = [
28
- { key: 'manual', option: 'Manual source policy' },
29
- { key: 'gitea', option: 'Gitea bot protection' },
30
- ];
27
+ const maxSourceBindingRows = 16;
31
28
  const giteaSourcePolicyProfileNames = ['TRUSTED NETWORKS', 'AI CRAWLERS', 'PUBLIC'] as const;
32
29
 
33
30
  function rateLimit(maxRequests: number): interfaces.data.IRouteSecurity['rateLimit'] {
@@ -38,10 +35,10 @@ function getDropdownKey(value: any): string {
38
35
  return typeof value === 'string' ? value : value?.key || '';
39
36
  }
40
37
 
41
- function getSourcePolicyRefsFromFormData(formData: Record<string, any>): string[] {
38
+ function getSourceBindingRefsFromFormData(formData: Record<string, any>): string[] {
42
39
  const refs: string[] = [];
43
- for (let index = 0; index < 4; index++) {
44
- const ref = getDropdownKey(formData[`sourcePolicyProfileRef${index}`]);
40
+ for (let index = 0; index < maxSourceBindingRows; index++) {
41
+ const ref = getDropdownKey(formData[`sourceBindingProfileRef${index}`]);
45
42
  if (ref && !refs.includes(ref)) {
46
43
  refs.push(ref);
47
44
  }
@@ -49,25 +46,23 @@ function getSourcePolicyRefsFromFormData(formData: Record<string, any>): string[
49
46
  return refs;
50
47
  }
51
48
 
52
- function buildSourcePolicyMetadata(
49
+ function buildSourceBindingsMetadata(
53
50
  profileRefs: string[],
54
- existingSourcePolicy?: interfaces.data.IRouteSourcePolicy,
55
- ): interfaces.data.IRouteSourcePolicy {
56
- return {
57
- bindings: profileRefs.map((sourceProfileRef) => {
58
- const existingBinding = existingSourcePolicy?.bindings.find((binding) => binding.sourceProfileRef === sourceProfileRef);
59
- return existingBinding
60
- ? {
61
- ...existingBinding,
62
- sourceProfileRef,
63
- onExceeded: existingBinding.onExceeded || { type: '429' as const },
64
- }
65
- : {
66
- sourceProfileRef,
67
- onExceeded: { type: '429' as const },
68
- };
69
- }),
70
- };
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
+ });
71
66
  }
72
67
 
73
68
  function getGiteaPresetProfileRefs(profiles: interfaces.data.ISourceProfile[]): {
@@ -87,56 +82,54 @@ function getGiteaPresetProfileRefs(profiles: interfaces.data.ISourceProfile[]):
87
82
  return { refs, missingNames };
88
83
  }
89
84
 
90
- function buildGiteaSourcePolicyMetadata(profileRefs: string[]): interfaces.data.IRouteSourcePolicy {
85
+ function buildGiteaSourceBindingsMetadata(profileRefs: string[]): interfaces.data.IRouteSourceBinding[] {
91
86
  const [trustedRef, aiRef, publicRef] = profileRefs;
92
- return {
93
- bindings: [
94
- {
95
- sourceProfileRef: trustedRef,
96
- onExceeded: { type: '429' as const },
97
- },
98
- {
99
- sourceProfileRef: aiRef,
100
- onExceeded: { type: '429' as const },
101
- pathPolicies: [
102
- { pathClass: 'git-smart-http', rateLimit: rateLimit(1200) },
103
- { pathClass: 'static', rateLimit: rateLimit(240) },
104
- { pathClass: 'raw', rateLimit: rateLimit(20) },
105
- { pathClass: 'archive', rateLimit: rateLimit(6) },
106
- { pathClass: 'expensive-html', rateLimit: rateLimit(6) },
107
- { pathClass: 'normal-html', rateLimit: rateLimit(20) },
108
- ],
109
- },
110
- {
111
- sourceProfileRef: publicRef,
112
- onExceeded: { type: '429' as const },
113
- pathPolicies: [
114
- { pathClass: 'git-smart-http', rateLimit: rateLimit(1200) },
115
- { pathClass: 'static', rateLimit: rateLimit(600) },
116
- { pathClass: 'raw', rateLimit: rateLimit(120) },
117
- { pathClass: 'archive', rateLimit: rateLimit(30) },
118
- { pathClass: 'expensive-html', rateLimit: rateLimit(30) },
119
- { pathClass: 'normal-html', rateLimit: rateLimit(120) },
120
- ],
121
- },
122
- ],
123
- };
87
+ return [
88
+ {
89
+ sourceProfileRef: trustedRef,
90
+ onExceeded: { type: '429' as const },
91
+ },
92
+ {
93
+ sourceProfileRef: aiRef,
94
+ onExceeded: { type: '429' as const },
95
+ pathPolicies: [
96
+ { pathClass: 'git-smart-http', rateLimit: rateLimit(1200) },
97
+ { pathClass: 'static', rateLimit: rateLimit(240) },
98
+ { pathClass: 'raw', rateLimit: rateLimit(20) },
99
+ { pathClass: 'archive', rateLimit: rateLimit(6) },
100
+ { pathClass: 'expensive-html', rateLimit: rateLimit(6) },
101
+ { pathClass: 'normal-html', rateLimit: rateLimit(20) },
102
+ ],
103
+ },
104
+ {
105
+ sourceProfileRef: publicRef,
106
+ onExceeded: { type: '429' as const },
107
+ pathPolicies: [
108
+ { pathClass: 'git-smart-http', rateLimit: rateLimit(1200) },
109
+ { pathClass: 'static', rateLimit: rateLimit(600) },
110
+ { pathClass: 'raw', rateLimit: rateLimit(120) },
111
+ { pathClass: 'archive', rateLimit: rateLimit(30) },
112
+ { pathClass: 'expensive-html', rateLimit: rateLimit(30) },
113
+ { pathClass: 'normal-html', rateLimit: rateLimit(120) },
114
+ ],
115
+ },
116
+ ];
124
117
  }
125
118
 
126
- function getGiteaPresetSourcePolicy(profiles: interfaces.data.ISourceProfile[]): interfaces.data.IRouteSourcePolicy | null {
119
+ function getGiteaPresetSourceBindings(profiles: interfaces.data.ISourceProfile[]): interfaces.data.IRouteSourceBinding[] | null {
127
120
  const { refs, missingNames } = getGiteaPresetProfileRefs(profiles);
128
121
  if (missingNames.length > 0) {
129
122
  alert(`Gitea source-policy preset needs these seeded profiles: ${missingNames.join(', ')}`);
130
123
  return null;
131
124
  }
132
- if (!validateSourcePolicySelection(refs, profiles)) {
125
+ if (!validateSourceBindingSelection(refs, profiles)) {
133
126
  return null;
134
127
  }
135
- return buildGiteaSourcePolicyMetadata(refs);
128
+ return buildGiteaSourceBindingsMetadata(refs);
136
129
  }
137
130
 
138
131
  function metadataUsesPathPolicies(metadata?: interfaces.data.IRouteMetadata): boolean {
139
- return Boolean(metadata?.sourcePolicy?.bindings.some((binding) => binding.pathPolicies?.length));
132
+ return Boolean(metadata?.sourceBindings?.some((binding) => binding.pathPolicies?.length));
140
133
  }
141
134
 
142
135
  function sourceProfileMatchesAll(profile: interfaces.data.ISourceProfile): boolean {
@@ -153,7 +146,7 @@ function sourceProfileHasSourceMatches(profile: interfaces.data.ISourceProfile):
153
146
  });
154
147
  }
155
148
 
156
- function validateSourcePolicySelection(
149
+ function validateSourceBindingSelection(
157
150
  profileRefs: string[],
158
151
  profiles: interfaces.data.ISourceProfile[],
159
152
  ): boolean {
@@ -176,19 +169,14 @@ function validateSourcePolicySelection(
176
169
  return false;
177
170
  }
178
171
 
179
- const fallbackProfile = selectedProfiles[selectedProfiles.length - 1];
180
- if (!sourceProfileMatchesAll(fallbackProfile)) {
181
- alert('Source policy needs an explicit public/wildcard fallback profile as the last binding. Add a profile with IP Allow List "*".');
182
- return false;
183
- }
184
-
185
172
  if (selectedProfiles.slice(0, -1).some((profile) => sourceProfileMatchesAll(profile))) {
186
173
  alert('Wildcard source profiles must be last. Earlier wildcard profiles would shadow all following profiles.');
187
174
  return false;
188
175
  }
189
176
 
190
- if (fallbackProfile.security?.rateLimit?.enabled !== true) {
191
- return confirm(`The fallback profile "${fallbackProfile.name}" has no enabled rate limit. Save anyway?`);
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?`);
192
180
  }
193
181
 
194
182
  return true;
@@ -521,7 +509,7 @@ export class OpsViewRoutes extends DeesElement {
521
509
 
522
510
  const meta = merged.metadata;
523
511
  const isSystemManaged = this.isSystemManagedRoute(merged);
524
- const sourcePolicySummary = this.describeSourcePolicy(meta);
512
+ const sourceBindingSummary = this.describeSourcePolicy(meta);
525
513
  await DeesModal.createAndShow({
526
514
  heading: `Route: ${merged.route.name}`,
527
515
  content: html`
@@ -531,7 +519,7 @@ export class OpsViewRoutes extends DeesElement {
531
519
  ${merged.route.vpnOnly ? html`<p>Access: <strong style="color: #22c55e;">VPN only</strong></p>` : ''}
532
520
  <p>ID: <code style="color: #888;">${merged.id}</code></p>
533
521
  ${isSystemManaged ? html`<p>This route is system-managed. Change its source config to modify it directly.</p>` : ''}
534
- ${sourcePolicySummary ? html`<p>Source Policy: <strong style="color: #a78bfa;">${sourcePolicySummary}</strong></p>` : ''}
522
+ ${sourceBindingSummary ? html`<p>Source Bindings: <strong style="color: #a78bfa;">${sourceBindingSummary}</strong></p>` : ''}
535
523
  ${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
536
524
  </div>
537
525
  `,
@@ -663,8 +651,7 @@ export class OpsViewRoutes extends DeesElement {
663
651
  const currentVpnOnly = route.vpnOnly === true;
664
652
  const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
665
653
  const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
666
- const currentSourcePolicyRefs = this.getSourcePolicyRefs(merged.metadata);
667
- const currentSourcePolicyPreset = metadataUsesPathPolicies(merged.metadata) ? 'gitea' : 'manual';
654
+ const currentSourceBindingRefs = this.getSourceBindingRefs(merged.metadata);
668
655
 
669
656
  // Compute current TLS state for pre-population
670
657
  const currentTls = (route.action as any).tls;
@@ -686,21 +673,20 @@ export class OpsViewRoutes extends DeesElement {
686
673
  <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
687
674
  <dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
688
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;">
689
- <strong>Source Policy</strong>
690
- <small>First matching profile wins. Exceeded limits return 429 and do not fall through.</small>
691
- <dees-input-dropdown
692
- .key=${'sourcePolicyPreset'}
693
- .label=${'Source Policy Preset'}
694
- .options=${sourcePolicyPresetOptions}
695
- .selectedOption=${sourcePolicyPresetOptions.find((o) => o.key === currentSourcePolicyPreset) || sourcePolicyPresetOptions[0]}
696
- ></dees-input-dropdown>
697
- <small>Gitea preset uses TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and applies path-class limits.</small>
698
- ${[0, 1, 2, 3].map((index) => html`
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`
699
685
  <dees-input-dropdown
700
- .key=${`sourcePolicyProfileRef${index}`}
701
- .label=${`Source Profile ${index + 1}`}
686
+ .key=${`sourceBindingProfileRef${index}`}
687
+ .label=${`Binding ${index + 1}`}
702
688
  .options=${profileOptions}
703
- .selectedOption=${profileOptions.find((o) => o.key === (currentSourcePolicyRefs[index] || '')) || profileOptions[0]}
689
+ .selectedOption=${profileOptions.find((o) => o.key === (currentSourceBindingRefs[index] || '')) || profileOptions[0]}
704
690
  ></dees-input-dropdown>
705
691
  `)}
706
692
  </div>
@@ -744,11 +730,11 @@ export class OpsViewRoutes extends DeesElement {
744
730
  : [];
745
731
  const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
746
732
 
747
- const sourcePolicyPreset = getDropdownKey(formData.sourcePolicyPreset) || 'manual';
748
- const sourcePolicyRefs = sourcePolicyPreset === 'gitea'
733
+ const useGiteaTemplate = Boolean(formData.useGiteaTemplate);
734
+ const sourceBindingRefs = useGiteaTemplate
749
735
  ? []
750
- : getSourcePolicyRefsFromFormData(formData);
751
- if (sourcePolicyPreset !== 'gitea' && !validateSourcePolicySelection(sourcePolicyRefs, profiles)) return;
736
+ : getSourceBindingRefsFromFormData(formData);
737
+ if (!useGiteaTemplate && !validateSourceBindingSelection(sourceBindingRefs, profiles)) return;
752
738
  const targetKey = getDropdownKey(formData.networkTargetRef);
753
739
  const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
754
740
  const targetPort = preserveMatchPort
@@ -812,20 +798,14 @@ export class OpsViewRoutes extends DeesElement {
812
798
  }
813
799
 
814
800
  const metadata: any = {};
815
- if (sourcePolicyPreset === 'gitea') {
816
- const sourcePolicy = getGiteaPresetSourcePolicy(profiles);
817
- if (!sourcePolicy) return;
818
- metadata.sourcePolicy = sourcePolicy;
819
- metadata.sourceProfileRef = '';
820
- metadata.sourceProfileName = '';
821
- } else if (sourcePolicyRefs.length > 0) {
822
- metadata.sourcePolicy = buildSourcePolicyMetadata(sourcePolicyRefs, merged.metadata?.sourcePolicy);
823
- metadata.sourceProfileRef = '';
824
- metadata.sourceProfileName = '';
825
- } else if (merged.metadata?.sourcePolicy || merged.metadata?.sourceProfileRef) {
826
- metadata.sourcePolicy = { bindings: [] };
827
- metadata.sourceProfileRef = '';
828
- metadata.sourceProfileName = '';
801
+ if (useGiteaTemplate) {
802
+ const sourceBindings = getGiteaPresetSourceBindings(profiles);
803
+ if (!sourceBindings) return;
804
+ metadata.sourceBindings = sourceBindings;
805
+ } else if (sourceBindingRefs.length > 0) {
806
+ metadata.sourceBindings = buildSourceBindingsMetadata(sourceBindingRefs, merged.metadata?.sourceBindings);
807
+ } else if (merged.metadata?.sourceBindings) {
808
+ metadata.sourceBindings = [];
829
809
  }
830
810
  if (targetKey) {
831
811
  metadata.networkTargetRef = targetKey;
@@ -886,19 +866,18 @@ export class OpsViewRoutes extends DeesElement {
886
866
  <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
887
867
  <dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'}></dees-input-text>
888
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;">
889
- <strong>Source Policy</strong>
890
- <small>First matching profile wins. Exceeded limits return 429 and do not fall through.</small>
891
- <dees-input-dropdown
892
- .key=${'sourcePolicyPreset'}
893
- .label=${'Source Policy Preset'}
894
- .options=${sourcePolicyPresetOptions}
895
- .selectedOption=${sourcePolicyPresetOptions[0]}
896
- ></dees-input-dropdown>
897
- <small>Gitea preset uses TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and applies path-class limits.</small>
898
- ${[0, 1, 2, 3].map((index) => html`
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`
899
878
  <dees-input-dropdown
900
- .key=${`sourcePolicyProfileRef${index}`}
901
- .label=${`Source Profile ${index + 1}`}
879
+ .key=${`sourceBindingProfileRef${index}`}
880
+ .label=${`Binding ${index + 1}`}
902
881
  .options=${profileOptions}
903
882
  .selectedOption=${profileOptions[0]}
904
883
  ></dees-input-dropdown>
@@ -944,11 +923,11 @@ export class OpsViewRoutes extends DeesElement {
944
923
  : [];
945
924
  const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
946
925
 
947
- const sourcePolicyPreset = getDropdownKey(formData.sourcePolicyPreset) || 'manual';
948
- const sourcePolicyRefs = sourcePolicyPreset === 'gitea'
926
+ const useGiteaTemplate = Boolean(formData.useGiteaTemplate);
927
+ const sourceBindingRefs = useGiteaTemplate
949
928
  ? []
950
- : getSourcePolicyRefsFromFormData(formData);
951
- if (sourcePolicyPreset !== 'gitea' && !validateSourcePolicySelection(sourcePolicyRefs, profiles)) return;
929
+ : getSourceBindingRefsFromFormData(formData);
930
+ if (!useGiteaTemplate && !validateSourceBindingSelection(sourceBindingRefs, profiles)) return;
952
931
  const targetKey = getDropdownKey(formData.networkTargetRef);
953
932
  const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
954
933
  const targetPort = preserveMatchPort
@@ -1013,12 +992,12 @@ export class OpsViewRoutes extends DeesElement {
1013
992
 
1014
993
  // Build metadata if profile/target selected
1015
994
  const metadata: any = {};
1016
- if (sourcePolicyPreset === 'gitea') {
1017
- const sourcePolicy = getGiteaPresetSourcePolicy(profiles);
1018
- if (!sourcePolicy) return;
1019
- metadata.sourcePolicy = sourcePolicy;
1020
- } else if (sourcePolicyRefs.length > 0) {
1021
- metadata.sourcePolicy = buildSourcePolicyMetadata(sourcePolicyRefs);
995
+ if (useGiteaTemplate) {
996
+ const sourceBindings = getGiteaPresetSourceBindings(profiles);
997
+ if (!sourceBindings) return;
998
+ metadata.sourceBindings = sourceBindings;
999
+ } else if (sourceBindingRefs.length > 0) {
1000
+ metadata.sourceBindings = buildSourceBindingsMetadata(sourceBindingRefs);
1022
1001
  }
1023
1002
  if (targetKey) {
1024
1003
  metadata.networkTargetRef = targetKey;
@@ -1049,23 +1028,20 @@ export class OpsViewRoutes extends DeesElement {
1049
1028
  appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
1050
1029
  }
1051
1030
 
1052
- private getSourcePolicyRefs(metadata?: interfaces.data.IRouteMetadata): string[] {
1053
- const policyRefs = metadata?.sourcePolicy?.bindings
1031
+ private getSourceBindingRefs(metadata?: interfaces.data.IRouteMetadata): string[] {
1032
+ const bindingRefs = metadata?.sourceBindings
1054
1033
  ?.map((binding) => binding.sourceProfileRef)
1055
1034
  .filter(Boolean) || [];
1056
- if (policyRefs.length > 0) {
1057
- return policyRefs;
1058
- }
1059
- return metadata?.sourceProfileRef ? [metadata.sourceProfileRef] : [];
1035
+ return bindingRefs;
1060
1036
  }
1061
1037
 
1062
1038
  private describeSourcePolicy(metadata?: interfaces.data.IRouteMetadata): string {
1063
- const refs = this.getSourcePolicyRefs(metadata);
1039
+ const refs = this.getSourceBindingRefs(metadata);
1064
1040
  if (refs.length === 0) {
1065
1041
  return '';
1066
1042
  }
1067
1043
  return refs.map((ref) => {
1068
- const binding = metadata?.sourcePolicy?.bindings?.find((item) => item.sourceProfileRef === ref);
1044
+ const binding = metadata?.sourceBindings?.find((item) => item.sourceProfileRef === ref);
1069
1045
  const profile = this.profilesTargetsState.profiles.find((item) => item.id === ref);
1070
1046
  return binding?.sourceProfileName || profile?.name || ref.slice(0, 8);
1071
1047
  }).join(' → ');