Files
NetMesh/domain/pluginConnection.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

167 lines
6.2 KiB
TypeScript

import type {
BuiltInHostProtocol,
Host,
HostProtocol,
PluginConfigurationValue,
PluginConnectionConfig,
} from './models';
import { isEncryptedCredentialPlaceholder } from './credentials';
const CONTRIBUTION_ID = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+\.[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-])*$/u;
const MAX_CONTRIBUTION_ID_LENGTH = 192;
const PLUGIN_PROTOCOL_PREFIX = 'plugin:';
const MAX_CONFIGURATION_BYTES = 128 * 1024;
const MAX_CONFIGURATION_DEPTH = 32;
const MAX_CONFIGURATION_ENTRIES = 4096;
const BUILT_IN_HOST_PROTOCOLS = new Set<BuiltInHostProtocol>([
'ssh',
'telnet',
'mosh',
'et',
'local',
'serial',
]);
export const pluginProtocolForProvider = (providerId: string): HostProtocol => `plugin:${providerId}`;
export const isPluginHostProtocol = (protocol?: string): protocol is `plugin:${string}` => (
typeof protocol === 'string'
&& protocol.startsWith(PLUGIN_PROTOCOL_PREFIX)
&& protocol.length <= PLUGIN_PROTOCOL_PREFIX.length + MAX_CONTRIBUTION_ID_LENGTH
&& CONTRIBUTION_ID.test(protocol.slice(PLUGIN_PROTOCOL_PREFIX.length))
);
export const isBuiltInHostProtocol = (protocol?: string): protocol is BuiltInHostProtocol => (
typeof protocol === 'string' && BUILT_IN_HOST_PROTOCOLS.has(protocol as BuiltInHostProtocol)
);
export const isPluginCredentialCatalogEntryAvailable = (
id: string,
value: string | undefined,
catalogIds: ReadonlySet<string>,
): value is string => (
catalogIds.has(id)
&& typeof value === 'string'
&& value.length > 0
&& !isEncryptedCredentialPlaceholder(value)
&& new TextEncoder().encode(value).byteLength <= 64 * 1024
);
export const isSafePluginAuthenticationUrl = (value: string): boolean => {
try {
const url = new URL(value);
if (url.username || url.password) return false;
if (url.protocol === 'https:') return true;
if (url.protocol !== 'http:') return false;
const hostname = url.hostname.toLowerCase();
return hostname === 'localhost'
|| hostname === '[::1]'
|| /^127(?:\.\d{1,3}){3}$/u.test(hostname);
} catch {
return false;
}
};
function validateConfiguration(value: unknown, depth: number, budget: { entries: number }): value is PluginConfigurationValue {
if (depth > MAX_CONFIGURATION_DEPTH || ++budget.entries > MAX_CONFIGURATION_ENTRIES) return false;
if (value === null || typeof value === 'string' || typeof value === 'boolean') return true;
if (typeof value === 'number') return Number.isFinite(value);
if (Array.isArray(value)) return value.every((item) => validateConfiguration(item, depth + 1, budget));
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) return false;
return Object.entries(value).every(([key, item]) => (
key.length > 0
&& key.length <= 256
&& key !== '__proto__'
&& key !== 'prototype'
&& key !== 'constructor'
&& validateConfiguration(item, depth + 1, budget)
));
}
export function sanitizePluginConnection(
value: unknown,
protocol?: string,
): PluginConnectionConfig | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
const candidate = value as Partial<PluginConnectionConfig>;
if (typeof candidate.providerId !== 'string' || !CONTRIBUTION_ID.test(candidate.providerId)
|| candidate.providerId.length > MAX_CONTRIBUTION_ID_LENGTH
|| protocol !== pluginProtocolForProvider(candidate.providerId)
|| !validateConfiguration(candidate.configuration, 0, { entries: 0 })) {
return undefined;
}
const serialized = JSON.stringify(candidate.configuration);
if (new TextEncoder().encode(serialized).byteLength > MAX_CONFIGURATION_BYTES) return undefined;
const authenticationProviderId = typeof candidate.authenticationProviderId === 'string'
&& CONTRIBUTION_ID.test(candidate.authenticationProviderId)
&& candidate.authenticationProviderId.length <= 192
? candidate.authenticationProviderId
: undefined;
const credentialId = typeof candidate.credentialId === 'string'
&& candidate.credentialId.length >= 16
&& candidate.credentialId.length <= 256
&& !candidate.credentialId.includes('\0')
? candidate.credentialId
: undefined;
return {
providerId: candidate.providerId,
configuration: structuredClone(candidate.configuration),
...(authenticationProviderId ? { authenticationProviderId } : {}),
...(credentialId ? { credentialId } : {}),
};
}
/**
* Plugin Providers own their connection and authentication configuration.
* Remove hidden first-party protocol state so switching an existing host to a
* plugin cannot retain credentials or transport behavior that the plugin UI
* cannot display or manage.
*/
export function stripBuiltInConnectionFieldsForPluginHost(host: Host): Host {
if (!isPluginHostProtocol(host.protocol)) return host;
const {
port: _port,
identityId: _identityId,
identityFileId: _identityFileId,
identityFilePaths: _identityFilePaths,
password: _password,
savePassword: _savePassword,
authMethod: _authMethod,
authPolicyVersion: _authPolicyVersion,
requiresMfa: _requiresMfa,
useSshAgent: _useSshAgent,
identityAgent: _identityAgent,
identitiesOnly: _identitiesOnly,
addKeysToAgent: _addKeysToAgent,
useKeychain: _useKeychain,
agentForwarding: _agentForwarding,
x11Forwarding: _x11Forwarding,
proxy: _proxy,
proxyProfileId: _proxyProfileId,
proxyConfig: _proxyConfig,
hostChain: _hostChain,
moshEnabled: _moshEnabled,
moshServerPath: _moshServerPath,
etEnabled: _etEnabled,
etPort: _etPort,
protocols: _protocols,
telnetPort: _telnetPort,
telnetEnabled: _telnetEnabled,
telnetIdentityId: _telnetIdentityId,
telnetUsername: _telnetUsername,
telnetPassword: _telnetPassword,
serialConfig: _serialConfig,
sftpSudo: _sftpSudo,
legacyAlgorithms: _legacyAlgorithms,
skipEcdsaHostKey: _skipEcdsaHostKey,
algorithms: _algorithms,
keepaliveInterval: _keepaliveInterval,
keepaliveCountMax: _keepaliveCountMax,
keepaliveOverride: _keepaliveOverride,
sshTcpConnectTimeoutSeconds: _sshTcpConnectTimeoutSeconds,
sshAuthReadyTimeoutSeconds: _sshAuthReadyTimeoutSeconds,
...pluginHost
} = host;
return pluginHost;
}