@serve.zone/dcrouter 13.41.2 → 13.42.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 (31) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +1025 -983
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/config/classes.db-seeder.js +29 -2
  5. package/dist_ts/config/classes.reference-resolver.d.ts +5 -0
  6. package/dist_ts/config/classes.reference-resolver.js +79 -15
  7. package/dist_ts/config/classes.route-config-manager.d.ts +5 -1
  8. package/dist_ts/config/classes.route-config-manager.js +136 -6
  9. package/dist_ts/config/classes.source-policy-compiler.d.ts +35 -0
  10. package/dist_ts/config/classes.source-policy-compiler.js +497 -0
  11. package/dist_ts/config/index.d.ts +1 -0
  12. package/dist_ts/config/index.js +2 -1
  13. package/dist_ts_interfaces/data/route-management.d.ts +39 -0
  14. package/dist_ts_interfaces/data/route-management.js +65 -1
  15. package/dist_ts_migrations/index.js +67 -1
  16. package/dist_ts_web/00_commitinfo_data.js +1 -1
  17. package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
  18. package/dist_ts_web/elements/network/ops-view-routes.js +237 -11
  19. package/dist_ts_web/elements/network/ops-view-sourceprofiles.js +42 -5
  20. package/package.json +3 -3
  21. package/readme.md +74 -0
  22. package/ts/00_commitinfo_data.ts +1 -1
  23. package/ts/config/classes.db-seeder.ts +28 -1
  24. package/ts/config/classes.reference-resolver.ts +94 -14
  25. package/ts/config/classes.route-config-manager.ts +162 -5
  26. package/ts/config/classes.source-policy-compiler.ts +614 -0
  27. package/ts/config/index.ts +1 -0
  28. package/ts/readme.md +1 -1
  29. package/ts_web/00_commitinfo_data.ts +1 -1
  30. package/ts_web/elements/network/ops-view-routes.ts +257 -10
  31. package/ts_web/elements/network/ops-view-sourceprofiles.ts +41 -4
@@ -1,15 +1,20 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { logger } from '../logger.js';
3
3
  import { RouteDoc } from '../db/index.js';
4
+ import { routePathClasses } from '../../ts_interfaces/data/route-management.js';
4
5
  import type {
5
6
  IRoute,
6
7
  IMergedRoute,
7
8
  IRouteWarning,
8
9
  IRouteMetadata,
10
+ IRoutePathPolicyBinding,
11
+ IRouteSourcePolicy,
12
+ IRouteSecurity,
9
13
  } from '../../ts_interfaces/data/route-management.js';
10
14
  import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
11
15
  import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
12
16
  import type { ReferenceResolver } from './classes.reference-resolver.js';
17
+ import { SourcePolicyCompiler } from './classes.source-policy-compiler.js';
13
18
 
14
19
  export type TVpnClientAllowEntry = string | { clientId: string; domains: string[] };
15
20
 
@@ -131,6 +136,10 @@ export class RouteConfigManager {
131
136
  ): Promise<string> {
132
137
  const id = plugins.uuid.v4();
133
138
  const now = Date.now();
139
+ const sourcePolicyPayloadError = SourcePolicyCompiler.validateSourcePolicyPayload(metadata?.sourcePolicy);
140
+ if (sourcePolicyPayloadError) {
141
+ throw new Error(sourcePolicyPayloadError);
142
+ }
134
143
 
135
144
  // Ensure route has a name
136
145
  if (!route.name) {
@@ -144,6 +153,10 @@ export class RouteConfigManager {
144
153
  route = resolved.route;
145
154
  resolvedMetadata = this.normalizeRouteMetadata(resolved.metadata);
146
155
  }
156
+ const sourcePolicyValidationError = this.validateSourcePolicy(resolvedMetadata?.sourcePolicy, route);
157
+ if (sourcePolicyValidationError) {
158
+ throw new Error(sourcePolicyValidationError);
159
+ }
147
160
 
148
161
  const stored: IRoute = {
149
162
  id,
@@ -174,8 +187,15 @@ export class RouteConfigManager {
174
187
  if (!stored) {
175
188
  return { success: false, message: 'Route not found' };
176
189
  }
190
+ const sourcePolicyPayloadError = SourcePolicyCompiler.validateSourcePolicyPayload(patch.metadata?.sourcePolicy);
191
+ if (sourcePolicyPayloadError) {
192
+ return { success: false, message: sourcePolicyPayloadError };
193
+ }
177
194
 
178
195
  const previousSourceProfileRef = stored.metadata?.sourceProfileRef;
196
+ const previousRoute = structuredClone(stored.route);
197
+ const previousMetadata = structuredClone(stored.metadata);
198
+ const previousEnabled = stored.enabled;
179
199
 
180
200
  const isToggleOnlyPatch = patch.enabled !== undefined
181
201
  && patch.route === undefined
@@ -234,6 +254,14 @@ export class RouteConfigManager {
234
254
  stored.metadata = this.normalizeRouteMetadata(resolved.metadata);
235
255
  }
236
256
 
257
+ const sourcePolicyValidationError = this.validateSourcePolicy(stored.metadata?.sourcePolicy, stored.route);
258
+ if (sourcePolicyValidationError) {
259
+ stored.route = previousRoute;
260
+ stored.metadata = previousMetadata;
261
+ stored.enabled = previousEnabled;
262
+ return { success: false, message: sourcePolicyValidationError };
263
+ }
264
+
237
265
  stored.updatedAt = Date.now();
238
266
 
239
267
  await this.persistRoute(stored);
@@ -454,6 +482,7 @@ export class RouteConfigManager {
454
482
 
455
483
  const normalized: IRouteMetadata = {
456
484
  sourceProfileRef: normalizeString(metadata.sourceProfileRef),
485
+ sourcePolicy: this.normalizeSourcePolicy(metadata.sourcePolicy),
457
486
  networkTargetRef: normalizeString(metadata.networkTargetRef),
458
487
  sourceProfileName: normalizeString(metadata.sourceProfileName),
459
488
  networkTargetName: normalizeString(metadata.networkTargetName),
@@ -482,7 +511,7 @@ export class RouteConfigManager {
482
511
  if (!normalized.networkTargetRef) {
483
512
  normalized.networkTargetName = undefined;
484
513
  }
485
- if (!normalized.sourceProfileRef && !normalized.networkTargetRef) {
514
+ if (!normalized.sourceProfileRef && !normalized.sourcePolicy && !normalized.networkTargetRef) {
486
515
  normalized.lastResolvedAt = undefined;
487
516
  }
488
517
  if (normalized.ownerType !== 'gatewayClient' && normalized.ownerType !== 'workhoster') {
@@ -507,6 +536,128 @@ export class RouteConfigManager {
507
536
  return normalized;
508
537
  }
509
538
 
539
+ private normalizeSourcePolicy(sourcePolicy?: Partial<IRouteSourcePolicy>): IRouteSourcePolicy | undefined {
540
+ const bindings = sourcePolicy?.bindings;
541
+ if (!Array.isArray(bindings)) {
542
+ return undefined;
543
+ }
544
+
545
+ const normalizedBindings: IRouteSourcePolicy['bindings'] = [];
546
+ for (const binding of bindings) {
547
+ const sourceProfileRef = typeof binding.sourceProfileRef === 'string'
548
+ ? binding.sourceProfileRef.trim()
549
+ : '';
550
+ if (!sourceProfileRef) {
551
+ continue;
552
+ }
553
+ const normalizedRateLimit = this.normalizeRateLimit(binding.rateLimit);
554
+ const normalizedPathPolicies = this.normalizePathPolicies(binding.pathPolicies);
555
+
556
+ normalizedBindings.push({
557
+ ...(typeof binding.id === 'string' && binding.id.trim() ? { id: binding.id.trim() } : {}),
558
+ sourceProfileRef,
559
+ ...(typeof binding.sourceProfileName === 'string' && binding.sourceProfileName.trim()
560
+ ? { sourceProfileName: binding.sourceProfileName.trim() }
561
+ : {}),
562
+ ...(normalizedRateLimit ? { rateLimit: normalizedRateLimit } : {}),
563
+ ...(typeof binding.maxConnections === 'number' && Number.isFinite(binding.maxConnections) && binding.maxConnections >= 0
564
+ ? { maxConnections: binding.maxConnections }
565
+ : {}),
566
+ ...(binding.onExceeded?.type === '429'
567
+ ? {
568
+ onExceeded: {
569
+ type: '429' as const,
570
+ ...(typeof binding.onExceeded.errorMessage === 'string' && binding.onExceeded.errorMessage.trim()
571
+ ? { errorMessage: binding.onExceeded.errorMessage.trim() }
572
+ : {}),
573
+ },
574
+ }
575
+ : {}),
576
+ ...(normalizedPathPolicies ? { pathPolicies: normalizedPathPolicies } : {}),
577
+ });
578
+ }
579
+
580
+ return normalizedBindings.length > 0 ? { bindings: normalizedBindings } : undefined;
581
+ }
582
+
583
+ private normalizePathPolicies(
584
+ pathPolicies?: IRoutePathPolicyBinding[],
585
+ ): IRoutePathPolicyBinding[] | undefined {
586
+ if (!Array.isArray(pathPolicies)) {
587
+ return undefined;
588
+ }
589
+
590
+ const validClasses = new Set<string>(routePathClasses);
591
+ const normalizedPathPolicies: IRoutePathPolicyBinding[] = [];
592
+ for (const pathPolicy of pathPolicies) {
593
+ if (!validClasses.has(pathPolicy.pathClass)) {
594
+ continue;
595
+ }
596
+
597
+ const normalizedRateLimit = this.normalizeRateLimit(pathPolicy.rateLimit);
598
+ const pathPatterns = Array.isArray(pathPolicy.pathPatterns)
599
+ ? [...new Set(pathPolicy.pathPatterns
600
+ .map((pattern) => typeof pattern === 'string' ? pattern.trim() : '')
601
+ .filter(Boolean))]
602
+ : undefined;
603
+
604
+ normalizedPathPolicies.push({
605
+ ...(typeof pathPolicy.id === 'string' && pathPolicy.id.trim() ? { id: pathPolicy.id.trim() } : {}),
606
+ pathClass: pathPolicy.pathClass,
607
+ ...(pathPatterns?.length ? { pathPatterns } : {}),
608
+ ...(normalizedRateLimit ? { rateLimit: normalizedRateLimit } : {}),
609
+ ...(typeof pathPolicy.maxConnections === 'number' && Number.isFinite(pathPolicy.maxConnections) && pathPolicy.maxConnections >= 0
610
+ ? { maxConnections: pathPolicy.maxConnections }
611
+ : {}),
612
+ ...(pathPolicy.onExceeded?.type === '429'
613
+ ? {
614
+ onExceeded: {
615
+ type: '429' as const,
616
+ ...(typeof pathPolicy.onExceeded.errorMessage === 'string' && pathPolicy.onExceeded.errorMessage.trim()
617
+ ? { errorMessage: pathPolicy.onExceeded.errorMessage.trim() }
618
+ : {}),
619
+ },
620
+ }
621
+ : {}),
622
+ });
623
+ }
624
+
625
+ return normalizedPathPolicies.length > 0 ? normalizedPathPolicies : undefined;
626
+ }
627
+
628
+ private validateSourcePolicy(
629
+ sourcePolicy: IRouteSourcePolicy | undefined,
630
+ route: IDcRouterRouteConfig,
631
+ ): string | undefined {
632
+ const shapeError = SourcePolicyCompiler.validateSourcePolicyShape(sourcePolicy, route);
633
+ if (shapeError) {
634
+ return shapeError;
635
+ }
636
+ return SourcePolicyCompiler.validateResolvedSourcePolicy(sourcePolicy, this.referenceResolver);
637
+ }
638
+
639
+ private normalizeRateLimit(rateLimit?: IRouteSecurity['rateLimit']): IRouteSecurity['rateLimit'] | undefined {
640
+ if (!rateLimit || typeof rateLimit !== 'object') {
641
+ return undefined;
642
+ }
643
+
644
+ const maxRequests = Number(rateLimit.maxRequests);
645
+ const window = Number(rateLimit.window);
646
+ if (!Number.isFinite(maxRequests) || maxRequests < 0 || !Number.isFinite(window) || window < 0) {
647
+ return undefined;
648
+ }
649
+
650
+ return {
651
+ enabled: rateLimit.enabled !== false,
652
+ maxRequests,
653
+ window,
654
+ keyBy: 'ip',
655
+ ...(typeof rateLimit.errorMessage === 'string' && rateLimit.errorMessage.trim()
656
+ ? { errorMessage: rateLimit.errorMessage.trim() }
657
+ : {}),
658
+ };
659
+ }
660
+
510
661
  // =========================================================================
511
662
  // Private: warnings
512
663
  // =========================================================================
@@ -569,10 +720,10 @@ export class RouteConfigManager {
569
720
 
570
721
  const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
571
722
 
572
- // Add all enabled routes with HTTP/3 and VPN augmentation
723
+ // Add all enabled routes with HTTP/3, VPN, and source-policy augmentation
573
724
  for (const route of this.routes.values()) {
574
725
  if (route.enabled) {
575
- enabledRoutes.push(this.prepareStoredRouteForApply(route));
726
+ enabledRoutes.push(...this.prepareStoredRoutesForApply(route));
576
727
  }
577
728
  }
578
729
 
@@ -592,9 +743,15 @@ export class RouteConfigManager {
592
743
  });
593
744
  }
594
745
 
595
- private prepareStoredRouteForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig {
746
+ private prepareStoredRoutesForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig[] {
596
747
  const hydratedRoute = this.hydrateStoredRoute?.(storedRoute);
597
- return this.prepareRouteForApply(hydratedRoute || storedRoute.route, storedRoute.id);
748
+ const sourcePolicyRoutes = SourcePolicyCompiler.compileRoute(
749
+ hydratedRoute || storedRoute.route,
750
+ storedRoute.metadata,
751
+ this.referenceResolver,
752
+ storedRoute.id,
753
+ );
754
+ return sourcePolicyRoutes.map((route) => this.prepareRouteForApply(route, storedRoute.id));
598
755
  }
599
756
 
600
757
  private prepareRouteForApply(