Files
NetMesh/domain/portForwardingAgentOps.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

148 lines
6.0 KiB
TypeScript

import type { Host, PortForwardingRule } from './models';
import { getNextVaultOrder } from './vaultOrder';
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
type NewRuleValues = { id: string; now: number };
export const hasPortForwardingConnectionChanged = (
existing: PortForwardingRule,
updated: PortForwardingRule,
): boolean => (
existing.type !== updated.type
|| existing.localPort !== updated.localPort
|| existing.remoteHost !== updated.remoteHost
|| existing.remotePort !== updated.remotePort
|| existing.bindAddress !== updated.bindAddress
|| existing.hostId !== updated.hostId
);
const parsePort = (value: unknown, name: string): Result<number> => {
const parsed = typeof value === 'number' ? value : Number(String(value ?? ''));
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535
? { ok: true, value: parsed }
: { ok: false, error: `${name} must be an integer between 1 and 65535.` };
};
export const validatePortForwardingHost = (
hosts: Host[],
hostId: string | undefined,
): Result<Host> => {
const host = hosts.find((candidate) => candidate.id === hostId);
if (!hostId || !host) {
return { ok: false, error: `Host "${hostId || ''}" was not found.` };
}
if (host.protocol && host.protocol !== 'ssh') {
return { ok: false, error: `Host "${hostId}" does not support port forwarding.` };
}
return { ok: true, value: host };
};
function buildRule(
source: Record<string, unknown>,
hosts: Host[],
existing?: PortForwardingRule,
newRule?: NewRuleValues,
): Result<PortForwardingRule> {
const type = source.type === undefined ? existing?.type : String(source.type).trim();
if (type !== 'local' && type !== 'remote' && type !== 'dynamic') {
return { ok: false, error: 'type must be local, remote, or dynamic.' };
}
const localPort = parsePort(source.localPort ?? existing?.localPort, 'localPort');
if ('error' in localPort) return { ok: false, error: localPort.error };
const hostId = source.hostId === undefined ? existing?.hostId : String(source.hostId).trim();
const validatedHost = validatePortForwardingHost(hosts, hostId);
if (!validatedHost.ok) return validatedHost;
const remoteHost = source.remoteHost === undefined ? existing?.remoteHost : String(source.remoteHost).trim();
let remotePort: number | undefined;
if (type !== 'dynamic') {
if (!remoteHost) return { ok: false, error: 'remoteHost is required for local and remote forwarding.' };
const parsedRemotePort = parsePort(source.remotePort ?? existing?.remotePort, 'remotePort');
if ('error' in parsedRemotePort) return { ok: false, error: parsedRemotePort.error };
remotePort = parsedRemotePort.value;
}
if (!existing && !newRule) return { ok: false, error: 'New rule id and timestamp are required.' };
let autoStart = existing?.autoStart ?? false;
if (source.autoStart !== undefined) {
if (typeof source.autoStart === 'boolean') autoStart = source.autoStart;
else {
const normalized = String(source.autoStart).trim().toLowerCase();
if (['true', '1', 'yes'].includes(normalized)) autoStart = true;
else if (['false', '0', 'no'].includes(normalized)) autoStart = false;
else return { ok: false, error: 'autoStart must be true or false.' };
}
}
return {
ok: true,
value: {
id: existing?.id ?? newRule!.id,
label: String(source.label ?? existing?.label ?? `${type} ${localPort.value}`).trim() || `${type} ${localPort.value}`,
type,
localPort: localPort.value,
bindAddress: String(source.bindAddress ?? existing?.bindAddress ?? '127.0.0.1').trim() || '127.0.0.1',
...(type === 'dynamic' ? {} : { remoteHost, remotePort }),
hostId,
autoStart,
status: existing?.status ?? 'inactive',
error: existing?.error,
lastUsedAt: existing?.lastUsedAt,
createdAt: existing?.createdAt ?? newRule!.now,
order: existing?.order,
},
};
}
export function createPortForwardingRule(
rules: PortForwardingRule[],
hosts: Host[],
source: Record<string, unknown>,
newRule: NewRuleValues,
): Result<{ rules: PortForwardingRule[]; rule: PortForwardingRule }> {
const built = buildRule(source, hosts, undefined, newRule);
if ('error' in built) return { ok: false, error: built.error };
const rule = { ...built.value, order: getNextVaultOrder(rules) };
return { ok: true, value: { rules: [...rules, rule], rule } };
}
export function updatePortForwardingRule(
rules: PortForwardingRule[],
hosts: Host[],
ruleId: string,
source: Record<string, unknown>,
): Result<{ rules: PortForwardingRule[]; rule: PortForwardingRule }> {
const existing = rules.find((rule) => rule.id === ruleId);
if (!existing) return { ok: false, error: `Port forwarding rule "${ruleId}" was not found.` };
const built = buildRule(source, hosts, existing);
if ('error' in built) return { ok: false, error: built.error };
const connectionChanged = hasPortForwardingConnectionChanged(existing, built.value);
const updatedRule = connectionChanged
? { ...built.value, status: 'inactive' as const, error: undefined }
: built.value;
return {
ok: true,
value: { rules: rules.map((rule) => rule.id === ruleId ? updatedRule : rule), rule: updatedRule },
};
}
export function duplicatePortForwardingRule(
rules: PortForwardingRule[],
hosts: Host[],
ruleId: string,
newRule: NewRuleValues,
): Result<{ rules: PortForwardingRule[]; rule: PortForwardingRule }> {
const existing = rules.find((rule) => rule.id === ruleId);
if (!existing) return { ok: false, error: `Port forwarding rule "${ruleId}" was not found.` };
const validated = buildRule({}, hosts, existing);
if ('error' in validated) return { ok: false, error: validated.error };
const rule: PortForwardingRule = {
...validated.value,
id: newRule.id,
label: `${existing.label} (Copy)`,
status: 'inactive',
error: undefined,
lastUsedAt: undefined,
createdAt: newRule.now,
order: getNextVaultOrder(rules),
};
return { ok: true, value: { rules: [...rules, rule], rule } };
}