@serve.zone/dcrouter 13.41.1 → 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 +3 -3
  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 +4 -4
  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
@@ -0,0 +1,614 @@
1
+ import * as plugins from '../plugins.js';
2
+ import {
3
+ giteaRoutePathClassLabels,
4
+ giteaRoutePathClassPatterns,
5
+ routePathClasses,
6
+ } from '../../ts_interfaces/data/route-management.js';
7
+ import type {
8
+ IRoutePathPolicyBinding,
9
+ IRouteMetadata,
10
+ IRouteSecurity,
11
+ IRouteSourcePolicy,
12
+ IRouteSourcePolicyBinding,
13
+ } from '../../ts_interfaces/data/route-management.js';
14
+ import type { ReferenceResolver } from './classes.reference-resolver.js';
15
+
16
+ const MIN_ROUTE_PRIORITY = 0;
17
+ const MAX_ROUTE_PRIORITY = 10000;
18
+ const SOURCE_PRIORITY_BAND = 0.0008;
19
+ const PATH_PRIORITY_BAND = 0.0001;
20
+
21
+ export const sourcePolicyLimits = {
22
+ maxBindings: 16,
23
+ maxPathPoliciesPerBinding: 12,
24
+ maxPathPatternsPerPolicy: 64,
25
+ maxPathPatternLength: 256,
26
+ maxPathPatternWildcards: 8,
27
+ maxSourceProfileRefLength: 256,
28
+ maxIdLength: 128,
29
+ maxExceededMessageLength: 512,
30
+ maxCompiledVariantsPerRoute: 512,
31
+ } as const;
32
+
33
+ export class SourcePolicyCompiler {
34
+ public static compileRoute(
35
+ route: plugins.smartproxy.IRouteConfig,
36
+ metadata: IRouteMetadata | undefined,
37
+ referenceResolver: ReferenceResolver | undefined,
38
+ routeId?: string,
39
+ ): plugins.smartproxy.IRouteConfig[] {
40
+ const bindings = metadata?.sourcePolicy?.bindings || [];
41
+ if (bindings.length === 0) {
42
+ return [route];
43
+ }
44
+ if (this.validateSourcePolicyShape(metadata?.sourcePolicy, route)) {
45
+ return [];
46
+ }
47
+ if (!referenceResolver) {
48
+ return [];
49
+ }
50
+ if (this.validateResolvedSourcePolicy(metadata?.sourcePolicy, referenceResolver)) {
51
+ return [];
52
+ }
53
+
54
+ const compiledRoutes: plugins.smartproxy.IRouteConfig[] = [];
55
+ const basePriority = route.priority ?? 0;
56
+
57
+ bindings.forEach((binding, index) => {
58
+ const profile = referenceResolver.getProfile(binding.sourceProfileRef);
59
+ const profileSecurity = referenceResolver.resolveSourceProfileSecurity(binding.sourceProfileRef);
60
+ if (!profile || !profileSecurity) {
61
+ return;
62
+ }
63
+
64
+ const sourceMatches = this.getSourceMatchEntries(profileSecurity);
65
+ if (sourceMatches.length === 0) {
66
+ return;
67
+ }
68
+ const sourcePriority = this.calculateSourcePriority(basePriority, index, bindings.length);
69
+ const sourceMatch = this.matchesAllSources(sourceMatches)
70
+ ? { ...route.match }
71
+ : { ...route.match, clientIp: sourceMatches };
72
+ const pathPolicies = binding.pathPolicies || [];
73
+
74
+ if (pathPolicies.length === 0) {
75
+ compiledRoutes.push(this.buildCompiledRoute({
76
+ route,
77
+ sourceMatch,
78
+ profileName: profile.name,
79
+ profileSecurity,
80
+ binding,
81
+ sourcePriority,
82
+ routeId,
83
+ sourceIndex: index,
84
+ }));
85
+ return;
86
+ }
87
+
88
+ let hasSourceFallback = false;
89
+ pathPolicies.forEach((pathPolicy, pathIndex) => {
90
+ const pathPatterns = this.getPathPatterns(pathPolicy);
91
+ if (pathPatterns.length === 0) {
92
+ hasSourceFallback = true;
93
+ compiledRoutes.push(this.buildCompiledRoute({
94
+ route,
95
+ sourceMatch,
96
+ profileName: profile.name,
97
+ profileSecurity,
98
+ binding,
99
+ pathPolicy,
100
+ sourcePriority,
101
+ routeId,
102
+ sourceIndex: index,
103
+ pathIndex,
104
+ pathPolicyCount: pathPolicies.length,
105
+ }));
106
+ return;
107
+ }
108
+
109
+ pathPatterns.forEach((pathPattern, pathPatternIndex) => {
110
+ compiledRoutes.push(this.buildCompiledRoute({
111
+ route,
112
+ sourceMatch,
113
+ profileName: profile.name,
114
+ profileSecurity,
115
+ binding,
116
+ pathPolicy,
117
+ pathPattern,
118
+ sourcePriority,
119
+ routeId,
120
+ sourceIndex: index,
121
+ pathIndex,
122
+ pathPolicyCount: pathPolicies.length,
123
+ pathPatternIndex,
124
+ pathPatternCount: pathPatterns.length,
125
+ }));
126
+ });
127
+ });
128
+
129
+ if (!hasSourceFallback) {
130
+ compiledRoutes.push(this.buildCompiledRoute({
131
+ route,
132
+ sourceMatch,
133
+ profileName: profile.name,
134
+ profileSecurity,
135
+ binding,
136
+ sourcePriority,
137
+ routeId,
138
+ sourceIndex: index,
139
+ }));
140
+ }
141
+ });
142
+
143
+ return compiledRoutes;
144
+ }
145
+
146
+ public static validateSourcePolicyPayload(sourcePolicy?: Partial<IRouteSourcePolicy>): string | undefined {
147
+ if (!sourcePolicy) {
148
+ return undefined;
149
+ }
150
+ if (!Array.isArray(sourcePolicy.bindings)) {
151
+ return 'Source policy bindings must be an array';
152
+ }
153
+ if (sourcePolicy.bindings.length === 0) {
154
+ return undefined;
155
+ }
156
+ if (sourcePolicy.bindings.length > sourcePolicyLimits.maxBindings) {
157
+ return `Source policy exceeds ${sourcePolicyLimits.maxBindings} bindings`;
158
+ }
159
+
160
+ const validClasses = new Set<string>(routePathClasses);
161
+ for (const binding of sourcePolicy.bindings) {
162
+ if (!binding || typeof binding !== 'object') {
163
+ return 'Source policy binding must be an object';
164
+ }
165
+ if (typeof binding.sourceProfileRef !== 'string') {
166
+ return 'Source policy binding requires a source profile';
167
+ }
168
+ if (binding.sourceProfileRef.length > sourcePolicyLimits.maxSourceProfileRefLength) {
169
+ return `Source policy source profile ref exceeds ${sourcePolicyLimits.maxSourceProfileRefLength} characters`;
170
+ }
171
+ if (binding.sourceProfileRef.trim().length === 0) {
172
+ return 'Source policy binding requires a source profile';
173
+ }
174
+ if (typeof binding.id === 'string' && binding.id.length > sourcePolicyLimits.maxIdLength) {
175
+ return `Source policy binding id exceeds ${sourcePolicyLimits.maxIdLength} characters`;
176
+ }
177
+ if (typeof binding.maxConnections === 'number' && binding.maxConnections < 0) {
178
+ return 'Source policy maxConnections must be non-negative';
179
+ }
180
+ const bindingRateLimitError = this.validateRateLimitPayload(binding.rateLimit);
181
+ if (bindingRateLimitError) {
182
+ return bindingRateLimitError;
183
+ }
184
+ const bindingMessage = binding.onExceeded?.errorMessage;
185
+ if (typeof bindingMessage === 'string' && bindingMessage.length > sourcePolicyLimits.maxExceededMessageLength) {
186
+ return `Source policy exceeded message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`;
187
+ }
188
+
189
+ const pathPolicies = binding.pathPolicies;
190
+ if (pathPolicies === undefined) {
191
+ continue;
192
+ }
193
+ if (!Array.isArray(pathPolicies)) {
194
+ return 'Source policy path policies must be an array';
195
+ }
196
+ if (pathPolicies.length > sourcePolicyLimits.maxPathPoliciesPerBinding) {
197
+ return `Source policy binding exceeds ${sourcePolicyLimits.maxPathPoliciesPerBinding} path policies`;
198
+ }
199
+
200
+ for (const pathPolicy of pathPolicies) {
201
+ if (!pathPolicy || typeof pathPolicy !== 'object') {
202
+ return 'Source policy path policy must be an object';
203
+ }
204
+ if (!validClasses.has(pathPolicy.pathClass)) {
205
+ return 'Source policy path policy uses an unsupported path class';
206
+ }
207
+ if (typeof pathPolicy.id === 'string' && pathPolicy.id.length > sourcePolicyLimits.maxIdLength) {
208
+ return `Source policy path policy id exceeds ${sourcePolicyLimits.maxIdLength} characters`;
209
+ }
210
+ if (typeof pathPolicy.maxConnections === 'number' && pathPolicy.maxConnections < 0) {
211
+ return 'Source policy path policy maxConnections must be non-negative';
212
+ }
213
+ const pathRateLimitError = this.validateRateLimitPayload(pathPolicy.rateLimit);
214
+ if (pathRateLimitError) {
215
+ return pathRateLimitError;
216
+ }
217
+ const pathMessage = pathPolicy.onExceeded?.errorMessage;
218
+ if (typeof pathMessage === 'string' && pathMessage.length > sourcePolicyLimits.maxExceededMessageLength) {
219
+ return `Source policy exceeded message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`;
220
+ }
221
+
222
+ const pathPatterns = pathPolicy.pathPatterns;
223
+ if (pathPatterns === undefined) {
224
+ continue;
225
+ }
226
+ if (!Array.isArray(pathPatterns)) {
227
+ return 'Source policy path patterns must be an array';
228
+ }
229
+ if (pathPatterns.length > sourcePolicyLimits.maxPathPatternsPerPolicy) {
230
+ return `Source policy path class exceeds ${sourcePolicyLimits.maxPathPatternsPerPolicy} path patterns`;
231
+ }
232
+ for (const pattern of pathPatterns) {
233
+ if (typeof pattern !== 'string') {
234
+ return 'Source policy path pattern must be a string';
235
+ }
236
+ if (pattern.length > sourcePolicyLimits.maxPathPatternLength) {
237
+ return `Source policy path pattern exceeds ${sourcePolicyLimits.maxPathPatternLength} characters`;
238
+ }
239
+ const wildcardCount = pattern.split('*').length - 1;
240
+ if (wildcardCount > sourcePolicyLimits.maxPathPatternWildcards) {
241
+ return `Source policy path pattern exceeds ${sourcePolicyLimits.maxPathPatternWildcards} wildcards`;
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ return undefined;
248
+ }
249
+
250
+ private static validateRateLimitPayload(rateLimit: IRouteSecurity['rateLimit'] | undefined): string | undefined {
251
+ if (!rateLimit || typeof rateLimit !== 'object') {
252
+ return undefined;
253
+ }
254
+ const rawRateLimit = rateLimit as unknown as Record<string, unknown>;
255
+ for (const key of ['maxRequests', 'window'] as const) {
256
+ const value = rawRateLimit[key];
257
+ if (typeof value === 'string' && value.length > 32) {
258
+ return `Source policy rate limit ${key} exceeds 32 characters`;
259
+ }
260
+ }
261
+ if (
262
+ typeof rateLimit.errorMessage === 'string'
263
+ && rateLimit.errorMessage.length > sourcePolicyLimits.maxExceededMessageLength
264
+ ) {
265
+ return `Source policy rate limit error message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`;
266
+ }
267
+ return undefined;
268
+ }
269
+
270
+ public static validateSourcePolicyShape(
271
+ sourcePolicy?: IRouteSourcePolicy,
272
+ route?: plugins.smartproxy.IRouteConfig,
273
+ ): string | undefined {
274
+ const payloadError = this.validateSourcePolicyPayload(sourcePolicy);
275
+ if (payloadError) {
276
+ return payloadError;
277
+ }
278
+ const bindings = sourcePolicy?.bindings || [];
279
+ if (bindings.length === 0) {
280
+ return undefined;
281
+ }
282
+
283
+ let estimatedCompiledRoutes = 0;
284
+ for (const binding of bindings) {
285
+ const pathPolicies = binding.pathPolicies || [];
286
+
287
+ if (pathPolicies.length === 0) {
288
+ estimatedCompiledRoutes++;
289
+ } else {
290
+ let hasSourceFallback = false;
291
+ for (const pathPolicy of pathPolicies) {
292
+ const pathPatterns = this.getPathPatterns(pathPolicy);
293
+ if (pathPatterns.length > sourcePolicyLimits.maxPathPatternsPerPolicy) {
294
+ return `Source policy path class expands beyond ${sourcePolicyLimits.maxPathPatternsPerPolicy} path patterns`;
295
+ }
296
+ if (pathPatterns.length === 0) {
297
+ hasSourceFallback = true;
298
+ estimatedCompiledRoutes++;
299
+ } else {
300
+ estimatedCompiledRoutes += pathPatterns.length;
301
+ }
302
+ }
303
+ if (!hasSourceFallback) {
304
+ estimatedCompiledRoutes++;
305
+ }
306
+ }
307
+
308
+ if (estimatedCompiledRoutes > sourcePolicyLimits.maxCompiledVariantsPerRoute) {
309
+ return `Source policy exceeds ${sourcePolicyLimits.maxCompiledVariantsPerRoute} compiled route variants`;
310
+ }
311
+ }
312
+
313
+ const expandedPortCount = route ? this.getExpandedPortCount(route.match?.ports) : 1;
314
+ if (estimatedCompiledRoutes * expandedPortCount > sourcePolicyLimits.maxCompiledVariantsPerRoute) {
315
+ return `Source policy exceeds ${sourcePolicyLimits.maxCompiledVariantsPerRoute} compiled route-port variants`;
316
+ }
317
+
318
+ return undefined;
319
+ }
320
+
321
+ public static validateResolvedSourcePolicy(
322
+ sourcePolicy: IRouteSourcePolicy | undefined,
323
+ referenceResolver: ReferenceResolver | undefined,
324
+ ): string | undefined {
325
+ const bindings = sourcePolicy?.bindings || [];
326
+ if (bindings.length === 0) {
327
+ return undefined;
328
+ }
329
+ if (!referenceResolver) {
330
+ return 'Source policy requires source profile resolution';
331
+ }
332
+
333
+ for (let index = 0; index < bindings.length; index++) {
334
+ const binding = bindings[index];
335
+ const profile = referenceResolver.getProfile(binding.sourceProfileRef);
336
+ if (!profile) {
337
+ return `Source profile '${binding.sourceProfileRef}' not found`;
338
+ }
339
+ const profileSecurity = referenceResolver.resolveSourceProfileSecurity(binding.sourceProfileRef);
340
+ if (!profileSecurity) {
341
+ return `Source profile '${profile.name}' could not be resolved`;
342
+ }
343
+ const sourceMatches = this.getSourceMatchEntries(profileSecurity);
344
+ if (sourceMatches.length === 0) {
345
+ return `Source profile '${profile.name}' has no source matches`;
346
+ }
347
+ const matchesAllSources = this.matchesAllSources(sourceMatches);
348
+ if (matchesAllSources && index < bindings.length - 1) {
349
+ return 'Wildcard source profile bindings must be last in a source policy';
350
+ }
351
+ if (index === bindings.length - 1 && !matchesAllSources) {
352
+ return 'Source policy must end with an all-source fallback profile';
353
+ }
354
+ }
355
+
356
+ return undefined;
357
+ }
358
+
359
+ private static buildCompiledRoute(options: {
360
+ route: plugins.smartproxy.IRouteConfig;
361
+ sourceMatch: plugins.smartproxy.IRouteConfig['match'];
362
+ profileName: string;
363
+ profileSecurity: IRouteSecurity;
364
+ binding: IRouteSourcePolicyBinding;
365
+ pathPolicy?: IRoutePathPolicyBinding;
366
+ pathPattern?: string;
367
+ sourcePriority: number;
368
+ routeId?: string;
369
+ sourceIndex: number;
370
+ pathIndex?: number;
371
+ pathPolicyCount?: number;
372
+ pathPatternIndex?: number;
373
+ pathPatternCount?: number;
374
+ }): plugins.smartproxy.IRouteConfig {
375
+ const routeKey = options.route.id || options.routeId || options.route.name || 'route';
376
+ const bindingKey = options.binding.id || options.binding.sourceProfileRef || String(options.sourceIndex + 1);
377
+ const pathPolicyKey = options.pathPolicy
378
+ ? options.pathPolicy.id || options.pathPolicy.pathClass
379
+ : undefined;
380
+ const pathLabel = options.pathPolicy
381
+ ? giteaRoutePathClassLabels[options.pathPolicy.pathClass]
382
+ : undefined;
383
+ const pathPatternSuffix = options.pathPatternCount && options.pathPatternCount > 1
384
+ ? `:${(options.pathPatternIndex || 0) + 1}`
385
+ : '';
386
+ const pathPriority = options.pathPolicy
387
+ ? this.calculatePathPriorityOffset(
388
+ options.pathPattern,
389
+ options.pathIndex || 0,
390
+ options.pathPolicyCount || 1,
391
+ options.pathPatternIndex || 0,
392
+ options.pathPatternCount || 1,
393
+ )
394
+ : 0;
395
+
396
+ return {
397
+ ...options.route,
398
+ id: pathPolicyKey
399
+ ? `${routeKey}:source:${bindingKey}:path:${pathPolicyKey}${pathPatternSuffix}`
400
+ : `${routeKey}:source:${bindingKey}`,
401
+ name: pathLabel
402
+ ? `${options.route.name || routeKey}:source:${options.profileName}:path:${pathLabel}${pathPatternSuffix}`
403
+ : `${options.route.name || routeKey}:source:${options.profileName}`,
404
+ match: options.pathPattern
405
+ ? { ...options.sourceMatch, path: options.pathPattern }
406
+ : { ...options.sourceMatch },
407
+ priority: this.clampPriority(options.sourcePriority + pathPriority),
408
+ security: this.buildBindingSecurity(
409
+ options.route.security,
410
+ options.profileSecurity,
411
+ options.binding,
412
+ options.pathPolicy,
413
+ ),
414
+ };
415
+ }
416
+
417
+ private static getPathPatterns(pathPolicy: IRoutePathPolicyBinding): string[] {
418
+ const patterns: string[] = pathPolicy.pathPatterns?.length
419
+ ? pathPolicy.pathPatterns
420
+ : giteaRoutePathClassPatterns[pathPolicy.pathClass];
421
+ return [...new Set(patterns.map((pattern) => pattern.trim()).filter(Boolean))];
422
+ }
423
+
424
+ private static calculatePathPriorityOffset(
425
+ pathPattern: string | undefined,
426
+ pathIndex: number,
427
+ pathPolicyCount: number,
428
+ pathPatternIndex: number,
429
+ pathPatternCount: number,
430
+ ): number {
431
+ if (!pathPattern) {
432
+ return 0;
433
+ }
434
+ const pathPolicyOffset = ((pathPolicyCount - pathIndex) / (pathPolicyCount + 1))
435
+ * (PATH_PRIORITY_BAND * 0.9);
436
+ const pathPatternOffset = ((pathPatternCount - pathPatternIndex) / (pathPatternCount + 1))
437
+ * (PATH_PRIORITY_BAND * 0.1 / (pathPolicyCount + 1));
438
+ return pathPolicyOffset + pathPatternOffset;
439
+ }
440
+
441
+ private static calculateSourcePriority(
442
+ basePriority: number,
443
+ sourceIndex: number,
444
+ sourceCount: number,
445
+ ): number {
446
+ const safeBasePriority = this.clampPriority(
447
+ basePriority,
448
+ MIN_ROUTE_PRIORITY,
449
+ MAX_ROUTE_PRIORITY - SOURCE_PRIORITY_BAND - PATH_PRIORITY_BAND,
450
+ );
451
+ const sourceStep = SOURCE_PRIORITY_BAND / (sourceCount + 1);
452
+ return safeBasePriority + ((sourceCount - sourceIndex) * sourceStep);
453
+ }
454
+
455
+ private static clampPriority(
456
+ priority: number,
457
+ min = MIN_ROUTE_PRIORITY,
458
+ max = MAX_ROUTE_PRIORITY,
459
+ ): number {
460
+ if (!Number.isFinite(priority)) {
461
+ return min;
462
+ }
463
+ return Math.min(max, Math.max(min, priority));
464
+ }
465
+
466
+ private static getExpandedPortCount(portRange: plugins.smartproxy.IRouteConfig['match']['ports'] | undefined): number {
467
+ if (portRange === undefined) {
468
+ return 1;
469
+ }
470
+ if (typeof portRange === 'number') {
471
+ return Number.isFinite(portRange) ? 1 : sourcePolicyLimits.maxCompiledVariantsPerRoute + 1;
472
+ }
473
+ if (!Array.isArray(portRange)) {
474
+ return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1;
475
+ }
476
+
477
+ let count = 0;
478
+ for (const portEntry of portRange) {
479
+ if (typeof portEntry === 'number') {
480
+ if (!Number.isFinite(portEntry)) {
481
+ return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1;
482
+ }
483
+ count++;
484
+ } else if (
485
+ portEntry
486
+ && typeof portEntry === 'object'
487
+ && Number.isFinite(portEntry.from)
488
+ && Number.isFinite(portEntry.to)
489
+ && portEntry.from <= portEntry.to
490
+ ) {
491
+ count += Math.floor(portEntry.to) - Math.floor(portEntry.from) + 1;
492
+ } else {
493
+ return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1;
494
+ }
495
+ if (count > sourcePolicyLimits.maxCompiledVariantsPerRoute) {
496
+ return count;
497
+ }
498
+ }
499
+
500
+ return Math.max(1, count);
501
+ }
502
+
503
+ private static normalizeMaxConnections(value: IRouteSecurity['maxConnections']): number | undefined {
504
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
505
+ }
506
+
507
+ private static forceIpRateLimit(
508
+ rateLimit: IRouteSecurity['rateLimit'] | undefined,
509
+ ): IRouteSecurity['rateLimit'] | undefined {
510
+ if (!rateLimit) {
511
+ return undefined;
512
+ }
513
+ const { headerName: _headerName, ...rest } = structuredClone(rateLimit as Record<string, any>);
514
+ return {
515
+ ...rest,
516
+ keyBy: 'ip',
517
+ } as IRouteSecurity['rateLimit'];
518
+ }
519
+
520
+ private static sanitizeSourcePolicySecurity(security: IRouteSecurity): IRouteSecurity {
521
+ const sanitized = structuredClone(security);
522
+ const maxConnections = this.normalizeMaxConnections(sanitized.maxConnections);
523
+ if (maxConnections === undefined) {
524
+ delete sanitized.maxConnections;
525
+ } else {
526
+ sanitized.maxConnections = maxConnections;
527
+ }
528
+ if (sanitized.rateLimit) {
529
+ sanitized.rateLimit = this.forceIpRateLimit(sanitized.rateLimit);
530
+ }
531
+ return sanitized;
532
+ }
533
+
534
+ private static isEmptySecurity(security: IRouteSecurity): boolean {
535
+ return Object.keys(security).length === 0;
536
+ }
537
+
538
+ private static getSourceMatchEntries(security: IRouteSecurity): string[] {
539
+ const entries = security.ipAllowList || [];
540
+ const normalizedEntries: string[] = [];
541
+ for (const entry of entries) {
542
+ const rawEntry = typeof entry === 'string' ? entry : entry.ip;
543
+ if (typeof rawEntry !== 'string') continue;
544
+ const normalizedEntry = rawEntry.trim();
545
+ if (normalizedEntry) {
546
+ normalizedEntries.push(normalizedEntry);
547
+ }
548
+ }
549
+ return [...new Set(normalizedEntries)];
550
+ }
551
+
552
+ private static matchesAllSources(sourceMatches: string[]): boolean {
553
+ return sourceMatches.includes('*')
554
+ || (sourceMatches.includes('0.0.0.0/0') && sourceMatches.includes('::/0'));
555
+ }
556
+
557
+ private static buildBindingSecurity(
558
+ routeSecurity: IRouteSecurity | undefined,
559
+ profileSecurity: IRouteSecurity,
560
+ binding: IRouteSourcePolicyBinding,
561
+ pathPolicy?: IRoutePathPolicyBinding,
562
+ ): IRouteSecurity | undefined {
563
+ const baseSecurity = this.omitSourceMatchFields(routeSecurity || {});
564
+ const sourceSecurity = this.omitSourceMatchFields(profileSecurity);
565
+
566
+ if (binding.rateLimit !== undefined) {
567
+ sourceSecurity.rateLimit = this.forceIpRateLimit(binding.rateLimit);
568
+ }
569
+ if (binding.maxConnections !== undefined) {
570
+ const maxConnections = this.normalizeMaxConnections(binding.maxConnections);
571
+ if (maxConnections === undefined) {
572
+ delete sourceSecurity.maxConnections;
573
+ } else {
574
+ sourceSecurity.maxConnections = maxConnections;
575
+ }
576
+ }
577
+ if (binding.onExceeded?.errorMessage && sourceSecurity.rateLimit) {
578
+ sourceSecurity.rateLimit = {
579
+ ...sourceSecurity.rateLimit,
580
+ errorMessage: binding.onExceeded.errorMessage,
581
+ };
582
+ }
583
+
584
+ if (pathPolicy?.rateLimit !== undefined) {
585
+ sourceSecurity.rateLimit = this.forceIpRateLimit(pathPolicy.rateLimit);
586
+ }
587
+ if (pathPolicy?.maxConnections !== undefined) {
588
+ const maxConnections = this.normalizeMaxConnections(pathPolicy.maxConnections);
589
+ if (maxConnections === undefined) {
590
+ delete sourceSecurity.maxConnections;
591
+ } else {
592
+ sourceSecurity.maxConnections = maxConnections;
593
+ }
594
+ }
595
+ if (pathPolicy?.onExceeded?.errorMessage && sourceSecurity.rateLimit) {
596
+ sourceSecurity.rateLimit = {
597
+ ...sourceSecurity.rateLimit,
598
+ errorMessage: pathPolicy.onExceeded.errorMessage,
599
+ };
600
+ }
601
+
602
+ const mergedSecurity = this.sanitizeSourcePolicySecurity({
603
+ ...baseSecurity,
604
+ ...sourceSecurity,
605
+ });
606
+
607
+ return this.isEmptySecurity(mergedSecurity) ? undefined : mergedSecurity;
608
+ }
609
+
610
+ private static omitSourceMatchFields(security: IRouteSecurity): IRouteSecurity {
611
+ const { ipAllowList: _ipAllowList, ...controls } = security;
612
+ return this.sanitizeSourcePolicySecurity(controls);
613
+ }
614
+ }
@@ -4,5 +4,6 @@ export { RouteConfigManager } from './classes.route-config-manager.js';
4
4
  export { ApiTokenManager } from './classes.api-token-manager.js';
5
5
  export { GatewayClientManager } from './classes.gateway-client-manager.js';
6
6
  export { ReferenceResolver } from './classes.reference-resolver.js';
7
+ export { SourcePolicyCompiler } from './classes.source-policy-compiler.js';
7
8
  export { DbSeeder } from './classes.db-seeder.js';
8
9
  export { TargetProfileManager } from './classes.target-profile-manager.js';
package/ts/readme.md CHANGED
@@ -66,7 +66,7 @@ await router.start();
66
66
  - System routes from config, email, and DNS are persisted with stable ownership and are toggle-only.
67
67
  - API-created routes are the only routes intended for full CRUD from the dashboard or client SDK.
68
68
  - Qualifying HTTPS forward routes on port `443` get HTTP/3 augmentation by default.
69
- - `runCli()` is the supported code-level bootstrap entrypoint; the package does not expose a separate npm `bin` command.
69
+ - The published package exposes the `dcrouter` npm bin through `./cli.js`; `runCli()` is the supported code-level bootstrap entrypoint.
70
70
 
71
71
  ## Use Another Module When...
72
72
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.41.1',
6
+ version: '13.42.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }