feat(remote-ingress): support auto-deriving ports for remote ingress edges and expose manual/derived port breakdown in API and UI
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-17 - 6.8.0 - feat(remote-ingress)
|
||||
support auto-deriving ports for remote ingress edges and expose manual/derived port breakdown in API and UI
|
||||
|
||||
- Add autoDerivePorts flag to IRemoteIngress with default true and migration to set existing stored edges to autoDerivePorts = true
|
||||
- RemoteIngressManager: getEffectiveListenPorts now returns the union of manual + derived ports when autoDerivePorts is enabled; added getPortBreakdown to return manual vs derived lists
|
||||
- API handlers updated: create/update requests accept autoDerivePorts; responses now include effectiveListenPorts, manualPorts, and derivedPorts (secrets still masked)
|
||||
- Web UI updated: create and edit dialogs include an Auto-derive checkbox; port badges now visually distinguish manual vs derived ports; added updateRemoteIngressAction
|
||||
- Non-breaking change: new field defaults to true so existing behavior is preserved
|
||||
|
||||
## 2026-02-17 - 6.7.0 - feat(remote-ingress)
|
||||
Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '6.7.0',
|
||||
version: '6.8.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -20,12 +20,17 @@ export class RemoteIngressHandler {
|
||||
if (!manager) {
|
||||
return { edges: [] };
|
||||
}
|
||||
// Return edges without secrets, enriched with effective listen ports
|
||||
const edges = manager.getAllEdges().map((e) => ({
|
||||
...e,
|
||||
secret: '********', // Never expose secrets via API
|
||||
effectiveListenPorts: manager.getEffectiveListenPorts(e),
|
||||
}));
|
||||
// Return edges without secrets, enriched with effective listen ports and breakdown
|
||||
const edges = manager.getAllEdges().map((e) => {
|
||||
const breakdown = manager.getPortBreakdown(e);
|
||||
return {
|
||||
...e,
|
||||
secret: '********', // Never expose secrets via API
|
||||
effectiveListenPorts: manager.getEffectiveListenPorts(e),
|
||||
manualPorts: breakdown.manual,
|
||||
derivedPorts: breakdown.derived,
|
||||
};
|
||||
});
|
||||
return { edges };
|
||||
},
|
||||
),
|
||||
@@ -50,6 +55,7 @@ export class RemoteIngressHandler {
|
||||
dataArg.name,
|
||||
dataArg.listenPorts || [],
|
||||
dataArg.tags,
|
||||
dataArg.autoDerivePorts ?? true,
|
||||
);
|
||||
|
||||
// Sync allowed edges with the hub
|
||||
@@ -102,6 +108,7 @@ export class RemoteIngressHandler {
|
||||
const edge = await manager.updateEdge(dataArg.id, {
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
enabled: dataArg.enabled,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
@@ -115,7 +122,17 @@ export class RemoteIngressHandler {
|
||||
await tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
|
||||
return { success: true, edge: { ...edge, secret: '********' } };
|
||||
const breakdown = manager.getPortBreakdown(edge);
|
||||
return {
|
||||
success: true,
|
||||
edge: {
|
||||
...edge,
|
||||
secret: '********',
|
||||
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
|
||||
manualPorts: breakdown.manual,
|
||||
derivedPorts: breakdown.derived,
|
||||
},
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -47,6 +47,11 @@ export class RemoteIngressManager {
|
||||
for (const key of keys) {
|
||||
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
|
||||
if (edge) {
|
||||
// Migration: old edges without autoDerivePorts default to true
|
||||
if ((edge as any).autoDerivePorts === undefined) {
|
||||
edge.autoDerivePorts = true;
|
||||
await this.storageManager.setJSON(key, edge);
|
||||
}
|
||||
this.edges.set(edge.id, edge);
|
||||
}
|
||||
}
|
||||
@@ -91,13 +96,28 @@ export class RemoteIngressManager {
|
||||
|
||||
/**
|
||||
* Get the effective listen ports for an edge.
|
||||
* Returns manual listenPorts if non-empty, otherwise derives ports from tagged routes.
|
||||
* Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true.
|
||||
*/
|
||||
public getEffectiveListenPorts(edge: IRemoteIngress): number[] {
|
||||
if (edge.listenPorts && edge.listenPorts.length > 0) {
|
||||
return edge.listenPorts;
|
||||
}
|
||||
return this.derivePortsForEdge(edge.id, edge.tags);
|
||||
const manualPorts = edge.listenPorts || [];
|
||||
const shouldDerive = edge.autoDerivePorts !== false;
|
||||
if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b);
|
||||
const derivedPorts = this.derivePortsForEdge(edge.id, edge.tags);
|
||||
return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manual and derived port breakdown for an edge (used in API responses).
|
||||
* Derived ports exclude any ports already present in the manual list.
|
||||
*/
|
||||
public getPortBreakdown(edge: IRemoteIngress): { manual: number[]; derived: number[] } {
|
||||
const manual = edge.listenPorts || [];
|
||||
const shouldDerive = edge.autoDerivePorts !== false;
|
||||
if (!shouldDerive) return { manual, derived: [] };
|
||||
const manualSet = new Set(manual);
|
||||
const allDerived = this.derivePortsForEdge(edge.id, edge.tags);
|
||||
const derived = allDerived.filter((p) => !manualSet.has(p));
|
||||
return { manual, derived };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,6 +127,7 @@ export class RemoteIngressManager {
|
||||
name: string,
|
||||
listenPorts: number[] = [],
|
||||
tags?: string[],
|
||||
autoDerivePorts: boolean = true,
|
||||
): Promise<IRemoteIngress> {
|
||||
const id = plugins.uuid.v4();
|
||||
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
||||
@@ -118,6 +139,7 @@ export class RemoteIngressManager {
|
||||
secret,
|
||||
listenPorts,
|
||||
enabled: true,
|
||||
autoDerivePorts,
|
||||
tags: tags || [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
@@ -150,6 +172,7 @@ export class RemoteIngressManager {
|
||||
updates: {
|
||||
name?: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
enabled?: boolean;
|
||||
tags?: string[];
|
||||
},
|
||||
@@ -161,6 +184,7 @@ export class RemoteIngressManager {
|
||||
|
||||
if (updates.name !== undefined) edge.name = updates.name;
|
||||
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
||||
if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts;
|
||||
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
||||
if (updates.tags !== undefined) edge.tags = updates.tags;
|
||||
edge.updatedAt = Date.now();
|
||||
|
||||
@@ -9,11 +9,17 @@ export interface IRemoteIngress {
|
||||
secret: string;
|
||||
listenPorts: number[];
|
||||
enabled: boolean;
|
||||
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
|
||||
autoDerivePorts: boolean;
|
||||
tags?: string[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
/** Effective ports derived from route configs — only present in API responses. */
|
||||
/** Effective ports (union of manual + derived) — only present in API responses. */
|
||||
effectiveListenPorts?: number[];
|
||||
/** Ports explicitly set by the user — only present in API responses. */
|
||||
manualPorts?: number[];
|
||||
/** Ports auto-derived from route configs — only present in API responses. */
|
||||
derivedPorts?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
|
||||
identity?: authInterfaces.IIdentity;
|
||||
name: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
tags?: string[];
|
||||
};
|
||||
response: {
|
||||
@@ -57,6 +58,7 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
|
||||
id: string;
|
||||
name?: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
enabled?: boolean;
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '6.7.0',
|
||||
version: '6.8.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -822,6 +822,7 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
||||
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
@@ -836,6 +837,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
identity: context.identity,
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
@@ -883,6 +885,40 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
|
||||
}
|
||||
);
|
||||
|
||||
export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
id: string;
|
||||
name?: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_UpdateRemoteIngress
|
||||
>('/typedrequest', 'updateRemoteIngress');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
id: dataArg.id,
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to update edge',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
||||
async (statePartArg, edgeId) => {
|
||||
const context = getActionContext();
|
||||
|
||||
@@ -114,6 +114,17 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
||||
}
|
||||
|
||||
.portBadge.manual {
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
||||
}
|
||||
|
||||
.portBadge.derived {
|
||||
background: ${cssManager.bdTheme('#ecfdf5', '#022c22')};
|
||||
color: ${cssManager.bdTheme('#047857', '#34d399')};
|
||||
border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -203,7 +214,8 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated, auto-derived if empty)'}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Additional Manual Ports (comma-separated, optional)'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
@@ -226,12 +238,13 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
const listenPorts = portsStr
|
||||
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||
: undefined;
|
||||
const autoDerivePorts = formData.autoDerivePorts !== false;
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.createRemoteIngressAction,
|
||||
{ name, listenPorts, tags },
|
||||
{ name, listenPorts, autoDerivePorts, tags },
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
@@ -266,6 +279,61 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'lucide:pencil',
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
await DeesModal.createAndShow({
|
||||
heading: `Edit Edge: ${edge.name}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .value=${edge.name}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports (comma-separated)'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated)'} .value=${(edge.tags || []).join(', ')}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:x',
|
||||
action: async (modalArg: any) => await modalArg.destroy(),
|
||||
},
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: 'lucide:check',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
const portsStr = formData.listenPorts?.trim();
|
||||
const listenPorts = portsStr
|
||||
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||
: [];
|
||||
const autoDerivePorts = formData.autoDerivePorts !== false;
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.updateRemoteIngressAction,
|
||||
{
|
||||
id: edge.id,
|
||||
name: formData.name || edge.name,
|
||||
listenPorts,
|
||||
autoDerivePorts,
|
||||
tags,
|
||||
},
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Regenerate Secret',
|
||||
iconName: 'lucide:key',
|
||||
@@ -317,13 +385,12 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
}
|
||||
|
||||
private getPortsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
|
||||
const hasManualPorts = edge.listenPorts && edge.listenPorts.length > 0;
|
||||
const ports = hasManualPorts ? edge.listenPorts : (edge.effectiveListenPorts || []);
|
||||
const isAuto = !hasManualPorts && ports.length > 0;
|
||||
if (ports.length === 0) {
|
||||
const manualPorts = edge.manualPorts || [];
|
||||
const derivedPorts = edge.derivedPorts || [];
|
||||
if (manualPorts.length === 0 && derivedPorts.length === 0) {
|
||||
return html`<span style="color: var(--text-muted, #6b7280); font-size: 12px;">none</span>`;
|
||||
}
|
||||
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}${isAuto ? html`<span style="font-size: 11px; color: var(--text-muted, #6b7280); align-self: center;">(auto)</span>` : ''}</div>`;
|
||||
return html`<div class="portsDisplay">${manualPorts.map(p => html`<span class="portBadge manual">${p}</span>`)}${derivedPorts.map(p => html`<span class="portBadge derived">${p}</span>`)}${derivedPorts.length > 0 ? html`<span style="font-size: 11px; color: var(--text-muted, #6b7280); align-self: center;">(auto)</span>` : ''}</div>`;
|
||||
}
|
||||
|
||||
private getEdgeTunnelCount(edgeId: string): number {
|
||||
|
||||
Reference in New Issue
Block a user