From 96d215fc66f95b883591265f2dc929c425078cec Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 4 Apr 2026 21:23:16 +0000 Subject: [PATCH] feat(routes): add TLS configuration controls for route create and edit flows --- changelog.md | 7 ++ package.json | 4 +- pnpm-lock.yaml | 24 ++--- ts/00_commitinfo_data.ts | 2 +- ts/config/classes.route-config-manager.ts | 13 ++- ts_web/00_commitinfo_data.ts | 2 +- ts_web/elements/ops-view-routes.ts | 113 +++++++++++++++++++++- 7 files changed, 146 insertions(+), 19 deletions(-) diff --git a/changelog.md b/changelog.md index 10c020a..90c7f18 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-04-04 - 12.10.0 - feat(routes) +add TLS configuration controls for route create and edit flows + +- Adds TLS mode and certificate selection to the route create and edit dialogs, including support for custom PEM key/certificate input. +- Allows route updates to explicitly remove nested TLS settings by treating null action properties as deletions during route patch merging. +- Bumps @design.estate/dees-catalog to ^3.55.6 and @serve.zone/catalog to ^2.11.1. + ## 2026-04-04 - 12.9.4 - fix(deps) bump @push.rocks/smartdb to ^2.3.1 diff --git a/package.json b/package.json index b3c9015..80f7cf2 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@api.global/typedserver": "^8.4.6", "@api.global/typedsocket": "^4.1.2", "@apiclient.xyz/cloudflare": "^7.1.0", - "@design.estate/dees-catalog": "^3.55.5", + "@design.estate/dees-catalog": "^3.55.6", "@design.estate/dees-element": "^2.2.4", "@push.rocks/lik": "^6.4.0", "@push.rocks/projectinfo": "^5.1.0", @@ -61,7 +61,7 @@ "@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartvpn": "1.19.1", "@push.rocks/taskbuffer": "^8.0.2", - "@serve.zone/catalog": "^2.11.0", + "@serve.zone/catalog": "^2.11.1", "@serve.zone/interfaces": "^5.3.0", "@serve.zone/remoteingress": "^4.15.3", "@tsclass/tsclass": "^9.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00893a1..88ab2bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^7.1.0 version: 7.1.0 '@design.estate/dees-catalog': - specifier: ^3.55.5 - version: 3.55.5(@tiptap/pm@2.27.2) + specifier: ^3.55.6 + version: 3.55.6(@tiptap/pm@2.27.2) '@design.estate/dees-element': specifier: ^2.2.4 version: 2.2.4 @@ -102,8 +102,8 @@ importers: specifier: ^8.0.2 version: 8.0.2 '@serve.zone/catalog': - specifier: ^2.11.0 - version: 2.11.0(@tiptap/pm@2.27.2) + specifier: ^2.11.1 + version: 2.11.1(@tiptap/pm@2.27.2) '@serve.zone/interfaces': specifier: ^5.3.0 version: 5.3.0 @@ -350,8 +350,8 @@ packages: '@configvault.io/interfaces@1.0.17': resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} - '@design.estate/dees-catalog@3.55.5': - resolution: {integrity: sha512-NAMUkTVqdZZmwI/g1xKOxOYM9QUd9FHODh6MYkP6LhLjD0NOGh3bITCnNN9Z3x8/mI7vQQOlSe9tyTtxCP1itQ==} + '@design.estate/dees-catalog@3.55.6': + resolution: {integrity: sha512-aBuofV18v2X9U+WXQcwx/uOMofDECYRoqGovpB2cD2gpf5eCvaD2Y6HZetocKeW/VeOFlzwgykeSxAY8KvfiKQ==} '@design.estate/dees-comms@1.0.30': resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} @@ -1583,8 +1583,8 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@serve.zone/catalog@2.11.0': - resolution: {integrity: sha512-4DFDewp1PFRhw5P+yQAoAw+i6gG2lfR3h+uPgbNxB5jCfW14eNDXi3nuwTMBQWRHL9jv8o0BokASjV9A0+q66g==} + '@serve.zone/catalog@2.11.1': + resolution: {integrity: sha512-KeeShLELKANWEAY3Z4h+Kx1bmatWLa0nvtw8wa7iHLxV6Vm3rRJNdRyh91ozuWPgFYMC48V8/4REA8zJsdDcbg==} '@serve.zone/interfaces@5.3.0': resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} @@ -4355,7 +4355,7 @@ snapshots: '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3) '@cloudflare/workers-types': 4.20260317.1 - '@design.estate/dees-catalog': 3.55.5(@tiptap/pm@2.27.2) + '@design.estate/dees-catalog': 3.55.6(@tiptap/pm@2.27.2) '@design.estate/dees-comms': 1.0.30 '@push.rocks/lik': 6.4.0 '@push.rocks/smartdelay': 3.0.5 @@ -4884,7 +4884,7 @@ snapshots: dependencies: '@api.global/typedrequest-interfaces': 3.0.19 - '@design.estate/dees-catalog@3.55.5(@tiptap/pm@2.27.2)': + '@design.estate/dees-catalog@3.55.6(@tiptap/pm@2.27.2)': dependencies: '@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-element': 2.2.4 @@ -6922,9 +6922,9 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 - '@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)': + '@serve.zone/catalog@2.11.1(@tiptap/pm@2.27.2)': dependencies: - '@design.estate/dees-catalog': 3.55.5(@tiptap/pm@2.27.2) + '@design.estate/dees-catalog': 3.55.6(@tiptap/pm@2.27.2) '@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-element': 2.2.4 '@design.estate/dees-wcctools': 3.8.0 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index d904dba..aafa914 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: '12.9.4', + version: '12.10.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/config/classes.route-config-manager.ts b/ts/config/classes.route-config-manager.ts index 2382224..023fa3a 100644 --- a/ts/config/classes.route-config-manager.ts +++ b/ts/config/classes.route-config-manager.ts @@ -132,7 +132,18 @@ export class RouteConfigManager { if (!stored) return false; if (patch.route) { - stored.route = { ...stored.route, ...patch.route } as IDcRouterRouteConfig; + const mergedAction = patch.route.action + ? { ...stored.route.action, ...patch.route.action } + : stored.route.action; + // Handle explicit null to remove nested action properties (e.g., tls: null) + if (patch.route.action) { + for (const [key, val] of Object.entries(patch.route.action)) { + if (val === null) { + delete (mergedAction as any)[key]; + } + } + } + stored.route = { ...stored.route, ...patch.route, action: mergedAction } as IDcRouterRouteConfig; } if (patch.enabled !== undefined) { stored.enabled = patch.enabled; diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index d904dba..aafa914 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: '12.9.4', + version: '12.10.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/elements/ops-view-routes.ts b/ts_web/elements/ops-view-routes.ts index 46d5019..4b9f608 100644 --- a/ts_web/elements/ops-view-routes.ts +++ b/ts_web/elements/ops-view-routes.ts @@ -13,6 +13,40 @@ import { type TemplateResult, } from '@design.estate/dees-element'; +// TLS dropdown options shared by create and edit dialogs +const tlsModeOptions = [ + { key: 'none', option: '(none — no TLS)' }, + { key: 'passthrough', option: 'Passthrough' }, + { key: 'terminate', option: 'Terminate' }, + { key: 'terminate-and-reencrypt', option: 'Terminate & Re-encrypt' }, +]; +const tlsCertOptions = [ + { key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' }, + { key: 'custom', option: 'Custom certificate' }, +]; + +/** + * Toggle TLS form field visibility based on selected TLS mode and certificate type. + */ +function setupTlsVisibility(formEl: any) { + const updateVisibility = async () => { + const data = await formEl.collectFormData(); + const contentEl = formEl.closest('.content') || formEl.parentElement; + if (!contentEl) return; + const tlsModeValue = data.tlsMode; + const modeKey = typeof tlsModeValue === 'string' ? tlsModeValue : tlsModeValue?.key; + const needsCert = modeKey === 'terminate' || modeKey === 'terminate-and-reencrypt'; + const certGroup = contentEl.querySelector('.tlsCertificateGroup') as HTMLElement; + if (certGroup) certGroup.style.display = needsCert ? 'flex' : 'none'; + const tlsCertValue = data.tlsCertificate; + const certKey = typeof tlsCertValue === 'string' ? tlsCertValue : tlsCertValue?.key; + const customGroup = contentEl.querySelector('.tlsCustomCertGroup') as HTMLElement; + if (customGroup) customGroup.style.display = (needsCert && certKey === 'custom') ? 'flex' : 'none'; + }; + formEl.changeSubject.subscribe(() => updateVisibility()); + updateVisibility(); +} + @customElement('ops-view-routes') export class OpsViewRoutes extends DeesElement { @state() accessor routeState: appstate.IRouteManagementState = { @@ -423,7 +457,18 @@ export class OpsViewRoutes extends DeesElement { : ''; const currentTargetPort = firstTarget?.port != null ? String(firstTarget.port) : ''; - await DeesModal.createAndShow({ + // Compute current TLS state for pre-population + const currentTls = (route.action as any).tls; + const currentTlsMode = currentTls?.mode || 'none'; + const currentTlsCert = currentTls + ? (currentTls.certificate === 'auto' || !currentTls.certificate ? 'auto' : 'custom') + : 'auto'; + const currentCustomKey = (typeof currentTls?.certificate === 'object') ? currentTls.certificate.key : ''; + const currentCustomCert = (typeof currentTls?.certificate === 'object') ? currentTls.certificate.cert : ''; + const needsCert = currentTlsMode === 'terminate' || currentTlsMode === 'terminate-and-reencrypt'; + const isCustom = currentTlsCert === 'custom'; + + const editModal = await DeesModal.createAndShow({ heading: `Edit Route: ${route.name}`, content: html` @@ -435,6 +480,14 @@ export class OpsViewRoutes extends DeesElement { o.key === (merged.metadata?.networkTargetRef || '')) || null}> + o.key === currentTlsMode) || tlsModeOptions[0]}> +
+ o.key === currentTlsCert) || tlsCertOptions[0]}> +
+ + +
+
`, menuOptions: [ @@ -476,6 +529,25 @@ export class OpsViewRoutes extends DeesElement { ...(priority != null && !isNaN(priority) ? { priority } : {}), }; + // Build TLS config from form + const tlsModeValue = formData.tlsMode as any; + const tlsModeKey = typeof tlsModeValue === 'string' ? tlsModeValue : tlsModeValue?.key; + if (tlsModeKey && tlsModeKey !== 'none') { + const tls: any = { mode: tlsModeKey }; + if (tlsModeKey !== 'passthrough') { + const tlsCertValue = formData.tlsCertificate as any; + const tlsCertKey = typeof tlsCertValue === 'string' ? tlsCertValue : tlsCertValue?.key; + if (tlsCertKey === 'custom' && formData.tlsCertKey && formData.tlsCertCert) { + tls.certificate = { key: formData.tlsCertKey, cert: formData.tlsCertCert }; + } else { + tls.certificate = 'auto'; + } + } + updatedRoute.action.tls = tls; + } else { + updatedRoute.action.tls = null; // explicit removal + } + const metadata: any = {}; const profileRefValue = formData.securityProfileRef as any; const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key; @@ -501,6 +573,12 @@ export class OpsViewRoutes extends DeesElement { }, ], }); + // Setup conditional TLS field visibility after modal renders + const editForm = editModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any; + if (editForm) { + await editForm.updateComplete; + setupTlsVisibility(editForm); + } } private async showCreateRouteDialog() { @@ -524,7 +602,7 @@ export class OpsViewRoutes extends DeesElement { })), ]; - await DeesModal.createAndShow({ + const createModal = await DeesModal.createAndShow({ heading: 'Add Programmatic Route', content: html` @@ -536,6 +614,14 @@ export class OpsViewRoutes extends DeesElement { + + `, menuOptions: [ @@ -577,6 +663,23 @@ export class OpsViewRoutes extends DeesElement { ...(priority != null && !isNaN(priority) ? { priority } : {}), }; + // Build TLS config from form + const tlsModeValue = formData.tlsMode as any; + const tlsModeKey = typeof tlsModeValue === 'string' ? tlsModeValue : tlsModeValue?.key; + if (tlsModeKey && tlsModeKey !== 'none') { + const tls: any = { mode: tlsModeKey }; + if (tlsModeKey !== 'passthrough') { + const tlsCertValue = formData.tlsCertificate as any; + const tlsCertKey = typeof tlsCertValue === 'string' ? tlsCertValue : tlsCertValue?.key; + if (tlsCertKey === 'custom' && formData.tlsCertKey && formData.tlsCertCert) { + tls.certificate = { key: formData.tlsCertKey, cert: formData.tlsCertCert }; + } else { + tls.certificate = 'auto'; + } + } + route.action.tls = tls; + } + // Build metadata if profile/target selected const metadata: any = {}; const profileRefValue = formData.securityProfileRef as any; @@ -602,6 +705,12 @@ export class OpsViewRoutes extends DeesElement { }, ], }); + // Setup conditional TLS field visibility after modal renders + const createForm = createModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any; + if (createForm) { + await createForm.updateComplete; + setupTlsVisibility(createForm); + } } private refreshData() {