From c7de3873d833ebfe9efc0a175db64ea3548df566 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 19 Mar 2026 19:06:15 +0000 Subject: [PATCH] feat(http3): add automatic HTTP/3 route augmentation for qualifying HTTPS routes --- changelog.md | 8 + package.json | 2 +- pnpm-lock.yaml | 10 +- test/test.http3-augmentation.ts | 304 ++++++++++++++++++++++ ts/00_commitinfo_data.ts | 2 +- ts/classes.dcrouter.ts | 17 ++ ts/config/classes.route-config-manager.ts | 11 +- ts/http3/http3-route-augmentation.ts | 153 +++++++++++ ts/http3/index.ts | 1 + ts/index.ts | 3 + ts_web/00_commitinfo_data.ts | 2 +- 11 files changed, 503 insertions(+), 10 deletions(-) create mode 100644 test/test.http3-augmentation.ts create mode 100644 ts/http3/http3-route-augmentation.ts create mode 100644 ts/http3/index.ts diff --git a/changelog.md b/changelog.md index 658fc44..29c1401 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-19 - 11.6.0 - feat(http3) +add automatic HTTP/3 route augmentation for qualifying HTTPS routes + +- introduce configurable HTTP/3 augmentation utilities for eligible SmartProxy routes on port 443 +- apply HTTP/3 settings to both constructor-defined and stored programmatic routes, with global and per-route opt-out support +- export the HTTP/3 config type and add test coverage for qualification, augmentation behavior, and defaults +- bump @push.rocks/smartproxy to ^25.15.0 for HTTP/3-related support + ## 2026-03-19 - 11.5.1 - fix(project) no changes to commit diff --git a/package.json b/package.json index 5ce38d0..246657a 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@push.rocks/smartnetwork": "^4.4.0", "@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpromise": "^4.2.3", - "@push.rocks/smartproxy": "^25.14.1", + "@push.rocks/smartproxy": "^25.15.0", "@push.rocks/smartradius": "^1.1.1", "@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrx": "^3.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b58837e..cc0bf2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,8 +78,8 @@ importers: specifier: ^4.2.3 version: 4.2.3 '@push.rocks/smartproxy': - specifier: ^25.14.1 - version: 25.14.1 + specifier: ^25.15.0 + version: 25.15.0 '@push.rocks/smartradius': specifier: ^1.1.1 version: 1.1.1 @@ -1256,8 +1256,8 @@ packages: '@push.rocks/smartpromise@4.2.3': resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} - '@push.rocks/smartproxy@25.14.1': - resolution: {integrity: sha512-QXJ1M7Or81lmCusAKkmIB8M9jJwl1/AKItnmTkn8IQ9zsPd6r+0uhP1j5tCO/LwRQRpzOADnpSrpVcrtMKK9kQ==} + '@push.rocks/smartproxy@25.15.0': + resolution: {integrity: sha512-quw4MH6Snr6X2vy27iykXbBwN1oDKU7AntbUAPOgsWERTTDZGZU79fk9VZTvk5hGNemb2wEgnkgsUxAnj0y4dQ==} '@push.rocks/smartpuppeteer@2.0.5': resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} @@ -6539,7 +6539,7 @@ snapshots: '@push.rocks/smartpromise@4.2.3': {} - '@push.rocks/smartproxy@25.14.1': + '@push.rocks/smartproxy@25.15.0': dependencies: '@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartlog': 3.2.1 diff --git a/test/test.http3-augmentation.ts b/test/test.http3-augmentation.ts new file mode 100644 index 0000000..cb4ecbe --- /dev/null +++ b/test/test.http3-augmentation.ts @@ -0,0 +1,304 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import { + routeQualifiesForHttp3, + augmentRouteWithHttp3, + augmentRoutesWithHttp3, + type IHttp3Config, +} from '../ts/http3/index.js'; +import type * as plugins from '../ts/plugins.js'; + +// Helper to create a basic HTTPS forward route on port 443 +function makeRoute( + overrides: Partial = {}, +): plugins.smartproxy.IRouteConfig { + return { + match: { ports: 443, ...overrides.match }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + ...overrides.action, + }, + name: overrides.name ?? 'test-https-route', + ...Object.fromEntries( + Object.entries(overrides).filter(([k]) => !['match', 'action', 'name'].includes(k)), + ), + } as plugins.smartproxy.IRouteConfig; +} + +const defaultConfig: IHttp3Config = { enabled: true }; + +// ────────────────────────────────────────────────────────────────────────────── +// Qualification tests +// ────────────────────────────────────────────────────────────────────────────── + +tap.test('should augment qualifying HTTPS route on port 443', async () => { + const route = makeRoute(); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toEqual('all'); + expect(result.action.udp).toBeTruthy(); + expect(result.action.udp!.quic).toBeTruthy(); + expect(result.action.udp!.quic!.enableHttp3).toBeTrue(); + expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(86400); +}); + +tap.test('should NOT augment route on non-443 port', async () => { + const route = makeRoute({ match: { ports: 8080 } }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toBeUndefined(); + expect(result.action.udp).toBeUndefined(); +}); + +tap.test('should NOT augment socket-handler type route', async () => { + const route = makeRoute({ + action: { + type: 'socket-handler' as any, + socketHandler: (() => {}) as any, + }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toBeUndefined(); +}); + +tap.test('should NOT augment route without TLS', async () => { + const route: plugins.smartproxy.IRouteConfig = { + match: { ports: 443 }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + }, + name: 'no-tls-route', + }; + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toBeUndefined(); +}); + +tap.test('should NOT augment email routes', async () => { + const emailNames = ['smtp-route', 'submission-route', 'smtps-route', 'email-port-2525-route']; + for (const name of emailNames) { + const route = makeRoute({ name }); + const result = augmentRouteWithHttp3(route, defaultConfig); + expect(result.match.transport).toBeUndefined(); + } +}); + +tap.test('should respect per-route opt-out (options.http3 = false)', async () => { + const route = makeRoute({ + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + options: { http3: false }, + }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toBeUndefined(); + expect(result.action.udp).toBeUndefined(); +}); + +tap.test('should respect per-route opt-in when global is disabled', async () => { + const route = makeRoute({ + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + options: { http3: true }, + }, + }); + const result = augmentRouteWithHttp3(route, { enabled: false }); + + expect(result.match.transport).toEqual('all'); + expect(result.action.udp!.quic!.enableHttp3).toBeTrue(); +}); + +tap.test('should NOT double-augment routes with transport: all', async () => { + const route = makeRoute({ + match: { ports: 443, transport: 'all' as any }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + // Should be the exact same object (no augmentation) + expect(result).toEqual(route); +}); + +tap.test('should NOT double-augment routes with existing udp.quic', async () => { + const route = makeRoute({ + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + udp: { quic: { enableHttp3: true } }, + }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result).toEqual(route); +}); + +tap.test('should augment route with port range including 443', async () => { + const route = makeRoute({ + match: { ports: [{ from: 400, to: 500 }] }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toEqual('all'); + expect(result.action.udp!.quic!.enableHttp3).toBeTrue(); +}); + +tap.test('should augment route with port array including 443', async () => { + const route = makeRoute({ + match: { ports: [80, 443] }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toEqual('all'); + expect(result.action.udp!.quic!.enableHttp3).toBeTrue(); +}); + +tap.test('should NOT augment route with port range NOT including 443', async () => { + const route = makeRoute({ + match: { ports: [{ from: 8000, to: 9000 }] }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toBeUndefined(); +}); + +tap.test('should augment TLS passthrough routes', async () => { + const route = makeRoute({ + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'passthrough' }, + }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toEqual('all'); + expect(result.action.udp!.quic!.enableHttp3).toBeTrue(); +}); + +tap.test('should augment terminate-and-reencrypt routes', async () => { + const route = makeRoute({ + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' }, + }, + }); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.match.transport).toEqual('all'); + expect(result.action.udp!.quic!.enableHttp3).toBeTrue(); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Configuration tests +// ────────────────────────────────────────────────────────────────────────────── + +tap.test('should apply default QUIC settings when none provided', async () => { + const route = makeRoute(); + const result = augmentRouteWithHttp3(route, defaultConfig); + + expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(86400); + // Undefined means SmartProxy will use its own defaults + expect(result.action.udp!.quic!.maxIdleTimeout).toBeUndefined(); + expect(result.action.udp!.quic!.altSvcPort).toBeUndefined(); +}); + +tap.test('should apply custom QUIC settings', async () => { + const route = makeRoute(); + const config: IHttp3Config = { + enabled: true, + quicSettings: { + maxIdleTimeout: 60000, + maxConcurrentBidiStreams: 200, + maxConcurrentUniStreams: 50, + initialCongestionWindow: 65536, + }, + altSvc: { + port: 8443, + maxAge: 3600, + }, + udpSettings: { + sessionTimeout: 120000, + maxSessionsPerIP: 500, + maxDatagramSize: 32768, + }, + }; + const result = augmentRouteWithHttp3(route, config); + + expect(result.action.udp!.quic!.maxIdleTimeout).toEqual(60000); + expect(result.action.udp!.quic!.maxConcurrentBidiStreams).toEqual(200); + expect(result.action.udp!.quic!.maxConcurrentUniStreams).toEqual(50); + expect(result.action.udp!.quic!.initialCongestionWindow).toEqual(65536); + expect(result.action.udp!.quic!.altSvcPort).toEqual(8443); + expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(3600); + expect(result.action.udp!.sessionTimeout).toEqual(120000); + expect(result.action.udp!.maxSessionsPerIP).toEqual(500); + expect(result.action.udp!.maxDatagramSize).toEqual(32768); +}); + +tap.test('should not mutate the original route', async () => { + const route = makeRoute(); + const originalTransport = route.match.transport; + const originalUdp = route.action.udp; + + augmentRouteWithHttp3(route, defaultConfig); + + expect(route.match.transport).toEqual(originalTransport); + expect(route.action.udp).toEqual(originalUdp); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Batch augmentation +// ────────────────────────────────────────────────────────────────────────────── + +tap.test('should augment multiple routes in a batch', async () => { + const routes = [ + makeRoute({ name: 'web-app' }), + makeRoute({ name: 'smtp-route', match: { ports: 25 } }), + makeRoute({ name: 'api-gateway' }), + makeRoute({ + name: 'dns-query', + action: { type: 'socket-handler' as any, socketHandler: (() => {}) as any }, + }), + ]; + + const results = augmentRoutesWithHttp3(routes, defaultConfig); + + // web-app and api-gateway should be augmented + expect(results[0].match.transport).toEqual('all'); + expect(results[2].match.transport).toEqual('all'); + + // smtp and dns should NOT be augmented + expect(results[1].match.transport).toBeUndefined(); + expect(results[3].match.transport).toBeUndefined(); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Default enabled behavior +// ────────────────────────────────────────────────────────────────────────────── + +tap.test('should treat undefined enabled as true (default on)', async () => { + const route = makeRoute(); + const result = augmentRouteWithHttp3(route, {}); // no enabled field at all + + expect(result.match.transport).toEqual('all'); + expect(result.action.udp!.quic!.enableHttp3).toBeTrue(); +}); + +tap.test('should disable when enabled is explicitly false', async () => { + const route = makeRoute(); + const result = augmentRouteWithHttp3(route, { enabled: false }); + + expect(result.match.transport).toBeUndefined(); + expect(result.action.udp).toBeUndefined(); +}); + +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index e95c75c..8ff588b 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '11.5.1', + version: '11.6.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 693c694..d967abc 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -24,6 +24,7 @@ import { RadiusServer, type IRadiusServerConfig } from './radius/index.js'; import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js'; import { RouteConfigManager, ApiTokenManager } from './config/index.js'; import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js'; +import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js'; export interface IDcRouterOptions { /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */ @@ -163,6 +164,14 @@ export interface IDcRouterOptions { * Remote Ingress configuration for edge tunnel nodes * Enables edge nodes to accept incoming connections and tunnel them to this DcRouter */ + /** + * HTTP/3 (QUIC) configuration for HTTPS routes. + * Enabled by default — qualifying HTTPS routes on port 443 are automatically + * augmented with QUIC/H3 fields. Set { enabled: false } to disable globally. + * Individual routes can opt out via action.options.http3 = false. + */ + http3?: IHttp3Config; + /** Port for the OpsServer web UI (default: 3000) */ opsServerPort?: number; @@ -297,6 +306,7 @@ export class DcRouter { this.storageManager, () => this.getConstructorRoutes(), () => this.smartProxy, + () => this.options.http3, ); this.apiTokenManager = new ApiTokenManager(this.storageManager); await this.apiTokenManager.initialize(); @@ -469,6 +479,13 @@ export class DcRouter { challengeHandlers.push(dns01Handler); } + // HTTP/3 augmentation (enabled by default unless explicitly disabled) + if (this.options.http3?.enabled !== false) { + const http3Config: IHttp3Config = { enabled: true, ...this.options.http3 }; + routes = augmentRoutesWithHttp3(routes, http3Config); + logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration'); + } + // Cache constructor routes for RouteConfigManager this.constructorRoutes = [...routes]; diff --git a/ts/config/classes.route-config-manager.ts b/ts/config/classes.route-config-manager.ts index a8d3bcd..53e19a3 100644 --- a/ts/config/classes.route-config-manager.ts +++ b/ts/config/classes.route-config-manager.ts @@ -7,6 +7,7 @@ import type { IMergedRoute, IRouteWarning, } from '../../ts_interfaces/data/route-management.js'; +import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js'; const ROUTES_PREFIX = '/config-api/routes/'; const OVERRIDES_PREFIX = '/config-api/overrides/'; @@ -20,6 +21,7 @@ export class RouteConfigManager { private storageManager: StorageManager, private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[], private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined, + private getHttp3Config?: () => IHttp3Config | undefined, ) {} /** @@ -258,10 +260,15 @@ export class RouteConfigManager { enabledRoutes.push(route); } - // Add enabled programmatic routes + // Add enabled programmatic routes (with HTTP/3 augmentation if enabled) + const http3Config = this.getHttp3Config?.(); for (const stored of this.storedRoutes.values()) { if (stored.enabled) { - enabledRoutes.push(stored.route); + if (http3Config && http3Config.enabled !== false) { + enabledRoutes.push(augmentRouteWithHttp3(stored.route, { enabled: true, ...http3Config })); + } else { + enabledRoutes.push(stored.route); + } } } diff --git a/ts/http3/http3-route-augmentation.ts b/ts/http3/http3-route-augmentation.ts new file mode 100644 index 0000000..6d37243 --- /dev/null +++ b/ts/http3/http3-route-augmentation.ts @@ -0,0 +1,153 @@ +import type * as plugins from '../plugins.js'; + +/** + * Configuration for HTTP/3 (QUIC) route augmentation. + * HTTP/3 is enabled by default on all qualifying HTTPS routes. + */ +export interface IHttp3Config { + /** Enable HTTP/3 augmentation on qualifying routes (default: true) */ + enabled?: boolean; + /** QUIC-specific settings applied to all augmented routes */ + quicSettings?: { + /** QUIC connection idle timeout in ms (default: 30000) */ + maxIdleTimeout?: number; + /** Max concurrent bidirectional streams per connection (default: 100) */ + maxConcurrentBidiStreams?: number; + /** Max concurrent unidirectional streams per connection (default: 100) */ + maxConcurrentUniStreams?: number; + /** Initial congestion window size in bytes */ + initialCongestionWindow?: number; + }; + /** Alt-Svc header settings */ + altSvc?: { + /** Port advertised in Alt-Svc header (default: same as listening port) */ + port?: number; + /** Max age for Alt-Svc advertisement in seconds (default: 86400) */ + maxAge?: number; + }; + /** UDP session settings */ + udpSettings?: { + /** Idle timeout for UDP sessions in ms (default: 60000) */ + sessionTimeout?: number; + /** Max concurrent UDP sessions per source IP (default: 1000) */ + maxSessionsPerIP?: number; + /** Max accepted datagram size in bytes (default: 65535) */ + maxDatagramSize?: number; + }; +} + +type TPortRange = plugins.smartproxy.IRouteConfig['match']['ports']; + +/** + * Check whether a TPortRange includes port 443. + */ +function portRangeIncludes443(ports: TPortRange): boolean { + if (typeof ports === 'number') return ports === 443; + if (Array.isArray(ports)) { + return ports.some((p) => { + if (typeof p === 'number') return p === 443; + return p.from <= 443 && p.to >= 443; + }); + } + return false; +} + +/** + * Check if a route name indicates an email route that should not get HTTP/3. + */ +function isEmailRoute(route: plugins.smartproxy.IRouteConfig): boolean { + const name = route.name?.toLowerCase() || ''; + return ( + name.startsWith('smtp-') || + name.startsWith('submission-') || + name.startsWith('smtps-') || + name.startsWith('email-') + ); +} + +/** + * Determine if a route qualifies for HTTP/3 augmentation. + */ +export function routeQualifiesForHttp3( + route: plugins.smartproxy.IRouteConfig, + globalConfig: IHttp3Config, +): boolean { + // Check global enable + per-route override + const globalEnabled = globalConfig.enabled !== false; // default true + const perRouteOverride = route.action.options?.http3; + + // If per-route explicitly set, use that; otherwise use global + const shouldAugment = + perRouteOverride !== undefined ? perRouteOverride : globalEnabled; + if (!shouldAugment) return false; + + // Must be forward type + if (route.action.type !== 'forward') return false; + + // Must include port 443 + if (!portRangeIncludes443(route.match.ports)) return false; + + // Must have TLS + if (!route.action.tls) return false; + + // Skip email routes + if (isEmailRoute(route)) return false; + + // Skip if already configured with transport 'all' or 'udp' + if (route.match.transport === 'all' || route.match.transport === 'udp') return false; + + // Skip if already has QUIC config + if (route.action.udp?.quic) return false; + + return true; +} + +/** + * Augment a single route with HTTP/3 fields. + * Returns a new route object (does not mutate the original). + */ +export function augmentRouteWithHttp3( + route: plugins.smartproxy.IRouteConfig, + config: IHttp3Config, +): plugins.smartproxy.IRouteConfig { + if (!routeQualifiesForHttp3(route, config)) { + return route; + } + + return { + ...route, + match: { + ...route.match, + transport: 'all' as const, + }, + action: { + ...route.action, + udp: { + ...(route.action.udp || {}), + sessionTimeout: config.udpSettings?.sessionTimeout, + maxSessionsPerIP: config.udpSettings?.maxSessionsPerIP, + maxDatagramSize: config.udpSettings?.maxDatagramSize, + quic: { + enableHttp3: true, + maxIdleTimeout: config.quicSettings?.maxIdleTimeout, + maxConcurrentBidiStreams: config.quicSettings?.maxConcurrentBidiStreams, + maxConcurrentUniStreams: config.quicSettings?.maxConcurrentUniStreams, + altSvcPort: config.altSvc?.port, + altSvcMaxAge: config.altSvc?.maxAge ?? 86400, + initialCongestionWindow: config.quicSettings?.initialCongestionWindow, + }, + }, + }, + }; +} + +/** + * Augment all qualifying routes in an array. + * Returns a new array (does not mutate originals). + */ +export function augmentRoutesWithHttp3( + routes: plugins.smartproxy.IRouteConfig[], + config: IHttp3Config, +): plugins.smartproxy.IRouteConfig[] { + return routes.map((route) => augmentRouteWithHttp3(route, config)); +} diff --git a/ts/http3/index.ts b/ts/http3/index.ts new file mode 100644 index 0000000..40acea5 --- /dev/null +++ b/ts/http3/index.ts @@ -0,0 +1 @@ +export * from './http3-route-augmentation.js'; diff --git a/ts/index.ts b/ts/index.ts index a139914..b0e5ac4 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -14,6 +14,9 @@ export * from './radius/index.js'; // Remote Ingress module export * from './remoteingress/index.js'; +// HTTP/3 module +export type { IHttp3Config } from './http3/index.js'; + export const runCli = async () => { let options: import('./classes.dcrouter.js').IDcRouterOptions = {}; diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index e95c75c..8ff588b 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '11.5.1', + version: '11.6.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }