@serve.zone/dcrouter 13.42.3 → 13.43.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 (44) hide show
  1. package/deno.json +11 -11
  2. package/dist_serve/bundle.js +722 -678
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.js +3 -3
  5. package/dist_ts/config/classes.route-config-manager.d.ts +4 -2
  6. package/dist_ts/config/classes.route-config-manager.js +17 -9
  7. package/dist_ts/config/classes.source-policy-compiler.d.ts +1 -0
  8. package/dist_ts/config/classes.source-policy-compiler.js +24 -2
  9. package/dist_ts/config/helpers.http-redirects.d.ts +10 -0
  10. package/dist_ts/config/helpers.http-redirects.js +387 -0
  11. package/dist_ts/config/index.d.ts +1 -0
  12. package/dist_ts/config/index.js +2 -1
  13. package/dist_ts/opsserver/handlers/route-management.handler.js +10 -1
  14. package/dist_ts_interfaces/data/route-management.d.ts +20 -0
  15. package/dist_ts_interfaces/requests/route-management.d.ts +14 -1
  16. package/dist_ts_web/00_commitinfo_data.js +1 -1
  17. package/dist_ts_web/appstate.d.ts +2 -0
  18. package/dist_ts_web/appstate.js +28 -1
  19. package/dist_ts_web/elements/access/ops-view-apitokens.js +2 -1
  20. package/dist_ts_web/elements/access/ops-view-gatewayclients.js +2 -1
  21. package/dist_ts_web/elements/network/index.d.ts +1 -0
  22. package/dist_ts_web/elements/network/index.js +2 -1
  23. package/dist_ts_web/elements/network/ops-view-redirects.d.ts +18 -0
  24. package/dist_ts_web/elements/network/ops-view-redirects.js +236 -0
  25. package/dist_ts_web/elements/network/ops-view-routes.js +2 -1
  26. package/dist_ts_web/elements/ops-dashboard.js +3 -1
  27. package/dist_ts_web/router.js +2 -2
  28. package/package.json +1 -1
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/classes.dcrouter.ts +2 -2
  31. package/ts/config/classes.route-config-manager.ts +22 -10
  32. package/ts/config/classes.source-policy-compiler.ts +33 -1
  33. package/ts/config/helpers.http-redirects.ts +462 -0
  34. package/ts/config/index.ts +1 -0
  35. package/ts/opsserver/handlers/route-management.handler.ts +15 -0
  36. package/ts_web/00_commitinfo_data.ts +1 -1
  37. package/ts_web/appstate.ts +32 -0
  38. package/ts_web/elements/access/ops-view-apitokens.ts +1 -0
  39. package/ts_web/elements/access/ops-view-gatewayclients.ts +1 -0
  40. package/ts_web/elements/network/index.ts +1 -0
  41. package/ts_web/elements/network/ops-view-redirects.ts +202 -0
  42. package/ts_web/elements/network/ops-view-routes.ts +1 -0
  43. package/ts_web/elements/ops-dashboard.ts +2 -0
  44. package/ts_web/router.ts +1 -1
@@ -0,0 +1,462 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type { IHttpRedirectInfo } from '../../ts_interfaces/data/route-management.js';
3
+ import type { IDcRouterRouteConfig, IRouteRemoteIngress } from '../../ts_interfaces/data/remoteingress.js';
4
+
5
+ const AUTO_REDIRECT_ROUTE_PREFIX = 'dcrouter-auto-http-redirect';
6
+ const REDIRECT_STATUS_CODE = 301;
7
+ const REDIRECT_PRIORITY = 0;
8
+ const REDIRECT_TARGET_TEMPLATE = 'https://{domain}{path}';
9
+ const REDIRECT_INITIAL_DATA_TIMEOUT_MS = 10_000;
10
+
11
+ interface IRedirectCandidate {
12
+ key: string;
13
+ id: string;
14
+ domainPattern: string;
15
+ pathPattern?: string;
16
+ sourceRouteNames: Set<string>;
17
+ sourceRouteIds: Set<string>;
18
+ remoteIngress?: IRouteRemoteIngress;
19
+ }
20
+
21
+ interface IRedirectConflict {
22
+ routeName: string;
23
+ covers: boolean;
24
+ }
25
+
26
+ export interface IHttpRedirectDerivationResult {
27
+ redirects: IHttpRedirectInfo[];
28
+ runtimeRoutes: IDcRouterRouteConfig[];
29
+ }
30
+
31
+ export function deriveHttpRedirectConfiguration(
32
+ routes: plugins.smartproxy.IRouteConfig[],
33
+ ): IHttpRedirectDerivationResult {
34
+ const candidates = collectRedirectCandidates(routes);
35
+ const httpRoutes = routes.filter((route) => isExplicitHttpRoute(route));
36
+ const redirects: IHttpRedirectInfo[] = [];
37
+ const runtimeRoutes: IDcRouterRouteConfig[] = [];
38
+
39
+ for (const candidate of candidates) {
40
+ const conflict = findHttpConflict(candidate, httpRoutes);
41
+ const redirectInfo: IHttpRedirectInfo = {
42
+ id: candidate.id,
43
+ status: conflict ? (conflict.covers ? 'covered' : 'skipped') : 'active',
44
+ domainPattern: candidate.domainPattern,
45
+ pathPattern: candidate.pathPattern,
46
+ fromTemplate: 'http://{domain}{path}',
47
+ toTemplate: REDIRECT_TARGET_TEMPLATE,
48
+ statusCode: REDIRECT_STATUS_CODE,
49
+ priority: REDIRECT_PRIORITY,
50
+ sourceRouteNames: [...candidate.sourceRouteNames].sort(),
51
+ sourceRouteIds: [...candidate.sourceRouteIds].sort(),
52
+ coveredByRouteNames: conflict ? [conflict.routeName] : [],
53
+ remoteIngress: Boolean(candidate.remoteIngress?.enabled),
54
+ notes: conflict
55
+ ? conflict.covers
56
+ ? 'An explicit HTTP route already covers this redirect scope.'
57
+ : 'Skipped because an explicit HTTP route overlaps this redirect scope.'
58
+ : undefined,
59
+ };
60
+
61
+ redirects.push(redirectInfo);
62
+
63
+ if (redirectInfo.status === 'active') {
64
+ runtimeRoutes.push(buildRuntimeRedirectRoute(candidate));
65
+ }
66
+ }
67
+
68
+ return { redirects, runtimeRoutes };
69
+ }
70
+
71
+ export function deriveHttpRedirects(
72
+ routes: plugins.smartproxy.IRouteConfig[],
73
+ ): IHttpRedirectInfo[] {
74
+ return deriveHttpRedirectConfiguration(routes).redirects;
75
+ }
76
+
77
+ export function buildHttpRedirectRuntimeRoutes(
78
+ routes: plugins.smartproxy.IRouteConfig[],
79
+ ): IDcRouterRouteConfig[] {
80
+ return deriveHttpRedirectConfiguration(routes).runtimeRoutes;
81
+ }
82
+
83
+ function collectRedirectCandidates(routes: plugins.smartproxy.IRouteConfig[]): IRedirectCandidate[] {
84
+ const candidates = new Map<string, IRedirectCandidate>();
85
+
86
+ for (const route of routes) {
87
+ if (!isHttpsRedirectSource(route)) {
88
+ continue;
89
+ }
90
+
91
+ for (const domainPattern of getDomainPatterns(route)) {
92
+ const key = createRedirectKey(domainPattern, route.match.path);
93
+ const existing = candidates.get(key);
94
+ if (existing) {
95
+ existing.sourceRouteNames.add(getRouteDisplayName(route));
96
+ if (route.id) existing.sourceRouteIds.add(route.id);
97
+ existing.remoteIngress = mergeRemoteIngress(existing.remoteIngress, (route as IDcRouterRouteConfig).remoteIngress);
98
+ continue;
99
+ }
100
+
101
+ const id = createRedirectRouteName(domainPattern, route.match.path);
102
+ candidates.set(key, {
103
+ key,
104
+ id,
105
+ domainPattern,
106
+ pathPattern: route.match.path,
107
+ sourceRouteNames: new Set([getRouteDisplayName(route)]),
108
+ sourceRouteIds: new Set(route.id ? [route.id] : []),
109
+ remoteIngress: mergeRemoteIngress(undefined, (route as IDcRouterRouteConfig).remoteIngress),
110
+ });
111
+ }
112
+ }
113
+
114
+ return [...candidates.values()].sort((a, b) => a.id.localeCompare(b.id));
115
+ }
116
+
117
+ function isHttpsRedirectSource(route: plugins.smartproxy.IRouteConfig): boolean {
118
+ if (isGeneratedRedirectRoute(route)) return false;
119
+ if (route.enabled === false) return false;
120
+ if (route.action.type !== 'forward') return false;
121
+ if (!route.match.ports) return false;
122
+ if (!plugins.smartproxy.portRangeIncludes(route.match.ports, 443)) return false;
123
+ if (!route.action.tls) return false;
124
+ if (!route.match.domains) return false;
125
+ if (route.match.transport === 'udp') return false;
126
+ if (route.match.protocol && route.match.protocol !== 'http') return false;
127
+ if (route.match.clientIp || route.match.headers || route.match.tlsVersion) return false;
128
+ return true;
129
+ }
130
+
131
+ function isExplicitHttpRoute(route: plugins.smartproxy.IRouteConfig): boolean {
132
+ if (isGeneratedRedirectRoute(route)) return false;
133
+ if (route.enabled === false) return false;
134
+ if (!route.match.ports) return false;
135
+ if (!plugins.smartproxy.portRangeIncludes(route.match.ports, 80)) return false;
136
+ if (route.match.transport === 'udp') return false;
137
+ return true;
138
+ }
139
+
140
+ function findHttpConflict(
141
+ candidate: IRedirectCandidate,
142
+ httpRoutes: plugins.smartproxy.IRouteConfig[],
143
+ ): IRedirectConflict | undefined {
144
+ for (const route of httpRoutes) {
145
+ if (!httpRouteOverlapsCandidate(route, candidate)) {
146
+ continue;
147
+ }
148
+
149
+ return {
150
+ routeName: getRouteDisplayName(route),
151
+ covers: httpRouteCoversCandidate(route, candidate),
152
+ };
153
+ }
154
+
155
+ return undefined;
156
+ }
157
+
158
+ function httpRouteOverlapsCandidate(
159
+ route: plugins.smartproxy.IRouteConfig,
160
+ candidate: IRedirectCandidate,
161
+ ): boolean {
162
+ return routeDomainOverlapsCandidate(route, candidate.domainPattern)
163
+ && pathOverlaps(route.match.path, candidate.pathPattern);
164
+ }
165
+
166
+ function httpRouteCoversCandidate(
167
+ route: plugins.smartproxy.IRouteConfig,
168
+ candidate: IRedirectCandidate,
169
+ ): boolean {
170
+ if (route.match.clientIp || route.match.headers || route.match.tlsVersion) {
171
+ return false;
172
+ }
173
+
174
+ return routeDomainCoversCandidate(route, candidate.domainPattern)
175
+ && pathCovers(route.match.path, candidate.pathPattern);
176
+ }
177
+
178
+ function routeDomainOverlapsCandidate(
179
+ route: plugins.smartproxy.IRouteConfig,
180
+ candidatePattern: string,
181
+ ): boolean {
182
+ const routePatterns = getDomainPatterns(route);
183
+ if (routePatterns.length === 0) {
184
+ return true;
185
+ }
186
+
187
+ return routePatterns.some((pattern) => domainPatternsOverlap(pattern, candidatePattern));
188
+ }
189
+
190
+ function routeDomainCoversCandidate(
191
+ route: plugins.smartproxy.IRouteConfig,
192
+ candidatePattern: string,
193
+ ): boolean {
194
+ const routePatterns = getDomainPatterns(route);
195
+ if (routePatterns.length === 0) {
196
+ return true;
197
+ }
198
+
199
+ return routePatterns.some((pattern) => domainPatternCovers(pattern, candidatePattern));
200
+ }
201
+
202
+ function getDomainPatterns(route: plugins.smartproxy.IRouteConfig): string[] {
203
+ if (!route.match.domains) return [];
204
+ return Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
205
+ }
206
+
207
+ function normalizePattern(pattern: string): string {
208
+ return pattern.trim().toLowerCase().replace(/\.$/, '');
209
+ }
210
+
211
+ function domainPatternCovers(coverPattern: string, candidatePattern: string): boolean {
212
+ const cover = normalizePattern(coverPattern);
213
+ const candidate = normalizePattern(candidatePattern);
214
+ if (cover === candidate) return true;
215
+ if (!candidate.includes('*')) return domainPatternMatchesHostname(cover, candidate);
216
+
217
+ const coverSuffix = getLeadingWildcardSuffix(cover);
218
+ const candidateSuffix = getLeadingWildcardSuffix(candidate);
219
+ if (coverSuffix && candidateSuffix) {
220
+ return candidateSuffix.endsWith(coverSuffix);
221
+ }
222
+
223
+ return false;
224
+ }
225
+
226
+ function domainPatternsOverlap(firstPattern: string, secondPattern: string): boolean {
227
+ const first = normalizePattern(firstPattern);
228
+ const second = normalizePattern(secondPattern);
229
+ if (first === second) return true;
230
+ if (!first.includes('*')) return domainPatternMatchesHostname(second, first);
231
+ if (!second.includes('*')) return domainPatternMatchesHostname(first, second);
232
+
233
+ const firstSuffix = getLeadingWildcardSuffix(first);
234
+ const secondSuffix = getLeadingWildcardSuffix(second);
235
+ if (firstSuffix && secondSuffix) {
236
+ return firstSuffix.endsWith(secondSuffix) || secondSuffix.endsWith(firstSuffix);
237
+ }
238
+
239
+ return false;
240
+ }
241
+
242
+ function domainPatternMatchesHostname(pattern: string, hostname: string): boolean {
243
+ const regex = wildcardPatternToRegex(normalizePattern(pattern));
244
+ return regex.test(normalizePattern(hostname));
245
+ }
246
+
247
+ function wildcardPatternToRegex(pattern: string): RegExp {
248
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
249
+ return new RegExp(`^${escaped.replace(/\*/g, '.*')}$`, 'i');
250
+ }
251
+
252
+ function getLeadingWildcardSuffix(pattern: string): string | undefined {
253
+ if (!pattern.startsWith('*')) return undefined;
254
+ if (pattern.slice(1).includes('*')) return undefined;
255
+ return pattern.slice(1);
256
+ }
257
+
258
+ function pathCovers(coverPath: string | undefined, candidatePath: string | undefined): boolean {
259
+ if (!coverPath) return true;
260
+ if (!candidatePath) return false;
261
+ if (coverPath === candidatePath) return true;
262
+ if (!coverPath.includes('*')) return false;
263
+ const coverPrefix = coverPath.split('*')[0];
264
+ if (!candidatePath.includes('*')) return candidatePath.startsWith(coverPrefix);
265
+ const candidatePrefix = candidatePath.split('*')[0];
266
+ return candidatePrefix.startsWith(coverPrefix);
267
+ }
268
+
269
+ function pathOverlaps(firstPath: string | undefined, secondPath: string | undefined): boolean {
270
+ if (!firstPath || !secondPath) return true;
271
+ if (firstPath === secondPath) return true;
272
+ const firstPrefix = firstPath.split('*')[0];
273
+ const secondPrefix = secondPath.split('*')[0];
274
+ return firstPrefix.startsWith(secondPrefix) || secondPrefix.startsWith(firstPrefix);
275
+ }
276
+
277
+ function buildRuntimeRedirectRoute(candidate: IRedirectCandidate): IDcRouterRouteConfig {
278
+ return {
279
+ id: candidate.id,
280
+ name: candidate.id,
281
+ description: 'Generated HTTP to HTTPS redirect',
282
+ priority: REDIRECT_PRIORITY,
283
+ tags: ['system', 'redirect', 'auto'],
284
+ match: {
285
+ ports: 80,
286
+ domains: candidate.domainPattern,
287
+ ...(candidate.pathPattern ? { path: candidate.pathPattern } : {}),
288
+ },
289
+ action: {
290
+ type: 'socket-handler',
291
+ socketHandler: createHttpRedirectHandler(REDIRECT_TARGET_TEMPLATE, REDIRECT_STATUS_CODE),
292
+ },
293
+ ...(candidate.remoteIngress ? { remoteIngress: candidate.remoteIngress } : {}),
294
+ };
295
+ }
296
+
297
+ function mergeRemoteIngress(
298
+ current: IRouteRemoteIngress | undefined,
299
+ next: IRouteRemoteIngress | undefined,
300
+ ): IRouteRemoteIngress | undefined {
301
+ if (!next?.enabled) return current;
302
+ if (!current?.enabled) {
303
+ return {
304
+ enabled: true,
305
+ ...(next.edgeFilter?.length ? { edgeFilter: [...next.edgeFilter] } : {}),
306
+ };
307
+ }
308
+
309
+ const currentFilter = current.edgeFilter || [];
310
+ const nextFilter = next.edgeFilter || [];
311
+ if (currentFilter.length === 0 || nextFilter.length === 0) {
312
+ return { enabled: true };
313
+ }
314
+
315
+ return {
316
+ enabled: true,
317
+ edgeFilter: [...new Set([...currentFilter, ...nextFilter])].sort(),
318
+ };
319
+ }
320
+
321
+ function createRedirectKey(domainPattern: string, pathPattern?: string): string {
322
+ return `${normalizePattern(domainPattern)}|${pathPattern || ''}`;
323
+ }
324
+
325
+ function createRedirectRouteName(domainPattern: string, pathPattern?: string): string {
326
+ const key = createRedirectKey(domainPattern, pathPattern);
327
+ const slug = key
328
+ .replace(/\*/g, 'wildcard')
329
+ .replace(/[^a-zA-Z0-9]+/g, '-')
330
+ .replace(/^-+|-+$/g, '')
331
+ .slice(0, 48) || 'route';
332
+ const hash = plugins.crypto.createHash('sha1').update(key).digest('hex').slice(0, 8);
333
+ return `${AUTO_REDIRECT_ROUTE_PREFIX}-${slug}-${hash}`;
334
+ }
335
+
336
+ function getRouteDisplayName(route: plugins.smartproxy.IRouteConfig): string {
337
+ return route.name || route.id || 'unnamed-route';
338
+ }
339
+
340
+ function isGeneratedRedirectRoute(route: plugins.smartproxy.IRouteConfig): boolean {
341
+ return Boolean(route.name?.startsWith(AUTO_REDIRECT_ROUTE_PREFIX) || route.id?.startsWith(AUTO_REDIRECT_ROUTE_PREFIX));
342
+ }
343
+
344
+ function createHttpRedirectHandler(
345
+ locationTemplate: string,
346
+ statusCode: number,
347
+ ): NonNullable<plugins.smartproxy.IRouteConfig['action']['socketHandler']> {
348
+ return (socket, context) => {
349
+ const cleanup = () => {
350
+ clearTimeout(timeout);
351
+ socket.removeListener('data', handleData);
352
+ socket.removeListener('error', cleanup);
353
+ socket.removeListener('close', cleanup);
354
+ };
355
+
356
+ const handleData = (data: string | Uint8Array) => {
357
+ cleanup();
358
+ const request = parseHttpRequest(data);
359
+ if (!request) {
360
+ socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
361
+ return;
362
+ }
363
+
364
+ const domain = normalizeHostHeader(request.headers.host) || context.domain || 'localhost';
365
+ const finalLocation = locationTemplate
366
+ .replace('{domain}', domain)
367
+ .replace('{port}', String(context.port))
368
+ .replace('{path}', request.path || '/')
369
+ .replace('{clientIp}', context.clientIp);
370
+ const message = `Redirecting to ${finalLocation}`;
371
+ const response = [
372
+ `HTTP/1.1 ${statusCode} ${getHttpStatusText(statusCode)}`,
373
+ `Location: ${finalLocation}`,
374
+ 'Content-Type: text/plain',
375
+ `Content-Length: ${message.length}`,
376
+ 'Connection: close',
377
+ '',
378
+ message,
379
+ ].join('\r\n');
380
+
381
+ socket.end(response);
382
+ };
383
+
384
+ const timeout = setTimeout(() => {
385
+ cleanup();
386
+ socket.end('HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n');
387
+ }, REDIRECT_INITIAL_DATA_TIMEOUT_MS) as ReturnType<typeof setTimeout> & { unref?: () => void };
388
+ timeout.unref?.();
389
+
390
+ socket.once('data', handleData);
391
+ socket.once('error', cleanup);
392
+ socket.once('close', cleanup);
393
+ };
394
+ }
395
+
396
+ function parseHttpRequest(data: string | Uint8Array): {
397
+ method: string;
398
+ path: string;
399
+ headers: Record<string, string>;
400
+ } | undefined {
401
+ const requestText = typeof data === 'string' ? data : new TextDecoder().decode(data);
402
+ const headerEnd = requestText.indexOf('\r\n\r\n');
403
+ const headerText = headerEnd >= 0 ? requestText.slice(0, headerEnd) : requestText;
404
+ const lines = headerText.split('\r\n');
405
+ const [method, rawPath] = (lines[0] || '').split(' ');
406
+ if (!method || !rawPath) return undefined;
407
+
408
+ const headers: Record<string, string> = {};
409
+ for (const line of lines.slice(1)) {
410
+ const colonIndex = line.indexOf(':');
411
+ if (colonIndex <= 0) continue;
412
+ const key = line.slice(0, colonIndex).trim().toLowerCase();
413
+ const value = line.slice(colonIndex + 1).trim();
414
+ headers[key] = value;
415
+ }
416
+
417
+ return {
418
+ method,
419
+ path: normalizeRequestPath(rawPath),
420
+ headers,
421
+ };
422
+ }
423
+
424
+ function normalizeRequestPath(rawPath: string): string {
425
+ if (rawPath.startsWith('http://') || rawPath.startsWith('https://')) {
426
+ try {
427
+ const url = new URL(rawPath);
428
+ return `${url.pathname}${url.search}` || '/';
429
+ } catch {
430
+ return '/';
431
+ }
432
+ }
433
+
434
+ return rawPath.startsWith('/') ? rawPath : '/';
435
+ }
436
+
437
+ function normalizeHostHeader(hostHeader: string | undefined): string | undefined {
438
+ if (!hostHeader) return undefined;
439
+ const host = hostHeader.split(',')[0].trim();
440
+ if (!host || /[\s\x00-\x1f\x7f]/.test(host)) return undefined;
441
+ if (host.startsWith('[')) {
442
+ const bracketIndex = host.indexOf(']');
443
+ return bracketIndex > 0 ? host.slice(0, bracketIndex + 1) : undefined;
444
+ }
445
+
446
+ return host.replace(/:(80|443)$/, '');
447
+ }
448
+
449
+ function getHttpStatusText(statusCode: number): string {
450
+ switch (statusCode) {
451
+ case 301:
452
+ return 'Moved Permanently';
453
+ case 302:
454
+ return 'Found';
455
+ case 307:
456
+ return 'Temporary Redirect';
457
+ case 308:
458
+ return 'Permanent Redirect';
459
+ default:
460
+ return 'Redirect';
461
+ }
462
+ }
@@ -5,5 +5,6 @@ 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
7
  export { SourcePolicyCompiler } from './classes.source-policy-compiler.js';
8
+ export * from './helpers.http-redirects.js';
8
9
  export { DbSeeder } from './classes.db-seeder.js';
9
10
  export { TargetProfileManager } from './classes.target-profile-manager.js';
@@ -42,6 +42,21 @@ export class RouteManagementHandler {
42
42
  ),
43
43
  );
44
44
 
45
+ // Get generated HTTP redirects
46
+ this.typedrouter.addTypedHandler(
47
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHttpRedirects>(
48
+ 'getHttpRedirects',
49
+ async (dataArg) => {
50
+ await this.requireAuth(dataArg, 'routes:read');
51
+ const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
52
+ if (!manager) {
53
+ return { redirects: [] };
54
+ }
55
+ return { redirects: manager.getHttpRedirects() };
56
+ },
57
+ ),
58
+ );
59
+
45
60
  // Create route
46
61
  this.typedrouter.addTypedHandler(
47
62
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRoute>(
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.42.3',
6
+ version: '13.43.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -290,6 +290,7 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
290
290
  export interface IRouteManagementState {
291
291
  mergedRoutes: interfaces.data.IMergedRoute[];
292
292
  warnings: interfaces.data.IRouteWarning[];
293
+ httpRedirects: interfaces.data.IHttpRedirectInfo[];
293
294
  apiTokens: interfaces.data.IApiTokenInfo[];
294
295
  gatewayClients: interfaces.data.IGatewayClient[];
295
296
  isLoading: boolean;
@@ -302,6 +303,7 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
302
303
  {
303
304
  mergedRoutes: [],
304
305
  warnings: [],
306
+ httpRedirects: [],
305
307
  apiTokens: [],
306
308
  gatewayClients: [],
307
309
  isLoading: false,
@@ -2474,6 +2476,36 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
2474
2476
  }
2475
2477
  });
2476
2478
 
2479
+ export const fetchHttpRedirectsAction = routeManagementStatePart.createAction(async (statePartArg): Promise<IRouteManagementState> => {
2480
+ const context = getActionContext();
2481
+ const currentState = statePartArg.getState()!;
2482
+ if (!context.identity) return currentState;
2483
+
2484
+ try {
2485
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2486
+ interfaces.requests.IReq_GetHttpRedirects
2487
+ >('/typedrequest', 'getHttpRedirects');
2488
+
2489
+ const response = await request.fire({
2490
+ identity: context.identity,
2491
+ });
2492
+
2493
+ return {
2494
+ ...currentState,
2495
+ httpRedirects: response.redirects,
2496
+ isLoading: false,
2497
+ error: null,
2498
+ lastUpdated: Date.now(),
2499
+ };
2500
+ } catch (error) {
2501
+ return {
2502
+ ...currentState,
2503
+ isLoading: false,
2504
+ error: error instanceof Error ? error.message : 'Failed to fetch HTTP redirects',
2505
+ };
2506
+ }
2507
+ });
2508
+
2477
2509
  export const createRouteAction = routeManagementStatePart.createAction<{
2478
2510
  route: any;
2479
2511
  enabled?: boolean;
@@ -19,6 +19,7 @@ export class OpsViewApiTokens extends DeesElement {
19
19
  @state() accessor routeState: appstate.IRouteManagementState = {
20
20
  mergedRoutes: [],
21
21
  warnings: [],
22
+ httpRedirects: [],
22
23
  apiTokens: [],
23
24
  gatewayClients: [],
24
25
  isLoading: false,
@@ -17,6 +17,7 @@ export class OpsViewGatewayClients extends DeesElement {
17
17
  @state() accessor routeState: appstate.IRouteManagementState = {
18
18
  mergedRoutes: [],
19
19
  warnings: [],
20
+ httpRedirects: [],
20
21
  apiTokens: [],
21
22
  gatewayClients: [],
22
23
  isLoading: false,
@@ -1,5 +1,6 @@
1
1
  export * from './ops-view-network-activity.js';
2
2
  export * from './ops-view-routes.js';
3
+ export * from './ops-view-redirects.js';
3
4
  export * from './ops-view-sourceprofiles.js';
4
5
  export * from './ops-view-networktargets.js';
5
6
  export * from './ops-view-targetprofiles.js';