[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
147
domain/portForwardingAgentOps.ts
Normal file
147
domain/portForwardingAgentOps.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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 } };
|
||||
}
|
||||
Reference in New Issue
Block a user