@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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "13.43.1",
4
+ "version": "13.43.3",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -50,7 +50,7 @@
50
50
  "@push.rocks/smartnetwork": "^4.7.2",
51
51
  "@push.rocks/smartpath": "^6.0.0",
52
52
  "@push.rocks/smartpromise": "^4.2.4",
53
- "@push.rocks/smartproxy": "^27.12.4",
53
+ "@push.rocks/smartproxy": "^27.12.6",
54
54
  "@push.rocks/smartradius": "^1.3.0",
55
55
  "@push.rocks/smartrequest": "^5.0.3",
56
56
  "@push.rocks/smartrx": "^3.0.10",
package/readme.md CHANGED
@@ -146,19 +146,20 @@ dcrouter keeps generated and operator-created routes separate so automation can
146
146
 
147
147
  System routes are persisted with stable `systemKey` values. API-created routes are the editable route layer intended for operators and automation.
148
148
 
149
- ## Route Source Policies
149
+ ## Route Source Bindings
150
150
 
151
- API-created route records pass `metadata.sourcePolicy` alongside the SmartProxy route config to express ordered source and path policy variants without duplicating whole routes by hand. A source policy contains ordered `bindings`, each pointing at a source profile id through `sourceProfileRef`. Dashboard presets resolve seeded profile names to ids before saving.
151
+ API-created route records pass ordered `metadata.sourceBindings[]` alongside the SmartProxy route config to express source and path policy variants without duplicating whole routes by hand. Each binding points at a source profile id through `sourceProfileRef`. Dashboard presets resolve seeded profile names to ids before saving.
152
152
 
153
153
  Runtime behavior:
154
154
 
155
155
  - Source matching uses the referenced `SourceProfile.security.ipAllowList`.
156
156
  - Bindings are evaluated in order and the first matching source profile wins.
157
157
  - A matched binding that exceeds its configured rate or connection limit is terminal and returns `429`; dcrouter does not fall through to later bindings.
158
- - Source-policy rate limits are always keyed by source IP; dcrouter ignores `path` and `header` keying on source-policy binding and path-policy overrides.
159
- - A public fallback binding must be last and must use `*`, or both `0.0.0.0/0` and `::/0`, in `security.ipAllowList`.
160
- - Create/update paths reject source policies with missing source profiles, source profiles without source matches, missing final all-source fallback, or any all-source binding that shadows later bindings; persisted invalid policies fail closed at compile time.
161
- - Server-side caps bound policy expansion to 16 source bindings, 12 path policies per binding, 64 path patterns per path policy, 256 characters and 8 wildcards per custom path pattern, and 512 compiled SmartProxy route-port variants per stored route.
158
+ - Source-binding rate limits are always keyed by source IP; dcrouter ignores `path` and `header` keying on source-binding and path-policy overrides.
159
+ - Private-only binding lists are valid. dcrouter adds a same-match terminal deny fallback so unmatched sources fail closed.
160
+ - A public or wildcard binding is optional. When present, it must be last and must use `*`, or both `0.0.0.0/0` and `::/0`, in `security.ipAllowList`.
161
+ - Create/update paths reject source bindings with missing source profiles, source profiles without source matches, or any all-source binding that shadows later bindings; persisted invalid bindings fail closed at compile time.
162
+ - Server-side caps bound policy expansion to 16 source bindings, 12 path policies per binding, 64 path patterns per path policy, 256 characters and 8 wildcards per custom path pattern, 512 compiled SmartProxy route-port variants per stored route, and enough priority headroom above the stored route priority for generated source-binding variants.
162
163
 
163
164
  Path policies let a source binding override rate limits or connection limits for specific path classes. dcrouter currently ships Gitea-oriented classes: `git-smart-http`, `static`, `normal-html`, `expensive-html`, `raw`, and `archive`. Path-specific variants win over the same binding's fallback; if every path policy is path-specific, dcrouter adds a source-level fallback route for unmatched paths so normal browsing cannot fall through to a later source binding. The Gitea preset keeps `git-smart-http` high-limit and separate from HTML crawling paths so normal `git clone`, `git fetch`, `git push`, and Git LFS traffic are not subject to the lower HTML crawler limits.
164
165
 
@@ -177,45 +178,43 @@ const createRoutePayload = {
177
178
  },
178
179
  },
179
180
  metadata: {
180
- sourcePolicy: {
181
- bindings: [
182
- {
183
- sourceProfileRef: trustedProfileId,
184
- maxConnections: 5000,
185
- onExceeded: { type: '429' },
186
- },
187
- {
188
- sourceProfileRef: publicProfileId,
189
- onExceeded: { type: '429' },
190
- pathPolicies: [
191
- {
192
- pathClass: 'git-smart-http',
193
- rateLimit: { enabled: true, maxRequests: 1200, window: 60, keyBy: 'ip' },
194
- },
195
- {
196
- pathClass: 'static',
197
- rateLimit: { enabled: true, maxRequests: 600, window: 60, keyBy: 'ip' },
198
- },
199
- {
200
- pathClass: 'raw',
201
- rateLimit: { enabled: true, maxRequests: 120, window: 60, keyBy: 'ip' },
202
- },
203
- {
204
- pathClass: 'archive',
205
- rateLimit: { enabled: true, maxRequests: 30, window: 60, keyBy: 'ip' },
206
- },
207
- {
208
- pathClass: 'expensive-html',
209
- rateLimit: { enabled: true, maxRequests: 30, window: 60, keyBy: 'ip' },
210
- },
211
- {
212
- pathClass: 'normal-html',
213
- rateLimit: { enabled: true, maxRequests: 120, window: 60, keyBy: 'ip' },
214
- },
215
- ],
216
- },
217
- ],
218
- },
181
+ sourceBindings: [
182
+ {
183
+ sourceProfileRef: trustedProfileId,
184
+ maxConnections: 5000,
185
+ onExceeded: { type: '429' },
186
+ },
187
+ {
188
+ sourceProfileRef: publicProfileId,
189
+ onExceeded: { type: '429' },
190
+ pathPolicies: [
191
+ {
192
+ pathClass: 'git-smart-http',
193
+ rateLimit: { enabled: true, maxRequests: 1200, window: 60, keyBy: 'ip' },
194
+ },
195
+ {
196
+ pathClass: 'static',
197
+ rateLimit: { enabled: true, maxRequests: 600, window: 60, keyBy: 'ip' },
198
+ },
199
+ {
200
+ pathClass: 'raw',
201
+ rateLimit: { enabled: true, maxRequests: 120, window: 60, keyBy: 'ip' },
202
+ },
203
+ {
204
+ pathClass: 'archive',
205
+ rateLimit: { enabled: true, maxRequests: 30, window: 60, keyBy: 'ip' },
206
+ },
207
+ {
208
+ pathClass: 'expensive-html',
209
+ rateLimit: { enabled: true, maxRequests: 30, window: 60, keyBy: 'ip' },
210
+ },
211
+ {
212
+ pathClass: 'normal-html',
213
+ rateLimit: { enabled: true, maxRequests: 120, window: 60, keyBy: 'ip' },
214
+ },
215
+ ],
216
+ },
217
+ ],
219
218
  },
220
219
  };
221
220
  ```
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.43.1',
6
+ version: '13.43.3',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -7,7 +7,7 @@ import type {
7
7
  IRouteMetadata,
8
8
  IRoute,
9
9
  IRouteSecurity,
10
- IRouteSourcePolicy,
10
+ IRouteSourceBinding,
11
11
  } from '../../ts_interfaces/data/route-management.js';
12
12
 
13
13
  const MAX_INHERITANCE_DEPTH = 5;
@@ -288,8 +288,8 @@ export class ReferenceResolver {
288
288
 
289
289
  /**
290
290
  * Resolve references for a single route.
291
- * Materializes source profile and/or network target into the route's fields.
292
- * When a source profile is selected, it owns the route security fully.
291
+ * Resolves source binding display names and/or network target references.
292
+ * Source profile security is resolved at apply time by SourcePolicyCompiler.
293
293
  * Returns the resolved route and updated metadata.
294
294
  */
295
295
  public resolveRoute(
@@ -298,27 +298,12 @@ export class ReferenceResolver {
298
298
  ): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } {
299
299
  const resolvedMetadata: IRouteMetadata = { ...metadata };
300
300
 
301
- if (resolvedMetadata.sourcePolicy?.bindings.length) {
302
- const resolvedSourcePolicy = this.resolveRouteSourcePolicy(resolvedMetadata.sourcePolicy);
303
- if (resolvedSourcePolicy) {
304
- resolvedMetadata.sourcePolicy = resolvedSourcePolicy;
305
- resolvedMetadata.sourceProfileRef = undefined;
306
- resolvedMetadata.sourceProfileName = undefined;
301
+ if (resolvedMetadata.sourceBindings?.length) {
302
+ const resolvedSourceBindings = this.resolveRouteSourceBindings(resolvedMetadata.sourceBindings);
303
+ if (resolvedSourceBindings) {
304
+ resolvedMetadata.sourceBindings = resolvedSourceBindings;
307
305
  resolvedMetadata.lastResolvedAt = Date.now();
308
306
  }
309
- } else if (resolvedMetadata.sourceProfileRef) {
310
- const resolvedSecurity = this.resolveSourceProfile(resolvedMetadata.sourceProfileRef);
311
- if (resolvedSecurity) {
312
- const profile = this.profiles.get(resolvedMetadata.sourceProfileRef);
313
- route = {
314
- ...route,
315
- security: this.cloneSecurityFields(resolvedSecurity),
316
- };
317
- resolvedMetadata.sourceProfileName = profile?.name;
318
- resolvedMetadata.lastResolvedAt = Date.now();
319
- } else {
320
- logger.log('warn', `Source profile '${resolvedMetadata.sourceProfileRef}' not found during resolution`);
321
- }
322
307
  }
323
308
 
324
309
  if (resolvedMetadata.networkTargetRef) {
@@ -387,12 +372,12 @@ export class ReferenceResolver {
387
372
  // Private: source profile resolution with inheritance
388
373
  // =========================================================================
389
374
 
390
- private resolveRouteSourcePolicy(sourcePolicy: IRouteSourcePolicy): IRouteSourcePolicy | undefined {
391
- const bindings = sourcePolicy.bindings
375
+ private resolveRouteSourceBindings(sourceBindings: IRouteSourceBinding[]): IRouteSourceBinding[] | undefined {
376
+ const bindings = sourceBindings
392
377
  .map((binding) => {
393
378
  const profile = this.profiles.get(binding.sourceProfileRef);
394
379
  if (!profile) {
395
- logger.log('warn', `Source profile '${binding.sourceProfileRef}' not found during source policy resolution`);
380
+ logger.log('warn', `Source profile '${binding.sourceProfileRef}' not found during source binding resolution`);
396
381
  return binding;
397
382
  }
398
383
  return {
@@ -402,7 +387,7 @@ export class ReferenceResolver {
402
387
  })
403
388
  .filter((binding) => binding.sourceProfileRef);
404
389
 
405
- return bindings.length > 0 ? { bindings } : undefined;
390
+ return bindings.length > 0 ? bindings : undefined;
406
391
  }
407
392
 
408
393
  private metadataUsesSourceProfile(metadata: IRouteMetadata | undefined, profileId: string): boolean {
@@ -411,10 +396,7 @@ export class ReferenceResolver {
411
396
 
412
397
  private getSourceProfileRefsFromMetadata(metadata: IRouteMetadata | undefined): string[] {
413
398
  const refs = new Set<string>();
414
- if (metadata?.sourceProfileRef) {
415
- refs.add(metadata.sourceProfileRef);
416
- }
417
- for (const binding of metadata?.sourcePolicy?.bindings || []) {
399
+ for (const binding of metadata?.sourceBindings || []) {
418
400
  if (binding.sourceProfileRef) {
419
401
  refs.add(binding.sourceProfileRef);
420
402
  }
@@ -623,22 +605,16 @@ export class ReferenceResolver {
623
605
  }
624
606
 
625
607
  private clearSourceProfileFromMetadata(metadata: IRouteMetadata, profileId: string): IRouteMetadata {
626
- const sourcePolicy = metadata.sourcePolicy?.bindings?.length
627
- ? {
628
- bindings: metadata.sourcePolicy.bindings.filter(
629
- (binding) => binding.sourceProfileRef !== profileId,
630
- ),
631
- }
608
+ const sourceBindings = metadata.sourceBindings?.length
609
+ ? metadata.sourceBindings.filter((binding) => binding.sourceProfileRef !== profileId)
632
610
  : undefined;
633
611
 
634
612
  const nextMetadata: IRouteMetadata = {
635
613
  ...metadata,
636
- sourceProfileRef: metadata.sourceProfileRef === profileId ? undefined : metadata.sourceProfileRef,
637
- sourceProfileName: metadata.sourceProfileRef === profileId ? undefined : metadata.sourceProfileName,
638
- sourcePolicy: sourcePolicy?.bindings.length ? sourcePolicy : undefined,
614
+ sourceBindings: sourceBindings?.length ? sourceBindings : undefined,
639
615
  };
640
616
 
641
- if (!nextMetadata.sourceProfileRef && !nextMetadata.sourcePolicy && !nextMetadata.networkTargetRef) {
617
+ if (!nextMetadata.sourceBindings && !nextMetadata.networkTargetRef) {
642
618
  nextMetadata.lastResolvedAt = undefined;
643
619
  }
644
620
 
@@ -9,7 +9,7 @@ import type {
9
9
  IRouteWarning,
10
10
  IRouteMetadata,
11
11
  IRoutePathPolicyBinding,
12
- IRouteSourcePolicy,
12
+ IRouteSourceBinding,
13
13
  IRouteSecurity,
14
14
  } from '../../ts_interfaces/data/route-management.js';
15
15
  import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
@@ -142,9 +142,9 @@ export class RouteConfigManager {
142
142
  ): Promise<string> {
143
143
  const id = plugins.uuid.v4();
144
144
  const now = Date.now();
145
- const sourcePolicyPayloadError = SourcePolicyCompiler.validateSourcePolicyPayload(metadata?.sourcePolicy);
146
- if (sourcePolicyPayloadError) {
147
- throw new Error(sourcePolicyPayloadError);
145
+ const sourceBindingsPayloadError = SourcePolicyCompiler.validateSourceBindingsPayload(metadata?.sourceBindings);
146
+ if (sourceBindingsPayloadError) {
147
+ throw new Error(sourceBindingsPayloadError);
148
148
  }
149
149
 
150
150
  // Ensure route has a name
@@ -159,9 +159,9 @@ export class RouteConfigManager {
159
159
  route = resolved.route;
160
160
  resolvedMetadata = this.normalizeRouteMetadata(resolved.metadata);
161
161
  }
162
- const sourcePolicyValidationError = this.validateSourcePolicy(resolvedMetadata?.sourcePolicy, route);
163
- if (sourcePolicyValidationError) {
164
- throw new Error(sourcePolicyValidationError);
162
+ const sourceBindingsValidationError = this.validateSourceBindings(resolvedMetadata?.sourceBindings, route);
163
+ if (sourceBindingsValidationError) {
164
+ throw new Error(sourceBindingsValidationError);
165
165
  }
166
166
 
167
167
  const stored: IRoute = {
@@ -193,12 +193,11 @@ export class RouteConfigManager {
193
193
  if (!stored) {
194
194
  return { success: false, message: 'Route not found' };
195
195
  }
196
- const sourcePolicyPayloadError = SourcePolicyCompiler.validateSourcePolicyPayload(patch.metadata?.sourcePolicy);
197
- if (sourcePolicyPayloadError) {
198
- return { success: false, message: sourcePolicyPayloadError };
196
+ const sourceBindingsPayloadError = SourcePolicyCompiler.validateSourceBindingsPayload(patch.metadata?.sourceBindings);
197
+ if (sourceBindingsPayloadError) {
198
+ return { success: false, message: sourceBindingsPayloadError };
199
199
  }
200
200
 
201
- const previousSourceProfileRef = stored.metadata?.sourceProfileRef;
202
201
  const previousRoute = structuredClone(stored.route);
203
202
  const previousMetadata = structuredClone(stored.metadata);
204
203
  const previousEnabled = stored.enabled;
@@ -244,13 +243,6 @@ export class RouteConfigManager {
244
243
  ...stored.metadata,
245
244
  ...patch.metadata,
246
245
  });
247
- if (
248
- previousSourceProfileRef
249
- && !stored.metadata?.sourceProfileRef
250
- && !patch.route?.security
251
- ) {
252
- delete stored.route.security;
253
- }
254
246
  }
255
247
 
256
248
  // Re-resolve if metadata refs exist and resolver is available
@@ -260,12 +252,12 @@ export class RouteConfigManager {
260
252
  stored.metadata = this.normalizeRouteMetadata(resolved.metadata);
261
253
  }
262
254
 
263
- const sourcePolicyValidationError = this.validateSourcePolicy(stored.metadata?.sourcePolicy, stored.route);
264
- if (sourcePolicyValidationError) {
255
+ const sourceBindingsValidationError = this.validateSourceBindings(stored.metadata?.sourceBindings, stored.route);
256
+ if (sourceBindingsValidationError) {
265
257
  stored.route = previousRoute;
266
258
  stored.metadata = previousMetadata;
267
259
  stored.enabled = previousEnabled;
268
- return { success: false, message: sourcePolicyValidationError };
260
+ return { success: false, message: sourceBindingsValidationError };
269
261
  }
270
262
 
271
263
  stored.updatedAt = Date.now();
@@ -487,10 +479,8 @@ export class RouteConfigManager {
487
479
  };
488
480
 
489
481
  const normalized: IRouteMetadata = {
490
- sourceProfileRef: normalizeString(metadata.sourceProfileRef),
491
- sourcePolicy: this.normalizeSourcePolicy(metadata.sourcePolicy),
482
+ sourceBindings: this.normalizeSourceBindings(metadata.sourceBindings),
492
483
  networkTargetRef: normalizeString(metadata.networkTargetRef),
493
- sourceProfileName: normalizeString(metadata.sourceProfileName),
494
484
  networkTargetName: normalizeString(metadata.networkTargetName),
495
485
  lastResolvedAt: typeof metadata.lastResolvedAt === 'number' && Number.isFinite(metadata.lastResolvedAt)
496
486
  ? metadata.lastResolvedAt
@@ -511,13 +501,10 @@ export class RouteConfigManager {
511
501
  externalKey: normalizeString(metadata.externalKey),
512
502
  };
513
503
 
514
- if (!normalized.sourceProfileRef) {
515
- normalized.sourceProfileName = undefined;
516
- }
517
504
  if (!normalized.networkTargetRef) {
518
505
  normalized.networkTargetName = undefined;
519
506
  }
520
- if (!normalized.sourceProfileRef && !normalized.sourcePolicy && !normalized.networkTargetRef) {
507
+ if (!normalized.sourceBindings && !normalized.networkTargetRef) {
521
508
  normalized.lastResolvedAt = undefined;
522
509
  }
523
510
  if (normalized.ownerType !== 'gatewayClient' && normalized.ownerType !== 'workhoster') {
@@ -542,14 +529,13 @@ export class RouteConfigManager {
542
529
  return normalized;
543
530
  }
544
531
 
545
- private normalizeSourcePolicy(sourcePolicy?: Partial<IRouteSourcePolicy>): IRouteSourcePolicy | undefined {
546
- const bindings = sourcePolicy?.bindings;
547
- if (!Array.isArray(bindings)) {
532
+ private normalizeSourceBindings(sourceBindings?: Partial<IRouteSourceBinding>[]): IRouteSourceBinding[] | undefined {
533
+ if (!Array.isArray(sourceBindings)) {
548
534
  return undefined;
549
535
  }
550
536
 
551
- const normalizedBindings: IRouteSourcePolicy['bindings'] = [];
552
- for (const binding of bindings) {
537
+ const normalizedBindings: IRouteSourceBinding[] = [];
538
+ for (const binding of sourceBindings) {
553
539
  const sourceProfileRef = typeof binding.sourceProfileRef === 'string'
554
540
  ? binding.sourceProfileRef.trim()
555
541
  : '';
@@ -583,7 +569,7 @@ export class RouteConfigManager {
583
569
  });
584
570
  }
585
571
 
586
- return normalizedBindings.length > 0 ? { bindings: normalizedBindings } : undefined;
572
+ return normalizedBindings.length > 0 ? normalizedBindings : undefined;
587
573
  }
588
574
 
589
575
  private normalizePathPolicies(
@@ -631,15 +617,15 @@ export class RouteConfigManager {
631
617
  return normalizedPathPolicies.length > 0 ? normalizedPathPolicies : undefined;
632
618
  }
633
619
 
634
- private validateSourcePolicy(
635
- sourcePolicy: IRouteSourcePolicy | undefined,
620
+ private validateSourceBindings(
621
+ sourceBindings: IRouteSourceBinding[] | undefined,
636
622
  route: IDcRouterRouteConfig,
637
623
  ): string | undefined {
638
- const shapeError = SourcePolicyCompiler.validateSourcePolicyShape(sourcePolicy, route);
624
+ const shapeError = SourcePolicyCompiler.validateSourceBindingsShape(sourceBindings, route);
639
625
  if (shapeError) {
640
626
  return shapeError;
641
627
  }
642
- return SourcePolicyCompiler.validateResolvedSourcePolicy(sourcePolicy, this.referenceResolver);
628
+ return SourcePolicyCompiler.validateResolvedSourceBindings(sourceBindings, this.referenceResolver);
643
629
  }
644
630
 
645
631
  private normalizeRateLimit(rateLimit?: IRouteSecurity['rateLimit']): IRouteSecurity['rateLimit'] | undefined {
@@ -756,14 +742,29 @@ export class RouteConfigManager {
756
742
  }
757
743
 
758
744
  private prepareStoredRoutesForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig[] {
745
+ if (this.isManagedAccessRoute(storedRoute) && !storedRoute.metadata?.sourceBindings?.length) {
746
+ return [];
747
+ }
759
748
  const hydratedRoute = this.hydrateStoredRoute?.(storedRoute);
760
- const sourcePolicyRoutes = SourcePolicyCompiler.compileRoute(
749
+ const sourceBoundRoutes = SourcePolicyCompiler.compileRoute(
761
750
  hydratedRoute || storedRoute.route,
762
751
  storedRoute.metadata,
763
752
  this.referenceResolver,
764
753
  storedRoute.id,
765
754
  );
766
- return sourcePolicyRoutes.map((route) => this.prepareRouteForApply(route, storedRoute.id));
755
+ return sourceBoundRoutes.map((route) => this.prepareRouteForApply(route, storedRoute.id));
756
+ }
757
+
758
+ private isManagedAccessRoute(storedRoute: IRoute): boolean {
759
+ const metadata = storedRoute.metadata;
760
+ if (storedRoute.origin !== 'api' || !metadata) {
761
+ return false;
762
+ }
763
+ return metadata.ownerType === 'gatewayClient'
764
+ || metadata.ownerType === 'workhoster'
765
+ || Boolean(metadata.gatewayClientId)
766
+ || Boolean(metadata.workHosterId)
767
+ || Boolean(metadata.externalKey);
767
768
  }
768
769
 
769
770
  private prepareRouteForApply(