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
464 lines
13 KiB
JavaScript
464 lines
13 KiB
JavaScript
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const {
|
|
parsePuttyCommandLine,
|
|
redactPuttyCommandLinePasswords,
|
|
} = require("./puttyCommandLine.cjs");
|
|
|
|
const SSH_DEEP_LINK_CHANNEL = "netcatty:deepLink:ssh";
|
|
const TELNET_DEEP_LINK_CHANNEL = "netcatty:deepLink:telnet";
|
|
const JMS_DEEP_LINK_CHANNEL = "netcatty:deepLink:jms";
|
|
const SSH_PROTOCOL = "ssh";
|
|
const TELNET_PROTOCOL = "telnet";
|
|
const JMS_PROTOCOL = "jms";
|
|
const SSH_DEEP_LINK_PREFERENCES_FILE = "ssh-deep-link-preferences.json";
|
|
const JMS_DEEP_LINK_PREFERENCES_FILE = "jms-deep-link-preferences.json";
|
|
|
|
function isDeepLinkUrl(rawUrl, protocol) {
|
|
if (typeof rawUrl !== "string" || !rawUrl.trim()) return false;
|
|
return rawUrl.trim().toLowerCase().startsWith(`${protocol}://`);
|
|
}
|
|
|
|
function collectDeepLinkUrls(argv, protocol) {
|
|
if (!Array.isArray(argv)) return [];
|
|
return argv.filter((rawUrl) => isDeepLinkUrl(rawUrl, protocol));
|
|
}
|
|
|
|
function isSshDeepLinkUrl(rawUrl) {
|
|
return isDeepLinkUrl(rawUrl, SSH_PROTOCOL);
|
|
}
|
|
|
|
function isTelnetDeepLinkUrl(rawUrl) {
|
|
return isDeepLinkUrl(rawUrl, TELNET_PROTOCOL);
|
|
}
|
|
|
|
function isJmsDeepLinkUrl(rawUrl) {
|
|
return isDeepLinkUrl(rawUrl, JMS_PROTOCOL);
|
|
}
|
|
|
|
function collectSshDeepLinkUrls(argv) {
|
|
return collectDeepLinkUrls(argv, SSH_PROTOCOL);
|
|
}
|
|
|
|
function collectTelnetDeepLinkUrls(argv) {
|
|
return collectDeepLinkUrls(argv, TELNET_PROTOCOL);
|
|
}
|
|
|
|
function collectJmsDeepLinkUrls(argv) {
|
|
return collectDeepLinkUrls(argv, JMS_PROTOCOL);
|
|
}
|
|
|
|
function collectPuttyStyleDeepLinkUrls(argv) {
|
|
if (
|
|
collectSshDeepLinkUrls(argv).length > 0
|
|
|| collectTelnetDeepLinkUrls(argv).length > 0
|
|
|| collectJmsDeepLinkUrls(argv).length > 0
|
|
) {
|
|
return { ssh: [], telnet: [] };
|
|
}
|
|
|
|
const parsed = parsePuttyCommandLine(argv);
|
|
if (!parsed?.url) return { ssh: [], telnet: [] };
|
|
if (parsed.protocol === TELNET_PROTOCOL) {
|
|
return { ssh: [], telnet: [parsed.url] };
|
|
}
|
|
return { ssh: [parsed.url], telnet: [] };
|
|
}
|
|
|
|
/**
|
|
* Build the pending-queue items for ssh/telnet connections requested by the
|
|
* current launch arguments. PuTTY-style CLI args (-ssh user@host -P 22 -pw
|
|
* pass) are an explicit launch method and must connect regardless of whether
|
|
* Netcatty is the registered ssh:// protocol client, so those items are only
|
|
* gated by includeSchemeUrls through the scheme-URL part of the queue.
|
|
*/
|
|
function collectSshDeepLinkQueueItems(argv, { includeSchemeUrls = true } = {}) {
|
|
const puttyStyleDeepLinks = collectPuttyStyleDeepLinkUrls(argv);
|
|
const queueItems = {
|
|
ssh: [],
|
|
telnet: [],
|
|
};
|
|
if (includeSchemeUrls) {
|
|
collectSshDeepLinkUrls(argv).forEach((rawUrl) => {
|
|
queueItems.ssh.push({ rawUrl, viaCommandLine: false });
|
|
});
|
|
collectTelnetDeepLinkUrls(argv).forEach((rawUrl) => {
|
|
queueItems.telnet.push({ rawUrl, viaCommandLine: false });
|
|
});
|
|
}
|
|
puttyStyleDeepLinks.ssh.forEach((rawUrl) => {
|
|
queueItems.ssh.push({ rawUrl, viaCommandLine: true });
|
|
});
|
|
puttyStyleDeepLinks.telnet.forEach((rawUrl) => {
|
|
queueItems.telnet.push({ rawUrl, viaCommandLine: true });
|
|
});
|
|
return queueItems;
|
|
}
|
|
|
|
function registerProtocolClient({
|
|
app,
|
|
protocol,
|
|
isDev,
|
|
argv = process.argv,
|
|
execPath = process.execPath,
|
|
logWarn = console.warn,
|
|
} = {}) {
|
|
if (!app || typeof app.setAsDefaultProtocolClient !== "function") return false;
|
|
try {
|
|
if (isDev && process.defaultApp && argv[1]) {
|
|
return app.setAsDefaultProtocolClient(protocol, execPath, [argv[1]]);
|
|
}
|
|
return app.setAsDefaultProtocolClient(protocol);
|
|
} catch (err) {
|
|
logWarn(`[Main] Failed to register ${protocol}:// protocol handler:`, err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function removeProtocolClient({
|
|
app,
|
|
protocol,
|
|
isDev,
|
|
argv = process.argv,
|
|
execPath = process.execPath,
|
|
logWarn = console.warn,
|
|
} = {}) {
|
|
if (!app || typeof app.removeAsDefaultProtocolClient !== "function") return false;
|
|
try {
|
|
if (isDev && process.defaultApp && argv[1]) {
|
|
return app.removeAsDefaultProtocolClient(protocol, execPath, [argv[1]]);
|
|
}
|
|
return app.removeAsDefaultProtocolClient(protocol);
|
|
} catch (err) {
|
|
logWarn(`[Main] Failed to remove ${protocol}:// protocol handler:`, err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function registerSshProtocolClient(options = {}) {
|
|
return registerProtocolClient({ ...options, protocol: SSH_PROTOCOL });
|
|
}
|
|
|
|
function registerTelnetProtocolClient(options = {}) {
|
|
return registerProtocolClient({ ...options, protocol: TELNET_PROTOCOL });
|
|
}
|
|
|
|
function removeSshProtocolClient(options = {}) {
|
|
return removeProtocolClient({ ...options, protocol: SSH_PROTOCOL });
|
|
}
|
|
|
|
function removeTelnetProtocolClient(options = {}) {
|
|
return removeProtocolClient({ ...options, protocol: TELNET_PROTOCOL });
|
|
}
|
|
|
|
function registerJmsProtocolClient(options = {}) {
|
|
return registerProtocolClient({ ...options, protocol: JMS_PROTOCOL });
|
|
}
|
|
|
|
function removeJmsProtocolClient(options = {}) {
|
|
return removeProtocolClient({ ...options, protocol: JMS_PROTOCOL });
|
|
}
|
|
|
|
function getDeepLinkPreferencePath({ app, preferencesFile, pathModule = path } = {}) {
|
|
if (!app || typeof app.getPath !== "function") return null;
|
|
try {
|
|
return pathModule.join(app.getPath("userData"), preferencesFile);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getSshDeepLinkPreferencePath(options = {}) {
|
|
return getDeepLinkPreferencePath({
|
|
...options,
|
|
preferencesFile: SSH_DEEP_LINK_PREFERENCES_FILE,
|
|
});
|
|
}
|
|
|
|
function getJmsDeepLinkPreferencePath(options = {}) {
|
|
return getDeepLinkPreferencePath({
|
|
...options,
|
|
preferencesFile: JMS_DEEP_LINK_PREFERENCES_FILE,
|
|
});
|
|
}
|
|
|
|
function readDeepLinkEnabledPreference({
|
|
app,
|
|
preferencesFile,
|
|
defaultWhenMissing,
|
|
parseEnabled,
|
|
fsModule = fs,
|
|
pathModule = path,
|
|
logWarn = console.warn,
|
|
logLabel,
|
|
} = {}) {
|
|
const filePath = getDeepLinkPreferencePath({ app, preferencesFile, pathModule });
|
|
if (!filePath) return defaultWhenMissing;
|
|
try {
|
|
if (!fsModule.existsSync(filePath)) return defaultWhenMissing;
|
|
const parsed = JSON.parse(fsModule.readFileSync(filePath, "utf8"));
|
|
return parseEnabled(parsed);
|
|
} catch (err) {
|
|
logWarn(`[Main] Failed to read ${logLabel} deep link preference:`, err);
|
|
return defaultWhenMissing;
|
|
}
|
|
}
|
|
|
|
function readSshDeepLinkEnabledPreference(options = {}) {
|
|
return readDeepLinkEnabledPreference({
|
|
...options,
|
|
preferencesFile: SSH_DEEP_LINK_PREFERENCES_FILE,
|
|
defaultWhenMissing: true,
|
|
parseEnabled: (parsed) => parsed?.enabled !== false,
|
|
logLabel: "ssh://",
|
|
});
|
|
}
|
|
|
|
function readJmsDeepLinkEnabledPreference(options = {}) {
|
|
return readDeepLinkEnabledPreference({
|
|
...options,
|
|
preferencesFile: JMS_DEEP_LINK_PREFERENCES_FILE,
|
|
defaultWhenMissing: false,
|
|
parseEnabled: (parsed) => parsed?.enabled === true,
|
|
logLabel: "jms://",
|
|
});
|
|
}
|
|
|
|
function writeDeepLinkEnabledPreference({
|
|
app,
|
|
enabled,
|
|
preferencesFile,
|
|
fsModule = fs,
|
|
pathModule = path,
|
|
logWarn = console.warn,
|
|
logLabel,
|
|
} = {}) {
|
|
const filePath = getDeepLinkPreferencePath({ app, preferencesFile, pathModule });
|
|
if (!filePath) return false;
|
|
try {
|
|
fsModule.mkdirSync(pathModule.dirname(filePath), { recursive: true });
|
|
fsModule.writeFileSync(filePath, JSON.stringify({ enabled: enabled !== false }, null, 2));
|
|
return true;
|
|
} catch (err) {
|
|
logWarn(`[Main] Failed to write ${logLabel} deep link preference:`, err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function writeSshDeepLinkEnabledPreference(options = {}) {
|
|
return writeDeepLinkEnabledPreference({
|
|
...options,
|
|
preferencesFile: SSH_DEEP_LINK_PREFERENCES_FILE,
|
|
logLabel: "ssh://",
|
|
});
|
|
}
|
|
|
|
function writeJmsDeepLinkEnabledPreference(options = {}) {
|
|
return writeDeepLinkEnabledPreference({
|
|
...options,
|
|
preferencesFile: JMS_DEEP_LINK_PREFERENCES_FILE,
|
|
logLabel: "jms://",
|
|
});
|
|
}
|
|
|
|
function applyDeepLinkProtocolClientPreference({
|
|
enabled,
|
|
registerClient,
|
|
removeClient,
|
|
options = {},
|
|
} = {}) {
|
|
if (enabled === false) {
|
|
return removeClient(options);
|
|
}
|
|
return registerClient(options);
|
|
}
|
|
|
|
function applySshProtocolClientPreference(options = {}) {
|
|
const sshApplied = applyDeepLinkProtocolClientPreference({
|
|
enabled: options.enabled,
|
|
registerClient: registerSshProtocolClient,
|
|
removeClient: removeSshProtocolClient,
|
|
options,
|
|
});
|
|
const telnetApplied = applyDeepLinkProtocolClientPreference({
|
|
enabled: options.enabled,
|
|
registerClient: registerTelnetProtocolClient,
|
|
removeClient: removeTelnetProtocolClient,
|
|
options,
|
|
});
|
|
if (sshApplied !== true && telnetApplied === true) {
|
|
applyDeepLinkProtocolClientPreference({
|
|
enabled: options.enabled === false,
|
|
registerClient: registerTelnetProtocolClient,
|
|
removeClient: removeTelnetProtocolClient,
|
|
options,
|
|
});
|
|
}
|
|
return sshApplied === true;
|
|
}
|
|
|
|
function applyJmsProtocolClientPreference(options = {}) {
|
|
return applyDeepLinkProtocolClientPreference({
|
|
enabled: options.enabled,
|
|
registerClient: registerJmsProtocolClient,
|
|
removeClient: removeJmsProtocolClient,
|
|
options,
|
|
});
|
|
}
|
|
|
|
function updateDeepLinkEnabledPreference({
|
|
currentEnabled = true,
|
|
enabled = true,
|
|
applyPreference = () => false,
|
|
writePreference = () => false,
|
|
clearPending,
|
|
} = {}) {
|
|
const nextEnabled = enabled !== false;
|
|
if (nextEnabled === currentEnabled) {
|
|
return { enabled: currentEnabled, success: true };
|
|
}
|
|
|
|
const success = applyPreference(nextEnabled) === true;
|
|
if (!success) {
|
|
return { enabled: currentEnabled, success: false };
|
|
}
|
|
|
|
const writeSucceeded = writePreference(nextEnabled) === true;
|
|
if (!writeSucceeded) {
|
|
const rolledBack = applyPreference(currentEnabled) === true;
|
|
const finalEnabled = rolledBack ? currentEnabled : nextEnabled;
|
|
if (!finalEnabled) {
|
|
clearPending?.();
|
|
}
|
|
return { enabled: finalEnabled, success: false };
|
|
}
|
|
if (!nextEnabled) {
|
|
clearPending?.();
|
|
}
|
|
return { enabled: nextEnabled, success: true };
|
|
}
|
|
|
|
function updateSshDeepLinkEnabledPreference(options = {}) {
|
|
return updateDeepLinkEnabledPreference(options);
|
|
}
|
|
|
|
function updateJmsDeepLinkEnabledPreference(options = {}) {
|
|
return updateDeepLinkEnabledPreference(options);
|
|
}
|
|
|
|
function shouldDeliverDeepLink({ enabled = true, deliveryGeneration = 0, expectedGeneration = 0 } = {}) {
|
|
return enabled !== false && deliveryGeneration === expectedGeneration;
|
|
}
|
|
|
|
/**
|
|
* App Lock can suppress rendererReady until unlock. A short readiness timeout
|
|
* would shift the URL out of the pending queue and drop it if the user takes
|
|
* longer than that to unlock. Prefer waiting indefinitely for readiness (or
|
|
* window death); callers re-queue only on non-cancel failures.
|
|
*/
|
|
function getSshDeepLinkRendererReadyTimeoutMs({ isDev = false } = {}) {
|
|
void isDev;
|
|
return 0;
|
|
}
|
|
|
|
function shouldRequeueFailedSshDeepLinkDelivery({
|
|
enabled = true,
|
|
deliveryGeneration = 0,
|
|
expectedGeneration = 0,
|
|
result = null,
|
|
cancelReason = "ssh-deep-link-disabled",
|
|
} = {}) {
|
|
if (!shouldDeliverSshDeepLink({ enabled, deliveryGeneration, expectedGeneration })) {
|
|
return false;
|
|
}
|
|
if (!result || result.success === true) return false;
|
|
if (result.reason === cancelReason) return false;
|
|
return true;
|
|
}
|
|
|
|
function shouldDeliverSshDeepLink(options = {}) {
|
|
return shouldDeliverDeepLink(options);
|
|
}
|
|
|
|
function shouldDeliverTelnetDeepLink(options = {}) {
|
|
return shouldDeliverDeepLink(options);
|
|
}
|
|
|
|
function shouldDeliverJmsDeepLink(options = {}) {
|
|
return shouldDeliverDeepLink(options);
|
|
}
|
|
|
|
function applyInitialDeepLinkPreference({
|
|
enabled = true,
|
|
applyPreference = () => false,
|
|
clearPending,
|
|
logWarn = console.warn,
|
|
logLabel,
|
|
warnOnFailure = true,
|
|
} = {}) {
|
|
const requestedEnabled = enabled !== false;
|
|
const success = applyPreference(requestedEnabled) === true;
|
|
if (success) {
|
|
return { enabled: requestedEnabled, success: true };
|
|
}
|
|
if (warnOnFailure && requestedEnabled) {
|
|
logWarn(`[Main] Failed to apply saved ${logLabel} deep link preference.`);
|
|
}
|
|
if (requestedEnabled) {
|
|
clearPending?.();
|
|
}
|
|
return { enabled: false, success: false };
|
|
}
|
|
|
|
function applyInitialSshDeepLinkPreference(options = {}) {
|
|
return applyInitialDeepLinkPreference({
|
|
...options,
|
|
logLabel: "ssh://",
|
|
warnOnFailure: true,
|
|
});
|
|
}
|
|
|
|
function applyInitialJmsDeepLinkPreference(options = {}) {
|
|
return applyInitialDeepLinkPreference({
|
|
...options,
|
|
logLabel: "jms://",
|
|
warnOnFailure: false,
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
SSH_DEEP_LINK_CHANNEL,
|
|
TELNET_DEEP_LINK_CHANNEL,
|
|
JMS_DEEP_LINK_CHANNEL,
|
|
applyInitialJmsDeepLinkPreference,
|
|
applyInitialSshDeepLinkPreference,
|
|
applyJmsProtocolClientPreference,
|
|
applySshProtocolClientPreference,
|
|
getSshDeepLinkRendererReadyTimeoutMs,
|
|
collectJmsDeepLinkUrls,
|
|
collectPuttyStyleDeepLinkUrls,
|
|
collectSshDeepLinkQueueItems,
|
|
collectSshDeepLinkUrls,
|
|
collectTelnetDeepLinkUrls,
|
|
redactPuttyCommandLinePasswords,
|
|
isJmsDeepLinkUrl,
|
|
isSshDeepLinkUrl,
|
|
isTelnetDeepLinkUrl,
|
|
readJmsDeepLinkEnabledPreference,
|
|
readSshDeepLinkEnabledPreference,
|
|
registerJmsProtocolClient,
|
|
registerSshProtocolClient,
|
|
registerTelnetProtocolClient,
|
|
removeJmsProtocolClient,
|
|
removeSshProtocolClient,
|
|
removeTelnetProtocolClient,
|
|
shouldDeliverJmsDeepLink,
|
|
shouldDeliverSshDeepLink,
|
|
shouldDeliverTelnetDeepLink,
|
|
shouldRequeueFailedSshDeepLinkDelivery,
|
|
updateJmsDeepLinkEnabledPreference,
|
|
updateSshDeepLinkEnabledPreference,
|
|
writeJmsDeepLinkEnabledPreference,
|
|
writeSshDeepLinkEnabledPreference,
|
|
};
|